Added IPAddressType, refactored types
This commit is contained in:
		| @@ -9,6 +9,7 @@ from .types import ( | ||||
|     EmailType, | ||||
|     instrumented_list, | ||||
|     InstrumentedList, | ||||
|     IPAddressType, | ||||
|     PhoneNumber, | ||||
|     PhoneNumberType, | ||||
|     NumberRange, | ||||
| @@ -37,6 +38,7 @@ __all__ = ( | ||||
|     ColorType, | ||||
|     EmailType, | ||||
|     InstrumentedList, | ||||
|     IPAddressType, | ||||
|     Merger, | ||||
|     NumberRange, | ||||
|     NumberRangeException, | ||||
|   | ||||
| @@ -1,361 +0,0 @@ | ||||
| import six | ||||
| import phonenumbers | ||||
| from colour import Color | ||||
| from functools import wraps | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy.orm.collections import InstrumentedList as _InstrumentedList | ||||
| from sqlalchemy import types | ||||
| from .operators import CaseInsensitiveComparator | ||||
|  | ||||
|  | ||||
| class PhoneNumber(phonenumbers.phonenumber.PhoneNumber): | ||||
|     ''' | ||||
|     Extends a PhoneNumber class from `Python phonenumbers library`_. Adds | ||||
|     different phone number formats to attributes, so they can be easily used | ||||
|     in templates. Phone number validation method is also implemented. | ||||
|  | ||||
|     Takes the raw phone number and country code as params and parses them | ||||
|     into a PhoneNumber object. | ||||
|  | ||||
|     .. _Python phonenumbers library: | ||||
|        https://github.com/daviddrysdale/python-phonenumbers | ||||
|  | ||||
|     :param raw_number: | ||||
|         String representation of the phone number. | ||||
|     :param country_code: | ||||
|         Country code of the phone number. | ||||
|     ''' | ||||
|     def __init__(self, raw_number, country_code=None): | ||||
|         self._phone_number = phonenumbers.parse(raw_number, country_code) | ||||
|         super(PhoneNumber, self).__init__( | ||||
|             country_code=self._phone_number.country_code, | ||||
|             national_number=self._phone_number.national_number, | ||||
|             extension=self._phone_number.extension, | ||||
|             italian_leading_zero=self._phone_number.italian_leading_zero, | ||||
|             raw_input=self._phone_number.raw_input, | ||||
|             country_code_source=self._phone_number.country_code_source, | ||||
|             preferred_domestic_carrier_code= | ||||
|             self._phone_number.preferred_domestic_carrier_code | ||||
|         ) | ||||
|         self.national = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.NATIONAL | ||||
|         ) | ||||
|         self.international = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.INTERNATIONAL | ||||
|         ) | ||||
|         self.e164 = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.E164 | ||||
|         ) | ||||
|  | ||||
|     def is_valid_number(self): | ||||
|         return phonenumbers.is_valid_number(self._phone_number) | ||||
|  | ||||
|     def __unicode__(self): | ||||
|         return self.national | ||||
|  | ||||
|     def __str__(self): | ||||
|         return six.text_type(self.national).encode('utf-8') | ||||
|  | ||||
|  | ||||
| class PhoneNumberType(types.TypeDecorator): | ||||
|     """ | ||||
|     Changes PhoneNumber objects to a string representation on the way in and | ||||
|     changes them back to PhoneNumber objects on the way out. If E164 is used | ||||
|     as storing format, no country code is needed for parsing the database | ||||
|     value to PhoneNumber object. | ||||
|     """ | ||||
|     STORE_FORMAT = 'e164' | ||||
|     impl = types.Unicode(20) | ||||
|  | ||||
|     def __init__(self, country_code='US', max_length=20, *args, **kwargs): | ||||
|         super(PhoneNumberType, self).__init__(*args, **kwargs) | ||||
|         self.country_code = country_code | ||||
|         self.impl = types.Unicode(max_length) | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value: | ||||
|             return getattr(value, self.STORE_FORMAT) | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             return PhoneNumber(value, self.country_code) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, PhoneNumber): | ||||
|             value = PhoneNumber(value, country_code=self.country_code) | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class ColorType(types.TypeDecorator): | ||||
|     """ | ||||
|     Changes Color objects to a string representation on the way in and | ||||
|     changes them back to Color objects on the way out. | ||||
|     """ | ||||
|     STORE_FORMAT = u'hex' | ||||
|     impl = types.Unicode(20) | ||||
|  | ||||
|     def __init__(self, max_length=20, *args, **kwargs): | ||||
|         super(ColorType, self).__init__(*args, **kwargs) | ||||
|         self.impl = types.Unicode(max_length) | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value: | ||||
|             return six.text_type(getattr(value, self.STORE_FORMAT)) | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             return Color(value) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, Color): | ||||
|             value = Color(value) | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class ScalarListException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ScalarListType(types.TypeDecorator): | ||||
|     impl = sa.UnicodeText() | ||||
|  | ||||
|     def __init__(self, coerce_func=six.text_type, separator=u','): | ||||
|         self.separator = six.text_type(separator) | ||||
|         self.coerce_func = coerce_func | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         # Convert list of values to unicode separator-separated list | ||||
|         # Example: [1, 2, 3, 4] -> u'1, 2, 3, 4' | ||||
|         if value is not None: | ||||
|             if any(self.separator in six.text_type(item) for item in value): | ||||
|                 raise ScalarListException( | ||||
|                     "List values can't contain string '%s' (its being used as " | ||||
|                     "separator. If you wish for scalar list values to contain " | ||||
|                     "these strings, use a different separator string." | ||||
|                 ) | ||||
|             return self.separator.join( | ||||
|                 map(six.text_type, value) | ||||
|             ) | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value is not None: | ||||
|             if value == u'': | ||||
|                 return [] | ||||
|             # coerce each value | ||||
|             return list(map( | ||||
|                 self.coerce_func, value.split(self.separator) | ||||
|             )) | ||||
|  | ||||
|  | ||||
| class EmailType(sa.types.TypeDecorator): | ||||
|     impl = sa.Unicode(255) | ||||
|     comparator_factory = CaseInsensitiveComparator | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value is not None: | ||||
|             return value.lower() | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class TSVectorType(types.UserDefinedType): | ||||
|     """ | ||||
|     Text search vector type for postgresql. | ||||
|     """ | ||||
|     def get_col_spec(self): | ||||
|         return 'tsvector' | ||||
|  | ||||
|  | ||||
| class NumberRangeRawType(types.UserDefinedType): | ||||
|     """ | ||||
|     Raw number range type, only supports PostgreSQL for now. | ||||
|     """ | ||||
|     def get_col_spec(self): | ||||
|         return 'int4range' | ||||
|  | ||||
|  | ||||
| class NumberRangeType(types.TypeDecorator): | ||||
|     impl = NumberRangeRawType | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value is not None: | ||||
|             return value.normalized | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             if not isinstance(value, six.string_types): | ||||
|                 value = NumberRange.from_range_object(value) | ||||
|             else: | ||||
|                 return NumberRange.from_normalized_str(value) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, NumberRange): | ||||
|             if isinstance(value, six.string_types): | ||||
|                 value = NumberRange.from_normalized_str(value) | ||||
|             else: | ||||
|                 raise TypeError | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class NumberRangeException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class RangeBoundsException(NumberRangeException): | ||||
|     def __init__(self, min_value, max_value): | ||||
|         self.message = 'Min value %d is bigger than max value %d.' % ( | ||||
|             min_value, | ||||
|             max_value | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class NumberRange(object): | ||||
|     def __init__(self, min_value, max_value): | ||||
|         if min_value > max_value: | ||||
|             raise RangeBoundsException(min_value, max_value) | ||||
|         self.min_value = min_value | ||||
|         self.max_value = max_value | ||||
|  | ||||
|     @classmethod | ||||
|     def from_range_object(cls, value): | ||||
|         min_value = value.lower | ||||
|         max_value = value.upper | ||||
|         if not value.lower_inc: | ||||
|             min_value += 1 | ||||
|  | ||||
|         if not value.upper_inc: | ||||
|             max_value -= 1 | ||||
|  | ||||
|         return cls(min_value, max_value) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_normalized_str(cls, value): | ||||
|         """ | ||||
|         Returns new NumberRange object from normalized number range format. | ||||
|  | ||||
|         Example :: | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('[23, 45]') | ||||
|             range.min_value = 23 | ||||
|             range.max_value = 45 | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('(23, 45]') | ||||
|             range.min_value = 24 | ||||
|             range.max_value = 45 | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('(23, 45)') | ||||
|             range.min_value = 24 | ||||
|             range.max_value = 44 | ||||
|         """ | ||||
|         if value is not None: | ||||
|             values = value[1:-1].split(',') | ||||
|             try: | ||||
|                 min_value, max_value = map( | ||||
|                     lambda a: int(a.strip()), values | ||||
|                 ) | ||||
|             except ValueError as e: | ||||
|                 raise NumberRangeException(e.message) | ||||
|  | ||||
|             if value[0] == '(': | ||||
|                 min_value += 1 | ||||
|  | ||||
|             if value[-1] == ')': | ||||
|                 max_value -= 1 | ||||
|  | ||||
|             return cls(min_value, max_value) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_str(cls, value): | ||||
|         if value is not None: | ||||
|             values = value.split('-') | ||||
|             if len(values) == 1: | ||||
|                 min_value = max_value = int(value.strip()) | ||||
|             else: | ||||
|                 try: | ||||
|                     min_value, max_value = map( | ||||
|                         lambda a: int(a.strip()), values | ||||
|                     ) | ||||
|                 except ValueError as e: | ||||
|                     raise NumberRangeException(str(e)) | ||||
|             return cls(min_value, max_value) | ||||
|  | ||||
|     @property | ||||
|     def normalized(self): | ||||
|         return '[%s, %s]' % (self.min_value, self.max_value) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         try: | ||||
|             return ( | ||||
|                 self.min_value == other.min_value and | ||||
|                 self.max_value == other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return 'NumberRange(%r, %r)' % (self.min_value, self.max_value) | ||||
|  | ||||
|     def __str__(self): | ||||
|         if self.min_value != self.max_value: | ||||
|             return '%s - %s' % (self.min_value, self.max_value) | ||||
|         return str(self.min_value) | ||||
|  | ||||
|     def __add__(self, other): | ||||
|         try: | ||||
|             return NumberRange( | ||||
|                 self.min_value + other.min_value, | ||||
|                 self.max_value + other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __iadd__(self, other): | ||||
|         try: | ||||
|             self.min_value += other.min_value | ||||
|             self.max_value += other.max_value | ||||
|             return self | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __sub__(self, other): | ||||
|         try: | ||||
|             return NumberRange( | ||||
|                 self.min_value - other.min_value, | ||||
|                 self.max_value - other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __isub__(self, other): | ||||
|         try: | ||||
|             self.min_value -= other.min_value | ||||
|             self.max_value -= other.max_value | ||||
|             return self | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|  | ||||
| class InstrumentedList(_InstrumentedList): | ||||
|     """Enhanced version of SQLAlchemy InstrumentedList. Provides some | ||||
|     additional functionality.""" | ||||
|  | ||||
|     def any(self, attr): | ||||
|         return any(getattr(item, attr) for item in self) | ||||
|  | ||||
|     def all(self, attr): | ||||
|         return all(getattr(item, attr) for item in self) | ||||
|  | ||||
|  | ||||
| def instrumented_list(f): | ||||
|     @wraps(f) | ||||
|     def wrapper(*args, **kwargs): | ||||
|         return InstrumentedList([item for item in f(*args, **kwargs)]) | ||||
|     return wrapper | ||||
							
								
								
									
										55
									
								
								sqlalchemy_utils/types/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								sqlalchemy_utils/types/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| from functools import wraps | ||||
| from sqlalchemy.orm.collections import InstrumentedList as _InstrumentedList | ||||
| from sqlalchemy import types | ||||
| from .color import ColorType | ||||
| from .email import EmailType | ||||
| from .ip_address import IPAddressType | ||||
| from .number_range import ( | ||||
|     NumberRange, | ||||
|     NumberRangeException, | ||||
|     NumberRangeRawType, | ||||
|     NumberRangeType, | ||||
| ) | ||||
| from .phone_number import PhoneNumber, PhoneNumberType | ||||
| from .scalar_list import ScalarListException, ScalarListType | ||||
|  | ||||
|  | ||||
| __all__ = ( | ||||
|     ColorType, | ||||
|     EmailType, | ||||
|     IPAddressType, | ||||
|     NumberRange, | ||||
|     NumberRangeException, | ||||
|     NumberRangeRawType, | ||||
|     NumberRangeType, | ||||
|     PhoneNumber, | ||||
|     PhoneNumberType, | ||||
|     ScalarListException, | ||||
|     ScalarListType, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TSVectorType(types.UserDefinedType): | ||||
|     """ | ||||
|     Text search vector type for postgresql. | ||||
|     """ | ||||
|     def get_col_spec(self): | ||||
|         return 'tsvector' | ||||
|  | ||||
|  | ||||
| class InstrumentedList(_InstrumentedList): | ||||
|     """Enhanced version of SQLAlchemy InstrumentedList. Provides some | ||||
|     additional functionality.""" | ||||
|  | ||||
|     def any(self, attr): | ||||
|         return any(getattr(item, attr) for item in self) | ||||
|  | ||||
|     def all(self, attr): | ||||
|         return all(getattr(item, attr) for item in self) | ||||
|  | ||||
|  | ||||
| def instrumented_list(f): | ||||
|     @wraps(f) | ||||
|     def wrapper(*args, **kwargs): | ||||
|         return InstrumentedList([item for item in f(*args, **kwargs)]) | ||||
|     return wrapper | ||||
							
								
								
									
										31
									
								
								sqlalchemy_utils/types/color.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								sqlalchemy_utils/types/color.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import six | ||||
| from colour import Color | ||||
| from sqlalchemy import types | ||||
|  | ||||
|  | ||||
| class ColorType(types.TypeDecorator): | ||||
|     """ | ||||
|     Changes Color objects to a string representation on the way in and | ||||
|     changes them back to Color objects on the way out. | ||||
|     """ | ||||
|     STORE_FORMAT = u'hex' | ||||
|     impl = types.Unicode(20) | ||||
|  | ||||
|     def __init__(self, max_length=20, *args, **kwargs): | ||||
|         super(ColorType, self).__init__(*args, **kwargs) | ||||
|         self.impl = types.Unicode(max_length) | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value: | ||||
|             return six.text_type(getattr(value, self.STORE_FORMAT)) | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             return Color(value) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, Color): | ||||
|             value = Color(value) | ||||
|         return value | ||||
							
								
								
									
										12
									
								
								sqlalchemy_utils/types/email.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								sqlalchemy_utils/types/email.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import sqlalchemy as sa | ||||
| from ..operators import CaseInsensitiveComparator | ||||
|  | ||||
|  | ||||
| class EmailType(sa.types.TypeDecorator): | ||||
|     impl = sa.Unicode(255) | ||||
|     comparator_factory = CaseInsensitiveComparator | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value is not None: | ||||
|             return value.lower() | ||||
|         return value | ||||
							
								
								
									
										34
									
								
								sqlalchemy_utils/types/ip_address.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								sqlalchemy_utils/types/ip_address.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import six | ||||
| import ipaddress | ||||
| from sqlalchemy import types | ||||
|  | ||||
|  | ||||
| class IPAddressType(types.TypeDecorator): | ||||
|     """ | ||||
|     Changes Color objects to a string representation on the way in and | ||||
|     changes them back to Color objects on the way out. | ||||
|     """ | ||||
|     impl = types.Unicode(50) | ||||
|  | ||||
|     def __init__(self, max_length=50, *args, **kwargs): | ||||
|         super(IPAddressType, self).__init__(*args, **kwargs) | ||||
|         self.impl = types.Unicode(max_length) | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value: | ||||
|             return six.text_type(value) | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             return ipaddress.ip_address(value) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if ( | ||||
|             value is not None and | ||||
|             not isinstance(value, ipaddress.IPv4Address) and | ||||
|             not isinstance(value, ipaddress.IPv6Address) | ||||
|         ): | ||||
|             value = ipaddress.ip_address(value) | ||||
|         return value | ||||
							
								
								
									
										173
									
								
								sqlalchemy_utils/types/number_range.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								sqlalchemy_utils/types/number_range.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| import six | ||||
| from sqlalchemy import types | ||||
|  | ||||
|  | ||||
| class NumberRangeRawType(types.UserDefinedType): | ||||
|     """ | ||||
|     Raw number range type, only supports PostgreSQL for now. | ||||
|     """ | ||||
|     def get_col_spec(self): | ||||
|         return 'int4range' | ||||
|  | ||||
|  | ||||
| class NumberRangeType(types.TypeDecorator): | ||||
|     impl = NumberRangeRawType | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value is not None: | ||||
|             return value.normalized | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             if not isinstance(value, six.string_types): | ||||
|                 value = NumberRange.from_range_object(value) | ||||
|             else: | ||||
|                 return NumberRange.from_normalized_str(value) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, NumberRange): | ||||
|             if isinstance(value, six.string_types): | ||||
|                 value = NumberRange.from_normalized_str(value) | ||||
|             else: | ||||
|                 raise TypeError | ||||
|         return value | ||||
|  | ||||
|  | ||||
| class NumberRangeException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class RangeBoundsException(NumberRangeException): | ||||
|     def __init__(self, min_value, max_value): | ||||
|         self.message = 'Min value %d is bigger than max value %d.' % ( | ||||
|             min_value, | ||||
|             max_value | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class NumberRange(object): | ||||
|     def __init__(self, min_value, max_value): | ||||
|         if min_value > max_value: | ||||
|             raise RangeBoundsException(min_value, max_value) | ||||
|         self.min_value = min_value | ||||
|         self.max_value = max_value | ||||
|  | ||||
|     @classmethod | ||||
|     def from_range_object(cls, value): | ||||
|         min_value = value.lower | ||||
|         max_value = value.upper | ||||
|         if not value.lower_inc: | ||||
|             min_value += 1 | ||||
|  | ||||
|         if not value.upper_inc: | ||||
|             max_value -= 1 | ||||
|  | ||||
|         return cls(min_value, max_value) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_normalized_str(cls, value): | ||||
|         """ | ||||
|         Returns new NumberRange object from normalized number range format. | ||||
|  | ||||
|         Example :: | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('[23, 45]') | ||||
|             range.min_value = 23 | ||||
|             range.max_value = 45 | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('(23, 45]') | ||||
|             range.min_value = 24 | ||||
|             range.max_value = 45 | ||||
|  | ||||
|             range = NumberRange.from_normalized_str('(23, 45)') | ||||
|             range.min_value = 24 | ||||
|             range.max_value = 44 | ||||
|         """ | ||||
|         if value is not None: | ||||
|             values = value[1:-1].split(',') | ||||
|             try: | ||||
|                 min_value, max_value = map( | ||||
|                     lambda a: int(a.strip()), values | ||||
|                 ) | ||||
|             except ValueError as e: | ||||
|                 raise NumberRangeException(e.message) | ||||
|  | ||||
|             if value[0] == '(': | ||||
|                 min_value += 1 | ||||
|  | ||||
|             if value[-1] == ')': | ||||
|                 max_value -= 1 | ||||
|  | ||||
|             return cls(min_value, max_value) | ||||
|  | ||||
|     @classmethod | ||||
|     def from_str(cls, value): | ||||
|         if value is not None: | ||||
|             values = value.split('-') | ||||
|             if len(values) == 1: | ||||
|                 min_value = max_value = int(value.strip()) | ||||
|             else: | ||||
|                 try: | ||||
|                     min_value, max_value = map( | ||||
|                         lambda a: int(a.strip()), values | ||||
|                     ) | ||||
|                 except ValueError as e: | ||||
|                     raise NumberRangeException(str(e)) | ||||
|             return cls(min_value, max_value) | ||||
|  | ||||
|     @property | ||||
|     def normalized(self): | ||||
|         return '[%s, %s]' % (self.min_value, self.max_value) | ||||
|  | ||||
|     def __eq__(self, other): | ||||
|         try: | ||||
|             return ( | ||||
|                 self.min_value == other.min_value and | ||||
|                 self.max_value == other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return 'NumberRange(%r, %r)' % (self.min_value, self.max_value) | ||||
|  | ||||
|     def __str__(self): | ||||
|         if self.min_value != self.max_value: | ||||
|             return '%s - %s' % (self.min_value, self.max_value) | ||||
|         return str(self.min_value) | ||||
|  | ||||
|     def __add__(self, other): | ||||
|         try: | ||||
|             return NumberRange( | ||||
|                 self.min_value + other.min_value, | ||||
|                 self.max_value + other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __iadd__(self, other): | ||||
|         try: | ||||
|             self.min_value += other.min_value | ||||
|             self.max_value += other.max_value | ||||
|             return self | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __sub__(self, other): | ||||
|         try: | ||||
|             return NumberRange( | ||||
|                 self.min_value - other.min_value, | ||||
|                 self.max_value - other.max_value | ||||
|             ) | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
|  | ||||
|     def __isub__(self, other): | ||||
|         try: | ||||
|             self.min_value -= other.min_value | ||||
|             self.max_value -= other.max_value | ||||
|             return self | ||||
|         except AttributeError: | ||||
|             return NotImplemented | ||||
							
								
								
									
										86
									
								
								sqlalchemy_utils/types/phone_number.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								sqlalchemy_utils/types/phone_number.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| import six | ||||
| import phonenumbers | ||||
| from sqlalchemy import types | ||||
|  | ||||
|  | ||||
| class PhoneNumber(phonenumbers.phonenumber.PhoneNumber): | ||||
|     ''' | ||||
|     Extends a PhoneNumber class from `Python phonenumbers library`_. Adds | ||||
|     different phone number formats to attributes, so they can be easily used | ||||
|     in templates. Phone number validation method is also implemented. | ||||
|  | ||||
|     Takes the raw phone number and country code as params and parses them | ||||
|     into a PhoneNumber object. | ||||
|  | ||||
|     .. _Python phonenumbers library: | ||||
|        https://github.com/daviddrysdale/python-phonenumbers | ||||
|  | ||||
|     :param raw_number: | ||||
|         String representation of the phone number. | ||||
|     :param country_code: | ||||
|         Country code of the phone number. | ||||
|     ''' | ||||
|     def __init__(self, raw_number, country_code=None): | ||||
|         self._phone_number = phonenumbers.parse(raw_number, country_code) | ||||
|         super(PhoneNumber, self).__init__( | ||||
|             country_code=self._phone_number.country_code, | ||||
|             national_number=self._phone_number.national_number, | ||||
|             extension=self._phone_number.extension, | ||||
|             italian_leading_zero=self._phone_number.italian_leading_zero, | ||||
|             raw_input=self._phone_number.raw_input, | ||||
|             country_code_source=self._phone_number.country_code_source, | ||||
|             preferred_domestic_carrier_code= | ||||
|             self._phone_number.preferred_domestic_carrier_code | ||||
|         ) | ||||
|         self.national = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.NATIONAL | ||||
|         ) | ||||
|         self.international = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.INTERNATIONAL | ||||
|         ) | ||||
|         self.e164 = phonenumbers.format_number( | ||||
|             self._phone_number, | ||||
|             phonenumbers.PhoneNumberFormat.E164 | ||||
|         ) | ||||
|  | ||||
|     def is_valid_number(self): | ||||
|         return phonenumbers.is_valid_number(self._phone_number) | ||||
|  | ||||
|     def __unicode__(self): | ||||
|         return self.national | ||||
|  | ||||
|     def __str__(self): | ||||
|         return six.text_type(self.national).encode('utf-8') | ||||
|  | ||||
|  | ||||
| class PhoneNumberType(types.TypeDecorator): | ||||
|     """ | ||||
|     Changes PhoneNumber objects to a string representation on the way in and | ||||
|     changes them back to PhoneNumber objects on the way out. If E164 is used | ||||
|     as storing format, no country code is needed for parsing the database | ||||
|     value to PhoneNumber object. | ||||
|     """ | ||||
|     STORE_FORMAT = 'e164' | ||||
|     impl = types.Unicode(20) | ||||
|  | ||||
|     def __init__(self, country_code='US', max_length=20, *args, **kwargs): | ||||
|         super(PhoneNumberType, self).__init__(*args, **kwargs) | ||||
|         self.country_code = country_code | ||||
|         self.impl = types.Unicode(max_length) | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         if value: | ||||
|             return getattr(value, self.STORE_FORMAT) | ||||
|         return value | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value: | ||||
|             return PhoneNumber(value, self.country_code) | ||||
|         return value | ||||
|  | ||||
|     def coercion_listener(self, target, value, oldvalue, initiator): | ||||
|         if value is not None and not isinstance(value, PhoneNumber): | ||||
|             value = PhoneNumber(value, country_code=self.country_code) | ||||
|         return value | ||||
							
								
								
									
										38
									
								
								sqlalchemy_utils/types/scalar_list.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								sqlalchemy_utils/types/scalar_list.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import six | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy import types | ||||
|  | ||||
|  | ||||
| class ScalarListException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class ScalarListType(types.TypeDecorator): | ||||
|     impl = sa.UnicodeText() | ||||
|  | ||||
|     def __init__(self, coerce_func=six.text_type, separator=u','): | ||||
|         self.separator = six.text_type(separator) | ||||
|         self.coerce_func = coerce_func | ||||
|  | ||||
|     def process_bind_param(self, value, dialect): | ||||
|         # Convert list of values to unicode separator-separated list | ||||
|         # Example: [1, 2, 3, 4] -> u'1, 2, 3, 4' | ||||
|         if value is not None: | ||||
|             if any(self.separator in six.text_type(item) for item in value): | ||||
|                 raise ScalarListException( | ||||
|                     "List values can't contain string '%s' (its being used as " | ||||
|                     "separator. If you wish for scalar list values to contain " | ||||
|                     "these strings, use a different separator string.)" | ||||
|                 ) | ||||
|             return self.separator.join( | ||||
|                 map(six.text_type, value) | ||||
|             ) | ||||
|  | ||||
|     def process_result_value(self, value, dialect): | ||||
|         if value is not None: | ||||
|             if value == u'': | ||||
|                 return [] | ||||
|             # coerce each value | ||||
|             return list(map( | ||||
|                 self.coerce_func, value.split(self.separator) | ||||
|             )) | ||||
							
								
								
									
										0
									
								
								sqlalchemy_utils/types/slug_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								sqlalchemy_utils/types/slug_type.py
									
									
									
									
									
										Normal file
									
								
							| @@ -1,3 +1,4 @@ | ||||
| import warnings | ||||
| import sqlalchemy as sa | ||||
|  | ||||
| from sqlalchemy import create_engine | ||||
| @@ -18,12 +19,17 @@ def count_sql_calls(conn, cursor, statement, parameters, context, executemany): | ||||
|         conn.query_count = 0 | ||||
|  | ||||
|  | ||||
| warnings.simplefilter('error', sa.exc.SAWarning) | ||||
|  | ||||
|  | ||||
| class TestCase(object): | ||||
|     dns = 'sqlite:///:memory:' | ||||
|  | ||||
|     def setup_method(self, method): | ||||
|         self.engine = create_engine('sqlite:///:memory:') | ||||
|         self.engine = create_engine(self.dns) | ||||
|         self.connection = self.engine.connect() | ||||
|         self.Base = declarative_base() | ||||
|         self.Base2 = declarative_base() | ||||
|  | ||||
|         self.create_models() | ||||
|         self.Base.metadata.create_all(self.connection) | ||||
|  | ||||
|   | ||||
							
								
								
									
										29
									
								
								tests/test_ip_address.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								tests/test_ip_address.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import ipaddress | ||||
| import six | ||||
| import sqlalchemy as sa | ||||
| from sqlalchemy_utils import IPAddressType | ||||
| from tests import TestCase | ||||
|  | ||||
|  | ||||
| class TestIPAddressType(TestCase): | ||||
|     def create_models(self): | ||||
|         class Visitor(self.Base): | ||||
|             __tablename__ = 'document' | ||||
|             id = sa.Column(sa.Integer, primary_key=True) | ||||
|             ip_address = sa.Column(IPAddressType) | ||||
|  | ||||
|             def __repr__(self): | ||||
|                 return 'Visitor(%r)' % self.id | ||||
|  | ||||
|         self.Visitor = Visitor | ||||
|  | ||||
|     def test_parameter_processing(self): | ||||
|         visitor = self.Visitor( | ||||
|             ip_address=ipaddress.ip_address(u'111.111.111.111') | ||||
|         ) | ||||
|  | ||||
|         self.session.add(visitor) | ||||
|         self.session.commit() | ||||
|  | ||||
|         visitor = self.session.query(self.Visitor).first() | ||||
|         assert six.text_type(visitor.ip_address) == u'111.111.111.111' | ||||
| @@ -17,6 +17,8 @@ class TestDeferExcept(TestCase): | ||||
|  | ||||
|  | ||||
| class TestFindNonIndexedForeignKeys(TestCase): | ||||
|     dns = 'postgres://postgres@localhost/sqlalchemy_utils_test' | ||||
|  | ||||
|     def create_models(self): | ||||
|         class User(self.Base): | ||||
|             __tablename__ = 'user' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Konsta Vesterinen
					Konsta Vesterinen