├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── setup.py ├── test ├── __init__.py └── test_converters.py └── typecast ├── __init__.py ├── converter.py ├── date.py ├── formats.py ├── guess.py ├── name.py └── value.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | before_install: 6 | - virtualenv ./pyenv --distribute 7 | - source ./pyenv/bin/activate 8 | install: 9 | - pip install -e . 10 | - pip install nose coverage python-dateutil coveralls unicodecsv wheel 11 | before_script: 12 | - nosetests --version 13 | script: 14 | - nosetests --with-coverage --cover-package=typecast 15 | after_success: 16 | - coveralls 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Friedrich Lindenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: install 3 | env/bin/nosetests --with-coverage --cover-package=typecast --cover-erase 4 | 5 | install: env/bin/python 6 | 7 | env/bin/python: 8 | virtualenv env 9 | env/bin/pip install --upgrade pip 10 | env/bin/pip install -e . 11 | env/bin/pip install nose coverage wheel 12 | 13 | upload: 14 | env/bin/python setup.py sdist bdist_wheel upload 15 | 16 | clean: 17 | rm -rf env 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typecast 2 | 3 | [![Build Status](https://travis-ci.org/pudo/typecast.svg?branch=master)](https://travis-ci.org/pudo/typecast) 4 | [![Coverage Status](https://coveralls.io/repos/pudo/typecast/badge.svg?branch=master&service=github)](https://coveralls.io/github/pudo/typecast?branch=master) 5 | 6 | 7 | This light-weight library contains a set of type converters commonly used to 8 | parse string-typed values into more specific types, such as numbers, booleans 9 | or dates. A typical use case might be converting values from HTTP query strings 10 | or CSV files. 11 | 12 | The benefits of using this library include a well-tested handling of type 13 | conversions, e.g. for JSON Schema or JTS. Further, a consistent system of 14 | exceptions (catch ``ConverterError`` and all is forgiven) makes it easier 15 | to handle data errors. 16 | 17 | ## Example usage 18 | 19 | ``typecast`` can easily be included in many applications. A Python snippet 20 | using the library could look like this: 21 | 22 | ```python 23 | import typecast 24 | 25 | type_name = 'date' 26 | value = '2031-01-05' 27 | converted = typecast.cast(type_name, value) 28 | assert converted.year == 2031 29 | 30 | other = typecast.stringify(type_name, converted) 31 | assert value == other 32 | ``` 33 | 34 | The supported type names are: 35 | 36 | * ``string``, ``text`` 37 | * ``number``, ``integer`` 38 | * ``float`` 39 | * ``decimal`` 40 | * ``date`` 41 | * ``datetime`` 42 | * ``boolean``, ``bool`` 43 | 44 | ## Tests 45 | 46 | The goal is to have a high-nineties test coverage, and to collect as many odd 47 | fringe cases as possible. 48 | 49 | ```bash 50 | $ make test 51 | ``` 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | setup( 5 | name='typecast', 6 | version='0.3.3', 7 | description="Convert types in source data.", 8 | long_description="", 9 | classifiers=[ 10 | "Development Status :: 3 - Alpha", 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Operating System :: OS Independent", 14 | 'Programming Language :: Python :: 2.6', 15 | 'Programming Language :: Python :: 2.7', 16 | 'Programming Language :: Python :: 3.3', 17 | 'Programming Language :: Python :: 3.4' 18 | ], 19 | keywords='types rdf', 20 | author='Friedrich Lindenberg', 21 | author_email='friedrich@pudo.org', 22 | url='http://github.com/pudo/typecast', 23 | license='MIT', 24 | packages=find_packages(exclude=['ez_setup', 'examples', 'test']), 25 | namespace_packages=[], 26 | package_data={}, 27 | include_package_data=True, 28 | zip_safe=False, 29 | test_suite='nose.collector', 30 | install_requires=[ 31 | 'python-dateutil>=1.5', 32 | 'normality', 33 | 'six' 34 | ], 35 | tests_require=[ 36 | 'nose', 37 | 'coverage', 38 | 'wheel' 39 | ], 40 | entry_points={} 41 | ) 42 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | if __name__ == '__main__': 4 | unittest.main() 5 | -------------------------------------------------------------------------------- /test/test_converters.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import decimal 3 | from datetime import datetime 4 | 5 | import typecast 6 | from typecast import ConverterError 7 | from typecast import value, date, formats 8 | 9 | 10 | class ConvertersUnitTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | pass 14 | 15 | def test_name(self): 16 | n = typecast.name('string') 17 | assert n == 'string', n 18 | n = typecast.name(7) 19 | assert n == 'integer', n 20 | n = typecast.name(False) 21 | assert n == 'bool', n 22 | 23 | def test_utils(self): 24 | conv = value.String() 25 | conv2 = typecast.converter('string') 26 | assert type(conv2) == type(conv), \ 27 | typecast.converter('string') 28 | assert typecast.cast('string', 'foo') == 'foo' 29 | assert typecast.stringify('number', 7) == '7' 30 | 31 | def test_converter(self): 32 | conv1, conv2, conv3 = value.String(), value.String(), value.Integer() 33 | dateconv = date.Date(format='%Y-%m-%d') 34 | assert conv1 == conv2 35 | assert conv2 != conv3 36 | assert repr(conv1) == repr(conv2) 37 | assert '%Y' in repr(dateconv) 38 | assert dateconv != date.Date() 39 | 40 | def test_date_utils(self): 41 | dateconv = date.Date(format='%Y-%m-%d') 42 | dateconv2 = date.Date(format='%Y-%m-%d') 43 | dateconv3 = date.Date(format='%Y-%mZZ%d') 44 | assert dateconv == dateconv2 45 | assert dateconv != dateconv3 46 | assert dateconv2.test('huhu') == -1 47 | assert dateconv2.test('2009-01-12') 48 | 49 | def test_jts_spec(self): 50 | field = {'type': 'date', 'format': '%Y!%m!%d'} 51 | out = typecast.cast(field, '2009!04!12') 52 | assert out.year == 2009, out 53 | assert out.day == 12, out 54 | 55 | def test_cast_test(self): 56 | conv = value.Integer() 57 | assert conv.test(1) == 1 58 | assert conv.test('45454') == 1 59 | assert conv.test('45454djksdfj') == -1 60 | assert conv.test(None) == 0 61 | 62 | def test_configs(self): 63 | configs = list(typecast.instances()) 64 | lens = len(typecast.CONVERTERS) - 3 + len(date.Date.formats) + \ 65 | len(date.DateTime.formats) 66 | assert len(configs) == lens, (len(configs), lens) 67 | 68 | def test_none(self): 69 | conv = value.String() 70 | text = None 71 | assert conv.stringify(text) == text, conv.stringify(text) 72 | assert conv.cast(text) == text, conv.cast(text) 73 | 74 | def test_string(self): 75 | conv = value.String() 76 | text = 'This is a string' 77 | assert conv.stringify(text) == text, conv.stringify(text) 78 | assert conv.cast(text) == text, conv.cast(text) 79 | 80 | def test_integer(self): 81 | conv = value.Integer() 82 | num, text = 7842, '7842' 83 | assert conv.stringify(num) == text, conv.stringify(num) 84 | assert conv.cast(text) == num, conv.cast(text) 85 | 86 | def test_integer_null(self): 87 | conv = value.Integer() 88 | num, text = 0, '0' 89 | assert conv.test('0') == 1 90 | assert conv.stringify(num) == text, conv.stringify(num) 91 | assert conv.cast(text) == num, conv.cast(text) 92 | 93 | def test_float(self): 94 | conv = value.Float() 95 | num, text = 2.1, '2.1' 96 | assert conv.stringify(num) == text, conv.stringify(num) 97 | assert conv.cast(text) == num, conv.cast(text) 98 | assert conv.cast(num) == num, conv.cast(num) 99 | 100 | with self.assertRaises(ConverterError): 101 | conv.cast('banana') 102 | 103 | def test_decimal(self): 104 | conv = value.Decimal() 105 | num, text = decimal.Decimal(2.1), '2.1' 106 | assert float(conv.stringify(num)) == float(text), conv.stringify(num) 107 | val = float(conv.cast(text) - num) 108 | assert conv.test(text) == 1, text 109 | assert conv.test(num) == 1, text 110 | assert conv.test('banana') == -1, text 111 | assert val < 0.1, (conv.cast(text), val) 112 | 113 | with self.assertRaises(ConverterError): 114 | conv.cast('banana') 115 | 116 | def test_boolean(self): 117 | conv = value.Boolean() 118 | val, text = True, 'true' 119 | assert conv.stringify(val) == text, conv.stringify(val) 120 | assert conv.cast(text) == val, conv.cast(text) 121 | assert conv.cast(None) is None, conv.cast(None) 122 | 123 | conv = value.Boolean() 124 | val, text = False, 'false' 125 | assert conv.stringify(val) == text, conv.stringify(val) 126 | assert conv.cast(text) == val, conv.cast(text) 127 | 128 | def test_date(self): 129 | conv = date.Date() 130 | val = datetime(2015, 5, 23).date() 131 | text = val.isoformat() 132 | assert conv.stringify(val) == text, conv.stringify(val) 133 | assert conv.cast(text).isoformat() == text, \ 134 | (conv.cast(text), conv.cast(text).isoformat()) 135 | 136 | with self.assertRaises(ConverterError): 137 | conv.cast('banana') 138 | 139 | with self.assertRaises(ConverterError): 140 | conv.stringify('banana') 141 | 142 | def test_datetime(self): 143 | conv = date.DateTime() 144 | val = datetime.utcnow() 145 | text = val.isoformat() 146 | assert conv.stringify(val) == text, conv.stringify(val) 147 | assert conv.cast(text).isoformat() == text, \ 148 | conv.cast(text) 149 | 150 | with self.assertRaises(ConverterError): 151 | conv.cast('banana') 152 | 153 | def test_datetime_formats(self): 154 | conv = date.DateTime() 155 | dt = conv.cast('6/16/99 0:00') 156 | assert dt is not None, dt 157 | assert dt.year == 1999, dt 158 | 159 | regex = formats.format_regex('%m/%d/%y %H:%M') 160 | m = regex.match('6/16/99 0:00') 161 | assert m is not None, m 162 | m = regex.match('6/16/1999 0:00') 163 | assert m is None, m 164 | 165 | def test_formats_regex(self): 166 | regex = formats.format_regex('%Y-%m-%d') 167 | assert regex.match('2012-01-04') 168 | assert date.Date().test('2012-01-04') == 1 169 | assert date.Date(format='%Y-%m-%d').test('2012-01-02') == 1 170 | assert date.Date().test('2012-01-x04') == -1 171 | -------------------------------------------------------------------------------- /typecast/__init__.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | from typecast.value import String, Boolean, Integer, Float, Decimal 4 | from typecast.date import DateTime, Date 5 | from typecast.guess import TypeGuesser, GUESS_TYPES 6 | from typecast.converter import ConverterError # noqa 7 | from typecast.name import name # noqa 8 | 9 | CONVERTERS = [String, Boolean, Integer, Float, Decimal, DateTime, Date] 10 | 11 | TYPES = { 12 | 'any': String, 13 | 'string': String, 14 | 'text': String, 15 | 'integer': Integer, 16 | 'number': Float, 17 | 'float': Float, 18 | 'double': Float, 19 | 'decimal': Decimal, 20 | 'date': Date, 21 | 'datetime': DateTime, 22 | 'date-time': DateTime, 23 | 'boolean': Boolean, 24 | 'bool': Boolean 25 | } 26 | 27 | 28 | def _field_options(field, opts): 29 | if isinstance(field, dict): 30 | _extra = opts 31 | opts = deepcopy(field) 32 | opts.update(_extra) 33 | field = opts.get('type') 34 | return field, opts 35 | 36 | 37 | def converter(type_name): 38 | """Get a given converter by name, or raise an exception.""" 39 | converter = TYPES.get(type_name) 40 | if converter is None: 41 | raise ConverterError('Unknown converter: %r' % type_name) 42 | return converter() 43 | 44 | 45 | def cast(type_name, value, **opts): 46 | """Convert a given string to the type indicated by ``type_name``. 47 | 48 | If ``None`` is passed in, it will always be returned. 49 | Optional arguments can include ``true_values`` and ``false_values`` to 50 | describe boolean types, and ``format`` for dates. 51 | """ 52 | type_name, opts = _field_options(type_name, opts) 53 | return converter(type_name).cast(value, **opts) 54 | 55 | 56 | def stringify(type_name, value, **opts): 57 | """Generate a string representation of the data in ``value``. 58 | 59 | Based on the converter specified by ``type_name``. This is 60 | guaranteed to yield a form which can easily be parsed by ``cast()``. 61 | """ 62 | type_name, opts = _field_options(type_name, opts) 63 | return converter(type_name).stringify(value, **opts) 64 | 65 | 66 | def instances(): 67 | """Yield a set of possible configurations for the various types. 68 | 69 | This can be used to perform type-guessing by checking the applicability 70 | of each converter against a set of sample data. 71 | """ 72 | for converter in CONVERTERS: 73 | for inst in converter.instances(): 74 | yield inst 75 | 76 | 77 | def guesser(types=GUESS_TYPES, strict=False): 78 | """Create a type guesser for multiple values.""" 79 | return TypeGuesser(types=types, strict=strict) 80 | -------------------------------------------------------------------------------- /typecast/converter.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | 4 | class Converter(object): 5 | """A type converter for a value (such as a string or a number).""" 6 | 7 | result_type = None 8 | 9 | def __init__(self): 10 | self.opts = {} 11 | 12 | def _stringify(self, value, **opts): 13 | return six.text_type(value) 14 | 15 | def _cast(self, value, **opts): 16 | return six.text_type(value) 17 | 18 | def _is_null(self, value): 19 | """Check if an incoming value is ``None`` or the empty string.""" 20 | if isinstance(value, six.string_types): 21 | if not len(value.strip()): 22 | return True 23 | return value is None 24 | 25 | def test(self, value): 26 | try: 27 | out = self.cast(value) 28 | return 0 if out is None else 1 29 | except ConverterError: 30 | return -1 31 | 32 | @classmethod 33 | def test_class(cls, value): 34 | # This is a work-around to the date checker generating too many 35 | # tests because of the many formats. Instead, we're running one 36 | # big regex first, then do detailed checks. 37 | return True 38 | 39 | def cast(self, value, **opts): 40 | """Convert the given value to the target type. 41 | 42 | Return ``None`` if the value is empty. If an error occurs, 43 | raise a ``ConverterError``. 44 | """ 45 | if isinstance(value, self.result_type): 46 | return value 47 | if self._is_null(value): 48 | return None 49 | try: 50 | opts_ = self.opts.copy() 51 | opts_.update(opts) 52 | return self._cast(value, **opts_) 53 | except Exception as e: 54 | if not isinstance(e, ConverterError): 55 | e = ConverterError(None, exc=e) 56 | e.converter = self.__class__ 57 | raise e 58 | 59 | def stringify(self, value, **opts): 60 | """Generate a string representation of the data. 61 | 62 | Inverse of conversion: generate a string representation of the data 63 | that is guaranteed to be parseable by this library. 64 | """ 65 | if self._is_null(value): 66 | return None 67 | try: 68 | opts_ = self.opts.copy() 69 | opts_.update(opts) 70 | return self._stringify(value, **opts_) 71 | except Exception as e: 72 | if not isinstance(e, ConverterError): 73 | e = ConverterError(None, exc=e) 74 | e.converter = self.__class__ 75 | raise e 76 | 77 | @classmethod 78 | def instances(cls): 79 | yield cls() 80 | 81 | def __eq__(self, other): 82 | return self.__class__ == other.__class__ 83 | 84 | def __hash__(self): 85 | return hash(self.__class__) 86 | 87 | def __repr__(self): 88 | return '<%s()>' % self.__class__.__name__ 89 | 90 | def __unicode__(self): 91 | return self.__class__.__name__.lower() 92 | 93 | 94 | class ConverterError(TypeError): 95 | """A wrapper for all errors occuring in the process of conversion. 96 | 97 | This can also be caught as a ``TypeError``. 98 | """ 99 | 100 | def __init__(self, message, exc=None, converter=None): 101 | self.converter = converter 102 | self.exc = exc 103 | if message is None: 104 | if hasattr(exc, 'message'): 105 | message = exc.message 106 | elif exc is not None: 107 | message = six.text_type(exc) 108 | self.message = message 109 | -------------------------------------------------------------------------------- /typecast/date.py: -------------------------------------------------------------------------------- 1 | import re 2 | import six 3 | import dateutil.parser 4 | from datetime import datetime, date 5 | 6 | from typecast.formats import DATE_FORMATS, DATETIME_FORMATS 7 | from typecast.formats import format_regex, make_regex 8 | from typecast.converter import Converter 9 | 10 | 11 | class DateTime(Converter): 12 | """Convert a timestamp.""" 13 | 14 | result_type = datetime 15 | jts_name = 'datetime' 16 | guess_score = 5 17 | formats = DATETIME_FORMATS 18 | 19 | def __init__(self, format=None): 20 | self.opts = {'format': format} 21 | self.format = format 22 | 23 | def test(self, value): 24 | """Apply the regex for this format.""" 25 | if isinstance(value, six.string_types): 26 | if not len(value.strip()): 27 | return 0 28 | if format_regex(self.format): 29 | match = format_regex(self.format).match(value) 30 | return 1 if match is not None else -1 31 | return super(DateTime, self).test(value) 32 | 33 | def _stringify(self, value, **opts): 34 | return value.isoformat() 35 | 36 | def _cast(self, value, format=None, **opts): 37 | """Optionally apply a format string.""" 38 | if format is not None: 39 | return datetime.strptime(value, format) 40 | return dateutil.parser.parse(value) 41 | 42 | @classmethod 43 | def test_class(cls, value): 44 | if not hasattr(cls, '_formats_re'): 45 | formats = [make_regex(f) for f in cls.formats] 46 | formats = '|'.join(formats) 47 | cls._formats_re = re.compile('(%s)' % formats) 48 | return cls._formats_re.match(value) is not None 49 | 50 | @classmethod 51 | def instances(cls): 52 | return (cls(format=f) for f in cls.formats) 53 | 54 | def __eq__(self, other): 55 | if self.__class__ == other.__class__: 56 | if self.format and other.format: 57 | return self.format == other.format 58 | return True 59 | 60 | def __hash__(self): 61 | return hash(hash(self.__class__) + hash(self.format)) 62 | 63 | def __repr__(self): 64 | return '<%s(%r)>' % (self.__class__.__name__, self.format) 65 | 66 | 67 | class Date(DateTime): 68 | """Convert a date.""" 69 | 70 | result_type = date 71 | jts_name = 'date' 72 | guess_score = 4 73 | formats = DATE_FORMATS 74 | 75 | def _stringify(self, value, **opts): 76 | if isinstance(value, datetime): 77 | value = value.date() 78 | return value.isoformat() 79 | 80 | def _cast(self, value, **opts): 81 | dt = super(Date, self)._cast(value, **opts) 82 | return dt.date() if isinstance(dt, datetime) else dt 83 | -------------------------------------------------------------------------------- /typecast/formats.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | DATE_REGEXES = {} 4 | 5 | 6 | def sub_regex(match): 7 | char = match.group(1) 8 | if char in ['H', 'M', 'S', 'm', 'd', 'y']: 9 | return '\d{1,2}' 10 | if char in ['Y']: 11 | return '\d{4}' 12 | if char in ['z', 'b', 'B', 'Z']: 13 | return '\w{0,100}' 14 | if char in ['X']: 15 | return '[\d:]{0,10}' 16 | raise TypeError() 17 | 18 | 19 | def make_regex(fmt): 20 | format_re = re.sub(r'([\.\-])', r'\\\1', fmt) 21 | return re.sub('%(.)', sub_regex, format_re) 22 | 23 | 24 | def format_regex(fmt): 25 | if fmt not in DATE_REGEXES and fmt is not None: 26 | DATE_REGEXES[fmt] = re.compile(make_regex(fmt)) 27 | return DATE_REGEXES.get(fmt) 28 | 29 | 30 | def create_date_formats(): 31 | """Generate time and date formats with different delimeters.""" 32 | # European style: 33 | base_formats = ['%d %m %Y', '%d %m %y', '%Y %m %d'] 34 | # US style: 35 | base_formats += ['%m %d %Y', '%m %d %y', '%Y %m %d'] 36 | # Things with words in 37 | base_formats += ['%d %b %Y', '%d %B %Y'] 38 | 39 | date_formats = [] 40 | for separator in ('-', '.', '/', ' '): 41 | for date_format in base_formats: 42 | date_formats.append((date_format.replace(' ', separator))) 43 | 44 | datetime_formats = [] 45 | time_formats = ('%H:%M%Z', '%H:%M:%S', '%H:%M:%S%Z', '%H:%M%z', 46 | '%H:%M:%S%z') 47 | 48 | for date_format in date_formats: 49 | for time_format in time_formats: 50 | for separator in ('', 'T', ' '): 51 | datetime_formats.append(date_format + separator + time_format) 52 | 53 | return tuple(date_formats), tuple(datetime_formats) 54 | 55 | 56 | DATE_FORMATS, DATETIME_FORMATS = create_date_formats() 57 | -------------------------------------------------------------------------------- /typecast/guess.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from typecast.value import String, Integer, Decimal, Boolean 4 | from typecast.date import Date, DateTime 5 | 6 | GUESS_TYPES = [String, Decimal, Integer, Boolean, Date, DateTime] 7 | FAILED = 'failed' 8 | 9 | 10 | class TypeGuesser(object): 11 | """Guess the best matching type for a series of values.""" 12 | 13 | default = String() 14 | 15 | def __init__(self, types=GUESS_TYPES, strict=False): 16 | self.scores = defaultdict(int) 17 | self.instances = [i for t in types for i in t.instances()] 18 | self.strict = strict 19 | 20 | def add(self, value): 21 | if self.default._is_null(value): 22 | return 23 | classes = {} 24 | for inst in self.instances: 25 | cls = type(inst) 26 | if cls not in classes: 27 | classes[cls] = inst.test_class(value) 28 | if self.scores[inst] is FAILED or not classes[cls]: 29 | continue 30 | 31 | result = inst.test(value) 32 | if self.strict and (result == -1): 33 | if not isinstance(inst, String): 34 | self.scores[inst] = FAILED 35 | elif result == 1: 36 | self.scores[inst] += inst.guess_score 37 | 38 | @property 39 | def best(self): 40 | best_score = 0 41 | best_inst = self.default 42 | for inst in self.instances: 43 | score = self.scores.get(inst, 0) 44 | if score is not FAILED and score > best_score: 45 | best_score = score 46 | best_inst = inst 47 | return best_inst 48 | -------------------------------------------------------------------------------- /typecast/name.py: -------------------------------------------------------------------------------- 1 | import six 2 | from datetime import datetime, date 3 | from decimal import Decimal 4 | 5 | 6 | TESTS = ((six.text_type, 'string'), 7 | (bool, 'bool'), 8 | (int, 'integer'), 9 | (float, 'float'), 10 | (date, 'date'), 11 | (datetime, 'datetime'), 12 | (Decimal, 'decimal')) 13 | 14 | 15 | def name(value): 16 | """Get the string title for a particular type. 17 | 18 | Given a value, get an appropriate string title for the type that can 19 | be used to re-cast the value later. 20 | """ 21 | if value is None: 22 | return 'any' 23 | for (test, name) in TESTS: 24 | if isinstance(value, test): 25 | return name 26 | return 'string' 27 | -------------------------------------------------------------------------------- /typecast/value.py: -------------------------------------------------------------------------------- 1 | import re 2 | import six 3 | import sys 4 | import decimal 5 | import locale 6 | 7 | from typecast.converter import Converter, ConverterError 8 | 9 | 10 | class String(Converter): 11 | """String.""" 12 | 13 | result_type = six.text_type 14 | jts_name = 'string' 15 | guess_score = 1 16 | allow_empty = True 17 | 18 | 19 | class Integer(Converter): 20 | """Integer.""" 21 | 22 | result_type = int 23 | jts_name = 'integer' 24 | guess_score = 6 25 | 26 | def _cast(self, value, **opts): 27 | try: 28 | if hasattr(value, 'is_integer') and not value.is_integer(): 29 | raise ConverterError('Invalid integer: %r' % value) 30 | return int(value) 31 | except: 32 | try: 33 | return int(locale.atoi(value)) 34 | except: 35 | raise ConverterError('Invalid integer: %r' % value) 36 | 37 | 38 | class Boolean(Converter): 39 | """A boolean field. 40 | 41 | Matches true/false, yes/no and 0/1 by default, 42 | but a custom set of values can be optionally provided. 43 | """ 44 | 45 | result_type = bool 46 | jts_name = 'boolean' 47 | guess_score = 7 48 | true_values = ('t', 'yes', 'y', 'true', 'aye') 49 | false_values = ('f', 'no', 'n', 'false', 'nay') 50 | 51 | def _stringify(self, value, **opts): 52 | return six.text_type(value).lower() 53 | 54 | def _cast(self, value, true_values=None, false_values=None, **opts): 55 | if isinstance(value, six.string_types): 56 | value = value.lower().strip() 57 | 58 | true_values = true_values or self.true_values 59 | if value in true_values: 60 | return True 61 | 62 | false_values = false_values or self.false_values 63 | if value in false_values: 64 | return False 65 | 66 | 67 | class Float(Converter): 68 | """Floating-point number.""" 69 | 70 | result_type = float 71 | jts_name = 'number' 72 | guess_score = 3 73 | 74 | def _cast(self, value, **opts): 75 | return float(value) 76 | 77 | @classmethod 78 | def instances(cls): 79 | # We don't want floats to be considered for type-testing, instead 80 | # use decimals (which are more conservative as a storage mechanism). 81 | return () 82 | 83 | 84 | class Decimal(Converter): 85 | """Decimal number, ``decimal.Decimal`` or float numbers.""" 86 | 87 | result_type = decimal.Decimal 88 | jts_name = 'number' 89 | guess_score = 3 90 | pattern = r'\s*[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?\s*' 91 | pattern = re.compile(pattern, re.M) 92 | 93 | def _stringify(self, value, **opts): 94 | return '{0:.7f}'.format(value) 95 | 96 | def _cast(self, value, **opts): 97 | try: 98 | return decimal.Decimal(value) 99 | except: 100 | value = locale.atof(value) 101 | if sys.version_info < (2, 7): 102 | value = str(value) 103 | return decimal.Decimal(value) 104 | --------------------------------------------------------------------------------