1
2#
3# spyne - Copyright (C) Spyne contributors.
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
18#
19
20from __future__ import print_function, unicode_literals
21
22import logging
23logger = logging.getLogger(__name__)
24
25import re
26import uuid
27import errno
28
29from os.path import isabs, join, abspath
30from collections import deque
31from datetime import datetime
32from decimal import Decimal as D
33from mmap import mmap, ACCESS_READ
34from time import mktime, strftime
35
36try:
37    from lxml import etree
38    from lxml import html
39except ImportError:
40    etree = None
41    html = None
42
43from spyne.protocol._base import ProtocolMixin
44from spyne.model import ModelBase, XmlAttribute, SimpleModel, Null, \
45    ByteArray, File, ComplexModelBase, AnyXml, AnyHtml, Unicode, Decimal, \
46    Double, Integer, Time, DateTime, Uuid, Duration, Boolean, AnyDict, \
47    AnyUri, PushBase, Date
48from spyne.model.relational import FileData
49
50from spyne.const.http import HTTP_400, HTTP_401, HTTP_404, HTTP_405, HTTP_413, \
51    HTTP_500
52from spyne.error import Fault, InternalError, ResourceNotFoundError, \
53    RequestTooLongError, RequestNotAllowed, InvalidCredentialsError
54from spyne.model.binary import binary_encoding_handlers, \
55    BINARY_ENCODING_USE_DEFAULT
56
57from spyne.util import six
58from spyne.util.cdict import cdict
59
60
61class OutProtocolBase(ProtocolMixin):
62    """This is the abstract base class for all out protocol implementations.
63    Child classes can implement only the required subset of the public methods.
64
65    An output protocol must implement :func:`serialize` and
66    :func:`create_out_string`.
67
68    The OutProtocolBase class supports the following events:
69
70    * ``before_serialize``:
71      Called before after the serialization operation is attempted.
72
73    * ``after_serialize``:
74      Called after the serialization operation is finished.
75
76    The arguments the constructor takes are as follows:
77
78    :param app: The application this protocol belongs to.
79    :param mime_type: The mime_type this protocol should set for transports
80        that support this. This is a quick way to override the mime_type by
81        default instead of subclassing the releavant protocol implementation.
82    :param ignore_uncap: Silently ignore cases when the protocol is not capable
83        of serializing return values instead of raising a TypeError.
84    """
85
86    def __init__(self, app=None, mime_type=None, ignore_uncap=False,
87             ignore_wrappers=False, binary_encoding=None, string_encoding=None):
88
89        super(OutProtocolBase, self).__init__(app=app, mime_type=mime_type,
90            ignore_wrappers=ignore_wrappers,
91               binary_encoding=binary_encoding, string_encoding=string_encoding)
92
93        self.ignore_uncap = ignore_uncap
94        self.message = None
95
96        if mime_type is not None:
97            self.mime_type = mime_type
98
99        self._to_bytes_handlers = cdict({
100            ModelBase: self.model_base_to_bytes,
101            File: self.file_to_bytes,
102            Time: self.time_to_bytes,
103            Uuid: self.uuid_to_bytes,
104            Null: self.null_to_bytes,
105            Date: self.date_to_bytes,
106            Double: self.double_to_bytes,
107            AnyXml: self.any_xml_to_bytes,
108            Unicode: self.unicode_to_bytes,
109            Boolean: self.boolean_to_bytes,
110            Decimal: self.decimal_to_bytes,
111            Integer: self.integer_to_bytes,
112            AnyHtml: self.any_html_to_bytes,
113            DateTime: self.datetime_to_bytes,
114            Duration: self.duration_to_bytes,
115            ByteArray: self.byte_array_to_bytes,
116            XmlAttribute: self.xmlattribute_to_bytes,
117            ComplexModelBase: self.complex_model_base_to_bytes,
118        })
119
120        self._to_unicode_handlers = cdict({
121            ModelBase: self.model_base_to_unicode,
122            File: self.file_to_unicode,
123            Time: self.time_to_unicode,
124            Date: self.date_to_unicode,
125            Uuid: self.uuid_to_unicode,
126            Null: self.null_to_unicode,
127            Double: self.double_to_unicode,
128            AnyXml: self.any_xml_to_unicode,
129            AnyUri: self.any_uri_to_unicode,
130            AnyDict: self.any_dict_to_unicode,
131            AnyHtml: self.any_html_to_unicode,
132            Unicode: self.unicode_to_unicode,
133            Boolean: self.boolean_to_unicode,
134            Decimal: self.decimal_to_unicode,
135            Integer: self.integer_to_unicode,
136            # FIXME: Would we need a to_unicode for localized dates?
137            DateTime: self.datetime_to_unicode,
138            Duration: self.duration_to_unicode,
139            ByteArray: self.byte_array_to_unicode,
140            XmlAttribute: self.xmlattribute_to_unicode,
141            ComplexModelBase: self.complex_model_base_to_unicode,
142        })
143
144        self._to_bytes_iterable_handlers = cdict({
145            File: self.file_to_bytes_iterable,
146            ByteArray: self.byte_array_to_bytes_iterable,
147            ModelBase: self.model_base_to_bytes_iterable,
148            SimpleModel: self.simple_model_to_bytes_iterable,
149            ComplexModelBase: self.complex_model_to_bytes_iterable,
150        })
151
152
153    def serialize(self, ctx, message):
154        """Serializes ``ctx.out_object``.
155
156        If ctx.out_stream is not None,  ``ctx.out_document`` and
157        ``ctx.out_string`` are skipped and the response is written directly to
158        ``ctx.out_stream``.
159
160        :param ctx: :class:`MethodContext` instance.
161        :param message: One of ``(ProtocolBase.REQUEST, ProtocolBase.RESPONSE)``.
162        """
163
164    def create_out_string(self, ctx, out_string_encoding=None):
165        """Uses ctx.out_document to set ctx.out_string"""
166
167    def fault_to_http_response_code(self, fault):
168        """Special function to convert native Python exceptions to Http response
169        codes.
170        """
171
172        if isinstance(fault, RequestTooLongError):
173            return HTTP_413
174
175        if isinstance(fault, ResourceNotFoundError):
176            return HTTP_404
177
178        if isinstance(fault, RequestNotAllowed):
179            return HTTP_405
180
181        if isinstance(fault, InvalidCredentialsError):
182            return HTTP_401
183
184        if isinstance(fault, Fault) and (fault.faultcode.startswith('Client.')
185                                                or fault.faultcode == 'Client'):
186            return HTTP_400
187
188        return HTTP_500
189
190    def set_validator(self, validator):
191        """You must override this function if you want your protocol to support
192        validation."""
193
194        assert validator is None
195
196        self.validator = None
197
198    def to_bytes(self, cls, value, *args, **kwargs):
199        if value is None:
200            return None
201
202        handler = self._to_bytes_handlers[cls]
203        retval = handler(cls, value, *args, **kwargs)
204
205        # enable this only for testing. we're not as strict for performance
206        # reasons
207        # assert isinstance(retval, six.binary_type), \
208        #                 "AssertionError: %r %r %r handler: %r" % \
209        #                       (type(retval), six.binary_type, retval, handler)
210        return retval
211
212    def to_unicode(self, cls, value, *args, **kwargs):
213        if value is None:
214            return None
215
216        handler = self._to_unicode_handlers[cls]
217        retval = handler(cls, value, *args, **kwargs)
218
219        # enable this only for testing. we're not as strict for performance
220        # reasons as well as not to take the joy of dealing with duck typing
221        # from the user
222        # assert isinstance(retval, six.text_type), \
223        #                  "AssertionError: %r %r handler: %r" % \
224        #                                        (type(retval), retval, handler)
225
226        return retval
227
228    def to_bytes_iterable(self, cls, value):
229        if value is None:
230            return []
231
232        if isinstance(value, PushBase):
233            return value
234
235        handler = self._to_bytes_iterable_handlers[cls]
236        return handler(cls, value)
237
238    def null_to_bytes(self, cls, value, **_):
239        return b""
240
241    def null_to_unicode(self, cls, value, **_):
242        return u""
243
244    def any_xml_to_bytes(self, cls, value, **_):
245        return etree.tostring(value)
246
247    def any_xml_to_unicode(self, cls, value, **_):
248        return etree.tostring(value, encoding='unicode')
249
250    def any_dict_to_unicode(self, cls, value, **_):
251        return repr(value)
252
253    def any_html_to_bytes(self, cls, value, **_):
254        return html.tostring(value)
255
256    def any_html_to_unicode(self, cls, value, **_):
257        return html.tostring(value, encoding='unicode')
258
259    def uuid_to_bytes(self, cls, value, suggested_encoding=None, **_):
260        ser_as = self.get_cls_attrs(cls).serialize_as
261        retval = self.uuid_to_unicode(cls, value,
262                                     suggested_encoding=suggested_encoding, **_)
263
264        if ser_as in ('bytes', 'bytes_le', 'fields', 'int', six.binary_type):
265            return retval
266
267        return retval.encode('ascii')
268
269    def uuid_to_unicode(self, cls, value, suggested_encoding=None, **_):
270        attr = self.get_cls_attrs(cls)
271        ser_as = attr.serialize_as
272        encoding = attr.encoding
273
274        if encoding is None:
275            encoding = suggested_encoding
276
277        retval = _uuid_serialize[ser_as](value)
278        if ser_as in ('bytes', 'bytes_le'):
279            retval = binary_encoding_handlers[encoding]((retval,))
280        return retval
281
282    def unicode_to_bytes(self, cls, value, **_):
283        retval = value
284
285        cls_attrs = self.get_cls_attrs(cls)
286
287        if isinstance(value, six.text_type):
288            if cls_attrs.encoding is not None:
289                retval = value.encode(cls_attrs.encoding)
290            elif self.default_string_encoding is not None:
291                retval = value.encode(self.default_string_encoding)
292            elif not six.PY2:
293                logger.warning("You need to set either an encoding for %r "
294                               "or a default_string_encoding for %r", cls, self)
295
296        if cls_attrs.str_format is not None:
297            return cls_attrs.str_format.format(value)
298        elif cls_attrs.format is not None:
299            return cls_attrs.format % retval
300
301        return retval
302
303    def any_uri_to_unicode(self, cls, value, **_):
304        return self.unicode_to_unicode(cls, value, **_)
305
306    def unicode_to_unicode(self, cls, value, **_):  # :)))
307        cls_attrs = self.get_cls_attrs(cls)
308
309        retval = value
310
311        if isinstance(value, six.binary_type):
312            if cls_attrs.encoding is not None:
313                retval = value.decode(cls_attrs.encoding)
314
315            if self.default_string_encoding is not None:
316                retval = value.decode(self.default_string_encoding)
317
318            elif not six.PY2:
319                logger.warning("You need to set either an encoding for %r "
320                               "or a default_string_encoding for %r", cls, self)
321
322        if cls_attrs.str_format is not None:
323            return cls_attrs.str_format.format(value)
324        elif cls_attrs.format is not None:
325            return cls_attrs.format % retval
326
327        return retval
328
329    def decimal_to_bytes(self, cls, value, **_):
330        return self.decimal_to_unicode(cls, value, **_).encode('utf8')
331
332    def decimal_to_unicode(self, cls, value, **_):
333        D(value)  # sanity check
334        cls_attrs = self.get_cls_attrs(cls)
335
336        if cls_attrs.str_format is not None:
337            return cls_attrs.str_format.format(value)
338        elif cls_attrs.format is not None:
339            return cls_attrs.format % value
340
341        return str(value)
342
343    def double_to_bytes(self, cls, value, **_):
344        return self.double_to_unicode(cls, value, **_).encode('utf8')
345
346    def double_to_unicode(self, cls, value, **_):
347        float(value) # sanity check
348        cls_attrs = self.get_cls_attrs(cls)
349
350        if cls_attrs.str_format is not None:
351            return cls_attrs.str_format.format(value)
352        elif cls_attrs.format is not None:
353            return cls_attrs.format % value
354
355        return repr(value)
356
357    def integer_to_bytes(self, cls, value, **_):
358        return self.integer_to_unicode(cls, value, **_).encode('utf8')
359
360    def integer_to_unicode(self, cls, value, **_):
361        int(value)  # sanity check
362        cls_attrs = self.get_cls_attrs(cls)
363
364        if cls_attrs.str_format is not None:
365            return cls_attrs.str_format.format(value)
366        elif cls_attrs.format is not None:
367            return cls_attrs.format % value
368
369        return str(value)
370
371    def time_to_bytes(self, cls, value, **kwargs):
372        return self.time_to_unicode(cls, value, **kwargs)
373
374    def time_to_unicode(self, cls, value, **_):
375        """Returns ISO formatted times."""
376        if isinstance(value, datetime):
377            value = value.time()
378        return value.isoformat()
379
380    def date_to_bytes(self, cls, val, **_):
381        return self.date_to_unicode(cls, val, **_).encode("utf8")
382
383    def date_to_unicode(self, cls, val, **_):
384        if isinstance(val, datetime):
385            val = val.date()
386
387        sa = self.get_cls_attrs(cls).serialize_as
388
389        if sa is None or sa in (str, 'str'):
390            return self._date_to_bytes(cls, val)
391
392        return _datetime_smap[sa](cls, val)
393
394    def datetime_to_bytes(self, cls, val, **_):
395        retval = self.datetime_to_unicode(cls, val, **_)
396        sa = self.get_cls_attrs(cls).serialize_as
397        if sa is None or sa in (six.text_type, str, 'str'):
398            return retval.encode('ascii')
399        return retval
400
401    def datetime_to_unicode(self, cls, val, **_):
402        sa = self.get_cls_attrs(cls).serialize_as
403
404        if sa is None or sa in (six.text_type, str, 'str'):
405            return self._datetime_to_unicode(cls, val)
406
407        return _datetime_smap[sa](cls, val)
408
409    def duration_to_bytes(self, cls, value, **_):
410        return self.duration_to_unicode(cls, value, **_).encode("utf8")
411
412    def duration_to_unicode(self, cls, value, **_):
413        if value.days < 0:
414            value = -value
415            negative = True
416        else:
417            negative = False
418
419        tot_sec = int(value.total_seconds())
420        seconds = value.seconds % 60
421        minutes = value.seconds // 60
422        hours = minutes // 60
423        minutes %= 60
424        seconds = float(seconds)
425        useconds = value.microseconds
426
427        retval = deque()
428        if negative:
429            retval.append("-P")
430        else:
431            retval.append("P")
432        if value.days != 0:
433            retval.append("%iD" % value.days)
434
435        if tot_sec != 0 and tot_sec % 86400 == 0 and useconds == 0:
436            return ''.join(retval)
437
438        retval.append('T')
439
440        if hours > 0:
441            retval.append("%iH" % hours)
442
443        if minutes > 0:
444            retval.append("%iM" % minutes)
445
446        if seconds > 0 or useconds > 0:
447            retval.append("%i" % seconds)
448            if useconds > 0:
449                retval.append(".%i" % useconds)
450            retval.append("S")
451
452        if len(retval) == 2:
453            retval.append('0S')
454
455        return ''.join(retval)
456
457    def boolean_to_bytes(self, cls, value, **_):
458        return str(bool(value)).lower().encode('ascii')
459
460    def boolean_to_unicode(self, cls, value, **_):
461        return str(bool(value)).lower()
462
463    def byte_array_to_bytes(self, cls, value, suggested_encoding=None, **_):
464        cls_attrs = self.get_cls_attrs(cls)
465
466        encoding = cls_attrs.encoding
467        if encoding is BINARY_ENCODING_USE_DEFAULT:
468            if suggested_encoding is None:
469                encoding = self.binary_encoding
470            else:
471                encoding = suggested_encoding
472
473        if encoding is None and isinstance(value, (list, tuple)) \
474                             and len(value) == 1 and isinstance(value[0], mmap):
475            return value[0]
476
477        encoder = binary_encoding_handlers[encoding]
478        logger.debug("Using binary encoder %r for encoding %r",
479                                                              encoder, encoding)
480        retval = encoder(value)
481        if encoding is not None and isinstance(retval, six.text_type):
482            retval = retval.encode('ascii')
483
484        return retval
485
486    def byte_array_to_unicode(self, cls, value, suggested_encoding=None, **_):
487        encoding = self.get_cls_attrs(cls).encoding
488        if encoding is BINARY_ENCODING_USE_DEFAULT:
489            if suggested_encoding is None:
490                encoding = self.binary_encoding
491            else:
492                encoding = suggested_encoding
493
494        if encoding is None:
495            raise ValueError("Arbitrary binary data can't be serialized to "
496                                                                      "unicode")
497
498        retval = binary_encoding_handlers[encoding](value)
499        if not isinstance(retval, six.text_type):
500            retval = retval.decode('ascii')
501
502        return retval
503
504    def byte_array_to_bytes_iterable(self, cls, value, **_):
505        return value
506
507    def file_to_bytes(self, cls, value, suggested_encoding=None):
508        """
509        :param cls: A :class:`spyne.model.File` subclass
510        :param value: Either a sequence of byte chunks or a
511            :class:`spyne.model.File.Value` instance.
512        """
513
514        encoding = self.get_cls_attrs(cls).encoding
515        if encoding is BINARY_ENCODING_USE_DEFAULT:
516            if suggested_encoding is None:
517                encoding = self.binary_encoding
518            else:
519                encoding = suggested_encoding
520
521        if isinstance(value, File.Value):
522            if value.data is not None:
523                return binary_encoding_handlers[encoding](value.data)
524
525            if value.handle is not None:
526                # maybe we should have used the sweeping except: here.
527                if hasattr(value.handle, 'fileno'):
528                    if six.PY2:
529                        fileno = value.handle.fileno()
530                        data = (mmap(fileno, 0, access=ACCESS_READ),)
531                    else:
532                        import io
533                        try:
534                            fileno = value.handle.fileno()
535                            data = mmap(fileno, 0, access=ACCESS_READ)
536                        except io.UnsupportedOperation:
537                            data = (value.handle.read(),)
538                else:
539                    data = (value.handle.read(),)
540
541                return binary_encoding_handlers[encoding](data)
542
543            if value.path is not None:
544                handle = open(value.path, 'rb')
545                fileno = handle.fileno()
546                data = mmap(fileno, 0, access=ACCESS_READ)
547
548                return binary_encoding_handlers[encoding](data)
549
550            assert False, "Unhandled file type"
551
552        if isinstance(value, FileData):
553            try:
554                return binary_encoding_handlers[encoding](value.data)
555            except Exception as e:
556                logger.error("Error encoding value to binary. Error: %r, Value: %r",
557                                                                           e, value)
558                raise
559
560        try:
561            return binary_encoding_handlers[encoding](value)
562        except Exception as e:
563            logger.error("Error encoding value to binary. Error: %r, Value: %r",
564                                                                       e, value)
565            raise
566
567    def file_to_unicode(self, cls, value, suggested_encoding=None):
568        """
569        :param cls: A :class:`spyne.model.File` subclass
570        :param value: Either a sequence of byte chunks or a
571            :class:`spyne.model.File.Value` instance.
572        """
573
574        cls_attrs = self.get_cls_attrs(cls)
575        encoding = cls_attrs.encoding
576        if encoding is BINARY_ENCODING_USE_DEFAULT:
577            encoding = suggested_encoding
578
579        if encoding is None and cls_attrs.mode is File.TEXT:
580            raise ValueError("Arbitrary binary data can't be serialized to "
581                             "unicode.")
582
583        retval = self.file_to_bytes(cls, value, suggested_encoding)
584        if not isinstance(retval, six.text_type):
585            retval = retval.decode('ascii')
586        return retval
587
588    def file_to_bytes_iterable(self, cls, value, **_):
589        if value.data is not None:
590            if isinstance(value.data, (list, tuple)) and \
591                                                isinstance(value.data[0], mmap):
592                return _file_to_iter(value.data[0])
593            return iter(value.data)
594
595        if value.handle is not None:
596            f = value.handle
597            f.seek(0)
598            return _file_to_iter(f)
599
600        assert value.path is not None, "You need to write data to " \
601                 "persistent storage first if you want to read it back."
602
603        try:
604            path = value.path
605            if not isabs(value.path):
606                path = join(value.store, value.path)
607                assert abspath(path).startswith(value.store), \
608                                                 "No relative paths are allowed"
609            return _file_to_iter(open(path, 'rb'))
610
611        except IOError as e:
612            if e.errno == errno.ENOENT:
613                raise ResourceNotFoundError(value.path)
614            else:
615                raise InternalError("Error accessing requested file")
616
617    def simple_model_to_bytes_iterable(self, cls, value, **kwargs):
618        retval = self.to_bytes(cls, value, **kwargs)
619        if retval is None:
620            return (b'',)
621        return (retval,)
622
623    def complex_model_to_bytes_iterable(self, cls, value, **_):
624        if self.ignore_uncap:
625            return tuple()
626        raise TypeError("This protocol can only serialize primitives.")
627
628    def complex_model_base_to_bytes(self, cls, value, **_):
629        raise TypeError("Only primitives can be serialized to string.")
630
631    def complex_model_base_to_unicode(self, cls, value, **_):
632        raise TypeError("Only primitives can be serialized to string.")
633
634    def xmlattribute_to_bytes(self, cls, string, **kwargs):
635        return self.to_bytes(cls.type, string, **kwargs)
636
637    def xmlattribute_to_unicode(self, cls, string, **kwargs):
638        return self.to_unicode(cls.type, string, **kwargs)
639
640    def model_base_to_bytes_iterable(self, cls, value, **kwargs):
641        return cls.to_bytes_iterable(value, **kwargs)
642
643    def model_base_to_bytes(self, cls, value, **kwargs):
644        return cls.to_bytes(value, **kwargs)
645
646    def model_base_to_unicode(self, cls, value, **kwargs):
647        return cls.to_unicode(value, **kwargs)
648
649    def _datetime_to_unicode(self, cls, value, **_):
650        """Returns ISO formatted datetimes."""
651
652        cls_attrs = self.get_cls_attrs(cls)
653
654        if cls_attrs.as_timezone is not None and value.tzinfo is not None:
655            value = value.astimezone(cls_attrs.as_timezone)
656
657        if not cls_attrs.timezone:
658            value = value.replace(tzinfo=None)
659
660        dt_format = self._get_datetime_format(cls_attrs)
661
662        if dt_format is None:
663            retval = value.isoformat()
664
665        elif six.PY2 and isinstance(dt_format, unicode):
666            retval = self.strftime(value, dt_format.encode('utf8')).decode('utf8')
667
668        else:
669            retval = self.strftime(value, dt_format)
670
671        # FIXME: must deprecate string_format, this should have been str_format
672        str_format = cls_attrs.string_format
673        if str_format is None:
674            str_format = cls_attrs.str_format
675        if str_format is not None:
676            return str_format.format(value)
677
678        # FIXME: must deprecate interp_format, this should have been just format
679        interp_format = cls_attrs.interp_format
680        if interp_format is not None:
681            return interp_format.format(value)
682
683        return retval
684
685    def _date_to_bytes(self, cls, value, **_):
686        cls_attrs = self.get_cls_attrs(cls)
687
688        date_format = cls_attrs.date_format
689        if date_format is None:
690            retval = value.isoformat()
691
692        elif six.PY2 and isinstance(date_format, unicode):
693            date_format = date_format.encode('utf8')
694            retval = self.strftime(value, date_format).decode('utf8')
695
696        else:
697            retval = self.strftime(value, date_format)
698
699        str_format = cls_attrs.str_format
700        if str_format is not None:
701            return str_format.format(value)
702
703        format = cls_attrs.format
704        if format is not None:
705            return format.format(value)
706
707        return retval
708
709    # Format a datetime through its full proleptic Gregorian date range.
710    # http://code.activestate.com/recipes/
711    #                306860-proleptic-gregorian-dates-and-strftime-before-1900/
712    # http://stackoverflow.com/a/32206673
713    #
714    # >>> strftime(datetime.date(1850, 8, 2), "%Y/%M/%d was a %A")
715    # '1850/00/02 was a Friday'
716    # >>>
717
718
719    # remove the unsupposed "%s" command.  But don't
720    # do it if there's an even number of %s before the s
721    # because those are all escaped.  Can't simply
722    # remove the s because the result of
723    #  %sY
724    # should be %Y if %s isn't supported, not the
725    # 4 digit year.
726    _illegal_s = re.compile(r"((^|[^%])(%%)*%s)")
727
728    @staticmethod
729    def _findall_datetime(text, substr):
730         # Also finds overlaps
731         sites = []
732         i = 0
733         while 1:
734             j = text.find(substr, i)
735             if j == -1:
736                 break
737             sites.append(j)
738             i=j+1
739         return sites
740
741    # Every 28 years the calendar repeats, except through century leap
742    # years where it's 6 years.  But only if you're using the Gregorian
743    # calendar.  ;)
744
745    @classmethod
746    def strftime(cls, dt, fmt):
747        if cls._illegal_s.search(fmt):
748            raise TypeError("This strftime implementation does not handle %s")
749        if dt.year > 1900:
750            return dt.strftime(fmt)
751
752        year = dt.year
753        # For every non-leap year century, advance by
754        # 6 years to get into the 28-year repeat cycle
755        delta = 2000 - year
756        off = 6*(delta // 100 + delta // 400)
757        year += off
758
759        # Move to around the year 2000
760        year += ((2000 - year) // 28) * 28
761        timetuple = dt.timetuple()
762        s1 = strftime(fmt, (year,) + timetuple[1:])
763        sites1 = cls._findall_datetime(s1, str(year))
764
765        s2 = strftime(fmt, (year+28,) + timetuple[1:])
766        sites2 = cls._findall_datetime(s2, str(year+28))
767
768        sites = []
769        for site in sites1:
770            if site in sites2:
771                sites.append(site)
772
773        s = s1
774        syear = "%4d" % (dt.year,)
775        for site in sites:
776            s = s[:site] + syear + s[site+4:]
777        return s
778
779
780_uuid_serialize = {
781    None: str,
782    str: str,
783    'str': str,
784
785    'hex': lambda u: u.hex,
786    'urn': lambda u: u.urn,
787    'bytes': lambda u: u.bytes,
788    'bytes_le': lambda u: u.bytes_le,
789    'fields': lambda u: u.fields,
790
791    int: lambda u: u.int,
792    'int': lambda u: u.int,
793}
794
795_uuid_deserialize = {
796    None: uuid.UUID,
797    str: uuid.UUID,
798    'str': uuid.UUID,
799
800    'hex': lambda s: uuid.UUID(hex=s),
801    'urn': lambda s: uuid.UUID(hex=s),
802    'bytes': lambda s: uuid.UUID(bytes=s),
803    'bytes_le': lambda s: uuid.UUID(bytes_le=s),
804    'fields': lambda s: uuid.UUID(fields=s),
805
806    int: lambda s: uuid.UUID(int=s),
807    'int': lambda s: uuid.UUID(int=s),
808
809    (int, int): lambda s: uuid.UUID(int=s),
810    ('int', int): lambda s: uuid.UUID(int=s),
811
812    (int, str): lambda s: uuid.UUID(int=int(s)),
813    ('int', str): lambda s: uuid.UUID(int=int(s)),
814}
815
816if six.PY2:
817    _uuid_deserialize[('int', long)] = _uuid_deserialize[('int', int)]
818    _uuid_deserialize[(int, long)] = _uuid_deserialize[('int', int)]
819
820
821def _parse_datetime_iso_match(date_match, tz=None):
822    fields = date_match.groupdict()
823
824    year = int(fields.get('year'))
825    month = int(fields.get('month'))
826    day = int(fields.get('day'))
827    hour = int(fields.get('hr'))
828    minute = int(fields.get('min'))
829    second = int(fields.get('sec'))
830    usecond = fields.get("sec_frac")
831    if usecond is None:
832        usecond = 0
833    else:
834        # we only get the most significant 6 digits because that's what
835        # datetime can handle.
836        usecond = int(round(float(usecond) * 1e6))
837
838    return datetime(year, month, day, hour, minute, second, usecond, tz)
839
840
841_dt_sec = lambda cls, val: \
842        int(mktime(val.timetuple()))
843_dt_sec_float = lambda cls, val: \
844        mktime(val.timetuple()) + (val.microsecond / 1e6)
845
846_dt_msec = lambda cls, val: \
847        int(mktime(val.timetuple())) * 1000 + (val.microsecond // 1000)
848_dt_msec_float = lambda cls, val: \
849        mktime(val.timetuple()) * 1000 + (val.microsecond / 1000.0)
850
851_dt_usec = lambda cls, val: \
852        int(mktime(val.timetuple())) * 1000000 + val.microsecond
853
854_datetime_smap = {
855    'sec': _dt_sec,
856    'secs': _dt_sec,
857    'second': _dt_sec,
858    'seconds': _dt_sec,
859
860    'sec_float': _dt_sec_float,
861    'secs_float': _dt_sec_float,
862    'second_float': _dt_sec_float,
863    'seconds_float': _dt_sec_float,
864
865    'msec': _dt_msec,
866    'msecs': _dt_msec,
867    'msecond': _dt_msec,
868    'mseconds': _dt_msec,
869    'millisecond': _dt_msec,
870    'milliseconds': _dt_msec,
871
872    'msec_float': _dt_msec_float,
873    'msecs_float': _dt_msec_float,
874    'msecond_float': _dt_msec_float,
875    'mseconds_float': _dt_msec_float,
876    'millisecond_float': _dt_msec_float,
877    'milliseconds_float': _dt_msec_float,
878
879    'usec': _dt_usec,
880    'usecs': _dt_usec,
881    'usecond': _dt_usec,
882    'useconds': _dt_usec,
883    'microsecond': _dt_usec,
884    'microseconds': _dt_usec,
885}
886
887
888def _file_to_iter(f):
889    try:
890        data = f.read(8192)
891        while len(data) > 0:
892            yield data
893            data = f.read(8192)
894
895    finally:
896        f.close()
897
898
899META_ATTR = ['nullable', 'default_factory']
900