On 04/25/2014 11:08 AM, Jan Cholasta wrote:
> On 22.4.2014 13:32, Tomas Babej wrote:
>> Thank you for the suggestions. Updated, rebased patch is attached.
>>
>
> This API.txt change from the next patch belongs in this patch:
>
> +capability: datetime_values 2.84
>
>
> I think you should use the LDAP_GENERALIZED_TIME_FORMAT constant here:
>
> +    accepted_formats = ['%Y%m%d%H%M%SZ',       # generalized time
>
>
> This is not right:
>
> +        elif isinstance(val, datetime.datetime):
> +            return val
>
> To actually decode LDAP generalized time attributes to datetime, you
> need to do this:
>
>          '2.16.840.1.113719.1.301.4.41.1' : DN,  # krbSubTrees
>          '2.16.840.1.113719.1.301.4.52.1' : DN,  # krbObjectReferences
>          '2.16.840.1.113719.1.301.4.53.1' : DN,  # krbPrincContainerRef
> +
> +        '1.3.6.1.4.1.1466.115.121.1.24'  : datetime.datetime,
>      }
>
>      # In most cases we lookup the syntax from the schema returned by
>
> and this:
>
>                      return val
>                  elif target_type is unicode:
>                      return val.decode('utf-8')
> +                elif target_type is datetime.datetime:
> +                    return datetime.datetime.strptime(val,
> LDAP_GENERALIZED_TIME_FORMAT)
>                  else:
>                      return target_type(val)
>              except Exception, e:
>
> and add code for formatting datetime values to the textui backend.
>

Thanks for the review. I fixed all the issues, updated patch is attached.

I also added unit tests for the new DateTime parameter.

-- 
Tomas Babej
Associate Software Engineer | Red Hat | Identity Management
RHCE | Brno Site | IRC: tbabej | freeipa.org 

>From d5f36d18dbc29fc4e8f01b84a4e786a1e79dacc4 Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Thu, 9 Jan 2014 11:14:56 +0100
Subject: [PATCH] ipalib: Add DateTime parameter

Adds a parameter that represents a DateTime format using datetime.datetime
object from python's native datetime library.

In the CLI, accepts one of the following formats:
    Accepts LDAP Generalized time without in the following format:
       '%Y%m%d%H%M%SZ'

    Accepts subset of values defined by ISO 8601:
        '%Y-%m-%dT%H:%M:%SZ'
        '%Y-%m-%dT%H:%MZ'
        '%Y-%m-%dZ'

    Also accepts above formats using ' ' (space) as a separator instead of 'T'.

As a simplification, it does not deal with timezone info and ISO 8601
values with timezone info (+-hhmm) are rejected. Values are expected
to be in the UTC timezone.

Values are saved to LDAP as LDAP Generalized time values in the format
'%Y%m%d%H%SZ' (no time fractions and UTC timezone is assumed). To avoid
confusion, in addition to subset of ISO 8601 values, the LDAP generalized
time in the format '%Y%m%d%H%M%SZ' is also accepted as an input (as this is the
format user will see on the output).

Part of: https://fedorahosted.org/freeipa/ticket/3306
---
 API.txt                |  1 +
 VERSION                |  4 ++--
 ipalib/__init__.py     |  2 +-
 ipalib/capabilities.py |  3 +++
 ipalib/cli.py          |  6 +++++-
 ipalib/constants.py    |  2 ++
 ipalib/parameters.py   | 52 +++++++++++++++++++++++++++++++++++++++++++++++++-
 ipalib/rpc.py          | 27 +++++++++++++++++++++++---
 ipapython/ipaldap.py   |  7 +++++++
 9 files changed, 96 insertions(+), 8 deletions(-)

diff --git a/API.txt b/API.txt
index c2654b144fadbdd789308f9d01fa33ccdd9b7960..eb8a61f645235610eba8dc6d3452a835a7af8b1a 100644
--- a/API.txt
+++ b/API.txt
@@ -4007,3 +4007,4 @@ capability: messages 2.52
 capability: optional_uid_params 2.54
 capability: permissions2 2.69
 capability: primary_key_types 2.83
