Skip to content

Commit b4d6405

Browse files
committed
Add prefix suffix contains email
1 parent 3643301 commit b4d6405

File tree

8 files changed

+179
-4
lines changed

8 files changed

+179
-4
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,36 @@ school = School(school_code="school_1", school_name="School 1")
4545
school = School(school_code="scho", school_name="School 1") # ValueError: 'scho' does not match regex pattern '^school.*$'
4646
```
4747

48+
- `PrefixStr`: Validate string attributes with a prefix.
49+
50+
- `SuffixStr`: Validate string attributes with a suffix.
51+
52+
- `ContainsStr`: Validate string attributes with a substring.
53+
54+
- `EmailStr`: Validate string attributes with an email pattern.
55+
56+
#### TBD
57+
58+
- `NotContainsStr`: Validate string attributes with a not contains pattern.
59+
60+
- `LengthStr`: Validate string attributes with a length pattern.
61+
62+
- `NumericStr`: Validate string attributes with a numeric pattern.
63+
64+
- `AlphabeticalStr`: Validate string attributes with an alphabetical pattern.
65+
66+
- `AlphanumericStr`: Validate string attributes with an alphanumeric pattern.
67+
68+
- `UrlStr`: Validate string attributes with a url pattern.
69+
70+
- `PhoneStr`: Validate string attributes with a phone pattern.
71+
72+
- `CreditCardStr`: Validate string attributes with a credit card pattern.
73+
74+
- `LowercaseStr`: Validate string attributes with a lowercase pattern.
75+
76+
- `UppercaseStr`: Validate string attributes with an uppercase pattern.
77+
4878
### Numeric
4979

5080
- `RangeInt`: Set boundaries for integer attributes.
@@ -97,6 +127,18 @@ transaction = Transaction(amount=Decimal("0.005"), amount2=Decimal("5.0")) # Val
97127
transaction = Transaction(amount=Decimal("150.75"), amount2=Decimal("15")) # ValueError: 15 is not in range [1, 10]
98128
```
99129

130+
#### TBD
131+
132+
- `Positive*`: Validate integer | float | Decimal attributes with a positive pattern.
133+
134+
- `Negative*`: Validate integer | float | Decimal attributes with a negative pattern.
135+
136+
- `Odd*`: Validate integer | float | Decimal attributes with an odd pattern.
137+
138+
- `Even*`: Validate integer | float | Decimal attributes with an even pattern.
139+
140+
- `Divisible*`: Validate integer | float | Decimal attributes with a divisible pattern.
141+
100142
### ETC
101143

102144
... More types will be added soon.

extended_type/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
from .contains_str import ContainsStr as ContainsStr
2+
from .prefix_str import PrefixStr as PrefixStr
13
from .pydantic_compatibility import PYDANTIC_AVAILABLE as _PYDANTIC_AVAILABLE
24
from .range_decimal import RangeDecimal as RangeDecimal
35
from .range_float import RangeFloat as RangeFloat
46
from .range_int import RangeInt as RangeInt
7+
from .regex_str import EmailStr as EmailStr
58
from .regex_str import RegexStr as RegexStr
9+
from .suffix_str import SuffixStr as SuffixStr
610
from .type_extended import TypeExtended as TypeExtended
711
from .type_extended import type_extended as type_extended
812

extended_type/contains_str.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Generic, TypeVar, cast
4+
5+
from typing_extensions import get_args
6+
7+
T = TypeVar("T", bound=str)
8+
9+
10+
class ContainsStrType(str, Generic[T]):
11+
@staticmethod
12+
def extract_substring(annotation: type[Any]) -> str:
13+
# extracts ContainsStr[typing.Literal[SUBSTRING]], <class 'str'>
14+
union_args = cast(tuple[str], get_args(annotation))
15+
16+
# extracts typing.Literal[SUBSTRING]
17+
extended_str_args = get_args(union_args[0])
18+
19+
# extracts (SUBSTRING,)
20+
literal_substring = get_args(extended_str_args[0])
21+
22+
# extracts SUBSTRING
23+
substring = literal_substring[0]
24+
return substring
25+
26+
@staticmethod
27+
def validate(substring: str, value: str) -> str:
28+
if substring not in value:
29+
raise ValueError(f"`{value}` does not contain `{substring}`")
30+
return value
31+
32+
33+
SUBSTRING = TypeVar("SUBSTRING", bound=str)
34+
ContainsStr = ContainsStrType[SUBSTRING] | str

extended_type/extended_type_validator.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
from typing_extensions import get_args, get_origin
44

5+
from extended_type.contains_str import ContainsStrType
6+
from extended_type.prefix_str import PrefixStrType
57
from extended_type.range_decimal import RangeDecimalType
68
from extended_type.range_float import RangeFloatType
79
from extended_type.range_int import RangeIntType
810
from extended_type.range_numeric import NUMERIC_TYPE, RangeNumericBase
911
from extended_type.regex_str import RegexStrType
12+
from extended_type.suffix_str import SuffixStrType
1013

1114

