1
0
mirror of https://github.com/djohnlewis/stackdump synced 2025-01-22 22:51:36 +00:00
stackdump/python/packages/formencode/national.py

753 lines
24 KiB
Python

"""
Country specific validators for use with FormEncode.
"""
import re
from api import FancyValidator
from compound import Any
from validators import Regex, Invalid, _
try:
import pycountry
has_pycountry = True
except:
has_pycountry = False
try:
from turbogears.i18n import format as tgformat
has_turbogears = True
except:
has_turbogears = False
no_country = False
if not (has_pycountry or has_turbogears):
import warnings
no_country = ('Please easy_install pycountry or validators handling'
' country names and/or languages will not work.')
############################################################
## country lists and functions
############################################################
country_additions = [
('BY', _('Belarus')),
('ME', _('Montenegro')),
('AU', _('Tasmania')),
]
fuzzy_countrynames = [
('US', 'U.S.A'),
('US', 'USA'),
('GB', _('Britain')),
('GB', _('Great Britain')),
('CI', _('Cote de Ivoire')),
]
if has_turbogears:
def get_countries():
c1 = tgformat.get_countries('en')
c2 = tgformat.get_countries()
if len(c1) > len(c2):
d = dict(country_additions)
d.update(dict(c1))
d.update(dict(c2))
else:
d = dict(country_additions)
d.update(dict(c2))
ret = d.items() + fuzzy_countrynames
return ret
def get_country(code):
return dict(get_countries())[code]
def get_languages():
c1 = tgformat.get_languages('en')
c2 = tgformat.get_languages()
if len(c1) > len(c2):
d = dict(c1)
d.update(dict(c2))
return d.items()
else:
return c2
def get_language(code):
try:
return tgformat.get_language(code)
except KeyError:
return tgformat.get_language(code, 'en')
elif has_pycountry:
# @@ mark: interestingly, common gettext notation does not work here
import gettext
gettext.bindtextdomain('iso3166', pycountry.LOCALES_DIR)
_c = lambda t: gettext.dgettext('iso3166', t)
gettext.bindtextdomain('iso639', pycountry.LOCALES_DIR)
_l = lambda t: gettext.dgettext('iso639', t)
def get_countries():
c1 = set([(e.alpha2, _c(e.name)) for e in pycountry.countries])
ret = c1.union(country_additions + fuzzy_countrynames)
return ret
def get_country(code):
return _c(pycountry.countries.get(alpha2=code).name)
def get_languages():
return [(e.alpha2, _l(e.name)) for e in pycountry.languages
if e.name and getattr(e, 'alpha2', None)]
def get_language(code):
return _l(pycountry.languages.get(alpha2=code).name)
############################################################
## country, state and postal code validators
############################################################
class DelimitedDigitsPostalCode(Regex):
"""
Abstraction of common postal code formats, such as 55555, 55-555 etc.
With constant amount of digits. By providing a single digit as partition you
can obtain a trivial 'x digits' postal code validator.
::
>>> german = DelimitedDigitsPostalCode(5)
>>> german.to_python('55555')
'55555'
>>> german.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (5 digits)
>>> polish = DelimitedDigitsPostalCode([2, 3], '-')
>>> polish.to_python('55555')
'55-555'
>>> polish.to_python('55-555')
'55-555'
>>> polish.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (nn-nnn)
>>> nicaragua = DelimitedDigitsPostalCode([3, 3, 1], '-')
>>> nicaragua.to_python('5554443')
'555-444-3'
>>> nicaragua.to_python('555-4443')
'555-444-3'
>>> nicaragua.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (nnn-nnn-n)
"""
strip = True
def assembly_formatstring(self, partition_lengths, delimiter):
if len(partition_lengths) == 1:
return _('%d digits') % partition_lengths[0]
else:
return delimiter.join(['n'*l for l in partition_lengths])
def assembly_regex(self, partition_lengths, delimiter):
mg = [r'(\d{%d})' % l for l in partition_lengths]
rd = r'\%s?' % delimiter
return rd.join(mg)
def __init__(self, partition_lengths, delimiter = None,
*args, **kw):
if type(partition_lengths) == type(1):
partition_lengths = [partition_lengths]
if not delimiter:
delimiter = ''
self.format = self.assembly_formatstring(partition_lengths, delimiter)
self.regex = self.assembly_regex(partition_lengths, delimiter)
(self.partition_lengths, self.delimiter) = (partition_lengths, delimiter)
Regex.__init__(self, *args, **kw)
messages = dict(
invalid=_('Please enter a zip code (%(format)s)'))
def _to_python(self, value, state):
self.assert_string(value, state)
match = self.regex.search(value)
if not match:
raise Invalid(
self.message('invalid', state, format=self.format),
value, state)
return self.delimiter.join(match.groups())
def USPostalCode(*args, **kw):
"""
US Postal codes (aka Zip Codes).
::
>>> uspc = USPostalCode()
>>> uspc.to_python('55555')
'55555'
>>> uspc.to_python('55555-5555')
'55555-5555'
>>> uspc.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (5 digits)
"""
return Any(DelimitedDigitsPostalCode(5, None, *args, **kw),
DelimitedDigitsPostalCode([5, 4], '-', *args, **kw))
def GermanPostalCode(*args, **kw):
return DelimitedDigitsPostalCode(5, None, *args, **kw)
def FourDigitsPostalCode(*args, **kw):
return DelimitedDigitsPostalCode(4, None, *args, **kw)
def PolishPostalCode(*args, **kw):
return DelimitedDigitsPostalCode([2, 3], '-', *args, **kw)
class ArgentinianPostalCode(Regex):
"""
Argentinian Postal codes.
::
>>> ArgentinianPostalCode.to_python('C1070AAM')
'C1070AAM'
>>> ArgentinianPostalCode.to_python('c 1070 aam')
'C1070AAM'
>>> ArgentinianPostalCode.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (LnnnnLLL)
"""
regex = re.compile(r'^([a-zA-Z]{1})\s*(\d{4})\s*([a-zA-Z]{3})$')
strip = True
messages = dict(
invalid=_('Please enter a zip code (%s)') % _('LnnnnLLL'))
def _to_python(self, value, state):
self.assert_string(value, state)
match = self.regex.search(value)
if not match:
raise Invalid(
self.message('invalid', state),
value, state)
return '%s%s%s' % (match.group(1).upper(),
match.group(2),
match.group(3).upper())
class CanadianPostalCode(Regex):
"""
Canadian Postal codes.
::
>>> CanadianPostalCode.to_python('V3H 1Z7')
'V3H 1Z7'
>>> CanadianPostalCode.to_python('v3h1z7')
'V3H 1Z7'
>>> CanadianPostalCode.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a zip code (LnL nLn)
"""
regex = re.compile(r'^([a-zA-Z]\d[a-zA-Z])\s?(\d[a-zA-Z]\d)$')
strip = True
messages = dict(
invalid=_('Please enter a zip code (%s)') % _('LnL nLn'))
def _to_python(self, value, state):
self.assert_string(value, state)
match = self.regex.search(value)
if not match:
raise Invalid(
self.message('invalid', state),
value, state)
return '%s %s' % (match.group(1).upper(), match.group(2).upper())
class UKPostalCode(Regex):
"""
UK Postal codes. Please see BS 7666.
::
>>> UKPostalCode.to_python('BFPO 3')
'BFPO 3'
>>> UKPostalCode.to_python('LE11 3GR')
'LE11 3GR'
>>> UKPostalCode.to_python('l1a 3gr')
'L1A 3GR'
>>> UKPostalCode.to_python('5555')
Traceback (most recent call last):
...
Invalid: Please enter a valid postal code (for format see BS 7666)
"""
regex = re.compile(r'^((ASCN|BBND|BIQQ|FIQQ|PCRN|SIQQ|STHL|TDCU|TKCA) 1ZZ|BFPO (c\/o )?[1-9]{1,4}|GIR 0AA|[A-PR-UWYZ]([0-9]{1,2}|([A-HK-Y][0-9]|[A-HK-Y][0-9]([0-9]|[ABEHMNPRV-Y]))|[0-9][A-HJKS-UW]) [0-9][ABD-HJLNP-UW-Z]{2})$', re.I)
strip = True
messages = dict(
invalid=_('Please enter a valid postal code (for format see BS 7666)'))
def _to_python(self, value, state):
self.assert_string(value, state)
match = self.regex.search(value)
if not match:
raise Invalid(
self.message('invalid', state),
value, state)
return match.group(1).upper()
class CountryValidator(FancyValidator):
"""
Will convert a country's name into its ISO-3166 abbreviation for unified
storage in databases etc. and return a localized country name in the
reverse step.
@See http://www.iso.org/iso/country_codes/iso_3166_code_lists.htm
::
>>> CountryValidator.to_python('Germany')
'DE'
>>> CountryValidator.to_python('Finland')
'FI'
>>> CountryValidator.to_python('UNITED STATES')
'US'
>>> CountryValidator.to_python('Krakovia')
Traceback (most recent call last):
...
Invalid: That country is not listed in ISO 3166
>>> CountryValidator.from_python('DE')
'Germany'
>>> CountryValidator.from_python('FI')
'Finland'
"""
key_ok = True
messages = dict(
valueNotFound=_('That country is not listed in ISO 3166'))
def __init__(self, *args, **kw):
FancyValidator.__init__(self, *args, **kw)
if no_country:
warnings.warn(no_country, Warning, 2)
def _to_python(self, value, state):
upval = value.upper()
if self.key_ok:
try:
c = get_country(upval)
return upval
except:
pass
for k, v in get_countries():
if v.upper() == upval:
return k
raise Invalid(self.message('valueNotFound', state), value, state)
def _from_python(self, value, state):
try:
return get_country(value.upper())
except KeyError:
return value
class PostalCodeInCountryFormat(FancyValidator):
"""
Makes sure the postal code is in the country's format by chosing postal
code validator by provided country code. Does convert it into the preferred
format, too.
::
>>> fs = PostalCodeInCountryFormat('country', 'zip')
>>> fs.to_python(dict(country='DE', zip='30167'))
{'country': 'DE', 'zip': '30167'}
>>> fs.to_python(dict(country='DE', zip='3008'))
Traceback (most recent call last):
...
Invalid: Given postal code does not match the country's format.
>>> fs.to_python(dict(country='PL', zip='34343'))
{'country': 'PL', 'zip': '34-343'}
>>> fs = PostalCodeInCountryFormat('staat', 'plz')
>>> fs.to_python(dict(staat='GB', plz='l1a 3gr'))
{'staat': 'GB', 'plz': 'L1A 3GR'}
"""
country_field = 'country'
zip_field = 'zip'
__unpackargs__ = ('country_field', 'zip_field')
messages = dict(
badFormat=_("Given postal code does not match the country's format."))
_vd = {
'AR': ArgentinianPostalCode,
'AT': FourDigitsPostalCode,
'BE': FourDigitsPostalCode,
'BG': FourDigitsPostalCode,
'CA': CanadianPostalCode,
'CL': lambda: DelimitedDigitsPostalCode(7),
'CN': lambda: DelimitedDigitsPostalCode(6),
'CR': FourDigitsPostalCode,
'DE': GermanPostalCode,
'DK': FourDigitsPostalCode,
'DO': lambda: DelimitedDigitsPostalCode(5),
'ES': lambda: DelimitedDigitsPostalCode(5),
'FI': lambda: DelimitedDigitsPostalCode(5),
'FR': lambda: DelimitedDigitsPostalCode(5),
'GB': UKPostalCode,
'GF': lambda: DelimitedDigitsPostalCode(5),
'GR': lambda: DelimitedDigitsPostalCode([2, 3], ' '),
'HN': lambda: DelimitedDigitsPostalCode(5),
'HT': FourDigitsPostalCode,
'HU': FourDigitsPostalCode,
'IS': lambda: DelimitedDigitsPostalCode(3),
'IT': lambda: DelimitedDigitsPostalCode(5),
'JP': lambda: DelimitedDigitsPostalCode([3, 4], '-'),
'KR': lambda: DelimitedDigitsPostalCode([3, 3], '-'),
'LI': FourDigitsPostalCode,
'LU': FourDigitsPostalCode,
'MC': lambda: DelimitedDigitsPostalCode(5),
'NI': lambda: DelimitedDigitsPostalCode([3, 3, 1], '-'),
'NO': FourDigitsPostalCode,
'PL': PolishPostalCode,
'PT': lambda: DelimitedDigitsPostalCode([4, 3], '-'),
'PY': FourDigitsPostalCode,
'RO': lambda: DelimitedDigitsPostalCode(6),
'SE': lambda: DelimitedDigitsPostalCode([3, 2], ' '),
'SG': lambda: DelimitedDigitsPostalCode(6),
'US': USPostalCode,
'UY': lambda: DelimitedDigitsPostalCode(5),
}
def validate_python(self, fields_dict, state):
if fields_dict[self.country_field] in self._vd:
try:
zip_validator = self._vd[fields_dict[self.country_field]]()
fields_dict[self.zip_field] = zip_validator.to_python(
fields_dict[self.zip_field])
except Invalid, e:
message = self.message('badFormat', state)
raise Invalid(message, fields_dict, state,
error_dict={self.zip_field: e.msg,
self.country_field: message})
class USStateProvince(FancyValidator):
"""
Valid state or province code (two-letter).
Well, for now I don't know the province codes, but it does state
codes. Give your own `states` list to validate other state-like
codes; give `extra_states` to add values without losing the
current state values.
::
>>> s = USStateProvince('XX')
>>> s.to_python('IL')
'IL'
>>> s.to_python('XX')
'XX'
>>> s.to_python('xx')
'XX'
>>> s.to_python('YY')
Traceback (most recent call last):
...
Invalid: That is not a valid state code
"""
states = ['AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE',
'FL', 'GA', 'HI', 'IA', 'ID', 'IN', 'IL', 'KS', 'KY',
'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT',
'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH',
'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT',
'VA', 'VT', 'WA', 'WI', 'WV', 'WY']
extra_states = []
__unpackargs__ = ('extra_states',)
messages = dict(
empty=_('Please enter a state code'),
wrongLength=_('Please enter a state code with TWO letters'),
invalid=_('That is not a valid state code'))
def validate_python(self, value, state):
value = str(value).strip().upper()
if not value:
raise Invalid(
self.message('empty', state),
value, state)
if not value or len(value) != 2:
raise Invalid(
self.message('wrongLength', state),
value, state)
if value not in self.states and not (
self.extra_states and value in self.extra_states):
raise Invalid(
self.message('invalid', state),
value, state)
def _to_python(self, value, state):
return str(value).strip().upper()
############################################################
## phone number validators
############################################################
class USPhoneNumber(FancyValidator):
"""
Validates, and converts to ###-###-####, optionally with extension
(as ext.##...). Only support US phone numbers. See
InternationalPhoneNumber for support for that kind of phone number.
::
>>> p = USPhoneNumber()
>>> p.to_python('333-3333')
Traceback (most recent call last):
...
Invalid: Please enter a number, with area code, in the form ###-###-####, optionally with "ext.####"
>>> p.to_python('555-555-5555')
'555-555-5555'
>>> p.to_python('1-393-555-3939')
'1-393-555-3939'
>>> p.to_python('321.555.4949')
'321.555.4949'
>>> p.to_python('3335550000')
'3335550000'
"""
# for emacs: "
_phoneRE = re.compile(r'^\s*(?:1-)?(\d\d\d)[\- \.]?(\d\d\d)[\- \.]?(\d\d\d\d)(?:\s*ext\.?\s*(\d+))?\s*$', re.I)
messages = dict(
phoneFormat=_('Please enter a number, with area code,'
' in the form ###-###-####, optionally with "ext.####"'))
def _to_python(self, value, state):
self.assert_string(value, state)
match = self._phoneRE.search(value)
if not match:
raise Invalid(
self.message('phoneFormat', state),
value, state)
return value
def _from_python(self, value, state):
self.assert_string(value, state)
match = self._phoneRE.search(value)
if not match:
raise Invalid(self.message('phoneFormat', state),
value, state)
result = '%s-%s-%s' % (match.group(1), match.group(2), match.group(3))
if match.group(4):
result += " ext.%s" % match.group(4)
return result
class InternationalPhoneNumber(FancyValidator):
"""
Validates, and converts phone numbers to +##-###-#######.
Adapted from RFC 3966
@param default_cc country code for prepending if none is provided
can be a paramerless callable
::
>>> c = InternationalPhoneNumber(default_cc=lambda: 49)
>>> c.to_python('0555/8114100')
'+49-555-8114100'
>>> p = InternationalPhoneNumber(default_cc=49)
>>> p.to_python('333-3333')
Traceback (most recent call last):
...
Invalid: Please enter a number, with area code, in the form +##-###-#######.
>>> p.to_python('0555/4860-300')
'+49-555-4860-300'
>>> p.to_python('0555-49924-51')
'+49-555-49924-51'
>>> p.to_python('0555 / 8114100')
'+49-555-8114100'
>>> p.to_python('0555/8114100')
'+49-555-8114100'
>>> p.to_python('0555 8114100')
'+49-555-8114100'
>>> p.to_python(' +49 (0)555 350 60 0')
'+49-555-35060-0'
>>> p.to_python('+49 555 350600')
'+49-555-350600'
>>> p.to_python('0049/ 555/ 871 82 96')
'+49-555-87182-96'
>>> p.to_python('0555-2 50-30')
'+49-555-250-30'
>>> p.to_python('0555 43-1200')
'+49-555-43-1200'
>>> p.to_python('(05 55)4 94 33 47')
'+49-555-49433-47'
>>> p.to_python('(00 48-555)2 31 72 41')
'+48-555-23172-41'
>>> p.to_python('+973-555431')
'+973-555431'
>>> p.to_python('1-393-555-3939')
'+1-393-555-3939'
>>> p.to_python('+43 (1) 55528/0')
'+43-1-55528-0'
>>> p.to_python('+43 5555 429 62-0')
'+43-5555-42962-0'
>>> p.to_python('00 218 55 33 50 317 321')
'+218-55-3350317-321'
>>> p.to_python('+218 (0)55-3636639/38')
'+218-55-3636639-38'
>>> p.to_python('032 555555 367')
'+49-32-555555-367'
>>> p.to_python('(+86) 555 3876693')
'+86-555-3876693'
"""
strip = True
# Use if there's a default country code you want to use:
default_cc = None
_mark_chars_re = re.compile(r"[_.!~*'/]")
_preTransformations = [
(re.compile(r'^(\(?)(?:00\s*)(.+)$'), '%s+%s'),
(re.compile(r'^\(\s*(\+?\d+)\s*(\d+)\s*\)(.+)$'), '(%s%s)%s'),
(re.compile(r'^\((\+?[-\d]+)\)\s?(\d.+)$'), '%s-%s'),
(re.compile(r'^(?:1-)(\d+.+)$'), '+1-%s'),
(re.compile(r'^(\+\d+)\s+\(0\)\s*(\d+.+)$'), '%s-%s'),
(re.compile(r'^([0+]\d+)[-\s](\d+)$'), '%s-%s'),
(re.compile(r'^([0+]\d+)[-\s](\d+)[-\s](\d+)$'), '%s-%s-%s'),
]
_ccIncluder = [
(re.compile(r'^\(?0([1-9]\d*)[-)](\d.*)$'), '+%d-%s-%s'),
]
_postTransformations = [
(re.compile(r'^(\+\d+)[-\s]\(?(\d+)\)?[-\s](\d+.+)$'), '%s-%s-%s'),
(re.compile(r'^(.+)\s(\d+)$'), '%s-%s'),
]
_phoneIsSane = re.compile(r'^(\+[1-9]\d*)-([\d\-]+)$')
messages = dict(
phoneFormat=_('Please enter a number, with area code,'
' in the form +##-###-#######.'))
def _perform_rex_transformation(self, value, transformations):
for rex, trf in transformations:
match = rex.search(value)
if match:
value = trf % match.groups()
return value
def _prepend_country_code(self, value, transformations, country_code):
for rex, trf in transformations:
match = rex.search(value)
if match:
return trf % ((country_code,)+match.groups())
return value
def _to_python(self, value, state):
self.assert_string(value, state)
try:
value = value.encode('ascii', 'replace')
except:
raise Invalid(self.message('phoneFormat', state), value, state)
value = self._mark_chars_re.sub('-', value)
for f, t in [(' ', ' '),
('--', '-'), (' - ', '-'), ('- ', '-'), (' -', '-')]:
value = value.replace(f, t)
value = self._perform_rex_transformation(value, self._preTransformations)
if self.default_cc:
if callable(self.default_cc):
cc = self.default_cc()
else:
cc = self.default_cc
value = self._prepend_country_code(value, self._ccIncluder, cc)
value = self._perform_rex_transformation(value, self._postTransformations)
value = value.replace(' ', '')
# did we successfully transform that phone number? Thus, is it valid?
if not self._phoneIsSane.search(value):
raise Invalid(self.message('phoneFormat', state), value, state)
return value
############################################################
## language validators
############################################################
class LanguageValidator(FancyValidator):
"""
Converts a given language into its ISO 639 alpha 2 code, if there is any.
Returns the language's full name in the reverse.
Warning: ISO 639 neither differentiates between languages such as Cantonese
and Mandarin nor does it contain all spoken languages. E.g., Lechitic
languages are missing.
Warning: ISO 639 is a smaller subset of ISO 639-2
@param key_ok accept the language's code instead of its name for input
defaults to True
::
>>> l = LanguageValidator()
>>> l.to_python('German')
'de'
>>> l.to_python('Chinese')
'zh'
>>> l.to_python('Klingonian')
Traceback (most recent call last):
...
Invalid: That language is not listed in ISO 639
>>> l.from_python('de')
'German'
>>> l.from_python('zh')
'Chinese'
"""
key_ok = True
messages = dict(
valueNotFound=_('That language is not listed in ISO 639'))
def __init__(self, *args, **kw):
FancyValidator.__init__(self, *args, **kw)
if no_country:
warnings.warn(no_country, Warning, 2)
def _to_python(self, value, state):
upval = value.upper()
if self.key_ok:
try:
c = get_language(value)
return value
except:
pass
for k, v in get_languages():
if v.upper() == upval:
return k
raise Invalid(self.message('valueNotFound', state), value, state)
def _from_python(self, value, state):
try:
return get_language(value.lower())
except KeyError:
return value