+capability: datetime_values 2.84
diff --git a/VERSION b/VERSION
index 3b48d640787964afdc0769b7a6f490aaf3410548..32bddcf9dd0640e8b2710831fced0d6c1c3f23d8 100644
--- a/VERSION
+++ b/VERSION
@@ -89,5 +89,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=83
-# Last change: jcholast - add 'primary_key_types' capability
+IPA_API_VERSION_MINOR=84
+# Last change: tbabej - added datetime value support
diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index 553c07197ddc95025da9df9e6c1fcddadadff4d8..2a87103b8dcb03a6d890029b830195cde52fc1e6 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -886,7 +886,7 @@ from frontend import Command, LocalOrRemote, Updater, Advice
 from frontend import Object, Method
 from crud import Create, Retrieve, Update, Delete, Search
 from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam, DeprecatedParam
-from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File
+from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File, DateTime
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py
index 3dd93294f430da88c8f4993a9cb96ce1202decbe..f2e45a0f66f596b23ed36c0e2c38ddc79e8625d7 100644
--- a/ipalib/capabilities.py
+++ b/ipalib/capabilities.py
@@ -48,6 +48,9 @@ capabilities = dict(
 
     # primary_key_types: Non-unicode primary keys in command output
     primary_key_types=u'2.83',
+
+    # support for datetime values on the client
+    datetime_values=u'2.84'
 )
 
 
diff --git a/ipalib/cli.py b/ipalib/cli.py
index 4250aaf54caf4f80a22a8dfe0500ce9b744c4869..f03db9c612b6e36e0ceda62a5e15c08261950dbe 100644
--- a/ipalib/cli.py
+++ b/ipalib/cli.py
@@ -46,11 +46,13 @@ import plugable
 from errors import (PublicError, CommandError, HelpError, InternalError,
                     NoSuchNamespaceError, ValidationError, NotFound,
                     NotConfiguredError, PromptFailed)
-from constants import CLI_TAB
+from constants import CLI_TAB, LDAP_GENERALIZED_TIME_FORMAT
 from parameters import File, Str, Enum, Any
 from text import _
 from ipapython.version import API_VERSION
 
+import datetime
+
 
 def to_cli(name):
     """
@@ -155,6 +157,8 @@ class textui(backend.Backend):
         """
         if type(value) is str:
             return base64.b64encode(value)
+        elif type(value) is datetime.datetime:
+            return value.strftime(LDAP_GENERALIZED_TIME_FORMAT)
         else:
             return value
 
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 6cc50eacf44678840ad0048a1ef60c05736879cb..e98eee6f8bcf2c227daa88327326b4dd850fbfe0 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -206,3 +206,5 @@ DEFAULT_CONFIG = (
     ('jsonrpc_uri', object),  # derived from xmlrpc_uri in Env._finalize_core()
 
 )
+
+LDAP_GENERALIZED_TIME_FORMAT = "%Y%m%d%H%M%SZ"
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index fc5e649815a71ea4a0f5f7a3f34ba9b7b846cfeb..33ae182b53446d0cbc6619f42cad09281494411c 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -102,6 +102,7 @@ a more detailed description for clarity.
 import re
 import decimal
 import base64
+import datetime
 from xmlrpclib import MAXINT, MININT
 
 from types import NoneType
@@ -109,7 +110,7 @@ from text import _ as ugettext
 from plugable import ReadOnly, lock, check_name
 from errors import ConversionError, RequirementError, ValidationError
 from errors import PasswordMismatch, Base64DecodeError
-from constants import TYPE_ERROR, CALLABLE_ERROR
+from constants import TYPE_ERROR, CALLABLE_ERROR, LDAP_GENERALIZED_TIME_FORMAT
 from text import Gettext, FixMe
 from util import json_serialize
 from ipapython.dn import DN