1215
class ExtendedTypeValidator:
@@ -18,6 +21,9 @@ def __init__(
1821
):
1922
self.validators = validators or {
2023
RegexStrType: self._validate_regex_str,
24+
PrefixStrType: self._validate_prefix_str,
25+
SuffixStrType: self._validate_suffix_str,
26+
ContainsStrType: self._validate_contains_str,
2127
RangeIntType: lambda value, annotation: self._validate_number(
2228
cast(RangeNumericBase, RangeIntType), value, annotation
2329
),
@@ -54,3 +60,19 @@ def _validate_number(
5460
def _validate_regex_str(self, value: str, annotation: type[str]):
5561
pattern = RegexStrType.extract_pattern(annotation)
5662
RegexStrType.validate(pattern, value)
63+
64+
def _validate_email_str(self, value: str, _annotation: type[str]):
65+
EMAIL_PATTERN = "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"
66+
RegexStrType.validate(EMAIL_PATTERN, value)
67+
68+
def _validate_prefix_str(self, value: str, annotation: type[str]):
69+
prefix = PrefixStrType.extract_prefix(annotation)
70+
PrefixStrType.validate(prefix, value)
71+
72+
def _validate_suffix_str(self, value: str, annotation: type[str]):
73+
subfix = SuffixStrType.extract_suffix(annotation)
74+
SuffixStrType.validate(subfix, value)
75+
76+
def _validate_contains_str(self, value: str, annotation: type[str]):
77+
subset = ContainsStrType.extract_substring(annotation)
78+
ContainsStrType.validate(subset, value)

extended_type/prefix_str.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Generic, TypeVar, cast
4+
5+
from typing_extensions import get_args
6+
7+
T = TypeVar("T", bound=str)
8+
9+
10+
class PrefixStrType(str, Generic[T]):
11+
@staticmethod
12+
def extract_prefix(annotation: type[Any]) -> str:
13+
# extracts PrefixStr[typing.Literal[PREFIX]], <class 'str'>
14+
union_args = cast(tuple[str], get_args(annotation))
15+
16+
# extracts typing.Literal[PREFIX]
17+
extended_str_args = get_args(union_args[0])
18+
19+
# extracts (PREFIX,)
20+
literal_prefix = get_args(extended_str_args[0])
21+
22+
# extracts PREFIX
23+
prefix = literal_prefix[0]
24+
return prefix
25+
26+
@staticmethod
27+
def validate(prefix: str, value: str) -> str:
28+
if not value.startswith(prefix):
29+
raise ValueError(f"`{value}` does not start with `{prefix}`")
30+
return value
31+
32+
33+
PREFIX = TypeVar("PREFIX", bound=str)
34+
PrefixStr = PrefixStrType[PREFIX] | str

extended_type/regex_str.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import re
4-
from typing import Any, Generic, TypeVar, cast
4+
from typing import Any, Generic, Literal, TypeVar, cast
55

66
from typing_extensions import get_args
77

@@ -33,3 +33,8 @@ def validate(pattern: str, value: str) -> str:
3333

3434
PATTERN = TypeVar("PATTERN", bound=str)
3535
RegexStr = RegexStrType[PATTERN] | str
36+
37+
38+
EmailStr = (
39+
RegexStrType[Literal["^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"]] | str
40+
)

extended_type/suffix_str.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Generic, TypeVar, cast
4+
5+
from typing_extensions import get_args
6+
7+
T = TypeVar("T", bound=str)
8+
9+
10+
class SuffixStrType(str, Generic[T]):
11+
@staticmethod
12+
def extract_suffix(annotation: type[Any]) -> str:
13+
# extracts SuffixStr[typing.Literal[SUFFIX]], <class 'str'>
14+
union_args = cast(tuple[str], get_args(annotation))
15+
16+
# extracts typing.Literal[SUFFIX]
17+
extended_str_args = get_args(union_args[0])
18+
19+
# extracts (SUFFIX,)
20+
literal_suffix = get_args(extended_str_args[0])
21+
22+
# extracts SUFFIX
23+
suffix = literal_suffix[0]
24+
return suffix
25+
26+
@staticmethod
27+
def validate(suffix: str, value: str) -> str:
28+
if not value.endswith(suffix):
29+
raise ValueError(f"`{value}` does not end with `{suffix}`")
30+
return value
31+
32+
33+
SUFFIX = TypeVar("SUFFIX", bound=str)
34+
SuffixStr = SuffixStrType[SUFFIX] | str

extended_type/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33

44
def class_wrapper(cls: Type, target: Type) -> Type:
5-
class NewClass(cls, target):
5+
class WrappedTypeExtendedClass(cls, target):
66
def __init__(self, *args, **kwargs):
77
cls.__init__(self, *args, **kwargs)
88
target.__init__(self, *args, **kwargs)
99

10-
NewClass.__name__ = cls.__name__
11-
return NewClass
10+
WrappedTypeExtendedClass.__name__ = cls.__name__
11+
return WrappedTypeExtendedClass

0 commit comments

Comments
 (0)