envflags 0.1.0__tar.gz → 0.2.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envflags
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for envflags
5
5
  Author: envflags
6
6
  License: MIT
@@ -21,13 +21,16 @@ flags = create_client("envflags_server_...").initialize()
21
21
  try:
22
22
  enabled = flags.bool_variation(
23
23
  "new-checkout",
24
- {"key": "user-123", "country": "BE"},
25
24
  False,
25
+ {"key": "user-123", "country": "BE"},
26
26
  )
27
27
  finally:
28
28
  flags.close()
29
29
  ```
30
30
 
31
+ Context is optional. Omit it for flags that do not depend on who is asking; pass
32
+ it last when targeting needs a user.
33
+
31
34
  The client polls every 30 seconds and fetches a payload only when its version
32
35
  changes. Set `poll_interval=None` to refresh manually. Supply a previously
33
36
  persisted payload through `bootstrap` to remain available when initialization
@@ -10,13 +10,16 @@ flags = create_client("envflags_server_...").initialize()
10
10
  try:
11
11
  enabled = flags.bool_variation(
12
12
  "new-checkout",
13
- {"key": "user-123", "country": "BE"},
14
13
  False,
14
+ {"key": "user-123", "country": "BE"},
15
15
  )
16
16
  finally:
17
17
  flags.close()
18
18
  ```
19
19
 
20
+ Context is optional. Omit it for flags that do not depend on who is asking; pass
21
+ it last when targeting needs a user.
22
+
20
23
  The client polls every 30 seconds and fetches a payload only when its version
21
24
  changes. Set `poll_interval=None` to refresh manually. Supply a previously
22
25
  persisted payload through `bootstrap` to remain available when initialization
@@ -121,7 +121,10 @@ class Client:
121
121
  thread.join()
122
122
 
123
123
  def variation(
124
- self, flag_key: str, context: Context, default_value: T
124
+ self,
125
+ flag_key: str,
126
+ default_value: T,
127
+ context: Optional[Context] = None,
125
128
  ) -> Any:
126
129
  with self._condition:
127
130
  payload = self._payload
@@ -130,21 +133,30 @@ class Client:
130
133
  return evaluate(payload, flag_key, context, default_value)
131
134
 
132
135
  def bool_variation(
133
- self, flag_key: str, context: Context, default_value: bool
136
+ self,
137
+ flag_key: str,
138
+ default_value: bool,
139
+ context: Optional[Context] = None,
134
140
  ) -> bool:
135
- value = self.variation(flag_key, context, default_value)
141
+ value = self.variation(flag_key, default_value, context)
136
142
  return value if isinstance(value, bool) else default_value
137
143
 
138
144
  def string_variation(
139
- self, flag_key: str, context: Context, default_value: str
145
+ self,
146
+ flag_key: str,
147
+ default_value: str,
148
+ context: Optional[Context] = None,
140
149
  ) -> str:
141
- value = self.variation(flag_key, context, default_value)
150
+ value = self.variation(flag_key, default_value, context)
142
151
  return value if isinstance(value, str) else default_value
143
152
 
144
153
  def number_variation(
145
- self, flag_key: str, context: Context, default_value: float
154
+ self,
155
+ flag_key: str,
156
+ default_value: float,
157
+ context: Optional[Context] = None,
146
158
  ) -> float:
