keycycle 0.3.1__tar.gz → 0.3.2__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.
Files changed (51) hide show
  1. {keycycle-0.3.1 → keycycle-0.3.2}/PKG-INFO +1 -1
  2. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/utils.py +21 -14
  3. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/PKG-INFO +1 -1
  4. {keycycle-0.3.1 → keycycle-0.3.2}/pyproject.toml +1 -1
  5. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_multi_client_wrapper.py +23 -11
  6. {keycycle-0.3.1 → keycycle-0.3.2}/MANIFEST.in +0 -0
  7. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/__init__.py +0 -0
  8. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/__init__.py +0 -0
  9. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/generic_adapter.py +0 -0
  10. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/adapters/openai_adapter.py +0 -0
  11. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/__init__.py +0 -0
  12. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/constants.py +0 -0
  13. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/dataclasses.py +0 -0
  14. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/enums.py +0 -0
  15. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/loader.py +0 -0
  16. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/log_config.py +0 -0
  17. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/cerebras.yaml +0 -0
  18. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/cohere.yaml +0 -0
  19. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/gemini.yaml +0 -0
  20. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/groq.yaml +0 -0
  21. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/mistral.yaml +0 -0
  22. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/moonshot.yaml +0 -0
  23. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/openrouter.yaml +0 -0
  24. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models/openrouter_models.yaml +0 -0
  25. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/config/models.py +0 -0
  26. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/__init__.py +0 -0
  27. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/backoff.py +0 -0
  28. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/core/exceptions.py +0 -0
  29. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/__init__.py +0 -0
  30. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/rotating_mixin.py +0 -0
  31. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/key_rotation/rotation_manager.py +0 -0
  32. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/legacy_multi_provider_wrapper.py +0 -0
  33. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/multi_client_wrapper.py +0 -0
  34. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/py.typed +0 -0
  35. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/__init__.py +0 -0
  36. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/db_logic.py +0 -0
  37. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/usage/usage_logger.py +0 -0
  38. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle/utils.py +0 -0
  39. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/SOURCES.txt +0 -0
  40. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/dependency_links.txt +0 -0
  41. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/requires.txt +0 -0
  42. {keycycle-0.3.1 → keycycle-0.3.2}/keycycle.egg-info/top_level.txt +0 -0
  43. {keycycle-0.3.1 → keycycle-0.3.2}/setup.cfg +0 -0
  44. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_api_key_pinning.py +0 -0
  45. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_api_key_retrieval.py +0 -0
  46. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_cerebras_direct.py +0 -0
  47. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_custom_key_limits.py +0 -0
  48. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_payment_required_rotation.py +0 -0
  49. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_rate_limit_logic.py +0 -0
  50. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_specific_key_retrieval.py +0 -0
  51. {keycycle-0.3.1 → keycycle-0.3.2}/tests/test_streams_recording.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keycycle
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A thread-safe key rotation and rate limiting manager for API keys.
5
5
  Author-email: William Jin <jinwilliam.jin@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -323,16 +323,14 @@ def load_api_keys(
323
323
  except ValueError:
324
324
  raise ValueError(f"'{num_keys_var}' must be an integer, got: {num_keys_raw}")
325
325
 
326
- # A key fleet isn't always numbered contiguously from 1..N -- a retired
327
- # or never-issued key can leave a gap (e.g. a provider's *_API_KEY_25
328
- # through *_API_KEY_36 missing while 1..24 and 37..46 are live, because
329
- # NUM_{PROVIDER} was set once and never reconciled after keys were added
330
- # or dropped). Scanning `range(1, expected_count + 1)` either crashes on
331
- # the first missing index or silently caps the fleet at expected_count,
332
- # even when more keys exist above that number. Instead, scan every
333
- # {PROVIDER}_API_KEY_<index> variable that actually exists, in whatever
334
- # order the indices fall, and use all of them -- NUM_{PROVIDER} becomes
335
- # an expectation to warn against, not a hard loop bound.
326
+ # NUM_{PROVIDER} is AUTHORITATIVE (0.3.2, reverting 0.3.1's scan-all):
327
+ # the fleet convention is that the first NUM indices (sorted ascending)
328
+ # are the sanctioned live keys, and keys parked at higher indices beyond
329
+ # a numbering gap are dead or spare on purpose -- loading them would put
330
+ # known-bad keys back into rotation. Gaps INSIDE the sanctioned range
331
+ # are tolerated (a retired key's index can be missing); the loader takes
332
+ # the first NUM indices that actually exist and warns if it comes up
333
+ # short, but never loads past NUM keys.
336
334
  key_pattern = re.compile(rf"^{re.escape(provider_upper)}_API_KEY_(\d+)$")
337
335
  found: Dict[int, str] = {}
338
336
  for env_name, value in os.environ.items():
@@ -343,11 +341,20 @@ def load_api_keys(
343
341
  if not found:
344
342
  raise ValueError(f"Missing API key: {provider_upper}_API_KEY_1")
345
343
 
346
- indices = sorted(found)
347
- if len(indices) != expected_count:
344
+ all_indices = sorted(found)
345
+ indices = all_indices[:expected_count]
346
+ excluded = all_indices[expected_count:]
347
+ if excluded:
348
+ logging.getLogger(__name__).info(
349
+ "%s: %s=%d -- using the first %d key indices %s; excluding %d "
350
+ "parked key(s) at indices %s (dead/spare by convention).",
351
+ provider_upper, num_keys_var, expected_count, len(indices), indices,
352
+ len(excluded), excluded,
353
+ )
354
+ if len(indices) < expected_count:
348
355
  logging.getLogger(__name__).warning(
349
- "%s: found %d API key(s) (indices %s) but %s=%d -- numbering has "
350
- "a gap or is stale. Using all %d key(s) actually found.",
356
+ "%s: only %d API key(s) found (indices %s) but %s=%d -- the "
357
+ "declared count is stale. Using the %d key(s) actually found.",
351
358
  provider_upper, len(indices), indices, num_keys_var, expected_count,
352
359
  len(indices),
353
360
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keycycle
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A thread-safe key rotation and rate limiting manager for API keys.
5
5
  Author-email: William Jin <jinwilliam.jin@gmail.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "keycycle"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  authors = [
9
9
  { name="William Jin", email="jinwilliam.jin@gmail.com" },
10
10
  ]
@@ -331,25 +331,37 @@ class TestLoadApiKeysStatic(unittest.TestCase):
331
331
  self.assertIn("Missing API key", str(ctx.exception))
332
332
 
333
333
  @patch.dict(os.environ, {
334
- # A real-world fleet whose NUM_ counter went stale after keys were
335
- # added/retired: 24 keys, then a gap, then 10 more (mirrors William's
336
- # GROQ fleet: 1..24 live, 25..36 retired, 37..46 re-issued).
334
+ # The fleet convention (0.3.2): the first NUM indices are the
335
+ # sanctioned live keys; keys parked at higher indices beyond a gap
336
+ # are dead or spare ON PURPOSE (mirrors William's GROQ fleet:
337
+ # 1..24 live, 37..46 parked dead). NUM is authoritative.
337
338
  "NUM_GAPPY": "24",
338
339
  **{f"GAPPY_API_KEY_{i}": f"key-{i}" for i in range(1, 25)},
339
340
  **{f"GAPPY_API_KEY_{i}": f"key-{i}" for i in range(37, 47)},
340
341
  })
341
342
  @patch('keycycle.multi_client_wrapper.load_dotenv')
342
- def test_load_keys_tolerates_numbering_gap(self, mock_dotenv):
343
- """A gap in the index sequence should not crash or truncate the fleet."""
343
+ def test_load_keys_num_caps_out_parked_keys(self, mock_dotenv):
344
+ """Only the first NUM sorted indices load; parked keys stay out."""
344
345
  keys = MultiClientWrapper.load_api_keys("gappy")
345
346
 
346
- # All 34 keys are found (both sides of the gap), even though
347
- # NUM_GAPPY only claims 24 -- the env var is a hint, not a cap.
348
- self.assertEqual(len(keys), 34)
347
+ self.assertEqual(len(keys), 24)
349
348
  self.assertEqual(keys[0], "key-1")
350
- self.assertEqual(keys[23], "key-24")
351
- self.assertEqual(keys[24], "key-37")
352
- self.assertEqual(keys[-1], "key-46")
349
+ self.assertEqual(keys[-1], "key-24")
350
+
351
+ @patch.dict(os.environ, {
352
+ # A gap INSIDE the sanctioned range is tolerated: index 2 retired,
353
+ # NUM still says 3, so the loader takes the first 3 that exist
354
+ # (1, 3, 4) rather than crashing or stopping at the gap.
355
+ "NUM_HOLEY": "3",
356
+ "HOLEY_API_KEY_1": "key-1",
357
+ "HOLEY_API_KEY_3": "key-3",
358
+ "HOLEY_API_KEY_4": "key-4",
359
+ "HOLEY_API_KEY_9": "key-9",
360
+ })
361
+ @patch('keycycle.multi_client_wrapper.load_dotenv')
362
+ def test_load_keys_tolerates_gap_inside_sanctioned_range(self, mock_dotenv):
363
+ keys = MultiClientWrapper.load_api_keys("holey")
364
+ self.assertEqual(keys, ["key-1", "key-3", "key-4"])
353
365
 
354
366
  @patch.dict(os.environ, {
355
367
  "NUM_SPARSE": "5",
File without changes
File without changes
File without changes
File without changes
File without changes