rapyer 1.1.5__tar.gz → 1.1.6__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.5 → rapyer-1.1.6}/PKG-INFO +1 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/pyproject.toml +5 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/base.py +69 -35
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/expression.py +0 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/init.py +12 -3
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/base.py +2 -3
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/byte.py +0 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/convert.py +0 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/datetime.py +1 -2
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/dct.py +0 -1
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/float.py +1 -2
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/integer.py +2 -3
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/lst.py +1 -2
- {rapyer-1.1.5 → rapyer-1.1.6}/README.md +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/__init__.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/config.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/context.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/errors/__init__.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/errors/base.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/__init__.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/index.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/key.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/safe_load.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/links.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/scripts.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/__init__.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/init.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/string.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/typing_support.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/__init__.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/annotation.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/fields.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/pythonic.py +0 -0
- {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/redis.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.6
|
|
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.6"
|
|
8
8
|
description = "Pydantic models with Redis as the backend"
|
|
9
9
|
authors = [{name = "YedidyaHKfir", email = "yedidyakfir@gmail.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -75,6 +75,10 @@ packages = [{include = "rapyer"}]
|
|
|
75
75
|
black = "^25.9.0"
|
|
76
76
|
mypy = "^1.0.0"
|
|
77
77
|
|
|
78
|
+
[tool.bandit]
|
|
79
|
+
exclude_dirs = ["tests"]
|
|
80
|
+
skips = ["B101"] # assert_used - valid in test files
|
|
81
|
+
|
|
78
82
|
[tool.coverage.run]
|
|
79
83
|
source = ["rapyer"]
|
|
80
84
|
omit = ["*/tests/*", "*/test_*"]
|
|
@@ -15,13 +15,9 @@ from pydantic import (
|
|
|
15
15
|
model_validator,
|
|
16
16
|
field_serializer,
|
|
17
17
|
field_validator,
|
|
18
|
+
ValidationError,
|
|
18
19
|
)
|
|
19
20
|
from pydantic_core.core_schema import FieldSerializationInfo, ValidationInfo
|
|
20
|
-
from redis.commands.search.index_definition import IndexDefinition, IndexType
|
|
21
|
-
from redis.commands.search.query import Query
|
|
22
|
-
from redis.exceptions import NoScriptError
|
|
23
|
-
from typing_extensions import deprecated
|
|
24
|
-
|
|
25
21
|
from rapyer.config import RedisConfig
|
|
26
22
|
from rapyer.context import _context_var, _context_xx_pipe
|
|
27
23
|
from rapyer.errors.base import (
|
|
@@ -56,6 +52,10 @@ from rapyer.utils.redis import (
|
|
|
56
52
|
update_keys_in_pipeline,
|
|
57
53
|
refresh_ttl_if_needed,
|
|
58
54
|
)
|
|
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
59
|
|
|
60
60
|
logger = logging.getLogger("rapyer")
|
|
61
61
|
|
|
@@ -169,6 +169,9 @@ class AtomicRedisModel(BaseModel):
|
|
|
169
169
|
field_path = self.field_path
|
|
170
170
|
return f"${field_path}" if field_path else "$"
|
|
171
171
|
|
|
172
|
+
def should_refresh(self):
|
|
173
|
+
return self.Meta.refresh_ttl and self.Meta.ttl is not None
|
|
174
|
+
|
|
172
175
|
@classmethod
|
|
173
176
|
def redis_schema(cls, redis_name: str = ""):
|
|
174
177
|
fields = []
|
|
@@ -399,6 +402,11 @@ class AtomicRedisModel(BaseModel):
|
|
|
399
402
|
self.Meta.redis, self.key, self.Meta.ttl, self.Meta.refresh_ttl
|
|
400
403
|
)
|
|
401
404
|
|
|
405
|
+
async def aset_ttl(self, ttl: int) -> None:
|
|
406
|
+
if self.is_inner_model():
|
|
407
|
+
raise RuntimeError("Can only set TTL from top level model")
|
|
408
|
+
await self.Meta.redis.expire(self.key, ttl)
|
|
409
|
+
|
|
402
410
|
@classmethod
|
|
403
411
|
@deprecated(
|
|
404
412
|
"get() classmethod is deprecated and will be removed in rapyer 1.2.0, use aget instead"
|
|
@@ -446,49 +454,66 @@ class AtomicRedisModel(BaseModel):
|
|
|
446
454
|
return instance
|
|
447
455
|
|
|
448
456
|
@classmethod
|
|
449
|
-
async def afind(cls, *
|
|
450
|
-
#
|
|
451
|
-
if
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
457
|
+
async def afind(cls, *args):
|
|
458
|
+
# Separate keys (str) from expressions (Expression)
|
|
459
|
+
provided_keys = [arg for arg in args if isinstance(arg, str)]
|
|
460
|
+
expressions = [arg for arg in args if isinstance(arg, Expression)]
|
|
461
|
+
|
|
462
|
+
if provided_keys and expressions:
|
|
463
|
+
logger.warning(
|
|
464
|
+
"afind called with both keys and expressions; expressions ignored"
|
|
465
|
+
)
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
467
|
+
if provided_keys:
|
|
468
|
+
# Case 1: Extract by keys
|
|
469
|
+
targeted_keys = [
|
|
470
|
+
k if ":" in k else f"{cls.class_key_initials()}:{k}"
|
|
471
|
+
for k in provided_keys
|
|
472
|
+
]
|
|
473
|
+
elif expressions:
|
|
474
|
+
# Case 2: Extract by expressions
|
|
460
475
|
combined_expression = functools.reduce(lambda a, b: a & b, expressions)
|
|
461
476
|
query_string = combined_expression.create_filter()
|
|
462
|
-
|
|
463
|
-
# Create a Query object
|
|
464
477
|
query = Query(query_string).no_content()
|
|
465
|
-
|
|
466
|
-
# Try to search using the index
|
|
467
478
|
index_name = cls.index_name()
|
|
468
479
|
search_result = await cls.Meta.redis.ft(index_name).search(query)
|
|
469
|
-
|
|
470
480
|
if not search_result.docs:
|
|
471
481
|
return []
|
|
482
|
+
targeted_keys = [doc.id for doc in search_result.docs]
|
|
483
|
+
else:
|
|
484
|
+
# Case 3: Extract all
|
|
485
|
+
targeted_keys = await cls.afind_keys()
|
|
472
486
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
# Fetch the actual documents
|
|
477
|
-
models = await cls.Meta.redis.json().mget(keys=keys, path="$")
|
|
487
|
+
if not targeted_keys:
|
|
488
|
+
return []
|
|
478
489
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
for key in keys:
|
|
482
|
-
pipe.expire(key, cls.Meta.ttl)
|
|
483
|
-
await pipe.execute()
|
|
490
|
+
# Fetch the actual documents
|
|
491
|
+
models = await cls.Meta.redis.json().mget(keys=targeted_keys, path="$")
|
|
484
492
|
|
|
485
493
|
instances = []
|
|
486
|
-
for model, key in zip(models,
|
|
494
|
+
for model, key in zip(models, targeted_keys):
|
|
495
|
+
if model is None:
|
|
496
|
+
continue
|
|
487
497
|
context = {REDIS_DUMP_FLAG_NAME: True, FAILED_FIELDS_KEY: set()}
|
|
488
|
-
|
|
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
|
+
)
|
|
506
|
+
continue
|
|
489
507
|
model.key = key
|
|
490
508
|
model._failed_fields = context.get(FAILED_FIELDS_KEY, set())
|
|
491
509
|
instances.append(model)
|
|
510
|
+
|
|
511
|
+
if cls.Meta.ttl is not None and cls.Meta.refresh_ttl:
|
|
512
|
+
async with cls.Meta.redis.pipeline() as pipe:
|
|
513
|
+
for model in instances:
|
|
514
|
+
pipe.expire(model.key, cls.Meta.ttl)
|
|
515
|
+
await pipe.execute()
|
|
516
|
+
|
|
492
517
|
return instances
|
|
493
518
|
|
|
494
519
|
@classmethod
|
|
@@ -616,9 +641,21 @@ class AtomicRedisModel(BaseModel):
|
|
|
616
641
|
noscript_on_retry = False
|
|
617
642
|
|
|
618
643
|
try:
|
|
644
|
+
if self.should_refresh():
|
|
645
|
+
pipe.expire(self.key, self.Meta.ttl)
|
|
619
646
|
await pipe.execute()
|
|
620
647
|
except NoScriptError:
|
|
621
648
|
noscript_on_first_attempt = True
|
|
649
|
+
except ResponseError as exc:
|
|
650
|
+
if ignore_if_deleted:
|
|
651
|
+
logger.warning(
|
|
652
|
+
"Swallowed ResponseError during pipeline.execute() with "
|
|
653
|
+
"ignore_if_deleted=True for key %r: %s",
|
|
654
|
+
getattr(self, "key", None),
|
|
655
|
+
exc,
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
raise
|
|
622
659
|
|
|
623
660
|
if noscript_on_first_attempt:
|
|
624
661
|
await handle_noscript_error(self.Meta.redis)
|
|
@@ -642,9 +679,6 @@ class AtomicRedisModel(BaseModel):
|
|
|
642
679
|
"This indicates a server-side problem with Redis."
|
|
643
680
|
)
|
|
644
681
|
|
|
645
|
-
await refresh_ttl_if_needed(
|
|
646
|
-
self.Meta.redis, self.key, self.Meta.ttl, self.Meta.refresh_ttl
|
|
647
|
-
)
|
|
648
682
|
_context_var.set(None)
|
|
649
683
|
_context_xx_pipe.set(False)
|
|
650
684
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
from redis import ResponseError
|
|
3
|
-
from redis.asyncio.client import Redis
|
|
1
|
+
import logging
|
|
4
2
|
|
|
3
|
+
import redis.asyncio as redis_async
|
|
5
4
|
from rapyer.base import REDIS_MODELS
|
|
6
5
|
from rapyer.scripts import register_scripts
|
|
6
|
+
from redis import ResponseError
|
|
7
|
+
from redis.asyncio.client import Redis
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
async def init_rapyer(
|
|
@@ -11,7 +12,15 @@ async def init_rapyer(
|
|
|
11
12
|
ttl: int = None,
|
|
12
13
|
override_old_idx: bool = True,
|
|
13
14
|
prefer_normal_json_dump: bool = None,
|
|
15
|
+
logger: logging.Logger = None,
|
|
14
16
|
):
|
|
17
|
+
if logger is not None:
|
|
18
|
+
rapyer_logger = logging.getLogger("rapyer")
|
|
19
|
+
rapyer_logger.setLevel(logger.level)
|
|
20
|
+
rapyer_logger.handlers.clear()
|
|
21
|
+
for handler in logger.handlers:
|
|
22
|
+
rapyer_logger.addHandler(handler)
|
|
23
|
+
|
|
15
24
|
if isinstance(redis, str):
|
|
16
25
|
redis = redis_async.from_url(redis, decode_responses=True, max_connections=20)
|
|
17
26
|
|
|
@@ -8,13 +8,12 @@ from typing import get_args, Any, TypeVar, Generic
|
|
|
8
8
|
from pydantic import GetCoreSchemaHandler, TypeAdapter
|
|
9
9
|
from pydantic_core import core_schema
|
|
10
10
|
from pydantic_core.core_schema import ValidationInfo, CoreSchema, SerializationInfo
|
|
11
|
-
from redis.commands.search.field import TextField
|
|
12
|
-
from typing_extensions import deprecated
|
|
13
|
-
|
|
14
11
|
from rapyer.context import _context_var
|
|
15
12
|
from rapyer.errors.base import CantSerializeRedisValueError
|
|
16
13
|
from rapyer.typing_support import Self
|
|
17
14
|
from rapyer.utils.redis import refresh_ttl_if_needed
|
|
15
|
+
from redis.commands.search.field import TextField
|
|
16
|
+
from typing_extensions import deprecated
|
|
18
17
|
|
|
19
18
|
logger = logging.getLogger("rapyer")
|
|
20
19
|
|
|
@@ -3,9 +3,8 @@ from typing import TYPE_CHECKING
|
|
|
3
3
|
|
|
4
4
|
from pydantic_core import core_schema
|
|
5
5
|
from pydantic_core.core_schema import ValidationInfo, SerializationInfo
|
|
6
|
-
from redis.commands.search.field import NumericField
|
|
7
|
-
|
|
8
6
|
from rapyer.types.base import RedisType, REDIS_DUMP_FLAG_NAME
|
|
7
|
+
from redis.commands.search.field import NumericField
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class RedisDatetime(datetime, RedisType):
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import TypeAlias, TYPE_CHECKING
|
|
2
2
|
|
|
3
|
-
from redis.commands.search.field import NumericField
|
|
4
|
-
|
|
5
3
|
from rapyer.types.base import RedisType
|
|
6
4
|
from rapyer.utils.redis import refresh_ttl_if_needed
|
|
5
|
+
from redis.commands.search.field import NumericField
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class RedisFloat(float, RedisType):
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
from typing import TypeAlias, TYPE_CHECKING
|
|
2
2
|
|
|
3
|
-
from redis.commands.search.field import NumericField
|
|
4
|
-
from typing_extensions import deprecated
|
|
5
|
-
|
|
6
3
|
from rapyer.types.base import RedisType
|
|
7
4
|
from rapyer.utils.redis import refresh_ttl_if_needed
|
|
5
|
+
from redis.commands.search.field import NumericField
|
|
6
|
+
from typing_extensions import deprecated
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class RedisInt(int, RedisType):
|
|
@@ -4,8 +4,6 @@ from typing import TypeVar, TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from pydantic_core import core_schema
|
|
6
6
|
from pydantic_core.core_schema import ValidationInfo, SerializationInfo
|
|
7
|
-
from typing_extensions import TypeAlias
|
|
8
|
-
|
|
9
7
|
from rapyer.scripts import run_sha, REMOVE_RANGE_SCRIPT_NAME
|
|
10
8
|
from rapyer.types.base import (
|
|
11
9
|
GenericRedisType,
|
|
@@ -14,6 +12,7 @@ from rapyer.types.base import (
|
|
|
14
12
|
SKIP_SENTINEL,
|
|
15
13
|
)
|
|
16
14
|
from rapyer.utils.redis import refresh_ttl_if_needed
|
|
15
|
+
from typing_extensions import TypeAlias
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger("rapyer")
|
|
19
18
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|