feature_gate 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Deft Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.1
2
+ Name: feature_gate
3
+ Version: 0.0.1
4
+ Summary: An extensible feature flagging library for Python
5
+ Home-page: https://github.com/deftinc/feature_gate
6
+ License: MIT
7
+ Keywords: feature flags,feature gate
8
+ Author: Patrick Wiseman
9
+ Author-email: patrick@deft.services
10
+ Maintainer: Patrick Wiseman
11
+ Maintainer-email: patrick@deft.services
12
+ Requires-Python: >=3.10
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
19
+ Requires-Dist: structlog (>=24.1.0,<25.0.0)
20
+ Project-URL: Repository, https://github.com/deftinc/feature_gate
21
+ Description-Content-Type: text/markdown
22
+
23
+ # FeatureGate
24
+
25
+ Featuregate is a simple feature flag API similar to [Flipper](https://github.com/flippercloud/flipper).
26
+
27
+ ## Configuration
28
+
29
+ ### PosthogAdapter
30
+
31
+ ```bash
32
+ export POSTHOG_API_KEY="someapikey"
33
+ export POSTHOG_PROJECT_ID="someprojectid"
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ from feature_gate.client import Client
40
+ from feature_gate.adapters import PosthogAdapter
41
+ from feature_gate.feature import Feature
42
+
43
+ adapter = PosthogAdapter() # Or MemoryAdapter for testing
44
+ client = Client(adapter)
45
+ feature = Feature("Test flag", "test_flag", "This is a test flag")
46
+
47
+ client.features()
48
+ # => []
49
+
50
+ client.add(feature)
51
+ # => True
52
+
53
+ client.features()
54
+ # => ["test_flag"]
55
+
56
+ client.is_enabled("test_flag")
57
+ # => False
58
+
59
+ client.enable("test_flag")
60
+ # => True
61
+
62
+ client.is_enabled("test_flag")
63
+ # => True
64
+
65
+ client.disable("test_flag")
66
+ # => True
67
+
68
+ client.is_enabled("test_flag")
69
+ # => False
70
+
71
+ client.remove("test_flag")
72
+ # => True
73
+
74
+ client.features()
75
+ # => []
76
+ ```
77
+
78
+ ## Errors
79
+
80
+ ### FeatureNotFound
81
+ If you try to run something that presumes the existence of a feature and it can't find it it'll throw a `FeatureNotFound`.
82
+
83
+ ```python
84
+ from feature_gate.client import FeatureNotFound
85
+
86
+ try:
87
+ client.remove("some_feature_that_definitely_does_not_exist")
88
+ except FeatureNotFound as err:
89
+ # Do what we want to do when the feature doesn't exist
90
+ ```
91
+
92
+ ### PostApiClientError
93
+ For the `PosthogAdapter` in particular it will raise error if it was unable to reach the Posthog API. These get bubbled up as `PosthogAPIClientError`.
94
+
95
+ ```python
96
+ from feature_gate.clients.posthog_api_client import PosthogAPIClientError
97
+
98
+ try:
99
+ client.features() #disable network connection
100
+ except PosthogAPIClientError as err:
101
+ # Handle the error -- define default behavior in outage
102
+ ```
103
+
104
+ ## Testing
105
+
106
+ The Memory Adapter can be used for writing tests. This creates an ephemeral memory only implementation of the feature_gate client API. This is non-suitable for production only for tests.
107
+
108
+ ```python
109
+ from feature_gate.client import Client
110
+ from feature_gate.adapters.memory import MemoryAdapter
111
+
112
+ client = Client(MemoryAdapter())
113
+ ```
114
+
115
+ ## Developing this library
116
+
117
+ ### Hot-reload the REPL
118
+
119
+ Hot-reload the client in the repl for development:
120
+
121
+ ```
122
+ $ repl
123
+ >>> exec(open('reload.py').read())
124
+ ```
125
+
@@ -0,0 +1,102 @@
1
+ # FeatureGate
2
+
3
+ Featuregate is a simple feature flag API similar to [Flipper](https://github.com/flippercloud/flipper).
4
+
5
+ ## Configuration
6
+
7
+ ### PosthogAdapter
8
+
9
+ ```bash
10
+ export POSTHOG_API_KEY="someapikey"
11
+ export POSTHOG_PROJECT_ID="someprojectid"
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from feature_gate.client import Client
18
+ from feature_gate.adapters import PosthogAdapter
19
+ from feature_gate.feature import Feature
20
+
21
+ adapter = PosthogAdapter() # Or MemoryAdapter for testing
22
+ client = Client(adapter)
23
+ feature = Feature("Test flag", "test_flag", "This is a test flag")
24
+
25
+ client.features()
26
+ # => []
27
+
28
+ client.add(feature)
29
+ # => True
30
+
31
+ client.features()
32
+ # => ["test_flag"]
33
+
34
+ client.is_enabled("test_flag")
35
+ # => False
36
+
37
+ client.enable("test_flag")
38
+ # => True
39
+
40
+ client.is_enabled("test_flag")
41
+ # => True
42
+
43
+ client.disable("test_flag")
44
+ # => True
45
+
46
+ client.is_enabled("test_flag")
47
+ # => False
48
+
49
+ client.remove("test_flag")
50
+ # => True
51
+
52
+ client.features()
53
+ # => []
54
+ ```
55
+
56
+ ## Errors
57
+
58
+ ### FeatureNotFound
59
+ If you try to run something that presumes the existence of a feature and it can't find it it'll throw a `FeatureNotFound`.
60
+
61
+ ```python
62
+ from feature_gate.client import FeatureNotFound
63
+
64
+ try:
65
+ client.remove("some_feature_that_definitely_does_not_exist")
66
+ except FeatureNotFound as err:
67
+ # Do what we want to do when the feature doesn't exist
68
+ ```
69
+
70
+ ### PostApiClientError
71
+ For the `PosthogAdapter` in particular it will raise error if it was unable to reach the Posthog API. These get bubbled up as `PosthogAPIClientError`.
72
+
73
+ ```python
74
+ from feature_gate.clients.posthog_api_client import PosthogAPIClientError
75
+
76
+ try:
77
+ client.features() #disable network connection
78
+ except PosthogAPIClientError as err:
79
+ # Handle the error -- define default behavior in outage
80
+ ```
81
+
82
+ ## Testing
83
+
84
+ The Memory Adapter can be used for writing tests. This creates an ephemeral memory only implementation of the feature_gate client API. This is non-suitable for production only for tests.
85
+
86
+ ```python
87
+ from feature_gate.client import Client
88
+ from feature_gate.adapters.memory import MemoryAdapter
89
+
90
+ client = Client(MemoryAdapter())
91
+ ```
92
+
93
+ ## Developing this library
94
+
95
+ ### Hot-reload the REPL
96
+
97
+ Hot-reload the client in the repl for development:
98
+
99
+ ```
100
+ $ repl
101
+ >>> exec(open('reload.py').read())
102
+ ```
File without changes
@@ -0,0 +1,79 @@
1
+ import structlog
2
+ from pathlib import Path
3
+ from feature_gate.client import FeatureNotFound
4
+
5
+ from structlog.contextvars import (
6
+ bind_contextvars,
7
+ merge_contextvars,
8
+ bound_contextvars,
9
+ )
10
+
11
+ class MemoryAdapter:
12
+ def __init__(self):
13
+ self._features = []
14
+ bind_contextvars(klass="MemoryAdapter")
15
+ structlog.configure(
16
+ processors=[
17
+ merge_contextvars,
18
+ structlog.processors.add_log_level,
19
+ structlog.processors.TimeStamper(fmt="iso"),
20
+ structlog.processors.dict_tracebacks,
21
+ structlog.processors.JSONRenderer(),
22
+ ],
23
+ logger_factory=structlog.WriteLoggerFactory(
24
+ file=Path("development").with_suffix(".log").open("wt")
25
+ ),
26
+ )
27
+ self._logger = structlog.get_logger()
28
+
29
+ def add(self, feature):
30
+ self._logger.info("Add feature key={feature.key}")
31
+ if feature.key not in self._features:
32
+ self._features.append({
33
+ "name": feature.name,
34
+ "key": feature.key,
35
+ "description": feature.description,
36
+ "gates": {
37
+ "boolean": {
38
+ "enabled": False
39
+ }
40
+ }
41
+ })
42
+ return True
43
+
44
+ def remove(self, feature_key):
45
+ self._logger.info("Remove feature key={feature.key}")
46
+ for feature in self._features:
47
+ if feature["key"] == feature_key:
48
+ self._features.remove(feature)
49
+ return True
50
+ raise FeatureNotFound("Feature {feature_key} not found.")
51
+
52
+ def features(self):
53
+ features = [feature["key"] for feature in self._features]
54
+ self._logger.info("Lists features {features}")
55
+ return features
56
+
57
+ def is_enabled(self, feature_key):
58
+ for feature in self._features:
59
+ if feature["key"] == feature_key:
60
+ is_enabled = feature["gates"]["boolean"]["enabled"]
61
+ self._logger.info("Check feature key={feature.key} enabled={is_enabled}")
62
+ return is_enabled
63
+ raise FeatureNotFound("Feature {feature_key} not found.")
64
+
65
+ def enable(self, feature_key):
66
+ self._logger.info("Enable feature key={feature.key}")
67
+ for feature in self._features:
68
+ if feature["key"] == feature_key:
69
+ feature["gates"]["boolean"]["enabled"] = True
70
+ return True
71
+ raise FeatureNotFound("Feature {feature_key} not found.")
72
+
73
+ def disable(self, feature_key):
74
+ self._logger.info("Disable feature key={feature.key}")
75
+ for feature in self._features:
76
+ if feature["key"] == feature_key:
77
+ feature["gates"]["boolean"]["enabled"] = False
78
+ return True
79
+ raise FeatureNotFound("Feature {feature_key} not found.")
@@ -0,0 +1,108 @@
1
+ from feature_gate.clients.posthog_api_client import PosthogAPIClient
2
+ class PosthogAdapter:
3
+ def __init__(self, api_key=None, project_id=None):
4
+ self.client = PosthogAPIClient(api_key=api_key, project_id=project_id)
5
+
6
+ def client(self):
7
+ return self.client
8
+
9
+ def add(self, feature):
10
+ if self.client.fetch_feature(feature.key) is None:
11
+ self.client.create_feature(feature.key, feature.description)
12
+ return True
13
+
14
+ def remove(self, feature_key):
15
+ if not self.client.fetch_feature(feature_key) is None:
16
+ self.client.delete_feature(feature_key)
17
+ return True
18
+
19
+ def features(self):
20
+ resp = self.client.list_features()
21
+ key='key'
22
+ return [item[key] for item in resp["data"] if key in item]
23
+
24
+ def is_enabled(self, feature_key):
25
+ return self.client.is_enabled(feature_key)
26
+
27
+ def enable(self, feature_key):
28
+ resp = self.client.enable_feature(feature_key)
29
+ return resp["data"]["active"]
30
+
31
+ def disable(self, feature_key):
32
+ resp = self.client.disable_feature(feature_key)
33
+ return resp["data"]["active"] == False
34
+
35
+ # def enable_expression(self, expression):
36
+ # raise NotImplementedError
37
+
38
+ # def disable_expression(self, expression):
39
+ # raise NotImplementedError
40
+
41
+ # def expression(self):
42
+ # raise NotImplementedError
43
+
44
+ # def add_expression(self, expression):
45
+ # raise NotImplementedError
46
+
47
+ # def remove_expression(self, expression):
48
+ # raise NotImplementedError
49
+
50
+ # def enable_actor(self, feature, actor):
51
+ # raise NotImplementedError
52
+
53
+ # def disable_actor(self, feature, actor):
54
+ # raise NotImplementedError
55
+
56
+ # def enable_group(self, feature, group):
57
+ # raise NotImplementedError
58
+
59
+ # def disable_group(self, feature, group):
60
+ # raise NotImplementedError
61
+
62
+ # def enable_percentage_of_actors(self, feature, percentage):
63
+ # raise NotImplementedError
64
+
65
+ # def disable_percentage_of_actors(self, feature, percentage):
66
+ # raise NotImplementedError
67
+
68
+ # def enable_percentage_of_time(self, feature, percentage):
69
+ # raise NotImplementedError
70
+
71
+ # def disable_percentage_of_time(self, feature, percentage):
72
+ # raise NotImplementedError
73
+
74
+ # def feature(self):
75
+ # raise NotImplementedError
76
+
77
+ # # def [](self):
78
+ # # raise NotImplementedError
79
+
80
+ # def preload(self):
81
+ # raise NotImplementedError
82
+
83
+ # def preload_all(self):
84
+ # raise NotImplementedError
85
+
86
+ # def adapter(self):
87
+ # raise NotImplementedError
88
+
89
+ # def do_import(self):
90
+ # raise NotImplementedError
91
+
92
+ # def do_export(self):
93
+ # raise NotImplementedError
94
+
95
+ # def memoize(self):
96
+ # raise NotImplementedError
97
+
98
+ # def is_memorizing(self):
99
+ # raise NotImplementedError
100
+
101
+ # def is_read_only(self):
102
+ # raise NotImplementedError
103
+
104
+ # def sync(self):
105
+ # raise NotImplementedError
106
+
107
+ # def sync_secret(self):
108
+ # raise NotImplementedError
@@ -0,0 +1,102 @@
1
+ class FeatureNotFound(ValueError):
2
+ pass
3
+
4
+ class Client:
5
+ def __init__(self, adapter):
6
+ self.adapter = adapter
7
+
8
+ def adapter(self):
9
+ return self.adapter
10
+
11
+ def add(self, feature):
12
+ return self.adapter.add(feature)
13
+
14
+ def remove(self, feature):
15
+ return self.adapter.remove(feature)
16
+
17
+ def features(self):
18
+ return self.adapter.features()
19
+
20
+ def is_enabled(self, feature):
21
+ return self.adapter.is_enabled(feature)
22
+
23
+ def enable(self, feature):
24
+ return self.adapter.enable(feature)
25
+
26
+ def disable(self, feature):
27
+ return self.adapter.disable(feature)
28
+
29
+ # def enable_expression(self, expression):
30
+ # raise NotImplementedError
31
+
32
+ # def disable_expression(self, expression):
33
+ # raise NotImplementedError
34
+
35
+ # def expression(self):
36
+ # raise NotImplementedError
37
+
38
+ # def add_expression(self, expression):
39
+ # raise NotImplementedError
40
+
41
+ # def remove_expression(self, expression):
42
+ # raise NotImplementedError
43
+
44
+ # def enable_actor(self, feature, actor):
45
+ # raise NotImplementedError
46
+
47
+ # def disable_actor(self, feature, actor):
48
+ # raise NotImplementedError
49
+
50
+ # def enable_group(self, feature, group):
51
+ # raise NotImplementedError
52
+
53
+ # def disable_group(self, feature, group):
54
+ # raise NotImplementedError
55
+
56
+ # def enable_percentage_of_actors(self, feature, percentage):
57
+ # raise NotImplementedError
58
+
59
+ # def disable_percentage_of_actors(self, feature, percentage):
60
+ # raise NotImplementedError
61
+
62
+ # def enable_percentage_of_time(self, feature, percentage):
63
+ # raise NotImplementedError
64
+
65
+ # def disable_percentage_of_time(self, feature, percentage):
66
+ # raise NotImplementedError
67
+
68
+ # def feature(self):
69
+ # raise NotImplementedError
70
+
71
+ # # def [](self):
72
+ # # raise NotImplementedError
73
+
74
+ # def preload(self):
75
+ # raise NotImplementedError
76
+
77
+ # def preload_all(self):
78
+ # raise NotImplementedError
79
+
80
+ # def adapter(self):
81
+ # raise NotImplementedError
82
+
83
+ # def do_import(self):
84
+ # raise NotImplementedError
85
+
86
+ # def do_export(self):
87
+ # raise NotImplementedError
88
+
89
+ # def memoize(self):
90
+ # raise NotImplementedError
91
+
92
+ # def is_memorizing(self):
93
+ # raise NotImplementedError
94
+
95
+ # def is_read_only(self):
96
+ # raise NotImplementedError
97
+
98
+ # def sync(self):
99
+ # raise NotImplementedError
100
+
101
+ # def sync_secret(self):
102
+ # raise NotImplementedError
@@ -0,0 +1,235 @@
1
+ import json
2
+ import os
3
+ import requests
4
+ import structlog
5
+
6
+ from pathlib import Path
7
+ from feature_gate.client import FeatureNotFound
8
+
9
+ from structlog.contextvars import (
10
+ bind_contextvars,
11
+ merge_contextvars,
12
+ bound_contextvars,
13
+ )
14
+
15
+ class PosthogAPIClientError(Exception):
16
+ pass
17
+
18
+ class PosthogAPIClient:
19
+ def __init__(self, api_base=None, api_key=None, project_id=None):
20
+ if api_base is None:
21
+ self.api_base = os.environ.get("POSTHOG_API_BASE", "https://app.posthog.com")
22
+ else:
23
+ self.api_base = api_base
24
+
25
+ if api_key is None:
26
+ self.api_key = os.environ.get('POSTHOG_API_KEY')
27
+ else:
28
+ self.api_key = api_key
29
+
30
+ if project_id is None:
31
+ self.project_id = os.environ.get('POSTHOG_PROJECT_ID')
32
+ else:
33
+ self.project_id = project_id
34
+
35
+ bind_contextvars(klass="PosthogAPIClient", project_id=project_id)
36
+ structlog.configure(
37
+ processors=[
38
+ merge_contextvars,
39
+ structlog.processors.add_log_level,
40
+ structlog.processors.TimeStamper(fmt="iso"),
41
+ structlog.processors.dict_tracebacks,
42
+ structlog.processors.JSONRenderer(),
43
+ ],
44
+ logger_factory=structlog.WriteLoggerFactory(
45
+ file=Path("development").with_suffix(".log").open("wt")
46
+ ),
47
+ )
48
+ self.logger = structlog.get_logger()
49
+
50
+ def api_base(self):
51
+ return self.api_base
52
+
53
+ def api_key(self):
54
+ return self.api_key
55
+
56
+ def project_id(self):
57
+ return self.project_id
58
+
59
+ def list_features(self):
60
+ path = f'/api/projects/{self.project_id}/feature_flags'
61
+ with bound_contextvars(method="list_features"):
62
+ response = self._get(path)
63
+ return self._map_list_response("GET", path, response)
64
+
65
+ def create_feature(self, name, description, deleted=False, active=False):
66
+ with bound_contextvars(method="create_feature"):
67
+ path = f'/api/projects/{self.project_id}/feature_flags'
68
+ payload = {
69
+ 'name': description,
70
+ 'key': name,
71
+ 'deleted': deleted,
72
+ 'active': active
73
+ }
74
+ response = self._post(path, payload)
75
+ return self._map_single_response("POST", path, response)
76
+
77
+ def fetch_feature(self, key):
78
+ features = self.list_features()["data"]
79
+ for entry in features:
80
+ if "key" in entry and entry["key"] == key:
81
+ return entry
82
+ return None
83
+
84
+ def delete_feature(self, key):
85
+ feature = self.fetch_feature(key)
86
+ if feature == None:
87
+ raise FeatureNotFound(f"Feature {key} not found")
88
+ else:
89
+ path = f'/api/projects/{self.project_id}/feature_flags/{feature["id"]}'
90
+ with bound_contextvars(method="delete_feature"):
91
+ payload = {
92
+ 'deleted': True
93
+ }
94
+ response = self._patch(path, payload)
95
+ return self._map_single_response("PATCH", path, response)
96
+
97
+ def is_enabled(self, key):
98
+ feature = self.fetch_feature(key)
99
+ if feature == None:
100
+ raise FeatureNotFound(f"Feature {key} not found")
101
+ else:
102
+ return feature["active"]
103
+
104
+ def enable_feature(self, key):
105
+ feature = self.fetch_feature(key)
106
+ if feature == None:
107
+ raise FeatureNotFound(f"Feature {key} not found")
108
+ else:
109
+ path = f'/api/projects/{self.project_id}/feature_flags/{feature["id"]}'
110
+ with bound_contextvars(method="enable_feature"):
111
+ payload = {
112
+ 'active': True
113
+ }
114
+ response = self._patch(path, payload)
115
+ return self._map_single_response("PATCH", path, response)
116
+
117
+ def disable_feature(self, key):
118
+ feature = self.fetch_feature(key)
119
+ if feature == None:
120
+ raise FeatureNotFound(f"Feature {key} not found")
121
+ else:
122
+ path = f'/api/projects/{self.project_id}/feature_flags/{feature["id"]}'
123
+ with bound_contextvars(method="disable_feature"):
124
+ payload = {
125
+ 'active': False
126
+ }
127
+ response = self._patch(path, payload)
128
+ return self._map_single_response("PATCH", path, response)
129
+
130
+ def _get(self, path):
131
+ try:
132
+ return self.__get(path)
133
+ except requests.ConnectionError as err:
134
+ self._log_posthog_connection_error(err)
135
+
136
+ def __get(self, path):
137
+ with bound_contextvars(method="get"):
138
+ url = f"{self.api_base}{path}"
139
+ headers = {
140
+ "Content-Type": "application/json",
141
+ "Authorization": f"Bearer {self.api_key}"
142
+ }
143
+ response = requests.get(url, headers=headers)
144
+ return response
145
+
146
+ def _post(self, path, payload):
147
+ try:
148
+ return self.__post(path, payload)
149
+ except requests.ConnectionError as err:
150
+ self._log_posthog_connection_error(err)
151
+
152
+ def __post(self, path, payload):
153
+ url = f"{self.api_base}{path}"
154
+ with bound_contextvars(method="post", url=url):
155
+ json_payload = json.dumps(payload)
156
+ headers = self._get_headers()
157
+ response = requests.post(url, data=json_payload, headers=headers)
158
+ return response
159
+
160
+ def _patch(self, path, payload):
161
+ try:
162
+ return self.__patch(path, payload)
163
+ except requests.ConnectionError as err:
164
+ self._log_posthog_connection_error(err)
165
+
166
+ def __patch(self, path, payload):
167
+ url = f"{self.api_base}{path}"
168
+ with bound_contextvars(method="patch", url=url):
169
+ json_payload = json.dumps(payload)
170
+ headers = self._get_headers()
171
+ response = requests.patch(url, data=json_payload, headers=headers)
172
+ return response
173
+
174
+ def _get_headers(self):
175
+ return {
176
+ "Content-Type": "application/json",
177
+ "Authorization": f"Bearer {self.api_key}"
178
+ }
179
+
180
+ def _check_status_ok(self, code):
181
+ return code == 200 or code == 201
182
+
183
+ def _map_single_response(self, method, path, response):
184
+ ret = None
185
+ if self._check_status_ok(response.status_code):
186
+ data = response.json()
187
+ self.logger.info("request successful", method=method, path=path, status_code=response.status_code, response=data)
188
+ ret = self._map_single_response_success(data)
189
+ else:
190
+ data = response.json()
191
+ self.logger.info("request failed", method=method, path=path, status_code=response.status_code, response=data)
192
+ ret = self._map_error_response(response.status_code, data)
193
+ return ret
194
+
195
+ def _map_list_response(self, method, path, response):
196
+ ret = None
197
+ if self._check_status_ok(response.status_code):
198
+ data = response.json()
199
+ self.logger.info("request successful", method=method, path=path, status_code=response.status_code, response=data)
200
+ ret = self._map_list_response_success(data)
201
+ else:
202
+ data = response.json()
203
+ self.logger.info("request failed", method=method, path=path, status_code=response.status_code, response=data)
204
+ ret = self._map_error_response(response.status_code, data)
205
+ return ret
206
+
207
+ def _map_error_response(self, code, data):
208
+ return {
209
+ "errors": [
210
+ {
211
+ "status": code,
212
+ "detail": data.get("detail"),
213
+ "code": data.get("code"),
214
+ "type": data.get("type")
215
+ }
216
+ ]
217
+ }
218
+
219
+ def _map_single_response_success(self, data):
220
+ return {
221
+ "data": data
222
+ }
223
+
224
+ def _map_list_response_success(self, data):
225
+ return {
226
+ "data": data.get("results"),
227
+ "pagination": {
228
+ "next": data.get("next"),
229
+ "previous": data.get("previous")
230
+ }
231
+ }
232
+
233
+ def _log_posthog_connection_error(self, error):
234
+ self.logger.error(f"Posthog connection error - {error}")
235
+ raise PosthogAPIClientError(f"Posthog connection error - {error}")
@@ -0,0 +1,17 @@
1
+ class Feature:
2
+ def __init__(self, name, key, description):
3
+ self._name = name
4
+ self._key = key
5
+ self._description = description
6
+
7
+ @property
8
+ def name(self):
9
+ return self._name
10
+
11
+ @property
12
+ def key(self):
13
+ return self._key
14
+
15
+ @property
16
+ def description(self):
17
+ return self._description
@@ -0,0 +1,25 @@
1
+
2
+ [tool.poetry]
3
+ name = "feature_gate"
4
+ version = "0.0.1"
5
+ description = "An extensible feature flagging library for Python"
6
+ authors = ["Patrick Wiseman <patrick@deft.services>"]
7
+ maintainers = ["Patrick Wiseman <patrick@deft.services>"]
8
+ license = "MIT"
9
+ readme = "README.md"
10
+ homepage = "https://github.com/deftinc/feature_gate"
11
+ repository = "https://github.com/deftinc/feature_gate"
12
+ keywords = ["feature flags", "feature gate"]
13
+
14
+ [tool.poetry.dependencies]
15
+ python = ">= 3.10"
16
+ structlog = "^24.1.0"
17
+ requests = "^2.31.0"
18
+
19
+ [tool.poetry.group.test.dependencies]
20
+ pytest = "^8.0.0"
21
+ pytest-mock = "^3.12.0"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core"]
25
+ build-backend = "poetry.core.masonry.api"