rapyer 1.2.0__py3-none-any.whl → 1.2.1__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.
rapyer/__init__.py CHANGED
@@ -7,6 +7,7 @@ from rapyer.base import (
7
7
  find_redis_models,
8
8
  ainsert,
9
9
  alock_from_key,
10
+ apipeline,
10
11
  )
11
12
  from rapyer.init import init_rapyer, teardown_rapyer
12
13
 
@@ -19,4 +20,5 @@ __all__ = [
19
20
  "find_redis_models",
20
21
  "ainsert",
21
22
  "alock_from_key",
23
+ "apipeline",
22
24
  ]
rapyer/base.py CHANGED
@@ -18,12 +18,10 @@ from pydantic import (
18
18
  ValidationError,
19
19
  )
20
20
  from pydantic_core.core_schema import FieldSerializationInfo, ValidationInfo
21
- from redis.commands.search.index_definition import IndexDefinition, IndexType
22
- from redis.commands.search.query import Query
23
- from redis.exceptions import NoScriptError, ResponseError
21
+ from redis.client import Pipeline
24
22
 
25
23
  from rapyer.config import RedisConfig
26
- from rapyer.context import _context_var, _context_xx_pipe
24
+ from rapyer.context import _context_var
27
25
  from rapyer.errors.base import (
28
26
  KeyNotFound,
29
27
  PersistentNoScriptError,
@@ -36,7 +34,7 @@ from rapyer.fields.index import IndexAnnotation
36
34
  from rapyer.fields.key import KeyAnnotation
37
35
  from rapyer.fields.safe_load import SafeLoadAnnotation
38
36
  from rapyer.links import REDIS_SUPPORTED_LINK
39
- from rapyer.scripts import handle_noscript_error
37
+ from rapyer.scripts import registry as scripts_registry
40
38
  from rapyer.types.base import RedisType, REDIS_DUMP_FLAG_NAME, FAILED_FIELDS_KEY
41
39
  from rapyer.types.convert import RedisConverter
42
40
  from rapyer.typing_support import Self, Unpack
@@ -56,6 +54,9 @@ from rapyer.utils.redis import (
56
54
  acquire_lock,
57
55
  update_keys_in_pipeline,
58
56
  )
57
+ from redis.commands.search.index_definition import IndexDefinition, IndexType
58
+ from redis.commands.search.query import Query
59
+ from redis.exceptions import NoScriptError, ResponseError
59
60
 
60
61
  logger = logging.getLogger("rapyer")
61
62
 
@@ -150,6 +151,10 @@ class AtomicRedisModel(BaseModel):
150
151
  field_path = self.field_path
151
152
  return f"${field_path}" if field_path else "$"
152
153
 
154
+ @property
155
+ def client(self):
156
+ return _context_var.get() or self.Meta.redis
157
+
153
158
  @classmethod
154
159
  def should_refresh(cls):
155
160
  return cls.Meta.refresh_ttl and cls.Meta.ttl is not None
@@ -312,10 +317,10 @@ class AtomicRedisModel(BaseModel):
312
317
 
313
318
  async def asave(self) -> Self:
314
319
  model_dump = self.redis_dump()
315
- await self.Meta.redis.json().set(self.key, self.json_path, model_dump)
320
+ await self.client.json().set(self.key, self.json_path, model_dump)
316
321
  if self.Meta.ttl is not None:
317
322
  nx = not self.Meta.refresh_ttl
318
- await self.Meta.redis.expire(self.key, self.Meta.ttl, nx=nx)
323
+ await self.client.expire(self.key, self.Meta.ttl, nx=nx)
319
324
  return self
320
325
 
321
326
  def redis_dump(self):
@@ -533,7 +538,9 @@ class AtomicRedisModel(BaseModel):
533
538
  async def apipeline(
534
539
  self, ignore_redis_error: bool = False
535
540
  ) -> AbstractAsyncContextManager[Self]:
536
- async with self.Meta.redis.pipeline(transaction=True) as pipe:
541
+ async with apipeline(
542
+ ignore_redis_error=ignore_redis_error, _meta=self.Meta
543
+ ) as pipe:
537
544
  try:
538
545
  redis_model = await self.__class__.aget(self.key)
539
546
  unset_fields = {
@@ -545,57 +552,13 @@ class AtomicRedisModel(BaseModel):
545
552
  redis_model = self
546
553
  else:
547
554
  raise
548
- _context_var.set(pipe)
549
- _context_xx_pipe.set(ignore_redis_error)
550
555
  yield redis_model
551
- commands_backup = list(pipe.command_stack)
552
- noscript_on_first_attempt = False
553
- noscript_on_retry = False
554
-
555
- try:
556
- if self.should_refresh():
557
- pipe.expire(self.key, self.Meta.ttl)
558
- await pipe.execute()
559
- except NoScriptError:
560
- noscript_on_first_attempt = True
561
- except ResponseError as exc:
562
- if ignore_redis_error:
563
- logger.warning(
564
- "Swallowed ResponseError during pipeline.execute() with "
565
- "ignore_redis_error=True for key %r: %s",
566
- getattr(self, "key", None),
567
- exc,
568
- )
569
- else:
570
- raise
571
-
572
- if noscript_on_first_attempt:
573
- await handle_noscript_error(self.Meta.redis)
574
- evalsha_commands = [
575
- (args, options)
576
- for args, options in commands_backup
577
- if args[0] == "EVALSHA"
578
- ]
579
- # Retry execute the pipeline actions
580
- async with self.Meta.redis.pipeline(transaction=True) as retry_pipe:
581
- for args, options in evalsha_commands:
582
- retry_pipe.execute_command(*args, **options)
583
- try:
584
- await retry_pipe.execute()
585
- except NoScriptError:
586
- noscript_on_retry = True
587
-
588
- if noscript_on_retry:
589
- raise PersistentNoScriptError(
590
- "NOSCRIPT error persisted after re-registering scripts. "
591
- "This indicates a server-side problem with Redis."
592
- )
593
556
 
594
- _context_var.set(None)
595
- _context_xx_pipe.set(False)
557
+ if self.should_refresh():
558
+ pipe.expire(self.key, self.Meta.ttl)
596
559
 
597
560
  def __setattr__(self, name: str, value: Any) -> None:
598
- if name not in self.__annotations__ or value is None:
561
+ if name not in self.__class__.model_fields or value is None:
599
562
  super().__setattr__(name, value)
600
563
  return
601
564
 
@@ -723,3 +686,56 @@ async def alock_from_key(
723
686
  yield redis_model
724
687
  if save_at_end and redis_model is not None:
725
688
  await redis_model.asave()
689
+
690
+
691
+ @contextlib.asynccontextmanager
692
+ async def apipeline(
693
+ ignore_redis_error: bool = False, _meta: RedisConfig = None
694
+ ) -> AbstractAsyncContextManager[Pipeline]:
695
+ _meta = _meta or AtomicRedisModel.Meta
696
+ redis = _meta.redis
697
+ async with redis.pipeline(transaction=True) as pipe:
698
+ pipe_prev = _context_var.set(pipe)
699
+ try:
700
+ yield pipe
701
+ commands_backup = list(pipe.command_stack)
702
+ noscript_on_first_attempt = False
703
+ noscript_on_retry = False
704
+
705
+ try:
706
+ await pipe.execute()
707
+ except NoScriptError:
708
+ noscript_on_first_attempt = True
709
+ except ResponseError as exc:
710
+ if ignore_redis_error:
711
+ logger.warning(
712
+ "Swallowed ResponseError during pipeline.execute() with "
713
+ "ignore_redis_error=True: %s",
714
+ exc,
715
+ )
716
+ else:
717
+ raise
718
+
719
+ if noscript_on_first_attempt:
720
+ await scripts_registry.handle_noscript_error(redis, _meta)
721
+ evalsha_commands = [
722
+ (args, options)
723
+ for args, options in commands_backup
724
+ if args[0] == "EVALSHA"
725
+ ]
726
+ # Retry execute the pipeline actions
727
+ async with redis.pipeline(transaction=True) as retry_pipe:
728
+ for args, options in evalsha_commands:
729
+ retry_pipe.execute_command(*args, **options)
730
+ try:
731
+ await retry_pipe.execute()
732
+ except NoScriptError:
733
+ noscript_on_retry = True
734
+
735
+ if noscript_on_retry:
736
+ raise PersistentNoScriptError(
737
+ "NOSCRIPT error persisted after re-registering scripts. "
738
+ "This indicates a server-side problem with Redis."
739
+ )
740
+ finally:
741
+ _context_var.reset(pipe_prev)
rapyer/context.py CHANGED
@@ -7,6 +7,3 @@ from redis.asyncio.client import Redis
7
7
  _context_var: contextvars.ContextVar[Optional["Redis"]] = contextvars.ContextVar(
8
8
  "redis", default=None
9
9
  )
10
- _context_xx_pipe: contextvars.ContextVar[bool] = contextvars.ContextVar(
11
- "redis_xx_pipe", default=False
12
- )
@@ -1,3 +1,6 @@
1
+ REDIS_VARIANT = "redis"
2
+ FAKEREDIS_VARIANT = "fakeredis"
3
+
1
4
  REMOVE_RANGE_SCRIPT_NAME = "remove_range"
2
5
  NUM_MUL_SCRIPT_NAME = "num_mul"
3
6
  NUM_FLOORDIV_SCRIPT_NAME = "num_floordiv"
rapyer/scripts/loader.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from functools import lru_cache
2
2
  from importlib import resources
3
3
 
4
+ from rapyer.scripts.constants import FAKEREDIS_VARIANT, REDIS_VARIANT
4
5
 
5
6
  VARIANTS = {
6
- "redis": {
7
+ REDIS_VARIANT: {
7
8
  "EXTRACT_ARRAY": "local arr = cjson.decode(arr_json)[1]",
8
9
  "EXTRACT_VALUE": "local value = tonumber(cjson.decode(current_json)[1])",
9
10
  "EXTRACT_STR": "local value = cjson.decode(current_json)[1]",
@@ -19,7 +20,7 @@ else
19
20
  extracted = parsed
20
21
  end""",
21
22
  },
22
- "fakeredis": {
23
+ FAKEREDIS_VARIANT: {
23
24
  "EXTRACT_ARRAY": "local arr = cjson.decode(arr_json)",
24
25
  "EXTRACT_VALUE": "local value = tonumber(cjson.decode(current_json)[1])",
25
26
  "EXTRACT_STR": "local value = cjson.decode(current_json)[1]",
@@ -45,7 +46,7 @@ def _load_template(category: str, name: str) -> str:
45
46
  return resources.files(package).joinpath(filename).read_text()
46
47
 
47
48
 
48
- def load_script(category: str, name: str, variant: str = "redis") -> str:
49
+ def load_script(category: str, name: str, variant: str = REDIS_VARIANT) -> str:
49
50
  template = _load_template(category, name)
50
51
  replacements = VARIANTS[variant]
51
52
  result = template
@@ -1,19 +1,27 @@
1
- from rapyer.errors import ScriptsNotInitializedError
1
+ from typing import TYPE_CHECKING
2
+
3
+ from rapyer.errors import PersistentNoScriptError, ScriptsNotInitializedError
2
4
  from rapyer.scripts.constants import (
3
5
  DATETIME_ADD_SCRIPT_NAME,
4
6
  DICT_POP_SCRIPT_NAME,
5
7
  DICT_POPITEM_SCRIPT_NAME,
8
+ FAKEREDIS_VARIANT,
6
9
  NUM_FLOORDIV_SCRIPT_NAME,
7
10
  NUM_MOD_SCRIPT_NAME,
8
11
  NUM_MUL_SCRIPT_NAME,
9
12
  NUM_POW_FLOAT_SCRIPT_NAME,
10
13
  NUM_POW_SCRIPT_NAME,
11
14
  NUM_TRUEDIV_SCRIPT_NAME,
15
+ REDIS_VARIANT,
12
16
  REMOVE_RANGE_SCRIPT_NAME,
13
17
  STR_APPEND_SCRIPT_NAME,
14
18
  STR_MUL_SCRIPT_NAME,
15
19
  )
16
20
  from rapyer.scripts.loader import load_script
21
+ from redis.exceptions import NoScriptError
22
+
23
+ if TYPE_CHECKING:
24
+ from rapyer.config import RedisConfig
17
25
 
18
26
  SCRIPT_REGISTRY: list[tuple[str, str, str]] = [
19
27
  ("list", "remove_range", REMOVE_RANGE_SCRIPT_NAME),
@@ -41,15 +49,15 @@ def _build_scripts(variant: str) -> dict[str, str]:
41
49
 
42
50
 
43
51
  def get_scripts() -> dict[str, str]:
44
- return _build_scripts("redis")
52
+ return _build_scripts(REDIS_VARIANT)
45
53
 
46
54
 
47
55
  def get_scripts_fakeredis() -> dict[str, str]:
48
- return _build_scripts("fakeredis")
56
+ return _build_scripts(FAKEREDIS_VARIANT)
49
57
 
50
58
 
51
59
  async def register_scripts(redis_client, is_fakeredis: bool = False) -> None:
52
- variant = "fakeredis" if is_fakeredis else "redis"
60
+ variant = FAKEREDIS_VARIANT if is_fakeredis else REDIS_VARIANT
53
61
  scripts = _build_scripts(variant)
54
62
  for name, script_text in scripts.items():
55
63
  sha = await redis_client.script_load(script_text)
@@ -70,10 +78,25 @@ def run_sha(pipeline, script_name: str, keys: int, *args):
70
78
  pipeline.evalsha(sha, keys, *args)
71
79
 
72
80
 
73
- async def arun_sha(client, script_name: str, keys: int, *args):
81
+ async def arun_sha(
82
+ client, redis_config: "RedisConfig", script_name: str, keys: int, *args
83
+ ):
84
+ sha = get_script(script_name)
85
+ try:
86
+ return await client.evalsha(sha, keys, *args)
87
+ except NoScriptError:
88
+ pass
89
+
90
+ await handle_noscript_error(client, redis_config)
74
91
  sha = get_script(script_name)
75
- return await client.evalsha(sha, keys, *args)
92
+ try:
93
+ return await client.evalsha(sha, keys, *args)
94
+ except NoScriptError as e:
95
+ raise PersistentNoScriptError(
96
+ "NOSCRIPT error persisted after re-registering scripts. "
97
+ "This indicates a server-side problem with Redis."
98
+ ) from e
76
99
 
77
100
 
78
- async def handle_noscript_error(redis_client) -> None:
79
- await register_scripts(redis_client)
101
+ async def handle_noscript_error(redis_client, redis_config: "RedisConfig"):
102
+ await register_scripts(redis_client, is_fakeredis=redis_config.is_fake_redis)
rapyer/types/dct.py CHANGED
@@ -106,7 +106,13 @@ class RedisDict(dict[str, T], GenericRedisType, Generic[T]):
106
106
 
107
107
  async def apop(self, key, default=None):
108
108
  result = await arun_sha(
109
- self.client, DICT_POP_SCRIPT_NAME, 1, self.key, self.json_path, key
109
+ self.client,
110
+ self.Meta,
111
+ DICT_POP_SCRIPT_NAME,
112
+ 1,
113
+ self.key,
114
+ self.json_path,
115
+ key,
110
116
  )
111
117
  super().pop(key, None)
112
118
  await self.refresh_ttl_if_needed()
@@ -120,7 +126,12 @@ class RedisDict(dict[str, T], GenericRedisType, Generic[T]):
120
126
 
121
127
  async def apopitem(self):
122
128
  result = await arun_sha(
123
- self.client, DICT_POPITEM_SCRIPT_NAME, 1, self.key, self.json_path
129
+ self.client,
130
+ self.Meta,
131
+ DICT_POPITEM_SCRIPT_NAME,
132
+ 1,
133
+ self.key,
134
+ self.json_path,
124
135
  )
125
136
  await self.refresh_ttl_if_needed()
126
137
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rapyer
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Pydantic models with Redis as the backend
5
5
  License: MIT
6
6
  Keywords: redis,redis-json,pydantic,pydantic-v2,orm,database,async,nosql,cache,key-value,data-modeling,python,backend,storage,serialization,validation
@@ -1,7 +1,7 @@
1
- rapyer/__init__.py,sha256=vgrOQkqvf0UBQMPpWSLvd9TxXXCQ8Oyt0g5E6nkhbPY,415
2
- rapyer/base.py,sha256=KeaRO5btbeV95CNeJHkS18Q5jmAkyxa11Ttsu0V5C-4,27004
1
+ rapyer/__init__.py,sha256=PEFZI3eMHz-1ojcHEczcHKWAwloF5UYIibsvNg7O6a4,447
2
+ rapyer/base.py,sha256=Q5SdwByj7cCGsKAVsITEfvp9ayp7b-KwTJzRR8Qd7fU,27196
3
3
  rapyer/config.py,sha256=Js1FGfyKfS16eOTgsPEhFfi1yTM_UkR8v2z_Vv6C3Bo,978
4
- rapyer/context.py,sha256=yuD_EGZB04gJv9YDlqeo7VD70J0Ldx5tE3NEGTcNdwA,357
4
+ rapyer/context.py,sha256=MyyjpcIe2y8wUpiPNv-uYA2iDJyTuL_YxIueByle2kg,247
5
5
  rapyer/errors/__init__.py,sha256=P-DIYe85wySkCpuyWufwF7zOOOL8NUiMaHZROsciG1M,530
6
6
  rapyer/errors/base.py,sha256=PBTEbR0Y8rEhQXuWHAhoHnSxvoZ5nDfUW3Th2KOdb9M,804
7
7
  rapyer/fields/__init__.py,sha256=KyFUM5v0kdM2chITE5cH-L-IL_LRhWDXGbezhXgtEF0,157
@@ -12,8 +12,8 @@ rapyer/fields/safe_load.py,sha256=xS3PwOe9K2D7az-ctNV798cTSIb7bcviWHQ1kL1tbBo,59
12
12
  rapyer/init.py,sha256=R6xncUnBeq07gtM_iWqH4SHLquH7MNQgTbVbC6xIA6U,2133
13
13
  rapyer/links.py,sha256=A0usszwvwXI7FqwTEYquGYrqyNQqT4HUWhqmLteGXWU,121
14
14
  rapyer/scripts/__init__.py,sha256=FvTJvulDxVnVqc8JK4GmHXeS-lMis4_cudaQYe7_VKg,1130
15
- rapyer/scripts/constants.py,sha256=TMbBxw3Kufsmn_tWxpfzTVIdS89cHOreo2Z-RbY8koc,452
16
- rapyer/scripts/loader.py,sha256=9aYRIwnmZ60lLToLKLK07vLBnWARsTkoBZQnrnjLVHw,1827
15
+ rapyer/scripts/constants.py,sha256=5PdpyNb94SRgzcskto41gG4Za35UxNJ8Nd0LtoC8RIg,509
16
+ rapyer/scripts/loader.py,sha256=m5dy3L_FbyFR5oor2JqG-dOLp3JnZDYg5Zhd9YJFrdY,1915
17
17
  rapyer/scripts/lua/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  rapyer/scripts/lua/datetime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  rapyer/scripts/lua/datetime/add.lua,sha256=wMhBkAr9KoDRtDR0qG0ofA8BENO1Ykr7f437kVLIUww,1536
@@ -32,13 +32,13 @@ rapyer/scripts/lua/numeric/truediv.lua,sha256=lQv6YuPb7YHsg50x7b_zDuQ3S1Oa3oeydO
32
32
  rapyer/scripts/lua/string/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  rapyer/scripts/lua/string/append.lua,sha256=JfSt21qCQzIMB_0dLmN58s9MFBDaOkiA8dBSBDO77bU,310
34
34
  rapyer/scripts/lua/string/mul.lua,sha256=rABORn6gxXobY44S1JYeLnLPeTZu26otemB0s6tBfx8,328
35
- rapyer/scripts/registry.py,sha256=RWGJdye4_UsirvsFM5gfOjBtMI-6Hx6PebcJmSs2N8k,2473
35
+ rapyer/scripts/registry.py,sha256=uPHw3SMYqkyWAAo_j9RqZtHl54jPTwSGR_5ggNYlp-k,3223
36
36
  rapyer/types/__init__.py,sha256=lM2ZdpkgWW_7eh0AXoabtgA9CGhzWUHOi9W0_RTqu6I,484
37
37
  rapyer/types/base.py,sha256=AA7E0knCy9dJd78O0APG5FEUXvHSqWknzkp3Rpj1O30,6034
38
38
  rapyer/types/byte.py,sha256=cnV-XMMPKv03tV0oKSJ1mlrpwdAygs4H_EhOVheu-2Q,1791
39
39
  rapyer/types/convert.py,sha256=nyElDMWaWXtI_dXpAAg5elqWo439HsZ6u6wLj9WuHsI,3548
40
40
  rapyer/types/datetime.py,sha256=EuT6XhFPfQWNtGRd_8bX3DWm6_EGo-HqMef9Fr3fiNQ,4294
41
- rapyer/types/dct.py,sha256=-PKZoQuBXrT7wcHlss-Oc3mQqAQq2buI8aqrSoN5F7U,6431
41
+ rapyer/types/dct.py,sha256=zsBJdA-y6UTUmc88q9h9Vhcjcdk-OJ18fJSRLvAe7LQ,6587
42
42
  rapyer/types/float.py,sha256=4TEKOJ778MFogULlfRq8iQV9bYdx-eArgRTtmeRzeP4,2864
43
43
  rapyer/types/init.py,sha256=SGH2uH9dIkWfaTMdiD-YV053nwRPaxUs4jTG1gOA2dg,495
44
44
  rapyer/types/integer.py,sha256=TvpFehA4LNvYWapaOph9izkWuf9eAAKqkPicjsKaz9s,2381
@@ -50,6 +50,6 @@ rapyer/utils/annotation.py,sha256=MB01l2k9g10AOSfttVfDIUc7JBzZWdH_Cn9FDe0IEGo,30
50
50
  rapyer/utils/fields.py,sha256=LhIhnuRBNrtE3RyFI-AiScXTzqYmOfcm-rdtmcdaHM0,2815
51
51
  rapyer/utils/pythonic.py,sha256=Xiv7RLqLozgLuwZSPIxBlsypIyoIYmPG_lV78TI4r80,141
52
52
  rapyer/utils/redis.py,sha256=d4qhR7QsER0572NMis_ATUUC17Jo4h9r8TKHTF8OIds,441
53
- rapyer-1.2.0.dist-info/METADATA,sha256=nYKTAQnSHNKFqU1x-RwLtYP3v93v-CSYinIZkabg1uI,11418
54
- rapyer-1.2.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
55
- rapyer-1.2.0.dist-info/RECORD,,
53
+ rapyer-1.2.1.dist-info/METADATA,sha256=lOK0hW34l7XZ75ewvyj8GaREF5EVYr9TXGmuVy_ZdZk,11418
54
+ rapyer-1.2.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
55
+ rapyer-1.2.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.3.0
2
+ Generator: poetry-core 2.3.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any