rapyer 1.1.6__tar.gz → 1.1.7__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 (55) hide show
  1. {rapyer-1.1.6 → rapyer-1.1.7}/PKG-INFO +1 -1
  2. {rapyer-1.1.6 → rapyer-1.1.7}/pyproject.toml +2 -1
  3. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/__init__.py +2 -0
  4. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/base.py +101 -29
  5. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/config.py +2 -0
  6. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/errors/__init__.py +6 -0
  7. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/errors/base.py +8 -0
  8. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/expression.py +1 -0
  9. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/init.py +10 -3
  10. rapyer-1.1.7/rapyer/scripts/__init__.py +41 -0
  11. rapyer-1.1.7/rapyer/scripts/constants.py +10 -0
  12. rapyer-1.1.7/rapyer/scripts/loader.py +34 -0
  13. rapyer-1.1.7/rapyer/scripts/lua/datetime/__init__.py +0 -0
  14. rapyer-1.1.7/rapyer/scripts/lua/datetime/add.lua +80 -0
  15. rapyer-1.1.7/rapyer/scripts/lua/list/__init__.py +0 -0
  16. rapyer-1.1.7/rapyer/scripts/lua/list/remove_range.lua +36 -0
  17. rapyer-1.1.7/rapyer/scripts/lua/numeric/__init__.py +0 -0
  18. rapyer-1.1.7/rapyer/scripts/lua/numeric/floordiv.lua +13 -0
  19. rapyer-1.1.7/rapyer/scripts/lua/numeric/mod.lua +13 -0
  20. rapyer-1.1.7/rapyer/scripts/lua/numeric/mul.lua +13 -0
  21. rapyer-1.1.7/rapyer/scripts/lua/numeric/pow.lua +13 -0
  22. rapyer-1.1.7/rapyer/scripts/lua/numeric/pow_float.lua +13 -0
  23. rapyer-1.1.7/rapyer/scripts/lua/numeric/truediv.lua +13 -0
  24. rapyer-1.1.7/rapyer/scripts/lua/string/__init__.py +0 -0
  25. rapyer-1.1.7/rapyer/scripts/lua/string/append.lua +13 -0
  26. rapyer-1.1.7/rapyer/scripts/lua/string/mul.lua +13 -0
  27. rapyer-1.1.7/rapyer/scripts/registry.py +65 -0
  28. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/base.py +8 -6
  29. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/byte.py +1 -0
  30. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/convert.py +1 -0
  31. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/datetime.py +58 -2
  32. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/dct.py +13 -20
  33. rapyer-1.1.7/rapyer/types/float.py +100 -0
  34. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/integer.py +31 -13
  35. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/lst.py +27 -21
  36. rapyer-1.1.7/rapyer/types/string.py +41 -0
  37. rapyer-1.1.7/rapyer/utils/__init__.py +0 -0
  38. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/redis.py +0 -7
  39. rapyer-1.1.6/rapyer/scripts.py +0 -86
  40. rapyer-1.1.6/rapyer/types/float.py +0 -51
  41. rapyer-1.1.6/rapyer/types/string.py +0 -20
  42. {rapyer-1.1.6 → rapyer-1.1.7}/README.md +0 -0
  43. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/context.py +0 -0
  44. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/__init__.py +0 -0
  45. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/index.py +0 -0
  46. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/key.py +0 -0
  47. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/safe_load.py +0 -0
  48. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/links.py +0 -0
  49. {rapyer-1.1.6/rapyer/utils → rapyer-1.1.7/rapyer/scripts/lua}/__init__.py +0 -0
  50. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/__init__.py +0 -0
  51. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/init.py +0 -0
  52. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/typing_support.py +0 -0
  53. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/annotation.py +0 -0
  54. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/fields.py +0 -0
  55. {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/pythonic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rapyer
3
- Version: 1.1.6
3
+ Version: 1.1.7
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
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "rapyer"
7
- version = "1.1.6"
7
+ version = "1.1.7"
8
8
  description = "Pydantic models with Redis as the backend"
9
9
  authors = [{name = "YedidyaHKfir", email = "yedidyakfir@gmail.com"}]
10
10
  readme = "README.md"
@@ -69,6 +69,7 @@ Repository = "https://github.com/imaginary-cherry/rapyer"
69
69
 
70
70
  [tool.poetry]
71
71
  packages = [{include = "rapyer"}]
72
+ include = ["rapyer/scripts/lua/**/*.lua"]
72
73
 
73
74
 
74
75
  [tool.poetry.group.dev.dependencies]
@@ -3,6 +3,7 @@
3
3
  from rapyer.base import (
4
4
  AtomicRedisModel,
5
5
  aget,
6
+ afind,
6
7
  find_redis_models,
7
8
  ainsert,
8
9
  get,
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "init_rapyer",
16
17
  "teardown_rapyer",
17
18
  "aget",
19
+ "afind",
18
20
  "get",
19
21
  "find_redis_models",
20
22
  "ainsert",
@@ -6,7 +6,7 @@ import logging
6
6
  import pickle
7
7
  import uuid
8
8
  from contextlib import AbstractAsyncContextManager
9
- from typing import ClassVar, Any, get_origin
9
+ from typing import ClassVar, Any, get_origin, Optional
10
10
 
11
11
  from pydantic import (
12
12
  BaseModel,
@@ -18,6 +18,11 @@ 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
24
+ from typing_extensions import deprecated
25
+
21
26
  from rapyer.config import RedisConfig
22
27
  from rapyer.context import _context_var, _context_xx_pipe
23
28
  from rapyer.errors.base import (
@@ -25,6 +30,7 @@ from rapyer.errors.base import (
25
30
  PersistentNoScriptError,
26
31
  UnsupportedIndexedFieldError,
27
32
  CantSerializeRedisValueError,
33
+ RapyerModelDoesntExistError,
28
34
  )
29
35
  from rapyer.fields.expression import ExpressionField, AtomicField, Expression
30
36
  from rapyer.fields.index import IndexAnnotation
@@ -50,12 +56,7 @@ from rapyer.utils.pythonic import safe_issubclass
50
56
  from rapyer.utils.redis import (
51
57
  acquire_lock,
52
58
  update_keys_in_pipeline,
53
- refresh_ttl_if_needed,
54
59
  )
55
- from redis.commands.search.index_definition import IndexDefinition, IndexType
56
- from redis.commands.search.query import Query
57
- from redis.exceptions import NoScriptError, ResponseError
58
- from typing_extensions import deprecated
59
60
 
60
61
  logger = logging.getLogger("rapyer")
61
62
 
@@ -169,8 +170,13 @@ class AtomicRedisModel(BaseModel):
169
170
  field_path = self.field_path
170
171
  return f"${field_path}" if field_path else "$"
171
172
 
172
- def should_refresh(self):
173
- return self.Meta.refresh_ttl and self.Meta.ttl is not None
173
+ @classmethod
174
+ def should_refresh(cls):
175
+ return cls.Meta.refresh_ttl and cls.Meta.ttl is not None
176
+
177
+ async def refresh_ttl_if_needed(self):
178
+ if self.should_refresh():
179
+ await self.Meta.redis.expire(self.key, self.Meta.ttl)
174
180
 
175
181
  @classmethod
176
182
  def redis_schema(cls, redis_name: str = ""):
@@ -398,9 +404,7 @@ class AtomicRedisModel(BaseModel):
398
404
  async with self.Meta.redis.pipeline() as pipe:
399
405
  update_keys_in_pipeline(pipe, self.key, **json_path_kwargs)
400
406
  await pipe.execute()
401
- await refresh_ttl_if_needed(
402
- self.Meta.redis, self.key, self.Meta.ttl, self.Meta.refresh_ttl
403
- )
407
+ await self.refresh_ttl_if_needed()
404
408
 
405
409
  async def aset_ttl(self, ttl: int) -> None:
406
410
  if self.is_inner_model():
@@ -427,9 +431,8 @@ class AtomicRedisModel(BaseModel):
427
431
  instance = cls.model_validate(model_dump, context=context)
428
432
  instance.key = key
429
433
  instance._failed_fields = context.get(FAILED_FIELDS_KEY, set())
430
- await refresh_ttl_if_needed(
431
- cls.Meta.redis, key, cls.Meta.ttl, cls.Meta.refresh_ttl
432
- )
434
+ if cls.should_refresh():
435
+ await cls.Meta.redis.expire(key, cls.Meta.ttl)
433
436
  return instance
434
437
 
435
438
  @deprecated(
@@ -448,16 +451,32 @@ class AtomicRedisModel(BaseModel):
448
451
  instance._pk = self._pk
449
452
  instance._base_model_link = self._base_model_link
450
453
  instance._failed_fields = context.get(FAILED_FIELDS_KEY, set())
451
- await refresh_ttl_if_needed(
452
- self.Meta.redis, self.key, self.Meta.ttl, self.Meta.refresh_ttl
453
- )
454
+ await self.refresh_ttl_if_needed()
454
455
  return instance
455
456
 
457
+ @classmethod
458
+ def create_redis_model(cls, model_dump: dict, key: str) -> Optional[Self]:
459
+ context = {REDIS_DUMP_FLAG_NAME: True, FAILED_FIELDS_KEY: set()}
460
+ try:
461
+ model = cls.model_validate(model_dump, context=context)
462
+ model.key = key
463
+ except ValidationError as exc:
464
+ logger.debug(
465
+ "Skipping key %s due to validation error during afind: %s",
466
+ key,
467
+ exc,
468
+ )
469
+ return None
470
+ model.key = key
471
+ model._failed_fields = context.get(FAILED_FIELDS_KEY, set())
472
+ return model
473
+
456
474
  @classmethod
457
475
  async def afind(cls, *args):
458
476
  # Separate keys (str) from expressions (Expression)
459
477
  provided_keys = [arg for arg in args if isinstance(arg, str)]
460
478
  expressions = [arg for arg in args if isinstance(arg, Expression)]
479
+ raise_on_missing = bool(provided_keys)
461
480
 
462
481
  if provided_keys and expressions:
463
482
  logger.warning(
@@ -493,22 +512,17 @@ class AtomicRedisModel(BaseModel):
493
512
  instances = []
494
513
  for model, key in zip(models, targeted_keys):
495
514
  if model is None:
515
+ if raise_on_missing:
516
+ raise KeyNotFound(f"{key} is missing in redis")
496
517
  continue
497
- context = {REDIS_DUMP_FLAG_NAME: True, FAILED_FIELDS_KEY: set()}
498
- try:
499
- model = cls.model_validate(model[0], context=context)
500
- except ValidationError as exc:
501
- logger.debug(
502
- "Skipping key %s due to validation error during afind: %s",
503
- key,
504
- exc,
505
- )
518
+ if not cls.Meta.is_fake_redis:
519
+ model = model[0]
520
+ model = cls.create_redis_model(model, key)
521
+ if model is None:
506
522
  continue
507
- model.key = key
508
- model._failed_fields = context.get(FAILED_FIELDS_KEY, set())
509
523
  instances.append(model)
510
524
 
511
- if cls.Meta.ttl is not None and cls.Meta.refresh_ttl:
525
+ if cls.should_refresh():
512
526
  async with cls.Meta.redis.pipeline() as pipe:
513
527
  for model in instances:
514
528
  pipe.expire(model.key, cls.Meta.ttl)
@@ -693,6 +707,16 @@ class AtomicRedisModel(BaseModel):
693
707
  if isinstance(attr, RedisType):
694
708
  attr._base_model_link = self
695
709
 
710
+ pipeline = _context_var.get()
711
+ if pipeline is not None:
712
+ serialized = self.model_dump(
713
+ mode="json",
714
+ context={REDIS_DUMP_FLAG_NAME: True},
715
+ include={name},
716
+ )
717
+ json_path = f"{self.json_path}.{name}"
718
+ pipeline.json().set(self.key, json_path, serialized[name])
719
+
696
720
  def __eq__(self, other):
697
721
  if not isinstance(other, BaseModel):
698
722
  return False
@@ -736,6 +760,52 @@ async def aget(redis_key: str) -> AtomicRedisModel:
736
760
  return await klass.aget(redis_key)
737
761
 
738
762
 
763
+ async def afind(*redis_keys: str, skip_missing: bool = False) -> list[AtomicRedisModel]:
764
+ if not redis_keys:
765
+ return []
766
+
767
+ redis_model_mapping = {klass.__name__: klass for klass in REDIS_MODELS}
768
+
769
+ key_to_class: dict[str, type[AtomicRedisModel]] = {}
770
+ for key in redis_keys:
771
+ class_name = key.split(":")[0]
772
+ if class_name not in redis_model_mapping:
773
+ raise RapyerModelDoesntExistError(
774
+ class_name, f"Unknown model class: {class_name}"
775
+ )
776
+ key_to_class[key] = redis_model_mapping[class_name]
777
+
778
+ models_data = await AtomicRedisModel.Meta.redis.json().mget(
779
+ keys=redis_keys, path="$"
780
+ )
781
+
782
+ instances = []
783
+ instances_by_class: dict[type[AtomicRedisModel], list[AtomicRedisModel]] = {}
784
+
785
+ for data, key in zip(models_data, redis_keys):
786
+ if data is None:
787
+ if not skip_missing:
788
+ raise KeyNotFound(f"{key} is missing in redis")
789
+ continue
790
+ klass = key_to_class[key]
791
+ if not klass.Meta.is_fake_redis:
792
+ data = data[0]
793
+ model = klass.create_redis_model(data, key)
794
+ if model is None:
795
+ continue
796
+ instances.append(model)
797
+ instances_by_class.setdefault(klass, []).append(model)
798
+
799
+ async with AtomicRedisModel.Meta.redis.pipeline() as pipe:
800
+ for klass, class_instances in instances_by_class.items():
801
+ if klass.should_refresh():
802
+ for model in class_instances:
803
+ pipe.expire(model.key, klass.Meta.ttl)
804
+ await pipe.execute()
805
+
806
+ return instances
807
+
808
+
739
809
  def find_redis_models() -> list[type[AtomicRedisModel]]:
740
810
  return REDIS_MODELS
741
811
 
@@ -744,6 +814,8 @@ async def ainsert(*models: Unpack[AtomicRedisModel]) -> list[AtomicRedisModel]:
744
814
  async with AtomicRedisModel.Meta.redis.pipeline() as pipe:
745
815
  for model in models:
746
816
  pipe.json().set(model.key, model.json_path, model.redis_dump())
817
+ if model.Meta.ttl is not None:
818
+ pipe.expire(model.key, model.Meta.ttl)
747
819
  await pipe.execute()
748
820
  return models
749
821
 
@@ -28,3 +28,5 @@ class RedisConfig:
28
28
  safe_load_all: bool = False
29
29
  # If True, use JSON serialization for fields that support it instead of pickle
30
30
  prefer_normal_json_dump: bool = False
31
+ # Set to True when using FakeRedis to normalize JSON responses
32
+ is_fake_redis: bool = False
@@ -5,6 +5,9 @@ from rapyer.errors.base import (
5
5
  RapyerError,
6
6
  ScriptsNotInitializedError,
7
7
  UnsupportedIndexedFieldError,
8
+ RapyerModelDoesntExistError,
9
+ CantSerializeRedisValueError,
10
+ KeyNotFound,
8
11
  )
9
12
 
10
13
  __all__ = [
@@ -12,6 +15,9 @@ __all__ = [
12
15
  "FindError",
13
16
  "PersistentNoScriptError",
14
17
  "RapyerError",
18
+ "KeyNotFound",
15
19
  "ScriptsNotInitializedError",
16
20
  "UnsupportedIndexedFieldError",
21
+ "RapyerModelDoesntExistError",
22
+ "CantSerializeRedisValueError",
17
23
  ]
@@ -10,6 +10,14 @@ class KeyNotFound(RapyerError):
10
10
  pass
11
11
 
12
12
 
13
+ class RapyerModelDoesntExistError(RapyerError):
14
+ """Raised when a model doesn't exist."""
15
+
16
+ def __init__(self, model_name: str, *args):
17
+ super().__init__(*args)
18
+ self.model_name = model_name
19
+
20
+
13
21
  class FindError(RapyerError):
14
22
  """Raised when a model cannot be found."""
15
23
 
@@ -1,6 +1,7 @@
1
1
  from typing import Any
2
2
 
3
3
  from pydantic import TypeAdapter
4
+
4
5
  from rapyer.errors import BadFilterError
5
6
  from rapyer.types.base import REDIS_DUMP_FLAG_NAME
6
7
  from rapyer.typing_support import Unpack
@@ -1,11 +1,16 @@
1
1
  import logging
2
2
 
3
3
  import redis.asyncio as redis_async
4
- from rapyer.base import REDIS_MODELS
5
- from rapyer.scripts import register_scripts
6
4
  from redis import ResponseError
7
5
  from redis.asyncio.client import Redis
8
6
 
7
+ from rapyer.base import REDIS_MODELS
8
+ from rapyer.scripts import register_scripts
9
+
10
+
11
+ def is_fakeredis(client) -> bool:
12
+ return "fakeredis" in type(client).__module__
13
+
9
14
 
10
15
  async def init_rapyer(
11
16
  redis: str | Redis = None,
@@ -24,12 +29,14 @@ async def init_rapyer(
24
29
  if isinstance(redis, str):
25
30
  redis = redis_async.from_url(redis, decode_responses=True, max_connections=20)
26
31
 
32
+ is_fake_redis = is_fakeredis(redis)
27
33
  if redis is not None:
28
- await register_scripts(redis)
34
+ await register_scripts(redis, is_fake_redis)
29
35
 
30
36
  for model in REDIS_MODELS:
31
37
  if redis is not None:
32
38
  model.Meta.redis = redis
39
+ model.Meta.is_fake_redis = is_fake_redis
33
40
  if ttl is not None:
34
41
  model.Meta.ttl = ttl
35
42
  if prefer_normal_json_dump is not None:
@@ -0,0 +1,41 @@
1
+ from rapyer.scripts.constants import (
2
+ DATETIME_ADD_SCRIPT_NAME,
3
+ NUM_FLOORDIV_SCRIPT_NAME,
4
+ NUM_MOD_SCRIPT_NAME,
5
+ NUM_MUL_SCRIPT_NAME,
6
+ NUM_POW_FLOAT_SCRIPT_NAME,
7
+ NUM_POW_SCRIPT_NAME,
8
+ NUM_TRUEDIV_SCRIPT_NAME,
9
+ REMOVE_RANGE_SCRIPT_NAME,
10
+ STR_APPEND_SCRIPT_NAME,
11
+ STR_MUL_SCRIPT_NAME,
12
+ )
13
+ from rapyer.scripts.registry import (
14
+ _REGISTERED_SCRIPT_SHAS,
15
+ get_scripts,
16
+ get_scripts_fakeredis,
17
+ handle_noscript_error,
18
+ register_scripts,
19
+ run_sha,
20
+ )
21
+
22
+ SCRIPTS = get_scripts()
23
+ SCRIPTS_FAKEREDIS = get_scripts_fakeredis()
24
+
25
+ __all__ = [
26
+ "DATETIME_ADD_SCRIPT_NAME",
27
+ "NUM_FLOORDIV_SCRIPT_NAME",
28
+ "NUM_MOD_SCRIPT_NAME",
29
+ "NUM_MUL_SCRIPT_NAME",
30
+ "NUM_POW_FLOAT_SCRIPT_NAME",
31
+ "NUM_POW_SCRIPT_NAME",
32
+ "NUM_TRUEDIV_SCRIPT_NAME",
33
+ "REMOVE_RANGE_SCRIPT_NAME",
34
+ "SCRIPTS",
35
+ "SCRIPTS_FAKEREDIS",
36
+ "STR_APPEND_SCRIPT_NAME",
37
+ "STR_MUL_SCRIPT_NAME",
38
+ "handle_noscript_error",
39
+ "register_scripts",
40
+ "run_sha",
41
+ ]
@@ -0,0 +1,10 @@
1
+ REMOVE_RANGE_SCRIPT_NAME = "remove_range"
2
+ NUM_MUL_SCRIPT_NAME = "num_mul"
3
+ NUM_FLOORDIV_SCRIPT_NAME = "num_floordiv"
4
+ NUM_MOD_SCRIPT_NAME = "num_mod"
5
+ NUM_POW_SCRIPT_NAME = "num_pow"
6
+ NUM_POW_FLOAT_SCRIPT_NAME = "num_pow_float"
7
+ NUM_TRUEDIV_SCRIPT_NAME = "num_truediv"
8
+ STR_APPEND_SCRIPT_NAME = "str_append"
9
+ STR_MUL_SCRIPT_NAME = "str_mul"
10
+ DATETIME_ADD_SCRIPT_NAME = "datetime_add"
@@ -0,0 +1,34 @@
1
+ from functools import lru_cache
2
+ from importlib import resources
3
+
4
+
5
+ VARIANTS = {
6
+ "redis": {
7
+ "EXTRACT_ARRAY": "local arr = cjson.decode(arr_json)[1]",
8
+ "EXTRACT_VALUE": "local value = tonumber(cjson.decode(current_json)[1])",
9
+ "EXTRACT_STR": "local value = cjson.decode(current_json)[1]",
10
+ "EXTRACT_DATETIME": "local value = cjson.decode(current_json)[1]",
11
+ },
12
+ "fakeredis": {
13
+ "EXTRACT_ARRAY": "local arr = cjson.decode(arr_json)",
14
+ "EXTRACT_VALUE": "local value = tonumber(cjson.decode(current_json)[1])",
15
+ "EXTRACT_STR": "local value = cjson.decode(current_json)[1]",
16
+ "EXTRACT_DATETIME": "local value = cjson.decode(current_json)[1]",
17
+ },
18
+ }
19
+
20
+
21
+ @lru_cache(maxsize=None)
22
+ def _load_template(category: str, name: str) -> str:
23
+ package = f"rapyer.scripts.lua.{category}"
24
+ filename = f"{name}.lua"
25
+ return resources.files(package).joinpath(filename).read_text()
26
+
27
+
28
+ def load_script(category: str, name: str, variant: str = "redis") -> str:
29
+ template = _load_template(category, name)
30
+ replacements = VARIANTS[variant]
31
+ result = template
32
+ for placeholder, value in replacements.items():
33
+ result = result.replace(f"--[[{placeholder}]]", value)
34
+ return result
File without changes
@@ -0,0 +1,80 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local delta_seconds = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_DATETIME]]
11
+ local pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"
12
+ local y, m, d, h, mi, s = string.match(value, pattern)
13
+ if not y then return nil end
14
+
15
+ y = tonumber(y)
16
+ m = tonumber(m)
17
+ d = tonumber(d)
18
+ h = tonumber(h)
19
+ mi = tonumber(mi)
20
+ s = tonumber(s)
21
+
22
+ s = s + delta_seconds
23
+
24
+ while s >= 60 do
25
+ s = s - 60
26
+ mi = mi + 1
27
+ end
28
+ while s < 0 do
29
+ s = s + 60
30
+ mi = mi - 1
31
+ end
32
+ while mi >= 60 do
33
+ mi = mi - 60
34
+ h = h + 1
35
+ end
36
+ while mi < 0 do
37
+ mi = mi + 60
38
+ h = h - 1
39
+ end
40
+ while h >= 24 do
41
+ h = h - 24
42
+ d = d + 1
43
+ end
44
+ while h < 0 do
45
+ h = h + 24
46
+ d = d - 1
47
+ end
48
+
49
+ local function is_leap_year(year)
50
+ return (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
51
+ end
52
+
53
+ local function days_in_month(year, month)
54
+ local days = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
55
+ if month == 2 and is_leap_year(year) then
56
+ return 29
57
+ end
58
+ return days[month]
59
+ end
60
+
61
+ while d > days_in_month(y, m) do
62
+ d = d - days_in_month(y, m)
63
+ m = m + 1
64
+ if m > 12 then
65
+ m = 1
66
+ y = y + 1
67
+ end
68
+ end
69
+ while d < 1 do
70
+ m = m - 1
71
+ if m < 1 then
72
+ m = 12
73
+ y = y - 1
74
+ end
75
+ d = d + days_in_month(y, m)
76
+ end
77
+
78
+ local new_date = string.format("%04d-%02d-%02dT%02d:%02d:%02d", y, m, d, h, mi, s)
79
+ redis.call('JSON.SET', key, path, cjson.encode(new_date))
80
+ return new_date
File without changes
@@ -0,0 +1,36 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local start_idx = tonumber(ARGV[2])
4
+ local end_idx = tonumber(ARGV[3])
5
+
6
+ local arr_json = redis.call('JSON.GET', key, path)
7
+ if not arr_json or arr_json == 'null' then
8
+ return nil
9
+ end
10
+
11
+ --[[EXTRACT_ARRAY]]
12
+ local n = #arr
13
+
14
+ if start_idx < 0 then start_idx = n + start_idx end
15
+ if end_idx < 0 then end_idx = n + end_idx end
16
+ if start_idx < 0 then start_idx = 0 end
17
+ if end_idx < 0 then end_idx = 0 end
18
+ if end_idx > n then end_idx = n end
19
+ if start_idx >= n or start_idx >= end_idx then return true end
20
+
21
+ local new_arr = {}
22
+ local j = 1
23
+
24
+ for i = 1, start_idx do
25
+ new_arr[j] = arr[i]
26
+ j = j + 1
27
+ end
28
+
29
+ for i = end_idx + 1, n do
30
+ new_arr[j] = arr[i]
31
+ j = j + 1
32
+ end
33
+
34
+ local encoded = j == 1 and '[]' or cjson.encode(new_arr)
35
+ redis.call('JSON.SET', key, path, encoded)
36
+ return true
File without changes
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = math.floor(value / operand)
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = value % operand
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = value * operand
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = math.floor(value ^ operand)
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = value ^ operand
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local operand = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_VALUE]]
11
+ local result = value / operand
12
+ redis.call('JSON.SET', key, path, result)
13
+ return result
File without changes
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local suffix = ARGV[2]
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_STR]]
11
+ local result = value .. suffix
12
+ redis.call('JSON.SET', key, path, cjson.encode(result))
13
+ return result
@@ -0,0 +1,13 @@
1
+ local key = KEYS[1]
2
+ local path = ARGV[1]
3
+ local count = tonumber(ARGV[2])
4
+
5
+ local current_json = redis.call('JSON.GET', key, path)
6
+ if not current_json or current_json == 'null' then
7
+ return nil
8
+ end
9
+
10
+ --[[EXTRACT_STR]]
11
+ local result = string.rep(value, count)
12
+ redis.call('JSON.SET', key, path, cjson.encode(result))
13
+ return result