@@ -1609,6 +1610,55 @@ class File(Str):
         ('noextrawhitespace', bool, False),
     )
 
+class DateTime(Param):
+    """
+    DateTime parameter type.
+
+    Accepts LDAP Generalized time without in the following format:
+       '%Y%m%d%H%M%SZ'
+
+    Accepts subset of values defined by ISO 8601:
+        '%Y-%m-%dT%H:%M:%SZ'
+        '%Y-%m-%dT%H:%MZ'
+        '%Y-%m-%dZ'
+
+    Also accepts above formats using ' ' (space) as a separator instead of 'T'.
+
+    Refer to the `man strftime` for the explanations for the %Y,%m,%d,%H.%M,%S.
+    """
+
+    accepted_formats = [LDAP_GENERALIZED_TIME_FORMAT,  # generalized time
+                        '%Y-%m-%dT%H:%M:%SZ',  # ISO 8601, second precision
+                        '%Y-%m-%dT%H:%MZ',     # ISO 8601, minute precision
+                        '%Y-%m-%dZ',           # ISO 8601, date only
+                        '%Y-%m-%d %H:%M:%SZ',  # non-ISO 8601, second precision
+                        '%Y-%m-%d %H:%MZ']     # non-ISO 8601, minute precision
+
+
+    type = datetime.datetime
+    type_error = _('must be datetime value')
+
+    def _convert_scalar(self, value, index=None):
+        if isinstance(value, basestring):
+            for date_format in self.accepted_formats:
+                try:
+                    time = datetime.datetime.strptime(value, date_format)
+                    return time
+                except ValueError:
+                    pass
+
+            # If we get here, the strptime call did not succeed for any
+            # the accepted formats, therefore raise error
+
+            error = (_("does not match any of accepted formats: ") +
+                      (', '.join(self.accepted_formats)))
+
+            raise ConversionError(name=self.get_param_name(),
+                                  index=index,
+                                  error=error)
+
+        return super(DateTime, self)._convert_scalar(value, index)
+
 
 class AccessTime(Str):
     """
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index 73ae115b3e290f90803f11d8167d383f20844f5f..c44ffb6e19f452006d7dcf0a89f0644efb0aea01 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -33,6 +33,7 @@ Also see the `ipaserver.rpcserver` module.
 from types import NoneType
 from decimal import Decimal
 import sys
+import datetime
 import os
 import locale
 import base64
@@ -41,17 +42,18 @@ import json
 import socket
 from urllib2 import urlparse
 
-from xmlrpclib import (Binary, Fault, dumps, loads, ServerProxy, Transport,
-        ProtocolError, MININT, MAXINT)
+from xmlrpclib import (Binary, Fault, DateTime, dumps, loads, ServerProxy,
+        Transport, ProtocolError, MININT, MAXINT)
 import kerberos
 from dns import resolver, rdatatype
 from dns.exception import DNSException
 from nss.error import NSPRError
 
 from ipalib.backend import Connectible
+from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT
 from ipalib.errors import (public_errors, UnknownError, NetworkError,
     KerberosError, XMLRPCMarshallError, JSONError, ConversionError)
-from ipalib import errors
+from ipalib import errors, capabilities
 from ipalib.request import context, Connection
 from ipalib.util import get_current_principal
 from ipapython.ipa_log_manager import root_logger
@@ -163,6 +165,14 @@ def xml_wrap(value, version):
         return unicode(value)
     if isinstance(value, DN):
         return str(value)
+
+    # Encode datetime.datetime objects as xmlrpclib.DateTime objects
+    if isinstance(value, datetime.datetime):
+        if capabilities.client_has_capability(version, 'datetime_values'):
+            return DateTime(value)
+        else:
+            return value.strftime(LDAP_GENERALIZED_TIME_FORMAT)
+
     assert type(value) in (unicode, int, long, float, bool, NoneType)
     return value
 
@@ -196,6 +206,9 @@ def xml_unwrap(value, encoding='UTF-8'):
     if isinstance(value, Binary):
         assert type(value.data) is str
         return value.data
+    if isinstance(value, DateTime):
+        # xmlprc DateTime is converted to string of %Y%m%dT%H:%M:%S format
+        return datetime.datetime.strptime(str(value), "%Y%m%dT%H:%M:%S")
     assert type(value) in (unicode, int, float, bool, NoneType)
     return value
 
@@ -266,6 +279,11 @@ def json_encode_binary(val, version):
         return {'__base64__': base64.b64encode(str(val))}
     elif isinstance(val, DN):
         return str(val)
+    elif isinstance(val, datetime.datetime):
+        if capabilities.client_has_capability(version, 'datetime_values'):
+            return {'__datetime__': val.strftime(LDAP_GENERALIZED_TIME_FORMAT)}
+        else:
+            return val.strftime(LDAP_GENERALIZED_TIME_FORMAT)
     else:
         return val
 
@@ -293,6 +311,9 @@ def json_decode_binary(val):
     if isinstance(val, dict):
         if '__base64__' in val:
             return base64.b64decode(val['__base64__'])
+        elif '__datetime__' in val:
+            return datetime.datetime.strptime(val['__datetime__'],
+                                              LDAP_GENERALIZED_TIME_FORMAT)
         else:
             return dict((k, json_decode_binary(v)) for k, v in val.items())
     elif isinstance(val, list):
diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 56e7d13848b980329071d9f12309b5cbca5d5803..12450e8f292cd430099407661d0f5933319c7826 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -21,6 +21,7 @@
 
 import string
 import time
+import datetime
 import shutil
 from decimal import Decimal
 from copy import deepcopy
@@ -35,6 +36,7 @@ from ldap.controls import SimplePagedResultsControl
 import ldapurl
 
 from ipalib import errors, _
+from ipalib.constants import LDAP_GENERALIZED_TIME_FORMAT
 from ipapython import ipautil
 from ipapython.ipautil import (
     format_netloc, wait_for_open_socket, wait_for_open_ports, CIDict)
@@ -239,6 +241,7 @@ class IPASimpleLDAPObject(object):
         '2.16.840.1.113719.1.301.4.41.1' : DN,  # krbSubTrees
         '2.16.840.1.113719.1.301.4.52.1' : DN,  # krbObjectReferences
         '2.16.840.1.113719.1.301.4.53.1' : DN,  # krbPrincContainerRef
+        '1.3.6.1.4.1.1466.115.121.1.24'  : datetime.datetime,
     }
 
     # In most cases we lookup the syntax from the schema returned by
@@ -408,6 +411,8 @@ class IPASimpleLDAPObject(object):
         elif isinstance(val, dict):
             dct = dict((self.encode(k), self.encode(v)) for k, v in val.iteritems())
             return dct
+        elif isinstance(val, datetime.datetime):
+            return val.strftime(LDAP_GENERALIZED_TIME_FORMAT)
         elif val is None:
             return None
         else:
@@ -426,6 +431,8 @@ class IPASimpleLDAPObject(object):
                     return val
                 elif target_type is unicode:
                     return val.decode('utf-8')
+                elif target_type is datetime.datetime:
+                    return datetime.datetime.strptime(val, LDAP_GENERALIZED_TIME_FORMAT)
                 else:
                     return target_type(val)
             except Exception, e:
-- 
1.8.5.3


>From c250b59a0604e92cd62d8fd1ddf2b400cd6fa95c Mon Sep 17 00:00:00 2001
From: Tomas Babej <tba...@redhat.com>
Date: Tue, 29 Apr 2014 16:03:25 +0200
Subject: [PATCH] ipatests: Cover DateTime in test_parameters.py

Adds tests for newly added DateTime parameter, focusing on conversion
of accepted datetime formats.

Part of: https://fedorahosted.org/freeipa/ticket/3306
---
 ipatests/test_ipalib/test_parameters.py | 46 +++++++++++++++++++++++++++++++++
 ipatests/test_xmlrpc/xmlrpc_test.py     |  3 ++-
 2 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/ipatests/test_ipalib/test_parameters.py b/ipatests/test_ipalib/test_parameters.py
index 278e07165f194137d79246a7289813c0dcae0db7..ee0f49d060c514f87c688e37f1cb3ebc1fb8bf68 100644
--- a/ipatests/test_ipalib/test_parameters.py
+++ b/ipatests/test_ipalib/test_parameters.py
@@ -22,6 +22,7 @@
 Test the `ipalib.parameters` module.
 """
 
