From a0df5e619def337b422c33e3c013bb0ec952cd05 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 18:40:01 +0100 Subject: [PATCH 01/14] feat: Support transient identities and traits - Support transient identities and traits - Bump requests - Bump mypy --- .pre-commit-config.yaml | 2 +- flagsmith/flagsmith.py | 44 ++++++++++++--------- flagsmith/types.py | 14 +++++++ flagsmith/utils/identities.py | 39 +++++++++++-------- poetry.lock | 71 +++++++++++++++++----------------- pyproject.toml | 8 ++-- tests/test_flagsmith.py | 72 +++++++++++++++++++++++++++++++++++ 7 files changed, 175 insertions(+), 75 deletions(-) create mode 100644 flagsmith/types.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d56e9ab..fb94344 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 + rev: v1.10.1 hooks: - id: mypy args: [--strict] diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 0245b7a..1e11c59 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -19,23 +19,14 @@ from flagsmith.offline_handlers import BaseOfflineHandler from flagsmith.polling_manager import EnvironmentDataPollingManager from flagsmith.streaming_manager import EventStreamManager, StreamEvent -from flagsmith.utils.identities import Identity, generate_identities_data +from flagsmith.types import JsonType +from flagsmith.utils.identities import generate_identity_data logger = logging.getLogger(__name__) DEFAULT_API_URL = "https://edge.api.flagsmith.com/api/v1/" DEFAULT_REALTIME_API_URL = "https://realtime.flagsmith.com/" -JsonType = typing.Union[ - None, - int, - str, - bool, - typing.List["JsonType"], - typing.List[typing.Mapping[str, "JsonType"]], - typing.Dict[str, "JsonType"], -] - class Flagsmith: """A Flagsmith client. @@ -238,6 +229,9 @@ def get_identity_flags( self, identifier: str, traits: typing.Optional[typing.Mapping[str, TraitValue]] = None, + *, + transient: bool = False, + transient_traits: typing.Optional[typing.List[str]] = None, ) -> Flags: """ Get all the flags for the current environment for a given identity. Will also @@ -247,13 +241,18 @@ def get_identity_flags( :param identifier: a unique identifier for the identity in the current environment, e.g. email address, username, uuid :param traits: a dictionary of traits to add / update on the identity in - Flagsmith, e.g. {"num_orders": 10} + Flagsmith, e.g. `{"num_orders": 10}` + :param transient: if `True`, the identity won't get persisted + :param transient_traits: a list of trait keys that won't get persisted, + e.g. `["num_orders"]` :return: Flags object holding all the flags for the given identity. """ traits = traits or {} if (self.offline_mode or self.enable_local_evaluation) and self._environment: return self._get_identity_flags_from_document(identifier, traits) - return self._get_identity_flags_from_api(identifier, traits) + return self._get_identity_flags_from_api( + identifier, traits, transient=transient, transient_traits=transient_traits + ) def get_identity_segments( self, @@ -339,13 +338,22 @@ def _get_environment_flags_from_api(self) -> Flags: raise def _get_identity_flags_from_api( - self, identifier: str, traits: typing.Mapping[str, typing.Any] + self, + identifier: str, + traits: typing.Mapping[str, typing.Any], + *, + transient: bool = False, + transient_traits: typing.Optional[typing.List[str]] = None, ) -> Flags: + request_body = generate_identity_data( + identifier, traits, transient=transient, transient_traits=transient_traits + ) try: - data = generate_identities_data(identifier, traits) json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = ( self._get_json_response( - url=self.identities_url, method="POST", body=data + url=self.identities_url, + method="POST", + body=request_body, ) ) return Flags.from_api_flags( @@ -364,9 +372,7 @@ def _get_json_response( self, url: str, method: str, - body: typing.Optional[ - typing.Union[Identity, typing.Dict[str, JsonType]] - ] = None, + body: typing.Optional[JsonType] = None, ) -> typing.Any: try: request_method = getattr(self.session, method.lower()) diff --git a/flagsmith/types.py b/flagsmith/types.py new file mode 100644 index 0000000..2a4b3cc --- /dev/null +++ b/flagsmith/types.py @@ -0,0 +1,14 @@ +import typing + +_JsonScalarType = typing.Union[ + int, + str, + float, + bool, + None, +] +JsonType = typing.Union[ + _JsonScalarType, + typing.Dict[str, "JsonType"], + typing.List["JsonType"], +] diff --git a/flagsmith/utils/identities.py b/flagsmith/utils/identities.py index 082721d..9c46cbd 100644 --- a/flagsmith/utils/identities.py +++ b/flagsmith/utils/identities.py @@ -2,20 +2,29 @@ from flag_engine.identities.traits.types import TraitValue -Identity = typing.TypedDict( - "Identity", - {"identifier": str, "traits": typing.List[typing.Mapping[str, TraitValue]]}, -) +from flagsmith.types import JsonType -def generate_identities_data( - identifier: str, traits: typing.Optional[typing.Mapping[str, TraitValue]] = None -) -> Identity: - return { - "identifier": identifier, - "traits": ( - [{"trait_key": k, "trait_value": v} for k, v in traits.items()] - if traits - else [] - ), - } +def generate_identity_data( + identifier: str, + traits: typing.Optional[typing.Mapping[str, TraitValue]], + *, + transient: bool, + transient_traits: typing.Optional[typing.List[str]], +) -> JsonType: + identity_data: typing.Dict[str, JsonType] = {"identifier": identifier} + if traits: + traits_data: typing.List[JsonType] = [] + transient_trait_keys = set(transient_traits) if transient_traits else set() + for trait_key, trait_value in traits.items(): + trait_data: typing.Dict[str, JsonType] = { + "trait_key": trait_key, + "trait_value": trait_value, + } + if trait_key in transient_trait_keys: + trait_data["transient"] = True + traits_data.append(trait_data) + identity_data["traits"] = traits_data + if transient: + identity_data["transient"] = True + return identity_data diff --git a/poetry.lock b/poetry.lock index 91743dc..9720e49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -408,38 +408,38 @@ files = [ [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -777,7 +777,6 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -814,13 +813,13 @@ files = [ [[package]] name = "requests" -version = "2.32.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, - {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -919,13 +918,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.20240218" +version = "2.32.0.20240712" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20240218.tar.gz", hash = "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"}, - {file = "types_requests-2.31.0.20240218-py3-none-any.whl", hash = "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b"}, + {file = "types-requests-2.32.0.20240712.tar.gz", hash = "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358"}, + {file = "types_requests-2.32.0.20240712-py3-none-any.whl", hash = "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"}, ] [package.dependencies] @@ -982,4 +981,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4" -content-hash = "555fbec9cbe8ecb76b900ee4958ed71b134f15d4c22d4c9db1c73a9c459306a8" +content-hash = "fdd4e06ec39ba0424f2c0d98b1a424f2ff5084768594d4d469cb462b4e08c599" diff --git a/pyproject.toml b/pyproject.toml index 78b225d..ec01d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,8 @@ packages = [{ include = "flagsmith" }] [tool.poetry.dependencies] python = ">=3.8.1,<4" -requests = "^2.27.1" -requests-futures = "^1.0.0" +requests = "^2.32.3" +requests-futures = "^1.0.1" flagsmith-flag-engine = "^5.1.0" sseclient-py = "^1.8.0" @@ -27,8 +27,8 @@ pre-commit = "^2.17.0" responses = "^0.24.1" flake8 = "^6.1.0" isort = "^5.12.0" -mypy = "^1.7.1" -types-requests = "^2.31.0.10" +mypy = "^1.10.1" +types-requests = "^2.32" pytest-cov = "^4.1.0" [tool.mypy] diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 093aa88..56b29aa 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -6,6 +6,7 @@ import pytest import requests import responses +from responses import matchers from flag_engine.environments.models import EnvironmentModel from flag_engine.features.models import FeatureModel, FeatureStateModel from pytest_mock import MockerFixture @@ -172,6 +173,77 @@ def test_get_identity_flags_uses_local_environment_when_available( assert identity_flags[0].value == feature_state.get_value() +@responses.activate() +def test_get_identity_flags__transient_identity__calls_expected( + flagsmith: Flagsmith, + identities_json: str, + environment_model: EnvironmentModel, + mocker: MockerFixture, +) -> None: + # Given + responses.add( + method="POST", + url=flagsmith.identities_url, + body=identities_json, + match=[matchers.json_params_matcher({"transient": True}, strict_match=False)], + ) + flagsmith._environment = environment_model + flagsmith.enable_local_evaluation = True + mock_engine = mocker.patch("flagsmith.flagsmith.engine") + + feature_state = FeatureStateModel( + feature=FeatureModel(id=1, name="some_feature", type="STANDARD"), + enabled=True, + featurestate_uuid=str(uuid.uuid4()), + ) + mock_engine.get_identity_feature_states.return_value = [feature_state] + + # When & Then + flagsmith.get_identity_flags( + "identifier", + traits={"some_trait": "some_value"}, + transient=True, + ).all_flags() + + +@responses.activate() +def test_get_identity_flags__transient_traits__calls_expected( + flagsmith: Flagsmith, + identities_json: str, + environment_model: EnvironmentModel, + mocker: MockerFixture, +) -> None: + # Given + responses.add( + method="POST", + url=flagsmith.identities_url, + body=identities_json, + match=[ + matchers.json_params_matcher( + {"traits": [{"trait_key": "some_trait", "transient": True}]}, + strict_match=False, + ) + ], + ) + flagsmith._environment = environment_model + flagsmith.enable_local_evaluation = True + mock_engine = mocker.patch("flagsmith.flagsmith.engine") + + feature_state = FeatureStateModel( + feature=FeatureModel(id=1, name="some_feature", type="STANDARD"), + enabled=True, + featurestate_uuid=str(uuid.uuid4()), + ) + mock_engine.get_identity_feature_states.return_value = [feature_state] + + # When & Then + flagsmith.get_identity_flags( + "identifier", + traits={"some_trait": "some_value"}, + transient_traits=["some_trait"], + ).all_flags() + + def test_request_connection_error_raises_flagsmith_api_error( mocker: MockerFixture, api_key: str ) -> None: From 908ad80d7b298f49af707345c810b6fdeef95943 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 18:52:35 +0100 Subject: [PATCH 02/14] formatting --- flagsmith/flagsmith.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 1e11c59..cff0312 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -322,9 +322,9 @@ def _get_identity_flags_from_document( def _get_environment_flags_from_api(self) -> Flags: try: - json_response: typing.List[typing.Mapping[str, JsonType]] = ( - self._get_json_response(url=self.environment_flags_url, method="GET") - ) + json_response: typing.List[ + typing.Mapping[str, JsonType] + ] = self._get_json_response(url=self.environment_flags_url, method="GET") return Flags.from_api_flags( api_flags=json_response, analytics_processor=self._analytics_processor, @@ -349,12 +349,12 @@ def _get_identity_flags_from_api( identifier, traits, transient=transient, transient_traits=transient_traits ) try: - json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = ( - self._get_json_response( - url=self.identities_url, - method="POST", - body=request_body, - ) + json_response: typing.Dict[ + str, typing.List[typing.Dict[str, JsonType]] + ] = self._get_json_response( + url=self.identities_url, + method="POST", + body=request_body, ) return Flags.from_api_flags( api_flags=json_response["flags"], From a1517ffdca46a999a9667bacef2e297bfdc20554 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:00:28 +0100 Subject: [PATCH 03/14] formatting? --- .pre-commit-config.yaml | 1 - flagsmith/streaming_manager.py | 2 +- pyproject.toml | 2 ++ 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb94344..edd21f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,6 @@ repos: rev: 24.3.0 hooks: - id: black - language_version: python3 - repo: https://github.com/pycqa/flake8 rev: 6.1.0 hooks: diff --git a/flagsmith/streaming_manager.py b/flagsmith/streaming_manager.py index 3fc6817..4afe87a 100644 --- a/flagsmith/streaming_manager.py +++ b/flagsmith/streaming_manager.py @@ -22,7 +22,7 @@ def __init__( stream_url: str, on_event: Callable[[StreamEvent], None], request_timeout_seconds: Optional[int] = None, - **kwargs: typing.Any + **kwargs: typing.Any, ) -> None: super().__init__(*args, **kwargs) self._stop_event = threading.Event() diff --git a/pyproject.toml b/pyproject.toml index ec01d58..e9b3c32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ pytest-cov = "^4.1.0" plugins = ["pydantic.mypy"] exclude = ["example/*"] +[tool.black] +target-version = ["py38"] [build-system] requires = ["poetry-core>=1.0.0"] From 558d70f7f80eb1d86d79235a1c60ae9d82c3420d Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:07:21 +0100 Subject: [PATCH 04/14] remove linting from GHA --- .github/workflows/pytest.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a70a392..a2a3d1a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -33,14 +33,5 @@ jobs: pip install poetry poetry install --with dev - - name: Check Formatting - run: | - poetry run black --check . - poetry run flake8 . - poetry run isort --check . - - - name: Check Typing - run: poetry run mypy --strict . - - name: Run Tests run: poetry run pytest From 0a5f0e55abffbc8417848ed7b06bf8b6481c541b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:09:17 +0100 Subject: [PATCH 05/14] formatting!!! --- README.md | 4 ++-- flagsmith/flagsmith.py | 18 +++++++++--------- tests/test_flagsmith.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2df3a1b..16ff107 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ For full documentation visit ## Contributing -Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code -of conduct, and the process for submitting pull requests +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull +requests ## Getting Help diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index cff0312..1e11c59 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -322,9 +322,9 @@ def _get_identity_flags_from_document( def _get_environment_flags_from_api(self) -> Flags: try: - json_response: typing.List[ - typing.Mapping[str, JsonType] - ] = self._get_json_response(url=self.environment_flags_url, method="GET") + json_response: typing.List[typing.Mapping[str, JsonType]] = ( + self._get_json_response(url=self.environment_flags_url, method="GET") + ) return Flags.from_api_flags( api_flags=json_response, analytics_processor=self._analytics_processor, @@ -349,12 +349,12 @@ def _get_identity_flags_from_api( identifier, traits, transient=transient, transient_traits=transient_traits ) try: - json_response: typing.Dict[ - str, typing.List[typing.Dict[str, JsonType]] - ] = self._get_json_response( - url=self.identities_url, - method="POST", - body=request_body, + json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = ( + self._get_json_response( + url=self.identities_url, + method="POST", + body=request_body, + ) ) return Flags.from_api_flags( api_flags=json_response["flags"], diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 56b29aa..4441917 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -6,10 +6,10 @@ import pytest import requests import responses -from responses import matchers from flag_engine.environments.models import EnvironmentModel from flag_engine.features.models import FeatureModel, FeatureStateModel from pytest_mock import MockerFixture +from responses import matchers from flagsmith import Flagsmith from flagsmith.exceptions import ( From f38df63fafd5e7518a9162c5f709b765ad13c85b Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:09:36 +0100 Subject: [PATCH 06/14] naming --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a2a3d1a..eb4d200 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -9,7 +9,7 @@ on: jobs: test: runs-on: ubuntu-latest - name: Linting and Tests + name: Run Tests strategy: max-parallel: 4 From 35e456dbf025cc2a1cbcd460ac62b50b513ef680 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:11:30 +0100 Subject: [PATCH 07/14] fix --- flagsmith/utils/identities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flagsmith/utils/identities.py b/flagsmith/utils/identities.py index 9c46cbd..c6dd6be 100644 --- a/flagsmith/utils/identities.py +++ b/flagsmith/utils/identities.py @@ -12,7 +12,7 @@ def generate_identity_data( transient: bool, transient_traits: typing.Optional[typing.List[str]], ) -> JsonType: - identity_data: typing.Dict[str, JsonType] = {"identifier": identifier} + identity_data: typing.Dict[str, JsonType] = {"identifier": identifier, "traits": []} if traits: traits_data: typing.List[JsonType] = [] transient_trait_keys = set(transient_traits) if transient_traits else set() From a8126ace7792a7c0f3ed7cb2272eb29afb7a6c24 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:13:05 +0100 Subject: [PATCH 08/14] naming --- .github/workflows/pytest.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index eb4d200..f363b77 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,4 +1,4 @@ -name: Linting and Tests +name: Run Tests on: pull_request: @@ -9,7 +9,6 @@ on: jobs: test: runs-on: ubuntu-latest - name: Run Tests strategy: max-parallel: 4 From 69d5769abd681c3b62b7af11dba74519ea56a5f8 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 19:22:48 +0100 Subject: [PATCH 09/14] fix tests --- tests/test_flagsmith.py | 50 +++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 4441917..aff00ae 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -177,33 +177,31 @@ def test_get_identity_flags_uses_local_environment_when_available( def test_get_identity_flags__transient_identity__calls_expected( flagsmith: Flagsmith, identities_json: str, - environment_model: EnvironmentModel, - mocker: MockerFixture, ) -> None: # Given responses.add( method="POST", url=flagsmith.identities_url, body=identities_json, - match=[matchers.json_params_matcher({"transient": True}, strict_match=False)], - ) - flagsmith._environment = environment_model - flagsmith.enable_local_evaluation = True - mock_engine = mocker.patch("flagsmith.flagsmith.engine") - - feature_state = FeatureStateModel( - feature=FeatureModel(id=1, name="some_feature", type="STANDARD"), - enabled=True, - featurestate_uuid=str(uuid.uuid4()), + match=[ + matchers.json_params_matcher( + { + "identifier": "identifier", + "traits": [ + {"trait_key": "some_trait", "trait_value": "some_value"} + ], + "transient": True, + } + ) + ], ) - mock_engine.get_identity_feature_states.return_value = [feature_state] # When & Then flagsmith.get_identity_flags( "identifier", traits={"some_trait": "some_value"}, transient=True, - ).all_flags() + ) @responses.activate() @@ -220,28 +218,26 @@ def test_get_identity_flags__transient_traits__calls_expected( body=identities_json, match=[ matchers.json_params_matcher( - {"traits": [{"trait_key": "some_trait", "transient": True}]}, - strict_match=False, + { + "identifier": "identifier", + "traits": [ + { + "trait_key": "some_trait", + "trait_value": "some_value", + "transient": True, + } + ], + }, ) ], ) - flagsmith._environment = environment_model - flagsmith.enable_local_evaluation = True - mock_engine = mocker.patch("flagsmith.flagsmith.engine") - - feature_state = FeatureStateModel( - feature=FeatureModel(id=1, name="some_feature", type="STANDARD"), - enabled=True, - featurestate_uuid=str(uuid.uuid4()), - ) - mock_engine.get_identity_feature_states.return_value = [feature_state] # When & Then flagsmith.get_identity_flags( "identifier", traits={"some_trait": "some_value"}, transient_traits=["some_trait"], - ).all_flags() + ) def test_request_connection_error_raises_flagsmith_api_error( From b0e85d8f84c1b8c5ab6c6bee51017b59402dda9e Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 20:22:51 +0100 Subject: [PATCH 10/14] transient_traits -> transient_trait_keys --- flagsmith/flagsmith.py | 16 +++++++++++----- flagsmith/utils/identities.py | 8 +++++--- tests/test_flagsmith.py | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 1e11c59..2015e71 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -231,7 +231,7 @@ def get_identity_flags( traits: typing.Optional[typing.Mapping[str, TraitValue]] = None, *, transient: bool = False, - transient_traits: typing.Optional[typing.List[str]] = None, + transient_trait_keys: typing.Optional[typing.List[str]] = None, ) -> Flags: """ Get all the flags for the current environment for a given identity. Will also @@ -243,7 +243,7 @@ def get_identity_flags( :param traits: a dictionary of traits to add / update on the identity in Flagsmith, e.g. `{"num_orders": 10}` :param transient: if `True`, the identity won't get persisted - :param transient_traits: a list of trait keys that won't get persisted, + :param transient_trait_keys: a list of trait keys that won't get persisted, e.g. `["num_orders"]` :return: Flags object holding all the flags for the given identity. """ @@ -251,7 +251,10 @@ def get_identity_flags( if (self.offline_mode or self.enable_local_evaluation) and self._environment: return self._get_identity_flags_from_document(identifier, traits) return self._get_identity_flags_from_api( - identifier, traits, transient=transient, transient_traits=transient_traits + identifier, + traits, + transient=transient, + transient_trait_keys=transient_trait_keys, ) def get_identity_segments( @@ -343,10 +346,13 @@ def _get_identity_flags_from_api( traits: typing.Mapping[str, typing.Any], *, transient: bool = False, - transient_traits: typing.Optional[typing.List[str]] = None, + transient_trait_keys: typing.Optional[typing.List[str]] = None, ) -> Flags: request_body = generate_identity_data( - identifier, traits, transient=transient, transient_traits=transient_traits + identifier, + traits, + transient=transient, + transient_trait_keys=transient_trait_keys, ) try: json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = ( diff --git a/flagsmith/utils/identities.py b/flagsmith/utils/identities.py index c6dd6be..86bf5d1 100644 --- a/flagsmith/utils/identities.py +++ b/flagsmith/utils/identities.py @@ -10,18 +10,20 @@ def generate_identity_data( traits: typing.Optional[typing.Mapping[str, TraitValue]], *, transient: bool, - transient_traits: typing.Optional[typing.List[str]], + transient_trait_keys: typing.Optional[typing.List[str]], ) -> JsonType: identity_data: typing.Dict[str, JsonType] = {"identifier": identifier, "traits": []} if traits: traits_data: typing.List[JsonType] = [] - transient_trait_keys = set(transient_traits) if transient_traits else set() + transient_trait_keys_set = ( + set(transient_trait_keys) if transient_trait_keys else set() + ) for trait_key, trait_value in traits.items(): trait_data: typing.Dict[str, JsonType] = { "trait_key": trait_key, "trait_value": trait_value, } - if trait_key in transient_trait_keys: + if trait_key in transient_trait_keys_set: trait_data["transient"] = True traits_data.append(trait_data) identity_data["traits"] = traits_data diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index aff00ae..5271321 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -205,7 +205,7 @@ def test_get_identity_flags__transient_identity__calls_expected( @responses.activate() -def test_get_identity_flags__transient_traits__calls_expected( +def test_get_identity_flags__transient_trait_keys__calls_expected( flagsmith: Flagsmith, identities_json: str, environment_model: EnvironmentModel, @@ -236,7 +236,7 @@ def test_get_identity_flags__transient_traits__calls_expected( flagsmith.get_identity_flags( "identifier", traits={"some_trait": "some_value"}, - transient_traits=["some_trait"], + transient_trait_keys=["some_trait"], ) From c1027d34426dd80e378536709aabaf34fbfa8afa Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Thu, 18 Jul 2024 21:09:02 +0100 Subject: [PATCH 11/14] remove redundant dev dependencies --- poetry.lock | 194 +------------------------------------------------ pyproject.toml | 6 +- 2 files changed, 2 insertions(+), 198 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9720e49..4c0bf5e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,52 +14,6 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} -[[package]] -name = "black" -version = "24.3.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2024.7.4" @@ -181,20 +135,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -329,22 +269,6 @@ pydantic = ">=2.3.0,<3" pydantic-collections = ">=0.5.1,<1" semver = ">=3.0.1" -[[package]] -name = "flake8" -version = "6.1.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-6.1.0-py2.py3-none-any.whl", hash = "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"}, - {file = "flake8-6.1.0.tar.gz", hash = "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.1.0,<3.2.0" - [[package]] name = "identify" version = "2.5.35" @@ -381,89 +305,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy" -version = "1.10.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "nodeenv" version = "1.8.0" @@ -489,17 +330,6 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.2.0" @@ -548,17 +378,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - [[package]] name = "pydantic" version = "2.6.3" @@ -684,17 +503,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pyflakes" -version = "3.1.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.1.0-py2.py3-none-any.whl", hash = "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774"}, - {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, -] - [[package]] name = "pytest" version = "7.4.4" @@ -981,4 +789,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4" -content-hash = "fdd4e06ec39ba0424f2c0d98b1a424f2ff5084768594d4d469cb462b4e08c599" +content-hash = "0e303bca656a71e8f3ca841709c427478812997c7adff1a61230313162842348" diff --git a/pyproject.toml b/pyproject.toml index e9b3c32..7c145fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,15 +21,11 @@ optional = true [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" +pytest-cov = "^4.1.0" pytest-mock = "^3.6.1" -black = ">=23.3,<25.0" pre-commit = "^2.17.0" responses = "^0.24.1" -flake8 = "^6.1.0" -isort = "^5.12.0" -mypy = "^1.10.1" types-requests = "^2.32" -pytest-cov = "^4.1.0" [tool.mypy] plugins = ["pydantic.mypy"] From d55fa913fb951a07e704301b091295797b773c45 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 19 Jul 2024 10:31:31 +0100 Subject: [PATCH 12/14] improve API, typing --- flagsmith/flagsmith.py | 25 ++++++++++++------------- flagsmith/types.py | 16 ++++++++++++++-- flagsmith/utils/identities.py | 32 +++++++++++++------------------- tests/test_flagsmith.py | 3 +-- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index 2015e71..a442405 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -19,7 +19,7 @@ from flagsmith.offline_handlers import BaseOfflineHandler from flagsmith.polling_manager import EnvironmentDataPollingManager from flagsmith.streaming_manager import EventStreamManager, StreamEvent -from flagsmith.types import JsonType +from flagsmith.types import JsonType, TraitConfig, UserTraits from flagsmith.utils.identities import generate_identity_data logger = logging.getLogger(__name__) @@ -228,10 +228,9 @@ def get_environment_flags(self) -> Flags: def get_identity_flags( self, identifier: str, - traits: typing.Optional[typing.Mapping[str, TraitValue]] = None, + traits: typing.Optional[UserTraits] = None, *, transient: bool = False, - transient_trait_keys: typing.Optional[typing.List[str]] = None, ) -> Flags: """ Get all the flags for the current environment for a given identity. Will also @@ -241,10 +240,10 @@ def get_identity_flags( :param identifier: a unique identifier for the identity in the current environment, e.g. email address, username, uuid :param traits: a dictionary of traits to add / update on the identity in - Flagsmith, e.g. `{"num_orders": 10}` + Flagsmith, e.g. `{"num_orders": 10}`. Envelope traits you don't want persisted + in a dictionary with `"transient"` and `"value"` keys, e.g. + `{"num_orders": 10, "color": {"value": "pink", "transient": True}}`. :param transient: if `True`, the identity won't get persisted - :param transient_trait_keys: a list of trait keys that won't get persisted, - e.g. `["num_orders"]` :return: Flags object holding all the flags for the given identity. """ traits = traits or {} @@ -254,7 +253,6 @@ def get_identity_flags( identifier, traits, transient=transient, - transient_trait_keys=transient_trait_keys, ) def get_identity_segments( @@ -308,7 +306,7 @@ def _get_environment_flags_from_document(self) -> Flags: ) def _get_identity_flags_from_document( - self, identifier: str, traits: typing.Mapping[str, TraitValue] + self, identifier: str, traits: UserTraits ) -> Flags: identity_model = self._get_identity_model(identifier, **traits) if self._environment is None: @@ -343,16 +341,14 @@ def _get_environment_flags_from_api(self) -> Flags: def _get_identity_flags_from_api( self, identifier: str, - traits: typing.Mapping[str, typing.Any], + traits: UserTraits, *, transient: bool = False, - transient_trait_keys: typing.Optional[typing.List[str]] = None, ) -> Flags: request_body = generate_identity_data( identifier, traits, transient=transient, - transient_trait_keys=transient_trait_keys, ) try: json_response: typing.Dict[str, typing.List[typing.Dict[str, JsonType]]] = ( @@ -399,7 +395,7 @@ def _get_json_response( def _get_identity_model( self, identifier: str, - **traits: TraitValue, + **traits: typing.Union[TraitValue, TraitConfig], ) -> IdentityModel: if not self._environment: raise FlagsmithClientError( @@ -407,7 +403,10 @@ def _get_identity_model( ) trait_models = [ - TraitModel(trait_key=key, trait_value=value) + TraitModel( + trait_key=key, + trait_value=value["value"] if isinstance(value, dict) else value, + ) for key, value in traits.items() ] diff --git a/flagsmith/types.py b/flagsmith/types.py index 2a4b3cc..a40358a 100644 --- a/flagsmith/types.py +++ b/flagsmith/types.py @@ -1,14 +1,26 @@ import typing -_JsonScalarType = typing.Union[ +from flag_engine.identities.traits.types import TraitValue + +_JsonScalarType: typing.TypeAlias = typing.Union[ int, str, float, bool, None, ] -JsonType = typing.Union[ +JsonType: typing.TypeAlias = typing.Union[ _JsonScalarType, typing.Dict[str, "JsonType"], typing.List["JsonType"], ] + + +class TraitConfig(typing.TypedDict): + value: TraitValue + transient: bool + + +UserTraits: typing.TypeAlias = typing.Mapping[ + str, typing.Union[TraitValue, TraitConfig] +] diff --git a/flagsmith/utils/identities.py b/flagsmith/utils/identities.py index 86bf5d1..891ec47 100644 --- a/flagsmith/utils/identities.py +++ b/flagsmith/utils/identities.py @@ -1,32 +1,26 @@ import typing -from flag_engine.identities.traits.types import TraitValue - -from flagsmith.types import JsonType +from flagsmith.types import JsonType, UserTraits def generate_identity_data( identifier: str, - traits: typing.Optional[typing.Mapping[str, TraitValue]], + traits: UserTraits, *, transient: bool, - transient_trait_keys: typing.Optional[typing.List[str]], ) -> JsonType: - identity_data: typing.Dict[str, JsonType] = {"identifier": identifier, "traits": []} - if traits: - traits_data: typing.List[JsonType] = [] - transient_trait_keys_set = ( - set(transient_trait_keys) if transient_trait_keys else set() - ) - for trait_key, trait_value in traits.items(): - trait_data: typing.Dict[str, JsonType] = { - "trait_key": trait_key, - "trait_value": trait_value, - } - if trait_key in transient_trait_keys_set: + identity_data: typing.Dict[str, JsonType] = {"identifier": identifier} + traits_data: typing.List[JsonType] = [] + for trait_key, trait_value in traits.items(): + trait_data: typing.Dict[str, JsonType] = {"trait_key": trait_key} + if isinstance(trait_value, dict): + trait_data["trait_value"] = trait_value["value"] + if trait_value.get("transient"): trait_data["transient"] = True - traits_data.append(trait_data) - identity_data["traits"] = traits_data + else: + trait_data["trait_value"] = trait_value + traits_data.append(trait_data) + identity_data["traits"] = traits_data if transient: identity_data["transient"] = True return identity_data diff --git a/tests/test_flagsmith.py b/tests/test_flagsmith.py index 5271321..3dde406 100644 --- a/tests/test_flagsmith.py +++ b/tests/test_flagsmith.py @@ -235,8 +235,7 @@ def test_get_identity_flags__transient_trait_keys__calls_expected( # When & Then flagsmith.get_identity_flags( "identifier", - traits={"some_trait": "some_value"}, - transient_trait_keys=["some_trait"], + traits={"some_trait": {"value": "some_value", "transient": True}}, ) From 0462c217acbb8fc21c61f655d69a5ff6d45f0308 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 19 Jul 2024 10:35:58 +0100 Subject: [PATCH 13/14] fix typing for older versions --- flagsmith/types.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flagsmith/types.py b/flagsmith/types.py index a40358a..15952c7 100644 --- a/flagsmith/types.py +++ b/flagsmith/types.py @@ -1,15 +1,16 @@ import typing from flag_engine.identities.traits.types import TraitValue +from typing_extensions import TypeAlias -_JsonScalarType: typing.TypeAlias = typing.Union[ +_JsonScalarType: TypeAlias = typing.Union[ int, str, float, bool, None, ] -JsonType: typing.TypeAlias = typing.Union[ +JsonType: TypeAlias = typing.Union[ _JsonScalarType, typing.Dict[str, "JsonType"], typing.List["JsonType"], @@ -21,6 +22,4 @@ class TraitConfig(typing.TypedDict): transient: bool -UserTraits: typing.TypeAlias = typing.Mapping[ - str, typing.Union[TraitValue, TraitConfig] -] +UserTraits: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]] From bfdbb422662207d000b3fd87b5bc3408c5eb3453 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 19 Jul 2024 11:37:11 +0100 Subject: [PATCH 14/14] improve naming --- flagsmith/flagsmith.py | 8 ++++---- flagsmith/types.py | 2 +- flagsmith/utils/identities.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/flagsmith/flagsmith.py b/flagsmith/flagsmith.py index a442405..8ab9205 100644 --- a/flagsmith/flagsmith.py +++ b/flagsmith/flagsmith.py @@ -19,7 +19,7 @@ from flagsmith.offline_handlers import BaseOfflineHandler from flagsmith.polling_manager import EnvironmentDataPollingManager from flagsmith.streaming_manager import EventStreamManager, StreamEvent -from flagsmith.types import JsonType, TraitConfig, UserTraits +from flagsmith.types import JsonType, TraitConfig, TraitMapping from flagsmith.utils.identities import generate_identity_data logger = logging.getLogger(__name__) @@ -228,7 +228,7 @@ def get_environment_flags(self) -> Flags: def get_identity_flags( self, identifier: str, - traits: typing.Optional[UserTraits] = None, + traits: typing.Optional[TraitMapping] = None, *, transient: bool = False, ) -> Flags: @@ -306,7 +306,7 @@ def _get_environment_flags_from_document(self) -> Flags: ) def _get_identity_flags_from_document( - self, identifier: str, traits: UserTraits + self, identifier: str, traits: TraitMapping ) -> Flags: identity_model = self._get_identity_model(identifier, **traits) if self._environment is None: @@ -341,7 +341,7 @@ def _get_environment_flags_from_api(self) -> Flags: def _get_identity_flags_from_api( self, identifier: str, - traits: UserTraits, + traits: TraitMapping, *, transient: bool = False, ) -> Flags: diff --git a/flagsmith/types.py b/flagsmith/types.py index 15952c7..b2a41a3 100644 --- a/flagsmith/types.py +++ b/flagsmith/types.py @@ -22,4 +22,4 @@ class TraitConfig(typing.TypedDict): transient: bool -UserTraits: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]] +TraitMapping: TypeAlias = typing.Mapping[str, typing.Union[TraitValue, TraitConfig]] diff --git a/flagsmith/utils/identities.py b/flagsmith/utils/identities.py index 891ec47..a1a419e 100644 --- a/flagsmith/utils/identities.py +++ b/flagsmith/utils/identities.py @@ -1,11 +1,11 @@ import typing -from flagsmith.types import JsonType, UserTraits +from flagsmith.types import JsonType, TraitMapping def generate_identity_data( identifier: str, - traits: UserTraits, + traits: TraitMapping, *, transient: bool, ) -> JsonType: