../README.rst
+ + +diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..d4bb2cbb9e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo new file mode 100644 index 0000000000..fa6917ebb3 --- /dev/null +++ b/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 12cdc5d0e42bbebda02cdf5255ea4e8a +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/README.html b/docs/_build/html/README.html new file mode 100644 index 0000000000..710bcee006 --- /dev/null +++ b/docs/_build/html/README.html @@ -0,0 +1,123 @@ + + + + + +
+ + +../README.rst
+ + +
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/base64.html b/docs/_build/html/_modules/base64.html
new file mode 100644
index 0000000000..df55b2571c
--- /dev/null
+++ b/docs/_build/html/_modules/base64.html
@@ -0,0 +1,717 @@
+
+
+
+
+
+
+
+
+
+#! /usr/bin/env python3
+
+"""Base16, Base32, Base64 (RFC 3548), Base85 and Ascii85 data encodings"""
+
+# Modified 04-Oct-1995 by Jack Jansen to use binascii module
+# Modified 30-Dec-2003 by Barry Warsaw to add full RFC 3548 support
+# Modified 22-May-2007 by Guido van Rossum to use bytes everywhere
+
+import re
+import struct
+import binascii
+
+
+__all__ = [
+ # Legacy interface exports traditional RFC 2045 Base64 encodings
+ 'encode', 'decode', 'encodebytes', 'decodebytes',
+ # Generalized interface for other encodings
+ 'b64encode', 'b64decode', 'b32encode', 'b32decode',
+ 'b16encode', 'b16decode',
+ # Base85 and Ascii85 encodings
+ 'b85encode', 'b85decode', 'a85encode', 'a85decode',
+ # Standard Base64 encoding
+ 'standard_b64encode', 'standard_b64decode',
+ # Some common Base64 alternatives. As referenced by RFC 3458, see thread
+ # starting at:
+ #
+ # http://zgp.org/pipermail/p2p-hackers/2001-September/000316.html
+ 'urlsafe_b64encode', 'urlsafe_b64decode',
+ ]
+
+
+bytes_types = (bytes, bytearray) # Types acceptable as binary data
+
+def _bytes_from_decode_data(s):
+ if isinstance(s, str):
+ try:
+ return s.encode('ascii')
+ except UnicodeEncodeError:
+ raise ValueError('string argument should contain only ASCII characters')
+ if isinstance(s, bytes_types):
+ return s
+ try:
+ return memoryview(s).tobytes()
+ except TypeError:
+ raise TypeError("argument should be a bytes-like object or ASCII "
+ "string, not %r" % s.__class__.__name__) from None
+
+
+# Base64 encoding/decoding uses binascii
+
+def b64encode(s, altchars=None):
+ """Encode the bytes-like object s using Base64 and return a bytes object.
+
+ Optional altchars should be a byte string of length 2 which specifies an
+ alternative alphabet for the '+' and '/' characters. This allows an
+ application to e.g. generate url or filesystem safe Base64 strings.
+ """
+ encoded = binascii.b2a_base64(s, newline=False)
+ if altchars is not None:
+ assert len(altchars) == 2, repr(altchars)
+ return encoded.translate(bytes.maketrans(b'+/', altchars))
+ return encoded
+
+
+def b64decode(s, altchars=None, validate=False):
+ """Decode the Base64 encoded bytes-like object or ASCII string s.
+
+ Optional altchars must be a bytes-like object or ASCII string of length 2
+ which specifies the alternative alphabet used instead of the '+' and '/'
+ characters.
+
+ The result is returned as a bytes object. A binascii.Error is raised if
+ s is incorrectly padded.
+
+ If validate is False (the default), characters that are neither in the
+ normal base-64 alphabet nor the alternative alphabet are discarded prior
+ to the padding check. If validate is True, these non-alphabet characters
+ in the input result in a binascii.Error.
+ """
+ s = _bytes_from_decode_data(s)
+ if altchars is not None:
+ altchars = _bytes_from_decode_data(altchars)
+ assert len(altchars) == 2, repr(altchars)
+ s = s.translate(bytes.maketrans(altchars, b'+/'))
+ if validate and not re.fullmatch(b'[A-Za-z0-9+/]*={0,2}', s):
+ raise binascii.Error('Non-base64 digit found')
+ return binascii.a2b_base64(s)
+
+
+def standard_b64encode(s):
+ """Encode bytes-like object s using the standard Base64 alphabet.
+
+ The result is returned as a bytes object.
+ """
+ return b64encode(s)
+
+def standard_b64decode(s):
+ """Decode bytes encoded with the standard Base64 alphabet.
+
+ Argument s is a bytes-like object or ASCII string to decode. The result
+ is returned as a bytes object. A binascii.Error is raised if the input
+ is incorrectly padded. Characters that are not in the standard alphabet
+ are discarded prior to the padding check.
+ """
+ return b64decode(s)
+
+
+_urlsafe_encode_translation = bytes.maketrans(b'+/', b'-_')
+_urlsafe_decode_translation = bytes.maketrans(b'-_', b'+/')
+
+def urlsafe_b64encode(s):
+ """Encode bytes using the URL- and filesystem-safe Base64 alphabet.
+
+ Argument s is a bytes-like object to encode. The result is returned as a
+ bytes object. The alphabet uses '-' instead of '+' and '_' instead of
+ '/'.
+ """
+ return b64encode(s).translate(_urlsafe_encode_translation)
+
+def urlsafe_b64decode(s):
+ """Decode bytes using the URL- and filesystem-safe Base64 alphabet.
+
+ Argument s is a bytes-like object or ASCII string to decode. The result
+ is returned as a bytes object. A binascii.Error is raised if the input
+ is incorrectly padded. Characters that are not in the URL-safe base-64
+ alphabet, and are not a plus '+' or slash '/', are discarded prior to the
+ padding check.
+
+ The alphabet uses '-' instead of '+' and '_' instead of '/'.
+ """
+ s = _bytes_from_decode_data(s)
+ s = s.translate(_urlsafe_decode_translation)
+ return b64decode(s)
+
+
+
+# Base32 encoding/decoding must be done in Python
+_b32alphabet = b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
+_b32tab2 = None
+_b32rev = None
+
+def b32encode(s):
+ """Encode the bytes-like object s using Base32 and return a bytes object.
+ """
+ global _b32tab2
+ # Delay the initialization of the table to not waste memory
+ # if the function is never called
+ if _b32tab2 is None:
+ b32tab = [bytes((i,)) for i in _b32alphabet]
+ _b32tab2 = [a + b for a in b32tab for b in b32tab]
+ b32tab = None
+
+ if not isinstance(s, bytes_types):
+ s = memoryview(s).tobytes()
+ leftover = len(s) % 5
+ # Pad the last quantum with zero bits if necessary
+ if leftover:
+ s = s + b'\0' * (5 - leftover) # Don't use += !
+ encoded = bytearray()
+ from_bytes = int.from_bytes
+ b32tab2 = _b32tab2
+ for i in range(0, len(s), 5):
+ c = from_bytes(s[i: i + 5], 'big')
+ encoded += (b32tab2[c >> 30] + # bits 1 - 10
+ b32tab2[(c >> 20) & 0x3ff] + # bits 11 - 20
+ b32tab2[(c >> 10) & 0x3ff] + # bits 21 - 30
+ b32tab2[c & 0x3ff] # bits 31 - 40
+ )
+ # Adjust for any leftover partial quanta
+ if leftover == 1:
+ encoded[-6:] = b'======'
+ elif leftover == 2:
+ encoded[-4:] = b'===='
+ elif leftover == 3:
+ encoded[-3:] = b'==='
+ elif leftover == 4:
+ encoded[-1:] = b'='
+ return bytes(encoded)
+
+def b32decode(s, casefold=False, map01=None):
+ """Decode the Base32 encoded bytes-like object or ASCII string s.
+
+ Optional casefold is a flag specifying whether a lowercase alphabet is
+ acceptable as input. For security purposes, the default is False.
+
+ RFC 3548 allows for optional mapping of the digit 0 (zero) to the
+ letter O (oh), and for optional mapping of the digit 1 (one) to
+ either the letter I (eye) or letter L (el). The optional argument
+ map01 when not None, specifies which letter the digit 1 should be
+ mapped to (when map01 is not None, the digit 0 is always mapped to
+ the letter O). For security purposes the default is None, so that
+ 0 and 1 are not allowed in the input.
+
+ The result is returned as a bytes object. A binascii.Error is raised if
+ the input is incorrectly padded or if there are non-alphabet
+ characters present in the input.
+ """
+ global _b32rev
+ # Delay the initialization of the table to not waste memory
+ # if the function is never called
+ if _b32rev is None:
+ _b32rev = {v: k for k, v in enumerate(_b32alphabet)}
+ s = _bytes_from_decode_data(s)
+ if len(s) % 8:
+ raise binascii.Error('Incorrect padding')
+ # Handle section 2.4 zero and one mapping. The flag map01 will be either
+ # False, or the character to map the digit 1 (one) to. It should be
+ # either L (el) or I (eye).
+ if map01 is not None:
+ map01 = _bytes_from_decode_data(map01)
+ assert len(map01) == 1, repr(map01)
+ s = s.translate(bytes.maketrans(b'01', b'O' + map01))
+ if casefold:
+ s = s.upper()
+ # Strip off pad characters from the right. We need to count the pad
+ # characters because this will tell us how many null bytes to remove from
+ # the end of the decoded string.
+ l = len(s)
+ s = s.rstrip(b'=')
+ padchars = l - len(s)
+ # Now decode the full quanta
+ decoded = bytearray()
+ b32rev = _b32rev
+ for i in range(0, len(s), 8):
+ quanta = s[i: i + 8]
+ acc = 0
+ try:
+ for c in quanta:
+ acc = (acc << 5) + b32rev[c]
+ except KeyError:
+ raise binascii.Error('Non-base32 digit found') from None
+ decoded += acc.to_bytes(5, 'big')
+ # Process the last, partial quanta
+ if l % 8 or padchars not in {0, 1, 3, 4, 6}:
+ raise binascii.Error('Incorrect padding')
+ if padchars and decoded:
+ acc <<= 5 * padchars
+ last = acc.to_bytes(5, 'big')
+ leftover = (43 - 5 * padchars) // 8 # 1: 4, 3: 3, 4: 2, 6: 1
+ decoded[-5:] = last[:leftover]
+ return bytes(decoded)
+
+
+# RFC 3548, Base 16 Alphabet specifies uppercase, but hexlify() returns
+# lowercase. The RFC also recommends against accepting input case
+# insensitively.
+def b16encode(s):
+ """Encode the bytes-like object s using Base16 and return a bytes object.
+ """
+ return binascii.hexlify(s).upper()
+
+
+def b16decode(s, casefold=False):
+ """Decode the Base16 encoded bytes-like object or ASCII string s.
+
+ Optional casefold is a flag specifying whether a lowercase alphabet is
+ acceptable as input. For security purposes, the default is False.
+
+ The result is returned as a bytes object. A binascii.Error is raised if
+ s is incorrectly padded or if there are non-alphabet characters present
+ in the input.
+ """
+ s = _bytes_from_decode_data(s)
+ if casefold:
+ s = s.upper()
+ if re.search(b'[^0-9A-F]', s):
+ raise binascii.Error('Non-base16 digit found')
+ return binascii.unhexlify(s)
+
+#
+# Ascii85 encoding/decoding
+#
+
+_a85chars = None
+_a85chars2 = None
+_A85START = b"<~"
+_A85END = b"~>"
+
+def _85encode(b, chars, chars2, pad=False, foldnuls=False, foldspaces=False):
+ # Helper function for a85encode and b85encode
+ if not isinstance(b, bytes_types):
+ b = memoryview(b).tobytes()
+
+ padding = (-len(b)) % 4
+ if padding:
+ b = b + b'\0' * padding
+ words = struct.Struct('!%dI' % (len(b) // 4)).unpack(b)
+
+ chunks = [b'z' if foldnuls and not word else
+ b'y' if foldspaces and word == 0x20202020 else
+ (chars2[word // 614125] +
+ chars2[word // 85 % 7225] +
+ chars[word % 85])
+ for word in words]
+
+ if padding and not pad:
+ if chunks[-1] == b'z':
+ chunks[-1] = chars[0] * 5
+ chunks[-1] = chunks[-1][:-padding]
+
+ return b''.join(chunks)
+
+def a85encode(b, *, foldspaces=False, wrapcol=0, pad=False, adobe=False):
+ """Encode bytes-like object b using Ascii85 and return a bytes object.
+
+ foldspaces is an optional flag that uses the special short sequence 'y'
+ instead of 4 consecutive spaces (ASCII 0x20) as supported by 'btoa'. This
+ feature is not supported by the "standard" Adobe encoding.
+
+ wrapcol controls whether the output should have newline (b'\\n') characters
+ added to it. If this is non-zero, each output line will be at most this
+ many characters long.
+
+ pad controls whether the input is padded to a multiple of 4 before
+ encoding. Note that the btoa implementation always pads.
+
+ adobe controls whether the encoded byte sequence is framed with <~ and ~>,
+ which is used by the Adobe implementation.
+ """
+ global _a85chars, _a85chars2
+ # Delay the initialization of tables to not waste memory
+ # if the function is never called
+ if _a85chars is None:
+ _a85chars = [bytes((i,)) for i in range(33, 118)]
+ _a85chars2 = [(a + b) for a in _a85chars for b in _a85chars]
+
+ result = _85encode(b, _a85chars, _a85chars2, pad, True, foldspaces)
+
+ if adobe:
+ result = _A85START + result
+ if wrapcol:
+ wrapcol = max(2 if adobe else 1, wrapcol)
+ chunks = [result[i: i + wrapcol]
+ for i in range(0, len(result), wrapcol)]
+ if adobe:
+ if len(chunks[-1]) + 2 > wrapcol:
+ chunks.append(b'')
+ result = b'\n'.join(chunks)
+ if adobe:
+ result += _A85END
+
+ return result
+
+def a85decode(b, *, foldspaces=False, adobe=False, ignorechars=b' \t\n\r\v'):
+ """Decode the Ascii85 encoded bytes-like object or ASCII string b.
+
+ foldspaces is a flag that specifies whether the 'y' short sequence should be
+ accepted as shorthand for 4 consecutive spaces (ASCII 0x20). This feature is
+ not supported by the "standard" Adobe encoding.
+
+ adobe controls whether the input sequence is in Adobe Ascii85 format (i.e.
+ is framed with <~ and ~>).
+
+ ignorechars should be a byte string containing characters to ignore from the
+ input. This should only contain whitespace characters, and by default
+ contains all whitespace characters in ASCII.
+
+ The result is returned as a bytes object.
+ """
+ b = _bytes_from_decode_data(b)
+ if adobe:
+ if not b.endswith(_A85END):
+ raise ValueError(
+ "Ascii85 encoded byte sequences must end "
+ "with {!r}".format(_A85END)
+ )
+ if b.startswith(_A85START):
+ b = b[2:-2] # Strip off start/end markers
+ else:
+ b = b[:-2]
+ #
+ # We have to go through this stepwise, so as to ignore spaces and handle
+ # special short sequences
+ #
+ packI = struct.Struct('!I').pack
+ decoded = []
+ decoded_append = decoded.append
+ curr = []
+ curr_append = curr.append
+ curr_clear = curr.clear
+ for x in b + b'u' * 4:
+ if b'!'[0] <= x <= b'u'[0]:
+ curr_append(x)
+ if len(curr) == 5:
+ acc = 0
+ for x in curr:
+ acc = 85 * acc + (x - 33)
+ try:
+ decoded_append(packI(acc))
+ except struct.error:
+ raise ValueError('Ascii85 overflow') from None
+ curr_clear()
+ elif x == b'z'[0]:
+ if curr:
+ raise ValueError('z inside Ascii85 5-tuple')
+ decoded_append(b'\0\0\0\0')
+ elif foldspaces and x == b'y'[0]:
+ if curr:
+ raise ValueError('y inside Ascii85 5-tuple')
+ decoded_append(b'\x20\x20\x20\x20')
+ elif x in ignorechars:
+ # Skip whitespace
+ continue
+ else:
+ raise ValueError('Non-Ascii85 digit found: %c' % x)
+
+ result = b''.join(decoded)
+ padding = 4 - len(curr)
+ if padding:
+ # Throw away the extra padding
+ result = result[:-padding]
+ return result
+
+# The following code is originally taken (with permission) from Mercurial
+
+_b85alphabet = (b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ b"abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~")
+_b85chars = None
+_b85chars2 = None
+_b85dec = None
+
+def b85encode(b, pad=False):
+ """Encode bytes-like object b in base85 format and return a bytes object.
+
+ If pad is true, the input is padded with b'\\0' so its length is a multiple of
+ 4 bytes before encoding.
+ """
+ global _b85chars, _b85chars2
+ # Delay the initialization of tables to not waste memory
+ # if the function is never called
+ if _b85chars is None:
+ _b85chars = [bytes((i,)) for i in _b85alphabet]
+ _b85chars2 = [(a + b) for a in _b85chars for b in _b85chars]
+ return _85encode(b, _b85chars, _b85chars2, pad)
+
+def b85decode(b):
+ """Decode the base85-encoded bytes-like object or ASCII string b
+
+ The result is returned as a bytes object.
+ """
+ global _b85dec
+ # Delay the initialization of tables to not waste memory
+ # if the function is never called
+ if _b85dec is None:
+ _b85dec = [None] * 256
+ for i, c in enumerate(_b85alphabet):
+ _b85dec[c] = i
+
+ b = _bytes_from_decode_data(b)
+ padding = (-len(b)) % 5
+ b = b + b'~' * padding
+ out = []
+ packI = struct.Struct('!I').pack
+ for i in range(0, len(b), 5):
+ chunk = b[i:i + 5]
+ acc = 0
+ try:
+ for c in chunk:
+ acc = acc * 85 + _b85dec[c]
+ except TypeError:
+ for j, c in enumerate(chunk):
+ if _b85dec[c] is None:
+ raise ValueError('bad base85 character at position %d'
+ % (i + j)) from None
+ raise
+ try:
+ out.append(packI(acc))
+ except struct.error:
+ raise ValueError('base85 overflow in hunk starting at byte %d'
+ % i) from None
+
+ result = b''.join(out)
+ if padding:
+ result = result[:-padding]
+ return result
+
+# Legacy interface. This code could be cleaned up since I don't believe
+# binascii has any line length limitations. It just doesn't seem worth it
+# though. The files should be opened in binary mode.
+
+MAXLINESIZE = 76 # Excluding the CRLF
+MAXBINSIZE = (MAXLINESIZE//4)*3
+
+def encode(input, output):
+ """Encode a file; input and output are binary files."""
+ while True:
+ s = input.read(MAXBINSIZE)
+ if not s:
+ break
+ while len(s) < MAXBINSIZE:
+ ns = input.read(MAXBINSIZE-len(s))
+ if not ns:
+ break
+ s += ns
+ line = binascii.b2a_base64(s)
+ output.write(line)
+
+
+def decode(input, output):
+ """Decode a file; input and output are binary files."""
+ while True:
+ line = input.readline()
+ if not line:
+ break
+ s = binascii.a2b_base64(line)
+ output.write(s)
+
+def _input_type_check(s):
+ try:
+ m = memoryview(s)
+ except TypeError as err:
+ msg = "expected bytes-like object, not %s" % s.__class__.__name__
+ raise TypeError(msg) from err
+ if m.format not in ('c', 'b', 'B'):
+ msg = ("expected single byte elements, not %r from %s" %
+ (m.format, s.__class__.__name__))
+ raise TypeError(msg)
+ if m.ndim != 1:
+ msg = ("expected 1-D data, not %d-D data from %s" %
+ (m.ndim, s.__class__.__name__))
+ raise TypeError(msg)
+
+
+def encodebytes(s):
+ """Encode a bytestring into a bytes object containing multiple lines
+ of base-64 data."""
+ _input_type_check(s)
+ pieces = []
+ for i in range(0, len(s), MAXBINSIZE):
+ chunk = s[i : i + MAXBINSIZE]
+ pieces.append(binascii.b2a_base64(chunk))
+ return b"".join(pieces)
+
+def encodestring(s):
+ """Legacy alias of encodebytes()."""
+ import warnings
+ warnings.warn("encodestring() is a deprecated alias since 3.1, "
+ "use encodebytes()",
+ DeprecationWarning, 2)
+ return encodebytes(s)
+
+
+def decodebytes(s):
+ """Decode a bytestring of base-64 data into a bytes object."""
+ _input_type_check(s)
+ return binascii.a2b_base64(s)
+
+def decodestring(s):
+ """Legacy alias of decodebytes()."""
+ import warnings
+ warnings.warn("decodestring() is a deprecated alias since Python 3.1, "
+ "use decodebytes()",
+ DeprecationWarning, 2)
+ return decodebytes(s)
+
+
+# Usable as a script...
+def main():
+ """Small main program"""
+ import sys, getopt
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], 'deut')
+ except getopt.error as msg:
+ sys.stdout = sys.stderr
+ print(msg)
+ print("""usage: %s [-d|-e|-u|-t] [file|-]
+ -d, -u: decode
+ -e: encode (default)
+ -t: encode and decode string 'Aladdin:open sesame'"""%sys.argv[0])
+ sys.exit(2)
+ func = encode
+ for o, a in opts:
+ if o == '-e': func = encode
+ if o == '-d': func = decode
+ if o == '-u': func = decode
+ if o == '-t': test(); return
+ if args and args[0] != '-':
+ with open(args[0], 'rb') as f:
+ func(f, sys.stdout.buffer)
+ else:
+ func(sys.stdin.buffer, sys.stdout.buffer)
+
+
+def test():
+ s0 = b"Aladdin:open sesame"
+ print(repr(s0))
+ s1 = encodebytes(s0)
+ print(repr(s1))
+ s2 = decodebytes(s1)
+ print(repr(s2))
+ assert s0 == s2
+
+
+if __name__ == '__main__':
+ main()
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/datetime.html b/docs/_build/html/_modules/datetime.html
new file mode 100644
index 0000000000..6770f880bf
--- /dev/null
+++ b/docs/_build/html/_modules/datetime.html
@@ -0,0 +1,2640 @@
+
+
+
+
+
+
+
+
+
+"""Concrete date/time and related types.
+
+See http://www.iana.org/time-zones/repository/tz-link.html for
+time zone and DST data sources.
+"""
+
+import time as _time
+import math as _math
+import sys
+
+def _cmp(x, y):
+ return 0 if x == y else 1 if x > y else -1
+
+MINYEAR = 1
+MAXYEAR = 9999
+_MAXORDINAL = 3652059 # date.max.toordinal()
+
+# Utility functions, adapted from Python's Demo/classes/Dates.py, which
+# also assumes the current Gregorian calendar indefinitely extended in
+# both directions. Difference: Dates.py calls January 1 of year 0 day
+# number 1. The code here calls January 1 of year 1 day number 1. This is
+# to match the definition of the "proleptic Gregorian" calendar in Dershowitz
+# and Reingold's "Calendrical Calculations", where it's the base calendar
+# for all computations. See the book for algorithms for converting between
+# proleptic Gregorian ordinals and many other calendar systems.
+
+# -1 is a placeholder for indexing purposes.
+_DAYS_IN_MONTH = [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
+
+_DAYS_BEFORE_MONTH = [-1] # -1 is a placeholder for indexing purposes.
+dbm = 0
+for dim in _DAYS_IN_MONTH[1:]:
+ _DAYS_BEFORE_MONTH.append(dbm)
+ dbm += dim
+del dbm, dim
+
+def _is_leap(year):
+ "year -> 1 if leap year, else 0."
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
+
+def _days_before_year(year):
+ "year -> number of days before January 1st of year."
+ y = year - 1
+ return y*365 + y//4 - y//100 + y//400
+
+def _days_in_month(year, month):
+ "year, month -> number of days in that month in that year."
+ assert 1 <= month <= 12, month
+ if month == 2 and _is_leap(year):
+ return 29
+ return _DAYS_IN_MONTH[month]
+
+def _days_before_month(year, month):
+ "year, month -> number of days in year preceding first day of month."
+ assert 1 <= month <= 12, 'month must be in 1..12'
+ return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year))
+
+def _ymd2ord(year, month, day):
+ "year, month, day -> ordinal, considering 01-Jan-0001 as day 1."
+ assert 1 <= month <= 12, 'month must be in 1..12'
+ dim = _days_in_month(year, month)
+ assert 1 <= day <= dim, ('day must be in 1..%d' % dim)
+ return (_days_before_year(year) +
+ _days_before_month(year, month) +
+ day)
+
+_DI400Y = _days_before_year(401) # number of days in 400 years
+_DI100Y = _days_before_year(101) # " " " " 100 "
+_DI4Y = _days_before_year(5) # " " " " 4 "
+
+# A 4-year cycle has an extra leap day over what we'd get from pasting
+# together 4 single years.
+assert _DI4Y == 4 * 365 + 1
+
+# Similarly, a 400-year cycle has an extra leap day over what we'd get from
+# pasting together 4 100-year cycles.
+assert _DI400Y == 4 * _DI100Y + 1
+
+# OTOH, a 100-year cycle has one fewer leap day than we'd get from
+# pasting together 25 4-year cycles.
+assert _DI100Y == 25 * _DI4Y - 1
+
+def _ord2ymd(n):
+ "ordinal -> (year, month, day), considering 01-Jan-0001 as day 1."
+
+ # n is a 1-based index, starting at 1-Jan-1. The pattern of leap years
+ # repeats exactly every 400 years. The basic strategy is to find the
+ # closest 400-year boundary at or before n, then work with the offset
+ # from that boundary to n. Life is much clearer if we subtract 1 from
+ # n first -- then the values of n at 400-year boundaries are exactly
+ # those divisible by _DI400Y:
+ #
+ # D M Y n n-1
+ # -- --- ---- ---------- ----------------
+ # 31 Dec -400 -_DI400Y -_DI400Y -1
+ # 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary
+ # ...
+ # 30 Dec 000 -1 -2
+ # 31 Dec 000 0 -1
+ # 1 Jan 001 1 0 400-year boundary
+ # 2 Jan 001 2 1
+ # 3 Jan 001 3 2
+ # ...
+ # 31 Dec 400 _DI400Y _DI400Y -1
+ # 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary
+ n -= 1
+ n400, n = divmod(n, _DI400Y)
+ year = n400 * 400 + 1 # ..., -399, 1, 401, ...
+
+ # Now n is the (non-negative) offset, in days, from January 1 of year, to
+ # the desired date. Now compute how many 100-year cycles precede n.
+ # Note that it's possible for n100 to equal 4! In that case 4 full
+ # 100-year cycles precede the desired day, which implies the desired
+ # day is December 31 at the end of a 400-year cycle.
+ n100, n = divmod(n, _DI100Y)
+
+ # Now compute how many 4-year cycles precede it.
+ n4, n = divmod(n, _DI4Y)
+
+ # And now how many single years. Again n1 can be 4, and again meaning
+ # that the desired day is December 31 at the end of the 4-year cycle.
+ n1, n = divmod(n, 365)
+
+ year += n100 * 100 + n4 * 4 + n1
+ if n1 == 4 or n100 == 4:
+ assert n == 0
+ return year-1, 12, 31
+
+ # Now the year is correct, and n is the offset from January 1. We find
+ # the month via an estimate that's either exact or one too large.
+ leapyear = n1 == 3 and (n4 != 24 or n100 == 3)
+ assert leapyear == _is_leap(year)
+ month = (n + 50) >> 5
+ preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear)
+ if preceding > n: # estimate is too large
+ month -= 1
+ preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear)
+ n -= preceding
+ assert 0 <= n < _days_in_month(year, month)
+
+ # Now the year and month are correct, and n is the offset from the
+ # start of that month: we're done!
+ return year, month, n+1
+
+# Month and day names. For localized versions, see the calendar module.
+_MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+_DAYNAMES = [None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+
+
+def _build_struct_time(y, m, d, hh, mm, ss, dstflag):
+ wday = (_ymd2ord(y, m, d) + 6) % 7
+ dnum = _days_before_month(y, m) + d
+ return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag))
+
+def _format_time(hh, mm, ss, us, timespec='auto'):
+ specs = {
+ 'hours': '{:02d}',
+ 'minutes': '{:02d}:{:02d}',
+ 'seconds': '{:02d}:{:02d}:{:02d}',
+ 'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}',
+ 'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}'
+ }
+
+ if timespec == 'auto':
+ # Skip trailing microseconds when us==0.
+ timespec = 'microseconds' if us else 'seconds'
+ elif timespec == 'milliseconds':
+ us //= 1000
+ try:
+ fmt = specs[timespec]
+ except KeyError:
+ raise ValueError('Unknown timespec value')
+ else:
+ return fmt.format(hh, mm, ss, us)
+
+def _format_offset(off):
+ s = ''
+ if off is not None:
+ if off.days < 0:
+ sign = "-"
+ off = -off
+ else:
+ sign = "+"
+ hh, mm = divmod(off, timedelta(hours=1))
+ mm, ss = divmod(mm, timedelta(minutes=1))
+ s += "%s%02d:%02d" % (sign, hh, mm)
+ if ss or ss.microseconds:
+ s += ":%02d" % ss.seconds
+
+ if ss.microseconds:
+ s += '.%06d' % ss.microseconds
+ return s
+
+# Correctly substitute for %z and %Z escapes in strftime formats.
+def _wrap_strftime(object, format, timetuple):
+ # Don't call utcoffset() or tzname() unless actually needed.
+ freplace = None # the string to use for %f
+ zreplace = None # the string to use for %z
+ Zreplace = None # the string to use for %Z
+
+ # Scan format for %z and %Z escapes, replacing as needed.
+ newformat = []
+ push = newformat.append
+ i, n = 0, len(format)
+ while i < n:
+ ch = format[i]
+ i += 1
+ if ch == '%':
+ if i < n:
+ ch = format[i]
+ i += 1
+ if ch == 'f':
+ if freplace is None:
+ freplace = '%06d' % getattr(object,
+ 'microsecond', 0)
+ newformat.append(freplace)
+ elif ch == 'z':
+ if zreplace is None:
+ zreplace = ""
+ if hasattr(object, "utcoffset"):
+ offset = object.utcoffset()
+ if offset is not None:
+ sign = '+'
+ if offset.days < 0:
+ offset = -offset
+ sign = '-'
+ h, rest = divmod(offset, timedelta(hours=1))
+ m, rest = divmod(rest, timedelta(minutes=1))
+ s = rest.seconds
+ u = offset.microseconds
+ if u:
+ zreplace = '%c%02d%02d%02d.%06d' % (sign, h, m, s, u)
+ elif s:
+ zreplace = '%c%02d%02d%02d' % (sign, h, m, s)
+ else:
+ zreplace = '%c%02d%02d' % (sign, h, m)
+ assert '%' not in zreplace
+ newformat.append(zreplace)
+ elif ch == 'Z':
+ if Zreplace is None:
+ Zreplace = ""
+ if hasattr(object, "tzname"):
+ s = object.tzname()
+ if s is not None:
+ # strftime is going to have at this: escape %
+ Zreplace = s.replace('%', '%%')
+ newformat.append(Zreplace)
+ else:
+ push('%')
+ push(ch)
+ else:
+ push('%')
+ else:
+ push(ch)
+ newformat = "".join(newformat)
+ return _time.strftime(newformat, timetuple)
+
+# Helpers for parsing the result of isoformat()
+def _parse_isoformat_date(dtstr):
+ # It is assumed that this function will only be called with a
+ # string of length exactly 10, and (though this is not used) ASCII-only
+ year = int(dtstr[0:4])
+ if dtstr[4] != '-':
+ raise ValueError('Invalid date separator: %s' % dtstr[4])
+
+ month = int(dtstr[5:7])
+
+ if dtstr[7] != '-':
+ raise ValueError('Invalid date separator')
+
+ day = int(dtstr[8:10])
+
+ return [year, month, day]
+
+def _parse_hh_mm_ss_ff(tstr):
+ # Parses things of the form HH[:MM[:SS[.fff[fff]]]]
+ len_str = len(tstr)
+
+ time_comps = [0, 0, 0, 0]
+ pos = 0
+ for comp in range(0, 3):
+ if (len_str - pos) < 2:
+ raise ValueError('Incomplete time component')
+
+ time_comps[comp] = int(tstr[pos:pos+2])
+
+ pos += 2
+ next_char = tstr[pos:pos+1]
+
+ if not next_char or comp >= 2:
+ break
+
+ if next_char != ':':
+ raise ValueError('Invalid time separator: %c' % next_char)
+
+ pos += 1
+
+ if pos < len_str:
+ if tstr[pos] != '.':
+ raise ValueError('Invalid microsecond component')
+ else:
+ pos += 1
+
+ len_remainder = len_str - pos
+ if len_remainder not in (3, 6):
+ raise ValueError('Invalid microsecond component')
+
+ time_comps[3] = int(tstr[pos:])
+ if len_remainder == 3:
+ time_comps[3] *= 1000
+
+ return time_comps
+
+def _parse_isoformat_time(tstr):
+ # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]
+ len_str = len(tstr)
+ if len_str < 2:
+ raise ValueError('Isoformat time too short')
+
+ # This is equivalent to re.search('[+-]', tstr), but faster
+ tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1)
+ timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr
+
+ time_comps = _parse_hh_mm_ss_ff(timestr)
+
+ tzi = None
+ if tz_pos > 0:
+ tzstr = tstr[tz_pos:]
+
+ # Valid time zone strings are:
+ # HH:MM len: 5
+ # HH:MM:SS len: 8
+ # HH:MM:SS.ffffff len: 15
+
+ if len(tzstr) not in (5, 8, 15):
+ raise ValueError('Malformed time zone string')
+
+ tz_comps = _parse_hh_mm_ss_ff(tzstr)
+ if all(x == 0 for x in tz_comps):
+ tzi = timezone.utc
+ else:
+ tzsign = -1 if tstr[tz_pos - 1] == '-' else 1
+
+ td = timedelta(hours=tz_comps[0], minutes=tz_comps[1],
+ seconds=tz_comps[2], microseconds=tz_comps[3])
+
+ tzi = timezone(tzsign * td)
+
+ time_comps.append(tzi)
+
+ return time_comps
+
+
+# Just raise TypeError if the arg isn't None or a string.
+def _check_tzname(name):
+ if name is not None and not isinstance(name, str):
+ raise TypeError("tzinfo.tzname() must return None or string, "
+ "not '%s'" % type(name))
+
+# name is the offset-producing method, "utcoffset" or "dst".
+# offset is what it returned.
+# If offset isn't None or timedelta, raises TypeError.
+# If offset is None, returns None.
+# Else offset is checked for being in range.
+# If it is, its integer value is returned. Else ValueError is raised.
+def _check_utc_offset(name, offset):
+ assert name in ("utcoffset", "dst")
+ if offset is None:
+ return
+ if not isinstance(offset, timedelta):
+ raise TypeError("tzinfo.%s() must return None "
+ "or timedelta, not '%s'" % (name, type(offset)))
+ if not -timedelta(1) < offset < timedelta(1):
+ raise ValueError("%s()=%s, must be strictly between "
+ "-timedelta(hours=24) and timedelta(hours=24)" %
+ (name, offset))
+
+def _check_int_field(value):
+ if isinstance(value, int):
+ return value
+ if isinstance(value, float):
+ raise TypeError('integer argument expected, got float')
+ try:
+ value = value.__index__()
+ except AttributeError:
+ pass
+ else:
+ if not isinstance(value, int):
+ raise TypeError('__index__ returned non-int (type %s)' %
+ type(value).__name__)
+ return value
+ orig = value
+ try:
+ value = value.__int__()
+ except AttributeError:
+ pass
+ else:
+ if not isinstance(value, int):
+ raise TypeError('__int__ returned non-int (type %s)' %
+ type(value).__name__)
+ import warnings
+ warnings.warn("an integer is required (got type %s)" %
+ type(orig).__name__,
+ DeprecationWarning,
+ stacklevel=2)
+ return value
+ raise TypeError('an integer is required (got type %s)' %
+ type(value).__name__)
+
+def _check_date_fields(year, month, day):
+ year = _check_int_field(year)
+ month = _check_int_field(month)
+ day = _check_int_field(day)
+ if not MINYEAR <= year <= MAXYEAR:
+ raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year)
+ if not 1 <= month <= 12:
+ raise ValueError('month must be in 1..12', month)
+ dim = _days_in_month(year, month)
+ if not 1 <= day <= dim:
+ raise ValueError('day must be in 1..%d' % dim, day)
+ return year, month, day
+
+def _check_time_fields(hour, minute, second, microsecond, fold):
+ hour = _check_int_field(hour)
+ minute = _check_int_field(minute)
+ second = _check_int_field(second)
+ microsecond = _check_int_field(microsecond)
+ if not 0 <= hour <= 23:
+ raise ValueError('hour must be in 0..23', hour)
+ if not 0 <= minute <= 59:
+ raise ValueError('minute must be in 0..59', minute)
+ if not 0 <= second <= 59:
+ raise ValueError('second must be in 0..59', second)
+ if not 0 <= microsecond <= 999999:
+ raise ValueError('microsecond must be in 0..999999', microsecond)
+ if fold not in (0, 1):
+ raise ValueError('fold must be either 0 or 1', fold)
+ return hour, minute, second, microsecond, fold
+
+def _check_tzinfo_arg(tz):
+ if tz is not None and not isinstance(tz, tzinfo):
+ raise TypeError("tzinfo argument must be None or of a tzinfo subclass")
+
+def _cmperror(x, y):
+ raise TypeError("can't compare '%s' to '%s'" % (
+ type(x).__name__, type(y).__name__))
+
+def _divide_and_round(a, b):
+ """divide a by b and round result to the nearest integer
+
+ When the ratio is exactly half-way between two integers,
+ the even integer is returned.
+ """
+ # Based on the reference implementation for divmod_near
+ # in Objects/longobject.c.
+ q, r = divmod(a, b)
+ # round up if either r / b > 0.5, or r / b == 0.5 and q is odd.
+ # The expression r / b > 0.5 is equivalent to 2 * r > b if b is
+ # positive, 2 * r < b if b negative.
+ r *= 2
+ greater_than_half = r > b if b > 0 else r < b
+ if greater_than_half or r == b and q % 2 == 1:
+ q += 1
+
+ return q
+
+
+class timedelta:
+ """Represent the difference between two datetime objects.
+
+ Supported operators:
+
+ - add, subtract timedelta
+ - unary plus, minus, abs
+ - compare to timedelta
+ - multiply, divide by int
+
+ In addition, datetime supports subtraction of two datetime objects
+ returning a timedelta, and addition or subtraction of a datetime
+ and a timedelta giving a datetime.
+
+ Representation: (days, seconds, microseconds). Why? Because I
+ felt like it.
+ """
+ __slots__ = '_days', '_seconds', '_microseconds', '_hashcode'
+
+ def __new__(cls, days=0, seconds=0, microseconds=0,
+ milliseconds=0, minutes=0, hours=0, weeks=0):
+ # Doing this efficiently and accurately in C is going to be difficult
+ # and error-prone, due to ubiquitous overflow possibilities, and that
+ # C double doesn't have enough bits of precision to represent
+ # microseconds over 10K years faithfully. The code here tries to make
+ # explicit where go-fast assumptions can be relied on, in order to
+ # guide the C implementation; it's way more convoluted than speed-
+ # ignoring auto-overflow-to-long idiomatic Python could be.
+
+ # XXX Check that all inputs are ints or floats.
+
+ # Final values, all integer.
+ # s and us fit in 32-bit signed ints; d isn't bounded.
+ d = s = us = 0
+
+ # Normalize everything to days, seconds, microseconds.
+ days += weeks*7
+ seconds += minutes*60 + hours*3600
+ microseconds += milliseconds*1000
+
+ # Get rid of all fractions, and normalize s and us.
+ # Take a deep breath <wink>.
+ if isinstance(days, float):
+ dayfrac, days = _math.modf(days)
+ daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.))
+ assert daysecondswhole == int(daysecondswhole) # can't overflow
+ s = int(daysecondswhole)
+ assert days == int(days)
+ d = int(days)
+ else:
+ daysecondsfrac = 0.0
+ d = days
+ assert isinstance(daysecondsfrac, float)
+ assert abs(daysecondsfrac) <= 1.0
+ assert isinstance(d, int)
+ assert abs(s) <= 24 * 3600
+ # days isn't referenced again before redefinition
+
+ if isinstance(seconds, float):
+ secondsfrac, seconds = _math.modf(seconds)
+ assert seconds == int(seconds)
+ seconds = int(seconds)
+ secondsfrac += daysecondsfrac
+ assert abs(secondsfrac) <= 2.0
+ else:
+ secondsfrac = daysecondsfrac
+ # daysecondsfrac isn't referenced again
+ assert isinstance(secondsfrac, float)
+ assert abs(secondsfrac) <= 2.0
+
+ assert isinstance(seconds, int)
+ days, seconds = divmod(seconds, 24*3600)
+ d += days
+ s += int(seconds) # can't overflow
+ assert isinstance(s, int)
+ assert abs(s) <= 2 * 24 * 3600
+ # seconds isn't referenced again before redefinition
+
+ usdouble = secondsfrac * 1e6
+ assert abs(usdouble) < 2.1e6 # exact value not critical
+ # secondsfrac isn't referenced again
+
+ if isinstance(microseconds, float):
+ microseconds = round(microseconds + usdouble)
+ seconds, microseconds = divmod(microseconds, 1000000)
+ days, seconds = divmod(seconds, 24*3600)
+ d += days
+ s += seconds
+ else:
+ microseconds = int(microseconds)
+ seconds, microseconds = divmod(microseconds, 1000000)
+ days, seconds = divmod(seconds, 24*3600)
+ d += days
+ s += seconds
+ microseconds = round(microseconds + usdouble)
+ assert isinstance(s, int)
+ assert isinstance(microseconds, int)
+ assert abs(s) <= 3 * 24 * 3600
+ assert abs(microseconds) < 3.1e6
+
+ # Just a little bit of carrying possible for microseconds and seconds.
+ seconds, us = divmod(microseconds, 1000000)
+ s += seconds
+ days, s = divmod(s, 24*3600)
+ d += days
+
+ assert isinstance(d, int)
+ assert isinstance(s, int) and 0 <= s < 24*3600
+ assert isinstance(us, int) and 0 <= us < 1000000
+
+ if abs(d) > 999999999:
+ raise OverflowError("timedelta # of days is too large: %d" % d)
+
+ self = object.__new__(cls)
+ self._days = d
+ self._seconds = s
+ self._microseconds = us
+ self._hashcode = -1
+ return self
+
+ def __repr__(self):
+ args = []
+ if self._days:
+ args.append("days=%d" % self._days)
+ if self._seconds:
+ args.append("seconds=%d" % self._seconds)
+ if self._microseconds:
+ args.append("microseconds=%d" % self._microseconds)
+ if not args:
+ args.append('0')
+ return "%s.%s(%s)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ ', '.join(args))
+
+ def __str__(self):
+ mm, ss = divmod(self._seconds, 60)
+ hh, mm = divmod(mm, 60)
+ s = "%d:%02d:%02d" % (hh, mm, ss)
+ if self._days:
+ def plural(n):
+ return n, abs(n) != 1 and "s" or ""
+ s = ("%d day%s, " % plural(self._days)) + s
+ if self._microseconds:
+ s = s + ".%06d" % self._microseconds
+ return s
+
+ def total_seconds(self):
+ """Total seconds in the duration."""
+ return ((self.days * 86400 + self.seconds) * 10**6 +
+ self.microseconds) / 10**6
+
+ # Read-only field accessors
+ @property
+ def days(self):
+ """days"""
+ return self._days
+
+ @property
+ def seconds(self):
+ """seconds"""
+ return self._seconds
+
+ @property
+ def microseconds(self):
+ """microseconds"""
+ return self._microseconds
+
+ def __add__(self, other):
+ if isinstance(other, timedelta):
+ # for CPython compatibility, we cannot use
+ # our __class__ here, but need a real timedelta
+ return timedelta(self._days + other._days,
+ self._seconds + other._seconds,
+ self._microseconds + other._microseconds)
+ return NotImplemented
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ if isinstance(other, timedelta):
+ # for CPython compatibility, we cannot use
+ # our __class__ here, but need a real timedelta
+ return timedelta(self._days - other._days,
+ self._seconds - other._seconds,
+ self._microseconds - other._microseconds)
+ return NotImplemented
+
+ def __rsub__(self, other):
+ if isinstance(other, timedelta):
+ return -self + other
+ return NotImplemented
+
+ def __neg__(self):
+ # for CPython compatibility, we cannot use
+ # our __class__ here, but need a real timedelta
+ return timedelta(-self._days,
+ -self._seconds,
+ -self._microseconds)
+
+ def __pos__(self):
+ return self
+
+ def __abs__(self):
+ if self._days < 0:
+ return -self
+ else:
+ return self
+
+ def __mul__(self, other):
+ if isinstance(other, int):
+ # for CPython compatibility, we cannot use
+ # our __class__ here, but need a real timedelta
+ return timedelta(self._days * other,
+ self._seconds * other,
+ self._microseconds * other)
+ if isinstance(other, float):
+ usec = self._to_microseconds()
+ a, b = other.as_integer_ratio()
+ return timedelta(0, 0, _divide_and_round(usec * a, b))
+ return NotImplemented
+
+ __rmul__ = __mul__
+
+ def _to_microseconds(self):
+ return ((self._days * (24*3600) + self._seconds) * 1000000 +
+ self._microseconds)
+
+ def __floordiv__(self, other):
+ if not isinstance(other, (int, timedelta)):
+ return NotImplemented
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return usec // other._to_microseconds()
+ if isinstance(other, int):
+ return timedelta(0, 0, usec // other)
+
+ def __truediv__(self, other):
+ if not isinstance(other, (int, float, timedelta)):
+ return NotImplemented
+ usec = self._to_microseconds()
+ if isinstance(other, timedelta):
+ return usec / other._to_microseconds()
+ if isinstance(other, int):
+ return timedelta(0, 0, _divide_and_round(usec, other))
+ if isinstance(other, float):
+ a, b = other.as_integer_ratio()
+ return timedelta(0, 0, _divide_and_round(b * usec, a))
+
+ def __mod__(self, other):
+ if isinstance(other, timedelta):
+ r = self._to_microseconds() % other._to_microseconds()
+ return timedelta(0, 0, r)
+ return NotImplemented
+
+ def __divmod__(self, other):
+ if isinstance(other, timedelta):
+ q, r = divmod(self._to_microseconds(),
+ other._to_microseconds())
+ return q, timedelta(0, 0, r)
+ return NotImplemented
+
+ # Comparisons of timedelta objects with other.
+
+ def __eq__(self, other):
+ if isinstance(other, timedelta):
+ return self._cmp(other) == 0
+ else:
+ return NotImplemented
+
+ def __le__(self, other):
+ if isinstance(other, timedelta):
+ return self._cmp(other) <= 0
+ else:
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, timedelta):
+ return self._cmp(other) < 0
+ else:
+ return NotImplemented
+
+ def __ge__(self, other):
+ if isinstance(other, timedelta):
+ return self._cmp(other) >= 0
+ else:
+ return NotImplemented
+
+ def __gt__(self, other):
+ if isinstance(other, timedelta):
+ return self._cmp(other) > 0
+ else:
+ return NotImplemented
+
+ def _cmp(self, other):
+ assert isinstance(other, timedelta)
+ return _cmp(self._getstate(), other._getstate())
+
+ def __hash__(self):
+ if self._hashcode == -1:
+ self._hashcode = hash(self._getstate())
+ return self._hashcode
+
+ def __bool__(self):
+ return (self._days != 0 or
+ self._seconds != 0 or
+ self._microseconds != 0)
+
+ # Pickle support.
+
+ def _getstate(self):
+ return (self._days, self._seconds, self._microseconds)
+
+ def __reduce__(self):
+ return (self.__class__, self._getstate())
+
+timedelta.min = timedelta(-999999999)
+timedelta.max = timedelta(days=999999999, hours=23, minutes=59, seconds=59,
+ microseconds=999999)
+timedelta.resolution = timedelta(microseconds=1)
+
+class date:
+ """Concrete date type.
+
+ Constructors:
+
+ __new__()
+ fromtimestamp()
+ today()
+ fromordinal()
+
+ Operators:
+
+ __repr__, __str__
+ __eq__, __le__, __lt__, __ge__, __gt__, __hash__
+ __add__, __radd__, __sub__ (add/radd only with timedelta arg)
+
+ Methods:
+
+ timetuple()
+ toordinal()
+ weekday()
+ isoweekday(), isocalendar(), isoformat()
+ ctime()
+ strftime()
+
+ Properties (readonly):
+ year, month, day
+ """
+ __slots__ = '_year', '_month', '_day', '_hashcode'
+
+ def __new__(cls, year, month=None, day=None):
+ """Constructor.
+
+ Arguments:
+
+ year, month, day (required, base 1)
+ """
+ if (month is None and
+ isinstance(year, (bytes, str)) and len(year) == 4 and
+ 1 <= ord(year[2:3]) <= 12):
+ # Pickle support
+ if isinstance(year, str):
+ try:
+ year = year.encode('latin1')
+ except UnicodeEncodeError:
+ # More informative error message.
+ raise ValueError(
+ "Failed to encode latin1 string when unpickling "
+ "a date object. "
+ "pickle.load(data, encoding='latin1') is assumed.")
+ self = object.__new__(cls)
+ self.__setstate(year)
+ self._hashcode = -1
+ return self
+ year, month, day = _check_date_fields(year, month, day)
+ self = object.__new__(cls)
+ self._year = year
+ self._month = month
+ self._day = day
+ self._hashcode = -1
+ return self
+
+ # Additional constructors
+
+ @classmethod
+ def fromtimestamp(cls, t):
+ "Construct a date from a POSIX timestamp (like time.time())."
+ y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t)
+ return cls(y, m, d)
+
+ @classmethod
+ def today(cls):
+ "Construct a date from time.time()."
+ t = _time.time()
+ return cls.fromtimestamp(t)
+
+ @classmethod
+ def fromordinal(cls, n):
+ """Construct a date from a proleptic Gregorian ordinal.
+
+ January 1 of year 1 is day 1. Only the year, month and day are
+ non-zero in the result.
+ """
+ y, m, d = _ord2ymd(n)
+ return cls(y, m, d)
+
+ @classmethod
+ def fromisoformat(cls, date_string):
+ """Construct a date from the output of date.isoformat()."""
+ if not isinstance(date_string, str):
+ raise TypeError('fromisoformat: argument must be str')
+
+ try:
+ assert len(date_string) == 10
+ return cls(*_parse_isoformat_date(date_string))
+ except Exception:
+ raise ValueError(f'Invalid isoformat string: {date_string!r}')
+
+ @classmethod
+ def fromisocalendar(cls, year, week, day):
+ """Construct a date from the ISO year, week number and weekday.
+
+ This is the inverse of the date.isocalendar() function"""
+ # Year is bounded this way because 9999-12-31 is (9999, 52, 5)
+ if not MINYEAR <= year <= MAXYEAR:
+ raise ValueError(f"Year is out of range: {year}")
+
+ if not 0 < week < 53:
+ out_of_range = True
+
+ if week == 53:
+ # ISO years have 53 weeks in them on years starting with a
+ # Thursday and leap years starting on a Wednesday
+ first_weekday = _ymd2ord(year, 1, 1) % 7
+ if (first_weekday == 4 or (first_weekday == 3 and
+ _is_leap(year))):
+ out_of_range = False
+
+ if out_of_range:
+ raise ValueError(f"Invalid week: {week}")
+
+ if not 0 < day < 8:
+ raise ValueError(f"Invalid weekday: {day} (range is [1, 7])")
+
+ # Now compute the offset from (Y, 1, 1) in days:
+ day_offset = (week - 1) * 7 + (day - 1)
+
+ # Calculate the ordinal day for monday, week 1
+ day_1 = _isoweek1monday(year)
+ ord_day = day_1 + day_offset
+
+ return cls(*_ord2ymd(ord_day))
+
+ # Conversions to string
+
+ def __repr__(self):
+ """Convert to formal string, for repr().
+
+ >>> dt = datetime(2010, 1, 1)
+ >>> repr(dt)
+ 'datetime.datetime(2010, 1, 1, 0, 0)'
+
+ >>> dt = datetime(2010, 1, 1, tzinfo=timezone.utc)
+ >>> repr(dt)
+ 'datetime.datetime(2010, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)'
+ """
+ return "%s.%s(%d, %d, %d)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ self._year,
+ self._month,
+ self._day)
+ # XXX These shouldn't depend on time.localtime(), because that
+ # clips the usable dates to [1970 .. 2038). At least ctime() is
+ # easily done without using strftime() -- that's better too because
+ # strftime("%c", ...) is locale specific.
+
+
+ def ctime(self):
+ "Return ctime() style string."
+ weekday = self.toordinal() % 7 or 7
+ return "%s %s %2d 00:00:00 %04d" % (
+ _DAYNAMES[weekday],
+ _MONTHNAMES[self._month],
+ self._day, self._year)
+
+ def strftime(self, fmt):
+ "Format using strftime()."
+ return _wrap_strftime(self, fmt, self.timetuple())
+
+ def __format__(self, fmt):
+ if not isinstance(fmt, str):
+ raise TypeError("must be str, not %s" % type(fmt).__name__)
+ if len(fmt) != 0:
+ return self.strftime(fmt)
+ return str(self)
+
+ def isoformat(self):
+ """Return the date formatted according to ISO.
+
+ This is 'YYYY-MM-DD'.
+
+ References:
+ - http://www.w3.org/TR/NOTE-datetime
+ - http://www.cl.cam.ac.uk/~mgk25/iso-time.html
+ """
+ return "%04d-%02d-%02d" % (self._year, self._month, self._day)
+
+ __str__ = isoformat
+
+ # Read-only field accessors
+ @property
+ def year(self):
+ """year (1-9999)"""
+ return self._year
+
+ @property
+ def month(self):
+ """month (1-12)"""
+ return self._month
+
+ @property
+ def day(self):
+ """day (1-31)"""
+ return self._day
+
+ # Standard conversions, __eq__, __le__, __lt__, __ge__, __gt__,
+ # __hash__ (and helpers)
+
+ def timetuple(self):
+ "Return local time tuple compatible with time.localtime()."
+ return _build_struct_time(self._year, self._month, self._day,
+ 0, 0, 0, -1)
+
+ def toordinal(self):
+ """Return proleptic Gregorian ordinal for the year, month and day.
+
+ January 1 of year 1 is day 1. Only the year, month and day values
+ contribute to the result.
+ """
+ return _ymd2ord(self._year, self._month, self._day)
+
+ def replace(self, year=None, month=None, day=None):
+ """Return a new date with new values for the specified fields."""
+ if year is None:
+ year = self._year
+ if month is None:
+ month = self._month
+ if day is None:
+ day = self._day
+ return type(self)(year, month, day)
+
+ # Comparisons of date objects with other.
+
+ def __eq__(self, other):
+ if isinstance(other, date):
+ return self._cmp(other) == 0
+ return NotImplemented
+
+ def __le__(self, other):
+ if isinstance(other, date):
+ return self._cmp(other) <= 0
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, date):
+ return self._cmp(other) < 0
+ return NotImplemented
+
+ def __ge__(self, other):
+ if isinstance(other, date):
+ return self._cmp(other) >= 0
+ return NotImplemented
+
+ def __gt__(self, other):
+ if isinstance(other, date):
+ return self._cmp(other) > 0
+ return NotImplemented
+
+ def _cmp(self, other):
+ assert isinstance(other, date)
+ y, m, d = self._year, self._month, self._day
+ y2, m2, d2 = other._year, other._month, other._day
+ return _cmp((y, m, d), (y2, m2, d2))
+
+ def __hash__(self):
+ "Hash."
+ if self._hashcode == -1:
+ self._hashcode = hash(self._getstate())
+ return self._hashcode
+
+ # Computations
+
+ def __add__(self, other):
+ "Add a date to a timedelta."
+ if isinstance(other, timedelta):
+ o = self.toordinal() + other.days
+ if 0 < o <= _MAXORDINAL:
+ return type(self).fromordinal(o)
+ raise OverflowError("result out of range")
+ return NotImplemented
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ """Subtract two dates, or a date and a timedelta."""
+ if isinstance(other, timedelta):
+ return self + timedelta(-other.days)
+ if isinstance(other, date):
+ days1 = self.toordinal()
+ days2 = other.toordinal()
+ return timedelta(days1 - days2)
+ return NotImplemented
+
+ def weekday(self):
+ "Return day of the week, where Monday == 0 ... Sunday == 6."
+ return (self.toordinal() + 6) % 7
+
+ # Day-of-the-week and week-of-the-year, according to ISO
+
+ def isoweekday(self):
+ "Return day of the week, where Monday == 1 ... Sunday == 7."
+ # 1-Jan-0001 is a Monday
+ return self.toordinal() % 7 or 7
+
+ def isocalendar(self):
+ """Return a 3-tuple containing ISO year, week number, and weekday.
+
+ The first ISO week of the year is the (Mon-Sun) week
+ containing the year's first Thursday; everything else derives
+ from that.
+
+ The first week is 1; Monday is 1 ... Sunday is 7.
+
+ ISO calendar algorithm taken from
+ http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
+ (used with permission)
+ """
+ year = self._year
+ week1monday = _isoweek1monday(year)
+ today = _ymd2ord(self._year, self._month, self._day)
+ # Internally, week and day have origin 0
+ week, day = divmod(today - week1monday, 7)
+ if week < 0:
+ year -= 1
+ week1monday = _isoweek1monday(year)
+ week, day = divmod(today - week1monday, 7)
+ elif week >= 52:
+ if today >= _isoweek1monday(year+1):
+ year += 1
+ week = 0
+ return year, week+1, day+1
+
+ # Pickle support.
+
+ def _getstate(self):
+ yhi, ylo = divmod(self._year, 256)
+ return bytes([yhi, ylo, self._month, self._day]),
+
+ def __setstate(self, string):
+ yhi, ylo, self._month, self._day = string
+ self._year = yhi * 256 + ylo
+
+ def __reduce__(self):
+ return (self.__class__, self._getstate())
+
+_date_class = date # so functions w/ args named "date" can get at the class
+
+date.min = date(1, 1, 1)
+date.max = date(9999, 12, 31)
+date.resolution = timedelta(days=1)
+
+
+class tzinfo:
+ """Abstract base class for time zone info classes.
+
+ Subclasses must override the name(), utcoffset() and dst() methods.
+ """
+ __slots__ = ()
+
+ def tzname(self, dt):
+ "datetime -> string name of time zone."
+ raise NotImplementedError("tzinfo subclass must override tzname()")
+
+ def utcoffset(self, dt):
+ "datetime -> timedelta, positive for east of UTC, negative for west of UTC"
+ raise NotImplementedError("tzinfo subclass must override utcoffset()")
+
+ def dst(self, dt):
+ """datetime -> DST offset as timedelta, positive for east of UTC.
+
+ Return 0 if DST not in effect. utcoffset() must include the DST
+ offset.
+ """
+ raise NotImplementedError("tzinfo subclass must override dst()")
+
+ def fromutc(self, dt):
+ "datetime in UTC -> datetime in local time."
+
+ if not isinstance(dt, datetime):
+ raise TypeError("fromutc() requires a datetime argument")
+ if dt.tzinfo is not self:
+ raise ValueError("dt.tzinfo is not self")
+
+ dtoff = dt.utcoffset()
+ if dtoff is None:
+ raise ValueError("fromutc() requires a non-None utcoffset() "
+ "result")
+
+ # See the long comment block at the end of this file for an
+ # explanation of this algorithm.
+ dtdst = dt.dst()
+ if dtdst is None:
+ raise ValueError("fromutc() requires a non-None dst() result")
+ delta = dtoff - dtdst
+ if delta:
+ dt += delta
+ dtdst = dt.dst()
+ if dtdst is None:
+ raise ValueError("fromutc(): dt.dst gave inconsistent "
+ "results; cannot convert")
+ return dt + dtdst
+
+ # Pickle support.
+
+ def __reduce__(self):
+ getinitargs = getattr(self, "__getinitargs__", None)
+ if getinitargs:
+ args = getinitargs()
+ else:
+ args = ()
+ getstate = getattr(self, "__getstate__", None)
+ if getstate:
+ state = getstate()
+ else:
+ state = getattr(self, "__dict__", None) or None
+ if state is None:
+ return (self.__class__, args)
+ else:
+ return (self.__class__, args, state)
+
+_tzinfo_class = tzinfo
+
+class time:
+ """Time with time zone.
+
+ Constructors:
+
+ __new__()
+
+ Operators:
+
+ __repr__, __str__
+ __eq__, __le__, __lt__, __ge__, __gt__, __hash__
+
+ Methods:
+
+ strftime()
+ isoformat()
+ utcoffset()
+ tzname()
+ dst()
+
+ Properties (readonly):
+ hour, minute, second, microsecond, tzinfo, fold
+ """
+ __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_fold'
+
+ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0):
+ """Constructor.
+
+ Arguments:
+
+ hour, minute (required)
+ second, microsecond (default to zero)
+ tzinfo (default to None)
+ fold (keyword only, default to zero)
+ """
+ if (isinstance(hour, (bytes, str)) and len(hour) == 6 and
+ ord(hour[0:1])&0x7F < 24):
+ # Pickle support
+ if isinstance(hour, str):
+ try:
+ hour = hour.encode('latin1')
+ except UnicodeEncodeError:
+ # More informative error message.
+ raise ValueError(
+ "Failed to encode latin1 string when unpickling "
+ "a time object. "
+ "pickle.load(data, encoding='latin1') is assumed.")
+ self = object.__new__(cls)
+ self.__setstate(hour, minute or None)
+ self._hashcode = -1
+ return self
+ hour, minute, second, microsecond, fold = _check_time_fields(
+ hour, minute, second, microsecond, fold)
+ _check_tzinfo_arg(tzinfo)
+ self = object.__new__(cls)
+ self._hour = hour
+ self._minute = minute
+ self._second = second
+ self._microsecond = microsecond
+ self._tzinfo = tzinfo
+ self._hashcode = -1
+ self._fold = fold
+ return self
+
+ # Read-only field accessors
+ @property
+ def hour(self):
+ """hour (0-23)"""
+ return self._hour
+
+ @property
+ def minute(self):
+ """minute (0-59)"""
+ return self._minute
+
+ @property
+ def second(self):
+ """second (0-59)"""
+ return self._second
+
+ @property
+ def microsecond(self):
+ """microsecond (0-999999)"""
+ return self._microsecond
+
+ @property
+ def tzinfo(self):
+ """timezone info object"""
+ return self._tzinfo
+
+ @property
+ def fold(self):
+ return self._fold
+
+ # Standard conversions, __hash__ (and helpers)
+
+ # Comparisons of time objects with other.
+
+ def __eq__(self, other):
+ if isinstance(other, time):
+ return self._cmp(other, allow_mixed=True) == 0
+ else:
+ return NotImplemented
+
+ def __le__(self, other):
+ if isinstance(other, time):
+ return self._cmp(other) <= 0
+ else:
+ return NotImplemented
+
+ def __lt__(self, other):
+ if isinstance(other, time):
+ return self._cmp(other) < 0
+ else:
+ return NotImplemented
+
+ def __ge__(self, other):
+ if isinstance(other, time):
+ return self._cmp(other) >= 0
+ else:
+ return NotImplemented
+
+ def __gt__(self, other):
+ if isinstance(other, time):
+ return self._cmp(other) > 0
+ else:
+ return NotImplemented
+
+ def _cmp(self, other, allow_mixed=False):
+ assert isinstance(other, time)
+ mytz = self._tzinfo
+ ottz = other._tzinfo
+ myoff = otoff = None
+
+ if mytz is ottz:
+ base_compare = True
+ else:
+ myoff = self.utcoffset()
+ otoff = other.utcoffset()
+ base_compare = myoff == otoff
+
+ if base_compare:
+ return _cmp((self._hour, self._minute, self._second,
+ self._microsecond),
+ (other._hour, other._minute, other._second,
+ other._microsecond))
+ if myoff is None or otoff is None:
+ if allow_mixed:
+ return 2 # arbitrary non-zero value
+ else:
+ raise TypeError("cannot compare naive and aware times")
+ myhhmm = self._hour * 60 + self._minute - myoff//timedelta(minutes=1)
+ othhmm = other._hour * 60 + other._minute - otoff//timedelta(minutes=1)
+ return _cmp((myhhmm, self._second, self._microsecond),
+ (othhmm, other._second, other._microsecond))
+
+ def __hash__(self):
+ """Hash."""
+ if self._hashcode == -1:
+ if self.fold:
+ t = self.replace(fold=0)
+ else:
+ t = self
+ tzoff = t.utcoffset()
+ if not tzoff: # zero or None
+ self._hashcode = hash(t._getstate()[0])
+ else:
+ h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff,
+ timedelta(hours=1))
+ assert not m % timedelta(minutes=1), "whole minute"
+ m //= timedelta(minutes=1)
+ if 0 <= h < 24:
+ self._hashcode = hash(time(h, m, self.second, self.microsecond))
+ else:
+ self._hashcode = hash((h, m, self.second, self.microsecond))
+ return self._hashcode
+
+ # Conversion to string
+
+ def _tzstr(self):
+ """Return formatted timezone offset (+xx:xx) or an empty string."""
+ off = self.utcoffset()
+ return _format_offset(off)
+
+ def __repr__(self):
+ """Convert to formal string, for repr()."""
+ if self._microsecond != 0:
+ s = ", %d, %d" % (self._second, self._microsecond)
+ elif self._second != 0:
+ s = ", %d" % self._second
+ else:
+ s = ""
+ s= "%s.%s(%d, %d%s)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ self._hour, self._minute, s)
+ if self._tzinfo is not None:
+ assert s[-1:] == ")"
+ s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
+ if self._fold:
+ assert s[-1:] == ")"
+ s = s[:-1] + ", fold=1)"
+ return s
+
+ def isoformat(self, timespec='auto'):
+ """Return the time formatted according to ISO.
+
+ The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional
+ part is omitted if self.microsecond == 0.
+
+ The optional argument timespec specifies the number of additional
+ terms of the time to include.
+ """
+ s = _format_time(self._hour, self._minute, self._second,
+ self._microsecond, timespec)
+ tz = self._tzstr()
+ if tz:
+ s += tz
+ return s
+
+ __str__ = isoformat
+
+ @classmethod
+ def fromisoformat(cls, time_string):
+ """Construct a time from the output of isoformat()."""
+ if not isinstance(time_string, str):
+ raise TypeError('fromisoformat: argument must be str')
+
+ try:
+ return cls(*_parse_isoformat_time(time_string))
+ except Exception:
+ raise ValueError(f'Invalid isoformat string: {time_string!r}')
+
+
+ def strftime(self, fmt):
+ """Format using strftime(). The date part of the timestamp passed
+ to underlying strftime should not be used.
+ """
+ # The year must be >= 1000 else Python's strftime implementation
+ # can raise a bogus exception.
+ timetuple = (1900, 1, 1,
+ self._hour, self._minute, self._second,
+ 0, 1, -1)
+ return _wrap_strftime(self, fmt, timetuple)
+
+ def __format__(self, fmt):
+ if not isinstance(fmt, str):
+ raise TypeError("must be str, not %s" % type(fmt).__name__)
+ if len(fmt) != 0:
+ return self.strftime(fmt)
+ return str(self)
+
+ # Timezone functions
+
+ def utcoffset(self):
+ """Return the timezone offset as timedelta, positive east of UTC
+ (negative west of UTC)."""
+ if self._tzinfo is None:
+ return None
+ offset = self._tzinfo.utcoffset(None)
+ _check_utc_offset("utcoffset", offset)
+ return offset
+
+ def tzname(self):
+ """Return the timezone name.
+
+ Note that the name is 100% informational -- there's no requirement that
+ it mean anything in particular. For example, "GMT", "UTC", "-500",
+ "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies.
+ """
+ if self._tzinfo is None:
+ return None
+ name = self._tzinfo.tzname(None)
+ _check_tzname(name)
+ return name
+
+ def dst(self):
+ """Return 0 if DST is not in effect, or the DST offset (as timedelta
+ positive eastward) if DST is in effect.
+
+ This is purely informational; the DST offset has already been added to
+ the UTC offset returned by utcoffset() if applicable, so there's no
+ need to consult dst() unless you're interested in displaying the DST
+ info.
+ """
+ if self._tzinfo is None:
+ return None
+ offset = self._tzinfo.dst(None)
+ _check_utc_offset("dst", offset)
+ return offset
+
+ def replace(self, hour=None, minute=None, second=None, microsecond=None,
+ tzinfo=True, *, fold=None):
+ """Return a new time with new values for the specified fields."""
+ if hour is None:
+ hour = self.hour
+ if minute is None:
+ minute = self.minute
+ if second is None:
+ second = self.second
+ if microsecond is None:
+ microsecond = self.microsecond
+ if tzinfo is True:
+ tzinfo = self.tzinfo
+ if fold is None:
+ fold = self._fold
+ return type(self)(hour, minute, second, microsecond, tzinfo, fold=fold)
+
+ # Pickle support.
+
+ def _getstate(self, protocol=3):
+ us2, us3 = divmod(self._microsecond, 256)
+ us1, us2 = divmod(us2, 256)
+ h = self._hour
+ if self._fold and protocol > 3:
+ h += 128
+ basestate = bytes([h, self._minute, self._second,
+ us1, us2, us3])
+ if self._tzinfo is None:
+ return (basestate,)
+ else:
+ return (basestate, self._tzinfo)
+
+ def __setstate(self, string, tzinfo):
+ if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
+ raise TypeError("bad tzinfo state arg")
+ h, self._minute, self._second, us1, us2, us3 = string
+ if h > 127:
+ self._fold = 1
+ self._hour = h - 128
+ else:
+ self._fold = 0
+ self._hour = h
+ self._microsecond = (((us1 << 8) | us2) << 8) | us3
+ self._tzinfo = tzinfo
+
+ def __reduce_ex__(self, protocol):
+ return (time, self._getstate(protocol))
+
+ def __reduce__(self):
+ return self.__reduce_ex__(2)
+
+_time_class = time # so functions w/ args named "time" can get at the class
+
+time.min = time(0, 0, 0)
+time.max = time(23, 59, 59, 999999)
+time.resolution = timedelta(microseconds=1)
+
+class datetime(date):
+ """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]])
+
+ The year, month and day arguments are required. tzinfo may be None, or an
+ instance of a tzinfo subclass. The remaining arguments may be ints.
+ """
+ __slots__ = date.__slots__ + time.__slots__
+
+ def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
+ microsecond=0, tzinfo=None, *, fold=0):
+ if (isinstance(year, (bytes, str)) and len(year) == 10 and
+ 1 <= ord(year[2:3])&0x7F <= 12):
+ # Pickle support
+ if isinstance(year, str):
+ try:
+ year = bytes(year, 'latin1')
+ except UnicodeEncodeError:
+ # More informative error message.
+ raise ValueError(
+ "Failed to encode latin1 string when unpickling "
+ "a datetime object. "
+ "pickle.load(data, encoding='latin1') is assumed.")
+ self = object.__new__(cls)
+ self.__setstate(year, month)
+ self._hashcode = -1
+ return self
+ year, month, day = _check_date_fields(year, month, day)
+ hour, minute, second, microsecond, fold = _check_time_fields(
+ hour, minute, second, microsecond, fold)
+ _check_tzinfo_arg(tzinfo)
+ self = object.__new__(cls)
+ self._year = year
+ self._month = month
+ self._day = day
+ self._hour = hour
+ self._minute = minute
+ self._second = second
+ self._microsecond = microsecond
+ self._tzinfo = tzinfo
+ self._hashcode = -1
+ self._fold = fold
+ return self
+
+ # Read-only field accessors
+ @property
+ def hour(self):
+ """hour (0-23)"""
+ return self._hour
+
+ @property
+ def minute(self):
+ """minute (0-59)"""
+ return self._minute
+
+ @property
+ def second(self):
+ """second (0-59)"""
+ return self._second
+
+ @property
+ def microsecond(self):
+ """microsecond (0-999999)"""
+ return self._microsecond
+
+ @property
+ def tzinfo(self):
+ """timezone info object"""
+ return self._tzinfo
+
+ @property
+ def fold(self):
+ return self._fold
+
+ @classmethod
+ def _fromtimestamp(cls, t, utc, tz):
+ """Construct a datetime from a POSIX timestamp (like time.time()).
+
+ A timezone info object may be passed in as well.
+ """
+ frac, t = _math.modf(t)
+ us = round(frac * 1e6)
+ if us >= 1000000:
+ t += 1
+ us -= 1000000
+ elif us < 0:
+ t -= 1
+ us += 1000000
+
+ converter = _time.gmtime if utc else _time.localtime
+ y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
+ ss = min(ss, 59) # clamp out leap seconds if the platform has them
+ result = cls(y, m, d, hh, mm, ss, us, tz)
+ if tz is None:
+ # As of version 2015f max fold in IANA database is
+ # 23 hours at 1969-09-30 13:00:00 in Kwajalein.
+ # Let's probe 24 hours in the past to detect a transition:
+ max_fold_seconds = 24 * 3600
+
+ # On Windows localtime_s throws an OSError for negative values,
+ # thus we can't perform fold detection for values of time less
+ # than the max time fold. See comments in _datetimemodule's
+ # version of this method for more details.
+ if t < max_fold_seconds and sys.platform.startswith("win"):
+ return result
+
+ y, m, d, hh, mm, ss = converter(t - max_fold_seconds)[:6]
+ probe1 = cls(y, m, d, hh, mm, ss, us, tz)
+ trans = result - probe1 - timedelta(0, max_fold_seconds)
+ if trans.days < 0:
+ y, m, d, hh, mm, ss = converter(t + trans // timedelta(0, 1))[:6]
+ probe2 = cls(y, m, d, hh, mm, ss, us, tz)
+ if probe2 == result:
+ result._fold = 1
+ else:
+ result = tz.fromutc(result)
+ return result
+
+ @classmethod
+ def fromtimestamp(cls, t, tz=None):
+ """Construct a datetime from a POSIX timestamp (like time.time()).
+
+ A timezone info object may be passed in as well.
+ """
+ _check_tzinfo_arg(tz)
+
+ return cls._fromtimestamp(t, tz is not None, tz)
+
+ @classmethod
+ def utcfromtimestamp(cls, t):
+ """Construct a naive UTC datetime from a POSIX timestamp."""
+ return cls._fromtimestamp(t, True, None)
+
+ @classmethod
+ def now(cls, tz=None):
+ "Construct a datetime from time.time() and optional time zone info."
+ t = _time.time()
+ return cls.fromtimestamp(t, tz)
+
+ @classmethod
+ def utcnow(cls):
+ "Construct a UTC datetime from time.time()."
+ t = _time.time()
+ return cls.utcfromtimestamp(t)
+
+ @classmethod
+ def combine(cls, date, time, tzinfo=True):
+ "Construct a datetime from a given date and a given time."
+ if not isinstance(date, _date_class):
+ raise TypeError("date argument must be a date instance")
+ if not isinstance(time, _time_class):
+ raise TypeError("time argument must be a time instance")
+ if tzinfo is True:
+ tzinfo = time.tzinfo
+ return cls(date.year, date.month, date.day,
+ time.hour, time.minute, time.second, time.microsecond,
+ tzinfo, fold=time.fold)
+
+ @classmethod
+ def fromisoformat(cls, date_string):
+ """Construct a datetime from the output of datetime.isoformat()."""
+ if not isinstance(date_string, str):
+ raise TypeError('fromisoformat: argument must be str')
+
+ # Split this at the separator
+ dstr = date_string[0:10]
+ tstr = date_string[11:]
+
+ try:
+ date_components = _parse_isoformat_date(dstr)
+ except ValueError:
+ raise ValueError(f'Invalid isoformat string: {date_string!r}')
+
+ if tstr:
+ try:
+ time_components = _parse_isoformat_time(tstr)
+ except ValueError:
+ raise ValueError(f'Invalid isoformat string: {date_string!r}')
+ else:
+ time_components = [0, 0, 0, 0, None]
+
+ return cls(*(date_components + time_components))
+
+ def timetuple(self):
+ "Return local time tuple compatible with time.localtime()."
+ dst = self.dst()
+ if dst is None:
+ dst = -1
+ elif dst:
+ dst = 1
+ else:
+ dst = 0
+ return _build_struct_time(self.year, self.month, self.day,
+ self.hour, self.minute, self.second,
+ dst)
+
+ def _mktime(self):
+ """Return integer POSIX timestamp."""
+ epoch = datetime(1970, 1, 1)
+ max_fold_seconds = 24 * 3600
+ t = (self - epoch) // timedelta(0, 1)
+ def local(u):
+ y, m, d, hh, mm, ss = _time.localtime(u)[:6]
+ return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1)
+
+ # Our goal is to solve t = local(u) for u.
+ a = local(t) - t
+ u1 = t - a
+ t1 = local(u1)
+ if t1 == t:
+ # We found one solution, but it may not be the one we need.
+ # Look for an earlier solution (if `fold` is 0), or a
+ # later one (if `fold` is 1).
+ u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold]
+ b = local(u2) - u2
+ if a == b:
+ return u1
+ else:
+ b = t1 - u1
+ assert a != b
+ u2 = t - b
+ t2 = local(u2)
+ if t2 == t:
+ return u2
+ if t1 == t:
+ return u1
+ # We have found both offsets a and b, but neither t - a nor t - b is
+ # a solution. This means t is in the gap.
+ return (max, min)[self.fold](u1, u2)
+
+
+ def timestamp(self):
+ "Return POSIX timestamp as float"
+ if self._tzinfo is None:
+ s = self._mktime()
+ return s + self.microsecond / 1e6
+ else:
+ return (self - _EPOCH).total_seconds()
+
+ def utctimetuple(self):
+ "Return UTC time tuple compatible with time.gmtime()."
+ offset = self.utcoffset()
+ if offset:
+ self -= offset
+ y, m, d = self.year, self.month, self.day
+ hh, mm, ss = self.hour, self.minute, self.second
+ return _build_struct_time(y, m, d, hh, mm, ss, 0)
+
+ def date(self):
+ "Return the date part."
+ return date(self._year, self._month, self._day)
+
+ def time(self):
+ "Return the time part, with tzinfo None."
+ return time(self.hour, self.minute, self.second, self.microsecond, fold=self.fold)
+
+ def timetz(self):
+ "Return the time part, with same tzinfo."
+ return time(self.hour, self.minute, self.second, self.microsecond,
+ self._tzinfo, fold=self.fold)
+
+ def replace(self, year=None, month=None, day=None, hour=None,
+ minute=None, second=None, microsecond=None, tzinfo=True,
+ *, fold=None):
+ """Return a new datetime with new values for the specified fields."""
+ if year is None:
+ year = self.year
+ if month is None:
+ month = self.month
+ if day is None:
+ day = self.day
+ if hour is None:
+ hour = self.hour
+ if minute is None:
+ minute = self.minute
+ if second is None:
+ second = self.second
+ if microsecond is None:
+ microsecond = self.microsecond
+ if tzinfo is True:
+ tzinfo = self.tzinfo
+ if fold is None:
+ fold = self.fold
+ return type(self)(year, month, day, hour, minute, second,
+ microsecond, tzinfo, fold=fold)
+
+ def _local_timezone(self):
+ if self.tzinfo is None:
+ ts = self._mktime()
+ else:
+ ts = (self - _EPOCH) // timedelta(seconds=1)
+ localtm = _time.localtime(ts)
+ local = datetime(*localtm[:6])
+ # Extract TZ data
+ gmtoff = localtm.tm_gmtoff
+ zone = localtm.tm_zone
+ return timezone(timedelta(seconds=gmtoff), zone)
+
+ def astimezone(self, tz=None):
+ if tz is None:
+ tz = self._local_timezone()
+ elif not isinstance(tz, tzinfo):
+ raise TypeError("tz argument must be an instance of tzinfo")
+
+ mytz = self.tzinfo
+ if mytz is None:
+ mytz = self._local_timezone()
+ myoffset = mytz.utcoffset(self)
+ else:
+ myoffset = mytz.utcoffset(self)
+ if myoffset is None:
+ mytz = self.replace(tzinfo=None)._local_timezone()
+ myoffset = mytz.utcoffset(self)
+
+ if tz is mytz:
+ return self
+
+ # Convert self to UTC, and attach the new time zone object.
+ utc = (self - myoffset).replace(tzinfo=tz)
+
+ # Convert from UTC to tz's local time.
+ return tz.fromutc(utc)
+
+ # Ways to produce a string.
+
+ def ctime(self):
+ "Return ctime() style string."
+ weekday = self.toordinal() % 7 or 7
+ return "%s %s %2d %02d:%02d:%02d %04d" % (
+ _DAYNAMES[weekday],
+ _MONTHNAMES[self._month],
+ self._day,
+ self._hour, self._minute, self._second,
+ self._year)
+
+ def isoformat(self, sep='T', timespec='auto'):
+ """Return the time formatted according to ISO.
+
+ The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'.
+ By default, the fractional part is omitted if self.microsecond == 0.
+
+ If self.tzinfo is not None, the UTC offset is also attached, giving
+ giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'.
+
+ Optional argument sep specifies the separator between date and
+ time, default 'T'.
+
+ The optional argument timespec specifies the number of additional
+ terms of the time to include.
+ """
+ s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
+ _format_time(self._hour, self._minute, self._second,
+ self._microsecond, timespec))
+
+ off = self.utcoffset()
+ tz = _format_offset(off)
+ if tz:
+ s += tz
+
+ return s
+
+ def __repr__(self):
+ """Convert to formal string, for repr()."""
+ L = [self._year, self._month, self._day, # These are never zero
+ self._hour, self._minute, self._second, self._microsecond]
+ if L[-1] == 0:
+ del L[-1]
+ if L[-1] == 0:
+ del L[-1]
+ s = "%s.%s(%s)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ ", ".join(map(str, L)))
+ if self._tzinfo is not None:
+ assert s[-1:] == ")"
+ s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
+ if self._fold:
+ assert s[-1:] == ")"
+ s = s[:-1] + ", fold=1)"
+ return s
+
+ def __str__(self):
+ "Convert to string, for str()."
+ return self.isoformat(sep=' ')
+
+ @classmethod
+ def strptime(cls, date_string, format):
+ 'string, format -> new datetime parsed from a string (like time.strptime()).'
+ import _strptime
+ return _strptime._strptime_datetime(cls, date_string, format)
+
+ def utcoffset(self):
+ """Return the timezone offset as timedelta positive east of UTC (negative west of
+ UTC)."""
+ if self._tzinfo is None:
+ return None
+ offset = self._tzinfo.utcoffset(self)
+ _check_utc_offset("utcoffset", offset)
+ return offset
+
+ def tzname(self):
+ """Return the timezone name.
+
+ Note that the name is 100% informational -- there's no requirement that
+ it mean anything in particular. For example, "GMT", "UTC", "-500",
+ "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies.
+ """
+ if self._tzinfo is None:
+ return None
+ name = self._tzinfo.tzname(self)
+ _check_tzname(name)
+ return name
+
+ def dst(self):
+ """Return 0 if DST is not in effect, or the DST offset (as timedelta
+ positive eastward) if DST is in effect.
+
+ This is purely informational; the DST offset has already been added to
+ the UTC offset returned by utcoffset() if applicable, so there's no
+ need to consult dst() unless you're interested in displaying the DST
+ info.
+ """
+ if self._tzinfo is None:
+ return None
+ offset = self._tzinfo.dst(self)
+ _check_utc_offset("dst", offset)
+ return offset
+
+ # Comparisons of datetime objects with other.
+
+ def __eq__(self, other):
+ if isinstance(other, datetime):
+ return self._cmp(other, allow_mixed=True) == 0
+ elif not isinstance(other, date):
+ return NotImplemented
+ else:
+ return False
+
+ def __le__(self, other):
+ if isinstance(other, datetime):
+ return self._cmp(other) <= 0
+ elif not isinstance(other, date):
+ return NotImplemented
+ else:
+ _cmperror(self, other)
+
+ def __lt__(self, other):
+ if isinstance(other, datetime):
+ return self._cmp(other) < 0
+ elif not isinstance(other, date):
+ return NotImplemented
+ else:
+ _cmperror(self, other)
+
+ def __ge__(self, other):
+ if isinstance(other, datetime):
+ return self._cmp(other) >= 0
+ elif not isinstance(other, date):
+ return NotImplemented
+ else:
+ _cmperror(self, other)
+
+ def __gt__(self, other):
+ if isinstance(other, datetime):
+ return self._cmp(other) > 0
+ elif not isinstance(other, date):
+ return NotImplemented
+ else:
+ _cmperror(self, other)
+
+ def _cmp(self, other, allow_mixed=False):
+ assert isinstance(other, datetime)
+ mytz = self._tzinfo
+ ottz = other._tzinfo
+ myoff = otoff = None
+
+ if mytz is ottz:
+ base_compare = True
+ else:
+ myoff = self.utcoffset()
+ otoff = other.utcoffset()
+ # Assume that allow_mixed means that we are called from __eq__
+ if allow_mixed:
+ if myoff != self.replace(fold=not self.fold).utcoffset():
+ return 2
+ if otoff != other.replace(fold=not other.fold).utcoffset():
+ return 2
+ base_compare = myoff == otoff
+
+ if base_compare:
+ return _cmp((self._year, self._month, self._day,
+ self._hour, self._minute, self._second,
+ self._microsecond),
+ (other._year, other._month, other._day,
+ other._hour, other._minute, other._second,
+ other._microsecond))
+ if myoff is None or otoff is None:
+ if allow_mixed:
+ return 2 # arbitrary non-zero value
+ else:
+ raise TypeError("cannot compare naive and aware datetimes")
+ # XXX What follows could be done more efficiently...
+ diff = self - other # this will take offsets into account
+ if diff.days < 0:
+ return -1
+ return diff and 1 or 0
+
+ def __add__(self, other):
+ "Add a datetime and a timedelta."
+ if not isinstance(other, timedelta):
+ return NotImplemented
+ delta = timedelta(self.toordinal(),
+ hours=self._hour,
+ minutes=self._minute,
+ seconds=self._second,
+ microseconds=self._microsecond)
+ delta += other
+ hour, rem = divmod(delta.seconds, 3600)
+ minute, second = divmod(rem, 60)
+ if 0 < delta.days <= _MAXORDINAL:
+ return type(self).combine(date.fromordinal(delta.days),
+ time(hour, minute, second,
+ delta.microseconds,
+ tzinfo=self._tzinfo))
+ raise OverflowError("result out of range")
+
+ __radd__ = __add__
+
+ def __sub__(self, other):
+ "Subtract two datetimes, or a datetime and a timedelta."
+ if not isinstance(other, datetime):
+ if isinstance(other, timedelta):
+ return self + -other
+ return NotImplemented
+
+ days1 = self.toordinal()
+ days2 = other.toordinal()
+ secs1 = self._second + self._minute * 60 + self._hour * 3600
+ secs2 = other._second + other._minute * 60 + other._hour * 3600
+ base = timedelta(days1 - days2,
+ secs1 - secs2,
+ self._microsecond - other._microsecond)
+ if self._tzinfo is other._tzinfo:
+ return base
+ myoff = self.utcoffset()
+ otoff = other.utcoffset()
+ if myoff == otoff:
+ return base
+ if myoff is None or otoff is None:
+ raise TypeError("cannot mix naive and timezone-aware time")
+ return base + otoff - myoff
+
+ def __hash__(self):
+ if self._hashcode == -1:
+ if self.fold:
+ t = self.replace(fold=0)
+ else:
+ t = self
+ tzoff = t.utcoffset()
+ if tzoff is None:
+ self._hashcode = hash(t._getstate()[0])
+ else:
+ days = _ymd2ord(self.year, self.month, self.day)
+ seconds = self.hour * 3600 + self.minute * 60 + self.second
+ self._hashcode = hash(timedelta(days, seconds, self.microsecond) - tzoff)
+ return self._hashcode
+
+ # Pickle support.
+
+ def _getstate(self, protocol=3):
+ yhi, ylo = divmod(self._year, 256)
+ us2, us3 = divmod(self._microsecond, 256)
+ us1, us2 = divmod(us2, 256)
+ m = self._month
+ if self._fold and protocol > 3:
+ m += 128
+ basestate = bytes([yhi, ylo, m, self._day,
+ self._hour, self._minute, self._second,
+ us1, us2, us3])
+ if self._tzinfo is None:
+ return (basestate,)
+ else:
+ return (basestate, self._tzinfo)
+
+ def __setstate(self, string, tzinfo):
+ if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
+ raise TypeError("bad tzinfo state arg")
+ (yhi, ylo, m, self._day, self._hour,
+ self._minute, self._second, us1, us2, us3) = string
+ if m > 127:
+ self._fold = 1
+ self._month = m - 128
+ else:
+ self._fold = 0
+ self._month = m
+ self._year = yhi * 256 + ylo
+ self._microsecond = (((us1 << 8) | us2) << 8) | us3
+ self._tzinfo = tzinfo
+
+ def __reduce_ex__(self, protocol):
+ return (self.__class__, self._getstate(protocol))
+
+ def __reduce__(self):
+ return self.__reduce_ex__(2)
+
+
+datetime.min = datetime(1, 1, 1)
+datetime.max = datetime(9999, 12, 31, 23, 59, 59, 999999)
+datetime.resolution = timedelta(microseconds=1)
+
+
+def _isoweek1monday(year):
+ # Helper to calculate the day number of the Monday starting week 1
+ # XXX This could be done more efficiently
+ THURSDAY = 3
+ firstday = _ymd2ord(year, 1, 1)
+ firstweekday = (firstday + 6) % 7 # See weekday() above
+ week1monday = firstday - firstweekday
+ if firstweekday > THURSDAY:
+ week1monday += 7
+ return week1monday
+
+
+class timezone(tzinfo):
+ __slots__ = '_offset', '_name'
+
+ # Sentinel value to disallow None
+ _Omitted = object()
+ def __new__(cls, offset, name=_Omitted):
+ if not isinstance(offset, timedelta):
+ raise TypeError("offset must be a timedelta")
+ if name is cls._Omitted:
+ if not offset:
+ return cls.utc
+ name = None
+ elif not isinstance(name, str):
+ raise TypeError("name must be a string")
+ if not cls._minoffset <= offset <= cls._maxoffset:
+ raise ValueError("offset must be a timedelta "
+ "strictly between -timedelta(hours=24) and "
+ "timedelta(hours=24).")
+ return cls._create(offset, name)
+
+ @classmethod
+ def _create(cls, offset, name=None):
+ self = tzinfo.__new__(cls)
+ self._offset = offset
+ self._name = name
+ return self
+
+ def __getinitargs__(self):
+ """pickle support"""
+ if self._name is None:
+ return (self._offset,)
+ return (self._offset, self._name)
+
+ def __eq__(self, other):
+ if isinstance(other, timezone):
+ return self._offset == other._offset
+ return NotImplemented
+
+ def __hash__(self):
+ return hash(self._offset)
+
+ def __repr__(self):
+ """Convert to formal string, for repr().
+
+ >>> tz = timezone.utc
+ >>> repr(tz)
+ 'datetime.timezone.utc'
+ >>> tz = timezone(timedelta(hours=-5), 'EST')
+ >>> repr(tz)
+ "datetime.timezone(datetime.timedelta(-1, 68400), 'EST')"
+ """
+ if self is self.utc:
+ return 'datetime.timezone.utc'
+ if self._name is None:
+ return "%s.%s(%r)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ self._offset)
+ return "%s.%s(%r, %r)" % (self.__class__.__module__,
+ self.__class__.__qualname__,
+ self._offset, self._name)
+
+ def __str__(self):
+ return self.tzname(None)
+
+ def utcoffset(self, dt):
+ if isinstance(dt, datetime) or dt is None:
+ return self._offset
+ raise TypeError("utcoffset() argument must be a datetime instance"
+ " or None")
+
+ def tzname(self, dt):
+ if isinstance(dt, datetime) or dt is None:
+ if self._name is None:
+ return self._name_from_offset(self._offset)
+ return self._name
+ raise TypeError("tzname() argument must be a datetime instance"
+ " or None")
+
+ def dst(self, dt):
+ if isinstance(dt, datetime) or dt is None:
+ return None
+ raise TypeError("dst() argument must be a datetime instance"
+ " or None")
+
+ def fromutc(self, dt):
+ if isinstance(dt, datetime):
+ if dt.tzinfo is not self:
+ raise ValueError("fromutc: dt.tzinfo "
+ "is not self")
+ return dt + self._offset
+ raise TypeError("fromutc() argument must be a datetime instance"
+ " or None")
+
+ _maxoffset = timedelta(hours=24, microseconds=-1)
+ _minoffset = -_maxoffset
+
+ @staticmethod
+ def _name_from_offset(delta):
+ if not delta:
+ return 'UTC'
+ if delta < timedelta(0):
+ sign = '-'
+ delta = -delta
+ else:
+ sign = '+'
+ hours, rest = divmod(delta, timedelta(hours=1))
+ minutes, rest = divmod(rest, timedelta(minutes=1))
+ seconds = rest.seconds
+ microseconds = rest.microseconds
+ if microseconds:
+ return (f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}'
+ f'.{microseconds:06d}')
+ if seconds:
+ return f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}'
+ return f'UTC{sign}{hours:02d}:{minutes:02d}'
+
+timezone.utc = timezone._create(timedelta(0))
+# bpo-37642: These attributes are rounded to the nearest minute for backwards
+# compatibility, even though the constructor will accept a wider range of
+# values. This may change in the future.
+timezone.min = timezone._create(-timedelta(hours=23, minutes=59))
+timezone.max = timezone._create(timedelta(hours=23, minutes=59))
+_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
+
+# Some time zone algebra. For a datetime x, let
+# x.n = x stripped of its timezone -- its naive time.
+# x.o = x.utcoffset(), and assuming that doesn't raise an exception or
+# return None
+# x.d = x.dst(), and assuming that doesn't raise an exception or
+# return None
+# x.s = x's standard offset, x.o - x.d
+#
+# Now some derived rules, where k is a duration (timedelta).
+#
+# 1. x.o = x.s + x.d
+# This follows from the definition of x.s.
+#
+# 2. If x and y have the same tzinfo member, x.s = y.s.
+# This is actually a requirement, an assumption we need to make about
+# sane tzinfo classes.
+#
+# 3. The naive UTC time corresponding to x is x.n - x.o.
+# This is again a requirement for a sane tzinfo class.
+#
+# 4. (x+k).s = x.s
+# This follows from #2, and that datimetimetz+timedelta preserves tzinfo.
+#
+# 5. (x+k).n = x.n + k
+# Again follows from how arithmetic is defined.
+#
+# Now we can explain tz.fromutc(x). Let's assume it's an interesting case
+# (meaning that the various tzinfo methods exist, and don't blow up or return
+# None when called).
+#
+# The function wants to return a datetime y with timezone tz, equivalent to x.
+# x is already in UTC.
+#
+# By #3, we want
+#
+# y.n - y.o = x.n [1]
+#
+# The algorithm starts by attaching tz to x.n, and calling that y. So
+# x.n = y.n at the start. Then it wants to add a duration k to y, so that [1]
+# becomes true; in effect, we want to solve [2] for k:
+#
+# (y+k).n - (y+k).o = x.n [2]
+#
+# By #1, this is the same as
+#
+# (y+k).n - ((y+k).s + (y+k).d) = x.n [3]
+#
+# By #5, (y+k).n = y.n + k, which equals x.n + k because x.n=y.n at the start.
+# Substituting that into [3],
+#
+# x.n + k - (y+k).s - (y+k).d = x.n; the x.n terms cancel, leaving
+# k - (y+k).s - (y+k).d = 0; rearranging,
+# k = (y+k).s - (y+k).d; by #4, (y+k).s == y.s, so
+# k = y.s - (y+k).d
+#
+# On the RHS, (y+k).d can't be computed directly, but y.s can be, and we
+# approximate k by ignoring the (y+k).d term at first. Note that k can't be
+# very large, since all offset-returning methods return a duration of magnitude
+# less than 24 hours. For that reason, if y is firmly in std time, (y+k).d must
+# be 0, so ignoring it has no consequence then.
+#
+# In any case, the new value is
+#
+# z = y + y.s [4]
+#
+# It's helpful to step back at look at [4] from a higher level: it's simply
+# mapping from UTC to tz's standard time.
+#
+# At this point, if
+#
+# z.n - z.o = x.n [5]
+#
+# we have an equivalent time, and are almost done. The insecurity here is
+# at the start of daylight time. Picture US Eastern for concreteness. The wall
+# time jumps from 1:59 to 3:00, and wall hours of the form 2:MM don't make good
+# sense then. The docs ask that an Eastern tzinfo class consider such a time to
+# be EDT (because it's "after 2"), which is a redundant spelling of 1:MM EST
+# on the day DST starts. We want to return the 1:MM EST spelling because that's
+# the only spelling that makes sense on the local wall clock.
+#
+# In fact, if [5] holds at this point, we do have the standard-time spelling,
+# but that takes a bit of proof. We first prove a stronger result. What's the
+# difference between the LHS and RHS of [5]? Let
+#
+# diff = x.n - (z.n - z.o) [6]
+#
+# Now
+# z.n = by [4]
+# (y + y.s).n = by #5
+# y.n + y.s = since y.n = x.n
+# x.n + y.s = since z and y are have the same tzinfo member,
+# y.s = z.s by #2
+# x.n + z.s
+#
+# Plugging that back into [6] gives
+#
+# diff =
+# x.n - ((x.n + z.s) - z.o) = expanding
+# x.n - x.n - z.s + z.o = cancelling
+# - z.s + z.o = by #2
+# z.d
+#
+# So diff = z.d.
+#
+# If [5] is true now, diff = 0, so z.d = 0 too, and we have the standard-time
+# spelling we wanted in the endcase described above. We're done. Contrarily,
+# if z.d = 0, then we have a UTC equivalent, and are also done.
+#
+# If [5] is not true now, diff = z.d != 0, and z.d is the offset we need to
+# add to z (in effect, z is in tz's standard time, and we need to shift the
+# local clock into tz's daylight time).
+#
+# Let
+#
+# z' = z + z.d = z + diff [7]
+#
+# and we can again ask whether
+#
+# z'.n - z'.o = x.n [8]
+#
+# If so, we're done. If not, the tzinfo class is insane, according to the
+# assumptions we've made. This also requires a bit of proof. As before, let's
+# compute the difference between the LHS and RHS of [8] (and skipping some of
+# the justifications for the kinds of substitutions we've done several times
+# already):
+#
+# diff' = x.n - (z'.n - z'.o) = replacing z'.n via [7]
+# x.n - (z.n + diff - z'.o) = replacing diff via [6]
+# x.n - (z.n + x.n - (z.n - z.o) - z'.o) =
+# x.n - z.n - x.n + z.n - z.o + z'.o = cancel x.n
+# - z.n + z.n - z.o + z'.o = cancel z.n
+# - z.o + z'.o = #1 twice
+# -z.s - z.d + z'.s + z'.d = z and z' have same tzinfo
+# z'.d - z.d
+#
+# So z' is UTC-equivalent to x iff z'.d = z.d at this point. If they are equal,
+# we've found the UTC-equivalent so are done. In fact, we stop with [7] and
+# return z', not bothering to compute z'.d.
+#
+# How could z.d and z'd differ? z' = z + z.d [7], so merely moving z' by
+# a dst() offset, and starting *from* a time already in DST (we know z.d != 0),
+# would have to change the result dst() returns: we start in DST, and moving
+# a little further into it takes us out of DST.
+#
+# There isn't a sane case where this can happen. The closest it gets is at
+# the end of DST, where there's an hour in UTC with no spelling in a hybrid
+# tzinfo class. In US Eastern, that's 5:MM UTC = 0:MM EST = 1:MM EDT. During
+# that hour, on an Eastern clock 1:MM is taken as being in standard time (6:MM
+# UTC) because the docs insist on that, but 0:MM is taken as being in daylight
+# time (4:MM UTC). There is no local time mapping to 5:MM UTC. The local
+# clock jumps from 1:59 back to 1:00 again, and repeats the 1:MM hour in
+# standard time. Since that's what the local clock *does*, we want to map both
+# UTC hours 5:MM and 6:MM to 1:MM Eastern. The result is ambiguous
+# in local time, but so it goes -- it's the way the local clock works.
+#
+# When x = 5:MM UTC is the input to this algorithm, x.o=0, y.o=-5 and y.d=0,
+# so z=0:MM. z.d=60 (minutes) then, so [5] doesn't hold and we keep going.
+# z' = z + z.d = 1:MM then, and z'.d=0, and z'.d - z.d = -60 != 0 so [8]
+# (correctly) concludes that z' is not UTC-equivalent to x.
+#
+# Because we know z.d said z was in daylight time (else [5] would have held and
+# we would have stopped then), and we know z.d != z'.d (else [8] would have held
+# and we have stopped then), and there are only 2 possible values dst() can
+# return in Eastern, it follows that z'.d must be 0 (which it is in the example,
+# but the reasoning doesn't depend on the example -- it depends on there being
+# two possible dst() outcomes, one zero and the other non-zero). Therefore
+# z' must be in standard time, and is the spelling we want in this case.
+#
+# Note again that z' is not UTC-equivalent as far as the hybrid tzinfo class is
+# concerned (because it takes z' as being in standard time rather than the
+# daylight time we intend here), but returning it gives the real-life "local
+# clock repeats an hour" behavior when mapping the "unspellable" UTC hour into
+# tz.
+#
+# When the input is 6:MM, z=1:MM and z.d=0, and we stop at once, again with
+# the 1:MM standard time spelling we want.
+#
+# So how can this break? One of the assumptions must be violated. Two
+# possibilities:
+#
+# 1) [2] effectively says that y.s is invariant across all y belong to a given
+# time zone. This isn't true if, for political reasons or continental drift,
+# a region decides to change its base offset from UTC.
+#
+# 2) There may be versions of "double daylight" time where the tail end of
+# the analysis gives up a step too early. I haven't thought about that
+# enough to say.
+#
+# In any case, it's clear that the default fromutc() is strong enough to handle
+# "almost all" time zones: so long as the standard offset is invariant, it
+# doesn't matter if daylight time transition points change from year to year, or
+# if daylight time is skipped in some years; it doesn't matter how large or
+# small dst() may get within its bounds; and it doesn't even matter if some
+# perverse time zone returns a negative dst()). So a breaking case must be
+# pretty bizarre, and a tzinfo subclass can override fromutc() if it is.
+
+try:
+ from _datetime import *
+except ImportError:
+ pass
+else:
+ # Clean up unused names
+ del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, _DI100Y, _DI400Y,
+ _DI4Y, _EPOCH, _MAXORDINAL, _MONTHNAMES, _build_struct_time,
+ _check_date_fields, _check_int_field, _check_time_fields,
+ _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror,
+ _date_class, _days_before_month, _days_before_year, _days_in_month,
+ _format_time, _format_offset, _is_leap, _isoweek1monday, _math,
+ _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,
+ _divide_and_round, _parse_isoformat_date, _parse_isoformat_time,
+ _parse_hh_mm_ss_ff)
+ # XXX Since import * above excludes names that start with _,
+ # docstring does not get overwritten. In the future, it may be
+ # appropriate to maintain a single module level docstring and
+ # remove the following line.
+ from _datetime import __doc__
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/connection.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/connection.html
new file mode 100644
index 0000000000..9307e85556
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/connection.html
@@ -0,0 +1,386 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""DB-API Connection for the Google Cloud Spanner."""
+
+import warnings
+
+from google.api_core.gapic_v1.client_info import ClientInfo
+from google.cloud import spanner_v1 as spanner
+
+from google.cloud.spanner_dbapi.cursor import Cursor
+from google.cloud.spanner_dbapi.exceptions import InterfaceError
+from google.cloud.spanner_dbapi.version import DEFAULT_USER_AGENT
+from google.cloud.spanner_dbapi.version import PY_VERSION
+
+
+AUTOCOMMIT_MODE_WARNING = "This method is non-operational in autocommit mode"
+
+
+[docs]class Connection:
+ """Representation of a DB-API connection to a Cloud Spanner database.
+
+ You most likely don't need to instantiate `Connection` objects
+ directly, use the `connect` module function instead.
+
+ :type instance: :class:`~google.cloud.spanner_v1.instance.Instance`
+ :param instance: Cloud Spanner instance to connect to.
+
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
+ :param database: The database to which the connection is linked.
+ """
+
+ def __init__(self, instance, database):
+ self._instance = instance
+ self._database = database
+ self._ddl_statements = []
+
+ self._transaction = None
+ self._session = None
+
+ self.is_closed = False
+ self._autocommit = False
+
+ @property
+ def autocommit(self):
+ """Autocommit mode flag for this connection.
+
+ :rtype: bool
+ :returns: Autocommit mode flag value.
+ """
+ return self._autocommit
+
+ @autocommit.setter
+ def autocommit(self, value):
+ """Change this connection autocommit mode. Setting this value to True
+ while a transaction is active will commit the current transaction.
+
+ :type value: bool
+ :param value: New autocommit mode state.
+ """
+ if value and not self._autocommit:
+ self.commit()
+
+ self._autocommit = value
+
+ @property
+ def database(self):
+ """Database to which this connection relates.
+
+ :rtype: :class:`~google.cloud.spanner_v1.database.Database`
+ :returns: The related database object.
+ """
+ return self._database
+
+ @property
+ def instance(self):
+ """Instance to which this connection relates.
+
+ :rtype: :class:`~google.cloud.spanner_v1.instance.Instance`
+ :returns: The related instance object.
+ """
+ return self._instance
+
+ def _session_checkout(self):
+ """Get a Cloud Spanner session from the pool.
+
+ If there is already a session associated with
+ this connection, it'll be used instead.
+
+ :rtype: :class:`google.cloud.spanner_v1.session.Session`
+ :returns: Cloud Spanner session object ready to use.
+ """
+ if not self._session:
+ self._session = self.database._pool.get()
+
+ return self._session
+
+ def _release_session(self):
+ """Release the currently used Spanner session.
+
+ The session will be returned into the sessions pool.
+ """
+ self.database._pool.put(self._session)
+ self._session = None
+
+[docs] def transaction_checkout(self):
+ """Get a Cloud Spanner transaction.
+
+ Begin a new transaction, if there is no transaction in
+ this connection yet. Return the begun one otherwise.
+
+ The method is non operational in autocommit mode.
+
+ :rtype: :class:`google.cloud.spanner_v1.transaction.Transaction`
+ :returns: A Cloud Spanner transaction object, ready to use.
+ """
+ if not self.autocommit:
+ if (
+ not self._transaction
+ or self._transaction.committed
+ or self._transaction.rolled_back
+ ):
+ self._transaction = self._session_checkout().transaction()
+ self._transaction.begin()
+
+ return self._transaction
+
+ def _raise_if_closed(self):
+ """Helper to check the connection state before running a query.
+ Raises an exception if this connection is closed.
+
+ :raises: :class:`InterfaceError`: if this connection is closed.
+ """
+ if self.is_closed:
+ raise InterfaceError("connection is already closed")
+
+[docs] def close(self):
+ """Closes this connection.
+
+ The connection will be unusable from this point forward. If the
+ connection has an active transaction, it will be rolled back.
+ """
+ if (
+ self._transaction
+ and not self._transaction.committed
+ and not self._transaction.rolled_back
+ ):
+ self._transaction.rollback()
+
+ self.is_closed = True
+
+[docs] def commit(self):
+ """Commits any pending transaction to the database.
+
+ This method is non-operational in autocommit mode.
+ """
+ if self._autocommit:
+ warnings.warn(AUTOCOMMIT_MODE_WARNING, UserWarning, stacklevel=2)
+ elif self._transaction:
+ self._transaction.commit()
+ self._release_session()
+
+[docs] def rollback(self):
+ """Rolls back any pending transaction.
+
+ This is a no-op if there is no active transaction or if the connection
+ is in autocommit mode.
+ """
+ if self._autocommit:
+ warnings.warn(AUTOCOMMIT_MODE_WARNING, UserWarning, stacklevel=2)
+ elif self._transaction:
+ self._transaction.rollback()
+ self._release_session()
+
+[docs] def cursor(self):
+ """Factory to create a DB-API Cursor."""
+ self._raise_if_closed()
+
+ return Cursor(self)
+
+[docs] def run_prior_DDL_statements(self):
+ self._raise_if_closed()
+
+ if self._ddl_statements:
+ ddl_statements = self._ddl_statements
+ self._ddl_statements = []
+
+ return self.database.update_ddl(ddl_statements).result()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, etype, value, traceback):
+ self.commit()
+ self.close()
+
+
+[docs]def connect(
+ instance_id,
+ database_id,
+ project=None,
+ credentials=None,
+ pool=None,
+ user_agent=None,
+):
+ """Creates a connection to a Google Cloud Spanner database.
+
+ :type instance_id: str
+ :param instance_id: The ID of the instance to connect to.
+
+ :type database_id: str
+ :param database_id: The ID of the database to connect to.
+
+ :type project: str
+ :param project: (Optional) The ID of the project which owns the
+ instances, tables and data. If not provided, will
+ attempt to determine from the environment.
+
+ :type credentials: :class:`~google.auth.credentials.Credentials`
+ :param credentials: (Optional) The authorization credentials to attach to
+ requests. These credentials identify this application
+ to the service. If none are specified, the client will
+ attempt to ascertain the credentials from the
+ environment.
+
+ :type pool: Concrete subclass of
+ :class:`~google.cloud.spanner_v1.pool.AbstractSessionPool`.
+ :param pool: (Optional). Session pool to be used by database.
+
+ :type user_agent: str
+ :param user_agent: (Optional) User agent to be used with this connection's
+ requests.
+
+ :rtype: :class:`google.cloud.spanner_dbapi.connection.Connection`
+ :returns: Connection object associated with the given Google Cloud Spanner
+ resource.
+
+ :raises: :class:`ValueError` in case of given instance/database
+ doesn't exist.
+ """
+
+ client_info = ClientInfo(
+ user_agent=user_agent or DEFAULT_USER_AGENT,
+ python_version=PY_VERSION,
+ )
+
+ client = spanner.Client(
+ project=project,
+ credentials=credentials,
+ client_info=client_info,
+ )
+
+ instance = client.instance(instance_id)
+ if not instance.exists():
+ raise ValueError("instance '%s' does not exist." % instance_id)
+
+ database = instance.database(database_id, pool=pool)
+ if not database.exists():
+ raise ValueError("database '%s' does not exist." % database_id)
+
+ return Connection(instance, database)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/cursor.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/cursor.html
new file mode 100644
index 0000000000..633d183585
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/cursor.html
@@ -0,0 +1,463 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Database cursor for Google Cloud Spanner DB-API."""
+
+from google.api_core.exceptions import AlreadyExists
+from google.api_core.exceptions import FailedPrecondition
+from google.api_core.exceptions import InternalServerError
+from google.api_core.exceptions import InvalidArgument
+
+from collections import namedtuple
+
+from google.cloud import spanner_v1 as spanner
+
+from google.cloud.spanner_dbapi.exceptions import IntegrityError
+from google.cloud.spanner_dbapi.exceptions import InterfaceError
+from google.cloud.spanner_dbapi.exceptions import OperationalError
+from google.cloud.spanner_dbapi.exceptions import ProgrammingError
+
+from google.cloud.spanner_dbapi import _helpers
+from google.cloud.spanner_dbapi._helpers import ColumnInfo
+from google.cloud.spanner_dbapi._helpers import code_to_display_size
+
+from google.cloud.spanner_dbapi import parse_utils
+from google.cloud.spanner_dbapi.parse_utils import get_param_types
+from google.cloud.spanner_dbapi.utils import PeekIterator
+
+_UNSET_COUNT = -1
+
+ColumnDetails = namedtuple("column_details", ["null_ok", "spanner_type"])
+
+
+[docs]class Cursor(object):
+ """Database cursor to manage the context of a fetch operation.
+
+ :type connection: :class:`~google.cloud.spanner_dbapi.connection.Connection`
+ :param connection: A DB-API connection to Google Cloud Spanner.
+ """
+
+ def __init__(self, connection):
+ self._itr = None
+ self._result_set = None
+ self._row_count = _UNSET_COUNT
+ self.connection = connection
+ self._is_closed = False
+
+ # the number of rows to fetch at a time with fetchmany()
+ self.arraysize = 1
+
+ @property
+ def is_closed(self):
+ """The cursor close indicator.
+
+ :rtype: bool
+ :returns: True if the cursor or the parent connection is closed,
+ otherwise False.
+ """
+ return self._is_closed or self.connection.is_closed
+
+ @property
+ def description(self):
+ """Read-only attribute containing a sequence of the following items:
+
+ - ``name``
+ - ``type_code``
+ - ``display_size``
+ - ``internal_size``
+ - ``precision``
+ - ``scale``
+ - ``null_ok``
+
+ :rtype: tuple
+ :returns: A tuple of columns' information.
+ """
+ if not (self._result_set and self._result_set.metadata):
+ return None
+
+ row_type = self._result_set.metadata.row_type
+ columns = []
+
+ for field in row_type.fields:
+ column_info = ColumnInfo(
+ name=field.name,
+ type_code=field.type.code,
+ # Size of the SQL type of the column.
+ display_size=code_to_display_size.get(field.type.code),
+ # Client perceived size of the column.
+ internal_size=field.ByteSize(),
+ )
+ columns.append(column_info)
+
+ return tuple(columns)
+
+ @property
+ def rowcount(self):
+ """The number of rows produced by the last `.execute()`.
+
+ :rtype: int
+ :returns: The number of rows produced by the last .execute*().
+ """
+ return self._row_count
+
+ def _raise_if_closed(self):
+ """Raise an exception if this cursor is closed.
+
+ Helper to check this cursor's state before running a
+ SQL/DDL/DML query. If the parent connection is
+ already closed it also raises an error.
+
+ :raises: :class:`InterfaceError` if this cursor is closed.
+ """
+ if self.is_closed:
+ raise InterfaceError("Cursor and/or connection is already closed.")
+
+[docs] def callproc(self, procname, args=None):
+ """A no-op, raising an error if the cursor or connection is closed."""
+ self._raise_if_closed()
+
+[docs] def close(self):
+ """Closes this Cursor, making it unusable from this point forward."""
+ self._is_closed = True
+
+ def _do_execute_update(self, transaction, sql, params, param_types=None):
+ sql = parse_utils.ensure_where_clause(sql)
+ sql, params = parse_utils.sql_pyformat_args_to_spanner(sql, params)
+
+ result = transaction.execute_update(
+ sql, params=params, param_types=get_param_types(params)
+ )
+ self._itr = None
+ if type(result) == int:
+ self._row_count = result
+
+ return result
+
+[docs] def execute(self, sql, args=None):
+ """Prepares and executes a Spanner database operation.
+
+ :type sql: str
+ :param sql: A SQL query statement.
+
+ :type args: list
+ :param args: Additional parameters to supplement the SQL query.
+ """
+ if not self.connection:
+ raise ProgrammingError("Cursor is not connected to the database")
+
+ self._raise_if_closed()
+
+ self._result_set = None
+
+ # Classify whether this is a read-only SQL statement.
+ try:
+ classification = parse_utils.classify_stmt(sql)
+ if classification == parse_utils.STMT_DDL:
+ self.connection._ddl_statements.append(sql)
+ return
+
+ # For every other operation, we've got to ensure that
+ # any prior DDL statements were run.
+ # self._run_prior_DDL_statements()
+ self.connection.run_prior_DDL_statements()
+
+ if not self.connection.autocommit:
+ transaction = self.connection.transaction_checkout()
+
+ sql, params = parse_utils.sql_pyformat_args_to_spanner(
+ sql, args
+ )
+
+ self._result_set = transaction.execute_sql(
+ sql, params, param_types=get_param_types(params)
+ )
+ self._itr = PeekIterator(self._result_set)
+ return
+
+ if classification == parse_utils.STMT_NON_UPDATING:
+ self._handle_DQL(sql, args or None)
+ elif classification == parse_utils.STMT_INSERT:
+ _helpers.handle_insert(self.connection, sql, args or None)
+ else:
+ self.connection.database.run_in_transaction(
+ self._do_execute_update, sql, args or None
+ )
+ except (AlreadyExists, FailedPrecondition) as e:
+ raise IntegrityError(e.details if hasattr(e, "details") else e)
+ except InvalidArgument as e:
+ raise ProgrammingError(e.details if hasattr(e, "details") else e)
+ except InternalServerError as e:
+ raise OperationalError(e.details if hasattr(e, "details") else e)
+
+[docs] def executemany(self, operation, seq_of_params):
+ """Execute the given SQL with every parameters set
+ from the given sequence of parameters.
+
+ :type operation: str
+ :param operation: SQL code to execute.
+
+ :type seq_of_params: list
+ :param seq_of_params: Sequence of additional parameters to run
+ the query with.
+ """
+ self._raise_if_closed()
+
+ for params in seq_of_params:
+ self.execute(operation, params)
+
+[docs] def fetchone(self):
+ """Fetch the next row of a query result set, returning a single
+ sequence, or None when no more data is available."""
+ self._raise_if_closed()
+
+ try:
+ return next(self)
+ except StopIteration:
+ return None
+
+[docs] def fetchmany(self, size=None):
+ """Fetch the next set of rows of a query result, returning a sequence
+ of sequences. An empty sequence is returned when no more rows are available.
+
+ :type size: int
+ :param size: (Optional) The maximum number of results to fetch.
+
+ :raises InterfaceError:
+ if the previous call to .execute*() did not produce any result set
+ or if no call was issued yet.
+ """
+ self._raise_if_closed()
+
+ if size is None:
+ size = self.arraysize
+
+ items = []
+ for i in range(size):
+ try:
+ items.append(tuple(self.__next__()))
+ except StopIteration:
+ break
+
+ return items
+
+[docs] def fetchall(self):
+ """Fetch all (remaining) rows of a query result, returning them as
+ a sequence of sequences.
+ """
+ self._raise_if_closed()
+
+ return list(self.__iter__())
+
+[docs] def nextset(self):
+ """A no-op, raising an error if the cursor or connection is closed."""
+ self._raise_if_closed()
+
+[docs] def setinputsizes(self, sizes):
+ """A no-op, raising an error if the cursor or connection is closed."""
+ self._raise_if_closed()
+
+[docs] def setoutputsize(self, size, column=None):
+ """A no-op, raising an error if the cursor or connection is closed."""
+ self._raise_if_closed()
+
+ def _handle_DQL(self, sql, params):
+ with self.connection.database.snapshot() as snapshot:
+ # Reference
+ # https://googleapis.dev/python/spanner/latest/session-api.html#google.cloud.spanner_v1.session.Session.execute_sql
+ sql, params = parse_utils.sql_pyformat_args_to_spanner(sql, params)
+ res = snapshot.execute_sql(
+ sql, params=params, param_types=get_param_types(params)
+ )
+ if type(res) == int:
+ self._row_count = res
+ self._itr = None
+ else:
+ # Immediately using:
+ # iter(response)
+ # here, because this Spanner API doesn't provide
+ # easy mechanisms to detect when only a single item
+ # is returned or many, yet mixing results that
+ # are for .fetchone() with those that would result in
+ # many items returns a RuntimeError if .fetchone() is
+ # invoked and vice versa.
+ self._result_set = res
+ # Read the first element so that the StreamedResultSet can
+ # return the metadata after a DQL statement. See issue #155.
+ self._itr = PeekIterator(self._result_set)
+ # Unfortunately, Spanner doesn't seem to send back
+ # information about the number of rows available.
+ self._row_count = _UNSET_COUNT
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, etype, value, traceback):
+ self.close()
+
+ def __next__(self):
+ if self._itr is None:
+ raise ProgrammingError("no results to return")
+ return next(self._itr)
+
+ def __iter__(self):
+ if self._itr is None:
+ raise ProgrammingError("no results to return")
+ return self._itr
+
+[docs] def list_tables(self):
+ """List the tables of the linked Database.
+
+ :rtype: list
+ :returns: The list of tables within the Database.
+ """
+ return self.run_sql_in_snapshot(_helpers.SQL_LIST_TABLES)
+
+[docs] def run_sql_in_snapshot(self, sql, params=None, param_types=None):
+ # Some SQL e.g. for INFORMATION_SCHEMA cannot be run in read-write transactions
+ # hence this method exists to circumvent that limit.
+ self.connection.run_prior_DDL_statements()
+
+ with self.connection.database.snapshot() as snapshot:
+ res = snapshot.execute_sql(
+ sql, params=params, param_types=param_types
+ )
+ return list(res)
+
+[docs] def get_table_column_schema(self, table_name):
+ rows = self.run_sql_in_snapshot(
+ sql=_helpers.SQL_GET_TABLE_COLUMN_SCHEMA,
+ params={"table_name": table_name},
+ param_types={"table_name": spanner.param_types.STRING},
+ )
+
+ column_details = {}
+ for column_name, is_nullable, spanner_type in rows:
+ column_details[column_name] = ColumnDetails(
+ null_ok=is_nullable == "YES", spanner_type=spanner_type
+ )
+ return column_details
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/exceptions.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/exceptions.html
new file mode 100644
index 0000000000..24255ea7de
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/exceptions.html
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Spanner DB API exceptions."""
+
+
+
+
+
+[docs]class Error(Exception):
+ """The base class for all the DB API exceptions.
+
+ Does not include :class:`Warning`.
+ """
+
+ pass
+
+
+[docs]class InterfaceError(Error):
+ """
+ Error related to the database interface
+ rather than the database itself.
+ """
+
+ pass
+
+
+
+
+
+[docs]class DataError(DatabaseError):
+ """
+ Error due to problems with the processed data like
+ division by zero, numeric value out of range, etc.
+ """
+
+ pass
+
+
+[docs]class OperationalError(DatabaseError):
+ """
+ Error related to the database's operation, e.g. an
+ unexpected disconnect, the data source name is not
+ found, a transaction could not be processed, a
+ memory allocation error, etc.
+ """
+
+ pass
+
+
+[docs]class IntegrityError(DatabaseError):
+ """
+ Error for cases of relational integrity of the database
+ is affected, e.g. a foreign key check fails.
+ """
+
+ pass
+
+
+[docs]class InternalError(DatabaseError):
+ """
+ Internal database error, e.g. the cursor is not valid
+ anymore, the transaction is out of sync, etc.
+ """
+
+ pass
+
+
+[docs]class ProgrammingError(DatabaseError):
+ """
+ Programming error, e.g. table not found or already
+ exists, syntax error in the SQL statement, wrong
+ number of parameters specified, etc.
+ """
+
+ pass
+
+
+[docs]class NotSupportedError(DatabaseError):
+ """
+ Error for case of a method or database API not
+ supported by the database was used.
+ """
+
+ pass
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/parse_utils.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/parse_utils.html
new file mode 100644
index 0000000000..00a4a72da4
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/parse_utils.html
@@ -0,0 +1,699 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"SQL parsing and classification utils."
+
+import datetime
+import decimal
+import re
+from functools import reduce
+
+import sqlparse
+from google.cloud import spanner_v1 as spanner
+
+from .exceptions import Error, ProgrammingError
+from .parser import parse_values
+from .types import DateStr, TimestampStr
+from .utils import sanitize_literals_for_upload
+
+TYPES_MAP = {
+ bool: spanner.param_types.BOOL,
+ bytes: spanner.param_types.BYTES,
+ str: spanner.param_types.STRING,
+ int: spanner.param_types.INT64,
+ float: spanner.param_types.FLOAT64,
+ datetime.datetime: spanner.param_types.TIMESTAMP,
+ datetime.date: spanner.param_types.DATE,
+ DateStr: spanner.param_types.DATE,
+ TimestampStr: spanner.param_types.TIMESTAMP,
+}
+
+SPANNER_RESERVED_KEYWORDS = {
+ "ALL",
+ "AND",
+ "ANY",
+ "ARRAY",
+ "AS",
+ "ASC",
+ "ASSERT_ROWS_MODIFIED",
+ "AT",
+ "BETWEEN",
+ "BY",
+ "CASE",
+ "CAST",
+ "COLLATE",
+ "CONTAINS",
+ "CREATE",
+ "CROSS",
+ "CUBE",
+ "CURRENT",
+ "DEFAULT",
+ "DEFINE",
+ "DESC",
+ "DISTINCT",
+ "DROP",
+ "ELSE",
+ "END",
+ "ENUM",
+ "ESCAPE",
+ "EXCEPT",
+ "EXCLUDE",
+ "EXISTS",
+ "EXTRACT",
+ "FALSE",
+ "FETCH",
+ "FOLLOWING",
+ "FOR",
+ "FROM",
+ "FULL",
+ "GROUP",
+ "GROUPING",
+ "GROUPS",
+ "HASH",
+ "HAVING",
+ "IF",
+ "IGNORE",
+ "IN",
+ "INNER",
+ "INTERSECT",
+ "INTERVAL",
+ "INTO",
+ "IS",
+ "JOIN",
+ "LATERAL",
+ "LEFT",
+ "LIKE",
+ "LIMIT",
+ "LOOKUP",
+ "MERGE",
+ "NATURAL",
+ "NEW",
+ "NO",
+ "NOT",
+ "NULL",
+ "NULLS",
+ "OF",
+ "ON",
+ "OR",
+ "ORDER",
+ "OUTER",
+ "OVER",
+ "PARTITION",
+ "PRECEDING",
+ "PROTO",
+ "RANGE",
+ "RECURSIVE",
+ "RESPECT",
+ "RIGHT",
+ "ROLLUP",
+ "ROWS",
+ "SELECT",
+ "SET",
+ "SOME",
+ "STRUCT",
+ "TABLESAMPLE",
+ "THEN",
+ "TO",
+ "TREAT",
+ "TRUE",
+ "UNBOUNDED",
+ "UNION",
+ "UNNEST",
+ "USING",
+ "WHEN",
+ "WHERE",
+ "WINDOW",
+ "WITH",
+ "WITHIN",
+}
+
+STMT_DDL = "DDL"
+STMT_NON_UPDATING = "NON_UPDATING"
+STMT_UPDATING = "UPDATING"
+STMT_INSERT = "INSERT"
+
+# Heuristic for identifying statements that don't need to be run as updates.
+RE_NON_UPDATE = re.compile(r"^\s*(SELECT)", re.IGNORECASE)
+
+RE_WITH = re.compile(r"^\s*(WITH)", re.IGNORECASE)
+
+# DDL statements follow
+# https://cloud.google.com/spanner/docs/data-definition-language
+RE_DDL = re.compile(r"^\s*(CREATE|ALTER|DROP)", re.IGNORECASE | re.DOTALL)
+
+RE_IS_INSERT = re.compile(r"^\s*(INSERT)", re.IGNORECASE | re.DOTALL)
+
+RE_INSERT = re.compile(
+ # Only match the `INSERT INTO <table_name> (columns...)
+ # otherwise the rest of the statement could be a complex
+ # operation.
+ r"^\s*INSERT INTO (?P<table_name>[^\s\(\)]+)\s*\((?P<columns>[^\(\)]+)\)",
+ re.IGNORECASE | re.DOTALL,
+)
+
+RE_VALUES_TILL_END = re.compile(r"VALUES\s*\(.+$", re.IGNORECASE | re.DOTALL)
+
+RE_VALUES_PYFORMAT = re.compile(
+ # To match: (%s, %s,....%s)
+ r"(\(\s*%s[^\(\)]+\))",
+ re.DOTALL,
+)
+
+RE_PYFORMAT = re.compile(r"(%s|%\([^\(\)]+\)s)+", re.DOTALL)
+
+
+[docs]def classify_stmt(query):
+ """Determine SQL query type.
+
+ :type query: str
+ :param query: A SQL query.
+
+ :rtype: str
+ :returns: The query type name.
+ """
+ if RE_DDL.match(query):
+ return STMT_DDL
+
+ if RE_IS_INSERT.match(query):
+ return STMT_INSERT
+
+ if RE_NON_UPDATE.match(query) or RE_WITH.match(query):
+ # As of 13-March-2020, Cloud Spanner only supports WITH for DQL
+ # statements and doesn't yet support WITH for DML statements.
+ return STMT_NON_UPDATING
+
+ return STMT_UPDATING
+
+
+[docs]def parse_insert(insert_sql, params):
+ """
+ Parse an INSERT statement an generate a list of tuples of the form:
+ [
+ (SQL, params_per_row1),
+ (SQL, params_per_row2),
+ (SQL, params_per_row3),
+ ...
+ ]
+
+ There are 4 variants of an INSERT statement:
+ a) INSERT INTO <table> (columns...) VALUES (<inlined values>): no params
+ b) INSERT INTO <table> (columns...) SELECT_STMT: no params
+ c) INSERT INTO <table> (columns...) VALUES (%s,...): with params
+ d) INSERT INTO <table> (columns...) VALUES (%s,..<EXPR>...) with params and expressions
+
+ Thus given each of the forms, it will produce a dictionary describing
+ how to upload the contents to Cloud Spanner:
+ Case a)
+ SQL: INSERT INTO T (f1, f2) VALUES (1, 2)
+ it produces:
+ {
+ 'sql_params_list': [
+ ('INSERT INTO T (f1, f2) VALUES (1, 2)', None),
+ ],
+ }
+
+ Case b)
+ SQL: 'INSERT INTO T (s, c) SELECT st, zc FROM cus ORDER BY fn, ln',
+ it produces:
+ {
+ 'sql_params_list': [
+ ('INSERT INTO T (s, c) SELECT st, zc FROM cus ORDER BY fn, ln', None),
+ ]
+ }
+
+ Case c)
+ SQL: INSERT INTO T (f1, f2) VALUES (%s, %s), (%s, %s)
+ Params: ['a', 'b', 'c', 'd']
+ it produces:
+ {
+ 'homogenous': True,
+ 'table': 'T',
+ 'columns': ['f1', 'f2'],
+ 'values': [('a', 'b',), ('c', 'd',)],
+ }
+
+ Case d)
+ SQL: INSERT INTO T (f1, f2) VALUES (%s, LOWER(%s)), (UPPER(%s), %s)
+ Params: ['a', 'b', 'c', 'd']
+ it produces:
+ {
+ 'sql_params_list': [
+ ('INSERT INTO T (f1, f2) VALUES (%s, LOWER(%s))', ('a', 'b',))
+ ('INSERT INTO T (f1, f2) VALUES (UPPER(%s), %s)', ('c', 'd',))
+ ],
+ }
+
+ :type insert_sql: str
+ :param insert_sql: A SQL insert request.
+
+ :type params: list
+ :param params: A list of parameters.
+
+ :rtype: dict
+ :returns: A dictionary that maps `sql_params_list` to the list of
+ parameters in cases a), b), d) or the dictionary with
+ information about the resulting table in case c).
+ """ # noqa
+ match = RE_INSERT.search(insert_sql)
+
+ if not match:
+ raise ProgrammingError(
+ "Could not parse an INSERT statement from %s" % insert_sql
+ )
+
+ after_values_sql = RE_VALUES_TILL_END.findall(insert_sql)
+ if not after_values_sql:
+ # Case b)
+ insert_sql = sanitize_literals_for_upload(insert_sql)
+ return {"sql_params_list": [(insert_sql, None)]}
+
+ if not params:
+ # Case a) perhaps?
+ # Check if any %s exists.
+
+ # pyformat_str_count = after_values_sql.count("%s")
+ # if pyformat_str_count > 0:
+ # raise ProgrammingError(
+ # 'no params yet there are %d "%%s" tokens' % pyformat_str_count
+ # )
+ for item in after_values_sql:
+ if item.count("%s") > 0:
+ raise ProgrammingError(
+ 'no params yet there are %d "%%s" tokens'
+ % item.count("%s")
+ )
+
+ insert_sql = sanitize_literals_for_upload(insert_sql)
+ # Confirmed case of:
+ # SQL: INSERT INTO T (a1, a2) VALUES (1, 2)
+ # Params: None
+ return {"sql_params_list": [(insert_sql, None)]}
+
+ values_str = after_values_sql[0]
+ _, values = parse_values(values_str)
+
+ if values.homogenous():
+ # Case c)
+
+ columns = [mi.strip(" `") for mi in match.group("columns").split(",")]
+ sql_params_list = []
+ insert_sql_preamble = "INSERT INTO %s (%s) VALUES %s" % (
+ match.group("table_name"),
+ match.group("columns"),
+ values.argv[0],
+ )
+ values_pyformat = [str(arg) for arg in values.argv]
+ rows_list = rows_for_insert_or_update(columns, params, values_pyformat)
+ insert_sql_preamble = sanitize_literals_for_upload(insert_sql_preamble)
+ for row in rows_list:
+ sql_params_list.append((insert_sql_preamble, row))
+
+ return {"sql_params_list": sql_params_list}
+
+ # Case d)
+ # insert_sql is of the form:
+ # INSERT INTO T(c1, c2) VALUES (%s, %s), (%s, LOWER(%s))
+
+ # Sanity check:
+ # length(all_args) == len(params)
+ args_len = reduce(lambda a, b: a + b, [len(arg) for arg in values.argv])
+ if args_len != len(params):
+ raise ProgrammingError(
+ "Invalid length: VALUES(...) len: %d != len(params): %d"
+ % (args_len, len(params))
+ )
+
+ trim_index = insert_sql.find(values_str)
+ before_values_sql = insert_sql[:trim_index]
+
+ sql_param_tuples = []
+ for token_arg in values.argv:
+ row_sql = before_values_sql + " VALUES%s" % token_arg
+ row_sql = sanitize_literals_for_upload(row_sql)
+ row_params, params = (
+ tuple(params[0 : len(token_arg)]),
+ params[len(token_arg) :],
+ )
+ sql_param_tuples.append((row_sql, row_params))
+
+ return {"sql_params_list": sql_param_tuples}
+
+
+[docs]def rows_for_insert_or_update(columns, params, pyformat_args=None):
+ """
+ Create a tupled list of params to be used as a single value per
+ value that inserted from a statement such as
+ SQL: 'INSERT INTO t (f1, f2, f3) VALUES (%s, %s, %s), (%s, %s, %s), (%s, %s, %s)'
+ Params A: [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
+ Params B: [1, 2, 3, 4, 5, 6, 7, 8, 9]
+
+ We'll have to convert both params types into:
+ Params: [(1, 2, 3,), (4, 5, 6,), (7, 8, 9,)]
+
+ :type columns: list
+ :param columns: A list of the columns of the table.
+
+ :type params: list
+ :param params: A list of parameters.
+
+ :rtype: list
+ :returns: A properly restructured list of the parameters.
+ """ # noqa
+
+ if not pyformat_args:
+ # This is the case where we have for example:
+ # SQL: 'INSERT INTO t (f1, f2, f3)'
+ # Params A: [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
+ # Params B: [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ #
+ # We'll have to convert both params types into:
+ # [(1, 2, 3,), (4, 5, 6,), (7, 8, 9,)]
+ contains_all_list_or_tuples = True
+ for param in params:
+ if not (isinstance(param, list) or isinstance(param, tuple)):
+ contains_all_list_or_tuples = False
+ break
+
+ if contains_all_list_or_tuples:
+ # The case with Params A: [(1, 2, 3), (4, 5, 6)]
+ # Ensure that each param's length == len(columns)
+ columns_len = len(columns)
+ for param in params:
+ if columns_len != len(param):
+ raise Error(
+ "\nlen(`%s`)=%d\n!=\ncolum_len(`%s`)=%d"
+ % (param, len(param), columns, columns_len)
+ )
+ return params
+ else:
+ # The case with Params B: [1, 2, 3]
+ # Insert statements' params are only passed as tuples or lists,
+ # yet for do_execute_update, we've got to pass in list of list.
+ # https://googleapis.dev/python/spanner/latest/transaction-api.html\
+ # #google.cloud.spanner_v1.transaction.Transaction.insert
+ n_stride = len(columns)
+ else:
+ # This is the case where we have for example:
+ # SQL: 'INSERT INTO t (f1, f2, f3) VALUES (%s, %s, %s),
+ # (%s, %s, %s), (%s, %s, %s)'
+ # Params: [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ # which should become
+ # Columns: (f1, f2, f3)
+ # new_params: [(1, 2, 3,), (4, 5, 6,), (7, 8, 9,)]
+
+ # Sanity check 1: all the pyformat_values should have the exact same
+ # length.
+ first, rest = pyformat_args[0], pyformat_args[1:]
+ n_stride = first.count("%s")
+ for pyfmt_value in rest:
+ n = pyfmt_value.count("%s")
+ if n_stride != n:
+ raise Error(
+ "\nlen(`%s`)=%d\n!=\nlen(`%s`)=%d"
+ % (first, n_stride, pyfmt_value, n)
+ )
+
+ # Sanity check 2: len(params) MUST be a multiple of n_stride aka
+ # len(count of %s).
+ # so that we can properly group for example:
+ # Given pyformat args:
+ # (%s, %s, %s)
+ # Params:
+ # [1, 2, 3, 4, 5, 6, 7, 8, 9]
+ # into
+ # [(1, 2, 3), (4, 5, 6), (7, 8, 9)]
+ if (len(params) % n_stride) != 0:
+ raise ProgrammingError(
+ "Invalid length: len(params)=%d MUST be a multiple of "
+ "len(pyformat_args)=%d" % (len(params), n_stride)
+ )
+
+ # Now chop up the strides.
+ strides = []
+ for step in range(0, len(params), n_stride):
+ stride = tuple(params[step : step + n_stride :])
+ strides.append(stride)
+
+ return strides
+
+
+[docs]def sql_pyformat_args_to_spanner(sql, params):
+ """
+ Transform pyformat set SQL to named arguments for Cloud Spanner.
+ It will also unescape previously escaped format specifiers
+ like %%s to %s.
+ For example:
+ SQL: 'SELECT * from t where f1=%s, f2=%s, f3=%s'
+ Params: ('a', 23, '888***')
+ becomes:
+ SQL: 'SELECT * from t where f1=@a0, f2=@a1, f3=@a2'
+ Params: {'a0': 'a', 'a1': 23, 'a2': '888***'}
+
+ OR
+ SQL: 'SELECT * from t where f1=%(f1)s, f2=%(f2)s, f3=%(f3)s'
+ Params: {'f1': 'a', 'f2': 23, 'f3': '888***', 'extra': 'aye')
+ becomes:
+ SQL: 'SELECT * from t where f1=@a0, f2=@a1, f3=@a2'
+ Params: {'a0': 'a', 'a1': 23, 'a2': '888***'}
+
+ :type sql: str
+ :param sql: A SQL request.
+
+ :type params: list
+ :param params: A list of parameters.
+
+ :rtype: tuple(str, dict)
+ :returns: A tuple of the sanitized SQL and a dictionary of the named arguments.
+ """
+ if not params:
+ return sanitize_literals_for_upload(sql), params
+
+ found_pyformat_placeholders = RE_PYFORMAT.findall(sql)
+ params_is_dict = isinstance(params, dict)
+
+ if params_is_dict:
+ if not found_pyformat_placeholders:
+ return sanitize_literals_for_upload(sql), params
+ else:
+ n_params = len(params) if params else 0
+ n_matches = len(found_pyformat_placeholders)
+ if n_matches != n_params:
+ raise Error(
+ "pyformat_args mismatch\ngot %d args from %s\n"
+ "want %d args in %s"
+ % (n_matches, found_pyformat_placeholders, n_params, params)
+ )
+
+ named_args = {}
+ # We've now got for example:
+ # Case a) Params is a non-dict
+ # SQL: 'SELECT * from t where f1=%s, f2=%s, f3=%s'
+ # Params: ('a', 23, '888***')
+ # Case b) Params is a dict and the matches are %(value)s'
+ for i, pyfmt in enumerate(found_pyformat_placeholders):
+ key = "a%d" % i
+ sql = sql.replace(pyfmt, "@" + key, 1)
+ if params_is_dict:
+ # The '%(key)s' case, so interpolate it.
+ resolved_value = pyfmt % params
+ named_args[key] = resolved_value
+ else:
+ named_args[key] = cast_for_spanner(params[i])
+
+ return sanitize_literals_for_upload(sql), named_args
+
+
+[docs]def cast_for_spanner(value):
+ """Convert the param to its Cloud Spanner equivalent type.
+
+ :type value: Any
+ :param value: The value to convert to a Cloud Spanner type.
+
+ :rtype: Any
+ :returns: The value converted to a Cloud Spanner type.
+ """
+ if isinstance(value, decimal.Decimal):
+ return float(value)
+ return value
+
+
+[docs]def get_param_types(params):
+ """Determine Cloud Spanner types for the given parameters.
+
+ :type params: dict
+ :param params: Parameters requiring to find Cloud Spanner types.
+
+ :rtype: dict
+ :returns: The types index for the given parameters.
+ """
+ if params is None:
+ return
+
+ param_types = {}
+
+ for key, value in params.items():
+ type_ = type(value)
+ if type_ in TYPES_MAP:
+ param_types[key] = TYPES_MAP[type_]
+
+ return param_types
+
+
+[docs]def ensure_where_clause(sql):
+ """
+ Cloud Spanner requires a WHERE clause on UPDATE and DELETE statements.
+ Add a dummy WHERE clause if necessary.
+
+ :type sql: str
+ :param sql: A SQL request.
+
+ :rtype: str
+ :returns: A SQL request with dummy WHERE clause if necessary.
+ """
+ if any(
+ isinstance(token, sqlparse.sql.Where)
+ for token in sqlparse.parse(sql)[0]
+ ):
+ return sql
+ return sql + " WHERE 1=1"
+
+
+[docs]def escape_name(name):
+ """
+ Apply backticks to the name that either contain '-' or
+ ' ', or is a Cloud Spanner's reserved keyword.
+
+ :type name: :class:`str`
+ :param name: The name to escape.
+
+ :rtype: :class:`str`
+ :returns: The name escaped if it has to be escaped.
+ """
+ if "-" in name or " " in name or name.upper() in SPANNER_RESERVED_KEYWORDS:
+ return "`" + name + "`"
+ return name
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/parser.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/parser.html
new file mode 100644
index 0000000000..eee1b3a06c
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/parser.html
@@ -0,0 +1,403 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""
+Grammar for parsing VALUES:
+ VALUES := `VALUES(` + ARGS + `)`
+ ARGS := [EXPR,]*EXPR
+ EXPR := TERMINAL / FUNC
+ TERMINAL := `%s`
+ FUNC := alphanum + `(` + ARGS + `)`
+ alphanum := (a-zA-Z_)[0-9a-ZA-Z_]*
+
+thus given:
+ statement: 'VALUES (%s, %s), (%s, LOWER(UPPER(%s))) , (%s)'
+ It'll parse:
+ VALUES
+ |- ARGS
+ |- (TERMINAL, TERMINAL)
+ |- (TERMINAL, FUNC
+ |- FUNC
+ |- (TERMINAL)
+ |- (TERMINAL)
+"""
+
+from .exceptions import ProgrammingError
+
+ARGS = "ARGS"
+EXPR = "EXPR"
+FUNC = "FUNC"
+VALUES = "VALUES"
+
+
+[docs]class func(object):
+ def __init__(self, func_name, args):
+ self.name = func_name
+ self.args = args
+
+ def __str__(self):
+ return "%s%s" % (self.name, self.args)
+
+ def __repr__(self):
+ return self.__str__()
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+ if self.name != other.name:
+ return False
+ if not isinstance(other.args, type(self.args)):
+ return False
+ if len(self.args) != len(other.args):
+ return False
+ return self.args == other.args
+
+ def __len__(self):
+ return len(self.args)
+
+
+[docs]class terminal(str):
+ """Represent the unit symbol that can be part of a SQL values clause."""
+
+ pass
+
+
+[docs]class a_args(object):
+ """Expression arguments.
+
+ :type argv: list
+ :param argv: A List of expression arguments.
+ """
+
+ def __init__(self, argv):
+ self.argv = argv
+
+ def __str__(self):
+ return "(" + ", ".join([str(arg) for arg in self.argv]) + ")"
+
+ def __repr__(self):
+ return self.__str__()
+
+[docs] def has_expr(self):
+ return any(
+ [token for token in self.argv if not isinstance(token, terminal)]
+ )
+
+ def __len__(self):
+ return len(self.argv)
+
+ def __eq__(self, other):
+ if type(self) != type(other):
+ return False
+
+ if len(self) != len(other):
+ return False
+
+ for i, item in enumerate(self):
+ if item != other[i]:
+ return False
+
+ return True
+
+ def __getitem__(self, index):
+ return self.argv[index]
+
+[docs] def homogenous(self):
+ """Check arguments of the expression to be homogeneous.
+
+ :rtype: bool
+ :return: True if all the arguments of the expression are in pyformat
+ and each has the same length, False otherwise.
+ """
+ if not self._is_equal_length():
+ return False
+
+ for arg in self.argv:
+ if isinstance(arg, terminal):
+ continue
+ elif isinstance(arg, a_args):
+ if not arg.homogenous():
+ return False
+ else:
+ return False
+ return True
+
+ def _is_equal_length(self):
+ """Return False if all the arguments have the same length.
+
+ :rtype: bool
+ :return: False if the sequences of the arguments have the same length.
+ """
+ if len(self) == 0:
+ return True
+
+ arg0_len = len(self.argv[0])
+ for arg in self.argv[1:]:
+ if len(arg) != arg0_len:
+ return False
+
+ return True
+
+
+[docs]class values(a_args):
+ """A wrapper for values.
+
+ :rtype: str
+ :returns: A string of the values expression in a tree view.
+ """
+
+ def __str__(self):
+ return "VALUES%s" % super().__str__()
+
+
+
+
+
+pyfmt_str = terminal("%s")
+
+
+[docs]def expect(word, token):
+ """Parse the given expression recursively.
+
+ :type word: str
+ :param word: A string expression.
+
+ :type token: str
+ :param token: An expression token.
+
+ :rtype: (str, Any)
+ :returns: A tuple containing the rest of the expression string and the tree
+ of the already parsed.
+ :raises :class:`ProgrammingError`: If there is a parsing error.
+ """
+ word = word.strip()
+ if token == VALUES:
+ if not word.startswith("VALUES"):
+ raise ProgrammingError(
+ "VALUES: `%s` does not start with VALUES" % word
+ )
+ word = word[len("VALUES") :].lstrip()
+
+ all_args = []
+ while word:
+ word = word.strip()
+
+ word, arg = expect(word, ARGS)
+ all_args.append(arg)
+ word = word.strip()
+
+ if word and not word.startswith(","):
+ raise ProgrammingError(
+ "VALUES: expected `,` got %s in %s" % (word[0], word)
+ )
+ word = word[1:]
+ return "", values(all_args)
+
+ elif token == FUNC:
+ begins_with_letter = word and (word[0].isalpha() or word[0] == "_")
+ if not begins_with_letter:
+ raise ProgrammingError(
+ "FUNC: `%s` does not begin with `a-zA-z` nor a `_`" % word
+ )
+
+ rest = word[1:]
+ end = 0
+ for ch in rest:
+ if ch.isalnum() or ch == "_":
+ end += 1
+ else:
+ break
+
+ func_name, rest = word[: end + 1], word[end + 1 :].strip()
+
+ word, args = expect(rest, ARGS)
+ return word, func(func_name, args)
+
+ elif token == ARGS:
+ # The form should be:
+ # (%s)
+ # (%s, %s...)
+ # (FUNC, %s...)
+ # (%s, %s...)
+ if not (word and word.startswith("(")):
+ raise ProgrammingError(
+ "ARGS: supposed to begin with `(` in `%s`" % word
+ )
+
+ word = word[1:]
+
+ terms = []
+ while True:
+ word = word.strip()
+ if not word or word.startswith(")"):
+ break
+
+ if word == "%s":
+ terms.append(pyfmt_str)
+ word = ""
+ elif not word.startswith("%s"):
+ word, parsed = expect(word, FUNC)
+ terms.append(parsed)
+ else:
+ terms.append(pyfmt_str)
+ word = word[2:].strip()
+
+ if word.startswith(","):
+ word = word[1:]
+
+ if not (word and word.startswith(")")):
+ raise ProgrammingError(
+ "ARGS: supposed to end with `)` in `%s`" % word
+ )
+
+ word = word[1:]
+ return word, a_args(terms)
+
+ elif token == EXPR:
+ if word == "%s":
+ # Terminal symbol.
+ return "", pyfmt_str
+
+ # Otherwise we expect a function.
+ return expect(word, FUNC)
+
+ raise ProgrammingError("Unknown token `%s`" % token)
+
+
+[docs]def as_values(values_stmt):
+ """Return the parsed values.
+
+ :type values_stmt: str
+ :param values_stmt: Raw values.
+
+ :rtype: Any
+ :returns: A tree of the already parsed expression.
+ """
+ _, _values = parse_values(values_stmt)
+ return _values
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/types.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/types.html
new file mode 100644
index 0000000000..f29d0e9ca7
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/types.html
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Implementation of the type objects and constructors according to the
+ PEP-0249 specification.
+
+ See
+ https://www.python.org/dev/peps/pep-0249/#type-objects-and-constructors
+"""
+
+import datetime
+import time
+from base64 import b64encode
+
+
+def _date_from_ticks(ticks):
+ """Based on PEP-249 Implementation Hints for Module Authors:
+
+ https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
+ """
+ return Date(*time.localtime(ticks)[:3])
+
+
+def _time_from_ticks(ticks):
+ """Based on PEP-249 Implementation Hints for Module Authors:
+
+ https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
+ """
+ return Time(*time.localtime(ticks)[3:6])
+
+
+def _timestamp_from_ticks(ticks):
+ """Based on PEP-249 Implementation Hints for Module Authors:
+
+ https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
+ """
+ return Timestamp(*time.localtime(ticks)[:6])
+
+
+class _DBAPITypeObject(object):
+ """Implementation of a helper class used for type comparison among similar
+ but possibly different types.
+
+ See
+ https://www.python.org/dev/peps/pep-0249/#implementation-hints-for-module-authors
+ """
+
+ def __init__(self, *values):
+ self.values = values
+
+ def __eq__(self, other):
+ return other in self.values
+
+
+Date = datetime.date
+Time = datetime.time
+Timestamp = datetime.datetime
+DateFromTicks = _date_from_ticks
+TimeFromTicks = _time_from_ticks
+TimestampFromTicks = _timestamp_from_ticks
+Binary = b64encode
+
+STRING = "STRING"
+BINARY = _DBAPITypeObject("TYPE_CODE_UNSPECIFIED", "BYTES", "ARRAY", "STRUCT")
+NUMBER = _DBAPITypeObject("BOOL", "INT64", "FLOAT64", "NUMERIC")
+DATETIME = _DBAPITypeObject("TIMESTAMP", "DATE")
+ROWID = "STRING"
+
+
+[docs]class TimestampStr(str):
+ """[inherited from the alpha release]
+
+ TODO: Decide whether this class is necessary
+
+ TimestampStr exists so that we can purposefully format types as timestamps
+ compatible with Cloud Spanner's TIMESTAMP type, but right before making
+ queries, it'll help differentiate between normal strings and the case of
+ types that should be TIMESTAMP.
+ """
+
+ pass
+
+
+[docs]class DateStr(str):
+ """[inherited from the alpha release]
+
+ TODO: Decide whether this class is necessary
+
+ DateStr is a sentinel type to help format Django dates as
+ compatible with Cloud Spanner's DATE type, but right before making
+ queries, it'll help differentiate between normal strings and the case of
+ types that should be DATE.
+ """
+
+ pass
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/google/cloud/spanner_dbapi/utils.html b/docs/_build/html/_modules/google/cloud/spanner_dbapi/utils.html
new file mode 100644
index 0000000000..46564c5d3a
--- /dev/null
+++ b/docs/_build/html/_modules/google/cloud/spanner_dbapi/utils.html
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+# Copyright 2020 Google LLC
+#
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+import re
+
+
+[docs]class PeekIterator:
+ """
+ Peek at the first element out of an iterator for the sake of operations
+ like auto-population of fields on reading the first element.
+ If next's result is an instance of list, it'll be converted into a tuple
+ to conform with DBAPI v2's sequence expectations.
+
+ :type source: list
+ :param source: A list of source for the Iterator.
+ """
+
+ def __init__(self, source):
+ itr_src = iter(source)
+
+ self.__iters = []
+ self.__index = 0
+
+ try:
+ head = next(itr_src)
+ # Restitch and prepare to read from multiple iterators.
+ self.__iters = [iter(itr) for itr in [[head], itr_src]]
+ except StopIteration:
+ pass
+
+ def __next__(self):
+ if self.__index >= len(self.__iters):
+ raise StopIteration
+
+ iterator = self.__iters[self.__index]
+ try:
+ head = next(iterator)
+ except StopIteration:
+ # That iterator has been exhausted, try with the next one.
+ self.__index += 1
+ return self.__next__()
+ else:
+ return tuple(head) if isinstance(head, list) else head
+
+ def __iter__(self):
+ return self
+
+
+re_UNICODE_POINTS = re.compile(r"([^\s]*[\u0080-\uFFFF]+[^\s]*)")
+
+
+[docs]def backtick_unicode(sql):
+ """Check the SQL to be valid and split it by segments.
+
+ :type sql: str
+ :param sql: A SQL request.
+
+ :rtype: str
+ :returns: A SQL parsed by segments in unicode if initial
+ SQL is valid, initial string otherwise.
+ """
+ matches = list(re_UNICODE_POINTS.finditer(sql))
+ if not matches:
+ return sql
+
+ segments = []
+
+ last_end = 0
+ for match in matches:
+ start, end = match.span()
+ if sql[start] != "`" and sql[end - 1] != "`":
+ segments.append(sql[last_end:start] + "`" + sql[start:end] + "`")
+ else:
+ segments.append(sql[last_end:end])
+
+ last_end = end
+
+ return "".join(segments)
+
+
+[docs]def sanitize_literals_for_upload(s):
+ """Convert literals in s, to be fit for consumption by Cloud Spanner.
+
+ * Convert %% (escaped percent literals) to %. Percent signs must be escaped
+ when values like %s are used as SQL parameter placeholders but Spanner's query
+ language uses placeholders like @a0 and doesn't expect percent signs to be
+ escaped.
+
+ * Quote words containing non-ASCII, with backticks, for example föö to `föö`.
+
+ :type s: str
+ :param s: A string with literals to be fitted for the consumption.
+
+ :rtype: str
+ :returns: A sanitized string for uploading.
+ """
+ return backtick_unicode(s.replace("%%", "%"))
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_modules/index.html b/docs/_build/html/_modules/index.html
new file mode 100644
index 0000000000..121a482305
--- /dev/null
+++ b/docs/_build/html/_modules/index.html
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/_build/html/_sources/README.rst.txt b/docs/_build/html/_sources/README.rst.txt
new file mode 100644
index 0000000000..89a0106941
--- /dev/null
+++ b/docs/_build/html/_sources/README.rst.txt
@@ -0,0 +1 @@
+../README.rst
\ No newline at end of file
diff --git a/docs/_build/html/_sources/api-reference.rst.txt b/docs/_build/html/_sources/api-reference.rst.txt
new file mode 100644
index 0000000000..7da735f86f
--- /dev/null
+++ b/docs/_build/html/_sources/api-reference.rst.txt
@@ -0,0 +1,6 @@
+API Reference
+=============
+
+The following classes and methods constitute the Spanner DB API connection.
+
+[this page is under construction]
diff --git a/docs/_build/html/_sources/changelog.md.txt b/docs/_build/html/_sources/changelog.md.txt
new file mode 100644
index 0000000000..825c32f0d0
--- /dev/null
+++ b/docs/_build/html/_sources/changelog.md.txt
@@ -0,0 +1 @@
+# Changelog
diff --git a/docs/_build/html/_sources/connection-usage.rst.txt b/docs/_build/html/_sources/connection-usage.rst.txt
new file mode 100644
index 0000000000..9049e3e2b2
--- /dev/null
+++ b/docs/_build/html/_sources/connection-usage.rst.txt
@@ -0,0 +1,32 @@
+DB API Connection
+=================
+
+.. _spanner-client:
+
+
+Creating a Connection
+---------------------
+
+To use the API, the :class:`~google.cloud.spanner_django.connection.Connection`
+class defines a high-level interface which handles connection to a Spanner
+databse:
+
+.. code:: python
+
+ from google.cloud.spanner import connection
+ conn = connection.Connection()
+
+Configuration
+-------------
+
+- For an overview of authentication in ``google.cloud-python``,
+ see `Authentication
+