+import datetime
 import re
 import sys
 from types import NoneType
@@ -1580,3 +1581,48 @@ class test_IA5Str(ClassChecker):
             assert e.name == 'my_str'
             assert e.index is None
             assert_equal(e.error, "The character '\\xc3' is not allowed.")
+
+
+class test_DateTime(ClassChecker):
+    """
+    Test the `ipalib.parameters.DateTime` class.
+    """
+    _cls = parameters.DateTime
+
+    def test_init(self):
+        """
+        Test the `ipalib.parameters.DateTime.__init__` method.
+        """
+
+        # Test with no kwargs:
+        o = self.cls('my_datetime')
+        assert o.type is datetime.datetime
+        assert isinstance(o, parameters.DateTime)
+        assert o.multivalue is False
+
+        # Check full time formats
+        date = datetime.datetime(1991, 12, 7, 6, 30, 5)
+        assert date == o.convert(u'19911207063005Z')
+        assert date == o.convert(u'1991-12-07T06:30:05Z')
+        assert date == o.convert(u'1991-12-07 06:30:05Z')
+
+        # Check time formats without seconds
+        date = datetime.datetime(1991, 12, 7, 6, 30)
+        assert date == o.convert(u'1991-12-07T06:30Z')
+        assert date == o.convert(u'1991-12-07 06:30Z')
+
+        # Check date formats
+        date = datetime.datetime(1991, 12, 7)
+        assert date == o.convert(u'1991-12-07Z')
+
+        # Check some wrong formats
+        for value in (u'19911207063005',
+                      u'1991-12-07T06:30:05',
+                      u'1991-12-07 06:30:05',
+                      u'1991-12-07T06:30',
+                      u'1991-12-07 06:30',
+                      u'1991-12-07',
+                      u'1991-31-12Z',
+                      u'1991-12-07T25:30:05Z',
+            ):
+            raises(ConversionError, o.convert, value)
\ No newline at end of file
diff --git a/ipatests/test_xmlrpc/xmlrpc_test.py b/ipatests/test_xmlrpc/xmlrpc_test.py
index a596cd69c9a4ab4d9f35361d6db4ed5b3ffa5763..f60e30412d1e3cd9281ba7a5581064f7dfe4aae2 100644
--- a/ipatests/test_xmlrpc/xmlrpc_test.py
+++ b/ipatests/test_xmlrpc/xmlrpc_test.py
@@ -21,6 +21,7 @@
 Base class for all XML-RPC tests
 """
 
+import datetime
 import nose
 from ipatests.util import assert_deepequal, Fuzzy
 from ipalib import api, request, errors
@@ -86,7 +87,7 @@ fuzzy_hex = Fuzzy('^0x[0-9a-fA-F]+$', type=basestring)
 fuzzy_password = Fuzzy('^\S([\S ]*\S)*$')
 
 # Matches generalized time value. Time format is: %Y%m%d%H%M%SZ
-fuzzy_dergeneralizedtime = Fuzzy('^[0-9]{14}Z$')
+fuzzy_dergeneralizedtime = Fuzzy(type=datetime.datetime)
 
 # match any string
 fuzzy_string = Fuzzy(type=basestring)
-- 
1.8.5.3


_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to