docs: def validate_arg(arg, argdef):
          """
          Validate an incoming (unicode) string argument according the UPnP spec. Raises UPNPError.
          """
          datatype = argdef['datatype']
          reasons = set()
          ranges = {
              'ui1': (int, 0, 255),
              'ui2': (int, 0, 65535),
              'ui4': (int, 0, 4294967295),
              'i1': (int, -128, 127),
              'i2': (int, -32768, 32767),
              'i4': (int, -2147483648, 2147483647),
              'r4': (Decimal, Decimal('3.40282347E+38'), Decimal('1.17549435E-38'))
          }
          try:
              if datatype in set(ranges.keys()):
                  v_type, v_min, v_max = ranges[datatype]
                  if not v_min <= v_type(arg) <= v_max:
                      reasons.add('%r datatype must be a number in the range %s to %s' % (
                          datatype, v_min, v_max))
  
              elif datatype in {'r8', 'number', 'float', 'fixed.14.4'}:
                  v = Decimal(arg)
                  if v < 0:
                      assert Decimal('-1.79769313486232E308') <= v <= Decimal('4.94065645841247E-324')
                  else:
                      assert Decimal('4.94065645841247E-324') <= v <= Decimal('1.79769313486232E308')
  
              elif datatype == 'char':
                  v = arg.decode('utf8') if six.PY2 or isinstance(arg, bytes) else arg
                  assert len(v) == 1
  
              elif datatype == 'string':
                  v = arg.decode("utf8") if six.PY2 or isinstance(arg, bytes) else arg
                  if argdef['allowed_values'] and v not in argdef['allowed_values']:
                      reasons.add('Value %r not in allowed values list' % arg)
  
              elif datatype == 'date':
                  v = parse_date(arg)
                  if any((v.hour, v.minute, v.second)):
                      reasons.add("'date' datatype must not contain a time")
  
              elif datatype in ('dateTime', 'dateTime.tz'):
                  v = parse_date(arg)
                  if datatype == 'dateTime' and v.tzinfo is not None:
                      reasons.add("'dateTime' datatype must not contain a timezone")
  
              elif datatype in ('time', 'time.tz'):
                  now = datetime.datetime.utcnow()
                  v = parse_date(arg, default=now)
                  if v.tzinfo is not None:
                      now += v.utcoffset()
                  if not all((
                          v.day == now.day,
                          v.month == now.month,
                          v.year == now.year)):
                      reasons.add('%r datatype must not contain a date' % datatype)
                  if datatype == 'time' and v.tzinfo is not None:
                      reasons.add('%r datatype must not have timezone information' % datatype)
  
              elif datatype == 'boolean':
                  valid = {'true', 'yes', '1', 'false', 'no', '0'}
                  if arg.lower() not in valid:
                      reasons.add('%r datatype must be one of %s' % (datatype, ','.join(valid)))
  
              elif datatype == 'bin.base64':
                  b64decode(arg)
  
              elif datatype == 'bin.hex':
                  unhexlify(arg)
  
              elif datatype == 'uri':
                  urlparse(arg)
  
              elif datatype == 'uuid':
                  if not re.match(
                          r'^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$',
                          arg, re.I):
                      reasons.add('%r datatype must contain a valid UUID')
  
              else:
                  reasons.add("%r datatype is unrecognised." % datatype)
  
          except ValueError as exc:
              reasons.add(str(exc))
  
          return not bool(len(reasons)), reasons