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.
- reach_commons/app_logging/logging_utils.py +17 -17
- reach_commons/mongo/mongo_token_bucket.py +108 -0
- reach_commons/redis_manager.py +11 -0
- {reach_commons-0.18.35.dist-info → reach_commons-0.18.36.dist-info}/METADATA +2 -4
- {reach_commons-0.18.35.dist-info → reach_commons-0.18.36.dist-info}/RECORD +6 -7
- {reach_commons-0.18.35.dist-info → reach_commons-0.18.36.dist-info}/WHEEL +1 -1
- reach_commons/.DS_Store +0 -0
- reach_commons/clients/.DS_Store +0 -0
|
@@ -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)
|
reach_commons/redis_manager.py
CHANGED
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: reach_commons
|
|
3
|
-
Version: 0.18.
|
|
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
|
|
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=
|
|
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.
|
|
34
|
-
reach_commons-0.18.
|
|
35
|
-
reach_commons-0.18.
|
|
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,,
|
reach_commons/.DS_Store
DELETED
|
Binary file
|
reach_commons/clients/.DS_Store
DELETED
|
Binary file
|