1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148 | """Marshmallow number fields."""
import re
from decimal import Decimal
from typing import Any
from loguru import logger
from marshmallow import fields
from typing_extensions import override
from comicbox.empty import is_empty
from comicbox.fields.fields import (
StringField,
TrapExceptionsMeta,
half_replace,
)
NumberType = int | float | Decimal
PAGE_COUNT_KEY = "page_count"
class RangedNumberMixin(metaclass=TrapExceptionsMeta):
"""Number range methods."""
ZERO_FILL: int = 0
def _set_range(
self, minimum: NumberType | None, maximum: NumberType | None
) -> None:
self._min = minimum
self._max = maximum
@classmethod
def parse_str(cls, num_obj) -> NumberType | None:
"""Parse numerical string method."""
raise NotImplementedError
def _deserialize_pre(self, value: Any) -> NumberType | None:
result = self.parse_str(value) if isinstance(value, str) else value
if is_empty(result):
return None
return result
def _deserialize_post(self, value: NumberType | None) -> NumberType | None:
result: NumberType | None = value
if result is not None:
old_result = result
if self._min is not None:
result = max(result, self._min)
if self._max is not None:
result = min(result, self._max)
if old_result != result:
logger.warning(f"Coerced {old_result} to {result}")
return result
def _serialize_post(self, result: Any) -> Any:
"""Zero pad as_string numbers for sorting."""
if self.as_string and self.ZERO_FILL and result is not None: # pyright: ignore[reportAttributeAccessIssue], # ty: ignore[unresolved-attribute]
result = str(result).zfill(self.ZERO_FILL)
return result
class IntegerField(fields.Integer, RangedNumberMixin):
"""Durable integer field."""
_FIRST_NUMBER_MATCHER = re.compile(r"\d+")
@override
@classmethod
def parse_str(cls, num_obj: str) -> int | None:
"""Parse the first number out of volume."""
num_str: str | None = StringField().deserialize(num_obj)
if not num_str:
return None
match: re.Match | None = cls._FIRST_NUMBER_MATCHER.search(num_str)
if match:
return int(match.group())
return None
def __init__(
self,
*args: Any,
minimum: int | None = None,
maximum: int | None = None,
**kwargs: Any,
) -> None:
"""Set the min and max value."""
super().__init__(*args, **kwargs)
self._set_range(minimum, maximum)
@override
def _deserialize(self, value: int | str, *args: Any, **kwargs: Any) -> int | None: # pyright: ignore[reportIncompatibleMethodOverride], # ty: ignore[invalid-method-override]
pre_value = self._deserialize_pre(value)
result = super()._deserialize(pre_value, *args, **kwargs)
return self._deserialize_post(result) # pyright: ignore[reportReturnType], # ty: ignore[invalid-return-type]
@override
def _serialize(self, *args: Any, **kwargs: Any) -> int:
result = super()._serialize(*args, **kwargs)
return self._serialize_post(result)
class DecimalField(fields.Decimal, RangedNumberMixin):
"""Durable Decimal field that parses some fractions."""
DECIMAL_MATCHER = re.compile(r"\d*\.?\d+")
@override
@classmethod
def parse_str(cls, num_obj: str) -> Decimal | None:
"""Fix half glyphs."""
num_str: str | None = StringField().deserialize(num_obj)
if not num_str:
return None
num_str = num_str.replace(" ", "")
num_str = half_replace(num_str)
match = cls.DECIMAL_MATCHER.search(num_str)
if match:
return Decimal(match.group())
return None
def __init__(
self,
*args: Any,
minimum: Decimal | None = None,
maximum: Decimal | None = None,
**kwargs: Any,
) -> None:
"""Set the min and max value."""
super().__init__(*args, **kwargs)
self._set_range(minimum, maximum)
@override
def _deserialize( # pyright: ignore[reportIncompatibleMethodOverride] # ty: ignore[invalid-method-override]
self, value: Any, *args: Any, **kwargs: Any
) -> Decimal | None:
pre_value = self._deserialize_pre(value)
result = super()._deserialize(pre_value, *args, **kwargs)
return self._deserialize_post(result) # pyright: ignore[reportReturnType], # ty: ignore[invalid-return-type]
@override
def _serialize(self, *args: Any, **kwargs: Any) -> Any:
result = super()._serialize(*args, **kwargs)
return self._serialize_post(result)
class BooleanField(fields.Boolean, metaclass=TrapExceptionsMeta):
"""A liberally parsed boolean field."""
|