reach_commons 0.18.35__py3-none-any.whl → 0.18.36__py3-none-any.whl

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,17 +1,17 @@
1
- import logging
2
- import os
3
-
4
-
5
- def init_logger(name: str):
6
- logging.basicConfig(
7
- level=getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO),
8
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
9
- force=True,
10
- )
11
- for noisy in ("botocore", "boto3", "urllib3"):
12
- logging.getLogger(noisy).setLevel(logging.WARNING)
13
- return logging.getLogger(name)
14
-
15
-
16
- def log_with_event(level_fn, msg, event):
17
- level_fn(f"{msg} | event={event}")
1
+ import logging
2
+ import os
3
+
4
+
5
+ def init_logger(name: str):
6
+ logging.basicConfig(
7
+ level=getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO),
8
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
9
+ force=True,
10
+ )
11
+ for noisy in ("botocore", "boto3", "urllib3"):
12
+ logging.getLogger(noisy).setLevel(logging.WARNING)
13
+ return logging.getLogger(name)
14
+
15
+
16
+ def log_with_event(level_fn, msg, event):
17
+ level_fn(f"{msg} | event={event}")
@@ -0,0 +1,108 @@
1
+ # mongo_token_bucket.py
2
+ import random
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Tuple
6
+
7
+ _LUA_WINDOW_LIMITER = """
8
+ -- KEYS[1] = window key
9
+ -- ARGV[1] = tokens_to_consume
10
+ -- ARGV[2] = ttl_seconds
11
+ -- ARGV[3] = limit
12
+
13
+ local tokens = tonumber(ARGV[1])
14
+ local ttl = tonumber(ARGV[2])
15
+ local limit = tonumber(ARGV[3])
16
+
17
+ local current = redis.call('INCRBY', KEYS[1], tokens)
18
+
19
+ -- If this was the first increment (key was 0 / nonexistent), set TTL
20
+ if current == tokens then
21
+ redis.call('EXPIRE', KEYS[1], ttl)
22
+ end
23
+
24
+ if current <= limit then
25
+ return 1
26
+ else
27
+ return 0
28
+ end
29
+ """
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AcquireResult:
34
+ allowed: bool
35
+ retry_after_seconds: int # for visibility timeout / delay
36
+
37
+
38
+ class MongoTokenBucketManager:
39
+ """
40
+ Window-based limiter:
41
+ - limit tokens per interval_seconds
42
+ - atomic via Redis Lua
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ redis_manager,
48
+ limit_per_window: int,
49
+ interval_seconds: int = 2,
50
+ bucket_key: str = "global",
51
+ jitter_seconds: Optional[int] = None,
52
+ key_prefix: str = "mongo_write_budget",
53
+ ):
54
+ if interval_seconds <= 0:
55
+ raise ValueError("interval_seconds must be > 0")
56
+ if limit_per_window <= 0:
57
+ raise ValueError("limit_per_window must be > 0")
58
+
59
+ self.redis = redis_manager
60
+ self.limit = int(limit_per_window)
61
+ self.interval = int(interval_seconds)
62
+ self.bucket_key = bucket_key
63
+ self.key_prefix = key_prefix
64
+ self.jitter = (
65
+ int(jitter_seconds) if jitter_seconds is not None else self.interval
66
+ ) # default: 0..interval
67
+
68
+ # Cache the script SHA if you want; eval is fine for now (2-day fix).
69
+ self._lua = _LUA_WINDOW_LIMITER
70
+
71
+ def _now(self) -> float:
72
+ return time.time()
73
+
74
+ def _window_start(self, now: float) -> int:
75
+ return int(now // self.interval) * self.interval
76
+
77
+ def _redis_key(self, window_start: int) -> str:
78
+ return f"{self.key_prefix}:{self.bucket_key}:{window_start}"
79
+
80
+ def acquire(self, tokens: int = 1) -> AcquireResult:
81
+ """
82
+ Try to consume tokens. If denied, returns retry_after_seconds to push visibility timeout.
83
+ """
84
+ now = self._now()
85
+ window_start = self._window_start(now)
86
+ window_end = window_start + self.interval
87
+
88
+ key = self._redis_key(window_start)
89
+
90
+ # TTL a bit bigger than the window so old keys go away safely
91
+ ttl_seconds = max(self.interval * 2, 5)
92
+
93
+ allowed = self.redis.eval(
94
+ self._lua,
95
+ numkeys=1,
96
+ keys=[key],
97
+ args=[str(int(tokens)), str(int(ttl_seconds)), str(int(self.limit))],
98
+ )
99
+
100
+ if allowed == 1:
101
+ return AcquireResult(allowed=True, retry_after_seconds=0)
102
+
103
+ # Denied: retry after next window, plus jitter to avoid waves
104
+ base = max(0.0, window_end - now) # seconds until next window
105
+ jitter = random.uniform(0.0, float(self.jitter))
106
+ retry_after = int(max(1.0, base + jitter)) # at least 1s
107
+
108
+ return AcquireResult(allowed=False, retry_after_seconds=retry_after)
@@ -80,3 +80,14 @@ class RedisManager:
80
80
  name=key, value="1", ex=expire_seconds, nx=True
81
81
  )
82
82
  return result is True
83
+
84
+ def incrby(self, key: str, amount: int = 1) -> int:
85
+ return int(self.redis_connection.incrby(key, amount))
86
+
87
+ def expire(self, key: str, seconds: int) -> bool:
88
+ return bool(self.redis_connection.expire(key, seconds))
89
+
90
+ def eval(self, script: str, numkeys: int, keys=None, args=None):
91
+ keys = keys or []
92
+ args = args or []
93
+ return self.redis_connection.eval(script, numkeys, *(keys + args))
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: reach_commons
3
- Version: 0.18.35
3
+ Version: 0.18.36
4
4
  Summary: Reach Commons is a versatile utility library designed to streamline and enhance development workflows within the Reach ecosystem.
5
5
  License: MIT
6
6
  Author: Engineering
@@ -14,8 +14,6 @@ Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Programming Language :: Python :: 3.14
19
17
  Requires-Dist: curlify (==3.0.0)
20
18
  Requires-Dist: fastapi (>=0.115.5)
21
19
  Requires-Dist: pydantic (>=2.9.2)
@@ -1,12 +1,10 @@
1
- reach_commons/.DS_Store,sha256=M7WbC5LcTvuTEuj3k7z3tqzMSVGgu87auq6UOLoSv2M,6148
2
1
  reach_commons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
2
  reach_commons/app_logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
3
  reach_commons/app_logging/http_logger.py,sha256=mljQCdmsmtD2HsC_gsFwZAxPlAiLPYVirVrCjXFxitY,2541
5
4
  reach_commons/app_logging/log_deprecated_endpoints.py,sha256=yXs9Jh7V0_0cMnzwXV9WRgCdFXe_tybcFE1eQl2KNC4,2020
6
5
  reach_commons/app_logging/logger.py,sha256=Iq2XTl1zLgHDmVsTMdlFadcYJOqQNhBcFSscacKs_Xs,2295
7
6
  reach_commons/app_logging/logging_config.py,sha256=Y1JaZOoQBWgQjkOqYmeDRIm0p2eCOl3yTzgsgqyqm8I,1539
8
- reach_commons/app_logging/logging_utils.py,sha256=zeDRm3DugbuqMIDstTsVyH5xU_ZV-WAcCINxb6qYdJY,491
9
- reach_commons/clients/.DS_Store,sha256=1lFlJ5EFymdzGAUAaI30vcaaLHt3F1LwpG7xILf9jsM,6148
7
+ reach_commons/app_logging/logging_utils.py,sha256=-QQ4l9CTYs-lCL-VNxGdWvPquRQtrnXWqzNfxOLO9ys,508
10
8
  reach_commons/clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
9
  reach_commons/clients/event_processor.py,sha256=KmYF0kuZxLmHQjJASXMr5jz2D_D3WNHB0c4QOlZo1_E,2024
12
10
  reach_commons/clients/hubspot.py,sha256=ntAzvwoaq78MkKaVoZ7geND-AafAzccNnJogfJDahVA,5497
@@ -16,6 +14,7 @@ reach_commons/clients/reach_ops_api.py,sha256=1kTY4cF2eBCl_kqs1r5lbzxt4_7EFRmltb
16
14
  reach_commons/mongo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
15
  reach_commons/mongo/customer_persistence.py,sha256=acrtpyCWr9vLVq61saJ3_Vp4DYHFBTM9XqoYC72J84w,3735
18
16
  reach_commons/mongo/customer_persistence_async.py,sha256=BmcP8TXyyQah-GYM3wcKi1baqSCycjw7UadlxGywyQM,3892
17
+ reach_commons/mongo/mongo_token_bucket.py,sha256=Q98EGoetrNhtAqlfvGk2tXex4EEl0d-npGXygld0gSQ,3272
19
18
  reach_commons/mongo/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
19
  reach_commons/reach_aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
20
  reach_commons/reach_aws/commons.py,sha256=qQba0li75BIpmyVc0sDVrrxbtYvDCedF6RmFD-V4MYQ,259
@@ -26,10 +25,10 @@ reach_commons/reach_aws/kms.py,sha256=ZOfyJMQUgxJEojRoB7-aCxtATpNx1Ig522IUYH11NZ
26
25
  reach_commons/reach_aws/s3.py,sha256=2MLlDNFx0SROJBpE_KjJefyrB7lMqTlrYuRhSZx4iKs,3945
27
26
  reach_commons/reach_aws/sqs.py,sha256=IKKWrd-qbhMMVYUvGbaq1ouVRdx-0u-SqwYaTcp0tWY,21645
28
27
  reach_commons/reach_base_model.py,sha256=vgdGDcZr3iXMmyRhmBhOf_LKWB_6QzT3r_Yiyo6OmEk,3009
29
- reach_commons/redis_manager.py,sha256=SgUtdtt0eV4bUwsWDankIa9Bjfgcm2DKcmVMQT6ptF0,2946
28
+ reach_commons/redis_manager.py,sha256=RXtEksz13_uXwJAxfDvT8sCNwNIPCdqhsLUlewRq3Jg,3392
30
29
  reach_commons/sms_smart_encoding.py,sha256=92y0RmZ0l4ONHpC9qeO5KfViSNq64yE2rc7lhNDSZqE,1241
31
30
  reach_commons/utils.py,sha256=dMgKIGqTgoSItuBI8oz81gKtW3qi21Jkljv9leS_V88,8475
32
31
  reach_commons/validations.py,sha256=x_lkrtlrCAJC_f7mZb19JjfKFbYlPFv-P84K_lbZyYs,1056
33
- reach_commons-0.18.35.dist-info/METADATA,sha256=gD0kZHrRs6H0VzzuUYrxs4EFOju-ubt9hdnB4AdLMLM,1965
34
- reach_commons-0.18.35.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
35
- reach_commons-0.18.35.dist-info/RECORD,,
32
+ reach_commons-0.18.36.dist-info/METADATA,sha256=cCN7jv1Iyn2GMKoYAkt15QalHcoN4pVWVwmXqKBESXg,1863
33
+ reach_commons-0.18.36.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
34
+ reach_commons-0.18.36.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 1.8.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
reach_commons/.DS_Store DELETED
Binary file
Binary file