This is an automated email from the ASF dual-hosted git repository.

fokko pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/master by this push:
     new 81605f7  AVRO-1837: Logical Types: date, time & timestamp (#545)
81605f7 is described below

commit 81605f7d6467e687c518be5c3097521edcbb9494
Author: Michael A. Smith <[email protected]>
AuthorDate: Wed Jun 19 08:27:14 2019 -0400

    AVRO-1837: Logical Types: date, time & timestamp (#545)
    
    * AVRO-1837: Logicaltype support for date, time and timestamp
    
    * AVRO-1837: add timezones.py file
    
    * AVRO-1837: fix init file in py
    
    closes #207
    closes #82
---
 lang/py/src/avro/__init__.py                   |   2 +-
 lang/py/src/avro/{__init__.py => constants.py} |  19 ++-
 lang/py/src/avro/io.py                         | 172 ++++++++++++++++++++++++-
 lang/py/src/avro/schema.py                     | 106 ++++++++++++++-
 lang/py/src/avro/{__init__.py => timezones.py} |  30 ++++-
 lang/py/test/test_io.py                        |  36 +++++-
 lang/py/test/test_schema.py                    |  66 ++++++++++
 7 files changed, 416 insertions(+), 15 deletions(-)

diff --git a/lang/py/src/avro/__init__.py b/lang/py/src/avro/__init__.py
index da51d9b..d7e2896 100644
--- a/lang/py/src/avro/__init__.py
+++ b/lang/py/src/avro/__init__.py
@@ -14,5 +14,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__all__ = ['schema', 'io', 'datafile', 'protocol', 'ipc']
+__all__ = ['schema', 'io', 'datafile', 'protocol', 'ipc', 'constants', 
'timezones']
 
diff --git a/lang/py/src/avro/__init__.py b/lang/py/src/avro/constants.py
similarity index 69%
copy from lang/py/src/avro/__init__.py
copy to lang/py/src/avro/constants.py
index da51d9b..fa0b7d3 100644
--- a/lang/py/src/avro/__init__.py
+++ b/lang/py/src/avro/constants.py
@@ -14,5 +14,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__all__ = ['schema', 'io', 'datafile', 'protocol', 'ipc']
+"""
+Contains Constants for Python Avro
+"""
 
+DATE = "date"
+DECIMAL = "decimal"
+TIMESTAMP_MICROS = "timestamp-micros"
+TIMESTAMP_MILLIS = "timestamp-millis"
+TIME_MICROS = "time-micros"
+TIME_MILLIS = "time-millis"
+
+SUPPORTED_LOGICAL_TYPE = [
+    DATE,
+    DECIMAL,
+    TIMESTAMP_MICROS,
+    TIMESTAMP_MILLIS,
+    TIME_MICROS,
+    TIME_MILLIS
+]
diff --git a/lang/py/src/avro/io.py b/lang/py/src/avro/io.py
index d978716..a5d4f4c 100644
--- a/lang/py/src/avro/io.py
+++ b/lang/py/src/avro/io.py
@@ -38,8 +38,11 @@ uses the following mapping:
 """
 import struct
 from avro import schema
+from avro import constants
+from avro import timezones
 import sys
 from binascii import crc32
+import datetime
 
 from decimal import Decimal
 from decimal import getcontext
@@ -103,6 +106,9 @@ class SchemaResolutionException(schema.AvroException):
 # Validate
 #
 
+def _is_timezone_aware_datetime(dt):
+  return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
+
 def validate(expected_schema, datum):
   """Determine if a python datum is an instance of a schema."""
   schema_type = expected_schema.type
@@ -118,10 +124,20 @@ def validate(expected_schema, datum):
       return isinstance(datum, Decimal)
     return isinstance(datum, str)
   elif schema_type == 'int':
-    return ((isinstance(datum, int) or isinstance(datum, long)) 
+    if hasattr(expected_schema, 'logical_type'):
+      if expected_schema.logical_type == constants.DATE:
+        return isinstance(datum, datetime.date)
+      elif expected_schema.logical_type == constants.TIME_MILLIS:
+        return isinstance(datum, datetime.time)
+    return (isinstance(datum, (int, long))
             and INT_MIN_VALUE <= datum <= INT_MAX_VALUE)
   elif schema_type == 'long':
-    return ((isinstance(datum, int) or isinstance(datum, long)) 
+    if hasattr(expected_schema, 'logical_type'):
+      if expected_schema.logical_type == constants.TIME_MICROS:
+        return isinstance(datum, datetime.time)
+      elif expected_schema.logical_type in [constants.TIMESTAMP_MILLIS, 
constants.TIMESTAMP_MICROS]:
+        return isinstance(datum, datetime.datetime) and 
_is_timezone_aware_datetime(datum)
+    return (isinstance(datum, (int, long))
             and LONG_MIN_VALUE <= datum <= LONG_MAX_VALUE)
   elif schema_type in ['float', 'double']:
     return (isinstance(datum, int) or isinstance(datum, long)
@@ -267,6 +283,66 @@ class BinaryDecoder(object):
     """
     return unicode(self.read_bytes(), "utf-8")
 
+  def read_date_from_int(self):
+    """
+    int is decoded as python date object.
+    int stores the number of days from
+    the unix epoch, 1 January 1970 (ISO calendar).
+    """
+    days_since_epoch = self.read_int()
+    return datetime.date(1970, 1, 1) + datetime.timedelta(days_since_epoch)
+
+  def _build_time_object(self, value, scale_to_micro):
+    value = value * scale_to_micro
+    value, microseconds =  value // 1000000, value % 1000000
+    value, seconds = value // 60, value % 60
+    value, minutes = value // 60, value % 60
+    hours = value
+
+    return datetime.time(
+      hour=hours,
+      minute=minutes,
+      second=seconds,
+      microsecond=microseconds
+    )
+
+  def read_time_millis_from_int(self):
+    """
+    int is decoded as python time object which represents 
+    the number of milliseconds after midnight, 00:00:00.000.
+    """
+    milliseconds = self.read_int()
+    return self._build_time_object(milliseconds, 1000)
+
+  def read_time_micros_from_long(self):
+    """
+    long is decoded as python time object which represents 
+    the number of microseconds after midnight, 00:00:00.000000.
+    """
+    microseconds = self.read_long()
+    return self._build_time_object(microseconds, 1)
+
+  def read_timestamp_millis_from_long(self):
+    """
+    long is decoded as python datetime object which represents 
+    the number of milliseconds from the unix epoch, 1 January 1970.
+    """
+    timestamp_millis = self.read_long()
+    timedelta = datetime.timedelta(microseconds=timestamp_millis * 1000)
+    unix_epoch_datetime = datetime.datetime(1970, 1, 1, 0, 0, 0, 0, 
tzinfo=timezones.utc) 
+    return unix_epoch_datetime + timedelta
+
+  def read_timestamp_micros_from_long(self):
+    """
+    long is decoded as python datetime object which represents 
+    the number of microseconds from the unix epoch, 1 January 1970.
+    """
+    timestamp_micros = self.read_long()
+    timedelta = datetime.timedelta(microseconds=timestamp_micros)
+    unix_epoch_datetime = datetime.datetime(1970, 1, 1, 0, 0, 0, 0, 
tzinfo=timezones.utc)
+    return unix_epoch_datetime + timedelta
+
+
   def check_crc32(self, bytes):
     checksum = STRUCT_CRC32.unpack(self.read(4))[0];
     if crc32(bytes) & 0xffffffff != checksum:
@@ -453,6 +529,56 @@ class BinaryEncoder(object):
     """
     self.write(STRUCT_CRC32.pack(crc32(bytes) & 0xffffffff));
 
+  def write_date_int(self, datum):
+    """
+    Encode python date object as int.
+    It stores the number of days from
+    the unix epoch, 1 January 1970 (ISO calendar).
+    """
+    delta_date = datum - datetime.date(1970, 1, 1)
+    self.write_int(delta_date.days)
+
+  def write_time_millis_int(self, datum):
+    """
+    Encode python time object as int.
+    It stores the number of milliseconds from midnight, 00:00:00.000
+    """
+    milliseconds = datum.hour*3600000 + datum.minute * 60000 + datum.second * 
1000 + datum.microsecond // 1000
+    self.write_int(milliseconds)
+
+  def write_time_micros_long(self, datum):
+    """
+    Encode python time object as long.
+    It stores the number of microseconds from midnight, 00:00:00.000000
+    """
+    microseconds = datum.hour*3600000000 + datum.minute * 60000000 + 
datum.second * 1000000 + datum.microsecond
+    self.write_long(microseconds)
+
+  def _timedelta_total_microseconds(self, timedelta):
+    return (
+        timedelta.microseconds + (timedelta.seconds + timedelta.days * 24 * 
3600) * 10 ** 6)
+
+  def write_timestamp_millis_long(self, datum):
+    """
+    Encode python datetime object as long.
+    It stores the number of milliseconds from midnight of unix epoch, 1 
January 1970.
+    """
+    datum = datum.astimezone(tz=timezones.utc)
+    timedelta = datum - datetime.datetime(1970, 1, 1, 0, 0, 0, 0, 
tzinfo=timezones.utc)
+    milliseconds = self._timedelta_total_microseconds(timedelta) / 1000
+    self.write_long(long(milliseconds))
+
+  def write_timestamp_micros_long(self, datum):
+    """
+    Encode python datetime object as long.
+    It stores the number of microseconds from midnight of unix epoch, 1 
January 1970.
+    """
+    datum = datum.astimezone(tz=timezones.utc)
+    timedelta = datum - datetime.datetime(1970, 1, 1, 0, 0, 0, 0, 
tzinfo=timezones.utc)
+    microseconds = self._timedelta_total_microseconds(timedelta)
+    self.write_long(long(microseconds))
+
+
 #
 # DatumReader/Writer
 #
@@ -558,9 +684,26 @@ class DatumReader(object):
     elif writers_schema.type == 'string':
       return decoder.read_utf8()
     elif writers_schema.type == 'int':
-      return decoder.read_int()
+      if (hasattr(writers_schema, 'logical_type') and
+          writers_schema.logical_type == constants.DATE):
+        return decoder.read_date_from_int()
+      elif (hasattr(writers_schema, 'logical_type') and
+        writers_schema.logical_type == constants.TIME_MILLIS):
+        return decoder.read_time_millis_from_int()
+      else:
+        return decoder.read_int()
     elif writers_schema.type == 'long':
-      return decoder.read_long()
+      if (hasattr(writers_schema, 'logical_type') and 
+          writers_schema.logical_type == constants.TIME_MICROS):
+        return decoder.read_time_micros_from_long()
+      elif (hasattr(writers_schema, 'logical_type') and 
+            writers_schema.logical_type == constants.TIMESTAMP_MILLIS):
+        return decoder.read_timestamp_millis_from_long()
+      elif (hasattr(writers_schema, 'logical_type') and 
+            writers_schema.logical_type == constants.TIMESTAMP_MICROS):
+        return decoder.read_timestamp_micros_from_long()
+      else:
+        return decoder.read_long()
     elif writers_schema.type == 'float':
       return decoder.read_float()
     elif writers_schema.type == 'double':
@@ -884,9 +1027,26 @@ class DatumWriter(object):
     elif writers_schema.type == 'string':
       encoder.write_utf8(datum)
     elif writers_schema.type == 'int':
-      encoder.write_int(datum)
+      if (hasattr(writers_schema, 'logical_type') and
+          writers_schema.logical_type == constants.DATE):
+        encoder.write_date_int(datum)
+      elif (hasattr(writers_schema, 'logical_type') and
+            writers_schema.logical_type == constants.TIME_MILLIS):
+        encoder.write_time_millis_int(datum)
+      else:
+        encoder.write_int(datum)
     elif writers_schema.type == 'long':
-      encoder.write_long(datum)
+      if (hasattr(writers_schema, 'logical_type') and
+          writers_schema.logical_type == constants.TIME_MICROS):
+        encoder.write_time_micros_long(datum)
+      elif (hasattr(writers_schema, 'logical_type') and
+            writers_schema.logical_type == constants.TIMESTAMP_MILLIS):
+        encoder.write_timestamp_millis_long(datum)
+      elif (hasattr(writers_schema, 'logical_type') and
+            writers_schema.logical_type == constants.TIMESTAMP_MICROS):
+        encoder.write_timestamp_micros_long(datum)
+      else:
+        encoder.write_long(datum)
     elif writers_schema.type == 'float':
       encoder.write_float(datum)
     elif writers_schema.type == 'double':
diff --git a/lang/py/src/avro/schema.py b/lang/py/src/avro/schema.py
index 012109f..406ba56 100644
--- a/lang/py/src/avro/schema.py
+++ b/lang/py/src/avro/schema.py
@@ -41,6 +41,8 @@ try:
 except ImportError:
   import simplejson as json
 
+from avro import constants
+
 #
 # Constants
 #
@@ -347,7 +349,7 @@ class DecimalLogicalSchema(LogicalSchema):
       raise SchemaParseException("Invalid DECIMAL scale %s. Cannot be greater 
than precision %s"
                                  %(scale, precision))
 
-    LogicalSchema.__init__(self, 'decimal')
+    super(DecimalLogicalSchema, self).__init__('decimal')
 
   def _max_precision(self):
     raise NotImplementedError()
@@ -788,6 +790,91 @@ class RecordSchema(NamedSchema):
     to_cmp = json.loads(str(self))
     return to_cmp == json.loads(str(that))
 
+
+#
+# Logical Type
+#
+
+class LogicalSchema(object):
+   def __init__(self, logical_type):
+     self.logical_type = logical_type
+
+
+#
+# Date Type
+#
+
+class DateSchema(LogicalSchema, PrimitiveSchema):
+  def __init__(self, other_props=None):
+    LogicalSchema.__init__(self, constants.DATE)
+    PrimitiveSchema.__init__(self, 'int', other_props)
+
+  def to_json(self, names=None):
+    return self.props
+
+  def __eq__(self, that):
+    return self.props == that.props
+
+#
+# time-millis Type
+#
+
+class TimeMillisSchema(LogicalSchema, PrimitiveSchema):
+  def __init__(self, other_props=None):
+    LogicalSchema.__init__(self, constants.TIME_MILLIS)
+    PrimitiveSchema.__init__(self, 'int', other_props)
+
+  def to_json(self, names=None):
+    return self.props
+
+  def __eq__(self, that):
+    return self.props == that.props
+
+#
+# time-micros Type
+#
+
+class TimeMicrosSchema(LogicalSchema, PrimitiveSchema):
+  def __init__(self, other_props=None):
+    LogicalSchema.__init__(self, constants.TIME_MICROS)
+    PrimitiveSchema.__init__(self, 'long', other_props)
+
+  def to_json(self, names=None):
+    return self.props
+
+  def __eq__(self, that):
+    return self.props == that.props
+
+#
+# timestamp-millis Type
+#
+
+class TimestampMillisSchema(LogicalSchema, PrimitiveSchema):
+  def __init__(self, other_props=None):
+    LogicalSchema.__init__(self, constants.TIMESTAMP_MILLIS)
+    PrimitiveSchema.__init__(self, 'long', other_props)
+
+  def to_json(self, names=None):
+    return self.props
+
+  def __eq__(self, that):
+    return self.props == that.props
+
+#
+# timestamp-micros Type
+#
+
+class TimestampMicrosSchema(LogicalSchema, PrimitiveSchema):
+  def __init__(self, other_props=None):
+    LogicalSchema.__init__(self, constants.TIMESTAMP_MICROS)
+    PrimitiveSchema.__init__(self, 'long', other_props)
+
+  def to_json(self, names=None):
+    return self.props
+
+  def __eq__(self, that):
+    return self.props == that.props
+
 #
 # Module Methods
 #
@@ -817,11 +904,20 @@ def make_avsc_object(json_data, names=None):
     logical_type = None
     if 'logicalType' in json_data:
       logical_type = json_data.get('logicalType')
-      if logical_type != 'decimal':
-       raise SchemaParseException("Currently does not support %s logical type" 
% logical_type)
+      if logical_type not in constants.SUPPORTED_LOGICAL_TYPE:
+        raise SchemaParseException("Currently does not support %s logical 
type" % logical_type)
     if type in PRIMITIVE_TYPES:
-      if type == 'bytes':
-        if logical_type == 'decimal':
+      if type == 'int' and logical_type == constants.DATE:
+        return DateSchema(other_props)
+      if type == 'int' and logical_type == constants.TIME_MILLIS:
+        return TimeMillisSchema(other_props=other_props)
+      if type == 'long' and logical_type == constants.TIME_MICROS:
+        return TimeMicrosSchema(other_props=other_props)
+      if type == 'long' and logical_type == constants.TIMESTAMP_MILLIS:
+        return TimestampMillisSchema(other_props=other_props)
+      if type == 'long' and logical_type == constants.TIMESTAMP_MICROS:
+        return TimestampMicrosSchema(other_props=other_props)
+      if type == 'bytes' and logical_type == constants.DECIMAL:
           precision = json_data.get('precision')
           scale = 0 if json_data.get('scale') is None else 
json_data.get('scale')
           return BytesDecimalSchema(precision, scale, other_props)
diff --git a/lang/py/src/avro/__init__.py b/lang/py/src/avro/timezones.py
similarity index 60%
copy from lang/py/src/avro/__init__.py
copy to lang/py/src/avro/timezones.py
index da51d9b..f26c4c5 100644
--- a/lang/py/src/avro/__init__.py
+++ b/lang/py/src/avro/timezones.py
@@ -14,5 +14,33 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__all__ = ['schema', 'io', 'datafile', 'protocol', 'ipc']
+from datetime import datetime
+from datetime import timedelta
+from datetime import tzinfo
 
+
+class UTCTzinfo(tzinfo):
+  def utcoffset(self, dt):
+    return timedelta(0)
+
+  def tzname(self, dt):
+    return "UTC"
+
+  def dst(self, dt):
+    return timedelta(0)
+
+utc = UTCTzinfo()
+
+
+# Test Time Zone with fixed offset and no DST
+class TSTTzinfo(tzinfo):
+  def utcoffset(self, dt):
+    return timedelta(hours=10)
+
+  def tzname(self, dt):
+    return "TST"
+
+  def dst(self, dt):
+    return timedelta(0)
+
+tst = TSTTzinfo()
diff --git a/lang/py/test/test_io.py b/lang/py/test/test_io.py
index df8b180..e4c0ce2 100644
--- a/lang/py/test/test_io.py
+++ b/lang/py/test/test_io.py
@@ -22,11 +22,13 @@ try:
 except ImportError:
   from StringIO import StringIO
 from binascii import hexlify
+import datetime
 
 import set_avro_test_path
 
 from avro import schema
 from avro import io
+from avro import timezones
 
 SCHEMAS_TO_VALIDATE = (
   ('"null"', None),
@@ -48,6 +50,35 @@ SCHEMAS_TO_VALIDATE = (
   ('{"type": "array", "items": "long"}', [1, 3, 2]),
   ('{"type": "map", "values": "long"}', {'a': 1, 'b': 3, 'c': 2}),
   ('["string", "null", "long"]', None),
+  ('{"type": "int", "logicalType": "date"}', datetime.date(2000, 1, 1)),
+  ('{"type": "int", "logicalType": "time-millis"}', datetime.time(23, 59, 59, 
999000)),
+  ('{"type": "int", "logicalType": "time-millis"}', datetime.time(0, 0, 0, 
000000)),
+  ('{"type": "long", "logicalType": "time-micros"}', datetime.time(23, 59, 59, 
999999)),
+  ('{"type": "long", "logicalType": "time-micros"}', datetime.time(0, 0, 0, 
000000)),
+  (
+    '{"type": "long", "logicalType": "timestamp-millis"}',
+    datetime.datetime(1000, 1, 1, 0, 0, 0, 000000, tzinfo=timezones.utc)
+  ),
+  (
+    '{"type": "long", "logicalType": "timestamp-millis"}',
+    datetime.datetime(9999, 12, 31, 23, 59, 59, 999000, tzinfo=timezones.utc)
+  ),
+  (
+    '{"type": "long", "logicalType": "timestamp-millis"}',
+    datetime.datetime(2000, 1, 18, 2, 2, 1, 100000, tzinfo=timezones.tst)
+  ),
+  (
+    '{"type": "long", "logicalType": "timestamp-micros"}',
+    datetime.datetime(1000, 1, 1, 0, 0, 0, 000000, tzinfo=timezones.utc)
+  ),
+  (
+    '{"type": "long", "logicalType": "timestamp-micros"}',
+    datetime.datetime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=timezones.utc)
+  ),
+  (
+    '{"type": "long", "logicalType": "timestamp-micros"}',
+    datetime.datetime(2000, 1, 18, 2, 2, 1, 123499, tzinfo=timezones.tst)
+  ),
   ("""\
    {"type": "record",
     "name": "Test",
@@ -211,7 +242,10 @@ class TestIO(unittest.TestCase):
       if isinstance(round_trip_datum, Decimal):
         round_trip_datum = round_trip_datum.to_eng_string()
         datum = str(datum)
-      if datum == round_trip_datum: correct += 1
+      elif isinstance(round_trip_datum, datetime.datetime):
+        datum = datum.astimezone(tz=timezones.utc)
+      if datum == round_trip_datum:
+        correct += 1
     self.assertEquals(correct, len(SCHEMAS_TO_VALIDATE))
 
   #
diff --git a/lang/py/test/test_schema.py b/lang/py/test/test_schema.py
index e556ae2..a32c3f8 100644
--- a/lang/py/test/test_schema.py
+++ b/lang/py/test/test_schema.py
@@ -313,6 +313,67 @@ DECIMAL_LOGICAL_TYPE = [
   "scale": 2}""", True)
 ]
 
+DATE_LOGICAL_TYPE = [
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "date"} """, True),
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "date1"} """, False),
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "date"} """, False),
+]
+
+TIMEMILLIS_LOGICAL_TYPE = [
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "time-millis"} """, True),
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "time-milis"} """, False),
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "time-millis"} """, False),
+]
+
+TIMEMICROS_LOGICAL_TYPE = [
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "time-micros"} """, True),
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "time-micro"} """, False),
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "time-micros"} """, False),
+]
+
+TIMESTAMPMILLIS_LOGICAL_TYPE = [
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "timestamp-millis"} """, True),
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "timestamp-milis"} """, False),
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "timestamp-millis"} """, False),
+]
+
+TIMESTAMPMICROS_LOGICAL_TYPE = [
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "timestamp-micros"} """, True),
+  ExampleSchema("""{
+  "type": "long",
+  "logicalType": "timestamp-micro"} """, False),
+  ExampleSchema("""{
+  "type": "int",
+  "logicalType": "timestamp-micros"} """, False),
+]
+
+
 EXAMPLES = PRIMITIVE_EXAMPLES
 EXAMPLES += FIXED_EXAMPLES
 EXAMPLES += ENUM_EXAMPLES
@@ -322,6 +383,11 @@ EXAMPLES += UNION_EXAMPLES
 EXAMPLES += RECORD_EXAMPLES
 EXAMPLES += DOC_EXAMPLES
 EXAMPLES += DECIMAL_LOGICAL_TYPE
+EXAMPLES += DATE_LOGICAL_TYPE
+EXAMPLES += TIMEMILLIS_LOGICAL_TYPE
+EXAMPLES += TIMEMICROS_LOGICAL_TYPE
+EXAMPLES += TIMESTAMPMILLIS_LOGICAL_TYPE
+EXAMPLES += TIMESTAMPMICROS_LOGICAL_TYPE
 
 VALID_EXAMPLES = [e for e in EXAMPLES if e.valid]
 

Reply via email to