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.
Files changed (34) hide show
  1. {rapyer-1.1.5 → rapyer-1.1.6}/PKG-INFO +1 -1
  2. {rapyer-1.1.5 → rapyer-1.1.6}/pyproject.toml +5 -1
  3. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/base.py +69 -35
  4. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/expression.py +0 -1
  5. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/init.py +12 -3
  6. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/base.py +2 -3
  7. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/byte.py +0 -1
  8. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/convert.py +0 -1
  9. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/datetime.py +1 -2
  10. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/dct.py +0 -1
  11. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/float.py +1 -2
  12. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/integer.py +2 -3
  13. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/lst.py +1 -2
  14. {rapyer-1.1.5 → rapyer-1.1.6}/README.md +0 -0
  15. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/__init__.py +0 -0
  16. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/config.py +0 -0
  17. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/context.py +0 -0
  18. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/errors/__init__.py +0 -0
  19. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/errors/base.py +0 -0
  20. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/__init__.py +0 -0
  21. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/index.py +0 -0
  22. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/key.py +0 -0
  23. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/fields/safe_load.py +0 -0
  24. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/links.py +0 -0
  25. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/scripts.py +0 -0
  26. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/__init__.py +0 -0
  27. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/init.py +0 -0
  28. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/types/string.py +0 -0
  29. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/typing_support.py +0 -0
  30. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/__init__.py +0 -0
  31. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/annotation.py +0 -0
  32. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/fields.py +0 -0
  33. {rapyer-1.1.5 → rapyer-1.1.6}/rapyer/utils/pythonic.py +0 -0
  34. {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.5
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.5"
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, *expressions):
450
- # Original behavior when no expressions provided - return all
451
- if not expressions:
452
- keys = await cls.afind_keys()
453
- if not keys:
454
- return []
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
- models = await cls.Meta.redis.json().mget(keys=keys, path="$")
457
- else:
458
- # With expressions - use Redis Search
459
- # Combine all expressions with & operator
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
- # Get the keys from search results
474
- keys = [doc.id for doc in search_result.docs]
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
- if cls.Meta.ttl is not None and cls.Meta.refresh_ttl:
480
- async with cls.Meta.redis.pipeline() as pipe:
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, keys):
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
- model = cls.model_validate(model[0], context=context)
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,7 +1,6 @@
1
1
  from typing import Any
2
2
 
3
3
  from pydantic import TypeAdapter
4
-
5
4
  from rapyer.errors import BadFilterError
6
5
  from rapyer.types.base import REDIS_DUMP_FLAG_NAME
7
6
  from rapyer.typing_support import Unpack
@@ -1,9 +1,10 @@
1
- import redis.asyncio as redis_async
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
 
@@ -2,7 +2,6 @@ from typing import TypeAlias, TYPE_CHECKING
2
2
 
3
3
  from pydantic_core import core_schema
4
4
  from pydantic_core.core_schema import ValidationInfo, SerializationInfo
5
-
6
5
  from rapyer.types.base import RedisType, REDIS_DUMP_FLAG_NAME
7
6
 
8
7
 
@@ -1,7 +1,6 @@
1
1
  from typing import get_origin
2
2
 
3
3
  from pydantic import BaseModel, PrivateAttr, TypeAdapter
4
-
5
4
  from rapyer.types.base import RedisType
6
5
  from rapyer.utils.annotation import TypeConverter, DYNAMIC_CLASS_DOC
7
6
  from rapyer.utils.pythonic import safe_issubclass
@@ -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,7 +1,6 @@
1
1
  from typing import TypeVar, Generic, get_args, Any, TypeAlias, TYPE_CHECKING
2
2
 
3
3
  from pydantic_core import core_schema
4
-
5
4
  from rapyer.types.base import (
6
5
  GenericRedisType,
7
6
  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