147
- value = self.variation(flag_key, context, default_value)
159
+ value = self.variation(flag_key, default_value, context)
148
160
  if (
149
161
  isinstance(value, bool)
150
162
  or not isinstance(value, (int, float))
@@ -154,9 +166,12 @@ class Client:
154
166
  return float(value)
155
167
 
156
168
  def json_variation(
157
- self, flag_key: str, context: Context, default_value: T
169
+ self,
170
+ flag_key: str,
171
+ default_value: T,
172
+ context: Optional[Context] = None,
158
173
  ) -> Any:
159
- return self.variation(flag_key, context, default_value)
174
+ return self.variation(flag_key, default_value, context)
160
175
 
161
176
  def _perform_refresh(self) -> bool:
162
177
  with self._condition:
@@ -25,22 +25,23 @@ def bucket(flag_key: str, salt: str, bucket_by: str) -> float:
25
25
  def evaluate(
26
26
  payload: Payload,
27
27
  flag_key: str,
28
- context: Context,
28
+ context: Optional[Context],
29
29
  default_value: Any,
30
30
  ) -> Any:
31
31
  """Evaluate one flag locally, falling back when no variation is valid."""
32
32
  flags = payload.get("flags")
33
33
  flag = flags.get(flag_key) if isinstance(flags, Mapping) else None
34
- key = context.get("key")
35
- if not isinstance(flag, Mapping) or not isinstance(key, str) or not key:
34
+ if not isinstance(flag, Mapping):
36
35
  return default_value
36
+ resolved: Mapping[str, Any] = {} if context is None else context
37
+ key = resolved.get("key")
37
38
 
38
39
  variation: Optional[int] = None
39
40
  if flag.get("on") is not True:
40
41
  variation = _index(flag.get("offVariation"))
41
42
  else:
42
43
  targets = flag.get("targets")
43
- if isinstance(targets, list):
44
+ if isinstance(key, str) and key and isinstance(targets, list):
44
45
  for target in targets:
45
46
  if not isinstance(target, Mapping):
46
47
  continue
@@ -57,10 +58,10 @@ def evaluate(
57
58
  continue
58
59
  clauses = rule.get("clauses")
59
60
  if isinstance(clauses, list) and _clauses_match(
60
- clauses, context, payload, set()
61
+ clauses, resolved, payload, set()
61
62
  ):
62
63
  variation = _select_variation(
63
- rule, flag_key, context, payload
64
+ rule, flag_key, resolved, payload
64
65
  )
65
66
  break
66
67
 
@@ -68,7 +69,7 @@ def evaluate(
68
69
  fallthrough = flag.get("fallthrough")
69
70
  if isinstance(fallthrough, Mapping):
70
71
  variation = _select_variation(
71
- fallthrough, flag_key, context, payload
72
+ fallthrough, flag_key, resolved, payload
72
73
  )
73
74
 
74
75
  variations = flag.get("variations")
@@ -245,11 +246,12 @@ def _segment_matches(
245
246
  ) -> bool:
246
247
  key = context.get("key")
247
248
  excluded = segment.get("excluded")
248
- if isinstance(excluded, list) and key in excluded:
249
- return False
250
249
  included = segment.get("included")
251
- if isinstance(included, list) and key in included:
252
- return True
250
+ if isinstance(key, str) and key:
251
+ if isinstance(excluded, list) and key in excluded:
252
+ return False
253
+ if isinstance(included, list) and key in included:
254
+ return True
253
255
  rules = segment.get("rules")
254
256
  if not isinstance(rules, list):
255
257
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: envflags
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python SDK for envflags
5
5
  Author: envflags
6
6
  License: MIT
@@ -21,13 +21,16 @@ flags = create_client("envflags_server_...").initialize()
21
21
  try:
22
22
  enabled = flags.bool_variation(
23
23
  "new-checkout",
24
- {"key": "user-123", "country": "BE"},
25
24
  False,
25
+ {"key": "user-123", "country": "BE"},
26
26
  )
27
27
  finally:
28
28
  flags.close()
29
29
  ```
30
30
 
31
+ Context is optional. Omit it for flags that do not depend on who is asking; pass
32
+ it last when targeting needs a user.
33
+
31
34
  The client polls every 30 seconds and fetches a payload only when its version
32
35
  changes. Set `poll_interval=None` to refresh manually. Supply a previously
33
36
  persisted payload through `bootstrap` to remain available when initialization
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "envflags"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Python SDK for envflags"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -90,11 +90,12 @@ class ClientTest(unittest.TestCase):
90
90
  self.assertIs(client.initialize(), client)
91
91
  self.assertTrue(client.initialized)
92
92
  self.assertEqual(client.version, 3)
93
- self.assertTrue(client.bool_variation("enabled", {"key": "user"}, False))
93
+ self.assertTrue(client.bool_variation("enabled", False, {"key": "user"}))
94
94
  self.assertEqual(
95
- client.string_variation("greeting", {"key": "user"}, "fallback"),
95
+ client.string_variation("greeting", "fallback", {"key": "user"}),
96
96
  "hello",
97
97
  )
98
+ self.assertTrue(client.bool_variation("enabled", False))
98
99
  self.assertEqual(opener.requests[1].full_url, "https://edge.example.test/sdk/payloads/3")
99
100
 
100
101
  self.assertFalse(client.refresh())
@@ -121,7 +122,7 @@ class ClientTest(unittest.TestCase):
121
122
  )
122
123
  self.assertIs(client.initialize(), client)
123
124
  self.assertEqual(len(errors), 1)
124
- self.assertTrue(client.bool_variation("enabled", {"key": "user"}, False))
125
+ self.assertTrue(client.bool_variation("enabled", False, {"key": "user"}))
125
126
 
126
127
  def test_rejects_offline_initialization_without_payload(self):
127
128
  client = Client(
@@ -132,17 +133,17 @@ class ClientTest(unittest.TestCase):
132
133
  with self.assertRaisesRegex(EnvflagsError, "offline"):
133
134
  client.initialize()
134
135
  self.assertFalse(
135
- client.bool_variation("enabled", {"key": "user"}, False)
136
+ client.bool_variation("enabled", False, {"key": "user"})
136
137
  )
137
138
 
138
139
  def test_returns_typed_defaults(self):
139
140
  client = Client("client_test", bootstrap=payload(), poll_interval=None)
140
141
  context = {"key": "user"}
141
- self.assertFalse(client.bool_variation("greeting", context, False))
142
+ self.assertFalse(client.bool_variation("greeting", False, context))
142
143
  self.assertEqual(
143
- client.string_variation("missing", context, "fallback"), "fallback"
144
+ client.string_variation("missing", "fallback", context), "fallback"
144
145
  )
145
- self.assertEqual(client.number_variation("enabled", context, 42), 42)
146
+ self.assertEqual(client.number_variation("enabled", 42, context), 42)
146
147
 
147
148
  def test_coalesces_concurrent_refreshes(self):
148
149
  started = threading.Event()
File without changes
File without changes