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.
- {rapyer-1.1.6 → rapyer-1.1.7}/PKG-INFO +1 -1
- {rapyer-1.1.6 → rapyer-1.1.7}/pyproject.toml +2 -1
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/__init__.py +2 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/base.py +101 -29
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/config.py +2 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/errors/__init__.py +6 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/errors/base.py +8 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/expression.py +1 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/init.py +10 -3
- rapyer-1.1.7/rapyer/scripts/__init__.py +41 -0
- rapyer-1.1.7/rapyer/scripts/constants.py +10 -0
- rapyer-1.1.7/rapyer/scripts/loader.py +34 -0
- rapyer-1.1.7/rapyer/scripts/lua/datetime/__init__.py +0 -0
- rapyer-1.1.7/rapyer/scripts/lua/datetime/add.lua +80 -0
- rapyer-1.1.7/rapyer/scripts/lua/list/__init__.py +0 -0
- rapyer-1.1.7/rapyer/scripts/lua/list/remove_range.lua +36 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/__init__.py +0 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/floordiv.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/mod.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/mul.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/pow.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/pow_float.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/numeric/truediv.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/string/__init__.py +0 -0
- rapyer-1.1.7/rapyer/scripts/lua/string/append.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/lua/string/mul.lua +13 -0
- rapyer-1.1.7/rapyer/scripts/registry.py +65 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/base.py +8 -6
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/byte.py +1 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/convert.py +1 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/datetime.py +58 -2
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/dct.py +13 -20
- rapyer-1.1.7/rapyer/types/float.py +100 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/integer.py +31 -13
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/lst.py +27 -21
- rapyer-1.1.7/rapyer/types/string.py +41 -0
- rapyer-1.1.7/rapyer/utils/__init__.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/redis.py +0 -7
- rapyer-1.1.6/rapyer/scripts.py +0 -86
- rapyer-1.1.6/rapyer/types/float.py +0 -51
- rapyer-1.1.6/rapyer/types/string.py +0 -20
- {rapyer-1.1.6 → rapyer-1.1.7}/README.md +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/context.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/__init__.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/index.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/key.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/fields/safe_load.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/links.py +0 -0
- {rapyer-1.1.6/rapyer/utils → rapyer-1.1.7/rapyer/scripts/lua}/__init__.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/__init__.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/types/init.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/typing_support.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/annotation.py +0 -0
- {rapyer-1.1.6 → rapyer-1.1.7}/rapyer/utils/fields.py +0 -0
- {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.
|
|
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.
|
|
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]
|
|
@@ -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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
431
|
-
cls.Meta.redis
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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.
|
|
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,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
|