diff --git a/docs/docs/data_types.md b/docs/docs/data_types.md index 36fec71..0d705ae 100644 --- a/docs/docs/data_types.md +++ b/docs/docs/data_types.md @@ -34,6 +34,10 @@ tailored to specific use cases. #### Supported bson types - `bson.ObjectId` +- `bson.Int64` +- `bson.Decimal128` +- `bson.Regex` +- `bson.Code` #### Custom types diff --git a/mongotoy/db.py b/mongotoy/db.py index a50766a..ed4e001 100644 --- a/mongotoy/db.py +++ b/mongotoy/db.py @@ -463,7 +463,7 @@ def connect( return self._connect(*conn, ping) @sync.proxy - async def migrate( + def migrate( self, document_cls: typing.Type[T], session: 'Session' = None @@ -1340,7 +1340,7 @@ def download_to( self, fs: FsBucket, dest: typing.IO, - revision: int = None + revision: int = 0 ) -> typing.Coroutine[typing.Any, typing.Any, None] | None: return self._download_to(fs, dest, revision) @@ -1348,7 +1348,7 @@ def download_to( def stream( self, fs: FsBucket, - revision: int = None + revision: int = 0 ) -> typing.Union[typing.Coroutine[typing.Any, typing.Any, 'FsObjectStream'], 'FsObjectStream']: return self._stream(fs, revision) diff --git a/mongotoy/mappers.py b/mongotoy/mappers.py index 1b1b176..0420d5c 100644 --- a/mongotoy/mappers.py +++ b/mongotoy/mappers.py @@ -322,9 +322,9 @@ def __init__(self, mapper: Mapper, options: MapperOptions): """ self._mapper = mapper - # SequenceMapper must be at least empty list not an EmptyValue for ReferencedDocumentMapper + # SequenceMapper must be a least an empty sequence not an EmptyValue for ReferencedDocumentMapper if options.default is expressions.EmptyValue and isinstance(self.unwrap(), ReferencedDocumentMapper): - options.default = [] + options.default = self.__bind__() super().__init__(options) @property @@ -603,48 +603,6 @@ def dump_bson(self, value, **options) -> typing.Any: return getattr(value, self.ref_field.name) -class ListMapper(SequenceMapper, bind=list): - """ - Mapper for handling lists. - - Inherits from ManyMapper and specifies 'list' as the binding type. - - """ - - -class TupleMapper(SequenceMapper, bind=tuple): - """ - Mapper for handling tuples. - - Inherits from ManyMapper and specifies 'tuple' as the binding type. - - """ - def validate(self, value) -> typing.Any: - if isinstance(value, list): - value = tuple(value) - return super().validate(value) - - -class SetMapper(SequenceMapper, bind=set): - """ - Mapper for handling sets. - - Inherits from ManyMapper and specifies 'set' as the binding type. - - """ - - def validate(self, value) -> typing.Any: - if isinstance(value, list): - value = set(value) - return super().validate(value) - - def dump_json(self, value, **options) -> typing.Any: - return list(value) - - def dump_bson(self, value, **options) -> typing.Any: - return list(value) - - class StrMapper(Mapper, bind=str): """ Mapper for handling string values. @@ -688,6 +646,74 @@ def validate(self, value) -> typing.Any: return value +class IntMapper(ComparableMapper, bind=int): + """ + Mapper for handling integer values. + """ + + +class FloatMapper(ComparableMapper, bind=float): + """ + Mapper for handling float values. + """ + + +class DecimalMapper(ComparableMapper, bind=decimal.Decimal): + """ + Mapper for handling decimal values. + """ + + def validate(self, value) -> typing.Any: + """ + Validate the decimal value. + + Args: + value: The value to be validated. + + Returns: + Any: The validated value. + + Raises: + TypeError: If validation fails due to incorrect data type. + + """ + if isinstance(value, bson.Decimal128): + value = value.to_decimal() + value = super().validate(value) + # Ensure decimal limits for MongoDB + # https://www.mongodb.com/docs/upcoming/release-notes/3.4/#decimal-type + ctx = decimal.Context(prec=34) + return ctx.create_decimal(value) + + def dump_json(self, value, **options) -> typing.Any: + """ + Dump the decimal value to a JSON-serializable format. + + Args: + value: The value to be dumped. + **options: Additional options. + + Returns: + Any: The dumped value. + + """ + return float(value) + + def dump_bson(self, value, **options) -> typing.Any: + """ + Dump the decimal value to BSON. + + Args: + value: The value to be dumped. + **options: Additional options. + + Returns: + Any: The dumped value. + + """ + return bson.Decimal128(value) + + class BoolMapper(Mapper, bind=bool): """ Mapper for handling boolean values. @@ -751,14 +777,14 @@ def dump_json(self, value, **options) -> typing.Any: return base64.b64encode(value).decode() -class ObjectIdMapper(Mapper, bind=bson.ObjectId): +class UUIDMapper(Mapper, bind=uuid.UUID): """ - Mapper for handling BSON ObjectId values. + Mapper for handling UUID values. """ def validate(self, value) -> typing.Any: """ - Validate the ObjectId value. + Validate the UUID value. Args: value: The value to be validated. @@ -770,13 +796,13 @@ def validate(self, value) -> typing.Any: TypeError: If validation fails due to incorrect data type. """ - if not bson.ObjectId.is_valid(value): - raise TypeError(f'Invalid data type {type(value)}, required is {bson.ObjectId}') - return bson.ObjectId(value) + if not isinstance(value, uuid.UUID): + raise TypeError(f'Invalid data type {type(value)}, required is {uuid.UUID}') + return value def dump_json(self, value, **options) -> typing.Any: """ - Dump the ObjectId value to a JSON-serializable format. + Dump the UUID value to a JSON-serializable format. Args: value: The value to be dumped. @@ -789,14 +815,34 @@ def dump_json(self, value, **options) -> typing.Any: return str(value) -class UUIDMapper(Mapper, bind=uuid.UUID): +class DateTimeMapper(ComparableMapper, bind=datetime.datetime): """ - Mapper for handling UUID values. + Mapper for handling datetime values. + """ + + def dump_json(self, value, **options) -> typing.Any: + """ + Dump the datetime value to a JSON-serializable format. + + Args: + value: The value to be dumped. + **options: Additional options. + + Returns: + Any: The dumped value. + + """ + return value.isoformat() + + +class DateMapper(ComparableMapper, bind=datetime.date): + """ + Mapper for handling date values. """ def validate(self, value) -> typing.Any: """ - Validate the UUID value. + Validate the date value. Args: value: The value to be validated. @@ -808,13 +854,13 @@ def validate(self, value) -> typing.Any: TypeError: If validation fails due to incorrect data type. """ - if not isinstance(value, uuid.UUID): - raise TypeError(f'Invalid data type {type(value)}, required is {uuid.UUID}') - return value + if isinstance(value, datetime.datetime): + value = value.date() + return super().validate(value) def dump_json(self, value, **options) -> typing.Any: """ - Dump the UUID value to a JSON-serializable format. + Dump the date value to a JSON-serializable format. Args: value: The value to be dumped. @@ -824,29 +870,31 @@ def dump_json(self, value, **options) -> typing.Any: Any: The dumped value. """ - return str(value) + return value.isoformat() + def dump_bson(self, value, **options) -> typing.Any: + """ + Dump the date value to BSON. -class IntMapper(ComparableMapper, bind=int): - """ - Mapper for handling integer values. - """ + Args: + value: The value to be dumped. + **options: Additional options. + Returns: + Any: The dumped value. -class FloatMapper(ComparableMapper, bind=float): - """ - Mapper for handling float values. - """ + """ + return datetime.datetime.combine(date=value, time=datetime.time.min) -class DecimalMapper(ComparableMapper, bind=decimal.Decimal): +class TimeMapper(ComparableMapper, bind=datetime.time): """ - Mapper for handling decimal values. + Mapper for handling time values. """ def validate(self, value) -> typing.Any: """ - Validate the decimal value. + Validate the time value. Args: value: The value to be validated. @@ -858,17 +906,13 @@ def validate(self, value) -> typing.Any: TypeError: If validation fails due to incorrect data type. """ - if isinstance(value, bson.Decimal128): - value = value.to_decimal() - value = super().validate(value) - # Ensure decimal limits for MongoDB - # https://www.mongodb.com/docs/upcoming/release-notes/3.4/#decimal-type - ctx = decimal.Context(prec=34) - return ctx.create_decimal(value) + if isinstance(value, datetime.datetime): + value = value.time() + return super().validate(value) def dump_json(self, value, **options) -> typing.Any: """ - Dump the decimal value to a JSON-serializable format. + Dump the time value to a JSON-serializable format. Args: value: The value to be dumped. @@ -878,11 +922,11 @@ def dump_json(self, value, **options) -> typing.Any: Any: The dumped value. """ - return float(value) + return value.isoformat() def dump_bson(self, value, **options) -> typing.Any: """ - Dump the decimal value to BSON. + Dump the time value to BSON. Args: value: The value to be dumped. @@ -892,17 +936,77 @@ def dump_bson(self, value, **options) -> typing.Any: Any: The dumped value. """ - return bson.Decimal128(value) + return datetime.datetime.combine(date=datetime.datetime.min, time=value) -class DateTimeMapper(ComparableMapper, bind=datetime.datetime): +class ListMapper(SequenceMapper, bind=list): """ - Mapper for handling datetime values. + Mapper for handling lists. + + Inherits from ManyMapper and specifies 'list' as the binding type. + + """ + + +class TupleMapper(SequenceMapper, bind=tuple): + """ + Mapper for handling tuples. + + Inherits from ManyMapper and specifies 'tuple' as the binding type. + + """ + def validate(self, value) -> typing.Any: + if isinstance(value, list): + value = tuple(value) + return super().validate(value) + + +class SetMapper(SequenceMapper, bind=set): + """ + Mapper for handling sets. + + Inherits from ManyMapper and specifies 'set' as the binding type. + """ + def validate(self, value) -> typing.Any: + if isinstance(value, list): + value = set(value) + return super().validate(value) + def dump_json(self, value, **options) -> typing.Any: + return list(value) + + def dump_bson(self, value, **options) -> typing.Any: + return list(value) + + +class ObjectIdMapper(Mapper, bind=bson.ObjectId): + """ + Mapper for handling BSON ObjectId values. + """ + + def validate(self, value) -> typing.Any: """ - Dump the datetime value to a JSON-serializable format. + Validate the ObjectId value. + + Args: + value: The value to be validated. + + Returns: + Any: The validated value. + + Raises: + TypeError: If validation fails due to incorrect data type. + + """ + if not bson.ObjectId.is_valid(value): + raise TypeError(f'Invalid data type {type(value)}, required is {bson.ObjectId}') + return bson.ObjectId(value) + + def dump_json(self, value, **options) -> typing.Any: + """ + Dump the ObjectId value to a JSON-serializable format. Args: value: The value to be dumped. @@ -912,17 +1016,17 @@ def dump_json(self, value, **options) -> typing.Any: Any: The dumped value. """ - return value.isoformat() + return str(value) -class DateMapper(ComparableMapper, bind=datetime.date): +class Int64Mapper(Mapper, bind=bson.Int64): """ - Mapper for handling date values. + Mapper for handling BSON Int64 values. """ def validate(self, value) -> typing.Any: """ - Validate the date value. + Validate the Int64 value. Args: value: The value to be validated. @@ -934,13 +1038,13 @@ def validate(self, value) -> typing.Any: TypeError: If validation fails due to incorrect data type. """ - if isinstance(value, datetime.datetime): - value = value.date() - return super().validate(value) + if not isinstance(value, bson.Int64): + raise TypeError(f'Invalid data type {type(value)}, required is {bson.Int64}') + return value def dump_json(self, value, **options) -> typing.Any: """ - Dump the date value to a JSON-serializable format. + Dump the Int64 value to a JSON-serializable format. Args: value: The value to be dumped. @@ -950,11 +1054,35 @@ def dump_json(self, value, **options) -> typing.Any: Any: The dumped value. """ - return value.isoformat() + return int(value) - def dump_bson(self, value, **options) -> typing.Any: + +class Decimal128Mapper(Mapper, bind=bson.Decimal128): + """ + Mapper for handling BSON Decimal128 values. + """ + + def validate(self, value) -> typing.Any: """ - Dump the date value to BSON. + Validate the Decimal128 value. + + Args: + value: The value to be validated. + + Returns: + Any: The validated value. + + Raises: + TypeError: If validation fails due to incorrect data type. + + """ + if not isinstance(value, bson.Decimal128): + raise TypeError(f'Invalid data type {type(value)}, required is {bson.Decimal128}') + return value + + def dump_json(self, value, **options) -> typing.Any: + """ + Dump the Decimal128 value to a JSON-serializable format. Args: value: The value to be dumped. @@ -964,17 +1092,17 @@ def dump_bson(self, value, **options) -> typing.Any: Any: The dumped value. """ - return datetime.datetime.combine(date=value, time=datetime.time.min) + return float(value.to_decimal()) -class TimeMapper(ComparableMapper, bind=datetime.time): +class RegexMapper(Mapper, bind=bson.Regex): """ - Mapper for handling time values. + Mapper for handling BSON Regex values. """ def validate(self, value) -> typing.Any: """ - Validate the time value. + Validate the Regex value. Args: value: The value to be validated. @@ -986,13 +1114,13 @@ def validate(self, value) -> typing.Any: TypeError: If validation fails due to incorrect data type. """ - if isinstance(value, datetime.datetime): - value = value.time() - return super().validate(value) + if not isinstance(value, bson.Regex): + value = bson.Regex.from_native(value) + return value def dump_json(self, value, **options) -> typing.Any: """ - Dump the time value to a JSON-serializable format. + Dump the Regex value to a JSON-serializable format. Args: value: The value to be dumped. @@ -1002,11 +1130,35 @@ def dump_json(self, value, **options) -> typing.Any: Any: The dumped value. """ - return value.isoformat() + return f'{value.pattern}' - def dump_bson(self, value, **options) -> typing.Any: + +class CodeMapper(Mapper, bind=bson.Code): + """ + Mapper for handling BSON Code values. + """ + + def validate(self, value) -> typing.Any: """ - Dump the time value to BSON. + Validate the Code value. + + Args: + value: The value to be validated. + + Returns: + Any: The validated value. + + Raises: + TypeError: If validation fails due to incorrect data type. + + """ + if not isinstance(value, bson.Code): + raise TypeError(f'Invalid data type {type(value)}, required is {bson.Code}') + return value + + def dump_json(self, value, **options) -> typing.Any: + """ + Dump the Decimal128 value to a JSON-serializable format. Args: value: The value to be dumped. @@ -1016,7 +1168,7 @@ def dump_bson(self, value, **options) -> typing.Any: Any: The dumped value. """ - return datetime.datetime.combine(date=datetime.datetime.min, time=value) + return str(value) class ConstrainedStrMapper(StrMapper): @@ -1262,32 +1414,6 @@ def validate(self, value) -> typing.Any: return types.Json(value) - def dump_json(self, value, **options) -> typing.Any: - """ - Dump the JSON data to JSON. - - Args: - value: The JSON data to be dumped. - **options: Additional options. - - Returns: - Any: The dumped JSON data. - """ - return dict(value) - - def dump_bson(self, value, **options) -> typing.Any: - """ - Dump the JSON data to BSON. - - Args: - value: The JSON data to be dumped. - **options: Additional options. - - Returns: - Any: The dumped JSON data. - """ - return dict(value) - class BsonMapper(Mapper, bind=types.Bson): """ @@ -1330,28 +1456,11 @@ def dump_json(self, value, **options) -> typing.Any: **options: Additional options. Returns: - Any: The dumped BSON data. - - Raises: - NotImplementedError: As BSON data cannot be directly dumped to JSON. - """ - # noinspection SpellCheckingInspection - raise NotImplementedError( - 'mongotoy.types.Bson does not implement dump_json; use mongotoy.types.Json instead' - ) - - def dump_bson(self, value, **options) -> typing.Any: - """ - Dump the BSON data to BSON. - - Args: - value: The BSON data to be dumped. - **options: Additional options. - - Returns: - Any: The dumped BSON data. + Any: The dumped JSON data. """ - return bson.SON(value) + from bson import json_util + # noinspection PyProtectedMember + return json_util._json_convert(value, json_options=json_util.RELAXED_JSON_OPTIONS) class FileMapper(ReferencedDocumentMapper, bind=types.File): diff --git a/mongotoy/types.py b/mongotoy/types.py index 464248e..6a92cef 100644 --- a/mongotoy/types.py +++ b/mongotoy/types.py @@ -23,10 +23,12 @@ class IpV4(collections.UserString): ValueError: If the value is not a valid IPv4 address. """ + _regex = re.compile( + r'(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}' + ) + def __init__(self, value): - if not re.compile( - r'(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid IPv4 address') super().__init__(value) @@ -42,18 +44,20 @@ class IpV6(collections.UserString): ValueError: If the value is not a valid IPv6 address. """ + # noinspection RegExpSimplifiable + _regex = re.compile( + r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' + r'{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:)' + r'{1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]' + r'{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]' + r'{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}' + r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|' + r'([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|' + r'1{0,1}[0-9]){0,1}[0-9]))' + ) + def __init__(self, value): - # noinspection RegExpSimplifiable - if not re.compile( - r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:)' - r'{1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:)' - r'{1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]' - r'{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]' - r'{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}' - r'((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|' - r'([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|' - r'1{0,1}[0-9]){0,1}[0-9]))' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid IPv6 address') super().__init__(value) @@ -69,11 +73,13 @@ class Port(collections.UserString): ValueError: If the value is not a valid port number. """ + _regex = re.compile( + r'^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|' + r'([0-9]{1,4}))$' + ) + def __init__(self, value): - if not re.compile( - r'^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|' - r'([0-9]{1,4}))$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid port number') super().__init__(value) @@ -89,10 +95,12 @@ class Mac(collections.UserString): ValueError: If the value is not a valid MAC address. """ + _regex = re.compile( + r'^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$' + ) + def __init__(self, value): - if not re.compile( - r'^[a-fA-F0-9]{2}(:[a-fA-F0-9]{2}){5}$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid MAC address') super().__init__(value) @@ -108,11 +116,13 @@ class Phone(collections.UserString): ValueError: If the value is not a valid phone number. """ + # noinspection RegExpRedundantEscape,RegExpSimplifiable + _regex = re.compile( + r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$' + ) + def __init__(self, value): - # noinspection RegExpRedundantEscape,RegExpSimplifiable - if not re.compile( - r'^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid phone number') super().__init__(value) @@ -128,12 +138,14 @@ class Email(collections.UserString): ValueError: If the value is not a valid email address. """ + _regex = re.compile( + r'(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|' + r'(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]' + r'{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))' + ) + def __init__(self, value): - if not re.compile( - r'(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|' - r'(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]' - r'{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid email address') super().__init__(value) @@ -149,13 +161,15 @@ class Card(collections.UserString): ValueError: If the value is not a valid credit card number. """ + _regex = re.compile( + r'(^4[0-9]{12}(?:[0-9]{3})?$)|(^(?:5[1-5][0-9]{2}|222[1-9]|22' + r'[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$)|(3[47][0-9]' + r'{13})|(^3(?:0[0-5]|[68][0-9])[0-9]{11}$)|(^6(?:011|5[0-9]{2})[0-9]' + r'{12}$)|(^(?:2131|1800|35\d{3})\d{11}$)' + ) + def __init__(self, value): - if not re.compile( - r'(^4[0-9]{12}(?:[0-9]{3})?$)|(^(?:5[1-5][0-9]{2}|222[1-9]|22' - r'[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}$)|(3[47][0-9]' - r'{13})|(^3(?:0[0-5]|[68][0-9])[0-9]{11}$)|(^6(?:011|5[0-9]{2})[0-9]' - r'{12}$)|(^(?:2131|1800|35\d{3})\d{11}$)' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid credit card number') super().__init__(value) @@ -171,10 +185,12 @@ class Ssn(collections.UserString): ValueError: If the value is not a valid SSN. """ + _regex = re.compile( + r'^(?!0{3})(?!6{3})[0-8]\d{2}-(?!0{2})\d{2}-(?!0{4})\d{4}$' + ) + def __init__(self, value): - if not re.compile( - r'^(?!0{3})(?!6{3})[0-8]\d{2}-(?!0{2})\d{2}-(?!0{4})\d{4}$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid SSN') super().__init__(value) @@ -190,10 +206,12 @@ class Hashtag(collections.UserString): ValueError: If the value is not a valid hashtag. """ + _regex = re.compile( + r'^#[^ !@#$%^&*(),.?":{}|<>]*$' + ) + def __init__(self, value): - if not re.compile( - r'^#[^ !@#$%^&*(),.?":{}|<>]*$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid hashtag') super().__init__(value) @@ -209,11 +227,13 @@ class Doi(collections.UserString): ValueError: If the value is not a valid DOI. """ + # noinspection RegExpRedundantEscape,RegExpSimplifiable + _regex = re.compile( + r'^(10\.\d{4,5}\/[\S]+[^;,.\s])$' + ) + def __init__(self, value): - # noinspection RegExpRedundantEscape,RegExpSimplifiable - if not re.compile( - r'^(10\.\d{4,5}\/[\S]+[^;,.\s])$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid DOI') super().__init__(value) @@ -229,12 +249,14 @@ class Url(collections.UserString): ValueError: If the value is not a valid URL. """ + # noinspection RegExpDuplicateCharacterInClass,RegExpRedundantEscape + _regex = re.compile( + r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b' + r'([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)' + ) + def __init__(self, value): - # noinspection RegExpDuplicateCharacterInClass,RegExpRedundantEscape - if not re.compile( - r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b' - r'([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid URL') super().__init__(value) @@ -250,12 +272,14 @@ class Version(collections.UserString): ValueError: If the value is not a valid Semantic Version Number. """ + _regex = re.compile( + r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-]' + r'[0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+' + r'([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' + ) + def __init__(self, value): - if not re.compile( - r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-]' - r'[0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+' - r'([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' - ).fullmatch(value): + if not self._regex.fullmatch(value): raise ValueError(f'Value {value} is not a valid Semantic Version Number') super().__init__(value) @@ -413,4 +437,3 @@ def readchunk(self) -> typing.Coroutine[typing.Any, typing.Any, bytes] | bytes: def readline(self, size: int = -1) -> typing.Coroutine[typing.Any, typing.Any, bytes] | bytes: pass -