From 415a01ee45e783f8b5b40bc4b30efbe8d4fb3a9f Mon Sep 17 00:00:00 2001 From: topper-123 Date: Sun, 29 Jul 2018 10:08:00 +0000 Subject: [PATCH 01/47] fix for #21224 wrong sort order (#22110) --- pandas/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index e878b32fcad7b1..c714ce2228d099 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -126,7 +126,7 @@ def all_arithmetic_operators(request): # use sorted as dicts in py<3.6 have random order, which xdist doesn't like _cython_table = sorted(((key, value) for key, value in pd.core.base.SelectionMixin._cython_table.items()), - key=lambda x: x[0].__class__.__name__) + key=lambda x: x[0].__name__) @pytest.fixture(params=_cython_table) From 21cbca6dabf5606964358f36a38a567ce3ea0ac1 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 29 Jul 2018 08:20:20 -0700 Subject: [PATCH 02/47] remove cnp usage from sas.pyx (#22111) --- pandas/io/sas/sas.pyx | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pandas/io/sas/sas.pyx b/pandas/io/sas/sas.pyx index 3d94dc127a1d21..221c07a0631d25 100644 --- a/pandas/io/sas/sas.pyx +++ b/pandas/io/sas/sas.pyx @@ -2,20 +2,22 @@ # cython: boundscheck=False, initializedcheck=False import numpy as np -cimport numpy as cnp -from numpy cimport uint8_t, uint16_t, int8_t, int64_t, ndarray import sas_constants as const +ctypedef signed long long int64_t +ctypedef unsigned char uint8_t +ctypedef unsigned short uint16_t + # rle_decompress decompresses data using a Run Length Encoding # algorithm. It is partially documented here: # # https://cran.r-project.org/web/packages/sas7bdat/vignettes/sas7bdat.pdf -cdef ndarray[uint8_t, ndim=1] rle_decompress( - int result_length, ndarray[uint8_t, ndim=1] inbuff): +cdef const uint8_t[:] rle_decompress(int result_length, + const uint8_t[:] inbuff): cdef: uint8_t control_byte, x - uint8_t [:] result = np.zeros(result_length, np.uint8) + uint8_t[:] result = np.zeros(result_length, np.uint8) int rpos = 0, ipos = 0, length = len(inbuff) int i, nbytes, end_of_first_byte @@ -115,14 +117,14 @@ cdef ndarray[uint8_t, ndim=1] rle_decompress( # rdc_decompress decompresses data using the Ross Data Compression algorithm: # # http://collaboration.cmc.ec.gc.ca/science/rpn/biblio/ddj/Website/articles/CUJ/1992/9210/ross/ross.htm -cdef ndarray[uint8_t, ndim=1] rdc_decompress( - int result_length, ndarray[uint8_t, ndim=1] inbuff): +cdef const uint8_t[:] rdc_decompress(int result_length, + const uint8_t[:] inbuff): cdef: uint8_t cmd uint16_t ctrl_bits, ctrl_mask = 0, ofs, cnt int ipos = 0, rpos = 0, k - uint8_t [:] outbuff = np.zeros(result_length, dtype=np.uint8) + uint8_t[:] outbuff = np.zeros(result_length, dtype=np.uint8) ii = -1 @@ -230,8 +232,8 @@ cdef class Parser(object): int subheader_pointer_length int current_page_type bint is_little_endian - ndarray[uint8_t, ndim=1] (*decompress)( - int result_length, ndarray[uint8_t, ndim=1] inbuff) + const uint8_t[:] (*decompress)(int result_length, + const uint8_t[:] inbuff) object parser def __init__(self, object parser): @@ -395,7 +397,7 @@ cdef class Parser(object): Py_ssize_t j int s, k, m, jb, js, current_row int64_t lngt, start, ct - ndarray[uint8_t, ndim=1] source + const uint8_t[:] source int64_t[:] column_types int64_t[:] lengths int64_t[:] offsets @@ -434,8 +436,8 @@ cdef class Parser(object): jb += 1 elif column_types[j] == column_type_string: # string - string_chunk[js, current_row] = source[start:( - start + lngt)].tostring().rstrip() + string_chunk[js, current_row] = np.array(source[start:( + start + lngt)]).tostring().rstrip() js += 1 self.current_row_on_page_index += 1 From e8e078f5657e762e874bb4635ff41c2cec694418 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Sun, 29 Jul 2018 08:25:13 -0700 Subject: [PATCH 03/47] CLN: Old helper functions (#22104) --- pandas/core/reshape/util.py | 7 ------ pandas/core/tools/datetimes.py | 23 +------------------- pandas/tests/io/test_sql.py | 6 ++++- pandas/tests/tseries/offsets/test_offsets.py | 14 ------------ 4 files changed, 6 insertions(+), 44 deletions(-) diff --git a/pandas/core/reshape/util.py b/pandas/core/reshape/util.py index e83bcf800e9491..1c2033d90cd8ad 100644 --- a/pandas/core/reshape/util.py +++ b/pandas/core/reshape/util.py @@ -3,16 +3,9 @@ from pandas.core.dtypes.common import is_list_like from pandas.compat import reduce -from pandas.core.index import Index from pandas.core import common as com -def match(needles, haystack): - haystack = Index(haystack) - needles = Index(needles) - return haystack.get_indexer(needles) - - def cartesian_product(X): """ Numpy version of itertools.product or pandas.compat.product. diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index 83de83ab76a2c8..be042c9bf8ab01 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -1,5 +1,5 @@ from functools import partial -from datetime import datetime, timedelta, time +from datetime import datetime, time from collections import MutableMapping import numpy as np @@ -850,24 +850,3 @@ def _convert_listlike(arg, format): return _convert_listlike(arg, format) return _convert_listlike(np.array([arg]), format)[0] - - -def format(dt): - """Returns date in YYYYMMDD format.""" - return dt.strftime('%Y%m%d') - - -OLE_TIME_ZERO = datetime(1899, 12, 30, 0, 0, 0) - - -def ole2datetime(oledt): - """function for converting excel date to normal date format""" - val = float(oledt) - - # Excel has a bug where it thinks the date 2/29/1900 exists - # we just reject any date before 3/1/1900. - if val < 61: - msg = "Value is outside of acceptable range: {value}".format(value=val) - raise ValueError(msg) - - return OLE_TIME_ZERO + timedelta(days=val) diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py index 4b0edfce891744..77dd06cccc532b 100644 --- a/pandas/tests/io/test_sql.py +++ b/pandas/tests/io/test_sql.py @@ -36,7 +36,6 @@ from pandas import date_range, to_datetime, to_timedelta, Timestamp import pandas.compat as compat from pandas.compat import range, lrange, string_types, PY36 -from pandas.core.tools.datetimes import format as date_format import pandas.io.sql as sql from pandas.io.sql import read_sql_table, read_sql_query @@ -2094,6 +2093,11 @@ def test_illegal_names(self): # -- Old tests from 0.13.1 (before refactor using sqlalchemy) +def date_format(dt): + """Returns date in YYYYMMDD format.""" + return dt.strftime('%Y%m%d') + + _formatters = { datetime: lambda dt: "'%s'" % date_format(dt), str: lambda x: "'%s'" % x, diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 35ee0d37e2b1af..57b9a281ac0eb6 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -29,7 +29,6 @@ QuarterEnd, BusinessMonthEnd, FY5253, Nano, Easter, FY5253Quarter, LastWeekOfMonth, Tick) -from pandas.core.tools.datetimes import format, ole2datetime import pandas.tseries.offsets as offsets from pandas.io.pickle import read_pickle from pandas._libs.tslibs import timezones @@ -45,19 +44,6 @@ #### -def test_format(): - actual = format(datetime(2008, 1, 15)) - assert actual == '20080115' - - -def test_ole2datetime(): - actual = ole2datetime(60000) - assert actual == datetime(2064, 4, 8) - - with pytest.raises(ValueError): - ole2datetime(60) - - def test_to_m8(): valb = datetime(2007, 10, 1) valu = _to_m8(valb) From 0c58a825181bde2c4d7e1a912e246181d33f55d6 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Sun, 29 Jul 2018 08:32:50 -0700 Subject: [PATCH 04/47] CLN: Remove unused variables (#21986) --- pandas/core/arrays/categorical.py | 1 - pandas/core/arrays/interval.py | 1 - pandas/core/generic.py | 9 ++++++++- pandas/core/groupby/ops.py | 1 - pandas/core/indexes/base.py | 6 ++++++ pandas/core/indexes/category.py | 2 +- pandas/core/indexes/interval.py | 1 - pandas/core/internals/blocks.py | 5 +---- pandas/core/nanops.py | 5 +++-- pandas/core/series.py | 14 ++++++++------ pandas/core/sparse/frame.py | 1 - pandas/core/window.py | 3 ++- pandas/io/formats/format.py | 2 -- pandas/io/formats/html.py | 1 - pandas/io/formats/terminal.py | 2 +- pandas/io/json/json.py | 2 +- pandas/io/sas/sas_xport.py | 4 ---- pandas/plotting/_timeseries.py | 1 - pandas/tests/io/json/test_pandas.py | 7 +++++++ 19 files changed, 38 insertions(+), 30 deletions(-) diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 4584e4694cdc53..204e800b932a98 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -348,7 +348,6 @@ def __init__(self, values, categories=None, ordered=None, dtype=None, " or `ordered`.") categories = dtype.categories - ordered = dtype.ordered elif is_categorical(values): # If no "dtype" was passed, use the one from "values", but honor diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 5ecc79e030f56d..ad01d4ec9b3caa 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -401,7 +401,6 @@ def from_tuples(cls, data, closed='right', copy=False, dtype=None): msg = ('{name}.from_tuples received an invalid ' 'item, {tpl}').format(name=name, tpl=d) raise TypeError(msg) - lhs, rhs = d left.append(lhs) right.append(rhs) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 376700f1418f6f..edf341ae2898f1 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1084,7 +1084,8 @@ def rename(self, *args, **kwargs): level = kwargs.pop('level', None) axis = kwargs.pop('axis', None) if axis is not None: - axis = self._get_axis_number(axis) + # Validate the axis + self._get_axis_number(axis) if kwargs: raise TypeError('rename() got an unexpected keyword ' @@ -5299,6 +5300,12 @@ def __copy__(self, deep=True): return self.copy(deep=deep) def __deepcopy__(self, memo=None): + """ + Parameters + ---------- + memo, default None + Standard signature. Unused + """ if memo is None: memo = {} return self.copy(deep=True) diff --git a/pandas/core/groupby/ops.py b/pandas/core/groupby/ops.py index 38ac144ac6c952..ba04ff3a3d3eee 100644 --- a/pandas/core/groupby/ops.py +++ b/pandas/core/groupby/ops.py @@ -582,7 +582,6 @@ def _transform(self, result, values, comp_ids, transform_func, elif values.ndim > 2: for i, chunk in enumerate(values.transpose(2, 0, 1)): - chunk = chunk.squeeze() transform_func(result[:, :, i], values, comp_ids, is_datetimelike, **kwargs) else: diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index f09fe8c8abdcf6..8ad058c001bba6 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -993,6 +993,12 @@ def __copy__(self, **kwargs): return self.copy(**kwargs) def __deepcopy__(self, memo=None): + """ + Parameters + ---------- + memo, default None + Standard signature. Unused + """ if memo is None: memo = {} return self.copy(deep=True) diff --git a/pandas/core/indexes/category.py b/pandas/core/indexes/category.py index d76a7ef00f6257..ab180a13ab4f3c 100644 --- a/pandas/core/indexes/category.py +++ b/pandas/core/indexes/category.py @@ -133,7 +133,7 @@ def _create_from_codes(self, codes, categories=None, ordered=None, if name is None: name = self.name cat = Categorical.from_codes(codes, categories=categories, - ordered=self.ordered) + ordered=ordered) return CategoricalIndex(cat, name=name) @classmethod diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 246bd3d541b729..0b467760d82d92 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -939,7 +939,6 @@ def _format_data(self, name=None): summary = '[{head} ... {tail}]'.format( head=', '.join(head), tail=', '.join(tail)) else: - head = [] tail = [formatter(x) for x in self] summary = '[{tail}]'.format(tail=', '.join(tail)) diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index ffa2267dd68778..0f3ffb8055330b 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -1248,7 +1248,7 @@ def take_nd(self, indexer, axis, new_mgr_locs=None, fill_tuple=None): if fill_tuple is None: fill_value = self.fill_value new_values = algos.take_nd(values, indexer, axis=axis, - allow_fill=False) + allow_fill=False, fill_value=fill_value) else: fill_value = fill_tuple[0] new_values = algos.take_nd(values, indexer, axis=axis, @@ -2699,7 +2699,6 @@ def _try_coerce_args(self, values, other): values_mask = isna(values) values = values.view('i8') - other_mask = False if isinstance(other, bool): raise TypeError @@ -2872,11 +2871,9 @@ def _try_coerce_args(self, values, other): values_mask = _block_shape(isna(values), ndim=self.ndim) # asi8 is a view, needs copy values = _block_shape(values.asi8, ndim=self.ndim) - other_mask = False if isinstance(other, ABCSeries): other = self._holder(other) - other_mask = isna(other) if isinstance(other, bool): raise TypeError diff --git a/pandas/core/nanops.py b/pandas/core/nanops.py index 32fd70bcf654de..f44fb4f6e9e144 100644 --- a/pandas/core/nanops.py +++ b/pandas/core/nanops.py @@ -479,7 +479,9 @@ def nanvar(values, axis=None, skipna=True, ddof=1): @disallow('M8', 'm8') def nansem(values, axis=None, skipna=True, ddof=1): - var = nanvar(values, axis, skipna, ddof=ddof) + # This checks if non-numeric-like data is passed with numeric_only=False + # and raises a TypeError otherwise + nanvar(values, axis, skipna, ddof=ddof) mask = isna(values) if not is_float_dtype(values.dtype): @@ -635,7 +637,6 @@ def nankurt(values, axis=None, skipna=True): adj = 3 * (count - 1) ** 2 / ((count - 2) * (count - 3)) numer = count * (count + 1) * (count - 1) * m4 denom = (count - 2) * (count - 3) * m2**2 - result = numer / denom - adj # floating point error # diff --git a/pandas/core/series.py b/pandas/core/series.py index 08b77c505463ea..8f9fe5ee516e69 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -2052,7 +2052,6 @@ def dot(self, other): lvals = left.values rvals = right.values else: - left = self lvals = self.values rvals = np.asarray(other) if lvals.shape[0] != rvals.shape[0]: @@ -2480,7 +2479,8 @@ def sort_values(self, axis=0, ascending=True, inplace=False, dtype: object """ inplace = validate_bool_kwarg(inplace, 'inplace') - axis = self._get_axis_number(axis) + # Validate the axis parameter + self._get_axis_number(axis) # GH 5856/5853 if inplace and self._is_cached: @@ -2652,7 +2652,8 @@ def sort_index(self, axis=0, level=None, ascending=True, inplace=False, # TODO: this can be combined with DataFrame.sort_index impl as # almost identical inplace = validate_bool_kwarg(inplace, 'inplace') - axis = self._get_axis_number(axis) + # Validate the axis parameter + self._get_axis_number(axis) index = self.index if level is not None: @@ -3073,7 +3074,8 @@ def _gotitem(self, key, ndim, subset=None): versionadded='.. versionadded:: 0.20.0', **_shared_doc_kwargs)) def aggregate(self, func, axis=0, *args, **kwargs): - axis = self._get_axis_number(axis) + # Validate the axis parameter + self._get_axis_number(axis) result, how = self._aggregate(func, *args, **kwargs) if result is None: @@ -3919,8 +3921,8 @@ def dropna(self, axis=0, inplace=False, **kwargs): if kwargs: raise TypeError('dropna() got an unexpected keyword ' 'argument "{0}"'.format(list(kwargs.keys())[0])) - - axis = self._get_axis_number(axis or 0) + # Validate the axis parameter + self._get_axis_number(axis or 0) if self._can_hold_na: result = remove_na_arraylike(self) diff --git a/pandas/core/sparse/frame.py b/pandas/core/sparse/frame.py index 5cb9f4744cc58a..58e3001bcfe6af 100644 --- a/pandas/core/sparse/frame.py +++ b/pandas/core/sparse/frame.py @@ -597,7 +597,6 @@ def _combine_match_index(self, other, func, level=None): new_data[col] = func(series.values, other.values) # fill_value is a function of our operator - fill_value = None if isna(other.fill_value) or isna(self.default_fill_value): fill_value = np.nan else: diff --git a/pandas/core/window.py b/pandas/core/window.py index f3b4aaa74ec6bc..eed0e97f30dc9a 100644 --- a/pandas/core/window.py +++ b/pandas/core/window.py @@ -933,7 +933,8 @@ class _Rolling_and_Expanding(_Rolling): def count(self): blocks, obj, index = self._create_blocks() - index, indexi = self._get_index(index=index) + # Validate the index + self._get_index(index=index) window = self._get_window() window = min(window, len(obj)) if not self.center else window diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index f69e4a484d1771..c6ca59aa08bf91 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -495,8 +495,6 @@ def _chk_truncate(self): frame.iloc[:, -col_num:]), axis=1) self.tr_col_num = col_num if truncate_v: - if max_rows_adj == 0: - row_num = len(frame) if max_rows_adj == 1: row_num = max_rows frame = frame.iloc[:max_rows, :] diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py index 20be903f549678..3ea5cb95b9c5a3 100644 --- a/pandas/io/formats/html.py +++ b/pandas/io/formats/html.py @@ -222,7 +222,6 @@ def _column_header(): return row self.write('', indent) - row = [] indent += self.indent_delta diff --git a/pandas/io/formats/terminal.py b/pandas/io/formats/terminal.py index 52262ea05bf963..dcd6f2cf4a718f 100644 --- a/pandas/io/formats/terminal.py +++ b/pandas/io/formats/terminal.py @@ -67,7 +67,7 @@ def is_terminal(): def _get_terminal_size_windows(): - res = None + try: from ctypes import windll, create_string_buffer diff --git a/pandas/io/json/json.py b/pandas/io/json/json.py index 3ec5e8d9be9554..629e00ebfa7d01 100644 --- a/pandas/io/json/json.py +++ b/pandas/io/json/json.py @@ -547,7 +547,7 @@ def _get_object_parser(self, json): if typ == 'series' or obj is None: if not isinstance(dtype, bool): - dtype = dict(data=dtype) + kwargs['dtype'] = dtype obj = SeriesParser(json, **kwargs).parse() return obj diff --git a/pandas/io/sas/sas_xport.py b/pandas/io/sas/sas_xport.py index 52b25898fc67eb..14e7ad9682db6e 100644 --- a/pandas/io/sas/sas_xport.py +++ b/pandas/io/sas/sas_xport.py @@ -181,10 +181,6 @@ def _parse_float_vec(vec): # number sans exponent ieee1 = xport1 & 0x00ffffff - # Get the second half of the ibm number into the second half of - # the ieee number - ieee2 = xport2 - # The fraction bit to the left of the binary point in the ieee # format was set and the number was shifted 0, 1, 2, or 3 # places. This will tell us how to adjust the ibm exponent to be a diff --git a/pandas/plotting/_timeseries.py b/pandas/plotting/_timeseries.py index 0522d7e721b653..96e7532747c78f 100644 --- a/pandas/plotting/_timeseries.py +++ b/pandas/plotting/_timeseries.py @@ -86,7 +86,6 @@ def _maybe_resample(series, ax, kwargs): freq = ax_freq elif frequencies.is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq): _upsample_others(ax, freq, kwargs) - ax_freq = freq else: # pragma: no cover raise ValueError('Incompatible frequency conversion') return freq, series diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index bcbac4400c953a..d6e7c644cc7806 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -642,6 +642,13 @@ def test_series_from_json_precise_float(self): result = read_json(s.to_json(), typ='series', precise_float=True) assert_series_equal(result, s, check_index_type=False) + def test_series_with_dtype(self): + # GH 21986 + s = Series([4.56, 4.56, 4.56]) + result = read_json(s.to_json(), typ='series', dtype=np.int64) + expected = Series([4] * 3) + assert_series_equal(result, expected) + def test_frame_from_json_precise_float(self): df = DataFrame([[4.56, 4.56, 4.56], [4.56, 4.56, 4.56]]) result = read_json(df.to_json(), precise_float=True) From ff1fa4e55c90bb1ec3dc03987f989791eabda7d8 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Sun, 29 Jul 2018 08:33:42 -0700 Subject: [PATCH 05/47] CLN/STYLE: Lint comprehensions (#22075) --- ci/environment-dev.yaml | 1 + ci/lint.sh | 25 ++++------------ ci/travis-27.yaml | 1 + pandas/core/arrays/interval.py | 2 +- pandas/core/common.py | 3 +- pandas/core/dtypes/common.py | 6 ++-- pandas/core/generic.py | 2 +- pandas/core/groupby/base.py | 3 +- pandas/core/indexes/api.py | 4 +-- pandas/core/indexes/multi.py | 4 +-- pandas/core/internals/concat.py | 2 +- pandas/core/internals/managers.py | 8 ++--- pandas/core/panel.py | 13 ++++----- pandas/io/json/normalize.py | 4 +-- pandas/io/parsers.py | 3 +- pandas/tests/api/test_api.py | 2 +- pandas/tests/extension/json/array.py | 4 +-- pandas/tests/frame/test_apply.py | 8 ++--- pandas/tests/frame/test_dtypes.py | 8 ++--- pandas/tests/frame/test_indexing.py | 10 +++---- pandas/tests/groupby/test_groupby.py | 4 +-- pandas/tests/indexes/multi/test_copy.py | 2 +- pandas/tests/io/formats/test_style.py | 39 ++++++++++++------------- pandas/tests/io/json/test_pandas.py | 4 +-- pandas/tests/io/parser/test_network.py | 2 +- pandas/tests/io/test_pytables.py | 6 ++-- pandas/tests/plotting/test_frame.py | 8 ++--- pandas/tests/reshape/test_concat.py | 7 ++--- pandas/tests/test_window.py | 4 +-- pandas/tseries/offsets.py | 4 +-- scripts/find_commits_touching_func.py | 2 +- 31 files changed, 88 insertions(+), 107 deletions(-) diff --git a/ci/environment-dev.yaml b/ci/environment-dev.yaml index 797506547b7734..8d516a6214f95d 100644 --- a/ci/environment-dev.yaml +++ b/ci/environment-dev.yaml @@ -6,6 +6,7 @@ dependencies: - Cython>=0.28.2 - NumPy - flake8 + - flake8-comprehensions - moto - pytest>=3.1 - python-dateutil>=2.5.0 diff --git a/ci/lint.sh b/ci/lint.sh index 9bcee55e1344c3..9fc283c04f09eb 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -10,42 +10,42 @@ if [ "$LINT" ]; then # pandas/_libs/src is C code, so no need to search there. echo "Linting *.py" - flake8 pandas --filename=*.py --exclude pandas/_libs/src + flake8 pandas --filename=*.py --exclude pandas/_libs/src --ignore=C405,C406,C408,C409,C410,E402,E731,E741,W503 if [ $? -ne "0" ]; then RET=1 fi echo "Linting *.py DONE" echo "Linting setup.py" - flake8 setup.py + flake8 setup.py --ignore=C405,C406,C408,C409,C410,E402,E731,E741,W503 if [ $? -ne "0" ]; then RET=1 fi echo "Linting setup.py DONE" echo "Linting asv_bench/benchmarks/" - flake8 asv_bench/benchmarks/ --exclude=asv_bench/benchmarks/*.py --ignore=F811 + flake8 asv_bench/benchmarks/ --exclude=asv_bench/benchmarks/*.py --ignore=F811,C405,C406,C408,C409,C410 if [ $? -ne "0" ]; then RET=1 fi echo "Linting asv_bench/benchmarks/*.py DONE" echo "Linting scripts/*.py" - flake8 scripts --filename=*.py + flake8 scripts --filename=*.py --ignore=C405,C406,C408,C409,C410,E402,E731,E741,W503 if [ $? -ne "0" ]; then RET=1 fi echo "Linting scripts/*.py DONE" echo "Linting doc scripts" - flake8 doc/make.py doc/source/conf.py + flake8 doc/make.py doc/source/conf.py --ignore=C405,C406,C408,C409,C410,E402,E731,E741,W503 if [ $? -ne "0" ]; then RET=1 fi echo "Linting doc scripts DONE" echo "Linting *.pyx" - flake8 pandas --filename=*.pyx --select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126,E265,E305,E301,E127,E261,E271,E129,W291,E222,E241,E123,F403 + flake8 pandas --filename=*.pyx --select=E501,E302,E203,E111,E114,E221,E303,E128,E231,E126,E265,E305,E301,E127,E261,E271,E129,W291,E222,E241,E123,F403,C400,C401,C402,C403,C404,C407,C411 if [ $? -ne "0" ]; then RET=1 fi @@ -131,19 +131,6 @@ if [ "$LINT" ]; then fi echo "Check for non-standard imports DONE" - echo "Check for use of lists instead of generators in built-in Python functions" - - # Example: Avoid `any([i for i in some_iterator])` in favor of `any(i for i in some_iterator)` - # - # Check the following functions: - # any(), all(), sum(), max(), min(), list(), dict(), set(), frozenset(), tuple(), str.join() - grep -R --include="*.py*" -E "[^_](any|all|sum|max|min|list|dict|set|frozenset|tuple|join)\(\[.* for .* in .*\]\)" pandas - - if [ $? = "0" ]; then - RET=1 - fi - echo "Check for use of lists instead of generators in built-in Python functions DONE" - echo "Check for incorrect sphinx directives" SPHINX_DIRECTIVES=$(echo \ "autosummary|contents|currentmodule|deprecated|function|image|"\ diff --git a/ci/travis-27.yaml b/ci/travis-27.yaml index 9cb20734dc63d9..3e94f334174e63 100644 --- a/ci/travis-27.yaml +++ b/ci/travis-27.yaml @@ -9,6 +9,7 @@ dependencies: - fastparquet - feather-format - flake8=3.4.1 + - flake8-comprehensions - gcsfs - html5lib - ipython diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index ad01d4ec9b3caa..928483005786a9 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -600,7 +600,7 @@ def _concat_same_type(cls, to_concat): ------- IntervalArray """ - closed = set(interval.closed for interval in to_concat) + closed = {interval.closed for interval in to_concat} if len(closed) != 1: raise ValueError("Intervals must all be closed on the same side.") closed = closed.pop() diff --git a/pandas/core/common.py b/pandas/core/common.py index 0350b338f2beee..a3fba762509f15 100644 --- a/pandas/core/common.py +++ b/pandas/core/common.py @@ -307,8 +307,7 @@ def dict_compat(d): dict """ - return dict((maybe_box_datetimelike(key), value) - for key, value in iteritems(d)) + return {maybe_box_datetimelike(key): value for key, value in iteritems(d)} def standardize_mapping(into): diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 355bf585402195..905073645fcb39 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -21,9 +21,9 @@ is_named_tuple, is_array_like, is_decimal, is_complex, is_interval) -_POSSIBLY_CAST_DTYPES = set([np.dtype(t).name - for t in ['O', 'int8', 'uint8', 'int16', 'uint16', - 'int32', 'uint32', 'int64', 'uint64']]) +_POSSIBLY_CAST_DTYPES = {np.dtype(t).name + for t in ['O', 'int8', 'uint8', 'int16', 'uint16', + 'int32', 'uint32', 'int64', 'uint64']} _NS_DTYPE = conversion.NS_DTYPE _TD_DTYPE = conversion.TD_DTYPE diff --git a/pandas/core/generic.py b/pandas/core/generic.py index edf341ae2898f1..7a12ce0e1385e3 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -8840,7 +8840,7 @@ def describe_1d(data): ldesc = [describe_1d(s) for _, s in data.iteritems()] # set a convenient order for rows names = [] - ldesc_indexes = sorted([x.index for x in ldesc], key=len) + ldesc_indexes = sorted((x.index for x in ldesc), key=len) for idxnames in ldesc_indexes: for name in idxnames: if name not in names: diff --git a/pandas/core/groupby/base.py b/pandas/core/groupby/base.py index b2c5a8cff9c1b5..96c74f7fd4d75a 100644 --- a/pandas/core/groupby/base.py +++ b/pandas/core/groupby/base.py @@ -43,8 +43,7 @@ def _gotitem(self, key, ndim, subset=None): # we need to make a shallow copy of ourselves # with the same groupby - kwargs = dict([(attr, getattr(self, attr)) - for attr in self._attributes]) + kwargs = {attr: getattr(self, attr) for attr in self._attributes} self = self.__class__(subset, groupby=self._groupby[key], parent=self, diff --git a/pandas/core/indexes/api.py b/pandas/core/indexes/api.py index 3f3448d1041658..e50a4b099a8e1f 100644 --- a/pandas/core/indexes/api.py +++ b/pandas/core/indexes/api.py @@ -147,8 +147,8 @@ def _get_consensus_names(indexes): # find the non-none names, need to tupleify to make # the set hashable, then reverse on return - consensus_names = set(tuple(i.names) for i in indexes - if com._any_not_none(*i.names)) + consensus_names = {tuple(i.names) for i in indexes + if com._any_not_none(*i.names)} if len(consensus_names) == 1: return list(list(consensus_names)[0]) return [None] * indexes[0].nlevels diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py index 4912014b43773d..2a97c37449e120 100644 --- a/pandas/core/indexes/multi.py +++ b/pandas/core/indexes/multi.py @@ -903,8 +903,8 @@ def f(k, stringify): if stringify and not isinstance(k, compat.string_types): k = str(k) return k - key = tuple([f(k, stringify) - for k, stringify in zip(key, self._have_mixed_levels)]) + key = tuple(f(k, stringify) + for k, stringify in zip(key, self._have_mixed_levels)) return hash_tuple(key) @Appender(Index.duplicated.__doc__) diff --git a/pandas/core/internals/concat.py b/pandas/core/internals/concat.py index 4eeeb069d71429..5a3f11525acf84 100644 --- a/pandas/core/internals/concat.py +++ b/pandas/core/internals/concat.py @@ -378,7 +378,7 @@ def is_uniform_reindex(join_units): return ( # TODO: should this be ju.block._can_hold_na? all(ju.block and ju.block.is_extension for ju in join_units) and - len(set(ju.block.dtype.name for ju in join_units)) == 1 + len({ju.block.dtype.name for ju in join_units}) == 1 ) diff --git a/pandas/core/internals/managers.py b/pandas/core/internals/managers.py index e7b7cb463a27b9..32e8372d5c6c9c 100644 --- a/pandas/core/internals/managers.py +++ b/pandas/core/internals/managers.py @@ -398,10 +398,10 @@ def apply(self, f, axes=None, filter=None, do_integrity_check=False, # TODO(EA): may interfere with ExtensionBlock.setitem for blocks # with a .values attribute. - aligned_args = dict((k, kwargs[k]) - for k in align_keys - if hasattr(kwargs[k], 'values') and - not isinstance(kwargs[k], ABCExtensionArray)) + aligned_args = {k: kwargs[k] + for k in align_keys + if hasattr(kwargs[k], 'values') and + not isinstance(kwargs[k], ABCExtensionArray)} for b in self.blocks: if filter is not None: diff --git a/pandas/core/panel.py b/pandas/core/panel.py index 4ebac55eea1371..38b84ab685c3bf 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -1429,10 +1429,8 @@ def _extract_axes(self, data, axes, **kwargs): @staticmethod def _extract_axes_for_slice(self, axes): """ return the slice dictionary for these axes """ - return dict((self._AXIS_SLICEMAP[i], a) - for i, a in zip( - self._AXIS_ORDERS[self._AXIS_LEN - len(axes):], - axes)) + return {self._AXIS_SLICEMAP[i]: a for i, a in + zip(self._AXIS_ORDERS[self._AXIS_LEN - len(axes):], axes)} @staticmethod def _prep_ndarray(self, values, copy=True): @@ -1480,11 +1478,10 @@ def _homogenize_dict(self, frames, intersect=True, dtype=None): adj_frames[k] = v axes = self._AXIS_ORDERS[1:] - axes_dict = dict((a, ax) for a, ax in zip(axes, self._extract_axes( - self, adj_frames, axes, intersect=intersect))) + axes_dict = {a: ax for a, ax in zip(axes, self._extract_axes( + self, adj_frames, axes, intersect=intersect))} - reindex_dict = dict( - [(self._AXIS_SLICEMAP[a], axes_dict[a]) for a in axes]) + reindex_dict = {self._AXIS_SLICEMAP[a]: axes_dict[a] for a in axes} reindex_dict['copy'] = False for key, frame in compat.iteritems(adj_frames): if frame is not None: diff --git a/pandas/io/json/normalize.py b/pandas/io/json/normalize.py index 2004a24c2ec5a8..03f0905d2023aa 100644 --- a/pandas/io/json/normalize.py +++ b/pandas/io/json/normalize.py @@ -194,8 +194,8 @@ def _pull_field(js, spec): data = [data] if record_path is None: - if any([[isinstance(x, dict) - for x in compat.itervalues(y)] for y in data]): + if any([isinstance(x, dict) + for x in compat.itervalues(y)] for y in data): # naive normalization, this is idempotent for flat records # and potentially will inflate the data considerably for # deeply nested structures: diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py index 2ae7622c135481..88358ff392cb65 100755 --- a/pandas/io/parsers.py +++ b/pandas/io/parsers.py @@ -3147,8 +3147,7 @@ def _clean_na_values(na_values, keep_default_na=True): v = set(v) | _NA_VALUES na_values[k] = v - na_fvalues = dict((k, _floatify_na_values(v)) - for k, v in na_values.items()) + na_fvalues = {k: _floatify_na_values(v) for k, v in na_values.items()} else: if not is_list_like(na_values): na_values = [na_values] diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index ddee4894456ea7..2aa875d1e095a6 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -15,7 +15,7 @@ def check(self, namespace, expected, ignored=None): # ignored ones # compare vs the expected - result = sorted([f for f in dir(namespace) if not f.startswith('_')]) + result = sorted(f for f in dir(namespace) if not f.startswith('_')) if ignored is not None: result = sorted(list(set(result) - set(ignored))) diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index 34c397252a8bb1..980c245d557118 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -160,7 +160,7 @@ def unique(self): # Parent method doesn't work since np.array will try to infer # a 2-dim object. return type(self)([ - dict(x) for x in list(set(tuple(d.items()) for d in self.data)) + dict(x) for x in list({tuple(d.items()) for d in self.data}) ]) @classmethod @@ -176,5 +176,5 @@ def _values_for_argsort(self): # Disable NumPy's shape inference by including an empty tuple... # If all the elemnts of self are the same size P, NumPy will # cast them to an (N, P) array, instead of an (N,) array of tuples. - frozen = [()] + list(tuple(x.items()) for x in self) + frozen = [()] + [tuple(x.items()) for x in self] return np.array(frozen, dtype=object)[1:] diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index e038588b76ffd4..344838493f0b1c 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -319,14 +319,14 @@ def test_apply_differently_indexed(self): df = DataFrame(np.random.randn(20, 10)) result0 = df.apply(Series.describe, axis=0) - expected0 = DataFrame(dict((i, v.describe()) - for i, v in compat.iteritems(df)), + expected0 = DataFrame({i: v.describe() + for i, v in compat.iteritems(df)}, columns=df.columns) assert_frame_equal(result0, expected0) result1 = df.apply(Series.describe, axis=1) - expected1 = DataFrame(dict((i, v.describe()) - for i, v in compat.iteritems(df.T)), + expected1 = DataFrame({i: v.describe() + for i, v in compat.iteritems(df.T)}, columns=df.index).T assert_frame_equal(result1, expected1) diff --git a/pandas/tests/frame/test_dtypes.py b/pandas/tests/frame/test_dtypes.py index 30a670ead3aa02..3b3ab3d03dce9b 100644 --- a/pandas/tests/frame/test_dtypes.py +++ b/pandas/tests/frame/test_dtypes.py @@ -397,8 +397,8 @@ def test_select_dtypes_typecodes(self): def test_dtypes_gh8722(self): self.mixed_frame['bool'] = self.mixed_frame['A'] > 0 result = self.mixed_frame.dtypes - expected = Series(dict((k, v.dtype) - for k, v in compat.iteritems(self.mixed_frame)), + expected = Series({k: v.dtype + for k, v in compat.iteritems(self.mixed_frame)}, index=result.index) assert_series_equal(result, expected) @@ -439,8 +439,8 @@ def test_astype(self): # mixed casting def _check_cast(df, v): - assert (list(set(s.dtype.name for - _, s in compat.iteritems(df)))[0] == v) + assert (list({s.dtype.name for + _, s in compat.iteritems(df)})[0] == v) mn = self.all_mixed._get_numeric_data().copy() mn['little_float'] = np.array(12345., dtype='float16') diff --git a/pandas/tests/frame/test_indexing.py b/pandas/tests/frame/test_indexing.py index 3e5c13208f164c..5f229aca5c25b8 100644 --- a/pandas/tests/frame/test_indexing.py +++ b/pandas/tests/frame/test_indexing.py @@ -276,8 +276,8 @@ def test_getitem_boolean(self): data = df._get_numeric_data() bif = df[df > 0] - bifw = DataFrame(dict((c, np.where(data[c] > 0, data[c], np.nan)) - for c in data.columns), + bifw = DataFrame({c: np.where(data[c] > 0, data[c], np.nan) + for c in data.columns}, index=data.index, columns=data.columns) # add back other columns to compare @@ -2506,9 +2506,9 @@ def _check_get(df, cond, check_dtypes=True): _check_get(df, cond) # upcasting case (GH # 2794) - df = DataFrame(dict((c, Series([1] * 3, dtype=c)) - for c in ['float32', 'float64', - 'int32', 'int64'])) + df = DataFrame({c: Series([1] * 3, dtype=c) + for c in ['float32', 'float64', + 'int32', 'int64']}) df.iloc[1, :] = 0 result = df.where(df >= 0).get_dtype_counts() diff --git a/pandas/tests/groupby/test_groupby.py b/pandas/tests/groupby/test_groupby.py index 8b2b74802556d4..9affd0241d028d 100644 --- a/pandas/tests/groupby/test_groupby.py +++ b/pandas/tests/groupby/test_groupby.py @@ -519,8 +519,8 @@ def test_groupby_multiple_columns(df, op): for n1, gp1 in data.groupby('A'): for n2, gp2 in gp1.groupby('B'): expected[n1][n2] = op(gp2.loc[:, ['C', 'D']]) - expected = dict((k, DataFrame(v)) - for k, v in compat.iteritems(expected)) + expected = {k: DataFrame(v) + for k, v in compat.iteritems(expected)} expected = Panel.fromDict(expected).swapaxes(0, 1) expected.major_axis.name, expected.minor_axis.name = 'A', 'B' diff --git a/pandas/tests/indexes/multi/test_copy.py b/pandas/tests/indexes/multi/test_copy.py index f6c5c0c5eb3469..786b90e8f13a23 100644 --- a/pandas/tests/indexes/multi/test_copy.py +++ b/pandas/tests/indexes/multi/test_copy.py @@ -83,4 +83,4 @@ def test_copy_method_kwargs(deep, kwarg, value): if kwarg == 'names': assert getattr(idx_copy, kwarg) == value else: - assert list(list(i) for i in getattr(idx_copy, kwarg)) == value + assert [list(i) for i in getattr(idx_copy, kwarg)] == value diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 293dadd19031d7..bcfd3cbb739ff9 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -250,11 +250,11 @@ def test_apply_subset(self): for slice_ in slices: result = self.df.style.apply(self.h, axis=ax, subset=slice_, foo='baz')._compute().ctx - expected = dict(((r, c), ['color: baz']) - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index and - col in self.df.loc[slice_].columns) + expected = {(r, c): ['color: baz'] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and + col in self.df.loc[slice_].columns} assert result == expected def test_applymap_subset(self): @@ -267,11 +267,11 @@ def f(x): for slice_ in slices: result = self.df.style.applymap(f, subset=slice_)._compute().ctx - expected = dict(((r, c), ['foo: bar']) - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index and - col in self.df.loc[slice_].columns) + expected = {(r, c): ['foo: bar'] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and + col in self.df.loc[slice_].columns} assert result == expected def test_where_with_one_style(self): @@ -282,10 +282,9 @@ def f(x): style1 = 'foo: bar' result = self.df.style.where(f, style1)._compute().ctx - expected = dict(((r, c), - [style1 if f(self.df.loc[row, col]) else '']) - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns)) + expected = {(r, c): [style1 if f(self.df.loc[row, col]) else ''] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns)} assert result == expected def test_where_subset(self): @@ -303,12 +302,12 @@ def f(x): for slice_ in slices: result = self.df.style.where(f, style1, style2, subset=slice_)._compute().ctx - expected = dict(((r, c), - [style1 if f(self.df.loc[row, col]) else style2]) - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index and - col in self.df.loc[slice_].columns) + expected = {(r, c): + [style1 if f(self.df.loc[row, col]) else style2] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and + col in self.df.loc[slice_].columns} assert result == expected def test_where_subset_compare_with_applymap(self): diff --git a/pandas/tests/io/json/test_pandas.py b/pandas/tests/io/json/test_pandas.py index d6e7c644cc7806..0715521a748198 100644 --- a/pandas/tests/io/json/test_pandas.py +++ b/pandas/tests/io/json/test_pandas.py @@ -21,8 +21,8 @@ _frame = DataFrame(_seriesd) _frame2 = DataFrame(_seriesd, columns=['D', 'C', 'B', 'A']) -_intframe = DataFrame(dict((k, v.astype(np.int64)) - for k, v in compat.iteritems(_seriesd))) +_intframe = DataFrame({k: v.astype(np.int64) + for k, v in compat.iteritems(_seriesd)}) _tsframe = DataFrame(_tsd) _cat_frame = _frame.copy() diff --git a/pandas/tests/io/parser/test_network.py b/pandas/tests/io/parser/test_network.py index e2243b8087a5b3..f6a31008bca5c3 100644 --- a/pandas/tests/io/parser/test_network.py +++ b/pandas/tests/io/parser/test_network.py @@ -197,4 +197,4 @@ def test_read_csv_chunked_download(self, s3_resource, caplog): with caplog.at_level(logging.DEBUG, logger='s3fs.core'): read_csv("s3://pandas-test/large-file.csv", nrows=5) # log of fetch_range (start, stop) - assert ((0, 5505024) in set(x.args[-2:] for x in caplog.records)) + assert ((0, 5505024) in {x.args[-2:] for x in caplog.records}) diff --git a/pandas/tests/io/test_pytables.py b/pandas/tests/io/test_pytables.py index 9b624ab78a406b..db8306d6dcb771 100644 --- a/pandas/tests/io/test_pytables.py +++ b/pandas/tests/io/test_pytables.py @@ -2104,9 +2104,9 @@ def test_table_values_dtypes_roundtrip(self): assert df1.dtypes[0] == 'float32' # check with mixed dtypes - df1 = DataFrame(dict((c, Series(np.random.randint(5), dtype=c)) - for c in ['float32', 'float64', 'int32', - 'int64', 'int16', 'int8'])) + df1 = DataFrame({c: Series(np.random.randint(5), dtype=c) + for c in ['float32', 'float64', 'int32', + 'int64', 'int16', 'int8']}) df1['string'] = 'foo' df1['float322'] = 1. df1['float322'] = df1['float322'].astype('float32') diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index f1ea847e760913..db10ea15f6e9c8 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -1126,10 +1126,10 @@ def test_if_hexbin_xaxis_label_is_visible(self): columns=['A label', 'B label', 'C label']) ax = df.plot.hexbin('A label', 'B label', gridsize=12) - assert all([vis.get_visible() for vis in - ax.xaxis.get_minorticklabels()]) - assert all([vis.get_visible() for vis in - ax.xaxis.get_majorticklabels()]) + assert all(vis.get_visible() for vis in + ax.xaxis.get_minorticklabels()) + assert all(vis.get_visible() for vis in + ax.xaxis.get_majorticklabels()) assert ax.xaxis.get_label().get_visible() @pytest.mark.slow diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py index d05fd689ed7544..a59836eb70d24a 100644 --- a/pandas/tests/reshape/test_concat.py +++ b/pandas/tests/reshape/test_concat.py @@ -1542,14 +1542,13 @@ def df(): return DataFrame(np.random.randn(index, cols), index=["I%s" % i for i in range(index)], columns=["C%s" % i for i in range(cols)]) - return Panel(dict(("Item%s" % x, df()) - for x in ['A', 'B', 'C'])) + return Panel({"Item%s" % x: df() for x in ['A', 'B', 'C']}) panel1 = make_panel() panel2 = make_panel() - panel2 = panel2.rename_axis(dict((x, "%s_1" % x) - for x in panel2.major_axis), + panel2 = panel2.rename_axis({x: "%s_1" % x + for x in panel2.major_axis}, axis=1) panel3 = panel2.rename_axis(lambda x: '%s_1' % x, axis=1) diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index 14966177978f45..397da2fa40cd85 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -2521,8 +2521,8 @@ def test_flex_binary_frame(self, method): frame2.values[:] = np.random.randn(*frame2.shape) res3 = getattr(self.frame.rolling(window=10), method)(frame2) - exp = DataFrame(dict((k, getattr(self.frame[k].rolling( - window=10), method)(frame2[k])) for k in self.frame)) + exp = DataFrame({k: getattr(self.frame[k].rolling( + window=10), method)(frame2[k]) for k in self.frame}) tm.assert_frame_equal(res3, exp) def test_ewmcov(self): diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index dd4356aac1cd59..60981f41ec716f 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2375,7 +2375,7 @@ def generate_range(start=None, end=None, periods=None, cur = next_date -prefix_mapping = dict((offset._prefix, offset) for offset in [ +prefix_mapping = {offset._prefix: offset for offset in [ YearBegin, # 'AS' YearEnd, # 'A' BYearBegin, # 'BAS' @@ -2407,4 +2407,4 @@ def generate_range(start=None, end=None, periods=None, WeekOfMonth, # 'WOM' FY5253, FY5253Quarter, -]) +]} diff --git a/scripts/find_commits_touching_func.py b/scripts/find_commits_touching_func.py index 29eb4161718fff..8f0c554b8aa9d0 100755 --- a/scripts/find_commits_touching_func.py +++ b/scripts/find_commits_touching_func.py @@ -91,7 +91,7 @@ def get_hits(defname, files=()): # remove comment lines lines = [x for x in lines if not re.search("^\w+\s*\(.+\)\s*#", x)] hits = set(map(lambda x: x.split(" ")[0], lines)) - cs.update(set(Hit(commit=c, path=f) for c in hits)) + cs.update({Hit(commit=c, path=f) for c in hits}) return cs From d30c4a0696d5fbdc3c7ce36a9b9b19224a557e09 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sun, 29 Jul 2018 09:04:57 -0700 Subject: [PATCH 06/47] [POC] implement test_arithmetic.py (#22033) --- pandas/tests/indexes/test_numeric.py | 38 +------- pandas/tests/series/test_arithmetic.py | 19 ---- pandas/tests/test_arithmetic.py | 119 +++++++++++++++++++++++++ pandas/util/testing.py | 44 +++++++++ 4 files changed, 164 insertions(+), 56 deletions(-) create mode 100644 pandas/tests/test_arithmetic.py diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py index a323e2487e356f..71b2774a926124 100644 --- a/pandas/tests/indexes/test_numeric.py +++ b/pandas/tests/indexes/test_numeric.py @@ -13,7 +13,7 @@ import pandas.util.testing as tm import pandas as pd -from pandas._libs.tslibs import Timestamp, Timedelta +from pandas._libs.tslibs import Timestamp from pandas.tests.indexes.common import Base @@ -26,42 +26,6 @@ def full_like(array, value): return ret -class TestIndexArithmeticWithTimedeltaScalar(object): - - @pytest.mark.parametrize('index', [ - Int64Index(range(1, 11)), - UInt64Index(range(1, 11)), - Float64Index(range(1, 11)), - RangeIndex(1, 11)]) - @pytest.mark.parametrize('scalar_td', [Timedelta(days=1), - Timedelta(days=1).to_timedelta64(), - Timedelta(days=1).to_pytimedelta()]) - def test_index_mul_timedelta(self, scalar_td, index): - # GH#19333 - expected = pd.timedelta_range('1 days', '10 days') - - result = index * scalar_td - tm.assert_index_equal(result, expected) - commute = scalar_td * index - tm.assert_index_equal(commute, expected) - - @pytest.mark.parametrize('index', [Int64Index(range(1, 3)), - UInt64Index(range(1, 3)), - Float64Index(range(1, 3)), - RangeIndex(1, 3)]) - @pytest.mark.parametrize('scalar_td', [Timedelta(days=1), - Timedelta(days=1).to_timedelta64(), - Timedelta(days=1).to_pytimedelta()]) - def test_index_rdiv_timedelta(self, scalar_td, index): - expected = pd.TimedeltaIndex(['1 Day', '12 Hours']) - - result = scalar_td / index - tm.assert_index_equal(result, expected) - - with pytest.raises(TypeError): - index / scalar_td - - class Numeric(Base): def test_can_hold_identifiers(self): diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index bf2308cd8c0970..2571498ca802ce 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -891,22 +891,3 @@ def test_td64series_mul_timedeltalike_invalid(self, scalar_td): td1 * scalar_td with tm.assert_raises_regex(TypeError, pattern): scalar_td * td1 - - -class TestTimedeltaSeriesInvalidArithmeticOps(object): - @pytest.mark.parametrize('scalar_td', [ - timedelta(minutes=5, seconds=4), - Timedelta('5m4s'), - Timedelta('5m4s').to_timedelta64()]) - def test_td64series_pow_invalid(self, scalar_td): - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - - # check that we are getting a TypeError - # with 'operate' (from core/ops.py) for the ops that are not - # defined - pattern = 'operate|unsupported|cannot|not supported' - with tm.assert_raises_regex(TypeError, pattern): - scalar_td ** td1 - with tm.assert_raises_regex(TypeError, pattern): - td1 ** scalar_td diff --git a/pandas/tests/test_arithmetic.py b/pandas/tests/test_arithmetic.py new file mode 100644 index 00000000000000..f15b629f15ae38 --- /dev/null +++ b/pandas/tests/test_arithmetic.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Arithmetc tests for DataFrame/Series/Index/Array classes that should +# behave identically. +from datetime import timedelta + +import pytest +import numpy as np + +import pandas as pd +import pandas.util.testing as tm + +from pandas import Timedelta + + +# ------------------------------------------------------------------ +# Numeric dtypes Arithmetic with Timedelta Scalar + +class TestNumericArraylikeArithmeticWithTimedeltaScalar(object): + + @pytest.mark.parametrize('box', [ + pd.Index, + pd.Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="block.eval incorrect", + strict=True)) + ]) + @pytest.mark.parametrize('index', [ + pd.Int64Index(range(1, 11)), + pd.UInt64Index(range(1, 11)), + pd.Float64Index(range(1, 11)), + pd.RangeIndex(1, 11)], + ids=lambda x: type(x).__name__) + @pytest.mark.parametrize('scalar_td', [ + Timedelta(days=1), + Timedelta(days=1).to_timedelta64(), + Timedelta(days=1).to_pytimedelta()], + ids=lambda x: type(x).__name__) + def test_index_mul_timedelta(self, scalar_td, index, box): + # GH#19333 + + if (box is pd.Series and + type(scalar_td) is timedelta and index.dtype == 'f8'): + raise pytest.xfail(reason="Cannot multiply timedelta by float") + + expected = pd.timedelta_range('1 days', '10 days') + + index = tm.box_expected(index, box) + expected = tm.box_expected(expected, box) + + result = index * scalar_td + tm.assert_equal(result, expected) + + commute = scalar_td * index + tm.assert_equal(commute, expected) + + @pytest.mark.parametrize('box', [pd.Index, pd.Series, pd.DataFrame]) + @pytest.mark.parametrize('index', [ + pd.Int64Index(range(1, 3)), + pd.UInt64Index(range(1, 3)), + pd.Float64Index(range(1, 3)), + pd.RangeIndex(1, 3)], + ids=lambda x: type(x).__name__) + @pytest.mark.parametrize('scalar_td', [ + Timedelta(days=1), + Timedelta(days=1).to_timedelta64(), + Timedelta(days=1).to_pytimedelta()], + ids=lambda x: type(x).__name__) + def test_index_rdiv_timedelta(self, scalar_td, index, box): + + if box is pd.Series and type(scalar_td) is timedelta: + raise pytest.xfail(reason="TODO: Figure out why this case fails") + if box is pd.DataFrame and isinstance(scalar_td, timedelta): + raise pytest.xfail(reason="TODO: Figure out why this case fails") + + expected = pd.TimedeltaIndex(['1 Day', '12 Hours']) + + index = tm.box_expected(index, box) + expected = tm.box_expected(expected, box) + + result = scalar_td / index + tm.assert_equal(result, expected) + + with pytest.raises(TypeError): + index / scalar_td + + +# ------------------------------------------------------------------ +# Timedelta64[ns] dtype Arithmetic Operations + + +class TestTimedeltaArraylikeInvalidArithmeticOps(object): + + @pytest.mark.parametrize('box', [ + pd.Index, + pd.Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="raises ValueError " + "instead of TypeError", + strict=True)) + ]) + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + Timedelta('5m4s'), + Timedelta('5m4s').to_timedelta64()]) + def test_td64series_pow_invalid(self, scalar_td, box): + td1 = pd.Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + + td1 = tm.box_expected(td1, box) + + # check that we are getting a TypeError + # with 'operate' (from core/ops.py) for the ops that are not + # defined + pattern = 'operate|unsupported|cannot|not supported' + with tm.assert_raises_regex(TypeError, pattern): + scalar_td ** td1 + + with tm.assert_raises_regex(TypeError, pattern): + td1 ** scalar_td diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 9697c991122dd0..6dffbcb0b4f010 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -1478,6 +1478,50 @@ def assert_panel_equal(left, right, assert item in left, msg +def assert_equal(left, right, **kwargs): + """ + Wrapper for tm.assert_*_equal to dispatch to the appropriate test function. + + Parameters + ---------- + left : Index, Series, or DataFrame + right : Index, Series, or DataFrame + **kwargs + """ + if isinstance(left, pd.Index): + assert_index_equal(left, right, **kwargs) + elif isinstance(left, pd.Series): + assert_series_equal(left, right, **kwargs) + elif isinstance(left, pd.DataFrame): + assert_frame_equal(left, right, **kwargs) + else: + raise NotImplementedError(type(left)) + + +def box_expected(expected, box_cls): + """ + Helper function to wrap the expected output of a test in a given box_class. + + Parameters + ---------- + expected : np.ndarray, Index, Series + box_cls : {Index, Series, DataFrame} + + Returns + ------- + subclass of box_cls + """ + if box_cls is pd.Index: + expected = pd.Index(expected) + elif box_cls is pd.Series: + expected = pd.Series(expected) + elif box_cls is pd.DataFrame: + expected = pd.Series(expected).to_frame() + else: + raise NotImplementedError(box_cls) + return expected + + # ----------------------------------------------------------------------------- # Sparse From a5e5ca6be07504f67f809410cffac7ad21bd1712 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 30 Jul 2018 03:00:23 -0700 Subject: [PATCH 07/47] Lint configuration followup (#22123) --- ci/lint.sh | 11 +++++++++++ ci/requirements_dev.txt | 3 ++- setup.cfg | 7 ++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ci/lint.sh b/ci/lint.sh index 9fc283c04f09eb..ec99e1e559d6eb 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -8,6 +8,17 @@ RET=0 if [ "$LINT" ]; then + # We're ignoring the following codes across the board + #E402, # module level import not at top of file + #E731, # do not assign a lambda expression, use a def + #E741, # do not use variables named 'l', 'O', or 'I' + #W503, # line break before binary operator + #C405, # Unnecessary (list/tuple) literal - rewrite as a set literal. + #C406, # Unnecessary (list/tuple) literal - rewrite as a dict literal. + #C408, # Unnecessary (dict/list/tuple) call - rewrite as a literal. + #C409, # Unnecessary (list/tuple) passed to tuple() - (remove the outer call to tuple()/rewrite as a tuple literal). + #C410 # Unnecessary (list/tuple) passed to list() - (remove the outer call to list()/rewrite as a list literal). + # pandas/_libs/src is C code, so no need to search there. echo "Linting *.py" flake8 pandas --filename=*.py --exclude pandas/_libs/src --ignore=C405,C406,C408,C409,C410,E402,E731,E741,W503 diff --git a/ci/requirements_dev.txt b/ci/requirements_dev.txt index 83ee30b52071d5..c89aae8f2ffca1 100644 --- a/ci/requirements_dev.txt +++ b/ci/requirements_dev.txt @@ -1,8 +1,9 @@ # This file was autogenerated by scripts/convert_deps.py # Do not modify directly -Cython +Cython>=0.28.2 NumPy flake8 +flake8-comprehensions moto pytest>=3.1 python-dateutil>=2.5.0 diff --git a/setup.cfg b/setup.cfg index 9ec967c25e2255..d00d527da49e20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,12 @@ ignore = E402, # module level import not at top of file E731, # do not assign a lambda expression, use a def E741, # do not use variables named 'l', 'O', or 'I' - W503 # line break before binary operator + W503, # line break before binary operator + C405, # Unnecessary (list/tuple) literal - rewrite as a set literal. + C406, # Unnecessary (list/tuple) literal - rewrite as a dict literal. + C408, # Unnecessary (dict/list/tuple) call - rewrite as a literal. + C409, # Unnecessary (list/tuple) passed to tuple() - (remove the outer call to tuple()/rewrite as a tuple literal). + C410 # Unnecessary (list/tuple) passed to list() - (remove the outer call to list()/rewrite as a list literal). max-line-length = 79 [yapf] From 8ca409e14436bd207ae05bf2a19151f24c712ee6 Mon Sep 17 00:00:00 2001 From: Kang Yoosam Date: Mon, 30 Jul 2018 19:05:23 +0900 Subject: [PATCH 08/47] Add Japanese version of Pandas_Cheat_Sheet (#22127) --- doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf | Bin 0 -> 205542 bytes doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx | Bin 0 -> 105265 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf create mode 100644 doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf b/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pdf new file mode 100644 index 0000000000000000000000000000000000000000..746d1b6c980feddaab21784f383ae474874f2cfb GIT binary patch literal 205542 zcmV)^K!Cp`P((&8F)lL-CB)_O4?5av(28Y+-a|L}g=dWMv>eJ_>Vma%Ev{3U~q4ooln4H*%Q2&#xG}lJGFa zJv;AhE?bgPu^d;DvMWx+s{KI8uETQ?={h8}B&F4sWB*2fi2qNXZUBwT3%v85LrJ^4 ztks#p0F6ciX!H$@-);Z*_IKMI|GI9s+a9Og_WkYlx7)YdUw`?b+g^XzcKYwb^{mp} zc6WI^bi<+F^Xb%g)Bd>M@M(Y8O~>PwCPUXao?o+a@Rrm1C`!v569{9H0*l5+}oFp`*OVO#)JHFIG!#~ zy7j3`r~7#jfO|$BqrNG4$^lX7{HpP(*UKnM;6!w zdH4PC@&vVg!F?<^45!Pk+n*qkYZ3}rvabp#vfGz90MkFZEnWGvVVh6GQAv6jPqHX3 zyWzMSwmmC-+OZH|D0f?!gOo4S+qinC<7I!?cMBCAKp2g%nSK=Jv+GX#-7$VLtPZ=& z;m{o-R^3FwVJwhZsI~!j!)c+Oj8VonjhFDIq1K~-3S?NQCXlShRy|KG>5-}4%TycX z>tk$HE9u^<=SX&L5wgB)RfaV;TZ9zU79sRwSs#vv%U)Je&k}YksdFnyFs&XdtLXfQ3+kM zQEdc8wpB8!1Jk@>RNITc%SN^NWOfl}Y7>}_;416$;CAWiVFfwjcZ^Tj&6ZPT<*@#1 zHmsC?Z63u1G*a_I1N+fSYw z-fxAMhq>hxtu&6c6M%>A@_0lX0QWWoP9M1EZqHfj4ha7>?S9kh-WpqNA+CpTw)+jE zmtIFs38bF`=hLZ| z@5$CjFfD{-v`IDja+X4}K0sm$^T=p_I{_i*6$`cM+3Gt)0ECtbwA z9dz9#Q(|SVFIiB_5(I+ekyW9hw`=l?kV%^bnM zw=XsqfBjTs)a}KU{2jIzAD?c2wPOib!Bz|S<+o3_-BGH%e~JhkE4+0THW%OC3k-gF z9h&%`win+5<(uvE|LKm3FfWK)O!(pC51JjTb_ZV98`iy`?Eg5F2Z!>-?_Ym>wf*ku z?akE(fQHj20Xc3Fp*%>@ssxb!*<^rTY%ji4JVE%Cet-(M;RED=43ro>j1FJDdb)AH zS#A4})~<~gsk{rg>2Sx*^J|Y@_)}u}>f^`ne)H>JyCE|N+l$NZ{%~{k@#^xOeq%Z) z`}XN3b|_1!XT^=f0Y$I7+(F6yfSS|6)Q0`#FznDBizAcick7Eu-0yUKvC@+Dg`7N` zj>mDkN6$M>s2yxrQ9DkjK9=3r%g|SM`$NC6wmm?mNi3z zd)aBB9!l-3O!ZoHC%B65k&^y$5&{^iz5wWcL_l(N1o<$?ScJ4Y(07E?U$Xmzw3Fy* z^koGOA=FSSI{C2xI@GEa1sYgip2nMqP@uZ#p!3pC(Lp0-eq!OVGCLE73APW@jOn4{ zJ6{-w|Dq~rk2K%+gZ$DTdbh&h&e${{ODHf{&?S`h2ZWyqtrmd3jD0^V6M-CJ??!6* zo(5>83pgOMwGz}QkM_pOP~O5&lCprK6~Q4xPcjsbh$x`eTolmkO8^r7COCPxqcpDC0X}<6`Su5zDwJLWU3~gmo{e=9w2vbPt=w z022$QqewM}$n`MhpvE1?{-mH}F^Pf4mqkt}W&C)6r(1{Jk)Czic&tK!@TM@HP^O#O zk1;J9!El7RTUlTfp}$QmaEJ=>fz5^SK!$~)KnCAeV8w8cfTfXG8Q-k5$@rK~Z+z1e zt!8{P#2mBlWh;!>eEJb((J?->>C>@yypHomzVM4ME!YL88DBjAIw1HS$FApuw-N(p zobRw14?5N%a=yd1UM6E@3s_fRg>`R#gf<>6D-qk9$J@%p_BJmwB&{A+vxDo3VHzf4 zthV;f_U3`f7lUFSlG-r8&8yg0nV&Zi^d}50Vs}PBy&Na(%?yaM<%sEJyzB=QuY&n8 zjWfZ72~7l(VYkQLjL>%+LHIZgFxzg&ao_RL%EV&r$~IXU`oIn|vz1}S#^yOm31h=z zD`sq8f^Fr5d6iWj$~H%YV$ksRs0`1jAZkUGBbeC@G zCi3PkjhF>!DxdT_Hpjq{6%+XZQVxUe%=`<&7G4DTxqv6 zIoYJv+{q^%mV607!rxdIa57KUG~c3Xm)h&N{m_tWcgX6Zr%IHANLFbuiK z(v8CSowO3PF9<~>M@P9=s#4sspw3dtBD8Gv$DCK;w$u)i~UV4jT z=@C442N7%v8ueF06liJ#+K3cr5?qgDnZSAe+z+KL$K{lDf}H53$P+?Z5i2Ixi>N?> zG(j8xH?NA{%d@tY_sr9Ytm!!PST1L67^tdcK)kxT8c_Tbvb1S^-W|n9!NYJOF$lLkfc&Z@$2& zCnbBt^EjY5zd6`M#I_Db*s)MHldms+lgR5r9=gP)qsC{9f+Kos=JB$E#M$7QBn3D= zBNTXU%hnrw-W5NWaCq+pV>$-(9C0GnJzmbEBL3!OkC(8byDd)Ea~>}TaM9zX?smay z*+peYwW@jC4X0K;hr7~Y*+peNwyKp>Xw`F=X^+b;D(kUTtsG;kp2MtIge)Ihb8b~4 ztA|Ah-$4i8dM|->~jZ=?C@R@Cb zM}sLTJQ@iE)ZYhsylRcjMYPJ}xXUa|V8dc5}d7DG6d>WV7BY1kAZ5Zdiw7$@=1;CVRW=`224jX~7? z{lSI75VEA=8Dw>T>9@hEqnp9kv9@FwRM=jk3uK#5>wEyspP#B&MDT zBOLvd<58F>_1HDE9_pcZwx!;1+IJgbbda>MIcPr>huPSXPN86e`#n9Qk-0cgKu^@XzCk zrI8R&T68S-oPZU85Rm4R7^G7Ime>mcC%h$?1~URukIgBfbV|SqL?NKpW^hdkJ3M;l zEf7`xbbr)l;8exUXS?ZY+}-xw?rOL~pE~Z2H;3I7t`2bk+`W7$I#x|z)(!~Kg$qk$ zQ`$k{Nx1EAb}u_(P|!UIfZQP=qNYWwf>EK~Let09b7`iV6o#JGGSq1anEy)iQg#b^9}vrH#HY+)O?wt->Pks=4y zh%*69oStIK-*-wIwwBd0(xlLgGN9=j8)3#xZ0tuXgK>x$S+X8tH3Qb5$tXe4M7`U_$#y8n%%26YGW6330+WlO+QY zBfv-=;>N@Qv6kc1I7$vcOo}dsC1EbgX3V0vk+_@f#k-duZr}fKC3ZrIn0faWi%rb} zl*5l43-Mxt>)@U*7+1j!TR7u`S-}MlYIPG%r4GZ@QZAZl++H zyyBp_=AapN97)41N6>9A{^!(~XMFekAUniDJW(@ponG#42P^`&*K!1@Nwhz_j3Ye{ z88Sr3&z}eDZDJ+l2<9pQ{7$HpN0aHC5bf=bFI(Z8xEN)O`9T(lJb!@|XIhCh){6n7cz;EN7mg`9^D-1L1e#g4qK{g>_e^YZSC_Ymd zUBoAhDYe|HK(ODLFtCy9KA(XdlMy#>YsM>`Aa{wb@%+u^*LgoqlHpoVSZc^M!%epv zt_Ybq>5>z>@eO2o$*`|6n;$(A8x6-U)`azK)0Z+R(Mud265JhDho8fWE2c#=abMs+ z76C_hjkoNJ$s3|Ek1ubonsb1pUJ=U*K+|2nyY3k+lY4!Xg@9W?*$HK*Yn=*1SZzLH zKt6Inhd4NK<{gMndVCz*fUmV5(DE#nP2A0dSsan+tV}OR)ylZqFmfR)M03#+q9tK) zvB09>tOyOo2f>v^C@8VWhow=VorNHk1WO;>h7sNd6XM;t^%TYziePY}NlWOTFA}%! z-jUUJmXvrTuQ#!b6{Zn=D5uu4TIS>6Vuv9PE@BlZr;cXBHI$}~=55{4OhiHJE>;*- zfX0fHup0}66Q!*?5vlHEZ1)9cs$9%Mt>p>{zlbMTmsp4kvjT%9j^F{oOPbNU3`5p*V;Pl znTnR@X)!>Lur)MC#7=k4nQIoA@c&5sbgxm!Y?tlD2OpD7c(Rt`OxE)6kkF^>60^ZfGxSM8KdG97aMxW(QWfR9ZS|x#To>zB7*@2)E=)@7mI`V9LV?!zy3^`Lc*3vZYm7 z78@yBXJ z*)803N%u0o9&=r!&DrX}4Hu8BRVZV%NngsUd}xQ;A*eOZ{Tz6e6{M?Dn}h<#tTHH- zRhiwnTbXzPith})? zr({osGQT@YhLr=bjh_IpGGkaa4Z;v2fK?1DtsWpV*2u6bEFiWqp5s`feKkmPc2j8_ zYCFDOXIS2tmXA;wR(cv#{Kg4Eq`EPugrcF$4^Vad@l@%lm%C`w6 zve!yDt9h2_kh6pi@GSaZY97wCfEN-l7?c**a$=h6sl~RO80P9M@-1cmo-#o~GU90g z8^H<^+W9s~NtK#{f=p{u)DldE?7`J8Y^YH2@c5$kMze5}dClUp76--xXb%iWs>$Wm zfvE}sJ52NaR0Xb0p6~^fXtOABMJa5yJwt2t;y~BHt!2$%)Om%uuLp7v2V^r z0|G{;gv6T)A1mt{)6(9YFl=QGVHg>U~TfPHg+!P@_EQh;l|Nt0lNsg%PD_sojssSsg@AI-6zM8{Kxy!Rhe zkaygkBc1qrZj7QL)1<@FAe$L(xWmYB;GWcu+qqiit8qS@NK+MyxSoNF z=)|QF(XhCKG;*=3S{6EW=SAq&Flh!t|NZUz>)Y=>et&hpeg9fE5JNZ(c+X|dFw=Bo zh{Efx`km|D<1K6c*I&JY3%>I5dAFn619nI85BG8nP(-m%CT;||IOR!z--~*K8fcYy zhk6nSBM}Qy&&nA2POmSiPKC1YgsCd&PC=v}F@lpRW$#<~U;X#!!auw8Q0w zT|0ad)oftfuo1CVl4pQKDcGii@#u`#bs#jItIK&?C#^tp$z>ttIX0%Gqk_FjvN5va zom^}_3Q*ZL^h+u+{&zr-;!3q9&tu0(!VFY5!5B9SH+b`YL)$Z1uynM9t#Jvx94+xp zT=rx`&IitI)MQJ8yF19%!a-9l<1{m8vnVaZ}03nwi?Q__ITTtUpzkD-AR-Q8)D zHGqE~eeNXrC^u`hhh_tjtMH^rFe`2>5;w}{nNa0X3ZkbX?;GgnRs$Wh8Rt@DqSBZQ z7Yq;sl*8%Av_TXqlLjiqBEBl>70Ilq3OqSJwYGjXVT?N{WCmnyxZb%?ic`gXBfdz# zP1rGnmRfTgzH!-ETKc-&jY6N1Lq6mKEp?nl{wn)dWRX2bR)xPxDoMOY{9o!3>I*lm z)trcYgMHL~f>pS*e~H#9WiA0==$_PZ6^6IK-sNEM2-Vx-gU8RLagrpTgkQ=qrahsP zy$r@QPY0E{P`vya2K-f|7LS<8puDmI5Xwq8(R?Rh}~>L!AMqwZ91%L3+z6=6tD zZsLph#J-_uA6IbE_8}OfYE<^zI*jXW*I}Hubr{QWl(V%*GDKK&$q?mWm$EfS=Ll{0 z1cv#e(;sYl;-D{yuv>Uck_P3`x!52aZ2lyKqZ0~MJqeQ@^ze}roj74>;7-EQ1X48c zN02s1j-cfDj}3IZIu751eqI{1poBi-tE*EbB4II{wl;BXtJe)cChCWrL&4U26`?iFchkk-nrb41v{YUT9hxR=Bc zvM2-?swSb&=w}k$hW3bRXPHUUEOo32vur;W+R&VK^%Gkx=0dM zkC+Am=EWW%w>fr_87irVjA1n+#59~Nh zadY?e)29|X7gi!-r2xfd02x9ohD(`{UY_tkL*(Fgdmd7Zv?OevuqHITG6;i99uk{V z1~gVMVl0^64I?yR0V%o5vS<_X9bUVndH3JHc`I={wNRKGeuPTs+nzjd65*8wY+OW?$? zqszG9<=E8w38oF~5tyPL;l2gU!PN}k{2dq5Kw8J9F`FejjS3`i|3U=_;)v5BydE(r z`z&fY?Rz3)Q(ccx?V*`I+`7-@`JmFDk;*+n^|t4va))u=HVk{g_+c35^8CXvuD4x> zao+YJ80YeQ!1#JhPlss_t?L{a&?9+5OSr$;1J*Xa?yL7@JU9+AL4qen=N;b_-8aHmx~ zF4Bl_efJHnElQ}b>A!KqF>R^gm?C`#i=b>tIo(&E9v3kMayuI1R#m1j1Ee#b;dT{;<#MDkhIv4CTk`F@e+N`tF+}vG$_xgu-7;3KH ziSgpY=TCmPdjI6v_J^zc?{D?n{gb){N4vuq!n}p<3cWv?g_bQiIi$oC@jwes4yS60 zXqa#|7fn+{4m2W%$_{_U!<5-cxIHVTh>F3h1&6~vg%fpuXSOM=R4p*vY}*mU6KqrA z+HY#MNgToQ7?arFve_%FU1ZNsAsS-bD8t&tro33PcIEIR*)j``GcQT%f~K{r31?=( zQOo0|VeM*ysakO44m1`Vn?*T$2-bk*h^s4 zf;uCIc#QM5VYH`Z<=kVOx3$qOJF1n&Okl-ahPWtf;g1Y5}nFXg=(u2wmXPstAgAo7HH)OY@s)%BKxGI&M z9c~b2T27~8pkA0MeRq-dkVPY5J?>jzJt#_-17fdRBKi$Rao+YJ7_$H?5hpWdS;e^Cb{)of+mcy^mG2Jd%AT>~y1Hk6 z0%l1hxJbw)vxFB`9t#wf;2V_8auP{u7z4K%9LJ*I+;B@p zcds*Z}J zoq{yQ%j>A7m4r=K0*%OZEKpt_o`*nqcX_E?=Ex;*NNStFi5S^DWV&S=3`Xw*$v8`u zS@LMzT_1O3v&@w)mLWTlWOXX}kSC~i`FM=; zwqdkqPGygA-quFDtaJ&+JPWo;@`)JhSe3?&wyQ8M+S+K_G}gHljG3c{OFoXqjkfDB z&ij1`#vCMOItX)fMpE^K^Hf{M3TqR@EW3jEbU59Vp)?qBV*e7x%$GtvLt{gc* z<>}FiT&e2gJj^mSP~pdkW%_ZkK@hagE8z)B>*UI!!FjndHdyI#jty3LoRi8?YF(s_ zP(Mnoye}wDNr}{5qQ25b903~A+pHSsu0VZl{p_Vy2?djkc`A5{Ig(e!IR!-w6$i5a zCJ(P6Mf#fZjo%0b%UZ5J8IjhiC!<7$^6qGDmoA))!nG&T8OTDewmsw8)LvPZ$>|oj z%8Zl6vqE^b?=^25xZs)lE7~iBT%()EUX!ub+6q_6=j5lK%~?Er#jz=_BYE%MbCsaG zwH=$jL0GJwNx)T5ORC}RNe>B=&<3I(bDy?|xKA3yUCAP^cv+LG!q>c|PFdX924Pn^ zOy;|?7^vq}T3bKIT`Ag28H!QSCG~cG9w09FF?n?YDHD)=@4~H}F3IKd?$~PrT@C1L5lpE^iJ z_yq50Vhi;pfST1xAc1f%wqS!z_&C!2kyPNZjA#{d_{%M5tH!fcWWHypHZZwsk2o(e z$M#WmNmVPz#R^-)2%$>Ao^bZTe}mPzZ`2Ux=4<+I!s@K$!jWtG>e;AQ2tRV+KM_a* z(>Y_poFt7**)}j8AMXN#oyEHd!`kme?I`*UZ5P<2vK_&g><|f?w3f@>6OVD;HjH*5 ziX*`|Z)^Q7D*>|CMy9R8ViyEA$6}Sn^|tFU&f7+zFIh)&*#6`zaK@hTWtMR#VkR1S z7q}wyH5goMplndMoDV@+Y*6rX6;&GiJob?Ab0^!>`FU(`mY>I}BKe&6yd?Aq{JbS) z`Kn={%EP^srJo}|=YAMe69rsoM{;P69`}tfJ^dysoJd)&rhbDF5v-B5{A4L>Pmrk? zJQCIuHq;p@YftPfQdYg~8Md`2xdHZvylr3t3t@V~1bF@ciMVJRa6Thv$!J9*uduBJ z=}wq{8*QWQ0s~RDrK2s{hS8p3Aa1lp+lP#nWmI8#>qc8|yKc03+lP%dZ|g=Y(yXri z&DmYUK6%)#5$t3qtznlE!-c6ku**`KWycn>X3}6CyYvl&i>Pkq_^W=b^Vg&Ysq5wC zBuuXqe~ShxBZuHP<*)5AB)*rk$gB+0a|8`ca~`J0w1~Wl3#h=`^H&{c%3mATDNP(x zaO~-5=bm_V0|SgqYB2B@K}%rZ^HnXm51wtWetiAm^UloINd^lEjiMKEiNrX%bAfHk zZiI3Ky=C{ctA#7w2%g4O91BXcya+CQ3qbSmt;F3CX&{tNJOLOEJ9$31G~wr$70unz zgoB$afm87))TTrhUyhDg`MIq)Lnrkp*yYD0_!D8fN3cc#LQfxgK$L@nPmTQj2z&=# zMq^>~VZYOKD^54-d-N$X%4-~G_<{62V;=IMeO zd{XMj6Jee{_N9h@M^Zx~uE*X_{yL{2ajhTTz5n=+E{yf-izjcs`&Qyu-CD@JRUW)& zD#a@&`Xd)S6OJ?I1)k&hAXnuD+Z7jrbJ%f@k=v<91!9jC4;Zy4o8uPqZfzWe@RBzod6a&Tho9!p8?{E*W8#S*6U3^IN3;;@2{R}8U!ZuT(}{NokRul` zayug#WREW{ft8>Xc3+!T3CXd5iIoVZobRrSZ+qXBb-+ue~-(1~)xMgirT=hq6VVIy(hSKaV zGL#ho*E!(0c8OCTc3nM9Lu-;nP}xgnc$wt*1{rrF?&`dxg^pOF101<5;EtZpnmj3kqi0K> zr|`h^?ID%}e7cUYNYGWmST@RngiFlWkX#5&M2F^1PZ-Pl+qYM5ZtXtK_VevOKl#Ix z-#q!lE%$Q%Wr3-TA^mRcOpQD&GL^r+`grv>@AW1JJ=T+!=nNa=xEBZ%m4r3v7G|3g z^3-B^2BRjwAz}n*fv;@WB~@wR*AWDFa&m3H0$VbatQ^BJj*}OaBAoMyLYn9xqw_+U z+Yo6|mD_8c+S6AA-9taMqrZh3LW#jlv#P9I$g1IrI+dP~@V5Fww(QKc`HQP7S;#7X zzkYjj`{UPgDW|qD5bP%dTuR;yTLK%C?Ni8!^qOyCByYc_MZH~I(-Cruz;LMyj%RP3F$U$XXU6-_cxDMm8PD z7V9^)ZNruF)_4dnrRA#$a22*F$8j>RbGGQomaL}tw;ygl{$pHDwK-uqMaYT=XLWdf zly#l6XQ8;uoXQ^fsd;9E9Ov?(JTt;*uP^1makAX{s%^Dwv=OIteHmX8iAz)xd8Ic# zQr2Sa5zPpeVCcAR)5kvpZ$blaN$-NGJ=(cyHT4g+4cxqc_njTFpTBtWx3_P<{rKw1 zvnT)k_Vu@~K0c8E;~EuZh4JQIA^=3=q^KPuUhQOIpJQ0W9#@$`1}RcC%V-w4!DqYU z)t=XAMgK<9=N;RZH~3xTh4}{a%HTVOuJ7QPIg+bJ^-O1a_p@p|OpKhK5_2Ad^_LW7 zsVX9tgy!APNYO$aCyqUV6PG`;*5V6cViHZY6SG*$czB%dc1WI)_v=M0_v))eNZ@NG zbOM_;i72@FPfWtF>-NDzVK!`xgOV#;(syerZlb?Q^e{|v*;4qCe|~h(JmLgJ=#>WY z0+HivzmeSHT85*><&CMx+6Oo&my=IV0BzJzS7SS^Fx6Gqn1;f7ag$w8PP<-VuGaV6YH!_C2~whYip+@r6dB)sM-MD_jnw_K&f zV+dsTzAI&ZAb>84@aZH)o&f&E*Mz_kqd?x|#n)JmfRVdxVligk9$+z!sJSVWy7GJOu5e39A12-#leT5faLjhqw{`FWWW0m5IuZv~KqVC04l1FM! zWbt>)HzHys63XMdvNDO~nU{GSBQWp}J@ImCK<=7xS#z;pXJ5iV1F+<0)EkLj3_S}G z#f2C4<-`N8*Z5nhaYa=|8Vv3^}Dwp+UXxt@DV7Pe|hmzvR6CFQ}9xz*sv<(XeMwl zm^nD!sz8LrV*1AhIuf-bK{#Hy>w?M+>>W>~?`}jYKsvbLLZ&>EeVWi<_Pd->5?AIK zk!O@_+PyrX@@Pg$##4qz0###n@<<0@c^x}tl%XuvlP5X+gVolS5`7)p_@pmL*rZhn zJV@cVR<$C}yFy=qNAlgh*6sP91V)6B1ddoMVNpmf5Z7vR;Ehp203}$@_mh+ukBnu; zBg-OqWK!gLB-uWFZ-FC5j6#X*y;(E1)f}BT&N`Vatz=bm7TmuKM^c2~eA!7XS&w@wFmgGSmc%)pJ-OFC&(BF|RAGaWH zd?3=4(o2O&5|9Fu3=(kjRczA5%G1SH!&T%9zk?qM2kpqOTn7T1c3kKAb7Ni?H95Qm z0QanW1KSyzG&jSliK&b*(MgennPM0jwWKOd#5J} z|3_@afx*jT1nqL-A_2QPMS(o>KnjLjW zz?0+(uGR^`ObaoxB7)0Uh-zpJ#Rq;d{#kxDX&r~sgb_zlsp61~1F2LAL|risLWP-x zd`x5b1t6bC8|2j`*;ICIvVUB|$PxNDWP8bwSC=4orq=9?vX|6WAJ-ud7xyi2M z87v0n?F#KdA(96VdK{E-%{e1w7MhH-GN=TS4vN)slILeCGbPJqHdDo*ND#4Rrmo{b zxN1<$LUT~(PY14M$)O{R5dnK)@l{>wmveGR{BCmYHJrPj?+oWS#i7>3vFe$nxTy(PxPMaJIRTjnyG4=M2VzP1dL9Z@dB zC2sMMy>)g>&Arv_ntVOX(^MPOFR-`fgIc?{7G^38>iCPUy|}mLgIc?{Y*4HAR`-jJ z&+jb+H$A`FLQ`XJk;Lb$%rw-N;m^Of&X1|Jw|r)z#dB+48`RIgwPD(tP( zFS_<@Z&e1ha&N^!t=e1tFFHOo-V`s>lQ>2)WfeDaB)T5Uhy5qN>U<=-GFL26O^Dw2 z#sk#v}zais*tsl(}frtXD@K=?CVlH!Bem6 z5CSYnb_7&n+9LI(7R>GuMJ&BdA#EfYtshwfLw=s{m}QMz!9q`*`bt0utCTA*5+yiE zo-nZY4J6cBzb;WgDF=M#WD1k=fA_Ej> zA`7pOV3^?g@(;$jK)IY6IX=bIqH+2tiaZQ#;R=><1(Ld@!;M&qL(M%G=lq3U;%^2~ zG{9u9#ih}M^%#m4l=88-Kr*pzhnpcu3=y6}EZ?W<6^BL_9ru@Altxa(G%zij6LJ5Z z+tD)M7+kiNSB&^A79o2K17zK0FJ6q#wrt>T_sz2g58|><33rjZx!wB$t^-%-x;9*5 zLy}93#p-M^o)>#qjWPn^H)cu8X*@hE4ZCPNhfno;yVeVq>I!lJxCd#xJPH4#JS=>+eJa7p@>_kyxU50CA^%VpsqF%VYP6nl0+`4KMuLP z=8q#|ELRTq>XlvSirJDksT=r++IFX8C3Xqd%>vR8_Bc*@Z7sMLInP6Ou@Bc988jD1 zWge12&&S-ZbKb*57xLgr(gptf8C7{2S5`%ag&Qf$0Dp`GzqUMsM${S*oxV+HG;P*N zNZPJ*cDQk!6oby(wzCn5e{PVc$q(x5v=Oy|#1Yl%l#8TR$1Q9d5#+782z8ARw;~qQ zY<9|h(XL}N$SCW+T^ny3NE~miPC4FM9f$BLMkdaJ79%5^;~|V}Gc*|)-n+=$0!vjF z3iRM2wghhoPbBu>cy#=TS&*Tgb9Hg|mFO9N?zWBBS5fy^MHZNKlPtdgfA8duJ8$n) z$s)5mBTM0{#i_f7EQMP$%+cmC$6jN#u)rRA?6p=4_7OrbiJ^{UIpFnfxvz7TzdJ$A zuu^!mOO8DmXPTUCa_-pvS+RGBQW@;(VVfXt|ID@yagAgG zl(@!%B-MSxB1weGPGpf4H2I;vNd8p!O;{u(gp{+*FB0CRbMnuOndCc1)(KY&RJB>y zI5%~guvhCM^dEWSbhb{o!VpMOxo)A1A`gU-MPdfH_QomPRDroR3G&B2bL=S22-4a$ z+uvcBQ9T4%U*f8Q!C_92pzs!f3ajDNk3uU#FxoT;2hk^NATMLxZ-VspDZ+h8lQnxtn99(W>;qGo z{>;P^V=5(0TH7cUPlT_mpvkd*6o*yuQ!v_}N335BbF8I^68ux& zMnQT*(|@3(B9fa+SXm9!2$5a4e`Z_AB)5VhYj%*xZYxOA;Ix0TJIG|Wl_W_wkTBXn zl8SsE7;O>G)7~{+!2M&(_r_=olB_v8e zip{2bh(qj?sPhX*a5bMwJr)W3F=}-DPacaz*gls+B?5%g%z0Pf+wmK>|D5buWD9of zM9sz%rmhngf#bhz&bQ;BP;ZKB7rrgGb1_V)O5$O04MmjIugfh3Q)wVcxv?X>*>prA zsf)~)%%XYX2&J9WhKv9S?R?4X&5qa(igx0!B78|`n6&jJjXp!X5iv<(-A~&2l14ij zV~m7y^nkC)%yq=5?Y94VLdO837z+Yf*ecDjuolQSplc|u1z+L_B!<{@d#+&N zNQAuw-YUDQ#wt7}Y6Hl}VC-l>u0?HVz(OU$%hcQ?9D*)_71D{cRpccLP8JyAqB!Oe zUapHgK7cMJefj~>urZ2SP=xQ|ArW5mG=}TF>v{^2U4+~H-S_t&U;htD`E_~s{@t5@ z{P_KM_qUP}!Tj|(*+}llo_)laUXaoE=E*Y>ct3NWdiUw}$+K2pT%LbOUjQVorh&J` zYOJG>n+E@1$?o@oltXXHHuQr8sK~!YM?8X^BE2F=CCr@W zIDKOoDH=%Sfj=yi!2!8;PNG)4?Zg)FOlm0E@P07i5zNrFm2Eo?(UIOoOtqG^GE$%n zC+=MKzQ;{R`XmZ~Z+L|g;M4{O-#TK$ugn~3nMJS8Oa-f4XjF~H}%(7JUzNeU_!m#@D%2Gl8+2g>&X2=$Ma@Mot zdwmg%gn>lAU@qFN%2Gj=2|F)iiX4$jG-GM!;_+UPree6c(WHsP%R6#!lQ!)o->*PH zf>epiq{i*t7u%Rhu@rB-Iz~FS54hb(MvZX9(%|Mcfh&C{>A&RD{vhvOi6xn5JlNqR zEwFva8e(tC3*S~I`gmEUJDrF1c!%5SC3_#fc7koBB|H}Z6rN*?9D?jV*!CqFf4Pj6 zdpvPHlx)DRNA|S7j<7MjLWyvtFeWPleyxm*BLmsgd4N1X)4ksz8Cj235^xP%Lu#=K zWK61@3)e`>)P`YYff}JlqqgB?CCW7dwxRTsa@#nrp}09-s~RT3j-yM~&Wy8T_hj>N zTqFO&S|Hg^U{ zax+FAJu$qj1o)vVD+7L|%&?5$*obUo?Mps-^dtwE0gxPE*%t#7hs|w0tAflZTt4(4 z@_@Ta2hD+cnXP7WC>DE%YozKWEhqt zdpF68QC^&pyc*-|<*wFA#$8-HAQxCI$B%#^*^m>tBe{$PJ0Wu;bh;(4o@TB*LW_O> z1*||w(;rAdy5}b$5lJnF;}vBj8!I_BzblB-;9`ak;};u8T-Q3~-Z?S32XzKP(oi!= zf*Ak5?XK$+hVXgKAnpdiST8utu}sdD&8c#(WSE&U+@ze#n9tRsTlCvI>6G{A^bHO& zDl5WJk|#Zll9R*zM8M+h$Pzq2(W^WE<^<;+ROH!)-SzQ^5KWf$#saiFzh^LmBriwE zHgcST*+A;JVdDR@Ic_d>3`uTsifcfutOutL=7z`D7bvK2Ztp*SeQ|SFzvKdBcP}y3 zNUQ9kROByq0NAvM5ocB0)&31_B4sl>}SjGSfhOHX0?G5p(|IND9`8TQ)^YvDDp>d|4xD zT1`n7$4+kiityVVhJjViCvq(I^6rRz3iwjk#&`DP29yH6vmY~jtG^n)`LAxQ9G9d_ z-t7>!#2SvSM%q=f;}P+WS1I7jMdLQU`H!B20pI*bH#3pG)n5(Y{8tB9ryEOF8=cyZ zM!x*Va=P8}22_~yU5v3~LfM)1@POmg4tWng$Y~llbim(?B^0Unit;m`LG#@(qQVq4Z7Nfv4jXosE4x+Ymbi zG3&?~ZXEVCG)ydhT%k{TBsq`nf7dR23(fR?w+l6l&4NRx69XCHQ;ucwZE0izpF<&O zz_*#VDM3scU~5Sl@NJ<7eNq^GIcdPRg$9Xp>HKgd080)AM<)2Y`?}arCRB%=;KW0o zODIWoWR_$Md~*#-LxpSsS+0_=A$#l&=(sHpNxE~n`lA>**|K%Gyh`cMdhd{pkM^D>8zWhhu($J~>8qvvrb%13n3T&i zae9vYDJ)2Ly*QGvT1Zx8-h32e&*|n0^QM?3Mjnmh_dVtd%1Me(oih78mX46{Gn!Fy z1ZvpvD#apAiogH}BJalh;|*Tz*Vk7c#j{;EBxu(LUAj)@ZtgH7ddFsKum&{I)s60IYIasZa>%N&vzufSpD@;kf$d_-nza$T^UM)0tWwgJnbB<0$;Svhn_ht_S!b~}1l96dWDLkqxm zzo(mz+s)mO4vt{|^eYgodG)|KB{Op;%ba$L3_mUJWZ433@N*@>P^eOL|C~cZ#vAq3e?b4aVJp(ML;t(8C6A4hDL=cD=Wcb zgsGQF)`g4EsNNb^6_}%eS{D@Jlzk2Ncr8n|ULd9vgiI_nPbIdlQI1Mei?YNbEmE&C zrNkl?l9(G;oh_g!N>TeU;}pz6t@RBVj}VKyUO6hxfTkmDf7Uq7S1K13FAv%+mDx3A z5JzN$nyvta=G$*hNr5qWvy&cTmte|jquz1(&}-!&C*a^pTMqX)6JV!Ka3UpR)EqYI zPFX%<`%_udOdgNCa0;$VviCC;_p`cgPYS>lrT@!wdDp%?)Wg*sgIAGm-^pbl$w=yo z0^maDqp$U;jICsHh=%Gn$)lHwmkE^R0ui9w-O{aX1y*8B+MV&p!R0C!RThQ~6tb%a zT6*g8_iKX{3Fpx)ta=HZS=~E!VLm(ZE6KdDe-&NaRv($!Y_SUYuHeq5`>ZXIU%}wu zS<88^{Dg^ES;m7F@0lHIi|_6c;lLhg1bFpcAugPEEWPk!OPkeBnZID+KG)_%fT>Cz z4q^!m2=D8YEGr-Mw>I4rH$w^jQ4tA22vD>H#`t9OV0Ul4zs$$TXwFY^?TkgpR04YU04RV z%6^_ErgM_b2APaA0-22;W7URX9}o@@CsRyH#bO5gfB+Oyr;O1DQO}0}z0XU|5zgp? zPKLPs=0uJWj4(7AUCvPJLOHXL%hnXfo zNr3iqW4N3N^deRs6jv2xJPqMl1cgxr@*;d6E|FpDhuaVtggg7+sg{lue$e4&Q*`+{ z76K3}XL^^S)W9-22Fs0H11}Q?MohSfVk1o5Hz=)iG?9~Az=@LJ*44KY<-1T3#hPS$ zXvvt1LUZBs&{wm|n1`?1_DmJEBcv#?6pZq=kq&Zmk9*q~&5IgE3b^K7j3&fe4|xfL zPVd57x3)|G6{`cG4h-M-s`1C%$V?{`f;<>Lj%eEieW7kIM*Hmr%}arlcTLdIjE&mM zNOi4BY}Tj{$MEF?$ya|l8_SbV0walIwrZQE&-}cS;hZ~s`k-ZTf{|d30G1!2BQ@R_ z@XBN?2BnA_BSA(rczq5sOGKba|pmuTynZp-`ew z;i6B2V$LZP5@XXbhcyX%Bj$T2qdP^7?9m~N=zlQJ+su{69y7&^iZ>@Ab^ARyWunjl z<<#+(;759fORjV1(Pf*>83wmrOB=?qqT9aoO7FQ))=nl)j zGyQ*a^xdD){Z>Wti%a*HUxzUmIo(@_2}YT@a`!B!QTLl^m;%7Mr2$Pb4fao?9H*Dc zE&0hb4{{uf=8DCg()$-l83+-7u1$J(OxG9M-=rV)uxboMU!?Kj(j5(ak$wgxX6J(i zIHRS={4zsj3|vey$YpYQTExwRa7*r49c|0k8^3n&CsU(ugFZ@LqOA1kV>S8bYe{Hl;RWNArmhe{Eaz zH2%K3&@!yPWKHY3kildu%dKsqRX2?6L2Q;6au;Z0@>&(FDvR^d&}d>loDcC#2;*Wsu?0MnyC1ncXkzu;_bJ>CFYWZ_ zz4>Kq-HME$@u0VzRX->gamcEDjkr^le{|>H`7(GB7z^VjnBcV^`p}xE+vNgX7LS)6;ah%-P?Bu(qI%YHQ5gL_v9SnhF)o}t5RqcymYIR(5 z!>8KGJmNiBHVjF??4gs+wnmhuw4{!pcqN3_{wP zDJid_lsa?fw8`T;qS0l)gm&dXd&B|dl$W;p;H|+FdlT$Vm!S}rNc}o zUo_0^*Wy|z3%sBI(6ZO2KT$aD+q?A%puq9twXfPzl<)L~;;y&C04YmL&&)w$;kun* zM(H18KG|ayLCu%EcUjZa(lv*0t{QcNhnn?i;XE3)i~ z*7I_YEWD@ziw?@Pk4v;r=E2ZAWlK_E5v-G{z2T{5K(lLA}Q8MPOJEK6Ofcf zVTzWOGHE6vb3>M!ekh@=bpI2*%6%$HVvpM~lTU)~JAd#R5)Nag`d8~s-k6D^VcY3N zCm}g}wTi+yT069*Hf^RwnI-?F>Xn| z@LZP{=Dqf(9hs!V-Y+YaQdt}TMk$*~G4U#b2;JDl))Nc_(YO&bh(hsZRCX=f!D|MA zwRRl|YtYn?!Kq`g#jty~`U1%c-MF2>>}{vDnI3v})Yahc zFjL%6hN)pVFWxOg4>+6$qZ_;4O|=CD9X1=?OoIcaG0Fn%AaU zsczrVeVcWh0>Hg`>?ktjoKU>NS`6os(nN7CXW>jO62Sd>T@K&Y6%f|g)3veZ)Bv#N z+!zo9xtzsRkd9l%yIPIq1y&LklfV zWE$zr<_8j&-Zu()_$|e|{ZrSUIb3+jVeYux__hWj@lRT*tWAwkkED;^aFbI-MZ7>g z)U|@wv{E@7EmGMcBExp;6Kr#4a_ zjZ9HjZppHec4^6Hvqgr*bp<%>Lw^WxJe9IbRpVMf4{tjE8?<6lIC#rl>rgkS08tq!!A*@$}uc?0>mZwC#IB+E2$AcDQO zk3fWVtm0~S?80En*H`TN<8!my)^Rw()`os>&)emGwsjKR*@*FvW`gv)MLUiyJ@?*i z;YP+KVn=5~B((@RFxdkrASoUvHr*k?A6rj$=THeG>WBdwdBSjcgh@Rm4+PE^DlqX?x4()1@TbA7po3c%*#(>5IKj$f_ndiWY? zM}`0{d1hbRuHFA@Z_(eQdv9|g&jU@@-l---^Et$FI0s3nMJx@kaAeAV`wfUJi1?z6 z(0s{pq%fdA7z8@B(e^<5PUa3LDYzxjdtor>h3ul=(SX}HGFDdV72d`fMGm1p z>=0^s_lPqjg}}+et~Dh0Xv!L7N~N}DU1V%87@Q4G8<(zSD^Oy5D2jfaiGIR}LCO)~ z>Rtv$-ELHqU8T$j1uZ=yNiHQP#y>eJtVUNrB53wSKgk=ltt6W=QfPV6AkZbqj=~YP z^P8M15ndiH{^h~fsoB|1t?_KhIhNWrQM7#@FCLkit`2W&>Yy8v(07T9Qs`qRsJ2tP z?n$IGc4zYA2Zb_jJ(1@ABz#dx>-J;9lg|6UL0En#mJmO3+1GyEE0M;q_M+9;PAH>w ze%W{4nG>!K=u1y_@$oPZ3H2Vzrt~4+v>&aC&5YKYR!hcdufrO;1u%(rj1935kn zQ_AYDL)1*u_(MMs}64EseDhk z0&+OjzOnHXsI~~zU@Magm=Ob?4^mj*o!hV#1Y!^dgRQ;d!(iJIjE%d1t zN6Q$`ZR?`g$st2R+FK6K*a7*#+eXD>e0;zDk!T~kiN9AC-P<<@{CUJL^q;v4WNw$ z*n*PI(nm@-7Ys$x-lA@DyBli0jnOAFHia%K0%18#;YRSnyTa6FjW*1w2)nBCyUcLA z|Lmv-1i)c|?>Uq3l;Gk8it#yn&&qmIQ|UG77uY(JC+K-KKv*fuyH`Of)a3b!XiwQc z*wWZ4zml-Lj0 zGVLML*PFRO-rOLkL&u{{G!x~oG;}%8j!Twn=cTt15ofTCj~7~3@Zy8ZEJ@065qho5j0?eR zi%t#jg5=_$LM5H(+Z2tWFi%~r0A#@)fT0ySNBT6A1Vf~2GTvJOw~Q(Zavd#2Xc3#s zWRrq>ma9-2Bao%eydKU}_(tubaYqp0=p>Jqjt<0@@~K;ZAl%kOw(~~$My*NP^(Ps? zddqh`rjl}sirR6HX zVE1@w@;KDgm3Dvr0u!_NO!D20yFmOUSm#nz#jb#2SyKa^Inf=K(^V=!CrFs}`8Y5e z^mcF?ft+@;bnjUyvbfC4LnBhIJ;T-MCxpJD{PVp~vV0Vcz|pbZEFQ?iZ1)d%1o%|O z9p=oTAws@H)^+!(qje9PeBZY_Gviq5;gW*+7~tX7r>6RP#du9|ua4MNgn<(yj<|8g zE@wWCFbP_cF8UDK^k8YvXSlWY71g??n=Fe-yTstA6KWu|U`>B?{nwX+-sK!BuHqT} zydBYUI7Q^2jHVr=Dy17|aWj)P`1U9`3B2**9WH)>w`NXiyaHnQ^*?nXoxIRP555fW zbL)2)x2Hk?94SeI#0Z0A3$%n6sqY34l0FbR_ILfx^KE>c#zKkAWokN#u3vs~cd`#U z8#-w_)DCYhSt$fOi>JJ>E;?mSa->pU;Hu*lLUELtYqPOOTjv(xv$LJ5%6_;!Mm6j zL$e-600;{)(oZq=-$XZZZiN!WJ-ea?z4h-7vN`*O^pZxi5T(>0iH7nnuU_xI{;(%p z&86xL9?FGdy^F2dKTKLU7d3#?Y!ve0gL!?jdH>lg!z`1VL_4+*>l1Lxf>zOgkTVA^ga;J&|x6A^?HrV5_9!hSmh{dZ1EFw;C47sZ2bNMCg*&l zO_{m%8~p?p<*65*vfzQ`5G8XpljrN+s z_F{JQxk2eDmG&hFFk_)pbQq5M)3bVdBC}qcuSN43$`Dz{>!8%`26S4vZ6?*ay$ZGe zHNIexU+8dJSzm=CCl(q_x{8m+SzyRIrOsxk@8T>r5+s@i^jbU$cHZm2GRDUNLyt2s z^T*CfY5E73*(`1&S5kaJylEMHRJx1aL{7aW&e_w3qR}k-+7srU{o@bJ@_Kc^1!sGf zZJ}V>CbozCp~PaYh^35GqqT)FWAV_#j0UkZs{($8Jcg5uu>dofI)rTRnp5FIKapB=y?RvczFiIcr|dO-p1=}Sf`eYjGI^rmlPo$X_xNpWJT0U;T5mOz&*KX^tXdwa*Lg^7( z{WaarI*{1Z{usU1m(StR1D#4rtW>|VV`vN#+Y|?0^uTm9Dza-KAE7;^3C<-j@~7mPrdgn>Ix`Iqt=Kvw-XwCVvg99F%${1=}HnMQx+ z!W_kOA}5M3QbemF4gweMaHn95M&tHrXmvM~nNo?ls`MC8h80s@Uufl7$)7fi9L}>P zd0ZdcM^qlgM5%;#+UGW$;%iVthU>KX8}*n-aU?WV8=TTOVosdRD^pSJB)sm|)XUQQ z*9Jx^3!7v`q*b)h*$K%b6QWKqHqJTTYL07EongHX2sx_`SyVNDp;L?+4h@OCLLzDZ z?1e9O8+e`YwI(UpVo)e#CDB8tEh`lB%mKZqyF$Sz$74InwBm&x0-0Ki);IP`7W=q= zKYnX%CE&j%Tx&c-@lzJ;f6%tnh8P;RTETAynu(cVj!22 z7Mo1-V7@_#QbJ8)P+zbc!C z5x9tW^6X_aT><2VMjI7Qj-c}{%%m!0J5&_y0m{t5-_+&<#LBFpY33THp5_zD->sFE z5;#X11|gKFuHWX>dfTj>Pj@T&sz420TrxB5TCCq6J|OCj%T%Gw>`nh+>Hci}A@}~n z=>6xDnT3^&>Cf%II{(4^t?K1yM#!LOWbxP4+05RRkmYX{aAh+W2RCOEGZ#YkzXc)= z_O74zT?qd$hkw$9%bA&483{Xh66!F13YeG}83{Sr81QDBV-V9uyt@&aWpb9BmAQ#;=)YG^sjvv zCPI#XhWsQa|McdskyKSV2$?wlk~4@fefsftQ&p9fkm;YPhh9`4k7d3t}y?t#r(GidjCe|uP^@_n*RvFKMwt4>#t|U{3k{Z&ZaJ( zq5CWDpJ8YG6FE@_J0mOmzoblrpHD%|^Ydw$e)e6R-OT>Jd}@ii*_*gpIoK02{~gu; zaQ2U}e|`9e5T5y;xXKeU5i$tWVkY5iv6bHuAW9US>{z@dhZg zAVwL8#Md7O2U07uvq+B6h`*x1fAd{EG{6{2Iu8J!u{oXg-c$Mt;Ui3OgX~aVAo`hu zOFzFln~6m?DTiV95xi(Q`*AZqmPXV$I}OI3+Evx&_~;&TpMKRI2-zv(n=t|xg)axidRb=1jpDX;$;3?9~9z?u&)j!jF=}jP>6gzBR`a}NyNb5 z4RI0SeTe4hIE-ZA%9ma2f`H&_FcWcI)B~4YqOj+P(z&0StXt?nbJ+}#u-J*oM_TPW zqhDAkKj>=-;l~cxFqzAJZWQzd|qy;UfluaQy(%O#sC~ z(KbZTfy^NblY_R{52U4>4rqJW<2)`xvy26c+YfT(WWlMyNoEEns3Fh_zaI^SyPN6p zyY}-cdd4m20otn#gpGsg1H#1+W(9Q}CkvhF!CgTJP{zU?5Af_lw-_a{K%{GR8~nl= z*K`|iJr+vy=&VO_l8{c)0xp<6^c6FXXnj8~xq~!12Ra)AOXxX)ww;|SM+ppf-`Szv zR`!HqI?>^cpm{31CGNrBw`f+60zvx*el3@+<5`9E0DBp8)Rr`< zLX9bhtJyg^@nS;xNEhki$8q0BLbJju_WU^`k%Q8n7m)?@heKCD2jw!A1{DG&2QW$x zpuko6nXp5q9U`BH;4Bh<1OnA(5Eto{CWt?d$uLBVw;k+&7>A#d0nmD*G9`ZhG!Fln z9Yk}Er(FbLrh(%8s1nRuMl8#Rxk-d5q6_8~`{@`EgO%TjM7Jj|=p$8$Z|Gjl>junt zvY0hJ-aJ~gA;tb}Q_+e+3Zs@OLGo}R4HkwpUt9?vY@EbEJV@DRiD(JP2B9+VF6VB0 z9(IXt$)QSqiJtR1L{kBmEDmK-6lNAUqi$I^hdk?`EYC7@x|LvA&h&AVD{ItokYkgw zC^7!2CP1)awrq-kzHzw(U9a+AhyWwAeP~%!)Uqg zOjTqPP%esO;%bo0$k}AmI|4aia~ZD0Ty@N1z~p1(dXgVXF%_vCzKYe@<(u%u1h{u( zpi&;HBf~yG9t4pX6_y{iZ;#1DU%i#9%Y{^Jo;IB?$^sV@tr-oKa}#p7L$SZqLu92g zP=xaBPG3W(r-xBZBV=F=v3jA@<8k8=1;{XI6x#?!&G_vIXbq z!&ijB{%+(5+Y~gv4dDo-3!b}8|C)CjL}7^71vY>gHp|Fe5_x1oo~MuoEBzgXe=9+YM5`7y25Qs(*jdD&@+@(KfeNgitP?@()V01P}j;CSh=m;hGZYA z(6`~ljvqI&*LVf%%da1kzEgQQ=?SMF&#_y2CF4o487t5i4k<{9!W}}jBiM!lEQ*>a zT#TY@%%miwio_-gS_OPI2v?YmNw}Gys+??^1kV^Wb=&N{;G~>XJ%`G%3`o zRHqu81&(?A4eBMGns@Ps(1&QEx}yf9kENHS`K7OCSJ!###5VpX!n7dH4;Ig1maiP)sb+c4xzHSb3Zrdis>U4>-=t?np zhIJWsX5Y%cq+Q}B$GjF@ZCpFLD79Rz{4RFMtXjwp?#yZ}axHXiac!iz(+v`j1}{#& zWy(5tfLo8-!7bKxwF6`Wtpl!O+0EccZti9o<+s{zyoHkmpUUY4gt_TCgGzwxM-Q-- zp$(!*-FL(AKCa+EDedGg@+)#RS#I7V3Xz{PON>j71v~Zw@B>67CPj!Ok+bMUEaITU z+{5p~nZr0S3YZVsmRXNk5wh$VIjmk23+4@(s}qwGjT1sDBJ+wWnMd^V<0S*;OXg7K z-^?K<&$DSVJF`o(9T**02$~q{GmMmsoxbl<$cV{!$P^rqkGqd+s4uBYsgGC3Y29g^ zXt`8HS0iX{H5RNtu1>U;*SB+Jaj1 z?OVH@OXoq&0sdLf+*=pKkV(e~a=dyxM_x_dQ(ib9;KBLw$MxF9;xD^1y35VIx0#WN z@RP>z()IMC?8BE-ht~XZg5}!b3k!e?pa)R>L2_^EPTraK=E`l;;rcv#R13ljiVmIw zO$s6cLIc78VFgA8W(VhuXp5eM*Rn3!glTz5UPkw)Cs{J&lnqxjLx?vL><$@)GhTvL0z@S?%-%K9?U8Vk`_~IHb`@ z*~*qwkyOo8B8@@jb~%gL$4T0GpZ~4#cHcRLae+NFo#x`#aHID)Ph^$WR$hB=^7u(< z59eWtZ?W0p72E~1Nxq5FBLD#HHP|N6r0`5RPwt`bW*DF_@e}WzQP3pAMFfv%iOMqz zGe4j3O+`y><~R{?SHADV4b5rFF|QBfl<{R>=4@qr$QqPara#KN-~#Nu3R$%^u_KIU z=Pswzf`9a#CajL~r{AGIqN`$b=_j;pOBQ$+wAB@jwpfbI7VU2D)?y^Vc)_eo%gUss z-qIQ~tTkim@H#lHBj1pj$#|!S(QdTLT4^mc5XfkKJCQcWq^Juu>eJS5A-RYFsA?w-gS$nI12t%-b zXL~Y$=+x_se*n98eZ_sa*$D3nSMVQuInZ-ht-Up<#LwidIB_|*IYT(_efC`1+oVXM z7yv!!E&pgaE1#Ud@PGK|HgP;OykI$ML;zIz)_nM0L|ZYeg%yV5cTIS4y;kftzn}Oy z{M>5lmGgeF^z7qV>zoA0zc(KEJ-+x5IP?2Ha?t5r;{H$M{ulK61IJic*g5_SC;khe zKVh?!h={O}iks^5Vn!ii19P+@|DjF+LV z5fz8~ltjf2Hezg`E~?pXHnZMFw6<1%wJxt(T=*?_04cr(f%Us;0tc`bXri8<^ywm_ zj1NA5U?Kv;YXX1YF*gU3coYR$c0Xqr|nfFcUk%XBocFokbVJz z7F%TJ{UnM!07+449KvG$0Q+&9O2r(;#Ef15Raiy5H&|IhK)5zF&Z?HHFp1 znitclxS2(^QxpSMpqoLqZBgV4Vb-lGl?8P0Ksi7Em?I0sh?;3I@r%5`Hj-Hk73)0m z8UzS3Wss}O89w@n%Q6qhv&3RQ3M^8}xCCiA!|LGosQL3%qso&p6HUs1+)jk2?DTCD zY#bv-K&?ZX7%-WZU8ZH?SvBt2nby*w2#h@OrF-3?P(Q5 zH#*QgsdhGAV^69YP?Oyk!GoPDg$qKUYjB{Z&72$jFkAhk!ChDinCR0}TNmJb8+w}G z954An4nKOsxE?ac&tKGeN-zWhNu;>9VX&taXWJ_J05MOaP-BWxpdUM67an4;ydlOjbyXRkRgi z+XsT20O9%-nl+G~5rozd4mW_s5pIZAdQ> zAw^D9&>@k?1jHo4ry24xOzPl@0ue>NTM}374oJP==dTbmgsflQfc-*{GC<~y;8uWI z0#@sIa3Ll7$G1g2(Q+U#zOU~{x1saHxAtS~;9LUHh8pxILkf;Ug3gL-LgHMB3MXii z0wWRGBoY^cl#690z!&4R$4VwT?Sp;^FEGl$L?6c53)hO-F;+KPGL~js%rMDNm&G__ zH%DX%d4#kd?9ZNT;ZWwQH%Io32wQmmo7Y@h zlUx(`n%Np7o&-nn%Y5?D&@I95?pr7`GBce6$^+a3?1RQ1%3t~!4If#QvA)G5$8^TT z->@FGPw!2yv)5Ybe{@l$K2h6K`=%17nx_J%##hLyIV_?s=2qBMC@e5qlUo~EuUfw> z(pSb#mri@+P3Ic0e(b-7AJ=t6dbd4W-whn|9VcUFVfSETVXI-MvktRiW!YzqWgTSw zVjXF+)P`vW(>~Dt)vV8$nt48%U`@v$yaSs)~IPwB;PoNqL)Ok z!y(kF+p6LrtRTan$zV{cbWy;slwX}++$+(m1NiZMhH%X=S}41(tRSkO*C@}ZXvc)4 zBC&Fox=6c-XPDEZ#VP8Fe@}3jHl;urE4CJ4T8&+u;ehZR?M%FYnK zMWmD8J8e@}F?$dcJ{0xyQl(qDT2oY0a8pGvuo#yZ3v`&yvrap0xXy|6rt~oeo{j|z zYU`@}u;)_f+~ZJQ}@$ytzMb+$>-IdR~4}0OJI!g8B_z3?>ey4#5w@422Bt z1~$+K+}9aIz%XY(s=I&=BK#`i9?A>F2CIkm?1D*8%UVT#>eZ&^>F?~k>h4_rFt;HS zzIVC{y?0K}qMO!uy5`(`P(D6glVH%N5 zRY=og)4VX5QB)m`9t$7EXHLvo#3<77!)ozd4W&?UHc^(+Ma#T1Bko=HPBMlxn=Duh zrqQ}#(?Y^5|A*29(&6oX*1r8X-q_99i$9w=n}?f4kNbPrYaiRW4SaRzQjP9*>V$vvVd{8Qn6j_SeAHg_@xUP0b9tke;V0l-)qWmh3 zDBkk#TV}MQvz%U z@V^!w4!w$w#0clsaOLl4+Mmm4@|GsziM|bU+;c-RZFk5sNvG%@yqq=J_~z` zUBd6oOEWn2w>oORuBRqEJQhizQBYx=bPo5$?0Ap1+)TnUWGvQ+v4N0d3pEk>DOh$>Bb0)_^>t+hL6;z zT7?1d>GEkB@h0(OiE4@In5UQx@A})J%kh}>`MuCys`r+M)Y-~>e~*`L3*)iDOPkq~ zOabZ5vL3GIrF*+yroZ>+J*+NDN~1O_dj#$S-wIw#ZbuT2P8A*%QuAX4Jba;EuRY1_ z%n!F7LT+CEXHxrD8u}+SWoBn${x_-px9;$N<+Oj-U;aC%{WtObkJD;d{;Bl*ThjQe zJiAf4~a05o*{%82PTuzZU@~^zXOp2+~xO`v}3;PBdcX58VAUZg^p!98}2D0ag4^u{EQL|JuQHsB( z6k@A@7%LmfUKhYg^$?ATZ0;Q=ay|1^!6#L(o1D0puj#RSNp!-LuWpo(x3GPulLM&? z`>9G%RxwyY?e8IiW}!aJae4BC)7DlGOGkyUmfBd;>-={VE^Ty8Lb{ofq?M}{_FGu2 zAcGWfS#%O4e;4H!`iOyv!b=c;ebg`Ehhl?VMv!jmnElY0!Hq%Nup|PUB6)`pL|%PX z`i9VNG#mlkV{o$0Z<_EoU?T{oFBGGDwgQ_RQ(xlF9u*}#d{@poH)rj3M_bZ64)6-_ zHm`;4y~8;8M1BY3;EO(d|Khd52Xt=W0lNJ2ZNG=jV=D{Bd2um%v+?*?tNqB-w3@Sg zTpr!-p#kOex4ye8V8Fe=gPzOqELY|}t~2@`-tnanug%bbVr>oG@Ryz4O^f<%nwQRa z4qAuTlypurdvLaSwWrKHt=I5eh(k`jAcw5Z_rowNHU$)N z&B-O$rY3jA_1+*o*8P&W9O{8IBeAJ_U$!#}kPVp30|?ksQ0@+jCd`VS8<0llkoX)m zHKggWmW1Gsq{Cc?F4uO6&IC9hc6wp0BSF^~1Sfw1JN6#3JYD$1`Q{%oRAE1>Od`>4^8I#uo|qTY0)+GKX8-c0&9 zRsHTU`XLCICj!jF8?4YX*rp?`n}#;kuA5F&H!MGDR5N$os?oHGJ@Y(|qf@m!wd|X* zT!}S;t&n;N`EQa2q6s*leuTYODZN$!Yu`2n>N0pk`ztdT*P!tA34C5^P=uGD0b(ED z)FY~q_We~!zN*9E&+=EH&s-Cz?^Qku6k(`d!BD&0KvR1moU32r2V7`eW{i71)c?#^ z9B&gS{FUx>GtT++E#X0$YXNzRZ>;yH zjdw@xsz*0Y`}@-QENVSK_tsg*S%*MQZ7XW6L3RAb{j=*@zVhmTJna{naj`G#hw@YMM@oypz1B#ryqBcV@MP1Jkq>X z^`tvZQDz8G(_K}a{CMyqCf3#Kx)&THaG==Uuw+p5YX}B+5b-Ey3h7U?zkTDa2E+CM zjsA&WV@Y1D{d)PUKeDrLF5X97?Gdu^f`?qs-QV87Q`*-cWZ(FE@K$|1;*ri{@x7v( zBHQ}MIp|lvoX7!AJM!W)t|sq01oGr7V@vu4F2xZ#PbS~Lwh+dh=(P&ecAoeH`0+F1 z6p?XEoc&fSc=BsrL>%lwq6P}j%w4)y?VcBA-RJc#*e8SnOweTPkr(Pu3$)OMVz%s- z3TP5w>#W8^R|t;7S0rB({kj7a462OHVs{C&%17P@vpCO|DGt3m1uXgM;aiqR^r(^B z7sP0y?w4U*t0aL<=LwceCnI|k0P4s+A}bgNBXy>ssg`yImJj&Lw2 z8_b^8$1=_9QduYa4qmxF?si_J#0-gOxq%WNB(9+Tw3#mI)=d)?+3?rNWV;A_F8j#I zn>)#%{POFEBus8%!bSD6 zRaz6Xdh{pi+cqQI@BXRV*JRp1fWIk%KD3y!X?a5%KQCcPrX@ zDj6z@u?NMh)GR#sy%S@m9XHSbsbL8l;HLmtZWqu`Ctu z-hSYDEUy#7mEsuN6(sH~f~oi{DE!Mo_0z#|<#+7Y0T%9fy1GOF+Z}y?|*@1{&i@vmOWpnS-Wz zA!##Vt5O~*HTmGI&giJ`ZZB9z7=ZgOc$TeF!;l1iZGC^us3Dd#N*E&e5(@JOdb7G@ zg`!&zBXuVENb=f;`m-&f^a&K#D+DhjmFmeDmZVA>f8-I=c-9Sh;04_0XFAUblMNx$dwWNEO3&tHasLxzL#X-Oi9(0yX5WQKPap zpHD;uef{a?`M8q(-^W3EBc#P-&g`j2u4fe}MBXVUt?pe@@4D{&I#`#b5vM}OALcEz{DA@aRv*RV4mwi8&L=4ugyaVWDA$?s^_%qpCQ>d0$5Jd3xTxTW5? z*T^cW@^vM*sVa@deA6kJ)cYP>OGPT_@@sfrZN5-2d^@N=nh>Bo56yT)Q_jX@pSbH0 z3Yc*8Gw6O`AhV@rT!TMZn90v39cTRcZD^^R|G*e8Md0HWlRSfH6OLT}rTVQB{4_Pv zu5lSG{9?c^idh`#79Ubcw{B+WUGME$n;In<)29cA+j@FZpRCM4J0)JdSDp_ijiTJ3 zj4^xftzEUZL^j5|b+(0uX#yVncMN#HtANSq26R}A^l$72fnBBPyfZw3o{sL18w>%z ztMrpzeSH4SxADpB_*#Lk5C0J0iYFC@29f5bL!YY`^_y-7|7w8tM3W^=Goc-9QD`!b zWu?CDtG!F>8GHJ>an`XHAW+0Do6mRQ$CPDVI?0C}$J+*9))_sV2^V+iX1x3oS(Sz# zGpD^J@hSd!s%8s8fAe;jlZ6RTqeHw9=5BcB@)pY!WNMjq={+ggEKTFRyUCeX8)CtS z(OdX<`1lqe@@u>I16iG(p8iI&ndPj%@N<9~l#UAaFI{uI3wS48wxx$({|BQ$T)$(I zUn8KWrjg9nwwKP&p4H=N%1l0#jLQep#>3Js`L1+q?HS?_NHi8lS^C=z&gs8#Zf06{ir=`~Ah~ zZ0}#*tZ$(?sd4J(?FaY!@U@Lpl&RwpbT`!jp z))GF)d?BR~7mxcz#nX}RtE$rJRrM@0Tc^-;DwTomSU5{Om5q3&7VL&Wk6$pWjo{#U>i&kT0f8rS1-w>@qdNNxD{ za$eCXHopCNr%oNNWz~r5u{$1i8YXr(GBxt+U;sN0nZvcL8{k#1yZu&X2L_l$Ox<(O z&|zUnxVGyqz;!S4$J1~vt3D3bvhL$>ZP#6ZE6d~bL|n_NkHfXB`#4-%b*C7EHboO;aCaV4`o*WZe3m>oSxBFhRw8f*ENY`e}R(N1l;k)=!IDRs9%D4xiT7a>Sdw1G?R=A@8i8l2fEnYvWUX z?Z$0{68!8k_l_>6C?V=9O-jJ|W~?O(GH9`uEnbir-jeNMSP@x;Z1LObF-9<07-fa9 z`mI8H^_LaG^0x~4@=uF2t1!3zvrlbV8mn_F)H;}f9{yrH^~jI0PsN7RthMjIw%>^b zv=qes`lm7?guTY5k^|jghqaKE$O<_V-yy!Fw&)OdJKi2SxDiThO5=mrlp>4F7HD5f zkvYPwjgqm}ZBMjB6KBN^i6?~CX(@ikrtfgzJGGAU^SoPSpAq2_KTHiKw>*Rgm0{ih z-mv|j+b>+by}Z7?yl?LR^TyKd4B|i?mSeftq_NF9o;LS*7`B8kX=8Too^S3iC)op9 zd(#|N_ztG&3Re7N))S*y?)v9I_U4ZSatC4H$YKlN%)j}*@^aYpx9lyS@BH4fzx0?X z*i*_xk+v>D?zaZnZ0f1IEr54j?9TRP+VZhC`~J$z0?{JCCd!Nh*xO&f|B9{UScmxf z-K?f=Aj!er?~mH6!BWlWfK8g+oE2=>SMd##t&PvsSK$raQm0>C{e%0FDg&G~rhT6M zcxyk{B>rA+`1=dsqiUav=2mKMef4*~_Rq1Fwvg72HjMV;HZ&OFjIVx(a7|S6->k3x z`AbX#@Pn%W^Mi*l{Pi8I%%x#{|1jp&5NS3Bypz*+x0TjnUW9G1$n5)@4SS)@33n!z zh%su9O&Yb=M1vvUHC|3ChQ7A1V+6=8vWzXAGaIck^PFrX7py39ci@M@W&*_zeMU69 zuWSx7A+8F#^9wSrqpSD9U*z3J7bsk|gkLm2{JsC^O$0Z=$l%J~_+kv2qqLvENicMT zyAhkfrqeMY$y{^jco+HR(CuU(o5e6|Qj#e(8U0~nmezcn@>WnZ_n06{8u_!l`Aopg z=6-`o$DDTC<7KnI;N8F6e7hz-RYopxPo|~o?eP{)blh#8_jvfQW~~lZdbs~~j7_wL zig*jSQG_URqpNO&>9-qU$AWdB;GeRr;eUj^H>{?jBaFlird70|fe9A8JAyOKn@Nl+ z6#6YbCo}dPaT$*A#neU1*p1iB+3#eVOt#sQ1&`BR1AO9R6k`#{&MyGTzTM^q@b7yB zhZfj*4+_K@-R|Tv*l4chKG^SW`1YiEkEFROL`#KHB@?mMxc>bLOh}m^SzSqxOs%B` zPCW``zd0#B$gwqBOV~|Ee6=Gp+oNV?&BOuM`Wc}Q6sbA-u%V4Zwf@noqb7wFCtBCH zl~||?NZnDXxGJRL7>zZ$LDY2nox*^_kU0VWyA5G0%=hz89Hq)o8LCyIH9TJ$=jXouqeaD+L}Af?apJYJy$sbW3N4bZ4g^+N{O!Yp)k{#EzV|o zhoO9KrgIdq zW~BFyAR%np|CP*Be`#}X0oaG}P85}f z$LV9O;c2df&F%fo2B%45cp6bhvGn`Qay6!{zPUhhHRHhYJjm zxSMQ^0JkkiWD@(vyeyt6#rWpKOR2#Vts0sUGK2_WgBYI!9RwCWPRW=XJSE3gm@20_ zMto>e`J6}PD~ZN#>t;j z5L(pqc~o9})ok>?g;PHh^IAc+X9ru4NZSCS32Df|ne{iIk;L#9(#W0X*A3CEA=#aRtp6Mw=}ud<8@6hY`z(RP zuCsk|M-bQ~x2^~z$s0EubNuM9mv>VHf`c_IdcTtF&yyDY(fIS}#9q`#TZ9oAC6HG4 zk0y{PVwMre`wzEaL|RN_hujz!=#P+Ck?oZ_h)A%}#ZduMo>_kvZAQ|AZ!ee4`EX&c zu-Tlp6{!VzAQuMbJ}_+^IqjvOiG6qE7~-Z|xdKi}_yS@iWCDBe--o9cRsDt|W2Lfi zGIla}LJp+ePTmYy_vyOpx6m2u&4)nDgfZ+eaH~qYI;#l3(BP$W{OoiLk^_|Os&aCT z20=uaiBia54leIK5!A9igZF_jRbssmum`!!ED9pQTvm4|N0`f(HO%GPFj`AsCOYC% zU@ogyFqbcDn9H}1ftgdTp8|7Py@I)XS;JhueGJTdG>t^hkEV^XdIfX&vWB^Q>#B~g zdd~*Zs$Ye5?zlvFTCnQvJbdoHTyaL4Vc_5LEc9-tW6P#4Z?Sy1HE+BqKJM9+4xM$r zqLe2L7_4plpymurkCPR}uQ@Box`%~2FJgS0t*GxgoyhT}z4#^Y^EB`$W+18pHs!1W zPh%AXnkpcpvI;ys)Nvfy=&1sp#jFBP6Fsb0NK{e8#Vooyhw8Aj8u8IpoB>K6*EmcK zuWO0Jiukzfd!CZlm0&me53=fT!1{d9K`j$rMD>LNPp2#QeWKd3enVCjSsWU&3jNv# z`ibxFkDD=xF%yD-IBCi%w|o!a(~wne^&YkyotuzVKA6KGZNwPj2hvpP7*SKmPKXV~ zT^P!3fiolO63S)u3gz-;jdJTPrUVyi2+GY+!-R6X`Vy4e@@g^4W%UZ>@@0*3>+K^@ zZdYG|a$DRjM!BqBp5&&B+DM9h;rU`EWt_+jn?ft6K!oS63&r5{AO?gU^-}~6aLB_5e^WI8e0;UMZzb-y!5Z+BjS}v4a6&_OHn+e`|KHo9cgb;>nPaq zJEs{5Z4w2cUG)1^ghC<6>C4n&9+!oCm}uc(eHbxjcs~a;X_jHmhg{yjfB6G;(Z79n zclqYw^6L5RWqo`9H~e?~U+-T3@ZrUO1T*`&d?OXSIErziGj@|s^s@bI6mSWXcqLfn1;1Rvqh9pU zcJ>_7NbLj<=W4`HHQo%dN|xSg;G>m66R#`LN*0Hb!b&#J@AmRBx6NBsPmzMQ9NgU~ z9hrtR3$)1GQkkLCTEPLGQKSoTi0x^Sde^k`vDpF0|E?qHcVGNv{Y{JdwGRGp_x1xv zlY-goN0yPA>d2s=aIUSVxXQVpmnq=I$YtRQX&9l)#xmL{S4g65cYfc|@@#u8T83C! zYj5Rf8RTbaIU%3mJ)@-W&pI+?Q<~r%i3l0Ro6sW~8lIsmxK%MxEpP~%;pn`Q3FNe_ zgMH^_GJ#QW2;a@x2}^5DB-HHSr@`sAlB1s9^YgD_RAB|pw$~y<5&=vui%LMey}kbN z=Hmw)T)pNj-dF3_U#%Z5&+i`IfAja3hxd0QgDh7R3GfCzMgEu;pl9R@iK%EfjTdo^ zgbQ;5u9&?E#^~B%C`#uRrtQ3)G<%bHx)II1GJ7Qk(4o2-oGdUUi#JNdIf5R+7l2S?E0Ih zJo+=)ujCGBRwrSHnOLfqZqFtkI7#S~$qHreOtO{ySAdGJr3O@k&7!Ol$>5)$YE_R= zwO&T3)Z!>qn8TJpmD~`r>LjRI)gx4`ml3KoRvCO@tOyZ(98}V!6I5k&H%Sqy@@1fo zB{L|4aiXg*_k$TsV^u;`R(BmosLGeFZSBa*^^um7Ex4uY%P+@QX09|%q}uc>*^w(V zC3!@}t)rz~+p+Aiz0LI%#s112`6@onR#Zo}KV~lugOLTku|Lj0w2h+d=dnU-f1H7) z3bsFv6awX`i5KRgX}$ zUPk7gvGgQ-!VI<<@Bk{>h&fG`6s^_DayLj~DnETFElvIe^Tudl?Y%_OXQ9bJWg1N6+%)P2$ z#?Q#MTFgBItSPkBqSmdkLl`PcQYbfcUEh-!uki)xy0Jow9cG}Zg6X=kLQB`JDi}MA6KX=~ea9py4!7mG9r zwWrvj?)!lC)n*I`K4XUpn;6kXmqv$v1+fgKL^xqzHmDxzCk@$n6AI>VK$ z<2FAVbSyTJOeCq}vif5%F8SG`F>Y61f^mzVEsr2D-cVa4k{>^iVFeRF-HrX zC+5#$GmtCxNj3vtvCs~~x@~HQRRz-yLj^g?2Mjj@t<4T=4qf#yWQR4Y$|}rhhuNn9 zZLx`7I}DE_+F@34oE;|Zu=?Dz!#GL>J3Nq$W{1G%tO)nV@C!y8%2wnW!PkTBRWWpY zM>yl2TpZj2p@9ycnSJ1lH@he!d>nU4tlb5UetXDgQ%<=Ale{ zB#RSJ2_XdR*MEGmUA=!nh`s3=bG?dxsMqFoKVtswx*zywJ9qEF4@??Bx4R8Wi%EM|qP)uj9<71|M-HuyuX?h4Gxkid`_2;mh6mms&g4DfgAI3kWDl+b8pApt zzK)$rZLvX!FwzqlDmZl}LO3w^+k>wE!GRD4-{+!n(Y)x9$sc+kwK#isEpvhbbf};E zvF!hLnpZU}Sqc8N+x@rPt$-UMCPZ;hcOyqv+fD1luH3uMtIM*q_=a`M=|;Lks4yH` z9E0t5gY*s-0>BASZYf}NIib&PZ?DkbRK`_VJFXUD=y&At=s6N614hrnQBkbA*Snok zPNp(4L*Qw!swADphr9fPLH4#_CGxbhntWd_Kw&JMb%+WY&)!tFmU)mTGEOyJwL_?? zEZ8jGJcm_u+T!V6k{o0VP%Mjt)C^K%S@IW-dtII@zX{n}91|To|6jJ=8dN)3i1Rb@ z)W%)ttK%%38Yb{7gcSiIm(xHZQcHGM0 z=c*sy4$x$ARM4ajoue+1vuiYyNtxd(%Vgs9P!|TpMQ47krjgc?&(nBV3X{=cJf4Si z!z5J$I1-W}!fQZ)WG0jcfn1NEBGqEbDsrkIe)uqD&dHP-TQsq47spj{q7$J~t``ri z=c0_S5@Hi7bz}ekKnm-7`$g6>N?)sZb)`f4WKN@CEWJ(%LKyqgIn)?jNkUZSVKoSPRR=jd;XkA62m1J_+uu(vjV$JRx zbV_JqnBNycpGCIvv9`0RMTxMxx6%>{d4K>XChlT`BORJF^bK{uC59pMS2$Wdo(3y2 zKc6~mJWx|wL}P@#{*bVLJU=N(a5v0Ugc??*Mv}S@`V0t6dz7;@nj*R}YbuG7asC|8 z&O)nj#IQR<&rWmX0rJVq#w13gElasaYo$kSiZh(=mBZEztk|J`y;k;LEb zt5Kgk~gNJzO83#IsT<1pxgjS>aft@3F=+trr z=0N1R-3vqF)!Q2q{rR%AaL5O~v;9&+FA%h!&<=y2rsGg*4 z=x>?e=Um0;Si5Cv6p`4SBgXBBiFZNv6Eoi!B3-h~We(7^XVwuFWK%=8El_FABFD~L zDD6`Sh_pOYTPv=PeXs@O5|NfG6bnfOM790v*#u6{)CPyv>#h+D5Jt+`gN77E< z5&VMWTyB&a*s#rf!Vm`bdjIm>`wtWABekxsEB=V>dv<-8sBfWa>0+g8Co(0al}k1{ z&TmqeaqcojsY97X3#r%B%h$JeKaWjdYB5cqvi$7!+C0=-_dQvo(o$+_f-D`DNo!!L z#_m{)+)iu9!blv~YD_T9swveP@oLI%NA#4k$%>%#5)a5=Qx>*1&z^+^-&9U~^#Sfl zXcj|ID<_xd+Ae)A#iRtLg(ye$Y*{T-Om05Dnn*Ef-5tZlb*=7@v-8sAo-5`qTuOa^ zcWS8*)=O-t)eCt}9>Y;VY9dXWbr)M~b+;W_Qv$IhNfeOEaWw8jiJaH$ zg18WJJ}X~C$Qg36#YT51GOZaaXqeNHe#Wwtd}GL^23>DawhNRl=6;K;E#$dvrn?2X zx9{GJrVA^ZY+*;}P@5j}1gIXnQH&amY=fsPr3!Wy$u(rOfizi2y5%gm{99_oxMHp>AFXoN5Xb;AM*`N}G zv@`D>*T#fFri_0Jx5|l~49ZTvTAeAYvA^~S<*t6tWXdEKq6$YbLo7yUL^DbTp8*zf z@{c>o$Y>`la&2)O3$LR7yiJxI7^(|fPh9JGWSij^dus*^ z_SMzjPJkYU+(=_$0tEVMxJdm3Y7e0jy3hvt} zTK_zw4$FJ=sB;=~e$>DF)kl2*`hrp4E}`}Q*Q0e&Ls!XC4uY)a0+>(4?dS$g#+|?~ z!~wJBe-a$h$hYGwXl3hLa&HjnLh!794TA2n*7Y&&Y5y_5#y$=$lO@A{%&)PJ4ekK6 z!da#v22fT2A6d&>h&ok4ZR^_zbcETff7~upA&7Iy!^|%Svp54+4bTop$gOAY=fL-i znav8|YpK8}j{^6-e|iy~ef=rb$sN9!i5WfedR5&|;OZu!==Yt^_eccNPJt*}t|S;E4%7qKYcIu&tlKA5&JAFJcy`2YvaupO~c zD4rWur*L`hp{Z#oTxa;RydRG7*$!e}RmrkOJOUqv%h7v5U#k{66 zwzp7{7&->~>_U_(-dHS|RhSXKhSC(ZQ9>Us>Ra@ipCYAB&+%EHhn-m;I)dz`Xk4rg z^so|$M>9!O?m+y>#Jv_pvvGNl<-k_u$(3)L%#l}Ohz}1q< zES-!6c>Eizj9ifmyeU7?JC;CEK}?*b(^SCay#QiD)8neG?PL{`LZ=+Eik5FFb-qH- z)0|QlDzp^4st2dog$h#c>XQ^%g`lpD>1x@hoGLg8Xn&F-JdP-NS;eU21(<^FmZbo! zK6i><_`Xo~@(IjIuehG|#9WZodWXuuk0Y_b7}IpYn({5gGIr~+3J4#b@D1G6My9b0%^;N7J-K8^>Nn=8)m69tA$$!d;hN|p zuChaFH7g@$7mhz2afwwMMQoc+UB5fhTI@TNQ8kvOs_fKa%P&fdblif3Ld5cJw<|6I zV1j`jOy!zK3^SX)jFw{=A?#eUt=SB9k@K+(`5RkAYd8>HSF{+)@)ohu;YTZ5Ol9pB zQ`Si~Mxo0DuuYy)W@6enpDZYfvG%{&mu+^OCt?uEne89IN5jL(6F|YFpQ^^9)B;1w zG^)D)gnysb{)W)4(=R7TQ<3qEePihb+t3P>)!OV)fwl?$oa zigHMdtdF30*kB2OffHER+vV^s6+inc{X?<(oqxHnM&qG@Y<V4ULMu@U+VF(m=SZA2DEa-!xaC1-9D*sl_WL}3idMrZDp7($j(+i%DCPR0%@TX{ zfkTc%stZukZHf^kGp4wpuE8vMwmQaSA4PR2W|LGWu~a#=xS@J|IEFy3@%{U{c502z z3pztBDMpbf=OvO_jYmytEj;l0gU5ocZ{^_{#RI80g9n(lB%U~sqpuo|n!XYq(I`Z! z$VGSzw!Rr2*dad4(s!(ksQH4ZmQ03-ibLFvm5meC3ZLCL|9Tt}W0Mhys>Wl%WN@%- z5gzNS%7OPQOhzQC8jl6@VMMiPJ}To@h6n2Yqljv3J{(b@W||V2L?m0Vpbkjx1gXLj zAzgnF6Jacid*|k;Ykl=ygSw)$eiSyg_bEr?Peg7=ZE@1^F$fKKu*ht;ga=nv`4XTE zp{MiUNM@r5EuuH30XWLBUCDmE+@VW%aB&=H}&s ziSw#u5zN5U$7T`K$NDt}7A>2h&a8p3&(O7>i&2#e*WfYO zvkM-uYGmN3Y1(u=#Mg+=_5SuiZiIc5lJ*p({T|~Ci5_KUaBi4C+y?D3xW`)xQ@a%I zxt0uWnH5F332qKN9*-Ps@PstIb;iKy2}vYB-O@DNtGEfDYbo*fu%?_8E7l}=;~uvr z{U)4H1TL0lVWoX|T3ES~(QumEZk&Jxr(`VLx;D2K6Nr&jszfZuw8dZ4 z2iO_}8Bv)K5?CP+G6*lj&1tQ)#w>#;+vnU;7(=7xq&ziwbRD$r-wSfs;I?CLd^_VIDL zY|9m~o0={Q?x>AJk3pz0T@B*f%h_mB?TN&brl!mG`~L>ScjO?TsV0&5@H7V1Bp1bvr6kU`kH^!eu&JV2!d}qv z;m^wX_M-?bB0k;W95~0<8AAH4wZn^P)_#l#IhyHvKF@sSPcd}Zxy z33#v_Vb}|YXYj2OS$mMxt19Y7KA=-}YTHD|=dp^yRTYUY*lc?uPJPArJXTRX7B&IH zY!W~-1gJCXL-%6N3`YR2v$u}KglLw&mOR}|8Oa!`HI~WW34>m za)rwY5DGXYMLIL(PW6@nUyxB*=f)2GXvbS zEltDCUBPVpnNNn>EHTsLJ@w%-k9vT6rlrq9pM+`zHlNd{+a+tp2~>p02>x|Mwo62o zi)Q1HqRqhMFu>unR)%5L{^Nbt*z$f=^p29OeswIykR4@-p*X=~F*I*btyP7gPyFOl z84O8?m@>a=7h>4jF+Tyr=8pMSz|i*1dm=eD#!u!*6qeia7VQ@Wc2}o1jmNJ(0Vm`{ zvFwoiW($aP`53f@J|md*E*$C@*RBR9I=$#J+N-4)q#N?6}OR6E#-)&yPU zvC6_Z?L`&=igj%7AhRI~kKC!aV2A{$_9PL#5kIS+4t-9$DDn(_Rx6e{yfWp83teoN z)Jvt2Mxp|(l~BOtHGB~^CgCZHA76iJSK^MU&bb<;CF1~RLqbe4VQ}H7svHxPu;VY^ zK4SlYZxezotGj*9qQKW3!Qtaz4c{(R(+#X)^`(KYJCY+!fHipgIIsrQ7lTFM?F3kZ zw+7a<`;f?6)`})^H>kcCEaxpQX=^g%*Y?VR__*0)h}GOCGK0IU?r3*z6^gYlT7_KO z;#RR~OHQ%1oG|DV9^s{7!uw4SyJkFmVwkXpXBEMB1xxX9Oeko7D7745vh^=!m{6)k zuI(c0J(d=JIJP*BQtk+9`y;~;&!XhdE=)1X(`Iu`0y^VKB}(1`AKZ{WRH)cCo^bsv zL8%H8E)tk{g`&y(XQ?jj_y<%6Gn>T63|EbqyWM%ocB8e6ilkD`%hLoHiG&=f4e+Q* zO`21U$Eic~hU5L?O6#glg!2iLk?VEYWJLNJ;8D?6HXo3hse>GcT876+n6RxA1waMP z^(jl=SQ&DzB&sEof$0L5q_-YTO-5^DG(uD}D`R9b5>eH7ESQX=h-v{IBVodyb21W9 z)p#tJ4xCcotGdn|>`c=-*`Ux|%3aXbA ztl_D(V@>2&BWL87B~WY#eB)^mxv5EdRwgq01c9IXTQY%fl6#mFERQ-jA9TwL&DBQU zkmI)sP^&qm-Q!USt=iU*t)Ii z3S&{{;3S$eYlzu*LJzGR6lTpz6)fRH>>(z62%ahL2X;MVw;|7>iv_1J1BFLdu}+Lp z0c9qjfW(*yDB?iE2pNCE_dF%DPm5Hd>v*Wu>Jq^N*KKAzmJ{$dO`|N+X+e)~Z?9k7 zz5C(r`zwygf3@a-`&Wreg-jb{R8!OOT%ED+*{0XwpBHa_`sRy=yZaAce6@b_`kOC) z?vx@uZgRqG(F@1W52cWBy?A}s{V6W&xw_>@{%ljI$!;ty$HR83I}59m`CMb9Q~1tZ zHwn_t#x%Cbari!KFPuG$Je6Pte$1;KY0{Clre{Fj`O|6g2oZBD=E3U*g04Y=OCEhY>RdCems^L9P(a zgL?KVeVH>)CT#1r3U^xWdtKZni)-yPzc`$-!VrCcFDm6^8| zCjcU4lO3Aqj%2$cu~_|JDS-G$DS$YVidBS6eB=!>HBqC?{(%%gWDgR}B;6CG7N~ZF ze`t%LEZCME*A?Jl3%vt7i2!Sou(u|2R1<@~_< zz4OL0G}vfpe0KGozq4&+A?=R1x^^LP6>*lFy=i)pvOWLcUV(C=XYs-1BK)TaMN?`C zkC{?F)j6<6=3dCA<#N&4mL(`c@keS!dCgV6wT%(hbZaf}ELbni98BKB^BFwhvKXo!{JeE#Iyc~I)s8+*_CgXTK zZ8AhEGf5am%wZDEb`}kR<{5;^s`$uZg{C>oDvA)b8qKQ3_3xHhbtI2MQNgTQp~6Tm zg#?%0>~gp|Io^3k^VioOG$;F%iM#-1NXB3>KKGq1javfvOC5RjV2tB za0eTA2p}pPd2bwE2)#{z=wsWpYQkugV_K~KG=z)B)Cqnry;E=;0}}-$2^NU8>uXLh zq5&viBiocdQ3GhBS1<*S-cpiD^5&B;L?L)IhJ#qu@d^AS44qoQ|6N#dJ%Ld6N{eU0 z=xh1`YMkFdO^BrGik6^07TEeAu?|~OSaYlyCEXzS(Q^1Y~7c4 z(3+Dw&fh)`tO{1I`eO16$TTK%)cXkhSXHj$cT{M zZFPRO`E;r2+f=tQO zcdOXcO(-HDoobyOvWeANMM{>2qrKNj4tiPP^-^Mf2q!=kOKuX#vFxE)7644GZ2-u3 zrJo+Q8<2{yD{Fu85L$`&qPE2k@M|$m+2bqIM{G+83XDz2Cco>rm(D#AWNuGn!*@nx z`_i5mcC^c)=9n55b%0qTGC9y}qg32ZOOiV!2V?(~7sp-VX!dtCrc>g1t&?l4E>`k2 z9o+Zdx_zksHWmAQtiI)>YOCG4SXaoGW8}uOF}hljQ=@D94B%v@7z_PByPY=Mf;9G! zx^8bj*80dia#0eOHJ`idwie+u;4F8ujNeE|A`QI7_oDbQ;t$r$KR z_X2`xjXLXml<$nu`l5u%b&U5I=ufBh0q6@xJ*GyU8uh+%O^t#6gi+U!KFRdXRYhp! z(wUasPJ46}w6ZnB&g7ooPKy{zriAiiXKvTW%>dH!WA1zMH@!xicDvaj#cV zb4F>Bc7rC^alj3=*{-=^CaycXROPIF(3vngj$UhJI!%XPW^g~-bM-2k*tye15|wS% z`9wqn<^hg3lJ$~!ztAYEdb`Q|g=^4&zyUA@MDvvZM}v=j_@*4#`3r~7%&4!kQ5T<> z9d%$9yv}2R+9c>1I88{V&jMJa3s&xq5`>I?p!-SS8@$Jl zJh#^Q4TYzlY58d5&iQd?D4UXxl~-dFbclTc=!I6|Nq+!(wt&Y*eIKImr$8?~7uDE( zmQgqNQa+KQ`kT5R+1>MDYWqN|IyX-tiTrg*l~)kG%4O(_p+p(89RP%YV)v0e|y~#D(y&R^qtB=dz9(*1{-$XUM%C@`g4ah zF|X^(iuM$WfR3fI2urgpwHHuJ&7n7j!T{gE!U}0`Dobjt6p)7;|Nn3s>|RMuxSPXG z@X7AIWeB)W4&tz}pbU`{%Z{Xa#GtM+UzbUbW!&8wTRfub@66qh)%In1i_9G<@yZrc zS-ZvZHrNPA0N5t)|3kOIZnQ)4T7uIu-MNB^D_nnRdpFNS!E%MFQ#ckj^{?%w6g0jK zrvfH!gH=fTdAHTsErP>w<6Ji1vfEPN`h}43{C3#CaJJT|_byaC-Ve*JYWjZI<>BGF z-WZF-mMEeFE)y|ZE%&kTaCZpTHZ>%WAcM_rt2;KV07euFI#DDXj7VcdfjzMVuy+>q zXEv$Aa_)v$t`$17*W-SnLzjn#n;%T>w!zy);o*!(=k8J&Yqxys_LxUiJ4}MpdTY*- zG%$$JIF9XsWhmzd7GN~=Zl}jB+irXog@b4c$W77IZlkt57eJKrB8{V1z{}_?&P$EQsEQSgbL8uQQGtgiwQ)R#5y=yH0QM9f zpgA!amKya5lW`0YB;!a7uOys6;!`K1#$)MZpvcS-)iCME1RhVDjFIise6|EB>D*io z3go%;41(2_J&r_VyNbd++N#NuAnDe(!^6u8X0Hwv+BWM1S~5e=0)s-2J)$Gl!?1{t zBjMpCP}(s?n4(EK?V4;$(Kn7vag;qX~^p}+L*`ZHZnJr7c%Ngpz!?|ONa=6Ushfa1!$3I%f$hGeXcN7iF zzz>6H^hQ#L(A)Hf;{2Ek$h}Uuwpjn5F|?G_*wlv#fu}KDamS`i9{H*T{?@=L+X|ZnB9)lJt zU^Q#7kKSCdGbYbE-ezK|Oyj8d_+YAPw2%gwCd)RE9Sa<-o~k9hroL`LsjRBtE7*54mFjXxEh%0(-Q4O=y{o4HRQCPJW)oDrY zy~^V=HBp~Jf7N!y+--|a38Al}NNakCv-awV+phRWF4G-7C6p?=EsIxc?20j|=O{Ef z6(CgyA4qk1c3GL5!Nd*ZHbKXCm1Eh!>7#uE-vq}DULvP^W|d>vSY_cXun(pLZ_mib z*mEco*wy`B3GB3qs)qo`bw8?y=?57 z6k4<)6%Gb^gogGeJF{+uEl$;H>t!66)uRU{2|aSb+Mp(xXS>%-HK|1c>xF+SNryKx zsE_0N|GXviF^G^I6UftsIY%T2x*C^SY6o1Rb62)DZe^dxr80r0*c=6&oBH?|T*xij zz{N$K+bgzbkVleiH6mKcwMvEh5jJJ3DRWz$8e<9cVelQQP3WD*8)jk(fsqV&M-+{K zKr>Avyv-uKlJ;)g`|}LH--%Q*%aB?sv+#;%BFl_NnFK>{G$IAWi(S2BU<>RN{n%Jy zq*l+n-S>&p&$S%6I$eNfe4MK%h?em8qgmYbwX^pH_+yN!I9F?6L*!qBnonE$j>@$0 z80XDwoF+x>uF`kn*ZgXYN0Q8%VWq}Fboo0wu10a{-#Gu$nhz&cb5H4LQ)$(qVi4zv z>uq=r)RMxv?9F9L(FBsa_`5m`$<8=$GJ;0l;eWmTas_Q%aa2{#QQtVDVuNn^N>k$r zNY?0y^nNd8SDDd&XKo}8mxh(!5mbB8Crk!x_2qITh=dK#Q9 zN9G(wRXIc#P?W~WWsAlzlv9+gkQPO;bTjraWIdj$$5~H7QD0>+W@G^Ao`bz$8Rf>L zf-hg+zP!17c=PU?XYW4VIX*Wa585xU)E?sD$1>n?{o}{GcfWjp^~JwfBINZq->iS5 zWX1pT=JmrbzZuI^Og}`FO^#RH&+WT6Z@<5~dGqk`)$8}){Mq;S#b3tP9ytWF9zJF1 z#g-CrO{_d+6@Z9+f=u$uG>>0HVX@ifLgd7Ai+wL#h_USi@|hHd1@}(t(1Ywwi-(im zj_xIj6=>DzEH@E}r@anvvoF;nHrNf0uR6~Ph8E?6)S-xBd<`3e?!X34iDihY;KLJp zwtmmIx+*)Au{9f%q22VlX_te_@jFUr?Tl-$=lCUY8o-rGNR83UkC~NmuI?-YsoDxP zTV;gaaNvStBeBN=_=5c^!R1RhycJRiQ> zW#_}wLx0`jALO$h!NEBQqBD;Wv3m-wXhz*@?nOLu5u>KFT_B6luIF3`32|K0I&N=t zYlp(=c@;};5}dK@p;;E;3XK7gxo=z8~pU3#&qMfWm$4ogwd$ZX0VNAv6owNC_5e(C0)jwzB?gmmq|(0@x73hM6e2*=2l6 zD2z98BG+xNrFVVWoR2!SF?hH1^rAVitx?gdrTDFg6RrCLFagXpREGy?OFb zD8Tg|i$F<6V+||@ziuGlzdh(5qygDR+9X8ae|w$ARWM6J6HN>d7$KpFCj3GM1!CE4 z`NoCpNoY%G#p7o{j3lmtMiXHO2Plk?P@{?EBt(vmnT1TR!B8E46+|@A#9|bNP=rxf zKu8#Qrx)@(A&oaNKw*rK#+#VuMO&opV;6EsMs)*W_y*vQ5mDWMBO&Dbs*un@oO^+v zl-Fi5w=N@1mY zyFHeLy2c_`OBoSY8t%!jrL^Yu2~o92;{XlMi=YHm$JgZZ%=a%fvHWp!$Yy3N&` zA5{G|9Mr1O3_eOpmLO!*9M#qkdJ*hXc8Qy1mqRF!u0b{}OrzZ$df1IEY+M@iMsjJ@DM*kYWzy*f4tuTvmUrk~4uHvggR^bGsLbO~E<~eM!x$Kl1e~a&p zObbaEwdZ@;`Vt2`wfssW@e0O3G_aj;8n|j{%o^J`!4-!VbLGhB zi0#jd=O z(Lf_rip^2MSVlP(*kgITG!eAN(wKGAt|d3RW(Bj6hSht?E36B}5v^{%P!VUj++bme zK!w1My#j4sm*>Y$Pbiaq_dA?i@V-ghu9sgUi1uov%qmXlZgXyfTLeM`Py`lU0imA! zYfTyC2L;7k~ z{y6s-Lh;IaYD^Z;x}s&Mw*I&F@HtwaiuD`BQ#k6d^})u@S5uJI1^1pkV>?Dplprn;vi_aMY6-0^{7XR?7(n-e9xt;7BW3|l7$vH$(`#Rrm&~x z7f0`>xEOGMQ`(Be9e~SnAA%M13f}el$h_`~)hGW{6}VKiy9dR9<2qPOEVGX~+(H1w zevcL@XSA-118D4~4ZcLqcv(H;3#9}%%nAN83@73A1YMoVYy zD^V;t9m7QT+DhW5TZ*%wasG(q)00z99mJOAtF$r86EO^L_B#AUtM5epacT*;D;tkx zw}krbE#VU7mA;y&Z$NOL?j8U?A|7PYiyChrk&ZlZpK|{Mhspnqt_^V@97X&hZvgg? zLVUp8+A21BOw-AuZxpd>toijC-M@eV^g6BOD2CA9oTwo}RyUq>+aI@HO9;lyKLWYW z0Ti|MG}r!ycR>gDfka2^B5JvGji|22cq9*vGzbGV_4KMx(7DlVH1@%f z8S(52O=R{yo2Id&;d6W#gk%!W#KYyztg35M+eFyV$ByM0wde~h6cQgScr0{Li_bey zda>hDO^`XafCxyNw6zj8;3BR?laQ6E6?pCkx$vcIbW&&e>2maT%|S@$vr-7AkysVE zdq$am!Pn*Qp?qjwCzw0JVseuaC0?YgI|X^JLF*BETIFja%h}u*S8zM+xVLS*KiFDsd^+IRGM>)80Y#eCV9MuM zeaw-;Y3}b^gVGK2gI346kgXu!3nDF=^DJ>rSdXvU zSlQXolJ^$R`W(y=b!t9|mmlN&R?0LTt2Pxi+ch)QfB|=mEPh3H23Q&>+aNR|_hEco zd3*7){S=1R z_gdOsPldd-W*lGD$f4i7v%Qan2yS(^#eO(vOM-35(AG}dJ?-qYVkGMp$=U_!V_f<4 z*tSMzM6~^I$ZgCkkfD_g*_3ELl&Dd`J;=Rg)D zh66|M@62x-Ur#}Pt9TKy4)&28lS|Z+x*p2ayW@MMkutb#Tv~8jPaT989SOn{K^eUs zyr;^jG=T~|!}9d%J71AVAuwk@NMkY`a4MU|tz4HRmdqe~sXPm9sAbd;T@+@FlcI04 zJMMx*cyXj2SPg2xS^YXO!xx9V)@N-?Vkt2s&r4eHR{?1s$1@IIX&3yrFG3D%7aZPj>InndP)Y zkUv!@etsKz6Nao;(M-Ut*wd~9?9)Bse}@vH4wfa1t@fa%DFL^=lZ6A?&(zY+IUBzrg+`0s^0Zx z^GS^g2G5g&;P$eSN)%GoG#yjinL1*V|006c?$-pxphq?HELzzd0C7 zG4L!ZJmNEd=b`dn{cQN@b0uV?B4yM^i9UO>1kb(&)|dsbmlK&j@*@9SWR7Gexika( zsk+71<+Fpr=Y`6u%`!p@s?v=`O+tf+>8lVrYT3}DZ*&bYl|~YaH=L)R8lq1mb+-qN zO#*3Gv!Q)&cQ(3<=8w1mdms4)2aQ>0>McgFc2I6uWg;xbV zEV5yh%d|!i;oz7qiYTVCXWS!;?0SVBEn`iNrrT{hX1i^%@$^fDo=p<0CJLX?E+eLj zr9U9V#jMJY5HXBmlzY);xCIXUYISFRc2Do>(aip|mhz0=jp|^3RoN+EGN*@P7(~?| zxz6A4Wu%8w=E@d5O{t!at?)p@`kSo#i5ym9oF)N3Fs~SPPNW(Ny-9xmHog1nh5o7d zB0k?@qDXR4&y-|G+~VQHa0GprBs*2f8ppinprO3!mU9^+=`!>>qd-@w_D=ZyfOD+R z2ZPvZhUu+%fwD3$^BY@+$=XG>@Gw&l zx{yY5t9A$etY(fwrr9bKi^oLH_{2^=ODK~*Buu7Ncv~S1gqt=6uZ}0{u%Ru7s4PO9 zPw`3xA}ND9?yH1c$hf0uth*yKZZxD)bkMTOi#1y}?eqbRHtEee@k#p~1}>XgT}daE zF=B^FfsQvmVJ}#3aH&b>j_cHoD?7hH7e>HCfdfu>8Cp0_)c!P}(%WG9I7-6ds99{ctq**S5+ zHiP&=mH5d$D__2(Mv}*Ru=d#(2=p;WwG?jSLSEfc9_c}XFgY7oyD}8i9n0TYRIFNc zWk@8$^Aw2g&Lz?4?Y?~P4H|l?N(*z~@?i3g4=Nz~oJtE5oJEUNELmm208h$b!qQT& zGqXE3lg7c~GcBLAJ%xQiv1MgUc&={CHeEuv9lLY-{KENS?Z|b=8(sQeRhOm#T~XxLu?PvxGYB3A~|@9i-YY@^&2TqS#n^tr8xcJH`G}cYiZ2C!H&!`L(_7t0m_3CAC@Qf{Sf1o0Me4Q zbp+1xgKdp>Z9~Cg{aJg@H61*h(yLBIP((rODW`u_o(eRxZcsu*Mt*>S1&7z;*-r0u zRNARPbg6ZdHj_xJ87d~CBW<-kwuA{4)Op@Be4sXQFbsC<`ea*BNf4CHODbib7ap%B z@_1*CKq>xVbz0X|i9bucJb!ukS>t&hU za~Nk|w`BP-cWy8i(LHgaevb2rq=1Gb-nZS9XZQTvd5e^z_nP*&rgaXMZZSsI7m|wQ zcxYIo@GvNDO%&LQc?%R&TF2F+y02)rX|yMd#)P~H1iiTv?;?ZCQI5P#0>zVJ6tY+`g7b4G&nbh4hS})4AxMP*tm*=m{EJf6N`WYDlx^%XIvQm9q~wfd=j80LFxfAS13XGL3^ATql{ixN{P$ zDy08RrLkm;G)_SA8?d6C%xEYTyhlmscNw5hzU`H`hv*7nxN%322^=glc-a4)Cy zj;IHgW94pX*d0+lVS>+S(5j_ZKZ#?JKy<Jf&MDn})t49jHbB8Q| zJ{E6@qyjHDCS3+P9eCEzO^BK?tVR^r9DKo*F+Ayrs4_yVnM4%7JzLR0fh39@zW}cu zX-luk=7LY~-)!oa--?Hr(+|GJ>WmTCY)sJFoDzSEj1G?BI}_>IZDGIhCo7~mmhaO~ z#~Bh7PF<+G>n?av8OI}m44>*JL_U)w&*3buq~KchYw##6V|wsIuiD^dNqxoXxWgKt zfzvzU5x(wKzp2S*>ZLW9H11iEVF(4%F2V*5LfUd>AV2`=ADK>yyUZ0_Zwzmw1s2J9 zy9=~kwI}=?hcE@!>e*CRqd8P&QrO*13!C)I?X_2R!)g7@760LAj))ftOG?;{=!IvI z-e$(ki{xYsEI7)7k;TRyPzIG6LeltAm6E4oe%n_>nXjz3(JyF>$)>7BY#%P#*jh2? zF^fMaXlR<~>+WN9%EO$TRTTv0OY7yLbK#86+;f`dJKpL*Xn4^Tf27|s`4q<9ltNNY z6~AM|^8H-JASHES>nlYT7brqS`lyV8mI%Cm9M6=w`X-n^x`_k=X(h@%C-9v`CAuUV z$lw>Ly>?!FlSotF;8%T&ZLt+0PkF#p~P`+tXbn)MH! z=6?`R|J+RbFU-?0|I9oMV7B_jJk9otZ23=a>6 z$+0S?9Vz(sQ8A$iDnGU10JgNWRIscgh2#^Aq-{TFU0$kMekn=K6JntiE|{OP5$Yyg zY!Vl-6h0^KqIzu@*E$_iM&Dw#Pj~DV|Lg9`_QSwUdk28>P|CLwDV^78XDt16VvO5q zCwg~>Kp+lT!x(D8h9g^k9zcwZgOibvkdU4pfdGeyczbg*F##YDBuqv@Io~kw<8iV(SvV$|p+uz^E#>O^$RSKZQN=;6Fy5G%ZNvETwg>?}T78WI$7Kn~h zrHP4&F(uQal9iXgkxYI69t#_LKLL)7({ANV5>RJ50RqgZX5oVkmYHxq64i!bMCr6e zR!&Y%x*r84<+vH^b~mn*ON@B$MX!fRNGOrNR)v`za-puSE+XNZE6JiSR{S&m>uY|m z{ZJ-FV6+ByH5vtzDPT0cV4cx83rj9wrs)Rh#;u*MSwdQ7_9G%XI7`>T?5(3%8orh-ePOrzfxOS zTL)o-$}Sv|F644^b9Yr&RVkFH3a15#xVhaffsfndv^$b78!@}Dfoq_hpP#o~^MZAG zhlzs@eS_ViMc`T8!?h3I^g9Lj&rV8A?7^M9wq=x)gFjxOUaFVrBQUQ#EJoeE2K;t? zd49f1?Uw60W8;eqU$;m0{%udsLqWkqGxwuiOqJaqxAWetDOX=ZSNAxZ_x6*lnVGY)CBe>1oWOvBY_w9;N|DD_M1mC4 z)xCG4##l*7p`Dk68BR=a>vF;Ta!UPTxqu4|9Y4SAXln3yrUEor;>AVi$_h?mjx1G5 z3`n=jfT6<766(`gFeXk$dqV>TB%#B+aajLANyovbnhYrDey8X=;MMOj#$I_p?vk1HKl2V4&J@E_u@o{KK3vzG1Kuz4S@~M#>44bfU8fFF9bMs-d9){47mP6 zq9%{eTGx~c6#LgV?US3I*U|O8`}I-D+vEG7KUxHZRR(V8wz8ag&0G6It zT0cELcJ`8Khm#``7I~)?6eJ{lHMN`Tc_m`v8ZbE%lNLb-1`ZCd8kptIr!&!tGW9AW zSVXe?_eeoPx=)J@?N83tt|KGs<_=Wv7UU`A`$B@LczE&kgw3rtBZ>>f4%WwSx#rIJvh*6E7%{Fnoz-#1*%nEEKsIjTv9P5nzcFW(_x^ZqGDtW z16EKYL(w_6u|I`@(>a^vsd4ndCnS91%O_6Mw7t8_g)A~JOL1W^ANpw+Qy)!DZ8I1| zM9FVmuuhG4djy7CSC>B^pthkQ79O`xB93QS-mWY@9c!CJNljIip=O!nQWq7_*^++dgMf1FE@JkhliQDLiRq2z1VjanwT2K zR>x497<||2gHV{)8WIwO3dBBHZIxJ%iSzD^=$d2fltqFv;pP^T2hpgItw~sJ2*KyY z2qK562dp-l@2mP~>fax84h{@BNTcB4Wu>NuPQxMs9f2`8E|r$0xVX3t(QrwXmspbM z?+SH&=@(td@C{Ju;7-G#}~C}UJhEAP}Zp>O#0<)gJh z*GAybFf{^<^sL2^^;TbBxcF?d$LArF8Ka_66R|Xg!o>1;RxU0BmPO{imE3GooH3IM zi4+ZHFH1+qti?`xj9tPvaD+{(2O<_vb7(Hw$9r~CQuOCDQx2G@OBUWu_POq}6vR>q zY3dE{w(6^^%T2Y2heKB=+^cfIIBy<&ZcnOj*AVCTS3a;!!oviruGocS6BzdO^@vwWgS=Tw>IF3dCL=KAOcONV1+58x&(VFTrMadBbl zWX=+o2pb&41}gsQDz{vzBjxiw#b7=#Wo#piGL#%y5t7N{QDp9*> zv7Ohc4yStWj6cxuwt}Fhml4!xjXRh1MGB1|Drnpl78X{YAya{5KT1x-IS8MRGQQ&R zTn(ZsWO-dkO+yksAtS|0ZvxRLC967a2-oVJ;`s{Bl!t-|_<(uk6%|1+AX~UD$cI<- zD}ltFbg|bso7+xXc=|nGhiW`co~#nahWa3Yh1(l%-vY81z)T%*nSz?%Xu-UlCOcc zvXnkPj@Z-7?KLme?M~U&hk9Jwkd`{MFr_F!=vLDg*h6)^=kS?NO?^I>KXcuV(oW!9 z^KD$3dqZ6v(u+ss3sRFmSt1_{8b@jkesRt0yPQWTl%cWmA|&lJ0%>Jw@9gbe zMBu&5#-r{Pu>>oBNO=TY-UK2vl|YKp{*AyrA_CD{wG@&}A%e`!BzRI2Yq&cMnsHu1 z9`KlrKRrf2ugC4F>j#qZ5)~UH6uPgD7=Ya#!Kw10<*hk_+Z*MHnd((ros)e4ks3i8A2YeW)2_8|;Ep?r< zGNDg^eHuVQ2+hvlVyeAug}MlE%f9u%^LmbDZJL{d2`K=X*H%LZiBrP@yi=(`z4)#l zm<0rg?Ih$CTI1G;JSzA5A~&EJrz*5ZE*y2{byK^d-(H}*Z$-f3p9QQFU-4eyIK>Hu zz70tarRq020c77((FDVSy@{C;YD9ul$UCI0iyr#1!1XWDO^T z5y?2|ezpsqno3c=)oUby+p8*+0V?=8&D0QWt z1hB%aA$adW#mC73+eL1epFnv91)v+lg9#>^15rNA^6hw zGrHQK;NbfF%@LLcSGv1tv5XXv&su6h-UHtC3vmWI#A;J1{zCzLckR@0o#Ek7aK%r9oev4|vkx>_Y0g=uMLM zAH28559sa|vhm%}&^W}`B)<@edVac`r1Pu&Ldo)q$P;-G_OH`1KS#(vM$G^w@jr>l zevDoJHk4-l#k$D&YjWnl8A@|={$nJ~0T}Q8-;Jc%|Cqq|gS+wnY$Tnvrl2Z;HfSzG z|Af!S;bVa+N%)|CXE&oxm`U^k+`&E|T2{1Q^)a#wMNr4D59Z6<5>Dx+y`Z(&6LMXM zZmMpoDQ4R7?u230{OD>s>%7=XYUVi8?Fl!ld56n=U&85LoppJ)iT4*0|D?Jhjl^4$me$6LqgZap!F18YP8qSOV{u4zMm^msSCx(KakzKxTqbjiySGO zwG0Pbu$*9FW(KZF8HhAcdjo?}#(-q$;J}n34IUd(*^j%@;sPrz5)Ot+TN5JokdgiD zHDCD>8=DgwdsnIZZe(KOE=~U?S^AFrH4>wnSInS^x(g-R!zHnBm8i%m+=9`yk zVQLD^pix<}oLv9cuc9?vDh#~L{TJ9qJ@?PQTU`1|5#b35G} zMVJ^>Z}P_KI|6A~yM49bXp5)y?jSfm2Wl|#8qBgi90m&7K*kZXY@)%DDV!xtF@KA9 z%%Gxbp%~paq}An|Y}?6)a&=UJt@0G+`|~gbp|MeYD>MdSuOS^8&aNosM775ufPyyh z7@A`9Bn=h`DU4QAcya^Qnftvd^2P5>X~Q*qdeE<2U^HydOHQ=NP{0`y2n-`=)dXIj z4jUTWy&ji#x8nSsu4YjQed@yCk$T7Dp)v)KH4YFG#+2UYbJKpK%!RXJ`+5_S{Gh$| zVump7x1{?>By79DuRXW^WQU*xmJ;4oE2Kr1sM_dBC^~Ucpe&`UWq}M_J?I`&TCJK! z(-a~YZsHFL3Yx~Cy9pM|TE4$;nTi-OeD3a8bEy!LxU0OYW-){@AI=t_jrq_o=9s<` zMo941N{SL^4>8yBI50GY#-hnR@huHzRAELYVtqZno&71`U$0wz{k89nrk}R48ZQ%IWSW^!ad7-+mJ}DlL!vxBE|9A!$H#bCh_-+*982C);T#Sf(V9XTt z(4vYsO0h!Elnf;+F;uQTdivx(rLz0BRc!qQ9+W)E04*|SI9A74jA7l^E!H~4aF~#> zi`rKWGK2UWiI~raSf9kUulELM_0eFEiMb`734nUaSoSxZ2_oTVNFW2XraKTxR@ier zw8JC&+bS?@Pbxoj%}k&7o~#DxdryB^Tm($cwR&-yK6M4WC`atOn)4wQymsWH!^jhq z&MiEis?^d{3>ba4UB)AnN&fU@gunAf2#jBA8;?<|<*>G*^#&{6vOYFcEnqJrH2gZF zy4eIGp-=2(k7o{(J*(9hpG3`}&f*IeiK6uKLqSRk#HAQ|c;8ex5sHX*bx61yv5k7& z_^OLHJ_$3M6Fo>F)CrF@tWx_XNkmLoSQs~0^SiBq-Y`TF1wp8-q+#wI$rAlW+tEWf z64WTu%#5Kn_xi1}{CrkQ1q5;w%6mm@9G=yshZto=#~I)pZ?pPAg6Tphkb-2upQ?1p zY086&1o#S1F5(;iH$FSEMh5;nD2~$%DU=F1@r?0k@oElI{p~4LZHPWDYql|{ND!^& zlR1C@3o;P|iFo#sPop(W@@^hZbWEZ)l<<1$j3YV&kr)X~H$!}=yzbLSY$xeA^5;JVXvE4#~*gRqpPE15WON{%6K68+9b%SM~dr_1Qskh z>P1igeeYA^hiAXldS39PBu=ORn&x8JLJI^$;E{+*mws`R+8~R!1D~%oJ~54lruR3zs9P^~^D zTXnX<4jgO}_=X>W&XZ&9m{)`E?q8Fl16-D+r&@WGmoPf*h~RFDuP zA$ayk{DM?&G4&&#&%;#&D&@dHWhU4%Zjl%YWCpI52|dia`TMrZ4?u->4RvRU(8xWq zbPZE0(eALzjc%iHn%8BUhx9hb!=XaK#zO8+;d#;PmE?lp0i{vC;02xT>A|*~fwZxe zS1%?Wk2f81fs$}BQxg~@e0(C+ENrPV%xn*i{>lfb%7fQZzs1X_vvt9bgzxHS5shlf-W{T(K9i);Ja3Z@v9v^tvEvKPKlXSr-}#qYn8SRm^fT(gs%`-YG`r6FpJ`7I0y3a0pZ8*{O8$@zOV^5@#k-|}C!|C9Xp?@LL4E{VY~ikiAw z8kYSdtNm>(@c*4E;1U0$D$Y2VEG5e7{#6M- zrTeLbf6&nXTx|Ti5`HXM16E)Ef1;R=>4DZpu>=BI7f4GnHMLu=)J(5GOijTK4-a(D zBe`W-VZ!Tm81@NTG0-%#eg2T)VTNON*OBg#dz*3ewA4qvc%Pi*#zqd=#z3g6kLhRZ zBdQ*4M->WUPZN3-vmtl4A4BZI)Xw}~4W_*i&%qACPUzdWBd5}bMj>-mpRS4*ICFcm zGB-%~R=2Rn6IVp7)z-3;7hi?dkgv|C%;xO6LlfmitgXErowLX#$1C%$J3z}$S|_VF zD|U~F5w}Z-T-t~?U9T(lQcn(@*JtbVtQ&s#SqUziuV2}R!$@`!Gmk#(@VOhmd^)#y zdbfqV-SaL;e@(++9`ovI@PX%5>iN8nlF~&`ex!^2+DPHA;~u_54(!Cyfl4{4cqU2Wa1;QB;?d-2{X02&hHlW zo=(+OXVNSZj{ND{oa6PMPw3iE+BWH`bwkiyef#@;k4KzZE~|*wnl7_)o^TcHF0>U8 zrZT;F-+jr%&D6bC_E~&;;?_Y|C-7)@lp!E9UVcvr!D4N^2FW5^Gv-waH#$^ZH)WtD zo&q^bB4X*W=d8PVRw8U7jZ9%L`cX|>SsIV4q;E0PJrNJRDw1MfR9Q^n6VwYmNp~!H z!+N7Jv@2f+d{yiqH|jg~PZY@J18mS%EJeVpQUgFLHTZV6G(~dA{>8zmUbo5q@&L@v zo}QKiMa$sIq-coQxzB+R24-$AqGJ;RPr)ejC9>`tN2+5bVi24+G-rgEy~+nshCY_w zlKLP5)FDHr389YRdeAsvxU@p9>=wanZ-a_x1}Y6HB%R>5s*KHE{0kA5pfo77aDltb zx}_gCN5Aw$k-LS5EAmC5Ip-sKHii%!$tbW^{@gHUg3^&tU?Z7(m9>KfQry0YCXvw6> zE0Ky~nn0edNIRB{lzl(`1*X<+@EFz_3a!o~I{ zOyw{0B7dS(oQN3ZOie5eh3(ym{*F2kv3CLB0|3AR8A}rLfwuqs#p$*`-4~&Wc_=ywIFW?HJnyI6crM(>yBQpcrU+S~`W|;ro2cw3j7N9e( zp8@h$lqE{s%3+u)U*+sUtv+A9FHHKM*j#ZuKX)1^|8d z4Xlv0w6i8+RQ`cO(dK01U|?YiO&`d97#2SNXi zabf>);va%K{UNENle3G7rTtIY+#KkQ?SFz&7+oA}>||kT>dfF^V)my_e&e%#;F$nA`3+O=bXat}T2SYm(Lnj6Yk00VP*gKm4X>Nem{ne*Ae$OlZ z4@6^SVr2kW`$vPUT+9sYtN^h%7`T4O`&Wnl*~M=M`>&#tf0gQg)1g`Z z*?0hV{F$QsPaK+qmHU^q{t%0Zo1OCy^Zh$Pe}|PHUh^LW{X6)M<9F2i`Q-jTacGv` zUHo!rj{l2D_7AK7)(IOE69YFBK+VkmPA7jg#(&Vs-=i7FZ#VrN&Hj^1|M1~|b}(_k z}cuWZ0`ue z@kcOJwSQ}8X>1QD!SM&?P23U?a6~K&9f<&#OGa74za0Do(*1bNaRRjd*SFixy1(!9 z4{#n}W#Cr=ZD|7l+5v9h2hvW|^k-jyN5RS2(bUiu#vNEUzT2*s2ubwRyPqIQBsg0S zSviF!5z)4^WCce3Q= z7?LNs^`N~?l#>-|E*Zu7JAT=Lsf=%E7E`zzOT0db$p9mo@*{T))gGZ)giJ{5!!W3X1MjB4#GQSB*bVl8eocKtkq7vOmS35U{ z4e7@{#3T~qE}Ta-SH&L)^{2k#L}WOkJisn@F)iBwJBTkgOk@IT8n1uEb+xJ8o{gH4 zU&Cl##L4|8roBk8_|@trcN#S7O9ueoXVnOVS$Aqv|L~b8;TeqCfqPZ7*_qok@B7E* zx;QG^1*M>S7LlBZ8D5so#*-zT$`%Z&qpyNgQ@h547AS*O-lBHopB+UYU22Uy@E zGd=;7t}L0!<2Uo`4sp`Ksyci^oL0~+2+r^p6+5HApCht}~pr#0zIg4LQJnfOyrTw!!#AzK8|G?xZQA5&L=R2UB{#LEgqNSHn6#dg@kFiWm)S^ z)&wzmg&yD9nVE-DDx^Q`Xj9!B=hE$-&@QXDD=am3-d%HcE%~5E+8$HcEd*>n94CFZ zZX|5W(eHd-pGch04XG@dw-u}LfQ|gxAbej|5!aYwm!h@YVoyhX^1iJxf1w?+5Aq}X zjAQhXw~$oa@Y=sxYuIBXOJTE^4MXcx9Vv6(h+;Wr<1Vs`7)EiW|C5 zW*a#QTiIwp?}Be|<|>8gGpmTOo%tn&%djiVHocJ>r%hbtzxz27K+Wlq*t<7mLV z;Jsng(<&kZx=%WUSm3_#)tRaFhIxFC^$7oHpWi>Bb(bIVYPwOiz$k{b!06Ah4k>u0 z@Ivd=$%tF|SQOB0!8sx#+;?I=2p4p#=^ni*vai^N`5yR*MiI`PvlUW>08cbYiFbs7 z*g$-+f@2*wS5G8>tOE??)TiL_y}YR6t`%2C#-9o;AFo?FXxm6K;N zDXCQvM42U0#O1oMICIO}?r-gbGxf&RS3rB#gUvVV@1(q*xV(<;BDR7C3wH}gKbjgj z&eXbd&WlR3U2$I?jOZm-LtAJh&-BlLh{~ORmtkka+y)01@$(TET=`U>ix2hRr63) z;}!!Q&O;}1n9LhUzmQzC^KHg+V$e?W4W*68OK;9jDcO)U6S!G!xV*Nx2w{`A0qf@P) zCKJ@dY*fS=+PjLK#OOHPoXG8F<-2nNSwEr`F6fvRCHwUF8qjSsJu*;lo0%`TZLzLO z2WCg4wH$hr9|mk&>BZXZvw8w?`W+uS)@EqSBaWn%tZE{3^Q`G&07}VUK zHvX-P4?=YKTeV3QE=#mKPppUm(ET%L!rBtiBZLp?9AaW(@6oV(0PJYjiIGGMaGVAf zXk=Xni7(V3m^S8VA`tz#h^0UKc`u0Cpcfd}Cckh4%{O|35RFe@Wif;pOpI@+-p|UJ z#d6~I2s}*}Ju5j&BY1rFWkk6|<#>Wd9Us%1pW``&l=PU+aOz3WhE!;O4tR0!^YT-F zRearl*?5V2V0}~&9reAI$#gZR6N$o?nvV_4y8!Tu?#{3JV1@>JZs-6 zucll{CZkelX_DqhUYrbBCxbu@sR7&;j+cn*h^_$xAdn@B+lEL9G*M(2k|%gUdqofs za(w}N^?HF{|M0~8!sP&ThsW(sxV;8`5I-0II<*hFFN}Ewwx={z@x(E*0eL5s{CH1a z!1lD-e|zDxKr5d!cxgD_?c~l7|89c3_A_$o_TAB-RE`8WbfJXcM3h6o(vZv-c~xQUU3w-#SXF8LnuKS(M_{u7 z#s0722sbR-lmQ~~onU<6*=a%|!kBZg4+s{x%M-4Gj6EE+a$WF8S{NKlnOiC&Bi`3G ztn}dcU{W@SA0G`WK($)^kEfQPS3qU(^IQ#)>#)&)ZGqm_(u|n0YJ!hw9+8X~eohaq z4Mz`3nuJlrV8P@{T0~2>q{AmSN0GKMRPua9E@4R=OJRJ6PVd~z;QF1>;rrL7qf6cN zOPzfuUFG_yohCE2?Xl9K?27NIR?V|tvaqe}EHz`5Q!@|8KAW4FF3+-3ERt(y;3qUB zZPQaJNzqWrq!p#6d}kQve@{z|LYu^p22uFYBx#XNLPuDXSIa(_82mr$RF0C!i1 zbSb3V_?T+IXnviqDB1au+n?u4Se>P39rkKVQOMPOf#YNy68mA4+F`9$m9D!Bl(e=D>#V4 zIo+WQx-s3M7256nlx+_&DnjOzaZff#dHP1BNlo{60#pWe!w6^@xwDLQ*8Fe55U4nE zXPHwbJ?daYcJH+%?2Cq8x-uTjd}eSI*J;{C#kStEVH6muA5!anFyg~*m;PQd;~mc| zvy6hGV3Pb4q4YxYmT)nJ?Ld_D6zW@AbKmDQqUAJZ5@!Vzgt_g_?e^g~%lM^FO#3pv z^!FRVX7(}!d8}6u*#*9ykpxAziULxMGeFO*?QsvZpZLWPr^GG%k~-v9yZj0$_k5S2 zI|RGA?{^y(%ipy*ImgDb64FEJxvwJ9-s%rMMIGyt$xy_%r)9}MS_-W7`^mSSc|u`y z%EiU)H-lgMLQE+4hxl`m%c&czuC2;f6>%_<6b%CE!N)>}dYV>et@e4mt2|N$S%IU@9Fl1=l zaKS5^4x9Il_Bvh2*^enmx$r_sU(6W!PzP?dw#}H($%%WNMDk5-;3e7B7!2|4#ybX1 z&Cs@Hy2Xr@q$ke{aFZa7XUd9lEYU-4XGUHGM2?v+&fu?a2#!RpA(zyr964gR_mmbo z7VwzgYPCwUERziOr(8^$iA^tQpUt!-Myas7!V!+`?!EO)M+p^(n_`Ys!rYvSc+yh< zzUzR}mwEsEhRIRG-M3>B!b<35Ll}Xf+xwf$BF6ih)SBChQ@fWjftp(dX~uLOlE@wQ z#8Ra8&=fW|5yXNT*z<+p6n^s~f*N1S8Y=~&dF5JK@xm%BYuX3K{QI7$?SgIKzL&Lq z>80*Rhh9hCE6)i;cCAN_rV?3(g4=~erQy4gwBg6+1rf^q?spKa`#4tkgUxyG44)W~ z6sBZ(qqGMf0qq*M`M4kF=FI|KJd$nJrKu&wAsFMY^}bm262y{h+B1P4Uh>G-L{vf@ z#z+fybYd6{Ghqc<7%+%&DE>+I-nJ$k^|IRwM zZ&z6EyrkOvvB_e@MBPxxH;0vT&~S2MBr7=~$1KS~Kz}&$svtDauS{$d>lePlA|6%K>zP@$Y?zE|;g!pTMvjlg)|jrEg)-kK)|&I%l+~VED)8)G zJjhBXwNRbQCJ0A9Tp*1dki!c#YJc_M{x(X~N%}j%m=&>|XZ}iOcd!k_)A2By#y%Iq;Mlnwwz-_;@OZ0sD z6cdh12EI$~F8PK>O;nMU7r6zS-7LDKm~#)i2z3Nw6?b|cux$l58gC@;ON`+-!P^fB zWkD@y0V?feDUZuqS0&>C*9P6ticNCJqpKEZM(1dLnE(vR75oQP+I~|%+ zQDH=*oo@+BRYO`apc;pxBGM7LEH5OCyr^)P*ZyYDUgbHwn4Jg&#OHr8_KwlD?AzY% zj6Gx9wr$(CZQD*}>}1BaZQIF=ZQD0%owN4d?LGIt=YAP&q(-HxMpatcuRZD+i&iuL`)K8l6V%JKL0`c3r1O)r5fTDQ`jYs)D~RFqe+Y>k^|>n z!Ad2Fr}ng-Kun-c#ahSOgg+xJmaLEZ5L9HLB1u%Wyj%aFk>G{-n59&DN3B9!Io)7K zjVSK?PYHz#^O9xr@ShL;!Ic;2chlGrkNMeOm(b0%!fHlww$d$uv})c-YS|=()PO%X zC6!$(&!+;->tSVL!Uval1cwN(26-acc?Y5{*y4-|#j$81G9l|OE0EWavMZnraIN~_ zK1<~Fe7EUS#tn+aavi>s8&&*_SVNc)L^3_gscE8GHVj~~#of#aF+7$}f3K>T}rq_X_JqZc&H6(AcqfI5t4o?$n0_9P9CXcr&lk8_hW7DbW)YUHtNOX zES)C6(HpixVFbia2%|2vP=#wGhr|NM@(42`2}{{Y3}u!%Xe?%>XD`8F#dG5iriB8< zN0S5^%NLa1HF!jX3noHlaoinS)vc%m2_owc#tR@P(wi2h7RYGhM zT)`;ssMd}h0vqopD0z`4=#5?iGs|H1Tv^REV&d79Dj|I(i$I4BwMo#Kw9B{WA+(Y! z@Ro!OkdD@wNx_fe4mLms{boif6X~jg+3grmWwzPCj2A7h%$*@vX;oP9hin@bsHs`3 z`@y8P=qpgHXUr&5mgbN5bFE5*$RmHHFZB}6W${;PZK{~rf})$(crwBG(Ze-66XtEX z6y?YwO)B`HLEqt@-w{l)gZpTQ`i(a*)4|ZygJQqrdIvrrmXQ5u_?RzY-^(4jZps0^xVAx5)b9Xc2azb;xcSPFC)3pODH+T69os^8v!HAQnkRyXx@vJe(%eu?0Bentk}htML;S?6cRK%KZe zcSr|04G-Xdbc_9Yw1@Ayxg+dwT(D(Z1+aC+fWLgl6n0xi4ZpsmMcVnV(`D@3NfY#W zetr>jxJm`x^phZN-G?7(-0{Ngf+aY?#XXY(0Yyma?`Dv2;qPDqc3d-xgCg@JOj#3^ zyf(7xGDf&NZn$4|I3Ph!2j{pKR!z6f9mnoBI`bn&z|H}gGn+FfN-K<4+ohhWm$pI`SYH7=EpJ82;GYz+mq!l<3d0c z3cdf^3xd*)l-%SAurk1zpUrmR)m(k*hL+)Jy?KcLvp7^L9w!GqI0fRYh9t(W=u^DhH+_?pmmC&lIpI5(^fVeJq)39V6i-rF-Ett7*(M z>4u`Q%IEzWMfLWAswNq=Nxy0gnSlNZ+06U=>njCWxD863e*Z+K#g(Z}psMnP;8a-f zaJ@ctS4Qe_K&Jih)Y4=nr)IOQ#Rkf%J9iuX`)uDtrxOJpWQCQPdaa0Nt>t{O**+yF z;adLCGEM0@(R)UynZ;b&0A0Rw5!zPP2dy2%#r6LLs(+Khzvv+|9s9qG4*LIDZ~FfM z&NUQ;)kOsoWJX5PST3h|AkBc3t#>RK4<-JMMl`>n`eKQ+y26xh6=XU`ZoXG z@^6q%{})gF7fGk5|K_y+pma#)znA&b;W2%;2l!8<{`VRFfujE#6aK$l_}^rh{V#a> zT>Fu=Koe4(>;>y8S&P zT|Q!lSE!7TNgNk1kyOTF?admF(GL3?-y$BjC|pcJ1B6_;Oi7u)kXOvN!Oin>Vi!ZG zd~>z_DDx`Q^U8fat5Y$w2vH8$e48IPEvmQ&2ZWYDQ8Y%XG-k>hnona`VJ{(uhU`bx z-X0hZuH$_`^NbI5T39Rx$2-(ZG7(^mj7FU8_X>LM!()&wejTUnZLv+AYsrO!(PzV# z4}d42hqz~>z8l(i@K#>u>uV*Q;t@;5Py}~hgWXTr%~I4!0DfAWM}J_@7C#^m3_aQ( zPd{8S>nJ#M~0C&ck*S-T#yaeG_TJsuzN5R zzhCQ$vdbHez+%cl9>0&%D)m+^y+I~7ZdxtdqIQumg3`zzz@_~MW zkze>>)(+mo_Czz568E&ae)-`*D&JDxOey`9(*cg#N5E6Pw-@E=$8EZ8{HC9}2gEXU z&qxObRif=by}kZjwL;zT4oRSJLx|}`YetueBYOw$OBMVv^$Rf7&#`$1}X z7kohV2YndYEU1T6V$M%< z`6Qj@gN-Jtg_SW1B`==LT#TgAUlQU{%x2GGa|{N#9=xL{ZGjqeJ5^7DOWI)u_!12& zlE&f}KMkO?Dr>pN{W~J8C0{HEc_i9Br3m+LUh-H#`8}yAKue9P3uWY@&}GWhzd+6w zIsp4?+w$bCnYy+Lys${sJM`5V8yaG4<^-JP5q--B07*)8q0k#-`g;gE$1&kf%T$;a z+aS<7Ts|;FIswCv;A=HnAtpu?v}h2CZ`K&}tkCcekM3&KWTwvOq9CQ8Qe&$6YL1j= zFi-OmBk9M3*OHk%(}=y->bSUz^o?OA!wz^wmQ5LAs5Y5$#oGial{#L zWYsjl83#1}@ZQ2fdT{AV5w5NHecqsv(D#nb7|wxG3y(MYDOv;D#aVcHr#>QqF`K#n z60joTjbuS%LyZm7uN|uSv8tgF+xmmLRkp6O?5GjNESM}ZPzAV_oH@ieCPvdBBXtvJJo5JqvU$A_h)7lRxv0i7Z2C^<1;S4dnPa)GzIN2kse&t z*TDYyovxr`hdN(|5+S?>{5E!Eo&i{@?!~6PLj}r$_-WsBkfu?aD} zg_XvYKQV?jkm|a+N8OF?pkwl?q{w`$&?Mm>#(P* z3Y}v#G;&sBV0y+Jdwm#jgFCalrDpu9qxn<_Nj>T~*k@VwzW>>thWp$p$Jq6%;bByQ z!Tp#M0Ook<^mE1NFt6c+-b~^PV%P_p?d4~~+ybZVIdWqaPY8W!xTf2*AX2MFZFl1j zU>V>iXffztERQVEH-l4G+&fj!HE*n5NIBe5Ne?9MDYb)=6Z)E3XU^3D-}SM?&&Tzs z3b?Mf1ZUNh8o7fk4X*6jAZ-fQs*8%MCRi_{L8pM1>VxRelLp%PU2z5xcWo(r*d)15 z8WWjERHcNAY&xKR1GbB~yS9sbgKGd0b|&GmanRY({wqcANamo;A&aaV6XX5Q%yuPg z2xus1H7rcs>;fATxRh?CUv;a2G&`vorp2Gc?r$iyGs?Mq)ON_5WNTFKE_;F%!mq-w~3M8jdgOqiWi+Ga)b@3mi1Jr`_ z%bG;3#sysz{o6;;hNS(sp24puttcZ# zDQ51VZ)$F1tEg||NUh5FAFkiuzw=K7`gipE59>ovT+(XX-!1?h9pvAs^54N2?>m_N_4$9~xqoyQO}u|uihu9)KfmCA z9Om!C{+I9Y|8cwi@=5-O^1#T(f=A8B%7{n*-G1!*h2{SYw%>Q_zl{erw(s}EWg{CBSX%WRM`*8e-?Gt+(V^{Ro{yNcL zPwOxHf$=X>;{Uio|44s-J(hn0?|-{9{}L|$^MQYNDEr@*3`}&mIw=j;@kF38)r$vO zqjCLN%a#UB$R7oVLysfEhl3U`RlpZXrS!Y3vszz*gBY8YtW$0Nh-sIh*h z@w|TBe$x3gta)ZmO*AX8p}n}Mtm~~Dz1e_1U;Nz;%G;yoKJnzl&@!g=Wv#Wv{Vny6 zJ6J_~jq#}{X+zK>;{lfcqa$~v-s>lO#b|H>BX0)%=W3MhgMZfRow;bWu;Zemn8(!I z^r1Ja>FCjX(Z?*SDf%nJ-pcSXluJ3u4gf#OP9-L zMwFLV|NDlP77%Ks!?VF`Q5I)&#af5`e6n!p`Fh8Om$!EZY8uBgC%Zc>$H94>Ph8ye zYhz>-5cV=H$M#9|XISl>?NTQ8U@h*3|6i_|%qR zT=E7)azTO2mx;DE89Ci{FnQ}aW^*i%T^4=*U_x`e33M44i3FGyLLav zD90d2KgTeGaa!$E{e(KIdWu>y6=gE>c!=?Mohf?_miiQxYBK3~)~G7m8rccbw;D*QmZ;i z{e+471Y~*=og`Peg-U-#k-DlW%tRaa+2T2S{YyPa{pc#DDQ=cELq>LGnl{th=J{~F z?zH}N8msAQCbA>oA<-24blik^#@*Vwq2tDp$As~r%0y+Br?Jh}dR%MwWY)gHy8HTc zOJqxAt7Hr1_Y{t^7R}b;7Sk4b|zP=I`dxjMR&F|YC01g4J!`G#@)=rhZ6@fYo0EwSW(!f9F@HtBw2Q(t}*ldjq1x6B3yTL^2-K zkVq+Vq9-u-TZMad#St$Q-bn(6Np&m_)**OzS}L9_S$9wCtS`POm*j0q6*S=#FytBX zpXP(DW4C40i$aqfe}J5a&xSFFFrcdIst;H-*H@MZx>{Q%J3nlW+Gy6YKRQXg^FPx4 zf8+P>wDw067K;Q9e`d0=-Rw?fwog4V;9z;P?ZIEoCXk-xbogp~o!=ep(Ejuq{5YLj zuJ6LvnRe5DKfG8&-MaHleIxvkd8qd=ZoeFM>C9Pk9=fQ=aqhnU9F$sqc4eGOL3Oip zx>aN=`cTqse)VR3&3IJw`#kRc+Vv)d^=^iXlA>Ag?QZig9zXP}6$kRRVN@G$_bnTC zGxJsj`#Nq$wY|XVZ6R&zJ+bMWDxId%12b^rjKN;3HN0QBJIc?W&cVgp;7^}0v#e`( zo3%NWRn*i}RKy%zba^^E;cDFI*t1I-oSOWeZ~wZVUEXcf@2jtH#cjr+r*gv6(Y*)y z<<;%Fdh>ac;@sTkJ6>9bAx&(SiGfw!qsjZVkS0%F$x1 zI8$I}zf`XQp3`x%4)H1nwehY(u=@~aDBF5JzVZWl3J_}20*dsxU%RBs#G?bqP1ktT z1wRe!T5nX?fTsFcw-~y3Z+_p53ijp?H+P)^0D~62IOVK{*s}0|KV$+Y?=(S}60dee zs6{`WC0HDygpAw8V z%r)E|XrilSfZRJc8ZmH4Q>1TWzV;FK+Ia??#^q@!wyE}-BAgIGWRuw!7=YwOGD(Bz zDW!&?AYx7&l)4br_(yZGsk+m7=kyE?anGgIJXBqDq;1X4NF}QzBR3@LJ?Wvvs?0!w zs?I4$kcauo7|Z*G8Z&Llu_BT~YAA?n(AIK#8TbkS5{F0jv>3uR z750NLTh=l!UKD8$TpY^lEl9lX4_gLAh>r^dKS zlp`SnUQ}Ih(INV{pkkQx&o_@h>+6XeTNk5F(O!tka1FF*yvcKGuK=NtEwkVLxCA~7 z4!F1>cYU@CMWUg7&ife%MZ$Ze%mb*x+>LNC2_Y|RWGLq!2x5b6vu8pGehm%j2>!>W zb=KD2K*W!WXA;0mo+I<=H#$&7?dR3kz;Gw0v?cK1V^Qf_IQN<1M=n4sYkUz9F&e8E znwHrz(6VzE7v1oU4HFwm9M4#&7yCPzExkr6$kla(vHKhf`br8z9)=hzhr?{&n&_BZHO=n6JidAgilmaBk za~Z?xsM1GCC<~Y>_37MV49_ibx6i|~njf1xquPz!RGuNRs$t$-;VXj&o5k?<5)#crVa~S4LJ-ie$`)PdYBb`i_sq!)@ABKQ-UG6X@n~pdf zn|#R{IpVxRSe)2L%}KyuY^+Gsr>5#B4D1OMgNNgKJ4R^JDw#;TNAc+gl-E+#Gc0!;|+;*9@?1Ke&5vGl{&!xL~^6tP@jTW@wMuJ|vsqEQh z?4|*mr+3Q3HEN-snsYyma6iDI28)=mO}^{&@J_d~(Z7=IQSCT0FNCbYTLaBr1Fdx7 z<0hK1-QEXgTLGSW--?Jw3K#w8h7;s^MeoiEicdYa~ zd|7YaiD3HF1bxeUrhBRgF;GWt(zEl0TuTGw2zK%I;>QF_<3ogPL7!m&ubM zM&wJiO?$_OpMdFnQxI$y0^!n;v?m)ZXdl!!lj93{rbdn&(#7L>I5%0-d|k^5fIc>A zoD83j9BM9|(nJ;bnuw4Hr-2?B&u==zML8c`GR};~1nm-p#r69T`Ce~u^I{`U(Ns4V zVHKxy@9Njm`g3$!OnTaQndqlm@PBZ0+j-Ng!^aw{Rze4@*{J`>v;;HG{fLLs%BgAi zn%UTHP4A7}kus}!|4lAEJfN40Z|@J#qG=+xYz&|Vu&TN)DjS_?Q$y*5iM2g!K_q1a zss$mI1K5I9Q==USQD()ZU06kl>?iBf;rwwJz2&1)uD~>6Omctbe~_;+OrPwMta6W^ zI11isK#?eJ(N0`A(1=s|jzRuV6zFBw!o0gZhzN*kNx9r_Gl+j0q}3g|T)ni=XwWHJ zmji&<5j9mH0j!FIJmw+Ysv&VS8tnQ!rN1;AQY*lh=cni=zuF)WT+AS0UZjUxcpdLS zxy=t07hFH!&t@l6WlykUzde=TSJEQnIf0lRu?1neBdMT@T8T&|fL_bwp*8y5JpJeq z(!mxPCh|H01J45yLAnTEh&4LX)}E&x_*20 zXfaIEPI~k){rZp#6z9|YUZ_h++1zgvoLgGE3IJREQSOsN_>t^0W1E#HvHDA^D4yP( zp+I+Qgb`k(Hr}VJAe5r0j*cQ!g)A;aSrH?7M9}CFvgK=wse%vV&}3s)2Ymr(7|t(z z)_X`YK_T9XLI1hXF5tK-nfNKrUL1i#9w~t+*C4^_9f&~oPt)q1Z`+6U5nruc5Uz0( zCHxc-o@Xq@8J;^7&&~zsj*%B2@0{IDS2140gYg9H!17i(qfVUq$VUk(~?C zd+Lg#R??wR`k|XD^B(J1MyM^sr!ynxtVvlz&PS}`!3Y}3hi?vKT@Q>7hwwZ*!a1*m z;XV>v;~7`{c6smC_oT3L;6HaW@v4WbbzfVVdk30ozQom=Z6`)ui<5KYyh^qk;r424+ zH}>oJ^R*L-daLi1wA%<*UDIW6X;I!STktYZy26-NjAImzBtQ=N5){{eI{uV#@t-_= z-lGnvsd~9#s$-OwR zAyff|eu15BC~=de(nTzalDHXf8g&}6>(5)}{|Jg!oD#kibr2kS;JemF30pY0F~XOU zqQl`2RnQ3lW)peA?N2GlVAX)AsD=jZAnKX8!q8nOscuYqSPD$TNu#&O=_~1{kMU1N zGs>`H12RQKwVI3c6043srjW-KL6#FPeRf8Fmil=JZnTZ)o>7OvZ*>Qtw^j+pAnJeB zOm!i>;jt+{sxyiKpA6hQ+rSE+CdQ~=aK3E@StAa*)3Uz;;baxQ z^5Oh<(Z&)2Ldkquf{HRnzVM{L5sv~TVbAiZ1_D*pGLdqGlY?a;Zur^OVEbSo_>x^v zs{8W)lpRW0g76khH4YZRywUip`nIk&9U?^x@Jl~#xKyI0-~ecX-8)H&s`KS1-0pWp zJ$Yn47VmN2JQEiS=?9Vdo9v1m?3g&G+-8mWxfC{&?%0){Yt=>SDc%6F+6B2iYbAXq zQzeSeddu)VKkUbom563?RZ86uVJiwc?v~dMFx;^5Ar{ZBs{~SSKl3iZF;RsBy(0D8vCiby3d32ze^2+mfVt2{ZS03>=Sn zktFDqMVQ0P78DQ@7wk8+n^iwUh4Vv%qA%I*DlrJxY4!3n^(EZ=2^*fNC6mrWG|xpV z9&7*?zp0(!2keeY@$V_!GZXBaJ(90s6khFte>rg14xARYf!W70P5!zt96y!okLGOF`EmY=+`nW?Q zT;ep$6(4T*uYTE&$7oPB4Ov_wfe2gyBK)#=<})+QLfRCQ`|(rPyF6$fKW;BQZaIH6 zpYt!~5Zg-l`wZ==ytEWi6RXP1TZphtHAk`GHnZ}}(@3f+6>W-~Qmp46>Lv$;_P7Iv zyHa5bq;ifV;uOdLo8B3{5uV5yyoCe%hQ*cT+eVNuZOwp67b56Gv6q6S+)c!axc}KM z*Up4cagWyL)+&YqVh7~JDk;6 zE!O16F6U4_sM+MnisbPP#ag@^<`evlU$4q=grB;r zyU)lnW0tn;54O82bEuO)8$Z9Ayik(gmv5i)gmUAp1%MF4IgzFDflYs~4(8H|Czg(E zS7-f>rpqmO#GMIKjmdM&bzkc)q!BFa06iP>jh!Ns+@3Q8pZhJP2Uo~323{&>s0xj` zk43ZLJ~q#|Fp9`&dE$IY)Kxrso|b}Fap3dLX^&tl7!kr<1x?m&@Q`W5t*Xyx=4p^i0@gX#6c(mHGNM%ZhGIVG# zd}i!yawC5dkba9Zxu*~WEV}kSh+230iKw^wV(lDY`YXGeyXIXE-cC-gq)yr z*#lGE)`{h|^L)pTK`2-wtxUWcch(5GN$aiY(RwiF;}*aGOof%fLHTEqaY1Gol?>^6~u3C==VaE2*dy+QQuZZ{k=rA6Nz+C4$aJA)-6_HaKq0lKhC8O zONPosqj=XWP-+Y~I&nZvw}u5qlkWVrChlZp>#SxA9UXE+5tg|1BKgXrFdRIQ9`b#> z3HTrJz!JN=6?CpG6ejdq0krby2UI8{0_eF+yD{;y zc&ESadHQ;>CYRm2p%{h~G#RT5^mw=?B(ZGZT;XAJ$QHN@ZZ-U+B*1NPq1KRK)f;*@ zC2x^5Z+}kgWO3~KellHVo+q8rw%9$${Ln)*%pbdlU_b>KVj{W3=RJN;nZ<+{VXW$O ztt>e(Asy{>RlQ2F-x%Yhvc?R+VkI^XcZOe~W#Pue!wkKPp$?d*23Fx$nFTX+08F$W zz1%~8C|!=D)a1Dar22t*jk~Uc{sa{w<0TJd3I`YJ!J_GsF#O0_sS(pJ+??X$TigtL z?Fqc>TMc%`07q#dbd$FOXxcL7gZ&s>8JL7^zj?U{yPXg4`%98{Rm+39Q`X-;|E(v6 z2QGH5;zh8>40r&hzJbabV~$6VF-S>75mKQnQ-b_}yn!e(abZC|TF4uOuv2Di8}fxi zoh})}tgwj`&vJSdTk|R%;9T8TSQNi>Dj5}J4?}}u6pjq8mJ*dx__%(*=>s_jaybIR zr$T}tS{~Q-VU$ZP=?Apjg4c2tRT3&??L9pP2;EJp;y`Pv_YJ5LPgYt0Z_1`zB3>A2 zv|(PKyS0Z8Uph~t!IPgV6q#F}r>TxO(9JV-UFo%dP$xltyrcJAI52r#k zv{^BkSIchU7lI@tUv$SHu)GvLATQzDy7LD>cRWI)UEA{Kc&Bvv?M@iI&*&WB6iA|Q zggCOz9&1wrvlSos{^A69%#eI8wXz}N$@onStn(637gb-8*3%h5ZaQo+_jIIECQjF5 zmp8pDL#qdy$=0tj(OiY*3EUa;p7 z-U`k>$6Y41;9}c0*qtNPiz`8qVWI(dPHR!f&C~r@`;R!qV*n`6o})1CRi2@_k39%l z4eIaQMdM!6=x&*NESjF|w4unQ5?j1L79YnQG4x`RyJoO$YOe$)v7eLcrbO^Y(}|6r zN+rTJ#0sT}0s?X~fo&ei1-mw!42FLpKqJ!Lj{gPtkX)u{Eq+=OFF(WZhtGe4UznuJ zhoVao$!Jarojf?_O|Gf|*Sf|alwmcbAZ8qH*U${;08Ez%s|EjrkiwQhihA^UHdl&i zPa2Hz1}$qKjEoD1ngpf9Dj&^tw=RWkHgX-oVW-c2ooBiBkM6eW20F6<*X6V=6=6M; zauT*120}HT9D*iX3i8+O?MCF}h<*pCrCYK%kGy=qZ@FSMM}u-I0T2wB2LCv*5&YOpg(97n9q}XN1 z2#r&plJ^_iTH1GAP2sEuHdF7}p}x)WnMORVB)RQ1-C3W)*2>2EU6o$!^K9=7&tTtC zL7zkh`Rv~S&0#LA1zgLm80CjaVa^37H$YW23n3 zfQJ%E=BtO&?<)hq_Nl;#tyd@+@F@Xk!MG4EGN*UH*Z7+iAA=%Y5y#Rn#wkZARc#K} zRS7_?fvqlqNo_wOp!HvSX4D3MyKwaR%{OdaqZMW*!!tN3Stux^OD4go6GJPC5*RC9 z0*+<<%-f|i*Fwk=vf}b^3e=B0vEwh}qRucXIfhI0BS9B>9c+Kw&cJ2&H`Lp4RbR{s z2+3Oyap|D8nsAibo*EF_U{-rr&D%h=554};ZpYuUkwVWI){io#Bs)h+Oi~VBEmlco z4bQ|YchtN+IcvNL0$2cjLdjoVA-WNlNuDxW8fBMwg<_1<$qFwiSQw7PPhS_J!3TG#Qe)R!&8+mM@)b4Jy^LlywR*_oUTtJR;TR_2xTGhTfVs3c#utRE z)V-ZydJ>8d8KOtra63*Zktd<<-xRgrYGdPDa;=|ZE)x?&OXEo``?H_V7DJVJX zGCs=a0!=enr1FXi`srum+i^3+YR=PgOV$A@W+g~{j8Hlg(CFGW20|TE*Vstj~25DzBY^&D;2d;o<{_0zS zgRVfDu-rtQ`G$j+1Xd#zeG6#19S`6GvYIA|9^hP(1ZSK#ROU$yay`E#OZ$kI8BYjy zhm=d}qNPlYN(S(;4~E%sq!2xC0S6VDzut z`Ghh;PVNkD+t$RFqnC};?KjgIDZ8P$H_qw{3b>yTzDQrsS{y}QSw70_2c~;hH-Qt| z0pM&|zt)QsZ*N4U((=Ug7$DrD7VziD>Djmfpnuv$)e=oqGDZZ9KJOP)NOhS0ezb)F z*vs&No=Q^iw9|^d3 zep4tv)s);m|6pW4j`*|s3x%=?#ghBi@>n;#pXac{(Jk@t&eVmaBL%d0Aub|zSfrT| zYJiUt<~Y0oeU`Vl_W*9}T@`GcwrW|ZsJyh}C{PRb0qp$7WGuMB9wK6XJ`uETnSOy( zSp{Di1@Jg?R)l~w$k5`2kTAC+aE+sr^*03%8zcF=wtH`*wbKwA9^U(&u7O6)KTN^Y zLYU70=aYoFKIyLJsQSai1=g7t&*xYJwz;>`L~?t=aG(7N3jQqr);p=TR0k2=yOlZ^ zP0jm@oPq&o_zSw-lojvTo~e?J|D4;VeKPUB38EFrkOW$!INcAhxq=$|*!VNAwUA2r zNM9=lrzQ`xLd2)uLCq6@-dwzGvcn4r;}*R}1>mrRA_*-pMp{G+{>QK1u<0yfV!uxc z3J67aOvGqjZ%fi5b0n`H65e?MKv`h@lYsHEQ!8HP&hHUy8BR5<^zF!(cI>Lo(Q5Y+ z2Ww$uY5fL!aH+nKt$?9WxC7gp&mo&wiHPgM&-5;B`F?;0jzI=J?TnBzE&ufoB6YN|FcVS_RtMzZp(x%YV%ZrdMf7ox z{1Hff*5gXAgU5snTBD5|6!Jo#j>bC>g_Pp}w2St4iyTOGpj$U~S|Nqr6N&bl)#27g zVHiZ)(8#9B7t;{;LuaUw^HvU~cVN%QD@c?Rn>VQS7ZZ{v*a>02W*pIMyO|Jn>_6zTO1wHF2&&|EP zi9Og$L+!(+BbJ>iSCA$PMrfusIox=*3oB~?vR+~DTJy0~{DsFeeIKwm9mh4HozI-d zt`k{4Ux!T3Zx`znoE8RvDVUm`={D<*gAj)#sIhwmxzza?P{lzF5dKAP(orI~DZ_mu z1*~{JGhoznHHU$Y@x&bUc-sFl>ITecY|H{LQ_C>_;T=;XQfp!-8Y!)Rk+cCnRxK`hUW%0F}fkVu!^F27$zr< z1X?|ZlXs?$Fqvc|sGi}<>hU#Z9K`o9HF{_AcZVSHYku2vod)R*fZYK1mQZH&6tQ_4 z9;CqC7^M7J`2=th2BLnleTrbMWX)fh8jg*}67IBl5Z}!PlC!B58PcTYkc70+ftpG5 z6yYgDl+5NN<|yhh;L&8)vZhcy2U^;2MPA<7%y1XyA&1rfxVMq$#H;?IUkH@8TP3Qy zHOmPFcyH)3>8U#H>$<|YJjNfZH+*L%%EJ`6ut$t3oe)GPpc_50>lWo|-=Le7QduAA zOnY15q*U2cO?2W}qFKRMyY%Y)gxk4HN=K9)GK!4&b3-rFT*FW#Cx>~crYDKjc3k(! zMxHnp$D3FVjWp`esI>WvEUn7*3GrJjCFGcT<5iz~qktwg1x;jrIBe9S34)@pJ?@R| zw#}d@VdiIjr*tN*`kIDz6D-@gCRRzz^fegN+h!L!n43keQgUX9^lP828#oqm-q<|I z@IhoZl2%RQ^%k<+adpJ_^$*94tBrGCwnya?PV|+@213ygz&sX!#dz+9gpQG(6*Jl^ z+U|8XXc<=SV>cGF!mH5C`R_R?Uv;0w0MMKJYI3u?KY9t9O2R0tdU`RZVxMevx)+n; zDUu3HX0`gjsI+sdLXHfbpRYX#_Yrslz?ypNQye~!?N4yF_j@)dYOWzzUf_qLAdy}n zbZUi(m3&yMHilm;whuOXPaU?a1-h!T3WpbHRlL=R*lELU| zH=iMo$Lt|94LFPeqH;)oLd4_@2qMt94JGf(fh4i1Rd$X+1gF6EP4DC_4qHvVRz1)@ z$`2KK`m?M0oO{{5g|dF=>ENp+e9w51Eb<28gDE?f45mZ2oup5@$_{A{uO$r*=_9`~D z2r0ND%Pxo<17${2#FX6S)5pf|$GNf##G;KQ`gt|&n|=t$pd^rwqn?|Lcg~8?(&=84 z7K=~K=|@G*>G6u{WXZrHY?m%<44j9RmiERI%lWcqdUmm?VW*sYG_>Ykdo0Ex>hfCR zcGLeBA201EYEm*Hm_jD}6e;;gGllQL+l`>FP%fD1Yye>E70NGyQf%%AHL)9NqPA@!!=L*AyF z88qy~1Wp;ge9*XkN5Ejsg{GVa#uuVn<$il# z>~v4s+fucE=3t23B>4i4-%q#-G=O*gQtb+IWV@k5it)eGCQ#0WkNwJ%T7}pZ0=Ex=SZFngpYi{5fF2(y^M2wZDoo7aY2GvVWSr zSQ^~e$#U<5HeFx@AC8;Lrsi8TR8|0p@7q+D<(Yl%WJE3R1`wgC)IjcYH}{aySdo*m-{fdki>Y^dUv_K=RPEUU z2uiGeiJ3jB5`?k@*z~Sbd8ZDjSKU%Q!L_J93Q5Bk7)CVciYOKfUv+32=-a6^^!PUvY5dxB;YiuA0l*ced_oY5<(- zQ1!?7#dCMT>91kg&gjTbX_7-<-z0>Y!fY{LTt@9{WBkSpNu9HqJFcBL1bql83?RITiH_&)*+i3|# zBst#PF$*^hDJ0%uCBR{hOx!}oA(?~}7htaetKk)J16=kP+v>z;3GLKYt?AZIUP(fr zYwIk$+6V0!(is%1^X(0q*vAJZY^a)a}Pj zTq|E?-c-hd&!0dDE}AUFWyH94@#}=-h=3$@X`m*lFFW>!YL^?kSOZQ)V>8%1`P942 z*yP+QEyfjoSMQ=TFroI)RFiw!8@l#%Z!=;5TUX*M$ah!tGkzY%agT#7VTgo~QckEr z7k8UiMqa=UOBaD>u}Ht+LIGbw-nxA~G3_kDEV?H%N3gPYU1uP$F@Z@#?|&_q&fx69 z9LncG-r|TJ<49x?k>u1y3!(*cy897Uj~L3`QyG!=%ydTn?vLy1**|8og?oR1>X8O1 zMe~AzwVEb&iN1H5a*{@^B-;oel#v)tq$e?crU$ z>SEwJ@8+zc8grzPhd0WDV#!HBpyJ` z>nHe6FW2`|ZXTvr5Q8i`Z&J`HyS6kg_gXzn%}ZJsy5X%e`zc94p{@Ymoa$d=S8tYG zQudFbFAI71r7u$f(wvtWGLy}Fo@8htl50hWn0(qLAc}V&^gB`XAtVJdioJP!&>eH5 zJc*EBm2pSad1^ow`=j;m#ZV3W@$c@c?WZgXv^7gSLeyKtQk1#cJvP+20|zAsn#VXo z+IS?q`nr6;Wt^=Bb&;giG{zj@7vSyrQ3%R$v}^(?dZ$$M>;pWQ6J%OqHn$}>c7jBF zftNq7C=6~QKsZ+!d`Z@EavJtl@;8I*{QD>t+v$Mss?fs$>ci}NR7*of-bU6A#1e4s zvi1aoo5a=DHz&o5!NDj?*Ap64flKq}Yp$u}dj~>4!PsNa=*a^K!9b$Qdygvh*2?po z$P-H2*v447p!=75xi`^N>C3Fm0VUJ#{XVp<`4H9q3;-szad%is6?NEI==w_y9F%&m z=6VaiKfPJV7to`ZxFqzV5pmnc2zw%8ycc(t=FNZo!91P#wG*Y_YoYl@(etXg9cdbD z?m+%$3GM5e9<3Y0=5es0y0P4S2$eRQW6{Ol?suiN-pp4ps=I5_O^`q5c_vUn6t`CU6nhh&?e1Pjw({{6Y;)NyvsP zP8{@)#k8-qdI);79{5i&wT)lq)IVnbG$pgDWfU_m|)Ux<4AD-%j>#E?ce9Q{I<$l1je87dpmN$$|q@)Asfc3*;P zF2U6fi%-Etm?Y3loJb%D%LJ0iJ5Xo5yk2Gim;zu~0`^oDSf_jBn@rU6kAQ6}!SUT- z`YAVl>(TaY9ZTS>Kp5i*4dO?^x-npO7bGTEsdAH1Xx1ImWuwq6jPO70y4A(V>HwVa z%%bXxUJG;n3+wMry*Y?j(^V(}PJ>(bq8uq6MKPmyfcyLJVA(HU4xQ{c+BLx6{rqJA zw%cuwD*A`Py7VeJ%A1f{-fFFRYa;9tLnR)xLHeO$O^%{TY49#mVhsLB?y#(8F`2uy(-VtWI5LG z6GLMpE(CnQHVvAJ*Lyi~birE*@GUzMF7txN{Sd=GIcLfQ{t2!>n7~i$Tcwn!ntZ}Z zat)x)p(NFE2<#f3^5=ob&B;p&k*@~dusaGVl!!m%BzcES<)g3IrpS$WliOe)^5Y$ICNtFku5e5cPjK&#in|L)K`d1UGNe_WnGghH0jP)`AOlilru4z%s%s% zy9WvPwXCVM0Fi-dJj5!5+u|^n<3I(A0|n37;zB9~wP@s{2NP+sLIHpaWkF2T1sv*g z;99YDtcf6Wz2(XE=P!6#hHnZf@>0g7c)u)a$!RV9sdMmd*>_lWkLqi%1qZV@Ey!sX zpjerYLK|}2F)lcCiGrVcZWXw%KX0`2M0sd(e6HV{S1qH}Z|i?i-&2D^X+O$Ei!iV& zb*fxJpTLtQpV78dLQ=TF6>E(Gk?abCbVPDyt9lEr80GhPvDWGzvG-SxjV{yG8pZ{C8LPryMRRLBmj*o6`v9Ik}tx2?B@b3FHoG4Amm zty@=D*E`ftdS-z2_xajvo;W<1vL|-3GkbC#s;g6b5nj87i30;sc^{^-?cAkuAMz+i zUR=mD6B@Q;4P_z1g{xzhh41%*0?Req))hpcJTv&->-yVOU|QMtsS~MSbtzolyyV%k zUAy*M!H+Gm+=%qZv|uo=>vr>#C*ucioBm*`$xq47E?C`itmWvP!TT*QZvw75Po`F9 zV$cQ%dH#?ab>4FW#^mSGxdd3iwufw|BWDhnkJ#VyeD3yLEp zm&zf&ElBSq!A;K1ye!p?0#Og&CMYt|A`}Cz*Er>Al(>i-=7@r$C{!l@ncXBUk(Z-( zh^a1oJDjV_LWHL@lu7W%H^goZ-Y&3(2R0*Lvv}=U2Q%Mo9+{Du!6^Y?*C?3r>}xQq z%p%99)~?m0-gVR}y(A?wHxGk)!@HlE{!!ak+>k!C?ct(>3Asg+j9OAu|Obdmbl zS{U^i7t5UNAWmyegknt-PPi(MtnQ{)TwvK0uBBJ%U-dvl(J*OYk}aTJwR((8ow z61%BJBCH@0R`&6fTq58`cGK@oU|t!RUW96}$rXkL{20hO4kOM&5}S0_@XRD$LRY~< zwy;i8-HdC?IJI}PuNB`(DVHi#*@p`FLf6RiVOBP9=JBPwwq=HA1*pQWcNA1r_hDjd z%Iz|)oeycAE7;@z#sc8RLHwKWg(;(oF&UVK0U;Ez zxjGBfV|og4>!;9@!1v2J<`Q?I2lAJ~N=fA{T&)61jQu&8GsHR%lLU0{Xnc8(2*pILl*lL@>-+6$z z|91G*;~Q5IWJo>N!FRKEa6EF})b^s=Q6~)4a;{11kd5s{Kwai|8GpZqv$r?~>;|h_ zHC07|yt4G}R4_eQ8LSD&On}qk%p-!Zut=vQ$R;2bsi-00>u8>}*h%Ct#c?-E8XItJ z9;fbk^%G4&Rx@(J$p`ZILdOK3VDpQRcX-K?W$BUG0V$!qgY`GdJ28nl9c_wpz56vi z+R#3EkDm7VAaFnjYy}#1pYr5jJ9^SuJavBFOvFjtlMa^n2+pxBT%?Ksl=~@4s-@S6 zK6V^D<_qUvgnFLG{as8?=7*(4Wd^hj6|PNM z5(+%<<<&yGxTw8+zRI$85Ao7Tq^0Rx`PQeVaDY+-(V~tOiaO@>EA|Jsx zTX%2mFAau4nCs&^nXaW== z(f~|+>$;?x_^}{knABP3avG2sluwzymz%_nc)yu#`>$qPQ7yefXjedAUL%t&FA|7- z11MVp+Ocf|7?~d__h$kfaX8NLmH46j^Y$AM$1ba(B)OzGkIJpW;RHzI0R=393N{y{ zcE7m=rgwm8o#@5K00{2~GITiqqP0UT|3=u&vGJ3lnkpj7%S)w=1W03BpR;i|WMyoB z!1+*MR#-|znm^by4CcK02`QiQqI^Seh;c4R{E@}qXD3xqh3LW1Oim7!EV(J@JyPw_ zXFFl>%>GB$ zG8|0ffNKkZ41F(SpEl+&dZDJd?d(#vt6<&-hm2tA4Yf+++Nto{=8~Y5}@nJM}_@85k(HMB3-NcjoNd4{bNvHSbT1U=xX)!W}{pi)I)DsPwb5PGQ zQpH5K&dg+jEvl51_u$$KoGa2Q6b`xJJ>bhupy~u0d-T2=25Sa@VgPI$M#5^>YlnCi zJ|6_|XF}a^=*{t zg^WvEj~-r%iX=5XxK5ALj@GRgh@8DCb7Jx<_62W7?n26)q^=hD3L){gg&hY)d_lC%qV%>VWO@k~DqBZ0VT(0qGK@8-_+crRALtc1#WO^W&|J~Q$ z4}%$9KQ(~qgvde_~P?xik%v1bsM`jqLP#MTQ#vj|MW0CeJ9H zFLIBzh_thWB&rN<-jp9YRjzvZ-N276U^WKMvJrI`a?*Lb!*$n+)6Ez+vI|&uY6@{4FtL`d7kL;PqKwN98b>$iFc6K^$ zZrtk7cwrnByU<${?C2!Hi$+hDB1kZ+qowq#fqUln902;^<(7Lk2u zD3o#{R^7hOZpfuX#bp175qn}kyBal}XA6$8 zEzm9XB~fiS)El?wcvIoxV8I}mtwnrG-&DyIb&b?jKet`2jq5r1Y@nB%4KA#I9bq&|H-+E4z=vV9Y^(` zOmiNnLKviqt##E!^N_<1GR7xA=NMC==n~vvi;AhdVO%TVbV|$6m7X6r`A0hs^-Xx5 z!QXG;_$MT}xH-sF)(8{JqZIy_xTaE4MEsbWagWq(<#bDH6{P~MYd^5-1*W~gq)!ic zk7$pg*fXgsF6QYBIBn=EUJ-~KebH5f-+#D@&ZOK?>THR>H#p6)_L3q#a6d;;Q-Sb? zZWsGTl?1*2y^k2fDQr2}yAarEk3u`n767b0TM`n&^tCZ2IJmsY)i5%*>W$qNV1x{WLwRa+!<9A28i_s`(_W>$Z*i03c1 zT*TQ6Yu1aMfe>22+EI{Lt&JCe2My-OgD^d&!t8PhKfQdCeflDTWy;jdy+k#uYrv_b zh)X0Cj`7m2;;(7^3_FUBH)MgH#J8#vzoq9(inH|zM%qUyDNN>{&6DP__w%01 z&@~8vX$z(L8*S&y8yS6?h$u1PUl=CFT9ei0gNtUwEWP6U_DP1|<(HNGRukXs|a{Aj<%*s7Zxo3Q->QIlv%h7)tQtM!vWt zN0(?+1r`W%vgp)|@O*X^k9{mdTPuKkm#3J;-F4B!Z%npu0zQrJJ${vDkL+PQMRG5xBMOQ=~J|u#|+*zh3QeeVIp<<(}{KX-9^ls#mWi%;OzRxCfpyJSl;FQk+HYb zwbk{MS8MepWzn4+Wr`eN6m}gdH|xqusvuASq+|yS9QO+oNw2^E!t=Z6c}kI-3Yik} zzo22YUEfIN6R}3<&wbUON`$eK2<lO!x+uM5U#uuLa=ll4@@G_ghS_x>tOHI!%o&=&P4E5Ei3LJt< zlp;m06ahO0Fp*7L&v#FrP)KAjZ?B+7i?S6(>Ads(b&ZV!G`y3@PZA|MOS3*JG=pzo ziYjCuPCUKsv^BH*TxIH)m+e!f_(}R>n7jRJay~P(s*4^W}v>yy$a!lK{)C)JU~T;UEQ4-5?kL6TW!8wuN6POoHBm^+6$tuI53kPW0ol z{{q>M90Wufn@QcsGoZrvb0Wn4vT(65B%Yq6+~b23si~?nyx{M;QBR#pVG>MH1Op1< zg@4h5I_xTi#dSmmGf&v9S=0^!8>?U%&U%U}fai@&4do`I{bUU%Gdf$E5>G#cnujaQ zrY;)4g_`YT-hy>BSw@ytKB{aftuqYJAE6#tbeqU}Dos!Z@mNn}v=EOA;xEm^Q0c5E{YiRuzCxa3$t=mUBrJ5bw>3}D?*A$1oHve@ zeowo{jjzZTnOm6NMzOR%gXt|Sjr`YQ`gM48zOa0h!BtS$SD{mDaTRi3X8{toiXuP2 z2&qDDM`v)*$T!278TxqegQ3+wL$fpUmszY{T(drFVtFS%!}QgXvSJR@L&>C}f|9Ti zW+?D5K8re_;LyJCyjvovhF(Ix3FO_hv1;KN z?%k(>@r;9x^FxrrDVt`v%Ykcg>MoeS;7_LRfJZm+41Rwa3#LAwSd9-}a2hUG zWbiY1U1>>iu~zfA9@LF$bqN%oT5WhY2PO}wZ&$XU$OK~OKSXKkn>8Ig=XL~oXTkk! zKZ;LMYm=bYa)DfXq3abD3i;QDoCbgBdK#1t=&|-tO)hd)ktA2e@m4Cl7^i`Kf5m`s z{BkGbz2orF%^tf??+kl4wpOOks}_IJbEW6{_(zlDqu1}?HSA;f$!GDAEtU3)DwCPs zS?}4fYv`#9mYuA!N7EVI#pp zMQ1^y$KHoO{2p~RDJwZi~%t)fYqgfMPC`&%*9rfGzZ6c*MbjU<%2 zu<{7=A^z=siv10*W3J=h_K;@}GdJ*WyD2t$Wjn(@jKV;8m?$_3mxmPC_+jwvs*q2d z1}Xzs$^f!B&vV{D{jCJgy9948Cd0KJufb1~>Io3rMM{xCswej8GSuawzV`uzt4WUJ zx*eavDdV5c%0UM>89DRq^g3tQJbr&}etx~*&h^i%a{qn;{~bG-2Dp9e1XtkpmBCfs zkMvhG?dnB(2{pPd%VkQDn4lE$JdwH`ASdoTyh#(OpkU;La)YR%+@cIQcQ1;S5O^Bu zyn4H)iFXA-c!RexmVWQ|UjP(R?*LKsHI%nM>r9eQ?Ax({`#c?KrFqq|@4uS(#ex6W zzN^je%3fOsm>JAFuSAUc!bNIjA7u(Wv%_T3(cXu{UP>^hS7ugAyQ`{8E#+hN-OY}U z*1@WwvODxGR)kJkGC#+>igLWc%$JsAmLyjd9L|r)Nu|@zr55<}eDbugjD@yQT`gqx zSJ+jx{ahzarUGpfzYu@~xH;u0SWE^VbQi)v?#lh3Qxz-ae2vu(%S@J2!QcVp?l%E5 z<^XkaEgk?obO5=YJOEGJf)`@O$q1Y5p2?j2a{9wLKPX`Do5crbzPN;&mewczvi^Zp z?n%xcmJ=9loka~Q@r|ejuB6S?s-+VwVNWi_qWYb*ZD5Y$&wh zLjDDUiXBAmF>>}<_8{ftIRC(473zZp7NCxLm7|A?Pf%0p8jXXm5GIn5T7cYFIimrI zCjgndQcfs2xKHcYa#n{zp~V4&P^3=s=d#~WffcFEIs9`?!-Ze+uwbODrA}b2uNn_wq^l}2@ z*;S$q`DK|U8O14PjX`tKNFVW}Vq|KaoGkSYE8vlXFQ5~detBnA)h9S*n%=S-RrDK_ z{9L{)L8dK^+4n)v2sJ_xC$4SZaX5SxpU9o;P+Gm7|>WAn7>;PoAHn5arQd zQTQ!&^}))>THnV{hxm(j#+Bt3=j-#TM$;?56Mij>E!K;`-?ZCj+ zLCHlBiU_bWU&5PFx2q+br3ygj9K<__s;pIxnhuW|PHJ#=q6|u0gp&KS%?aJ!a*jRD zC^HCY_xplYJTn<>Z4KO^cok}bJEfM~O|n#&&)8Kc9x`oY0$j}sm_k_wSz(?=QLw_u z1c#x>u&CR3_$Ab0bTwazIRx&jA6}%~?|b;(P4y~r-}G=ab|Ik_RDPzeLL z>}_CoVhf|8=uxyN`(PUGr6q>=;S8OUVm0-8we#>{GiY1PBTyl0m5{*Z#REKLmhUHdH zpfeeM7xluICuFEDj;!pCB9b2wt>LafP>_Qm9MQNa+0O7G)!^ zuU=nKBXF2Z1J&QqW{k@4t60PZ?g7x56tH{kh9J^0RMll@t7t1WYxKJEeI`ypSlTJc z@yIu#PVU=l&D1HXQq&5qLRT<%scod|6B?V@^UU*y-wb9^2;8#;@j=hz-CUiHm0&E%GE&R@0R@BG3u>#gv-pqGA1~nJQ~1fr)y^m8 z0mojoh@XVo^0RDwW?9DU2f4y2ei#b*y@J%)9Z(H-p!`#L;LfGw>&w^}wa z=xDVKTZb$9iszu}XP-r(P)#QCFC%TLI{7R&jXgtogHT5-H~=+GvwE0^eZc(Gg-*q2 zVEjYP(AuHt=PmQlKnLM5fF_W{sl0hCd`2up!%altcp2snF;Qy7SEEjl9a#ALkix90 z?*+kI7j+(rn0m52CP<-ui^NaX5K{xxM=%d0NZ=6Lfr8T{?J9-7DA^SJ2Z`oFnAk*&@$=1muI83$~$uB{Wwzpp`RxJMaV5XQfS^1GD1 z7r4%KKTxAk5q1&+`s`WdN!}|s5cO+1AqEv-KA^g!~( z=x;$$&|)PD{MkT|f7(gyP(V60n2ce}OC?I6>he((_)^3I_rFXG!^^a_E+lH-?{ne; zrI4kQ5@X;0DKTovT!{DmJ{kU>mq0b6O%_~Vc?y8e@XMsjakOzUhk|kyCPRW_33=>c z6bmntxhf0*r5>ju;FJqt2&r%}19Qv^tT!TjK={l*rtxcFTZ@~+rx)>>1?&d*`#-1f z=MeB%sczQPsarMuTAIB=g}@*La)gkPj`Ljn0W}V>Dy{nB6m4#aWW0_ex|P}$eZgPK zF~^i-7|#0U2NoVv_DzgjYnvGP2#U~Jk9JOaNM7bn_QA8=MMZIK1DCDs?bpk|Ggel4 z#KFl8jM?Y&PF7`JsoWe9z?nVO7&p6DdNwm9E{rZpQ^0UYazS&>A8;~=4_>E&T{RD= zOd8stS;4iaw~0)=PjkC2vesb25I&nv-9q!PY*= z;32OoNKvco=;C58D1YsQM@y0s@U0fbK_@H|XrTZL0p}MLs1g@S;WFS4*5V;H96Ug7 z*fEENu>QPv!FfM0U%xp2>ims?HPz5wVy+q~tJjtn*X*-!3d3{4l269I8FSQoZ)3V4 zw<^7R+S1kxpJ5v-Wc^W4G-()M8FF;McfkH2sSX2~L=68XDri+U_Hl6@_N3aoe&=)J z!~mkR9$1K)eEWs6J>wUGkDR5dqORA#%N4Q$lkyoGUEF7pG=Dv;TH+jxmSjbw(KxT4azMjj z7hHgeB^wHwV4O98+5p71!#q*|hz}HX&!aHV#7W4+-&jekY}}O?MsYalWQf1tNsqIy zCt8y$(<_qx)p=!Tp_fR8H?78}kh7p1ojsRll;Je4(Q9v^bb!H3lg}wz?CXqVPm&;-bst*F} zG}J>m5aY~c@J|4D{e-;N!jo11enP<$H$XD+Ta$R*6n=OrczWI67V+Cl;|p*KKb>2j zu+v>U;r!W2Gxbl@dJpfL-E!dsUURUzzNFk@etP`y*ojZTk~T2dT~e$r((C;OIf-X? zdF(tR9M5i$e+1!PsIx((Y!WEzRn-ez97bILw#>mVOcn!T339YRXo7$Tk5R`)aPPiC z?MsU-h*MM_fGbg=aTMcMJ145}nl{B8XQxD-zY(PE-%{?$J5@x~a+7_Sy-B{$O?DkT zS$S^dMJ5s^LJ|K1m z3BN1}8V@n8tgR?A@_Oysl4?Ptp}KX1+o-6^xAQZmWnCUu2R?6HXuqR3>dW+HQN0{R zcCsoK?nV?ZQ)|)%z>g-1!5w27TMe;*Yn+SApE8+|zI(lnr&VT^M>GZpaQ=LtEAHyS z6mY4YJWVU!C@MXFE5IPxVjF zw2c@o;3O*#?cylX;)%TU1gdQocreU`K3#@>l%wo$}5g^a%6$0554tjOiQ{kw7s{#Yp7+wXwzy-D`-rw zbTKh!A|t&*Pj|<(hfmP#Ip+sV^p5RYH=Hn~6J+<($%{+VH>=+FMq);{w<^+g&dqe)jW zr)X6xOLuh@YrZ*npq$Bq9Qy|DzPy^1FZsio|=6~=@H>q<}X%cQy#t%z3i4=G!CU)R-K5I7IbP=lRH0Q+IBkwz&2d;16S z3)evL1RYgfk7kI+hB*S#MBN1QH_#vR{!_{e)c)oB%Jm6DOJMo4_5m3}H=FxTzJDbvj2l=Hrcl<9RpEJCms zzHc6Hm|NeK?S4Mn`8{ZaYwC(CY#oPdIOpVf>6yuAwjK9;#oIy#qkN{jv9_mg(Ay^M zIs4w}9WgA5HKtdjS0;ivZ{Tw4=d+)kxE+P*v*q^UN~4)wZGW$M$H=vr#^$TvMR#xr zF9-FY7%2K26yTMRa5C7hE72Y_P48G4VNRe(7or8ri^EQ8@6t^u+GX>B>a1Q_+rj-3 zKSK5XyM_lm0AL~p|LIHA;}R%9T@QFbFmICyHL7jU;qHSDS1nE8rmzuIH~?WX*UDuA zsB9h@cn}(IVE;t9AJ1lQgP>TEo<`d3J?LDXT9YBr0EQ*z-u0%UT#)w%Na0X6b#h@& ze4UEjnBe36X7gENia|y<1XiC4<{r(-@|9*m^TIDphP3^7T4kvLdqsD>zPwuCsIKZ9 z=62?_XLRw?`cfN%CpzZq$Lgk9z{*z)){?wAoU#B_{%Y)X$(amAgwq+ce2od(A0D{p z*xS1{c|Q|SD=p2gklyZI>b^4m^_8ia%YPX-Ja~G^=k~r|4*u;7U1=|`Ff+AzKO z+V-V@(U2=NTMIOw*6L6<^Y8`K1>}|u%0)#|BGK3_p-3p;QUrM`Uoo@JWKU=j8kJ1| zJ|s}xpv9|)LXRkOR$@pK0N@~;N20$-aMjy!{2TbMj5C={0n3Zq*(tn_h}?dh_#QgM z$a%s$IGvHp&oQsDG78pp02YYgq?RDpvX|Kj6adJA3wdv9BqiM+P$0W`&ok_!uMnY6 zb;8ZdeM7?^_Fbx*ZvF=ig~~mx0BPDpq_(^ABUZQoAXnE0ngBp*6yM|e-(Ls4iAY2$ z;#W~~Yi(om=wTa25x$AwzKAxl3YXL&8~La+~}nLZ1kO|3Q$fNjP%w9&!&^-2L_t@ zhVPUbfuS#_-%DY__a>eV2@W1iZjJ+AW4zOoAtE;D9%4k$W3QY>0YbGlM&N$T3OeFM z=;Qtf8n{HFf$M01bOUD~>IUq+g<$Wd4(`fd@g~OEvv;Kj`K`V8=mtnR-c0^qjZa;` zLonp-I7dN6xbih1Mg&va-i_|m6? z*N1+f*+eX6^20NOrKfz@|JeJ&o7?KZH(?Ua&NcV6&(R7Gl^Emx&B?P94#TUJ?5yB(YHp2P1>Vl?^Bsdax}#E&fD2N&?#CH%~V_3ur%k5Aw} z5O+O;;)EK)4JN21QQVbXRJby#APSlom?nD$xR6dT$G%LZjP9=usegB>ffMt2YRl=5Y3D-K=AvnK8| zcjYmVUde#Dgv!#PvRvXiAETz#4hT_n-#>vLyTE{TAljRIcOxaqD~JdW*Oi{)mrHb+ zdRSx6Hz@0PZH-#JSB|}os2!>v7au= z&V>v*Rb~zVGa@f7)Ybj$^UP^b1-r}?jjbTo!tbH&yS4_ie+o6HmgX1}@W#1srtbj# z^ai@}8h-dayv4h`uA;ie#6I1@qWPoOX`pzATa`eiz^o*RRe~Y(K~!z6s4+F{sp8~_ za#F=nv~wMMi%Akk#{0&xkL+My^kIZd3h+3U?1_L0Qul;WH+34W;QTK#9k9WYtkoTTRuG3m*oq~HYh{sZj&;q2>) zHFEtqU^S0);wNYEx^J67%TcO_f(I12iWn1)QqNG^805>pM#; z?ZY;9b@QgNw>;lEdHT6SO=6AGnE3e=l(Dzz`L`!eeSfsFL9MB4-!ig)(d*Nm>kS`R zzB110J4!yI`>IJ-%05%fh$*spU5y%(Eus1CK{$*K4S&=%?w&W z;4zcb=>gm%b{!}~AdJLJmVJpjEtH!ir$PbLmjHmTvk?;6%+e7k5)3k8LYp?$Y5<9v zL_H<~3PO7-NrU1c1=E!T4lT5~I#w5rcIG65*ReEcj*hJyW8DAe37qvDO3g2nh@_h0 zv-~oNI!^~(P(4gnyc-tdgaCg7AH`0Hu>_$Ku(X3HCZ61{8n8_OVRaeQ8R3B8gBa1S zYMX#W^0lAgtam6j3v-#9IO|=??c0g#!%IVpLyIH$e&F-Z%wp%l7qc!(yBz``Eb%q* z=8#DnC#@m9F1dl8M17#4xQE`kR>@b90Tc+;tw*e!^uSD!Ff`6zBK8llWL9KXC--!> z_Oy@F&ZtN2(Z)ne=tM0iwIS1-)I|S=dSSdak*_WkDWNr%p{jb9HpR*jZ4TOh$g?)9 zEUzZ5rN!cCI!`yQu*^Ox7%(^~xxE{dja}TUcr!Hs<*SPZhd~c2$j#T=(KyWL^#X-W zWuGBB^AV6bG=uz5t~$wE*^2y=)Ec2cBT^I8{{VGTB~?iHS`b!@1C*=^P!%a8H0Uo* z!CLLA9NL-z^Te;AlnAlJKRGHbDSunKCAloCQhK9i+S*&+Q(xC&8lw#pjMfg(4V^R& z--B4E^BKmeJi4NU1@V6$kHF7v$1XW*CqO*08UUPEo%b=lF&)t@VTmVWJj2gK1;obs zgjmv`Vwe8;i;;z~pQirNjvuPVd_BYxYpZC5oGkt?A;VZ{Pw_CEL^6IFU|CiGVh1(q z0Te7E5H=LHF5PV_%J)7Op6)ApG3MBT9i$a7%9cKEUuyjcs%VtGDbrK7Pt+`Q1a;EO zT=p?0EnX}-dM2eeyDf7Tzi;B`yRB+7Y?7ph9QynVvF9?C{soV-hvfO~3d^~P$MJK< zZe6X~26-47*l3HFznG9GSNIk_&OV=B*s7{79DN+WSKX(xYmAW73>&o+3ksH`dBVa_ zc**uOB}5MwRXtt;d5+!TW)Y+O?-4S*2`*|FN7-AZEkWoe=`j6O)?Ycfl%PZD~ zs`H9{AAdhjst~I(;GN+Wxjg;Rx?-!gQ}_7CCB;EHaY^Lk>;qZR3b`r;UZKb<%H)+C ztSfHOw?m!th@om%MXck*<4^B>CWw73z9zXe%OJ+<=|!JuUY)I`(I}{H zFY7G(II%KcCozNV_FS5`lqd8Xx*8^EYiM$`v|DXpPXq~HgtM0!ll4djBf1ATr#fQI zr|dwBjZfp(r~d}f@JovrEjSND&+Bwf<8dbYNKRaGWO#?DEwj<t_K`WSWcF)n))MYv=uP?mxR5sK#%VMulSe)es2CPS?OxwTZC4QuzGs05I` zK|R$_OSCiZ;AlpPP6;*>&SWdQ6LWeDczu^PjiJ{k*`ECr$Jp5L2lv>RXf0- z160fP%9gqtr4DUbi3KR9yff!wrN=^8>ck8C-ZQ0?Wf~F(=B=%DW3=K(QQm2;Tj~r} z%;2=?)z0umJVI3X&Nsm3Qmr>=+O+sUb)Oz@w$;SwbIc(#ZJeX{4{>8};Rn4%oasH* zxK=NAU7AeEK9dFFPs@mPz5%kz%56K$oV8gBQN{(8cAz)v@FEN#K zr31xn`et>R-n6@fqb?{aRB*#{52d`t4@PIDgwO;K-A;5Sttf$AE8ZHh`Fe0;@<^Ef zd#6usI%3W+WEhjazC1KO`bB38&bBZJZ+&}fz?C|tD9K3`#FCJUKw-(nY z?sV6!!okZAGsjS@>r274uXg~6Jj~eH%!nl8^|E zaMWK>m!<{iOtP7UCN`Xld$bI3d)C980NYy-R%<8qerqQ7!LJVJO%41ei=nOjcCtRX zED1isI*7ty!9}*Vg05$9$(bC&JmM!q_cR+FhJ|?Cu+kud6(CPuO{B z8sG9J)Q`#nyDzo7DpYU>07eXVOgnIy1f+$31S>vt4e$>Z%nZH-+L~JkkXDe_+=4x> zZXq|1Ijoz-r)GYgT}K4+mpv}yj~3U9-S2`zK9XTAp#35yMv$o_$3fy=qEZU?%4PiI zvN!wWWkPw>)^TTGFAp2IrVFFvh?1ti2^8{mfGNC7sBH$;K%5f$baG7`l^E9@R zUbdf=AX!dcZ1~ZV(lCC7NSC3N&;^4%${ya$dg}s!(O|6Y174!h1@6R=3X?Z>LrrkWJJ34wYJYjbhqdkz{kffoZ`++-Zu9Lx! zv1;})qOW?4ZGb0EX&`g@>e>F zZQuY78Y%Xr*{vRo)4^_7aUjpN@Z^nuKiL9L-0+tZ_+=2;Rl>6K$0h6qZ+#oKw3IO` z-%z>U>B=OYWsgx`rL({~$ZP!6vIOhe13)K7!v}$;u1?v0p1bTvHRy_zJw%ozbt$*5 zq4-2nRzNLlx2YYx>yKhDU_iZyM%Rmd8J_r%Cy$|>N#VKM(`l!>^CA-o!XgPwjAm__ zx>;Sv`ggK!AF_`ge*LiL%O`45D^e;Ye_mXizwq_^ris(P?yhXCG*{Ko&n|gRp7{JT z{HJmJq8;zhT6FcAa$1onQ6=!yo{X5?7R9cCjZx!g66fSsQ!br5pY>PT&36H7=I}_&FMbx`E;5jTN(8ZWS7*Nqf>xVYj7ze1?X z)Qai+iCINA%waW)0-T1ssR0;v_YBM~TXr|t6AI9I7cmOZsW6@tumzyYzz^0yEWld> z-gBHYl1)#(_>;TtiXJ~+L%T&nJ9K08i;>SpQzo0o*WnYh z__1lcc^dDTUe5-*Q+L3Ia}}Vvl7k z)f6GhT~N`cQvNO-3yh~WRnsUJtEu85gUZY=DpMJ2xT-b?E^W0Ot%+>)sj#UvwT*$3 z$sK3Er0Wr6rYp?kl7rwpjwi(1P+e(#c~hmi6q28d!$vtd=gt+TjK`YSimOn&G>NiAjJhMy6U*y?yXd6(=)1 zT@)KcvvF{!sA%|ueKnjsS*9dSa%$`GrIu$`CXII4>pFUAMVu-Q%CvVWF`V^u zIO`Qfojmvd!s1=*IPwCfmQ0E|Xma4Z7on(_aIueQN7#Pp3n`}L@{G!a8|FpZOvBBt z>7Mx|S_O<#Di^Z!@^ZMU?4sN}?z>d9Dz!icvZ(@#sxq9soDA5a`(4(E zJidVQnh)nSe}rN4Kf?KFWbM62H~;A`ram6IJ#uU03iPY@%+4BEg#IVn83O1!XqZC5 zX-_ofx~6*4yp8RLQw1_dQW}ZX>0nyePl;4n3K3uHaH;L$cCZ;J3>GbyAjstl!x>v3 zaV*qz3~IZ^MxqCL--Y}(5iLZ#3z_}s3;0HTF3|)mNiZ;Bk=1{UGhM7R7)R`?D$uU( zr|Q*J0DvhHWrUN+x`MBxemD&-eGtqYFbJ=Ijy^8=u>5@KSJh1rITtj;iOfjIJDfu& zf+ol>jeVV0l#@~<whJExHMb2^A3F%-kpRXtF%=7fTM7Th{4)PIF*_=4;37@}RG$!vbEJ##;b;h28kBF0FB1b$-XJ9H!2?@M+h2WpCv=591K32QYIN@+uV(2}bZ= zP#aWHWd9TZ2ce4Ez`jGBfg(Wal7zRwa!|30NqapjNODMiCLN^XQ;PM z+AkZ1EI-p&UCQ*bTbg&?U>`79t12yq-Tx9^j=&q}pRgBmd|08~I2{qjnTzU;8~3B{ z*8I#|IouL4xqb9J$L~VG=%EGLYvl-|vlAiGC0NRRe2(}BB()yo)KpcJRg_KkSxx=@ zO#v;!zGPZ7Kbenr^Y9~o*XaKxD25%E42^W=3+x~A+5J5BA)zIcvH&K9pv+q5583afPJ<`oxmTV^&TOyXx?kmv>lG zD>F*cJKIW1yM|~5BnQD-^9~iKN{4t|t=$gPkMobjrvS(hOj#l^nZ~w!gC9iw4MfK} z3v^Hd=}{i|lpF&Dsg4{2eStr3W$mLkvCq2{>*LE{pZCC6<+~R9yaA72u`vwXD+5pl zHbWI#001&jlphw8^boKG??&MHO^yez!#ow7a8JHX@(tjYpL!LAD@6s71f6eHw)AjU zHZJR!OHpl-Y47e)&dK3;+rA$9Wj2$!6x%78_w5?zc%F~49c-hajM>WU-L_%-(YRhu zj=35F%PTIAjSnwgg^vEw$zR8Q82e%3pEIj}p7{{|`(zFe&XZ(;<&6+;og0%Cl^R8# zB$`H|mk_IoTJIAoQYBJ|T`h74547kVu+oA?1nND|qJKj5s%y#Q;OhL?N06bRs<#4Q zq=(Uv%XkOnugnFIO(SopsFDd$RBHihT+IS(KKKedh)%El-^XX*=arD<2N=i?fW1Ok zCB?4EdnS7uKWBehK>W$tbd9n>NKa$R2J!q5&Zhw_ycO>2i}4JIQc^v+D+<62C5m|H z8)5#choV?X%@>)$TXM0HY@wKVfV;9n?NvzuR&>3j_Bz+OLS1(`bMVExBIfGu^ReIi zRagyXW7V5ofz6)tG&f3FJsh;w>W+)|4xUu()bu+^_i?%&LaMa zoZMVL-dV5W($j+S1Z}o9md;x>>ICbJ|eRg?9^}>f8Q%x7jYQXtf9M{QFBoUu%=vSymT?5-# zKyLx+x3k>00x6H2B2ch`rODDxs?0qo^o`Kt2M>68MT>U(o04^TM)71_vN7_jk6joZYpF>Z0oSu3AHR zOC1gJR#;@4c`KV&&M=|M2-lZZG%Gv0xy!!5aL2qJf7Ev?ePw&C$IINnjI;4 z6NM#2B`5ob+fsE3Yr>tIts@h7J?(b#0x-=G0TjGE$LNF_P&M4$P9^6SM1=+ED-V-) zAwwHS7tHi1dwHMNnl1>I_dtS26D&MH`R6iP*g->=)$&}`X|-yIeXMKO9TwGCjFk@4 z{x3pqg#R4zm$a#Kv-HaOd`1zLH&(?X$ym1BNeYT+VJlG2q;cg6MUaB0+F~0qUaeSYx#b!TpTW z4$%zIW;*Q~;1PD^ors&l+zH2|d>Xcb@|j-uDM#$wIO~x_dGBHP#n$zyMHqjg5gP z?zTJld<6)KLV~+VLkI;~FbZQ6h@erAg5)l%s7#%5iaX!V z;3demUP3F+vlo~PmzoE9zwvT#&b|xowxhI@mNWS`^M+C{dFF6xcIiXa+i5&h&B$+O z_KGi^$lx^Ws|+&ir17s-#_v|#d9Ta9LeA9wX8-;zG;G7>eDUIH=ke=g_karLaRwKo zeyDw$A+)ei7)yprF%EHId2Rg7B!?vq)Ph?8BJ>4iEy*|5cXg67`c+c>7IZ;&abZjx z>6`&bJ35OEqaRFM8WB#9jC=r{(*e*o&f%};)+bMZ=G%D%mofhDZQRY$K31SdD&%1&_{7nB9CHsj(qC-y}^$B^yuQsJLuS(WqHyLV8olVozO`V+s_Wt@M8nC|i@(ySF z(#JFN&V})!BekXef=a1A6;Lx}bE~qJcg0@b0Xb6bCX7n)gPI=L^xXhEMI{miKn(fPL_Ib(SH$GUZ0!X!qba z&9>p|OekpBNuC5m<~i!Ydf3l)qpguSg%Rh>dpTwL24i)}oa4LE+0MdwPJ|>qH#QY+ z(>kgFzt9rktW)o3+V;UuVKIBZm{^;1PYVN4rcX7z^rZs z=_xcNWaJuA5Y!G3)ge@8Lgn@-X~IcKVrzgVT!f1l{PiyOYjkEU{(1*Iy5aIE=aW!E zM$E5q+VDkY1e~|TC$b+w7*b9Q_YB)f`AGilpdhvYoQ+<8hW(Fs*_XW`PAaur@=fnY z{quukT^;SS7indTN|eY2p}4;)rGR9k6gX7X(4V_ewW@k$7k|vNUOW|Y^VkO=cvYaG zzRXx-W2x>P-LG8?X-=q(=yP?BAjE3{=&wL11~10TPG5$YD_|j)pq6f9jlsCz!in_` z^E>J96L2DWd$cvBEW14AR@Z3jh1pS`R^ezA``|(LA>gPb)w1I35)hI-*0^B)YyY^d zvFg12ytp;BJME{iTQcmPSmr3HsIFz-wzK!Qv42>&bbhq|#>aGVsYb2W(<@J;F?bA} z@C=JR7Ut0z({OSY=H~86ymsdFEZz#$Oy#WSlJnHX^-J!Xop)h#Ne~ud#`>}O^EO9? zx~NQhdYU6ikz|Kt&sqy1<5$qRd;Np!d2hbVlf)ZT%6P10G>paj&^#ZnFC-M_%B8d4`25}-&PqjiO zr$Yp3JlbD&{G-asUO!{x=V8QPk%5z^}+t^fw=&59`V$c;{_1mypuEb{n(Fy8Kf>cH^Iu8zUqQ0qHH0&0W|Tk$YeLz z>iYK^Iq9LPXOrUTS5O#KPKm_F56jz%8&!DRj|i{w3yN}hhJz+8xKJU4cbgIKv*L#> z>~<*Qc5Pw0u7@&%onj#4X|a0uhv^xDcM z_z^$NtZe$xxe2+Y%`ESkS^e#WPX9}%B%GmxwZV=Z^a`~y#LO)p z@PNLpLDjIxUA~{1fO)I?-`Qb*{{Ll%-CaW+Rf%9~xVAjNEFZYL=k5WRg^UxpYTOTQ zD94O@=YM%Y&|m>TL@=L4BSUvdgJHxl0^%Cu{whx45&65KUD2;b9X)l-mQ|i-N^b3| zcGL}9*@qjpPHsH1>Ahp@gMd0$x&)+a=5JkEy0y6F>ap*93=O5mYRfC52WO6b)-hW< zX})5)QZicdH+rmsj6C*vJ)`hYCgnwCY)RZ5x!>n633p^z06VzAmPa(ZWu<`$XiP5) zwswyh>uW!v&8W?!DzuOVA|;i$n+~CJun-zrj~h8zVfh(Z68a4UhD}`x(JGor)XoBu z1ZAbf1l*3V&mKtf^xqfrs-#K|`7AR0P~Sr9;_P5zP4-|6?E@0JRIfB)bZ~@8UUJfE z?=G*ZZlK*gK6b8QFc%P&8gPO^Se8_slE4Mn`B7IeR}w){tt7MVdz2iXpP!Ky78EJk z6XH?>B>jA?yJzexS|L(p%eh`u21MLt@*%v*K+=h(DU+&F(ZFx_bcp7Htj%h5P4&s9 z@Md8r{WEHVa1;}uc8b(=_?{~WzrnNynqsXdK`bI2Pic-Bq45IuDJ^J@1l`2$}zg!-0Qp0smf`YeEzfvGi*o0qxYZ z-QyZ#HD>{z`UCh}OX~F|y}fc;t<{$7GjfQ*K0j8<(ovg_Z|MLtfF<+%RZ~|3Z0>X) zdxmNJ-T=A9t9;5h1Nn>A$bI>pk*H%0y^!~c)3~Bb{L(GB5Wft_-O~gE0xXiWE=Sz zUhKjf9+h3W}rw`LI(@+Io+jZWB)M&CoeG7 zFTq!QS{xk`VM{ALSDEH$G}P3!*-q7mbO#>u_KMpobxGI~>93k6nwREgPtHW$i3XK< zMUCwphj)|b=-7f|%<&c7RYg*FSb?6A?@I~^IUUw79Fi=;1}++nj%GLAeS%@Pdl{I|W{a97Z?>*Uh_HuY-ZE;C;^$|yC zeONzz2@M)m$|`}_pKlnC$0k@0uzV`WG|;H*^bZ6ahC}EC61= z_);e!`7X|teAfn!h-Cp9a!nYJd>0)D9yPWKe-3%1gMb*@iDgw14~0DIhr4h(>W3n; z6?O$HJR1uV@RiSzKU}Uj!7i`_w}G1tcObW0!%OF9&d&^wjmW1O2Iq#?;nQ>Y#aaB+ zED(_E*{kjk{epjUrh=8V9(qLQE2yVdRp1I?5+@L;&4~+8!jp`mT9P#Yoh6yA=0NfZ zJCebGf)f!hAyU3=z+zkAI>Ok2{FGQIo_@cM5O)A69B>_<^#K1g$zGHSAnw5Ca#%lf z7{uo!v_dZEpxl4^MUhP4kXT^jn#&t1>=hbysm`d69Oq=`<`yL79*El|e>JOGraMY7e#e{)=76MykGno=f^NF-r1CI`3}q0} z83VYE%RA8wqPt?MSGlMd=G*s}Vmi-MSO}PPaz;2J`Iv4*jTVwz<~+V~m}#>S(-|oB z2vv6gdSf#%5z2e1O*1GoQLT{*Ac{WqG{}*tP*t2FnO~`HQV*+X5x&IW}OLxlk5IIi#N^SyYuURxU>F|RqothmVa`-2-@`&QscECp$mjP zL|L_!u(8t^OE&~x-M8EG{jg`EL9CQjDfw65&A}^kA0E9Fg&*<<0<5aK7Lupi_qOku zT38yr-tmc{9?Tf!^t&=9{GAv-PtOAv;`^g+(<^73Jn%@@5*q~%wSY(3)n+ET7_LHRcfU)8J zt(e5H-1A63iQ38tMgy;cm?oGn;Z>=`BfYfZ$4uf)p)lz1p0VoVxpkQN#q{P3rq{;C;KGC zor*T)RArjvy)BknYgg-hG5&-T*?Vk@Nbs48D) z;mq-Nm$p+8V4S-gWu*xjxk=*0j9hs#+J@u zywV~lt_!pWb;7gmVbI zvk=_6p}U~TV5O@m>dpTmq97$dOefis<((SN1|KzsDeyb9d{ch^2@U%Zpr}Isew}UY{vq~{XF!)eceKubblXUu z(Ed_T)BcO>;l(|ivZ`EW6wd*wbm*{~uX);8imdc^V zg{>G&{U1d~9J z!J{)_+zD3X`#_T~Pge!AIq3vE3SYGvAk zNnb6HIm=C*llQPZzGVvLy2F}6>imu#v-5q=y&}FKRUI|k*r@L|-k6%d-j57r<7XA0 z`51*cQQ=@>yvu-Bp@culP@vB%-d7^b@=gzqIpgu3^{$Q9<%w!sG7~_3V98a*QFfq z2b4wwgAf{=k#aC5k4ii;T`gU3s(GkEYoPF)7AD<0)-lG5c!OAMF0L*rEh}fYcb@EZob~I8X^EU?+ej=ohu{HXQH|E@6(5(?jAoY)NLs|s(24v zo?0&Ii|9%3+ct^9)@^$0vh^&vW8j>=4(i+SjbniD!WN_B!0YUCe-Im{$y!L$+#pT6 zYjQJvK+*7?X)^r#dNgMS{Db>U|;Gu8Js3RfqpHbZ7k5rK@vY{X;jVHV-)5K78xQ-IITFWlL&{%XF0=HLe@lG`sG+ z&GboRn3xWYem;706n_f!!F@$ugY8o*uN42TAkV9AqUw1We2y4|A7kuZC#e83<5dB9 z9=a;>j;1<96W{7G+#%d^+pTBa4QGF0TVQcoZ3J(Vn49}z_Jy7v^>P!JJ!?R(bL`1&0bo2?$Ga>OAN)O`bvK3tpgX1ee(sS{Jh;jHs`e| ztMi)?Iz$C##mBfayn_e-=>C^%$WlZQN0X!Mszoj5KNQe!^Kr-hdh&#Z{7Kc7D^u06 zlJID$KlSW*;hoX6XlgfssyBg&_`Sb((hW|ep=Zg?`QJxcW=2L- zQ+XrPvt6Srq8nE{vfE<*e-{1<)Mx=RZ7Cc6Al=O2M983Q80_6<&0u34MgMGO4sBVz zj~ASQQZiW``B_I@*=WZo^5yE4WlFozZI6Gbz1Fka!=i*B`DviG>s8JUyfxw>;dc~(@g+dx{Qzxhe zxlKohJ#mQ;yoUS$t~50;n#Zr`Cpt%kCx<;64s@~PalV}kd@bU*$Ie*aZ#*+|L|;`@Sgt-g7S!f>gC%hl z9Ez!$5gH6IgfF*_u16tq8bD(ORY(w~BmDJP&UaIY_udnf>|P#y>AbPG;|tc54PM8T z+z8nmt_L4^fP?6dw*`@zIj$jo3YC{7xWdUeiAbsjkgfGqg=Wrb`Qd3InbDlfUf*f|A7SqHLi48p%-V?=()3~)sQ*SzP{Q!o%>7(*zkI3Nh-U>EZnR0gwI zy^X-peNwKOx8lEA5Oclo+#H=xA9JRihLW&}p-O#A8>S9$Ge0m} zgC5Y>DKfn%n7`dV)Bind+RpU0NiZOV4m*Of#};2?3UsPsrCQUb$SY3 zlJ~PWXWBfsIXx%CUmO#x%n!6G3Rfg5F*2$Hs7-vnO!sAxRsKvNc~|~5iz$^%V;P$k zlkSnkzl)gW_XX`pcB!Y|cCb7Io#*`mezpBF*tNP54^c;jiorDTg|`%hqy?u1W`t%x zSGu>XP^}u*Jy)K$Z1E96s4BiV)l!2U_f+CsmvA!L-j!c=?Ctm8DvwoUR|F1Cm*{$KusQZpG|iWjUq;Ef(Iw=n=_WT3 z&=Jxc#6P~7f7G5A`j$l($LB{Aw&PCQy|LfsfBo$CR~3~7g>#?yfp8OH_Gg@6wB<^b zx)chD+`{5r<-(+pm@psj4Ii#O$Uk8RY<99bnmj&zd*U8+DhB587FV~Aj+|-oHP|+> zCO^J{JFvmQaob6o-pI1!t@hr+dEcgp4j=x>o&4HU{9BRwP-O;U5swf2)=rp<lYblg z`yF=qi5XM#loA(7(K(Y14n0CrwD1bYKgM{O1&7r3RY?z5o=oP;37bNex#2oVmeJ}KaEJbMhBC=0dSvyo5q1cN zsV4MYm7xkB7m+90b553gJ#)%m4mI2sfvUbvj$q4bCYFEm%&IN#9^Aguk$=X%A_|{x z$j^NfE$1fx@%M}5=~}Y-Iv+>=3~E{=|D@JfP^?E%8#Ax^oM{qMBWL1)7R z!Z1ZLIGB>AR%xyHa;<*Yip&eft2B9oRmgC#i-DumEY8B+^oDti42c=MsaP$ zMPM)r^K~$TX1bgWh&=4hKj!$+whuIkrP0N)y;Hiz`YATYP8w$~B2l2IOd*S}2O{An zS`vkZL9w$KXtHH`<*gXbgA5)b_k%L)h~StsI2$;spt0G3bA7XYQ~lE;Gl;+h5PSC2 z{6A;%Cdr{m!p+KNSNun_;t@}a|BRSY!F|XKmS$u|vix<%6;(hGo!d6mLdb-W^7IHv zMp`I)gtm?3fmZThnV+kO-vF#zh*`#Flnu~)*a2A5aS`avDQX*V5j0c!bVLx^NPGgL zZqAkESFvRJp}hbVOArmK0Te$UYLEOT77U}=YI>M^kf}3>Or3Jd?TzrlWwdyKu@9h- zYAESrgG5d^v&H5_S<0*GWK-xQ4|4pzyJR0j(${hPp}PQTO|IkMEm|j7OyM=0Y3GT> zos8>J)6QcL9}nZv;y>H@4KHxyOUU)8BHz3Ug@K>)oxt17NxIy-@VdE$LYXjBEa(aX zP2DV!11C})k}Ff-vSjgxzEcvA}1rf zT(F$n2F>*L_@^+z#o@$rf8kqln`_6M?LoQ^-2fH`zcB53NE(K7Fgk>axpv*gYi(xw z4qXGN?0Rw6rzUrEZ_%thGLK*vw=xbW-tmbC%MwnC`1vgY8rT{^)iUOMy;~4r^GtY# z#9IPhp-xq#C{+@tp>JxwYmv+e_k`NUzX|nG19@7|wW0U;r4!$MM3&f)H{FWs3d=N= zJX8H@_dByAM$lTOXpVwhQBn+@gUY&97~+25*b(;wH~P)!*JIye zVf@QQvV6w$;taLIkb_91Ivq+ApirD40SXnt7?bXo8Jp_EhA}WADnMKdm-%ByfIv^` zM=#f=S{aCIgUlopo6R)##5gJ<6=)k7DN-jeND7y&JELxs7ZpR1HDm@mnuYlRW)egz zqteU34#{hf7br^h<_o3a*&!KN6jk(!ckXzT*UX3Lk{7Yg;NUn?)aY#GR~c8C1j~S(U?%{ob67 z;@wV)0yirXPEHECYNT0abp{Bj=vbX3>&Y&r7i+dTn=6!Zv=-kpyO7O-{DvC3zV2Zx z{zwD~pEocb;5Z=@E0#jm4r2-(hZXK3d12uw|AIMbOy3|nhZ2_#5OV^8DR``Jh^ds9 zo0D@94UrHjT7F;#QEG3FL74E#=*3CzkuQ-GWIab^rhlCy&(E&dbj2d_zUf~r+@UJ> zrqkyND)RLCmHD-bf;{a(Eq1t+)6otA+x?x6Sy!f(q-o=aMq9>=mzvzGBI{#V*Ykgm zBiqDk+Yz550xZmJU`CnU%i*Cg z@iMZ(P^FuAh-4j8ekbIrz>rA3ffz4-!c8>bBVcf40RE`b3*VSjTzei zIs+PV)4VscGbr?|XGZycVM$(R+2uNx@4$%QepGs9Frt`Y2!QH2K_1EGw9?|`lS(Z> z{+|mv$|{OFDp*_0KXfuMf%h|DI;`Y6o1~H|tDH7z1+op-;f|@IA2i})aFVWOJn)9W zREdxUfZX;9!fJvg!ItvsI&_3W2pZ;Jujjv}+yB?=@VMM|u6>f^OpwqSn~7)d%@e!1 z6~Pw4WY@w+Trd+KOp$n4m?FW9Rw4$}8};y|Z;0KYi^wULGy`OnhhJYq>7>C@iLKQ^ zM%P=?rkmPbB0PI4cpH{xB` zfRv=O-eMnDmHMbvakw&Gj?(met)xvnV=V92TeW^xLPUk87i4X@1&VSuCnkg*hq4$= zM`2ZIXMTmUSdj-Xkt8b3JITxY6~}c)50<5t$Ct#9UaIeJz5=Z9;?vxzf1kN{_!Ij; z;q;#N!;}0|>~fcx*@>pUANm?5QwP2o`T?M}BlF+S+#Z`BogVvYX2trSEMgW-e=~i= zZFd)Vni14+&L#wl)?#0eltf!jA;pZTMbHvvMcUlLF3}kFNON!}`glHK-;X~rXGhgi zaRFFLp5kpxtE>oL*D*TeO7RozX1p=o4~1kvQA=Ulg**~6wy)*4GvskoH#a0Jg1rS- zR+uaqXPLc>lR46c%1mm}BJE>JLrMeGzN|ZQ1g#OAc>-lI*aI&L93@_8ja;E$QwLa~ zk7C6Cnjkh4?r0x{GKgMoX^JpbT5kZm=<6~yoYnurdR0r;D;T@%X~tDcNqtEpJ3x6R z54yRb!BpVYf-~gV`lRLrBTM|56yKzP#1Qt6N0A41?pWLDiOpDBPu%ox-*bVr$XPtP zcmzYJGW=oZV7WVf{G_`pwcIHb3OS$$>cV`PY^GmZIyeO5T@Z|Sq0>_tWQ&;mvy^-sMC@AcDs6zbowiX>w}(=kZ_0 zu`!(*d)DMUOIDp*5o_@zsb4t8Re5Oq%N(ac02@d##55quro(KBKpzpSAiUi;)`FN^WL&+oI#TaARV9pTO{$J;VG#`8ZKS#| z{1g;s_$dKQMMqvyp$>}N(5-F)S~T^&{r=X|8=Bvtajx4F_*D_OxjL8``auz zKKtbiX`Wee`~Heo9&m#BD$flKT+w;{M>^ zz|8Rnx2l$Q-7T4BxGc?nV_hGOY3_nV8Y$Po3G}^*7%KP%u?K`wPtmLqabOrV9cQld zOga-WZS=jqe&fg2SuE(}$_He+lN_R$ z$F?wOubjvZ6rYWkA;-34uTEJ2t;lm^YuT?Q76 z745{UFa64NoBQD?p$y<6u#g*-0pTVAS;ABN(mZfjgZMwFpfx)-oa!!~y3I@%4HUH( zw-zFiUEww&+~>zz2k;-}l!YnJD&s!MYg1eYCpls1PJYD=0Xe#o2zSDFcHD*A!mqgX z;Ds%C!6I|aw1%ALWX-wFvTH1@sC1~PRH3r?-6HSYU?7tHnbR27nU$|~G<>LrIm@4l z;-7GNE$~2y*NNkmDOE7we)((ntwzFD-cf&_PhO!GQ9de(w~6}DGAJW8!&@8@tSqv( z%8!I*DvHhOY0aq>Pt=yQmRi+bD1j<~OvulH>s`s_gi_|@@>9^PO%U4&_!h_`(@TE4 zNUY|FcAi+Fa!X15tp$V>e*_zY|Jx|38x$?bN!Y6P>q#2c?0TD-sOe!V}u?6OlL{zkh&k%S5YZPjE;Txi3s&q7PP*O%u z^877=onMO>7_Ax16E=Ql{QB@`BOhan-E^M3KTA62C37p1Q&%k6Zn2oU5JeWakN7_X z9=JWtw19eZzTY3b0}}%$9<+L|S8^vE$Qd59uyPzHW6Q!rOM%YKBVS6-FXz*4E?i<;A9O&S%rMO>3=nurH}WH|}a17_Wj7Z0tIV zYTI;%zsz;QD~1{$ag(@_T3l!ZP{I292<+Ur|>p`&(ggc7- zx*g>Q)|6j2;gLMQ7{b|K3~e|$;^ZwHcWCtP*~czEX5bvm%o%w8j1(bd+92qjs%SKH z9&Zz-1f)d71leyrdSttuHUZeJ@PXFa+WJmb;wlaF5G}WzcA6%u#(o~VKIMzL>+v}f zHuJ;WiuI`$0jV>{9V}54$yyaZ=aHASH`OJPicC%5SfkMK>!3l0i_Q_kI!|!Sop+!0 z%wCgG(>3r_ks+^CRa{uQM=wlqNsI7581J7J8;=7*a4z_mx#>}|B(Z6ZEaAFk4~Mp4 zatahk6&$ZlBQFuILsd~`S*9;+&;K$iFCJQ#?Dw+564KbSNkNi##A{O(byoKTzc+r; zc%!Qm&0lSS+nCVWX_ft&5O#6p(jkcDD`;~lS{94dMT6)2T^pLUKUnW-*<|Mp=a`GxC8lcv5gfUtctBXwiJy(Od02 zD%@lHhTF?w>|!SG!nq)>BGjymq*iM33<-nY9Q5nB%UflO8k8B zzqLi6d~Xqy16l|k)r0Z=F2)<}6AbG@Wg*_8xh9T;y~&4BD+keTKnNrq_#js)i1vp08bRK!NSQO zJ-U6{VYk;#S4LBE@6RI_2Cv;@Z*D*D^y|r@s-jZ8Vb###o;_Elul0V`az9@uFHjY+ zJHj~MH6eDk`;J}nIv;qKwW#M8A${TF1RDaQGQ6|U%q=Y_Q8g6T6qV%}Ak`}qM5V*` zg(iz1d^(C`_;e(7wK#v=s4gG1LPllFdUe%MgfME4YYG7+BgPKOjBeu6>=Dqd&69@U%T`D3# zO^q7S>kE4-*xW=}bc_fsDnBS@!!0k2n-Wi604L`W8nubus~nAn0$G)1TYO&5oq(e1 zf)W_v*7foh=huG8LgqjT)ien*3(0jj5j@2-50y7m4;`%%MjVUv@rFX_Dda`oDI_a( zH#9}^e3=p@vCwsjf7+c-4dLT+pNo#pm3df|#HvyXL+{J}q537C<&P~s$>k-Qs+5*mO7n9S(!5(+M!m@+C-5g_;(uK9 zw&RG$E4&9*AS|u~LIOM|apv--n&Vlw@M3+{;1d$)Rr3OhYZ5*u7uUi}7FA}^B< zt5V;Iw6{BDNGQlGi=MhzHD5vmJxWDxgA)0&A$a=WhxS6X2utwx(od{e8Qw1PFhFMrvfz%_jt@{5hAQ!uiQ_s3|) zhAAw^m_lc*5oQFWM)xCNdz zMjC>9S&Q@aL_{PUzf=$}&nU>WOe-mooELZ1<(HOPX-ZWF#e9S+O%Yqn+T=u?&9ux$ z2y!B&gG()CY3W#u<8$O{tG@{hpxBgJN)l+!_p3}O#D}p~II*WBh@3J5lOipXW1?b1 zQ_GVJ&#Ge$fO%iYx4*#~;yfzxu)T6i)N5nrNl5aG2=;FQkDAbl?Z@5L=wL zbtA8V?Ll!AIi57GWif#9K{Tz0l`>cs=_{IT;K++G$-T%B2eS45`+N;vUjw@8lPq>1$~ikSQVkgtq_eSMam-auoi4SKJNa|yB;|C~=HwDo=!lC9y2Z1#!sAFU&=qHLrs zJ2qQXS*^sWYq;*p@~Eqmisfy*#`;P&cy8Q!~ zMTq{jM`^h1Q^CeJ=wdvfX%CKHSf%PBJCj%*5=Mm^t4DaleY8-rq2@PQ5~SP#4#_p9 zR3{_IuO*eb^MV3FKVlmGK6FQc6?G*Wg=y&NV)DLQEfb(7t*92UWrvA6(Y*t*dlH5N zqX3Wp+VNM9#4}@ZEx{Qo>46-u{=w+cGGwy&LwiVUhTC1z+R3k>g8$FM6=y-!1kK#( zxp^nUP!=fCWI(+s_~O7Y`A$$68l)jVmRL_LG8t?$a)uJ-8P{IV{SK|g(Jf@y49OMI zK${{oSO+TN##>lVJ7g~xDf5qnE87fAXd(r)Owf7h)@Lox(RB1;O!Zl~s(iVxahW*H z)v#aujA#Pz{C@7yS;Cbvao5atUd~?`+HyFZZ4DI*RrDPb2l7eV#^TR^Hz-Gow9Sxz1SbF zx;R()W)?S9K^*PKJHU3=vNeXRSIsxFA97PEvIGADRcLkPZ z8d%qbI=*@1{RlgsHVSFvnf5Nv%h8O(qTk%JXUD_6B z*zHk)c-=)kXdLyIKkCtiS?Z~GwiutBn2?*U46iN5mA~|@+jfZnJvuE67>Vwo9chQN z3XxHt(NR*&8&3F*d|OqKj>HfW>C}oYb+2t}DNOT_MX=!Qq&V}NYV>MBni86kii8_} zrPxdee?#F=1MzVQRa%g{V= z@lHWjY0Xq5(zU^>mO$BvE#9LuCT^XEc{xG8PqXo+)fGOsVCXPynjw_$zZop0DlKBq zIU^M%_kO0i%qexaXaS)<|-}}Dm)ofU9se~@z zhTRp{+{Mw|_f)bEU@;M}=~Ek|dxzuCiUgKOugb_<^*Xt8MuVLUwRq241NB$@Lh$No zYw$>o2z6g4lT`FrK)F~K?Xj)%UMIuf^G08bQj5A!FlUp&u1;=BW-VQKIn`C|tLQPQ zX0+D+%s?gVdMYMDxr{k^SJ~b1P&HmgXCD^BftovCW@LF0P#A8IToY2#ko)Y{-Qe>u zIEWb~*u5d_5O?z%qF>HW&mtZsDjVaK*O^X|loUac9md=`UJ!S+R&+MZ=Is`&Zxl`U zWF90t2SCkEKvT+!P`TBuQbO*2A=M}8-w}U-F?VsrACAZw_!<>`p%`6g6=4;LI6BD}=rH)_*7n`<0f>Fch&@LK|iMpseL$ESd`)d}8 zxU0e(cLx@z1qG2(7G~d1fE}2bvEn8J9t*HY86KvarrGq(c1!X5O%)`pLBACVn8xAp z_&GeBujdqQNkbk8b9(azJR>)L>D^3A(0=ln;S9uDbsK)p`)eNKL>%8MaibZp}|D;N^unL<*bM%$2M(6zRV?>N(Awz zNJV)(5gY=H^M2q^$x7U$R!9tD{&C$_+*SeOC=D$+a6tK6lbuW3?R1s$>s#bNUgIn& z041W_l9F<6v8L{1!(*q7K+K;At3*%JGLRNACjGlhsZt-q{)R3{zr4qKgBKgPTMhqp zUcC0(i#8MtQM21lf;yz*B)zO(;pHHNs-IR_*qODT9+#bh?1%_k;h1h3mK*i*6 zaG}mg=uL88VNpTTI$w?)(Go+8_cNT73Qp|D#M4fR3eAK~J*B5Y&55jfASOneM%zq^ zx-!=-uFMLNzI7U^+)|exk8MM(r=^%-5-1zn$3Cm^oc5j>Ijtt|jby6O+$x>cCAWRu zFHIw_t$T#D8^6TLFT)b|Cj1?BywY&0AM_ETT8EY6h5Aq1Wv?yqykiBEqgRrgFMmA+}a zia?bEhu_@2>i&SvR^7j5$&~X?zyHRWkB>R!qYg8hIf`J1@6oMsfReaV6o{XS0iw1Q zRAbQ+&LK&Uk}&OINHhlFIFF24)Ngp*L$8rU@!p}*+8}i{+JH~RV_6U-&=a^#879!- zPB`t;xT7jx#z_-f8z7%SUmlLG{H?aT5NGk-zi&eG?CzTLzoVUx!2>BC{TOjcJKlZw zO%S{E6&ObrNMGhjxfojy-aO~{B*#yj$$;4756<;;d1)h;FV(^k{aMhZoadyY?wgo8Y0BGK0>=xy23nr6N?mu{V`o1 zic5u!6x2*^4>#ZWiq5O(eTAUp(3QfGXd@yxF_XC@4NPnevJs1-0e>r+ZF#pR1_+mR z(=u10$?zFCcFs)_h0nw8GBCjQ&DZn(l%u}WrHvw1b)=^K^$|1Yb%W@2AzL1j2A*__ z040;U9-Y|OAB^A1_Bg&WT!Y@pD<3QW&i%Rnytw)tfi9&^`{_K$DyVn3B}x)YtvsD! z>Dga!mCmxKoUMN8XFT;q;>sn;fVwHI9Fy>s`cG6D+HqhOQr4LJ906Z)meimt5ucL z&L3lr9K5_NvGr+m|50$S29sAynHDIIXj`4LOuxjhv4>5+*-0Zj2G4(2L(>nD z0yl)(m{FxZZq!%Xs>rk_mX3=T@J{f}4+$mK)kNd9`nf#E*)ZRv0Qa1nS5`U_6KLC!uh%tFe@yhN2pE|;Q=%r>KzZ*O6FHp|93QB$1a zxH0VAmUXn%YiatGcFC7%Z^UL5obLCix#tmhuRqpSx5qCT^(Eq9B8|!^=zlDM2{^fO zK=lIDWT;ys4B~648sX+T zd{fNFa(@3(1)Ke}(D+gWUc~ysFa^ilF-B?T3=cKu*Oz6rVSsQ5^Lt~sn`z2oTNodW z2$qk25Ax@WyIZH3rWAS`(!;;etnC#MtPN6ex|Tf!AP>?_dqZE-p5<%ZR#NFWDRJXQ-p3=#ZFuIHq{Az5AR z>;&$Pk9?7ejdkwIpj(t+$zx7JBQ-ruHC->nrS^TIPITacRC|3jxB_}{0D~M&{G<9JI`1xH^s{N9!^>u~yISo_UbbSDo0w)?oyhFskLtBpA zC{+Q%hc#O5`sGgx;4$zY0$yj}JJH=9SvLc|7{chz0e+wlb(#9(QkzboznM-XK1b4H zt9?LejaC8otUu3*K2_!RNFyd5O<(xF3cj4sgD_GqoGOiyb>G9!4{_+# z#Sp^~@yo)79nMuh$LfsWIUU6HU9punPGwjy>qv;#I-cDq>Z?3YelC69f~%#y*L}`% zz_rJJN?TdEV)5@2YmOB+zegEG#}Qbx;gSsB_3igB?Fcs|HCyvv#M4X{z9?fJAw2mlJhJKN#NlCa3g#LqyX<1CvL~|PYXqu{C`OG#EO+cJn9vY&5f6!u@|g{h|SK`4QO}z2izh+!0!At zkP%s#@o%lLcp0r>iiYBV?sGetE{dpRBdu624c~A`{XB`N!R8%$GUXyM5c@MQg4GsP z$xDRiqWQjdl;GmF`h|l0FcA|58m=@wM()6t?F& zPR-KcPs}D@(hzpKtAp~eh;Qat~pi~WN2^J~kNSgBYzMb}O7;{Uj zT-T;wR1~u;LPm{A!7;vYaDNJL{PX2?vG_xDFq%2rk+~#6k)M{g5Yq8;5SdPNu2_LPLEv=1@QB%Ev>(1a zk)MtuXk-}N`n@9gAH2uXvUz72|LAU6apb; z2u`r5X{DD_gf-TTY0lhN3`_2L*9YIKly#}F|42BpCB(e>{f1Uy19sYOXuok#poK`e zn1oVA+)9rG(_wC&=9JEPBZ=~3JoQE@m1=4R>6jA`MlT4D5p=l78Yek58I{otUM!Qd z&rWaVCztyXqTLPE@^!0w$59M5psZc;$=?ZvJhF7$;zDTY=7y6QpOcJCk09(kJOpzI zucd85Qdq0TtCUf)tZA75r0*4SVe&dL3%sH>5gsO3Z18)h;}ir|h61Ow(d8kys~>%h z`4CKw4D&F%HDru>AVRdKuB9c@5eNgII3;{UJ&t0Wn&7K;kkv);SGOk#kHkXmod5t1 zk8~r?r1+l1)XuBdnn+J;Cmzmx|GGQ^Q1?KN`TktCGjT#V${KMrs*iQXZfA)t;vp_c zn*BgP+@t_F-@I{M=-h%6ur*f)~3L8NP>2QUSXUvc7(T= zRwnc<+oUhQu)XKL3_ru4Q5!8lpZ>8Qwwu=LV&zz7T60aBY)YDQ^g+TXo2f8Z%P31* zFb0OlXpq#ZZ>OW!vT|>8x_<4le)M-;k#b0%LIB6LU*rdatfd`>cTnIwM z!X_2s;7mFX5cM6f`Oy)J`IBsTV0@oKYK`>;`Zyc+SoZ9i!}o`ojO+&6^63hZ`M21c zM=$WFt#x{2ViZZ&RSt{ZrQr)LFoxcUc(vTGrBwroKgkUsx=A5^rNC%+StlBtJeY*W z;m^yt{-3G5y0Lz;wQD7ifzJ8&mk6dVSfG#kD`B8rQPukqB5&?C{B>)^!~_nx8pJKH zgDLV|hNG1ASGJgMliufV#Em>}tKxp)ES3B77|TrK&c`E)>7VAKk1u2<$7D%3XBWiD z0qA&h7Ae4LV*oGCw|*3+_Fj!WdmYwN#2vgjKq+C53Q{vwBXplK@anJ;9MXeHyHpHV zh1dfVg8U<0S%$In%>iW&ZF4rqM>xI7-i9&D0ey-3m=h2Bg=APuMD69Uq!MCYch7r$ z0-U?VOjU&v+K9%4(BYsvsW1xDVEqi-C z{L&0zIgMXMUD>Oc1fB__a-w;jMD?%&cj!_wO!0t59>X76Lh+d2>tQDjC8FwIx`A)L zHoEvUN^GTT&r?GC2K}_{7Q_A%Mc4AfS9e>y$mBNd>KpoUOEo5O(JVG}d>Xf>4RTmt zzPBEX0qE`*TqWB-B@q0qF-beF^GdWJ#M3DlevaxHDl7`ypG#d(&|rVa2-&`XFExB0pmrE3 zUPl6-q3xPNW2a&h%f{MCK1i`whb1wIKQo?LsD6A#k1)B3RA=2INuZ+0m66-*Oa46j8z3N`3K#p2q7WNk8G{OjXt%bYc|*s^lAO&IGK>lihl zgEln+Uvy{|=>TVF-n3VX0i(oIB>;J0#55=Qx+r4<`Hqq)Cl+O#CSV80RO!MvHds3h z56bb;X;BP~97Bx#-m;9w3y1!XHg&gg(al`4O9HD74#lL2bC#3yERWsbbpW~Lu8b~C zKIo8?P*elQv9xq&%usbNu!g^_RK{D)mCrlW`WTj=@O%bjbi|(N9dmQQ+xPCbFsWD8{$Dsen8!pY#= zNvAtUl_SDo4VVHqsQb1sGVvRE)nCqPuvh)tdLjvpSgl4T8-1MVFK1)(t+nN1K9`z) zt_$m_&~F7g^6Lwv7^W)X`mgy%v7tFZRz%o*;0`fdG8jvM@Zo~AMBV3$=C5z1hBC?YVdPhK?oVR@l7Dos#?c0W^o-=bsj>%oRJw$ zKSj+Vlfu=YAdWBR>)*VCppK)_+z}=lNyOu%li{`SFOS>&2)uqtXA9cZ2kvRcs;;AW zo8QJB;cn1>I62^Ug=Oocm{vyyJj{MWc-Q>iN{!&=WPxb;dEnpytRJ%9g#j8I}+nm9cPb7Zt{QI7l3&?mKLdg*4 zNY5zCK`mcI#MGAV50d~RbZl=Js0|;YM>}5qEIArQYUt*hHcxW* zC!+>YbE%$H7AmrQa#5NyIUNuCV*B=nmX2ol(cA1zz)??^q_LN!562tdd<1HW=Pe;d zDC)L#UO7Tq)1S9-IB=xp!`4{nePv~4UnP$rUuD{_lHj@Y_uoMynPf39cQ^qC42~zr zA#3kn?3N|9Ot^HTOHy*!fG2*U*L||J-w)RUl_Q{|lsYNl;p>@`3u+@1| z>kwBrx5f(8GU)`_qgvK;V69*r4XpHo2(9CxO#X^Kyq z(I$rrbb8}T2|a0z)LY%ErviWy{28(_i#n!m7i}&7Q99mAXv(0e#PH6=Jm6euts3#QGUNKP4JL_PNIVPUbMERv>DU&N#@41Zwq)8Q;8fZ zv#f7#FhUt=Ak1soU;a>)wsv6j1r1u$;zk6whM6PAhHojN;ybdD2ww3PREutJYC}lD zk^37ia{CB{uvCQ6vdHZX^JTb+>jSL7sGWqyKL3PVY1(!T|HQBV8WWIv_!YUl#oYaf z|Ae7`n+-%B3iy6n)9%KcS{NXn;`+tQ&9wp7$eU2sYR`{zM--PsTS>iS&^LI`!)n(N zNPn@v-*2oGjoXF8#Uh@Xmra}rsGa}q<0W>LKA6umPmMCv_{;ldCTsxA7}6&u!8%^E zBvpv*52YC*qk?$xP=u1xJQ;LqIkx-=t_W2Fly0cq&31-L7+AN0e}KKnBjgDxjIwm$ zCm$cz_Ph6bmD-j3#i9bP)j0e8E32oTIg4y6UM}o|bu3^Umk&|Tuz}~O^ z08scV{W~}~uU_op%mN;qQMRU91XY!ettskHn5>$-1X9mvTe!YSgY9YY9>Rg>J!rp` zS&Ue)qH;p0V`PCH40U7PpKhDeaZ3I1G-X5EmvTfx{oZ{eCbO7IIj`+VWdmVxgkg5i zLJ^+)9pZ{Y-%cHGk&wyG*P$a=+S*rRP|O|lOb7CB1r6>=Dh#1r%c9xsN4VyxZ1XFs z7OFFX?dc0?>V;oU7sZz>F-^)2@-mq}h9bz^glDs;5Q5C_zrTcbo&-{K{yN2R1N@9aeHd&V{aZUtzkNj}e-_?you~j&9aIrN84*`3 zI%68$CaIM)%3DK|vk!Vv-Br*vlYlk}`W_H&SJHKnG>2v6k)te3L^6x7sDLOnt>fVhN;{W>c8?keie9P z5M8OnOCC0It(?`eH)j;t99d9+xiv&oGnpg)<`iFh?YMIDc;eUn*7o)sspR=k=Rrny z50KKy^r1FMGY<=!ZOde%3)z3))o_Z~0h+55{%fY4Sw1Xj6kz~C!6ypV=@kpwxN=5{ zFD-6j=FJ009RTJUL+o>yUJRmq_APpZ*t{V>$abOq3s!P^lD#`MY7+o!M?!IU@cv<0 zhkX$A7oDhtPWbYd9mDw^`<*p7u$eDfC?2#yZ#!`@Payf&j#3!-D?E`8Rf0It_p8dz{nrtl%IUKB0ejnc+S_y*+0}(3FCwZ;d*~oLFb=t9<(l(MEnP0$LhS7=`&=Ev{vlZ0oZ2gtWuHD@O!`yqvk z#$x;E`|iUzA2x}+JbsR+x%*GU&^(QOUjj5O60|N~Os>chvHs9w*@Ee3(}kq*O98;Q zbH2$kQz-uDY#P|sMK#~@VKIk@P$+eE`+Y($@Qt+L3Athw$qhOw)<_&Dhh;`IzKVee zx*hGD-Rg=kxzPO*l$9ooWN}U4;Xt;{g3&qEGoWo@6O5I2_4t+GLeDK z9=rTriRozF0ozYOJ8@?BQ0QJ>V((^C?*;l=ZlWMvBWc+F9SM@*7n0gXET+i4I&5t-{|XtZrLLJY!1pz?Akp){maH8-E^td;dw5)>zC~sL(M~?Z%#9XHqAr{B4|J zQiJ(s2QK#N>FYGoPk#pl4@-Nt6}?5#ox*{@n5byF){)e#O#diO)Txqzn`v5Ozm7OT|DU|0Suz1&c@&PhITIh%yI^3b8vm*De1y6 z2pc+^{s&1?Qc6-zm`2jl$UihA9<*LEg#4)ajpuD1R6_7}_y?o0MH0 z9BfQ&ztaQ!6CZ#<9l*)>Pd?v?ssor985v>zW#^x)oh%((?41Cd|L~{uKa%-(!avDq z19bm8!GE3p?*#t`%)hX5hPL1I|3Bzr5VtWjcLuQjr#`~M_8u_WbgYa_06JDy4geDi z^Y?|BlS%ixJ}!nfmc~MM<~F9^u`vk!Qy~`s=XYTLRW26*9TW5SNW{=V($v!2!sUBy zeB&yA^QHC;!UbTEF*W?R)@+R5ZW{k_0p|23bRo ze}^zk99-YC{;8L^rHv^IfRp+Er05^Rf72mjYG?k9!O8Nk)Bj#}|Fh&G-*A>TE~ZZ3 zuQrA*rlO|bYti%{(*J9TdjN-cL%N`4Tc7Y>{H-^eKi#j|^k9d$aR?_Z zz5Pwc*T&p}QyLo|)HB&_w;GHmpECOjK7{+8K_~>eP9W37Qk$33sqTW$negtGm&~TZ zRgG;zi{ZFrYclzIr`8aj5B+Q#Yo!fxc|vIj?=FI;5n?h}4NH&HM^VLn?gqimAdQP< zY7pKhB5Rt4hDO*XOD;R~@hPtn*Y+sx_osDfkjO>DLV_bHe~r#4c_ z;f0uNCU)cV#pzN0U3+w*py^ z>tAj|n62^Gdo$|Si7zzmz1Uw_g4GlH!1vRaxmTZD;sx7XV3?s@4R7Y+G+Y{yKcz+X zFV1_}b_{&cbx0R&!JQ1GGvKjm=;F;A9f2{Y?;}b|ltmg>?mfuY!>k23F;~{#FIeJq zo+M|;S_FePzPC%!y`?<*?d!(w2Gzce5rSofUucrw?p6~CACrGPgT8rRyce(H_5ZTU z-5-bYMhfNzqB%+!_AftW)}z45wIJ*=5s+^~eNFHG5?eTQ$qd*wP(Q-$jLD1`A|n%! zGnSXi!n`GssL|Z1w<9pD2HDp7VxuHZVY>Y(^9N=rwU6pzKKyDuwoU){Wo`$k&TH$X ztkkcWrh8dv9x-odA*~-y%;?CDmm|u z0s+(Bnmzy>#MEBL9q+!a4&a@opIrgApejFkq!{V298h{v;pm>D$>gQe%oQC7vh-%* zY+GgwM^bEBAbD{E@gEEU7K;$uwgN8>>d(S7z@&9DIpDJ|Z#_mIDUt1+U|8NSw~zs~ z_F;M(TN6wkwhUT7=(HRFIC6VavWeq`7JYh9;+e<(4eXgcl(-H)*KkzR@r21W{VZ;z zf;4iWEO?n1GU$DZ51!MF5_Q9nJ@(4)KHad|*fpt`K;f0Nu3cG*X2)scho|pJ2H&Us zhB#7=@2so?`SNEh)16M6d+?*TNhmj_W#aw`k21O{Q9j|6J0EHWI&{O%k$htoK3@V2*&9O2dunT!B61{d7- z&?mY^Z+$yQFpe^SqWWOfoC?+t&LVhcd+r*OWH? z5)?a zJ8HVsl;FE1O_=KTMou?dp)Kl|!=qEYi)g3_nFVz*8_L`wEfK-DTQ34 zd{E08mjta^XGw1}!d}&KK8Wm+CEX?y=vd8jxxGWct>)bYuW*?H4es4O$MM^1s0GO( z_&5lECUWgaQH|kfXwi(F&s={Jg1@W;Z&5acX0VUB)?J+iB#`xj>d<4w9vNnx=t;a)c@O5}ech zrvCM|E$_S@BRlx98thBi69A*3i8us02hjlk8V|Jo#DrG?X9LLzyOttYf{LpJH4@k^ z#T_b_6ppgTkdDi*Ha+2h9el;tndQeJH!i2}`t~jiCx#!vNkb-yG9{Z+Yk`N)EX{@T zBvaoOqDH1|t|lnmGl&eN8oNR83;yVV$#{xNj*eWK<6(;b$PY?SZ4}o|8jvp|6J=|Gg=HNW?#;`6|K)`&=!34FI{u%$N*$}k2Vq(FP<53QV(7JpVYchTaF(?#Jo0W9*TVyY< zUWI--51l$hNq6QtQzs`Iv76dVst@5hm4R2(ypVP-5xkNW;%; zYY@TG!V(^HWmhu@D}#{jo(%(_^e03oeE&2QrZ@w{#sjp?NDn$%t0$zXWlmwrMfpg$ zqC6`Vr?m&;!xZE~N=Sdguf~5SVgjjT9E6H8erP=DM8(U;kIE~@NP0tLRzB=g9aE(s zmG{{dUK%A44Xf20-lS3&qW16u`PYKh*YUDF-}4I@XPFsSe~ds+^=|XF&7I4h?A;X% z^$*sTKuz`DaBX@1&zo23Jg=UKAaYf`uFx7nUOvw3b6R9Qq-OBPIDLHGV{M##XIy!6 z(mCPr`ISMkSr;4*^+JVFY}c*l_uG!!jkykH@r0iQgFT7jq#n1ARX=0LkA%w`zr2#Y z1q8kv-k~|mjOc!fIdE-2vCXfE{4l=HF}T&dUL}m%;sa&(jgt$62ah@J>mUYk*p4qy z-edm?Y@Ds|vM;n=q)*lxhPxz-qdocg2oWC0;H|-LKzwzIU?7%0kN(?5qR;6Q#GiA> zc6aTN#!sgMH`e70W{z~}3N$y{*WvRqm#}AKeP>v$M#@<#BTYR}d`;j)UK~F>BxdJ>J+X$G69UyFsY5DDJ$>6@}ogYjl%A-Lvu1T{e&e^Wyd)PLtsric)6jE#NQ5W%k$MMQhlmK`^q+2s_onszqT&$rpGRI7tOQ zB*afh*wKk=PA@WW?^;zy88T?L1pypybcY%y`3Lf%hlZ|P_=c81`eYo=R?Qz; zX$Yy6&o#$eIWz`nu=LNUQ=JIimn1z1@6jESs^fwB#?<-5W7!{w5m$vFn#g$G@y35+ zMK`f6`xT|#xwYQ(H)Ypg$SYg+*f+L2Bpl|i;w@_L-6N@#m*Ac#jP}wu!A>3GWGImx zdhU98>&E+$6uS3+u+W@G*9<}uG2Eaq-v;Lmwwo(cmM=DM)h}UB-sl^UHt@i8ar0hY z>HX}5aw-fTMcD0QaCG&>zJoQzFw!=}-hgxOSqs_Z*Q(tD*MXp|A$Z;b?nXus z=BCJ;DxP|H%H=tm>vk>AJj3{PHX}n(Jj)AH(JIQ0`$D7xX$X~ziPu+cYsHL{$@zFh zd>zB~40lcv)%dLxvO4SB?QbC=EAgU7Mz zZEZC-?ve0{_r~Mz--{1i{SI?Fk3A=go5iK1%I+NS?G*QNC)lwWb}8(I1nT_v02bV( zD56%3J-nPS$rFMZ+= zTd^_1?Qrh8Msjv}*#!PqerNaNKt62At9?WFqty>OmRBOdRI2Uqx=N<{TchMIJd zXtkK0DXD-AcMu+HMqnR9jGqiPc-si%S)?35C~2oBzs#Cmn71a9Y3}hdI0GMxLZ^&@ zYomn+RADln0gp52yx|y9Vt9gjy))a!2k|Q%^Rz?ffgd|*2g+&fnf}`MK>5As%gs+c zv)xDpUn5ewRf(44;Pq#{$6;H#Ao>D+2;!TN+PcV8(C3GA=$vmhAJ9hiYCQ~(Q_um0 zFR=AcZ>rUkMP!PJ@1rIe|Ug$DRX z7zGMqPWfzA%m*x>$#6!LnLB(uVAP{@C85y&RMj_k*h0sdFl)g|I^!imrTR6At4I95aQX0q( z&L>l)hu(@M4yT<6{+hE@b2YmBP2>yQ_R8=;pH117NBr9esv9h%UQoKAtLHv;)}<}f z*)+A%`g^g+a=!I*9N+z|HyDIu4F?=-0<6~rZa_Kf85UZcA&o-u z#(#HHmd6)JqpXsj2ep8D(b%NkHRQPGLe+fG>El5->dP6#=YU3cLQ}1zpt6@l>siuRCnL$CiPd$^oV`>v~3hwVyjt0(IE zG51EdhNrV%P^;uGp9)RBGvlyv0Ux=9x%^(HzIXGBX$;$)zK$13G5h`q>DiE-BcAa+ zB1r8>&MC7p!eF?cF<~=Q7Bn;uWHN-{euG{=EJ|}|Du1(0+FWz!4XaSna4FTMBjoR# zNaP_~U!_X@YB2z#hlWe`RQ{3~=KYCIovbdgQ}$>k#sLoJ0Lx`LB!TDQ^WrwAVl$?L zvNjF$*k7Ihp#ooadb1By;qK5t z0jQ}!(9{-KKab|1=2nTpdvmSsIcOKy_Vd1*DH7qRxbg$D*4Oohck^p-gu}wp0ar_u zMQ`{W`39CYFfc2GmK8}wovu`z2xK#%A|)-|is)!*1A;FXVrbgfI*79$&8!x5+?)I? zX#|}*6R(85$*MguCvi}0SrEZU%NwCMp)G>SN41og~?fj9ogsHNU9 zMQ@r_sk-KXkw`!V4|`gAJzay2GIkLnaJJIiyUp)x+i~`;Sg_J?;k3XfkXZ;_c;;2u zlM7t@>_@I6r1_q&OiWn>3A*U)CE7+?>3k9N zFP1xi=^0M9?B4G7a=mlU^kZ4xxpCuaP?_tY^6aF7`jAgB4w{ z4r8d^GGLCtW@A3ivg2Ar0m8_1XvRI1q%sSr7MLcG<4Ik{-bO^TFew3V@KqI+YFMQt zSQnyqCj0{gv?`o<5$6u7ve^X*r$Y!fk`Li`Xm$fJbc^K;qgER;4bdr$s1goK4r!_uiZElQPS}zbVmqS4 z;x{lNpNQkTC%crE&|0c23uRKEn(=!=;2dq2)5y{}{^)3~?{dw%ad7nhG&X9~lnFwc z=ruM#mFYcR3qo@lq+NbEv9iy(MgZ%mhlmm=2rvm@#?atx$qWWoZ1IOHjNKE4Q(rzw zS1ZHWZMiUbr9b(jkZ_xAe;q|B< zvd9f=?R<`h>s$%dlQ79%AU4Euhj7VZ<~9IubOuFlVAOrR)3*e zbokOb7y^~YCnDp-KeqWZ<;dLRI#lv&GLze=9MyUNv)Vn!u2W4xbG2-2(=CK7CQbs` zz=A*ZXOD8AP>Q|*<=M~Nv^r+r;IkkO4>}jEc1!alg-jJfmqVqm!@%PK!cUBJWZXf$ z+zz>#Y}EJts*C!p&ft71>w)5H6vbAK=gW~{T=lJGd}>SDR+WPcRQ*1di&GcAb9o)t zHf$hq2&*_WHQcq`CtFl``xXhCR{TeZl^SX6n7VLd!WR%)55G6E9({o0F`LH2?!oXl zrJXN#(102v-!=ll;O1Y8V81~4R8@&Hg!*314)ju4@*--ng#6e}-Qmb~eCJ>8ZftR? z6UrA1P)|t%^Oti~sFf6cetHGdQ*_kX&cWzssTp0SO2S2=*&0g@Eyc~BMtT|46wL-+ zG}hhK4q@Bkdidin!-e#XyT0qg6r$41h5gVkfj^z7&{M`JgLUpmc4Yn+W$yqTS<|k6 zPc#$T*2K1L+qUhAZQHgzv2AN&TQl+G>pXa$_nhzjpR>-|>DqT~RaNi3dZmATUsnlu zGF~DT%iXpj*%y^5&kFf5R2Jxvl(SSuzwH>3OD74r!efyPr9dMUw^^r3y%fZc}IOO9Wd z8go&Z>eB5K5P#`8**^nSgP4xPU&V>+DHKtcq{MRD>x{%Lf-#RA@%JvT%Ow-Rm{f*L z1!ZM{?tL@M3!Da8LT59Eir)cFja;qqf*{P%@#c8gDGJf1UFO32lI#td`Jgu?`a@JS z22yh4`J(IlC!_e22CwGHJ8(((^R1L6bR}#}Z124MxpG{yFjYkOKST3AU*^^!dBu1^L2a( zhO;I!keFk!EQZ}X0q$d1bXZ}{8L^*PP}J7hhTv=%Yzae#cInmYeZ|m}r+A{5XgMjc z_l55=?GR70c`c(I-HfsJI4J;UAI;B4VFwTw^Y@8#$q%!SHk%Wml9 zFTZJnc;7CHS_mKbP0Q$VJN{O|K(vR14Evo{Sl%V**C)@sv_zG_1Z#?=lR6RQ&oKd} zk5q_>^LX^>gz#=OeBXDvuF^w&{nPqH?@woU;kM`9@Q%J$f4r$$e^kOzhq zq&T-2S}*uy3@|CAu2@UX(QsL7Uw^;`5;gq2&i#Xht!Cx>h@na-eJ!2Jnd^5IUC*v4 zw-t%8`G$S-g#;+=tP;FTZI_9I80$bTe*g5~p?5<`TzXp<;SKI_AuNv-e3UCAXo^I; z0YVO6%y@ufr~Hszk|)?!PLxAPOe87)jv>=y0+VfUdoT-;!R?#m_9HM`SL5e#UZBpe zejBW=8M^U;ynCExI;b3^hILc{m6uFdS%-KxsC(CAfJsQW2nhMEMYq9G^{JcQAGRgk z1c(=V2TPsdmKMnxM(mFFp$Mr-yD`Trj1tqMGaZa|yipn}OpSv`+J&#~vrfAl4sL^I zb^|HiC0So=ow*71U`9w7K@?vNwX_Jj4nh_)%HP7D26dz-7mJ5<1lrWHQ6___TO+8b zD8Y-Us4CQ#md07|dS80QzIPNd)<~o&%0e!>3$6gb_ z^k?c*s5wgU;FSHr)0NMCeUY9&6pre3mWiZ;Bg1Oz2NBXMi&?9nUO}dcj4(&$2QBW5 zNyU+@kJxHz7XpFk4V`Z2h6sg32G1pc)vX}k!$dbAM~dr7Cr?rwS2#3hw7lQA?{cI~ z;iV`!(FbA#a3tbeDbp}HHHE8gu9`@mf7+;W3nL66Hw@otef3JJ7;jd-tlH}GO7ppn z)DG;I`ZSp%MxTkxJ#88NP!SkmuSdUQXKFTRj)-pu^L>R&vnMOG+HuDDb2Ix6ekXfU zEgBG|-O*m_aE}RdEzo5TY#>O+LC?@lQNQ$^i6cG4kJ&k9I%V+TetPu2AARsFPQ!yv z9aNut;~|j=d-&bHIJ&{k&tzFo*e&2jkQX4w`B|Zd2lrzI`@bEM(j@aQj@)-z!H2bw zN0M3f)+O^4^sU&DCkAlleyS<2mlF@Ut4C~8nEs55N@GnRVn|mM8U|t7KS+lSevK=f zG{z>Cl8-!#YxOPmZTGa|t8MR!>R!)pxSsdHj-!$Ei$+kCIgOfc)x zchti^#L3E%STYTh^W`@x1J3Y${M$@J{&v`xZ-RYsh1lfj5LU}&t)|!o;R4r+%8I>C z3P}_Ra2^ ze&t zeN1#Qm*$wiUHakmjW!2tiG+>;jp{2lOVb?KtpoXvUSNe1_*c)kS~9!3tGAU3ai6jX zfYG!9unnFW=W+2BDJVWTjkalCT)TOyH@02Za66ir>wdNvMS4>$D^) z`n_iIhOLVBN!8c~ibN#0oUxcYj?CpGZ+4N6$6c-8Z<4s8+OkRTk-`dw-| z2vevGhC{@c;4kGAa}#s@9dCf)=RR`t2JyLT0PWnS|BR~dPmjz!>@Pcdo!^tCH1U2F ztSj8qkjuj2pTJx7MK_KVl99&rG~@@~^Mr@|WCg>KSnz0y*4^2}RYA1`$Awc*<TyYB|vrsMhWTzu9?i7|5Jvt4+^zO3NSI z6#G_F9{eckHm5nzo&rJ#DmJJuQrBb<2s^F>NsB|~O&eVuUIaCXSm+9@tPW(6W6fZ) z{;Fjy`iqZerO-?n9l0owX^AkX*BX63Q0Wm3lq~|K$2rMPYIr|M^5`maf4zrRjM1I@ zw9R>Uh&E908IIJ}mO}u?k_(IsXIV8R9d9Z27YIYkfRIHpv7&(SP+~HPgQ2s1@Z%6s z%6hdcPz9oqnHsX|Q3%Mx3$CwnO0=%@+x)|3>(Um7lZ_SudkQezi>8)FMzdZh-N_$s;9%{CTP+{arY6dmC1F zK1)|+u~zCx`A}QZ375KzncF-!;sg?dz7Zr1ddfI%O%G`{+!<}uSfjc!k%l=Rj!b%D zYG&Zn2_U&P49a0?W0@eGptVt?0wVF(LiCLK%2pEQ#?N0Bwzx|a*uR~bXPYHfaxiTF z<&LKIb$lx`Ss&JfyeIfpjO`NEgwGVFhyhx0kU^a5*PM21OE)ms=}Buj!l>ZFHbo^8 zeWJ;wWuTYylw?5Ii_*$0cNCR_I3p1P%`~KUzQv38O zj3H#XL+1foe#Ogt&r6tY`3r5}+u+Y>U`OJu;2Q(aC^)AguueS-y^ucp!|#|8wy3Zf zQlQnBR?67LpK)saRMb@{(cs-wd~?7ONqk9a5zhqV7MU`?5fU1al~;o*YzH|zj}ey+ ze73iF=Sw4HzNb9IEtLa{TS^Xuisdd8XH%rPe?^8o^ljtml3y z97WmtaVYAiFT+Z}B%@(Ce8VHJ>gM5V@R&B^-c zE-c}DV_eF(1xg2f2!oZC+aP@a!(#VuEi_x2loV0(LAvZNJ2F>zpnCeFml*b=2>z#R z(r!U7CMprN6kQUD@L6%ut^857>AL)>Sbo9hcC-kKmtFJIh%KT{fsIJ-m2WW^`$6r& zT{3#C6%i;5qzmrjB7I!b0bGx&Ca!$}S99uvl#A;qiV)X*LPOj#*x>CCUR2|aS`7te zH2S!!f*w4f=jy*b!zj-t914baY{6d+YI2>V6hMq#5K2aYY$pcNQ5)+ccob@ODVUR+*#?C%@y&v#mKT zy|SdKFk_b)5pq)F_=a{VJFE`+i4#ht?A$1)2kHRDi6%rughks<|5pv??^`O1g(c-B z*_!v0+Ie?ii1;Wo-bIHkqAY5u;7?~J)G8#?D#-Te(#;*Az>@UZ5Y&_h65|J8BEKX% zvdLA<*io&C!~GrUNtmKX^RXvHg#EeFhJWMACYt6u;IT%E`R4cBZR9Ik^q6w|c}`5#xYP*RguS z1v#2d&>KJ{#(?<&!YI}XwuPKRbs)}c2U;OkWkBH&uH5v5`h`C-;_KWGPg+}onN2CTR zWy!e3s;s;RtzxocA%p<(cYPF8MBa%OgH$)%7iUa}UHUIpKSoq;2A1WP5v-5NG=Q~1tKTgYS(#zp)L%ez z;N4RMVeZ5<*d6kdelZkRW8bL@ILado4>xfKQ@2V6}wa+w89d4Z{h5|Yeec@P;}DM<5^Si1;~^^ruy z!9i%G%Fd5liv4DZy#cW%-1VYamGU2IVd7q0o?657dlBu^MJj5a$#_J(unQWte&5Z(P-Pi`1U`^*2Sxwxbnv?Mh(e1On zX$?TkUeQgum@KPyYQrqKv%!O#|J+RgqFTMhWi1O+!zO~|R(>L9DVx(pLfZ0h@(W+=5*JVQUv5vzIIO@asN>n^Yzq)>KcYd0phNd?Isx z86_|zBKECcNbN!Mb}VUIUx=V`A*X@k2!tqCb@URRVTS46q!)#=vZYO&LXs-e0~KXb z3Y8f0g?63omd3+aF0g!^Y(buoqN3=?>;5c|)yu)v#6Uy?t!oF)7Zq=(je~8D$1%^? z#G$VT4eKah%VB}V>Tnb3V`W0))r|_j4i@Q4m1&Kdp`cMk4(5lJ4oE4$V(oUc7Z&x3cbOL^Rjv+uuUQ>et0NV6!%x$IFH954+cT%P;NUQ4nWn!HxVd`5X9}oS>@0v{V}w(i0E{T_ziI+V%r#(meM zjf{+@7aHhd5GqOH)BCJr`Q65o<=KlsP8;Gur!a@%e4@_#X%%Vv^n$LLhq_Wu)g+BT zklbeBb;)B7{Bn=r>+e!Yu~xU2-Hn7C`LCim=v(6W54r8)CggXueGP$e z_qdQQnIHr!;sfYW8?3ELWcjWNbo-`qzBtV4Gw}I2Vq}iRxscLYVQuLky!{J=B%3Kw zsN*PUqg&zJDZY5H|W>q6uaW;+g^^m#_vj+jzbzL-MwB(_w$h$FegfCdE!D@(?! z!2dCH8tvsm*AG{XD={EE!&NWcc|7`c@z%)jLc|gAO?N&CUOhqjpMvEw(FTBBy_@qri zTW;d&U*lzuP`~`Xs3Kq~r%Ai1#Yz=Olu0aPf^y)9T_N1Z3tY*PI;ad^o3*n%99N@Z zKoD69g98M#K00l%S1Pn|(JDBn$zjfmBOA0O?59;D5JW{53xzyO<^`r!_`|1&){DKM z;1jo!!1Fk8Awgz0Yycdpz1AF|rHvs7BP25Om_>A=?Wnq-VI3bbnE{m_;ZOw3E;=H) zgf4#a7aq?rZ%3hSuP>eAN>`6y=$spDsLauW7lyh*-W}Jw6IivUC|5sQAMm1qD*#;8 zR{M6gj`c_0RoAKwT^Q50h%lH$Y%&r~Nhm@sn4;Z-{x3^}a7yQ7!mcwl)9*o@I=74+Odrp5g7CNlhMWd-Vj-+g3 z)z4MnSGpaYX{g-ZB%<2G#obTC5!FHO9dZixE)^1WG1nioGb-_Vm4=7*q>P z5aUNBhD-p(Aq(?ULf@|etFj@WpmSJt@E$V*etU!$5k3g0sJlj&v=U~<1Lk?bBi&F% zHIx4-qOUHZ-Y@@a+d&BeuOYR?=3zi_D#eOu}`qKq8DxM!Z#{(2l@J z*}e37Czd}=ck+T}_94^Q^@=xsW)z^aP`vVN-fL+ahlzognwlgRs9x%1O@0>R%;?2B z{AT3SU%MDBK@=cpJfczA9*lvaY_Xq6G(?~h3V#=1{Mbbo^sEb=SB63#s8gvgQD!?1 zP#sHOdcBa)`m@m36|~a}QIh@i(s31dXgIOBE4oDT>M0d35_?KI~0n_06m0& z3--e9ls=@=QV+#L-SpAu&1RcX>P3Vg*Wh91EFh3e8?*!tlJQw9P5RFE@l* zAkFTTa8Ke&SOB*+Ob?Z1g(clBcnzF&9Ef4}Y02O_<3;^Jfhag2t??~YkdXP|z^JmL zJ)an!Mm%`&AnP>|FDR|VPPsd2$&#|By!M86tfcnb+Ab|AkCz(*E%%p59_O8r{@=4) zDacpS+h8xE5W7fkgoz4CQePK~>%!&Ot9Vqtj*d=DA6rmUvNo}}m|H@V!Y_srUA-zD z`B?pKsHn7N<<34Iz0P#G>14YI5({t$_CJuQt+l2|`pu^Nli=wxFAoLrhDkyz_7l z{|TZ-Y@q{PdE{RA+?)g2xs=a$7&78<(d?FPDISw5c7)2U83`_b=R(T;bKjhh&z))$3I!HLnel`PS~_bR zE;SChHxT;WJDu*IP>!y3S2G2g$!}8*vc?>O#AnNhx5+nPP+s!Zz>uLWjhRPyI|EY~ zIE=a)%EoNXtC$`gp-tfG6$RdL zH^0Uzr0b-r>HL`S(q1-1Flx9nA$(^db~2o~d<#ljC12y_t*^G7kmYS7ppl423FZrM z>wd*YN$Dkv{~q8`n;z(AZs)ZDJ@u>cp2?PCK{M%SjQVCWOyvcBjs&TyWUA#L`Ly-t z^PJ(=PqeL?pO)x8DGN#o1W7vwGLiHe*xb?^v7@3k9bwM6(D*kWR90RyZMn6SI(wgO z{9=M~-n|{!OwbUVXEq>%%l^tpkle;!dmYC+eqH6!WjXp~QVh{Aa`?dHd>L4qnJRWn~QVc&|sk zC~>2^1HEojrEI}bjZyK+WqBV!K&?52AoGVTB`>9`c=6tlx|H@hT=U{L#ED)NZWvO;Tg`1 zy&7&vj809bE+3t<7R&e7Yx=)y1@5P5Th0?cwjg)@Fxc@@+dJMrMXh z*t|&7PWDdo;Jf-PVBK9Vr|7P;iKJg;;3dojvMC8Qp^RHApdMnA;=*NAB*$}#Ck9FO zkfT3ue%Xaduq&#DCm*u6D`#!gU}y+eB_xC5+X^G2t>HY-N>xF+WN{o0PQyBTT66OP zUjZ`V0Uejljh9myn^m2>*8*8u7NzJ(mO>VVVTqNs3lW{m*`Mzv^ zeA}0&IL$*XJoR*CV@C$2UXy(9bp@i;i@Mew=D;~<1(Q`mMa$t_0u)ls*vX?KxpFB7 z`Ev${mNf-&gOT9C`S_u9D*CtrCT@nMQlEvnnSF(ON2icXrvyQzln$NuRIU!9k3drc zi>RkV;jcoOuAQdLK)gX^#gzjIJ>kBMpl%67P=P|abAN7o243d^{`lW8X=jmL0VUT- zkf^!8fT2j{cb`ccPG+-+)t0q#rG(#u|YX}gI1H+-zYWjJLmA0 z+S0l)7?pW`y+19PO$Qc*45fX4Us|KrKe@9%3v1o8RB2 zmJ57;950uuw!gn^q80E&defUsguGu(y@Ql2#QA@S>i(;;@el3Thn|d{k&TY&Z_yn< zqV|u<>_00T9h{B--0_d1P(fBwN=1}X(AL?(+}MFyz}Cu0*1*c#-uW+KZ@f35xZz5o5lzt8&fO4|P}Nc`yL-)g&m-iLn%0ua>!w0eJv5C6yE{~lc6r-wa<><*N{`#EFheTby4 zv+b$Dc!79z_q)A}gF;Q1Y=UtK+u0}rn|Az?bvlbo)_p6J^OB2Xhnr@-adSI!7WJ?8 z2X4PgCsR_MqrD$5oiExyUS7N(Ub9jHJEu$56G1HpzJ-!O8uZ*g?b7^*IDhdc9Zgywb{kkfDK~m!krGk z;Dnn9_487Gz_W8IZ9@l`4~v`G6fig{k>-E`^LzU*ZL|1 zV_R$+ZTYn%ko&PUh7>KoK9%?bH~&k4Qb6S7n-R;*i8i5HaRG!Fu;jv+=cq zdOD@M*i7kF?8!jIcgI-0v6OaO4CdlZVpC}qZV~u8yJw>Qhm3#X6} z!O{2-a}9S(Y2m*Uezmf~=cn*Y22HLd4=sw;bLEdfcLg2+Nb>IlafdDx3js?)BVLY zX_WRuhsC(;mABb(X_!f%65fT^sG2OUf-!n4Tucae7HjukuafNSXX_cuN-B(2?a93H zXZwRII@C|!^fW=fFnBg0b6aC}sg&0i?`ZO&faME7N%4swAOTBlvmzh`>N|^bd>u@9@Db{5${Cu2ZmNO)fC>e!TH~+* zYs{UW9RquHmjDB*q=C4XqeMG|MmAmZrFAi1`J0|-bO-#$mJ?C{}E$i%X(|4^Z z>2PqcL|VWjEkj(Y39k*$Y05}pY|GX4(6dMS>x$jJwT2G)_#ki+pAdes?(C zf9HA5!Gj_OWEuE3=k|GxLgWFtjwfb5Y`I+_xkOpMl2GCohH~V53jA^jf^y)G^*@C{ zD2Ebv`lJCT$2cr(;@*y+RSsG?M;>(-KmH zGPkKXw+Z-T{ZC3UC#O9Q`#o6b zoBCMEgrIo-+Ud$@DI(eHE*r_pyKKgi_q}fl! z`@_iwJWa)nZA`D9F;AzCs~(OS*Vx&)Fz(FF1hwBrfAXeHOlq9f$!2`{67{MIJURJu zG`5>vOBc`Pm!s_xYsj1QQI4f~#jm5ES9)DM$s5#H1L|Y^ue>>R{fm!RC7L4QEr{<$ zzD0#MImu?sR!83FuMwiV$n;9+Q|qLl$2y`ALvIhW&8QD1=LR;x-H^iUaV_(0dNyU4vH5Q2ZmLCzH@I;wD1E z6)dhtg$PUE{|T4PQ>fVVBn|g6)Y9HsZ4)xPFwQw;i{zu`E}-tFUD!Vsx3JDb>dl`Yo{C8654 z@uaDSzSo(JqU>;ZR|1u+1Y+TOo55SU1DQ65d2%+P(i+FJMv3#6gBO#ls8UjQ2S$@2 zAnhJu1W!moFK{-(Gvec7Dcr31x>@mYu>fND7?{xE&jW@aT3{}O14Vt=AX3gcTEsw9 z13hD*jSNvJke}(rB=uueZ1X*uVrRnH<;a;VFfrubQS$Ds6OAaUx!6 zfL%n>7m%LyU?kdBpi~-ib?+u-SYcb=36S%F=#Y2M8P_jyB!7Dpz3Z0}XxUO7U+6q^ zvi6+4$f=^oJvi8ryi){KCLXG|Q-2juftt5^7zD{qz36f?clm~K7;izR0die`BBVdt zdvX$#-xqLJy>;yoFRRt<5>U*sS|f{-?*4p|CRpN~{CYn(osbhQ9(@b$iv8rV@Y}{s zmM|zSzAsY*3rl%r4q06(PS_k^O>dT503`l}9t4`3DWVWv@mQVPHef0(KMsk>q1bF6 zG%7RqBlH)MnHdQSV#A2K&y`1+Onf&DE_9x~5LftRaK2ZXf>Z1|u+ggDhx5~70VgYk zD#-srcIyW_NB+E(_1!tsa&~MIiS8)06KV1AF83rij_W+za2(^fYGC&P+9;+Bh^K^H zr)nXETiK>oCwve&mHrJ=Rbt9LW}$$QB0+r_{Hju@6@?HB3L#b%nSJY2?hONA_F?_c z_%yr=GH{&S=l8Yq{eK4JAGL{UP?t;O5gXrg8Nc(-AM3x0g>{?H3{7{%G@3dqc|B8m{Wy>c@LLO~S421t&PN-gy-s8~Z1Dx@t<#9HSx z3u=y70XqU!Z4SlV--L?Zqqd%3Z~Ee8yVifp6_$3jjuqD%eW9%AVqW5`ob%?C^K_pw zkR+kK=T6Sh-M0Q#O0=LY9Y3U9qb7pQZ(t@2ori@%a#A)J3 zn9lCEJN*EFf0);15DH%UX!W884)H$arw z)i29xhK7+h4*>^02&mW0?^?+&3Y#4BQ!RcJ(1dZT#)|?d$rD`q`Wtb2x40>_tlSt5b6g9%rLcxIYB>~f^z}I~CG}d4cTQs9IrsqT6fxanm;%6oFK5u7R zb38}Hi1LCWS6u$JKXdZ7s67G#4whA;Ce>#aSJU#I2fTWQkL~bUH9GX1?E76$(DmWL zd*EmISmD+6ivF*iM51mXTs6223VYzf)>LW!5t)Ed8I)g4gA^Hv1F&{x8a=GsRYQ>QY&o+or&wdxlHel?XK| z@$X2Mv7Ol+8Q=UaLs0U4aH%^bzoy_wkfW2DO)Tr#Uf9JnvXbZ5n6Qy2S5ZKom_W_5 z$xEXw<`Y6?EE+UKr1D0X?W_y$E7S#9!fhVbwYWT2C3RNCYCtG_VS9zwU|!f;<;$ZF zQsE_<>C?7>uMwa;@RLl-vum{hk#;G!oyyKhnAdQH_-Z3lIhdsNyQ>VtSt9D{9rbmRsVwvBeY4*s}OA0f#i>@~3t9Gs_ob}&4wxX?HxNzD_hFGGdE>>9|-USZTwR#LQxbz%>Ar4zDNge(}%@ zwshi69yh#GiZ$R(yYa@&mbA>=Z)Hpz%NpOi0{%?zAB^uGOz&6UwF|3kIriRqBY#6* zyweVT>$mz3kmuuj{Kt2DMs`-ZzZuWpmRA2y<0z&7fOwSE)#PN=D3#2ujUB1~;5C0j zg@1?$B-;l&0=Pc?19JW+w8H`*7ym*#tN_mQFS5hRzySF{YCaZb=8qBlv#_zT794D0+NmHo$xKzp|Q6HYg`#<>UtyWuJsoS+Th+Ck|z6fQ;{? z5CkGeEI}#$Qc7$96nqUP{6`?4{}6oWBL-jK4w{~^zR+j;*T`-RlVpiI_9$HyePOJ^PrFPEKHU7A02e$Iq*df=li0YN#q z`WUdArM&5aOP%U46PuVDLVQwfGkTi)O_hZOJY%iyvHnA{-bsgNRQ2H(F{-I~tCXDt zEqy)H5+c>v1zsDa}%wJk>#J!Hs48Y%&0l8e*%faMC zXC8XjQ#*}ev}lAh;UhTzIwW@H99tU@NPi7puu0)U3*uBU1O9;I&AW4kj>AucuII}J zGJt_U^~s*CHKB=KN!16vb&Yn=H|MOeS0 zS-)CZzizU48)Es$Vs#frJv81#NIcdfZYuYc*j>n<(VWRprh;>KBy2S$xS_#GLqkT9 zzmCUY<`L2fi|s5KsrvQ=8!ZFn)qT1G;SFIbaNaHf1#iXd!|cGSV0J@OvApF>;OB+W zOsjk+_vcB`FB;9G#fvv&It!)2XcqB8%r+Cqx+-#A{1A^Vyo;FLN@dzYBsnH z3U1BvK+KZauU}5)+d#bZF_4<06JzDbo3Y3$3#GUYftsVc^?dC!(&t|F%B7G}D2JbO zkW9X)g5#*mGWMqrE^RlgHU1{Ni?Ye1KSniK=q8tfo0p0g$l~B4Uogp%EuSl9t^3W9 z=U+UGgF@mYja*HTLZ%9NmyBscKfx%)EXAUPH3p+2Sk`Mj!g`2dEfPV$Y{r5a5E(B*(nX?A#=eF(=t2ncMo00O=o6Q+NhAaTDl$GrwlU{+~M&}Tk?^! zt#v3egtF_*f%4nhkw}tmtKk?i%4n06ft{Gf!DI zRL5q@WoHq^NBKfpVgIf`!iv6_(7X`{1v6Ef8NjcyYCDEAMZop_Qb64nArda(#m&qW zsKk_os49NtE~x(L8nNsZWu0Z^QC2sieMH`;V#2CmnoLR5Z4}Buh}lsc>A}IG{tI!* z>b(}!mlh5A7!H=f78YVffH}IIj1*wVBNwFR26}4-{VLsLRJcTuXdgASl2CZlQFtYG zuOx2XgO}&;-$ZaUu4lYnh`1GqX@otFqbb)_esUx^M+n*PcZDeVmDV2I2> z6X(`H;321qrkWY@lI+k$p^fb$s^y(S!C9q!2uy(52SW%$y`L$@35cukwC>!sv>SGP zT+746Qu4B%8}b@g6+*F9@Df(iMOa)WaZfmu%3~b<<-fY& zHQ=5N*j^)ek8;kBY$oX7?YNZ~rL4-_uuPpd=Y zU1>j%SpR8rJePjx(Dx2miH)I~ah_qpb903?B;;0rzA7A_|v*!5*Eohzk@6gGH zUl|iup?~h%(sDsv8R{3~Lgu0hzy}x2K)u;y-fN3uJ8bYa%?hxlIZyjGSqJphoW>i3$gD4mlN@x&4fks#(9qQ5 z=-S!&C(a4nec$!z&XS5{YZ=}oo(%PLcl1^r0c}{rPDt6WlzPO!mQ! zNz?|LrC}K(wEAx=qARAYKiUS{_R89Y%wSv)ynyh!`P*gb(=V#b9W z)mB_Zmtew))n*2k(!;G$9kQsA?*`k8U?ECgezfh*CS3Q1OCvX_UCfo*$i+sEw5S$B zP+7v3K_AI@vNG?d-N?ggfQ-}%y}YgQif~!-i;hPGAZx!s=LxahHunr!Q|A)BfD#!- zM<*qsKZu-YRBVsa@INu}NaHEaS>?YVKg4%T6|d`AYM{3APw&Gu-7?wI=hb`9pgat2 z#dGkP&f1-_GyTr%m~&>;X7PjT2aHEamrS?Bd*JWb9hLqXXj6P~_TQZ8^Q<*Ctr@HR zo)DI4cr%W_FfZI|7^@l)EwABu_|rTPf6d>?OPbznDHxB@#iggMd5rCVTHwdfJ|v4T zy9wnjXzx&^;gp!>@i^F@l~-wBwpj_lao*! zF?%KlUoF^W+r@-@4n;7vRa2s)5lKu|?(C0qYh~~qHX6(R&cHXwzd9lhLphVFCb$m7 zRDp?5qy{OPNEl`pU$-e{P&dcUi3!opm91vX*L!GV9tW(DU{HmQRUKWh-0Uio=&Abo z+00Z`PxI2i{H8e(I%1b1xs|-UHY~MoKpF2Bk1#AK`j8ffBl{WWH&u zaNOQH*$D-PYT1BG{@S9piayIwKBP_I_5yz6N<+hljn(EOgFL;sw8GVOJ=+2Hp-{r0 zLtS$K*$jp>>ZmZLJONJWHpW>UmK4(SVz;$FK4CJ8)k`a<-gCgapSv+eK1QVKwl{8G?z5lf#>-w)~npJ}<4OIak=Xi&PN!>e9hjj$LvZ zJ}4G($c9SZ%j`8(ms&w~I0iusXHqqT;e?YG3IkWcawPIK#=+BVg}IUkNu49;+B%qP zNX)N1KUo5D*?CS4IDWW@fjDMAuhu}{L0S6X=i=>L%C+wFbs?qw{0L^aOMLRq+Is=C z>~9{7?AuNTuAdTxs>W`ZgIvm!c0b;AgKOSIf}LypLoO9;zOq5vaFCbj(%r)ds?xm4 z%Zt<^Jkm#x882Lp4MyuL(!t>wpN1Kj)qws8aOyHN-?HMF*p|x5=DFKP+=gaUr#ov+ zqZ&ufxDQSP7Z#Z}%}!>Y3J*V{4K#@gu5R(dO*h7Z;TJSxGHU1hF>s36KFpJC6Jw)% zI+xP+i#DckWlQTHxfPo=VtKhUE9?rT zF1(m1XwZ|-Q=euRF67k&hBbve8vRbPYRFKZzu1RaB!Xo487doeKqVb^Jas#LJ7_n5 zR{lsdF-9)*(tw%0fZ$se_q2S7MT)9w%c_EMo~9wE_)HlAip%cYbXgxh4tl_81hlf0 zFw44=rA#2py1-}6P~RYj$N*MD*xh{(4*ta%@iF;r0#65PcX)18su5RY4#?Oa&cElK zTt!8(;U4L<84H~6Bc7U`-0wtqPmoEFTXx-s`WB+IG2a@N8~5SPpCnyjrJ28Viij5d zY=a#Pr~X$bT7)SqI{Gb??z%}RvQ3e9%t?ucRt z-)J;!QIVYB*V^q(0{31aMIzVoG1PgsxMK0r3iDjHEc-3nW%g#Hza`583N((G&#r5( zt)Qj8b5nt{*Yl5&nd5PtE`6eJraldITwW%k-@=u%b575z<9Q)Lgt?X&FeCW^E)rHHN$x8c3O=<%_TJwCPD z)5=~Y*GwgC_dS>?1uM&gF0R%lUyyNhrRjr?MlW^8>k=b#wwA=^N;~;H2)E%?Q3cSO zz97m`31g`T!G2Wd@_k@$hgxF;k;d%e@>WB=!xSIDmG~VfJ-DmrKLDMNFZ&-rhl!4k z^>3g<|G`K8&fxebSp|R?{$)r0ABc{+kchg5IHi(u%!M! z5S=`b^VmRR=cK(h()*r|H`wIYq z{@DWgZ}Gp*{@C^>CSnHknwfNO^W9aZ1{%~{$906#bm5v>f zh4BNaFf;#A0!+7=0Jh`Ie^`&x0a9f?Jh%Vu+n+uBs|~>Q0FLnZOo05Gk2NzBGbAJH z$5j~sF$+G!A4xTU)&Tq27y;V>+n5;v#{rH7Tn7NLm;i}CjDWPDk3CF)9e)zc3vG z6B8ZV|IX1q6W2(ScJzLqQ2h@G0== z%ZOl^#Zb;GP&By`M@=CsZ?jTiOlSFUe0y34a;8&)8SsZFoX#+IqCPX>`U-%_?Zcx-IXNDK`o zn=IF=TUsV*h8m@3u$PzyIBaIclb}Rd2=XdDGsj=58V{BoCIxdqs&Eg+@U)EHbp`@&nAa`Bb7b^_) z$Cq^}A5=fHb$97c-#TkK(rUf;w--I!EH3+nEn9DhOj!@^t3H<)FU2HGCLklbTixWA zuK~hUv0lot_HCnC+mkeb(a56D);1U6iPWA&MN8!ymL{zA&S+JUZki^_OC8C| zz@18ql-*6~N~CTn^OQ>+>B_DhktWGU)_KaEP4UXD9Y{^Mo9;-}K~MAr%2GFw>ISdV zMas(79M$yh2XoYmY^h4~8yrm$PqRhJ!q!;T{T;f6fBVm+D)n!uH6c9B7AAJJgsAlF zSgO0anaq#RstZy!v8tGnsjx>*SqpSRSZjT~ucIgK+EvV*Oyg#&g`(_mTbDOD-6}b4 ziC=j7TDLxqx@UJk8om5nYFmoG#-C3&Q&Ll`R3mAk-cm)pWx#eBU&h;KXTR3B^rn|v zro0??9v+fup5#{e^lGKMZDwo?ud8WVYuZ&bm@;eE2S#ly)*DQt23VawwK7+&(X1cZ zrdLkdGqvjS|1IBH2~T?NI3nvQjcXubq(Oj{sGMZJPeQ%;aJ_62g-Re4ddzd1+cKkP zAWsLF7GRolW`vd$P?&?p6l%0GQ18a1N4O1`9+a(0f6f?@?$~EWtm>E^w8}EwVTYx^ zHd~PTn*52-J2yQDHzn0onjWf*Om`RgS7XoQ@FzW@sFiY7C&gZMc)CKn+phxd@@hP1ckSVCUCBlcG+?bR8;P~LoHe%Ec((8XjpaESdqsJwQgtSG#Ojpw znz`QX{g)T+U2VIenTE#lggb)P+?r7{v&uEj=*p)(DE27h>d?y8Z@-Jx%?qhi;PBmd zZB|PU26J9E|K=p){J^<7nyK~e?~Sk}MX9=*)97L}`jTWYUtC6K-jy&yWFYVx*HrBG zg?uxF@1r#icVrjrlH&QFDbehOz6H$hQLEq9%eG*(zJA$%AC&)424lJ_hP>v9F6w$F3Tqvz z^fcYt>M5@%cAPTrSv=0l$=W%`Vam92?f`TEo=`|uFONjj?4iR8Dk!)hn0TBw@kn|t z*CTV16Ae>Db% zVLXxWOfK+v_5g39T^ERT`IQj#Siza1;fRMGNPaP=);?-nVp%`_b0(YHjjpq-cTZty zZ7mUNn$Ldc^I-n8m<`S6L6^0Y0BuwmuSk6aPG8mMz^x9AMvqKQ06+0q@IMZDBn2i} zCO0j*t6Y>Ce#t>;OG0Zy_i3hznl8^c7&&}4pm7gL3J!LSnSzVH%FLKoZVo)(b|ULlgj6Wn0=HMhQW^* zy72$3I8D}Jj$$CcXulgVwrZe&#%efWT{uFlIsROk zky{bGm|T~AI3i`z;POL$BAvXVMcZX=3=7Dju2b}|FWBY+6SQ6|!5n{$mZ8Z^eS))X zBgK3nW4o%V-fk*^83>!U0f&2p>$y$2%)vxpetOl~cte@HE;*V)%}jj2rlJSQt7=F8 z(<~;fH}?lGzN@ha0|yM6(Z(#%E~|C%0Q;2Aa_hp3y2nAlsCLiLbF!(VgyA*|qiIdD zimcB2393{3gLaczwh?{t>W4aC#52}25M^4BEBYCtNxG?X9<4|Vd8Kh-bf@5%LFLLc zuA*1pX<12qR+!2l&XQ_fn<3;%g~#8oZ$Ojv-A;6KCP#5+q<*$8?;HE1-$hrzN}jBs zHpqo4w~ng5MkRAS<>1C&<&n;0y8v=kYXEOK*L1t7dS&9UBL(NV%#ov05p((^59oSa zJ5`-+z^g*vde5+7RRwkUMS49S{lwZ-3W6`h1+HWH5eB}M z(nx|;@cCqt#0Wwiy=0$=H8I9;*qRx8kT!^qOZGV~>{y_n7)2{d9h1r|X&vPmLbadP zOs!}(D?=BU+tondMzzk&nyoTnfL2fR{3~}FNCY$Fa)5LvbQfz#nOmW94sxZ-BL`d~ zXQ*{WVb{b(;IPt(+9J6IUTd7ZwYx>VWZC8TndA|U1$O7}hCU4DnnurLI zrxRy-1j~dQYd?4D!jYX{P<3CGL0v4K9EFu1UZtMjJEeD}_Gxo1+iK5MSZTCz?$|Ag z?rH9_R_|Hql&M5-rYahL{5nK`W4Y{nHe)_^25n(|uIv}mYiKPp%nv~mr=mRf`Vsfk zEASIIe*!lBUHBs-6ApOwZpZbdIu2<3)+kofT^bW--Pw5c7VwewkT98oU&6^&RC4FD#CrR|c1xy6 zRc0gZa{tk9lLlt?(hrpHGhnzK$9VQ%)~>d%7aYf}CvrN9+^V;RP#dJm>Onm>@tGe~ zs3&tLCx4GbK#$@H34;-tQaVCXD4avA?<(L%nx>dSDsUHZfuE(&PC(aIINYB?)-X8o zXcH?7g?R)BfY||v)mdNYY-E9Lz`uRiU%sEEtNi@T<5I6f5+uk(mr|bY|7Jkxjt|*XJ0vUkeC)nrhY_s5-Z!Iq z>mMJtqn@Xv;kZ*ax-CS$XZ8_E;B5huSmyb5`uo4i-M#I)SvJ5 zt9>Ug`10~S?eyV8uHfsv<=x(uZ_(}9=y{%2R*kcm7OvY;Rw)}L9N9Qj{fb=S2K|_8X3_{}HoBSX#&_D(tJNP%a&bqGy=%Hw;&=j$*2nFoYAQr|CH zhzNPsMzZMDy1%@wJg+6UYGZh6&iIiQhsV=U(^N71et+|KJ?H3DxcU{(oq0DBjp9l& ztwBct6>NxtqRYnHjAd)Ynp-15ZbHu4VWtW6K z;?lANqpblkeygz^%853aBXD}3k}U+F8{r)H^}xQa_+2;ZpY@-U_&lIjvFI#)I!tT zCwlq`|Dci=bv>f&CaST#rh9%tmjdlYTqeQ_+v+yP#{CP*@TM^iPV{b^10`S&4u<@b zK#O`tHk^Ot#Uxhxqcq)Pxe^pKkMGXmmzbwuUS9{Trj?WN$ zIo__nTPYZu==}4&fMb(7e2w&s-5=5wj^$&;@sskqdtse_TYlvkL9HE**c;Kpzc!^v zF%`06`08HQ-m$%Je(8M2e2;#H@`;XqICQaO;Vq{T-E+n(b?G#*;&Vydn_UA`pfG`DJz`-$he$^)1 zTt%=cccLe)%53hTmZH^Ls>?*rLuC(P56dk=;*FcobvM+f*t%YI_}NJ)@i}1JVYygu zm^heZhwT;VcQ*KpmuhVAR9=TTv>;>ThJJJ` zH0`rH3gh4RC_+Whq<&|x;bD4Jao0ZT1V(UjDq6fmny+uL@4`7h`J4O!ej;L|_@i^V zBh*w8$pa8TDdq4c*v+iw%SL8-H1W0z_RXF~Il?t^Z7eF?jW~~j>&YybUG1hTv7wQ? zXCMs*cR3E&>AMD6tLvtqf_OoQ^Y1+nje`Xqcj-e&Zp{*!^904|9}P{^eY0^flCb|O9(*^_#S6Ly;rhur+OPB zZ|9pgHgXoVdTG)+eqURScD?DGWYQt22k~3FMLni%`L6*pA6)~|<3p0+hIryZx;J z?4)c!`pX1^tK&&XM;O3yq1N!v<|Y`W0Jeod7Z5Rim7eJ=kh7cVh?Y`CKq|5ah8+DB zbaW=nmi;LqferJ^Rw4RgL-V)dZ@ya#kGHIC`u( z?mUB_!k8gf5tS(c8X{3zJF(?+_;A4GuxDSPl!+*wED8*kFq?2rUVqfkx?E8Neme0l zaQk{Hb`M7Hf^o$fW+4zq#Olwxz^Im{d`Ue9dI!ZGnNH08_#6Q|M4Nk)X*PVX@6z5L zBVM-IPg<2X47+W2pTPi|8*AQNQ_>2|4GFDG zZe8Ok54GXwuc4Ur1!u4G53##&>TJp<&PbogKsYuDpsjjPXcIzhM^#5zNAbcm5BH+W z!poveX9ywWGCGPr-NgG3$5tSaC6m$bA$LZ&+D8Z^gW(wjVdAgB_uwX-1Y+sqxOi0J zeYW3T zSsTBN>S?_1V&cNP0fl+s*>UM?3WA1n2dm~4C!~i_uer9r^!>Vg+ATiwYR#O<;NcNXYm1bDj`t$^IuWvO!M+&cdjUL6vp21HwTNb8Aah zXHn)@*I>))FHXNqr14jRuqM-hP@5B^Gz7B=@C`i~)4USjoBruC2;o-@AzsDxLhN3K ztnZtysUQnos^?&0B!4oORaVT5t67@Tp^3cx@|=cEtn3`*>2lzM=!+=Z$xi;?t@3)z z&;!^oLL$>L@#{Fn(lVl5a+csbWXLfVPX(&wz?`{C ziHcSB2CGe+tD+ME7S?63TQzw0aPY}dES5n+9lB$cpm{}rCuj_Je>uv5zXZKU zLvGo4=g^TP+~*Um`{juyQNzh4UL$xqus$!fc~oD!uOsi;lnYPrTdFS=yaC-Sm36Tg zXUpQiLq=&H)=wlvL1Er_5^87y#gUa`MNI3*chkELL$5Ll+~qsMw~%gzrwW&2jc_$~ zTgoE@uB z_c8Z{GbdApabH3mnPtrWkh*9v#&rCrB|jLT(&N`Eo}Y;-$!s+vw}M=L=~SWTJLIee zZH9ha^6(ljF&q!TPQo>X$Jx;{4Njf6qrxn>KVn%ST4YCZ!8MX>Rk=NM-5TptXjhKrFi zcWhm>*qq{0C@Kv5+P8lew|e1g+Pm+-!U_L4M04C*2Yu-RkNSo;Vv@LM|Eq)JH`2fq za)7+Fje!M**p_BlH6 z`3e8l?*6SS^d%96#Mc*=K_Uslqnw4)8i{X&o1U+;ZuQaT5n4~BbKV3hW5hNH9dio` z`IH}VBQ-5KoOXPgQT2CgVPawNl`+)#BrB|Mv&X!>YDqfMrL$#t|2O)d`$9rpVZ`8) zAnZiq4@ka7QbdJYd1+CMWJtMEvAPgj#qo&(fEo-7E#ubKS>H%B&ReEykJ zNl&8N@jJQ1r)71#x6gTeISPl1a=#2o@HXJ2lmtYX9hUp>N%^qAal zI)xXABUQb{VDFjM;->b#?s#Vn z|Lj7A5|Ty(-i7W-G(-a-^f;7b3wVA2i1WqK=Kk3HS9%rajpI%pgXVwtaOSvi)Pe!q zmY6MU#1djd5+p{C07V798SJpz?@J&i_BlbB=;8ORY3or?__fv? z;_cI+2eH{{lm4h%p3N4@+c`m({OiaakGeQqG`)qq?go zS8k@@m8Z1C~#7%~H{i zpQ;=V4o%<4#yT~)e6f%jWA2Z2?kGWD(XU+5)O&99nrwVFA?+e=^2ZBQ)0(6ubkQ!%>&ozbEx5%Ykp(&n?XCB+K zx=xxO(@eF6TlzASo%A;8kjuoGiS@5b;qG}1_1&EJ%pdm&4vv|Hz%ck~A_`!jL&(~% zn3L_FzD^ZC$cmmX*G7Y)JeYD@!sC>E?R_im58mE3{%vAV8})CC=tz(OGM<5bZ`sm_ z{t6{ul>BuR3=naZ{yR{{%;1(1nJ(gOIWhFBX%(L&3G~$+9Tx%cOXZvi1I_W{(An{~ zba43BX*E3rn+)GwLBwAdWeR0-O5+DOK&Bm_=uI>sI?m=_$(uT*lsoyF#0}|GJ}zVd zioYzW&PISL2(pU(jIgbO3kL>DTN6#G6z>i{;ODcDIedyBGYVSW`N!V9ugobfr{W>K zj0js;*NHw^PjKx?D`DVrDDhq{>C-Qt4qiRIn%$xc$!PRD$2Gf{_$%~Z{@L$%I%bZM z3`WmQAnKjYGzIYJ+z@px55-#FzOY4NnOnwS((u-4EBSTs(!5(j+DHjr!f;6ezu-ag zW^nAUGO5115L7mEyKAp_ZWOfI)7E~&d zEU^g32YBmC2=%FCAJ!^U!NEw%gAdF+fxcq&kP%G=<1DzLWX120`#&s%LGq7;ZKWih zgmZ^pgZH@xf@HObE=-xPvmm;wVA|hyX;Z`AiKJID+KnrJrgsct%L_7btTIAJ;W>sd znsNRqUq2zCW{en1gf3F@7b9Am_29N}LY71h6$_#M_ye0v>aI&j?l1tA$5@4}t-0lj zqV(o*F16U}xi*Zv80;tZ1Gw|{#gfvQbG^={?acl|pW;o&z)fJ|~qG_+k%%j-)euBQ1ZT8l*{4L-7Pc-Q|Dm;D4shAlgii>HFkm%$m z)P8@$Pg!_TG9|~Su{C0Ob1oN;@sBAa91giu{MyLY5w3%Ca&$pXkl*!h=| z_uN;LE#Gvw?n+7;8ozPp$*HBodKB)#n34_463mrD`4T;Tnx@)iuGD+XB(d>rcv!yp z>Ai;(l$vR8GE}(wtQP7?`n-?k%H3dVv>3cqaam28Uf%e=S98q5Wj6Ubyd_dikg4^4 zt}&72%?Iwyhvm=1w)A7^LY=A~j|^(#%CwZd)7MfDQGpL6JN}3^hbYqS7V)iv6BGmzLZV>7-?3URmh&7EN!d-0x}!Cq_@ZHxeGSF}Z8W z!7!ev%1I6`+^Ht`ZT^7qHjuL+@)S0OEd}}n-M~?dG^WNWXY9*cw&@NM&{)s3;5HOb zqmI6tkd!&k=txVu7l*KywH*f z2(oPh&x^21mNBNRU{(C!wojmj7(daAuo7nE!ULDww@&cewi9vRi*V=koDwv+W3z-< zCr@i{Xp?CLcro>8|3CnE8%!5sqUD)?yK8kZ!3S-t9U@!D-NfI>>`LHIU3|jks-jiK zvJ7n)X4hY@t^7fntaVzk5k^g5hGi;}i;g7f^p!|O%isG^EYk2vF2p&|(3-dgBSPF} zB5Gjh+CGv_>f<0o8pwPmX{8usc~30^SyeU%)=>k1kdWk^TK?+fZmDg1jX* ze@b}Wc#a^0w4?cOv`FbJ&QkKLY#|Tl;KrDQ|v85NN-v|W!-R9Lf z|M7=)_$)&*IJxlKct_qsh5%=Jj7r{G<7eDIKKRQj*Ymx>C}od(MXSkmQ7+5p@e{;_ z9i=YwLH;%!pYvh0*0c5JOm1V_#{JRXa=R>P8+ZH51&dF)eKF0}Z3L|I&{2ArrgO8w zw#7)+wzlx;8rSZ#U&}Ss?xJMS$`)V=F#@dd%*KGVaR9*VH`usQcmZ@BViq`x^2NUo zF?^uw5-k^|t6oM-3JI>>I?O5Lbf z%&}g=`&0Q+`clI9L|K5?7n9!W;Pomc0eTI1?>E#Z)^YmEEoF?~&$qmSse9?EK(k7- zOfjUXTg0-lDVPKTK<9=8=+G)aR%0@}j%VcZUSCGu`9~{6IqAExypaGV1cD?4M)cyH zfN^mB5_F8fxT^r>D9l8cIDP63wTmhT>cr@@BI>Y?oqGXZK<8!a?@dqduAG8bs0;OA z?a+JFC7S&SaPZyOXMycE)hF-z`y5aT2oZs?=81bc`ULVUA6x79#;| zo%$k0KSMPrLbiz~9@G%L+TQQB>9`yBuQzIYU&5BWynmxV=qc`W*sTq<`uzjgrSUaq zp?gCza9Kc#Y+L>KNI?)-)lD4_(@L=Rx^&5rcGF>j=|2<>{#9y09fTF}&P~cg?CMQ~ zkJRTNg$C|PFm7CM!DG>4G&;J{EF7Fumu$GdzTKf?Ipv&GQ)D)u;*1HtR6Msx^6Zix zwD_;kcyDqX%1B91LQj=P9z$M(nt(B_C9G;Ce4YZxUzgGe@chwmJ#Z0h=k3Y1E$pCR zg6Q}<7EF)~myCXhun?x0d@CHVAN5M=jE)e3M6qLgk#_K@h#L{R5CMfRD)881oIjAs zc-(Nz_chFc$M?8h`FvH`{&m53Ok)&pmeH6sDhG_A%bEtd(rTqQy}QeuKaV>MaFT0J zgM^q7IdPb~Y^G0+kgL7J_Kd%V)(^V(yAQv1=SM-tD2=!jzWr(6UW%M@T%^swJ3nVa z$h1qhT>(`8p5ms6B~i&toST||oGKYArl44#%GTk*vGt8^(#Tvg-Zb0}Z z1X*K%2i%DF!~35gzhS_rE|7rWq%leRyWmdSMMw6%{clF+K6vW z!_dJ*HL=#Xbq=tW2C9Ak!J*8{8+bA_WaWlnZNfg(L0VmbHQg1Qq+h3 zZKBMH6gwcszuXJ*E?C{(3wyvE*+o9fINSd-vziLaP4k6snd#^KB2ZUtLvw<4VqX+&>>*& z0qNyQ7X=!ioZ~-G{AsTjI@39>!RmpU&?ZRR^I~TGol|ztN7F6XQCW!^3NbzM~`SJB5 zQ-Re~%%iQksqj%0{997$sPflA3I#IOie)eGtA{Md{kZcX%@TFrORF)A$Hlk0;kcX_ zANGgdvFi$OTJhn~kkCm2 z?R@SY$qzBOgerv$vjY8RJSYo zt-EIsVcufs4x8S zVZ|3=hwt^-!Exhrd&!oWy3=BNLL~?Enps{&xxxFI&MueJv*GK%NZtJ|Zdq|@E&0A( zi;-Epj^)$xR5AbOf$#M??ue~S_5IqsdHs_7zD;P?y74l8p_cK{AI=DCtPv1Fcp$v4=f+g5#r!VAmqo8C-WD#diiH_87?@DJYh2h9oF!9DCgu4pwJoH`6H=5O z;v+^9GZs|T@zX=1LjR)*l_(J*y@s=KhTp(GU&uh7)3pT8-)0s`f71!xVs_ldVwR(b z=QN%zF$|djl-*1s;s~l%(M}_QM0c(#YKVAnV zzWxgU4}vg`22k#y^O9W?g}@EifEi%iVFZBFCT)t*lpBDO7I%UY&T{i{h^C^bUb5=M zB87sVj|UJ-VhV2SCC~njNnqeMS1XT4fVMkGtY8!&h4#35t*E#=te~94J9%gVQ#mwu z@;+{d5zn6W6-o}Z292zd&2!wJ(>($ATWI8nP>6_ysuP_D!<4OPrE`K`9z5KHR|uX) z)md7HfU2m+q3Jgh4im2pw7($`2knpK$c3$wf?z`iq>(!Mxti z+((Y-+rA(@o-!3rRf_rXeq}MDg{3cJU0P-EnAdKqgK5covxUZJakoDyx`5A5b8EY=H=^|7zv;)|?O*K*rZ~rh08iw=q$pj!Dc@s(EI&*WzH`gj_te@F_Sz>7+u6>FXoxaLT>uZNR`U7 zY-!w;X78KRxCKG6<#7*-3=o1r?dt{^m&dgdI6$Xzc5J;ZDfs#I`nEOI=E(EjG%!gT zpHhWiqOxftJ$ag%mPa$({{a=CH}h6bh@ zX8)K|u(NdsB*tMQa!#~NS7Nn9;_cX8KxWP0H>j_$bg&v(xnztX(j#3`Z=Eb{I>1_| z$UUAteF;(Ts4)K<8U&hBJv2d3Wm3&?^+v8p@pRY6FCO<~u&0Z~<@0(}WJQCQ_bF`Y zZHfQf?UQjvakcl!sOJqXBkp?XmkaaB;An$KHYz5m#rqLGUb@cJ=TgLbS+y+4nXk4i z0Kjzw);GfSpVMA&krwBOAiI363nZv^^`VPGS4w*9N0#uB7?4ib_tI-@Ob(jx$|)f? zOH{7&2ZWPND58o1FI7@l@rd*X-xJ=^@Fgq>h_NMsfU2E+ZNZ>R>kFcOh(+9}$yg&C zN9M@fCG2n%fiXQunk+&DMevpWZt3u^7W2^i@z*i`I_Lr{G0~WK(umDa=-Q1|yQkH- zQpokulsK`#i^*4(%@?$4pde*__f8w93-{A}`cma#@;n%VN!%$y_j{7#LSNm}SnhMf zY4yG=2HerZ$fwBnpzl-si%jk9c`Rpl4ekb5hv2yxpv4m+UbQw(F?KjP>=?V#qj#-A1q+@BB*i+EHvsA?X4WD9k;)GNS!d$ju;~{y_nnzny<}3MEyGUizADH1`39 zy^=oMIK%|nH&LD5!Qh#GdZ8Dej!pnfzSr?vhJ~g;)GXLDU7S4L1A{KNk*Eaj!dfU4 z-4bngwH$6JE-T2~!-+FF+KY9E*@Lr)uQ<0yS_W69E zUfXc*R)Br2H-VQ-mGsmUKSL&Eg8}l)xWgB^*|);Ww(NyuGQ+|-LL=HU5*30dBPV2_ z*0UqYckgNIFg$=e@d75lJ zyW=G1sCwa=8_tIJLxi6(zxB+u3GCd|a_`kjRN`YCZ{#OGJG ztP9yt1SA3np#X^Y^MTdgSUH96^o(jzi`;LL$A~jnzF$di%_!8bD$IPeksq1hJnHbAp{<=o>$D~#npM=$*s}V@*L4m2Jq%05u{j`SE1xhe zLab=JtZQ4KU-y$F$c|90YIa`RNtD4ieFWLIrtL<51DEnRGFz7GhxHx=8R1W|^=$Yj z+4LW=1-=~{+{V6M^JAWkfW!V&PYfDs^oC)TAGbgwb;?JV9@!^B~CPK zD-}3v4pNtEv>?NauM38poHM-IlKL6qC_joDhX|+4%Y<#R<2nCu zG84M2R^B`qApesB^1&a9>tM0jVEAc8A1A?e>D)g>Z;ub|~;3FJ7UFC#BW7d+owY!xoL7 z9>tUE^Gk^P`tiF~dtw3;__p;(k(f=#GiW)KCVLy{Mz~}~m^f9oiy|Iy*gRF>uYa6_ z6~_-cs{_$EEZ^<#Yacded!p)1-j8K?b2va2F z(zhu<0G~_BKZa#+F?sC8B@EJn*D}r@G)I+EtBbZ#g*$r#Y@NYXiYa=7gkknLTh_q<4Y)&CDPi0>JY{{a>j*gQl;rT zSe!(d9pHvH(d=50v4`bGuj7E0SVIDyl)7|^c{?iGFAheNfEUt>@SlwY-22HDND|oh zP(>Opdt>!MVwYwK5Mjnb1WeE}>XITLm|eO{xxH%C*snPao};nQ6Q8(A=9jxiU=yNu zSd1M`RTSUwo)80n^H78ccO2wpfTA}>4cxEvuV2ZlYkxhbnzxrvIWIjYT_!>hP_-*P zYFUQT)|Vo`-5~9yyt!c_LwJY#CA*EThm?H#wetnW5z z&(G1eG}Xk2Ezk@cs@3CQ?yY~C`lBqqLZbDqerv*>%*4&H`_PnVunV^pL-dTZ9#2lh zAv!q{hdCFkw27|gL7{k`zpx8gePVwO;6DXlum3u}`>Qnnr9hIAVqi-2%IyNg{t$qd z4|wf@(_Q&DpHHgDqbQ2upzYV=dKP+LxY1zM^Um&8bS(+C5wx;4kx)6x-Ic`E!lDCe zq%3Eo%qJUX*?`*1)I{=I5YO)jFYZv*=cfhQu){1Qw_}Uko)98>=!0D0omI#y&be?G zr8c8Lr?b$T!};QwBTo4Op*Wbc15R3B+cd-Rq+t=xto@`LbSyXNzTsaczP!{~LjLGa zwKz#87pT>r;}^#o{ZVp9qqMbor}0bkR%_C%KlQ?aM43TEBnf0sv>9%qt<_|Or_TAdLO@ty+ZEcMF&$( zBk~Q&m1Z=SOkm1HnWE9DuX@Ui-aoHAxjX>zMLzFR{;#x&|B|!*pFDPke=r4x|BjpZ z$1(W-fSXX1Qq@ow|G&pgF#UV5A_1MUorETJt}RF#k!*{~wTv|6KeJ z#>9Wv13LeU!sWl*`+rWt|F80s|3^CJzY&A&5CNjFSC8Rj=Rxol7NH1w_7TxIg)A>% za6>Q>y=f!_$jhG&H6S7LeZ&?p{~W1(~tUy8MTo2XF&+%&oJqPa__MZnRw#t(W~u?DN= zip6G~X&@&Ajh?r11GbZ`(u;glDrnAaeFcW0N8JpYUAsN{|0c};X_f!96yrbg>OWD? ze~KLciN$|!z5mG+`42?F(Zb%@&hftj5dND0=YJR5|3Yl|Z=8hx##R0Q4Y1)qB#{4= ztNIVb@ju`8zXKco)6M_&#{bS$WnyRkf5B^LR@1UZS;hRGozjILp|+wQsUyrWsI#)< zYH3fGM=yDvF6E)gq|`(yDpMI+mnh08qYURx zW<_+gP*6Dz2rE-a$$QS{X4&&KTX6+>Dkzy=|2p}wJIw-S0jHu(|Mp2?QWhI1j$h(R zpN?7OE6#oH2nlH_AlMU;tmbYqsV}`55c&De?%X`0KglD%IZmtL4Ix<-IcPM!Xt9<;yQ5Z^z=mzYf-WvPLBzG@9*zA{( z@ypc82TDKV{Ej%#JLl-)=JRsUGiG@RQ!pXM(u9 zfx=YcWm#&fOPQ7Ic>{yWB~AIuIC`34yQ1Q&*K*|rB&bh!4`RxbggSu^{V^6tD%2>J^^X~UGYWH1J(4S?J~*6J^8+jkaW9jkIKWjnX`}RVZ%DCb@oUjZDH- zDUM|^NqN^M@ut32WCp~wF288iMgp->YR0osX@-ZLvSda^Jnh&fPI=2Ffn{)wpk&oc zcE*aG&OE7AM=`ZkR`KU)49l!GHRa9#$+#V@M8Te0!ZE03dYP3Tbj*oWWtd2M8I9iT zz+Ne=uS?}&hC7}8#G+H&IM2AKoEC@uW8XY-quhg79eQnYT`6-6w!CO#-Ly4LV`3y3 z9SI$IC7BKROWrW_@Ti1y{k7o!uB`KTjm%&|pTfS`S#jFRkV9U%`5fkWWGPF}1-qC7 zF;pZ=WD0R8BBUfi2D4)rb6-XP4Z5*1wQZZa@v2p7^Ikg?+qJ>GNCA=vJ)~e4Og2gi zTmsN65FD5v&?cm(FBAn3=$K*P@tS0%kJiL3H(}+w4aB04=E!Yyq2p^WQl$?=9ScqAkK=WXzB1xfv;%NKF;sWAm&Z0yUXLmV8w^ zsBIaO+xmFh1#N+jc$<8ccH5UVn{K9B7kw_RT|dPo$HJ7_T2G-E+1{>FTj-bI8fw7p z5f5wom%vM!)Ij{uP^gM&Q5Ul@x2+t~Y17d1k5X8t<_ESvw{~q=Zz$}9+1WVf6!c?F zfjO`AI4MpGBJZwi3Cgh0tEO@v{K&~6;&*tBdwgSG`*vgp@!|tkImMH`x+uOY{3{D2 zONwU|c!OIQzBP0%F^0bfv=Z7Abh7W=we?Hm1-F##ma0`}KK8@+Lj2jX>QO*F!Yc~@ z($hif4X<#9$XNahl@|wTZzW3&YI$7yz?r;siP7x|7v3+|1Lmg)udb*%8@6%c*HDA z^stMDv1CNgSs^Upv3trS(3TZ*%kov~-^gr4V67vprIuqRg(DNl&fKpct zoTjYOczmJGEtjQX%Qd)+m7}Wh!&I;prd2Q{sBzn$1CxFz>;0D0DJQ<(bkAAq-4~Zx z4OZHFFTu5oRMa3*eUmRe1dD0l)k5dU$!ekqkHVNyAKoI$im1xhckh7d2+@oQO76_&S5F{`YcEx>^*o;_EM@NUKfM2s-y zFm${gN+vQyj$5tIclBF7EmmX(1^aSXD=a8P$w1xOT?ZHzCxu=J&>%I zVN^IU0S_B8Ok-``Z$i(>`aI--V^!ywol+zWS^&#mZg)or>CIs(h&8j6h%U7sBoa33YKzJ2EhhjJAaZI( z=Q^l@_LWTWq4Xtg#*l90{0hD5_+kHHSr_ZNs=(0t__c;IhJrP7sRFEvU163JO+m4X zHwtfL#Ews(K(Tzr-S=qydJ1MKr;5Q|try^^`B-&5K02?l=bkZpiqO(1Jd@c-tbA^E z(X40dpDk?CvrJeig^?$J<<^H08;qQLUm}oK|NaJxt8zQnglSEZqRS z=0D9LkBrvu;X!XKwIhSpaB}%~x@k6h??sIDTf)DK4u;Pls~*1BnHsV{GY_Nr3V*+( zrEJZsd=@C&FIcxSF%w$c0H)Q*PyZk0fqy3UAM?O}s0A?onH>KAk_!-#kyQ~?{x?O) zzbB0UoIpZ-@U#8Dv&(%@c>QDM|G_8s-=rY_=9v2<@A$|4_t<|A{Ymq$w*DpVqo)5F z`tAQa=AZ6AI=guGZKg#<@3;(3}?fyrq|ET4^rv5Mg z|7ht?x<57jzjFV#tZ@HV7XI|i|AVF9HU78t-tXb`f4gG&pU3C_L^8+rp{DS+Rwn~H z%m1X+`9T`@4=sp~<<9Q`{(q3_WMujfX!xfV#K%zntq~#qo5${7SmyqlZYSs8G7taM z?ffg^PkaAs&BFNCnE01P3lkIThm6JlalJe5>F%jH|NXoy^l+sRW0t1;hEX4 z4{H)t%jGBQm^nlT8M3wU2c{VtyjNs@&{y=nMmb|ATkhW`Vsv`FqH_PZXa1_&`;B^` zO1Ua)#n*7OFjBLzhv8|%2;^-7!N1(s>-(WwbdBJEFqyM{I|zO^ zYgPI3b=l|$*Bi;)&h?=7-P9E8oZDe>{e5`R#0$}TI2~e$p%sSFAyuE}YW`WSlh&A9%60WM>fJHpuH7*H3u2ZfAx#gFq~U`U0IUJe5C8 ze?1WBAp%`*EzWX70K4JwpLNCVrFgr7Eb<1wVF4|_c#H569!(F#+7YjU@~P~`)fg0_ zi+5oEjAnqy5z&M==*n}a!B>=-(F#1qFe*t%WCUfdbpUiz5t5Oa47jnD>Hiq>2jKcL zQfK4o^(q}^eP_MOdn)>@I7kqD=rcanuR#!QjbT^kvgdd5l#&Im78Atqu{9CRm=VqN zbCMC|LO0hE%R+yPIy^~B^lcq5+fZQ<{{4G5;@7NqDFfcdn#DjLTNTd_USVJ>K@X#85W^-k3{t;X}lVG6dZ z1~2tI>K)}TIaiIN{8bMsyQeC&Dz@n+={gKPWnaB-P23^44lhr6q%bGTpB zc%bE&Dp!P`(JSO=8RB%dJrNq|4Dk?A!Rk6f3NlK7A(B9r@dfAy6n$60s zT^;5JOR&hI-hMqWq5UB^x|Yl17^W1-{t~W>t4DyFp?7H~dl0!n2j!u!Q`wot&=+JA zPZ-6X6k#-AaSF(|PwGF&FAYvbD95^}rqWOMBR@S`NY{>AJyARUkYStuRHG4ZRK|AB zn0{oeRN<68Kxsu+SyEX-2e{+5bc6qnwN6aw{g9!^YPq^Ipy~ zESh?dJEA#c`~{rY)PS@I*j|=wK(0o&`(%f~0Oy8wftCGf^03Dd>k=y>j}?-3Do=+& z4q3n@J)0?T6xw7;RmC7X^V_N*o@ul!;U21%Mm$Ep71QFc0W*>lZTCm?68h`!@a!X&n315sl@EYXT?kG@6fF1FbHbm*Lr% zd-TEJVwJ}YL)bv6TR)&XSywfVjkInSY>V1Og$*eoD*F^ieK-?(5N3 z>X(h)KO}Vd8iJdThjQT`>PZ4#AW-pnC|t!ZqojD>4N`+Tx5i9z2MKC*KUIPv6d`h) zZ?zc88pcU+SH+DFL+$76)UlQdAai5$%(7x-?ZnI#eaMG`&a!D}+QPB%85-A_&`{HZ z#4UDB$LiSIZoNAz;xr`HqQh152x`l<0orp7+JbflL>;PG5PjUr-0GNOj#?}Ysr8rj zx`6jdZG$;pBe}eR+3g@+yZtXAba?it`m2<9jRyc27Z(+DeI_EmJ*?U9uzpd5^uunLm_fT&9pSNS$CLS-@#ICCf(e;uCGdj z2WTYIuNUz=H;N?&pPk@UE2r({XFvwctW>_Al&*I|9=B8AB%nMj1X!`@=!7S;7m`m2 zAs;B*!UYJ;fbQ{q!z|Feji;S7Dig8<{@#VD3X2o(&V`z1OojZ!(!Yt!a_DL?_(>fx zU0_dm{%TvlAX|WrhOTyC<5Tj*f=v6DsBnhG&it z)$pPU>c^J1xOx=^^bbgI0`VTmDH`=)zPCQ2XDw5teVA{SB0gGi7(nN%h$lEnyupE? zP>?J@XSj@YNTfx5zyXXJ9H6gYpHo@Bc@hna8mHG?A0Fmp;BAPi1Io1ud(KJXvPDZ; z{j?*q7vQIVd}?&8XQO|_6=ZjVSyI2AOHE{UZy&k-k9b;dpeca+oIUEGIm$SCjyM10(-0q z-me0l_=#WmM@DnYwG=M$1OHU8ag%ArytKxs!}hR)iX$4R#12-s#6uMexj9O$#Of+b ztN)mpo=oJ8DvoyDap@0B*Hex6O8L?Z?|bWAIlugYGvJJ1Sn{9Rwd6 zsue$N@y~#OcwpMrAHrO8`x$Z@VHV;uS6??vX5j^J+nTtDLp2rf(Mq_p9JfUnfyi;yO-msK7-08eh-<*c#JgXlv^2~QfTh1BIdNa3kDrley@@F5&2k=srTThDJ3EG zi@IBw2P1;qe}<+^2mg-!QR?aXdZjm`(Zz7$Fabp?T@Q6x4SP(`Vh#ALEwwckiOG)f@o-JB^bDHI|NpwrB35Ymt zx<)%w+sU5Ti~d4`Jbp|V)(6vfyE|mEKw#^JYD%WbSXEtA^C=(q9A89SoD)6i5 zGG|-R7<0JAd3MQ%_sT}KX)kC=N>!(}ZnQmHt+VpEr#tnR4BdtpzuQj#XrCC)#@kaL z_R1bO7kY(Oi-Vu+4aq(VwBS-i^DMmvl7qIxyrC;iEAWXOLL1UFY7f73O)wdQE3Ut+ z%77kwH#&(WTzQZdPTV+1L~K5H7g}ii<`WRt7R&x3qQ_jx$(IAxE7Mp*`Ypa%XHlN$ zwoH3KFo4rG)ovotQHFyHNVJ#3aoen`22(MC`Y{ zNl2kkXoX!EcZs{d_A<@kHC6AfwYLP*;jwhOBDixcamtY@+AVpUS0iPPhLl3+g@Afc zO}6lTFH`UF2z_AwMF~Nm#`mB?|5TO4)_!rz>ULg?s}q~zbEk&1e%%G2R<@D_r^O{z z^R4G>eA=tQkdaQ#iB&c1ipj{Om1;Wd`aGZv2R>#$XK6iLWHrbrLXruvz8!m`M!6Af zLc#`l5i4XHh@~z$CXJm30Z)55@R(4sfvFU@-xH=yMy*6L(ON2V9Cf=viG`5H2s;iv z_YlM&VqVL(=-2eCP=~vpO0Rs!N}%bT9bO{$vEKqWmPyXC&@Nnu3(i#%-rq`myiCt< z-US?zo4IFL_l)%|D(oq)f+jS5g64{vbQ8Y8`nz75}mW55J6y%QjA0o(= z`G4BSJt*yAhMGB`0&0Qstpv#qH`p;8S`aUfVAks0q3&|R(JnPtP!EHy{50ER4_Tl> zC4T0dJhoQt{;;hwQrWc*eg1sw({@3h1*HmAQzP=MD07d4>O#BXwY$B$@skiIs^IM= zkDMQcNHCQ_tf_3;*}Q`WI}1WlU|xHlFE$hM{b$Tdw;-ifvbjucOCgt*;+U$SqEVmZ ziysIbOv5XHLXGOPIVZ6RatD+`}Nun z)ZKnkQm(@a2MSfT3^-CBHn(4(|E#V3AR_|;k*b0RU?@@a~ex)?D)*4i`tSEUCj#$-fE|~_ljwDc> zU(yS($`ZoSZZjXvB~5QwZF($qJn7$0tW&jKufCj|)8^6&kfg@2E3aC`VP*Vu9uh89IYF``%(ev((L} zXj~$iSg`5_k^!Qu&?LlEAf_4Z0xPdEcKewW-pZV4Nq_`9#0WHYsO!t+Cd7c6XP&aImb^ymw5FAHR2*+OuWf>coT{oNpTg@{8A)Em0=ek8=%$C54yfS zU^tL%P!t;eR{gDJo)8G+`@nrg#6RV$GXf*r(y{x)Xe1KK-`eX4O(x$*vl2QLy_>&I zcfjqp^INR*@VGjyy|`lwyO#f|8_ax>ch}*p;3+M)aL`U8A4Qc5?HO6)rqk_smK?+9 zq&$<7Qg!rkK{kiNoeM@&NCH+c7Es1SP#Hi}fdDJPktq#KVW-Vc%~%Grql;k%DkN?b zDpfgsLB(WKfp%ofylF?LUu!cS-nP~ZVlCSn*R69|UuCSZm~WF>DF4nMUF+l)4V`*b zF{C|}87bMvgC9#4ohpE(2p5l7IduYp*BU5+nnZqrXdCq-cx~5aB{lkn)RJva>Idz3 z!C5sq)tP)|RZ!+9^kgVYQtj+U`h4Vu1oWU4oLG&EJ!yUE4xMGt`vE{u-Lg$k&gCjGgp_I5~jT{m5A^bi?oP z-osonG;*4z8WiLH>aZ8&%@kzgJgv0bpBpeW(hH7WA}yhBO=ag>sh9oFQksN9=0wfD zi|%T+$m?e5(o?x6pari+AyizW7*xo)N6k;#q;@&X<4`|F#O;A8y$F8LnSrV1pSFS@ zO@KI?`n3c!!rYNXV?_~yIa{CsZsCtR(+r2=392Gmqt#!Ke%*x(MkdfsX&sjH;1KGR zW9l?|Z}m7^`XoUGEBblIKTp+4nYf}RxwW4yY^>i$)ALh4pvmInU}9!;Tm&pt@YzaP z>`9B+&qt^*)73o1!Ra0x{VdPov0DRclHzg2(zs? z8+qp&Pr6|Y1oi%)p*;R!lALA1<+P+pB`fupw9rD9i1{g0)>6Wo0CE@{L-(|Zf&kj$ zr}$sxAX7lqpc?i;Xr4Q6VRRlk6Cp`l0b4ApyAm$$C5%|X!4FpgOq^45rDOK&w?$K` z5OJ5q3KW?_y!wM7zxp+U2G|V{vm1ypS37tU#DjWE#HC*dGNgm(lPeX`V%x&R(U>1a zH!JX@UN6CJs^H!wtYA3e#{H9r(1pFDCIr}$u;n0|hJkGyeADePn?f^;?n&5pk!`N( zs*5#o);+!k&+az9I2%a z;aZMCV0lTDBo<6l=BB&bRn!344MjB}i=v*;nH_V~c!FvzwvYw|&6RbMY|iJQ(V1Av zFTE_y_B@eH@Q3;Qe)ThXf~^8K77hC6E|#m4|Gkkl243`ac`LM9BURlZPQQou(o-wv~nmu+Ueu!6-pkyT;TdP7gY_4I?5~nwi}{oe(NW z2qY6(h|Gb%xCX59=vZ91z;xY<9gj2;DKd+-M1u?oS!?9@kWC`)D_+^#XQM*pH@bvT zL8d_@)dX|aG6^qHdb6~E;6JrA=9uFNmlF#J?X9Ejl36=6~g z#-A-13FFL-_5|7lBaB`E)b>HF;WA9kW%efDwFr&Nm5o&;;6bOLhEds&De|Wij&bqMLW^30f?Je^@iZtWHz~<;qi!7HW5tn1jeDB! ztJgR5+j#|pTJFVL>)KNUV30#{WL5-pa6(B$N?rY8kt9Gcrs zdoY?HpU>q|Nkq%c@184IC)G4f6UD2(j@w+s58hnLESeYk)<|HhAarOAF(gs6wV)`( zl63NX-$4RVp>=ll*;OS>I-*}lX;)gSkVFViPW+*N$wOjYxmETGP1^3Y8I?0y|8ajR zt4_WU3}$eSf)*kjH()%f+oV^x+oT9|*}L2sYk>wOadTfCEd&SHBCxcl~F;Y(o04qEY)6 zoR3RdT>4a0O5^_Ghj`my0K??}=ImVueSypl?BV6bBJ*uhcXlFpws1ljxbAV&!I%sP zEqDT@^%(rFiGJ$ST~b`S<1yk{zSOU+m4y{)dYAV2BeojUlUQ44;Y4y-WC=ZMJwM*c z@*A49@+vF8*F(5c^4U3aJ!h-GKhD94IsII#jZU-MO}k%G7A{I4`cqZ1txd6u>w7)L zZn{bWdxfRW?mpK(y-OT-xh*e6-td$!X#A8LXCi)?@N0EtZoT?GvMJU{=7) z?wv0{FP{%|sbF)h%rxHF5E!v)4^#y}!Ks|Fe7(&ehY-L~tM$xc&q&*)mL+bj{L<(z zQLdCToZ?Pmdi5%&Vn`x8oto%V1KG}G$e!Rj^P?ZOV~K{>sAnFnW-X((L_Y)^>Tx;2~GeBA0_?8uE=jpB^`>4rU z`-NUumv+J>V^>xA5Li!q&O71ERhUzn#{wJ`?+CT4vmpD3ETZF+9-gbW@$U} za~Pg+)&r%kCO;|T+)&`EMw&er-f%fRvcpA;{ODs@6ljH9^6m!&Z#pK$CwT493_(x^ zPcGgew|lF;lc;n0$f<#C(W|DW5|h>qw4TpA^(M%MF?S!TT#n4}Q#J^82YuPE8M#Le z6{QsBk42C&XRNM@nl(KwqM*nGReYXW2uku#8E|9wD-%-s4HG(Z3e(J(5c(Xij8=mm!}9gkhv;q2E@QwyQt9vrPkuR5`GI%a^aHPP;TK?s zEqm%mgHd8YmZDLxEF-0Z0n6 ztr3i4u_ocXi1pb{wRc0`jr7!2NE?RJwHeUjqm;S2$l;||J5QD^lju9|xOOSu*@I=F zz()>|RZs4J9-ZyIp@hWaq?lEQ4Z>TkyKLT7q}y!F@2PsfAI*;V+}+*+(kQc&0VJC= zOKIYT!G6yoGlmH9__6}!&JhhF%{S0huCB}?GCc?99se(#98 zw;QWnN!ULfKsDusJ((*anjc?64BdMHxdH&2Rg zp3Sjly;6)f#!zm4fB4vdCp0!Zuu!BfzoI#kU7DiIDA_e!zhZ~phd3ko4XK$36(8w> z5cuQbCykmC?lM>7Qv~IRTP+VsGY1U)Q;pEI(c8h&BQD;^VM|$b6d7%9{qnXdtJ3}{ z0bUjX`%$)&Y>rfP0ps3Ur?UvaQElIw$ap`~zL}#6*4vy{a_5m0p;H6n#2s}=JG+!~ z*U1JfrzA#*QK$*zq9?DlV*zkHE9nbCB{nKJY4(QEr70F4fbtYCkg{+I;lWx~D(J$z z2cbY0PGtukgm9RdLDXs(6v|o6&sCuHJF1RLrssR0Qt52_n(>zDg?kv=&1(6uq5F1; z%}s>&oH#E<7gGsGm>rV#dnZS1cv&qS&?*g z`MBsyg#fK`zXPD^7B@UPtdOuMV$YFSNzTX@$f(R|O3CQJiZG#R+ z_9Vu;4Mu5a=1C-uF!>z5oVmk0G~b-2H@8CxftmLEEmpy>5_P6)M?>`Grh0fu?3}!- zu>j@)y~rP0cEm!&Jqs`g%s*L49LiPGrA_m_3;SwYf9nUzlOr*U*1S6K?K zX0xh?g+{jWl}6jOZ??3ss(_%lA(@Yf34IIX3-0c*&1HSd+soLsKcC}E9FF#()l3FxAE>8K(EoU|#IS$ZTAV*)jacYbxeL-=QcPtMa#m9creA@(D?4gMJ8 zW*$>y>rygY3j3JR*BQmqc07KH+KH|6FDBL+087HMA2IV9LsU)#ENIqV3l8&uFv?x~ zy&@PSnKUxV!)q7c;FPTL@)AkI*N8}!My5&Zi5-U0K-08J%;x)ZH=>K{4q+CHgEUrC z=t)h_i9>^kt_Qw#HR^t@)l))$G^Pj?| z$ugTU3lL~Dfo3ra?k3ynj%br6nxIrd^2e!63%*F*2^$Q*z8D#hzqU)4XacB|w%Uy zUI_DKexN2DLZ&dCQFsG-2`a~Yv85GEM9dipkvA4D#sGr_$D!1BiD*-zbp}+mc-%HM z;h``7GH}qI*Scp6EIEW$g7{%U8Pwlkw}v?of<0Q4hOdahS3l0-B72qltt&!1GZ6IpCywhxR*kHefUR{ha%`lR*9c8{I?|7+8Zk>D-UY6L|BX%M3+VzhLJyV zPsxKKqhxpNRy3iYmqC~lFDZE1T{?TUT|BS(Sr-*br8Q#vyg^f-;Yf4Bw2; zMork57b+IwcbQgv#tpVLtKj;OUro_(New`&Y_dt}H!JJRPZFTSsfWNBW*37)p1XvK z^vLN@Aclb#a|qJhhTy)Ct9pXe#O0fpDaxzBYsK@bGS?}G3f@Jvjef06HB+MKVO8L+#be?G;4dtBY_(_k2Gr}z2;@$aFEuX=t&F{#Z&`vCd_!Ze2{i?rJxJi^EAILU zSrr<-L--VPQq^6TiKU+&XvN`{lJ93jj^+7;VitYO*CEA?1*C{X_t!7=*cuxCp-fry zDNou=2iJ*7ParvSuoZxPHCG2emXpi1`@mXqw8KxS)cxYR=R-WZZ_>8YTDxx)wcLG0 zJ*_Bw%U(GlkT7D;QS83>NGH(FuIc`k&aN=2iWrv5izE9eg&4Z8ZlQDdsFw;Qup)CT zXKY3m^Jb8VM_57^F`uQ(wXsRJh3gIu3xI7G1Vr&zHgJYa_uGYdR8Cq&PrhW2BF;v)o7eg&Qx-AW|(0-l%Jo7{QCtu@m0w*JJp=~fec|3N6YI3ccU%8 z)BfTcgy4^;Hr+Qe(q@T0}Jfn4bw zum&EL_pm|V(-7blM9>Ceh`{L2GqVjeoUh8%qLL#+>WPZU^-@Y>s7N+4U1I6L7{KLZ zKCq2!TMH5KWxF-ZYBOwT%%B?HG2G33ADTONlyahp5%uhHt$kPyn%xZ*dMkix z+8O5RpMaflO_hO{a6d5X1k(ams@fUco~^QB2#Q`KRK;rjOTX3%=NwIXUcV}k4i4|j z4j(mdSYxhA^-@nkY!~2szk-UN`=0?SaJTTAaq%PgJlnmE)=Lg6e&d=pO72KbZnEL@ z>))|-i*Ipw6OOt0JF=H~=ItqfSfQ%@R3oN3zNLePQI}^B-4wKQ-T7eao9#SiiDC$c zV#y*e=UVzrJe`A~mL|tu-cx)j6S**u{Lr}o9BZ$-pB_k&J*gOI;jXjaqmN*Morl{X zZ%F@gU5LR_$yh2<%p)ejSjFphyK2hDC+M_=bh-iF(OWw?pW`)TMVI65hp1-l&pNtG z8(9`i8sLpqpETSbYjFFTo>I?k&7c36n5i)ykzw>f7s|5F7T}oP-8{aj#lR~Z+)zF* z0M4~Z?VfzD%Ay$Bkn&KBuZd==n>*#2gpJwQtx9uyoTb+3xspLQS!wa^oGatbv&i4! z^&A!p9fy8H2NZS2dmn`p7gZyxPioCRqr(`LBKxGNa%FT=x}y0xwkmK)Rd6SksH=^V zP;U_2WmOm$B+Qz^3sJ-np&Ez*N}irK42M*`jqb|mARPnJmm5C~H2zXl(WqUq^mb(A zXMapV`d>e`#wSC)Y>GB%7E+NM)_vy0ve;jIh}m!>e>$-k%ua{Mh!H#Bidu1D&HgA5 zolDxbp(((doQ_lZ^PQ5(lJ%ISw4+ztJShHfDsQ(nJ}yMyXChFm;V4b$A8~-A|MH&) z4wGc=C%P@^c3N=9C6Kc?`iPDa?%9|@Nm&{KT$Manc3Bd^E7RHQH``-wV}o_va`y*v zsIq>{udUB~_3S?Zj}AJ1f3@A7Pg{h7H>6=$hMG`zfQJp15Qt zLm{0C`o{5(gtuRtyocwp7Gyjxcy-Jexx#+G<9OfvxVldI-IDpYb6Te$(nZU%sj1?T6!u7YdW0iz!FB-( zYlP_xninCGF;wOp641#?<9=K9!$2olQ>H(}xO%r>a?uTCY zU5rvNd^RP=S-iFt4=9*?j=M{&mv&;sEfxz?Q!@0%*o>%6#LQ#9Ca`n($y73B$YaVD!;mX~OS>=7ZOv|@m(A)7f~f$j*GtB%N3bKb zCNs6A&T1^ku;;1YBxjDIHyr0Q^pBd%-Og6x$Oe|bl6b>4UV(RYO zYljuAzsUfD^UY6)Dvn^zO^qY6SCQi4zfL-Tlp_VdVMZe}DRtDr9Nb2E>ejbu@d)!V z18siThdPM*(oz_DQzX-)4s-wr;f(&sO)q*72O2O1t`_KhH=JQfnq+N&_0a^cYS7-b z%f*k4H1WkparGA&5!D<{%g8Nx3at)%wEds(pC<>_f`mX{g}$8{lGZ&#KtNZCF3|8@ zcwDgv_)F@$iq#pWIkaCWU|pNq!aRHG>qpm8V_XD<5)C87e!|cO?KI#$m=@x0)t!v)CVUm&kjUQ=;9?v2r*HDF;&_asOE#1;I!l!nSb8^7tv#V7}R8+@oj<1ZT z`@M}WMTka^+RmwC1jwK`!O2SFWWW;|6ea&-LaoAs)?m1CT%{tLqc*CZuHNS^%0sFV z!uaT8|BS74brsd!vvK51Ev?%(cBjN+9_@lb?qY@}|MgIp7OGT9L#8`n>d4_3`8+if zfjr8LuOSE_`C(c%=F(}NM!3rNk*kwr_05QUa;x<_oB1nv&rqz5GrdxqrZZ$KjmcE> zucXFGVX}q0D@=fL&uJQ7SLCc=k%iqfm36>xr{F;$pInZZTGdL;_jq;qk%((a_hwf?k~WX8)lmhR^&Va6w| zd-g_pcvSH?`x5puYy0!pw}wF)18|)(4|z#Oijc{DD=E6^cz1h!VU1V#Wx4QEL(>tPI>^5}Z3`^y zMU}3;6d@|WsRrK*U6wlIr3KM)a96>;KLXCj^IIn~-!+zN_O7UhyQY+8dUwl*_7)cd z`TB19(xEIAd0Qh6&(taO4*C;lx0^?}>Esq}w3<^(E`s-YzWj`OlvOKwhXWPj)nhpH zOYG38k0Y)b8&w-|v}Di_8>^qm4I*wCMC{5u-)*#2QQPOaCi;#KYc~VUW#es;XppJO;79! zn>tT3_MR`tsuk%p2S7TCb{dLy>hJmHHxf-)?03qEc1k1-J(1SloX5a+8%+oUJkai- z=>^|z4cL;$4X>9-t81YN8;O7JH=}I$(dtVr8J{?~fIl;u2jN0(Jt91A+q)0h$Tk2H}Q(!5I?| zUkp(XwCzjS#h?dM{cqb*H@&*{1+@6Gv8F&mK>u2dbjfzD`kpxW+%6MKLtgec#{<2j~~v$mm2HBXUH9rJ3I|%c#Fy6v-T^UWhKp? zj~w3b+#G&zv^-u^%+h%J;umI+n@@Il17zQOGTz!U-d-}^{4?GPI6nwUUr9M%KRlxj zPpUgN$-6hn`!~%RLmK?=rwiq8W)rt%4o@-Zln?6PXLZwOb(3dxvt~D9W;ar1Hxgzy zGG;g8W;fDiHd1>*?4Z2b2PL48MK zQCl0Q4+x8(kf@Nxzw$@W3ffv3{RgUyk%95U{09U^#O8xS!raF6H>Ac;(bihu=ASYD z!YA>8zWE2A1S1pUKVfB1%zs(>R~QpB8#}@8&!4!z{j49f2LJu@C&gdxzkr+AJ}4_# zIavwV7?}vzSblRXuyXvq{yA7#e}~vU%3x>sNXhZLEH=j9E&82<;lmOK$8S!I|9-Ik zw#&gp!1|Gjm4lIhm7SGZ57Z=mNV1ooKA7HMJf#LRdh>?-)!@}=@CTebF%=Cf8`snO` zV~F@W=1;T#0k`_ydmm%%4@bm@o!=O)4l3t4kiaHHSEr-I%vyBN?R?|3taJ40p;U2-jgIEtH@+z~qQ@Lu(FhUSiqQ3r3LGV3%sW^0#%!tml=ZmiFr2&S01U%r3dDFXYcvcp6a zYe9q{sjMgt*h*EgL)nLZviFN|5PIg0T&S%hQChHl@2ix`)aUN>d8~0I3qvA@6+I-$ zw49kVg}msP8DI7kgh`e~k)dJ5n8eo?TT(>GX!3mgLc!DYu!#-X%(}(71p5 zv%+LWU2R>S5|7&t2)1Tz(6AbcvGqv)GD7ASA`PfzB>T-7OS$o|- z!Oq2hl)$68`Hgl81K3&DPWQQ^^^Ujxyud4WKXd8+q$L=-p;voR=y^h$8-E#d;zchqO<%yNX# zjp+E=l^wmVmWNxX3k|{MLis4hominI12Zc8mXL3?;$~qdNwdbmR>||(G7UpyOBAEE z6xU3Np<28IGQy?5uEJQSdx2c%22bbnpRNkTK6mxeHcq~D8&hg2<4$T}MsD~hcsaLy zavc&0o*kZ1HoX(MWhSh*-&HKuLFA?R<@UI|!=`Pm>%@WZ$@->mcz4>Zx};hA4(V8x2bc3aSAD=`TOHb`A@oIvRaJ?`GL2DIq!1cBzD?BMqs zgn`>4J@&cOoJ!x`b)hfxfT1yoh##cr3^@jViC+a9mN<$ZWq|H@Uy*Q1M#UrVI9~OD zKU=*RU!h?ajc%HMrT*a5{CZ$@<;W3q{|Ewgaqrp$PdB*22d^pw%qh$N4Z{`rok;65 z)SVBUYS+er)fAP)%s<`dyj&!y=>dVQitl&28;3}S8g84>Re!4lN<&;rU@;0vz# z6E|UZ+-MoSI$C8Zj7kE%Hj-AGnf_8V+890T7&T>^8^DjB-!_e%edV;e1W;$dG89K~ zfdj6Ha0Os<-+q*_=i9)Bn>6sUWrkzXWtc*_-<03IpA7Z*={)Xs;9&}JIR^XgV#45V za`BRy{dGiqsT@RZ=r|N!#f=wq%1#~S-nQyu!DBn`$vd)Ra{m~Y*~P)(0!nUZ4X_m| z6i<#))z(l!Kc~!C1_Vbv;9%<>j`+HwpXc8Sb=Lp7e%HIH3-$ze8;piiD9lzjIMCcC zfBp0jHkz9gHkOg`)hi<5_&zLr*IuGC+0+onoUD~M7+UA3)mnh`IL#z&!DErVDsrIN zq^&p_`u6s=&zkwyY&+`Z;R?SqF{tJ?_p|yA9wd23bRnKDf;%~xC}>VWCU!o$giOFyQbb&mSJn0uJoOp-q53D z5-}lrG4~7>?bH@39gH4J1rb3nX@&>^7y5JK>AZZm?O(l)ys2{N@y+>}(};5PVmUj( zPr1kJeRb(fi+SEQ7sAYr+udJ{blcgN6({p|RDR9!4-9VkgUnU+G{3Ga#gqT4|D~9D z5;`MYnJEDc$&=W0MMv8UYM~|VlKQ_Y`wFPImSyeWlHl&{Ixr0G?rwqLZXpB+65NBk z2e;rF+}$O(26qV>B>9Jvd+s?Q@2$7~N!Bp)O?6lAuHM~yb=OxVmYCq09VJbz*j11_ zYT&-wgyLM^{EbyFOsBbqFD=<`u$1K|N(@^ph7jBOH`f#wN|Bq0{LQyN%5$m4%%J73 z{a?~Elb*VB_CzQc?Ar;-(U>+%0idL#lDWEUkgE}CkKIoz2n)CrJ%Xht!qH3uX!dQ- zX55v7d>OLkxWZ-|-xq7T6|-0e?@ubsn`9i8>dp5_wB!wtt?%hz(-bNQ1M^hwmNqS@ zcq&_4Wy%Yx%8SZf4UA@a$|Wzah|0>2tx>Yh$r!Q=p`b9vzzNtjS2Rd8Qx^;%_U$s> z&EE=~3|inJKI2v#Vu{Qz949F#Mj#gYTzOG}M^QO*OJ4Ti zz#Od;<6?{eVD^yCmu3}o!&S7sVbF7AXYch|1CJ6kp&3uX8sj(ASUL+29QJkL83#Ud>Ua!_*vI}_`@B2B^s~YRE6PTUr$kaTbm3G^Ex%2 z3E}{y_9i)-x?o!Pm#rw>^_~@AjO4eF$eRI2Ha}JaVf$6Zm;F|=6c`79(|v*C@D|YS94?h-6x4r!_SN(YDg+%r3}ddhyme% zrD1SRP9vA=AZnG4VKYScugn35)?sK|_IO07`7@FrU~3wM2H3S^q0!|t@JO9+-`p=P73=(GkSV}#*VLC;0$XVdD>3ID)I$E-BJjrUA=H1AT_{*baUxF= z>F&jQFIorE^TsW>AJwQSd=mb=#1Ps(8~?IvrZB!5me47J8E%KPs$*1bxle5wF95U~ zsMp;aHM17g!$BQ1(0!A|M*cw=XZmaJ-1uqnei3d}0KjDQ5cgYtJ@j_`@a48ltE;rI zOw`6bOmoBC+{#?+TfzPB(0HfJBW-a4Ajiemp3|S-T#tKQc;&!IMBVZ#S6NE61RWyJ ztgbhXi9ff%Yl(BvtJPxClY8zSvpWRUuvB;Z&n}>G@&X=4@&hANIBcR4-=szS=zU^!Q5fX zZROER+bQ@c)eK1<>dkv-=w4C1DqKqQn ztKWSq7JPNR;N;b^pYU}@Z~pYcce*bF&+hf?$}J&BJz-uAUY@Pksx}EDLejom+>M-& ze6#qWn#RzQ6*{X1-a!~4;H4>y5Vkihy188x!8nqSw2_QYcVt9Dk?8QPqh%UC69JKW z(4`M|b;ow?R-8~}=^R2-S303=jr|JKwUthdUfdzlLb!dG$CB~JyJiKi{4806HzKSE zqJ@4E-yG~swF=ScNH`$V$rRiIj*K#Hyj_%ln+Q}QQd(3s@adgQogSSOzCP!v+0^u@ zOVommnji7G(83^#;_79H&$U}$MZr?9yq8e#v0CDO2ph;fY5pFTP5I6@85ZieHdLKU zTlg6Dg`o_LUZEALOw$tB z5{YBn7eh@yDfDqGn(r4ynZg%BHuS$0)aswH2&A~k4#kwUIPGB9bYNjn+~;&Ib|Zj; zZsL~mKv<+tesZKE^nbI868q7Rg|HotFeuhw)6+gq*T}2=2OMvo)AJVEj#A4txhAS` z88$9)3y57c?{hkJ#7je`5cGjfw(oKfs#qza#YIDusk%mmgMsW9sidE3=$2*5t2EIt zRf%duaC%Gm-8OjwhlwF545&XkZ2N{)_*UH+qB@-`0A^vn7XqHL7F*|bc}3v zOyxR!=AulmalQ~EHO9jN7-bO zK^S$g$=o1$oESMkvx(NN3R#3oPdP!OLXJ;1d`^xan;ixSrDLyFPG`IjJL%;SF!iME z3%P-xJ$<%?j$oVS9`B{&exYWRZjTwtHef#1YyNz5eN8Ro`i!5i?=9AVLAO18BX#t1 z47^%ss=`$PfIqf!$?BEo+TKIw#A_Baz@}#yS=-c3B`+64SXQmhvlK~h3?A-DI`#e< z-`I)xMlKSZwNG3=>>r4?aZ+Bx3P_=5_69ZvdI`=T0Htjd}Ojjgjqs-hjT& zpDotg8u!R?j(_vV!hOKJ5FY8s`wZV6FhyE&td3Ghc$5HPhU*i%Y=F|x?JXs)8cbGm z!a9tg#;{(+%+8hYMv8dKSwbS_)vkmiPA9FA9dxCvOsMLb_$+QCWIPLjj}w!`9!{;G zRk`oh_-j4lNl&+3?OV+xl%QHWTaq6sYr=Dx@)j9>P?Up~Ug=+soeGx`$*aRVKA<)Z z)5mvIML`tuZ}OJQ_}T;_hBOvN!dhVQODHOZj5{i@au34}vE@uC5Dh`6T-@z0C97bu zlW3=<$>EJkTnE`u7wYe#@i${|cuqdgJ{HUjhthv$Z6Lj_)IlYCo^gA!Fjg=%+-+_F zaq^%YyEn8TS9D;Wkze9KSRAP_EcVhb)UE@Pc&ojWA$Kx`7?EK-$Gfu_>wN%az2ggQ zDtNJeHFhz-p58nrC{(rtykN%uU@lgbN|$b0313ypgBer;BD1&2V`;9)n|D#C5nq@qE>}iJToznBT3+Q1e|Lo7CEL*-+zm+BqG78X-;nFL?jFX8SA4zQlM6dCh?4N#}nVpAP_MY3=n1 zN&E<)o@kft5UG>&vmwe{OSo4l>TU@4G^T?ReTV8+z_{Fj0rgs_LO8aav}nhzv0Dxm z5p6QhUU(3B9+tbUBC05cSdNU^1)@0$>IL~Gd{1gkV$Ceba^4^SnA8em=1o+BeB5z% z*@?=oGNYuKkm*%vobQRYomURyP8l>LEM0(8x6^*bw>#{2M7`E1kBZ$smiW!^qaUb& zpx{777z>N7ws$Yl;Z+0hbDbpggeE6awkp{^gvfHTD#eEm{Al&h`q{_T)u0Jv&yB4&@Yu9Dt8p;KAGehfMz0%t1SdUK52Id-a@XqfpTTd#Z~3y ztS18Tk>bvADo*|SXZMi8p@lbt^!EigAQ(|1DCU2FA?GRSkOQno zQDFCUH^R@8l{E`1-cunUaasE z`%pENzjVLsnoXM$@PRS#h;aYe5I=Tptytz!YlL4N2OY;oS0+M-orRg)X7fC7MfQ#{ z+`%@&4lbbB{|FKZPXmvZ#{7FVt?~#*g2(6r6gqS+Pm$yT#icA{G}{QaCUl-<4Zdp+ ztzpjwukyZtK*9~ZvPOOoqXf_T3VQldOhayce?W|Q-thDefTca&Y_;j$*cS4=*FvkO zjg9#7Lt9|p6zG$zLnxW-l))P`K4lQKLqG>bq^07BocUV=XF~Dh{48ouEW%y1YXg&= zezP}0Qkr?jyW&eH#)fq&&{czoH6O}1=yvfLWTo5UBcykOyQJCAGI^86r(rO`i z>klE^<6DMSj8~0uj@AkTUJgL>=kw=#PN7W!rh=t`rkN*)={$z)7X%+Xw< z0mD}Djg)$9^+cX<(+vj(`+>VqE}Gqgk}p1RF_4_wynupt&zWr3MPvPm)KOoNqZy4n8ex|(4 ze-C)^(|DHXlEqFW%zLmx=x)8cCtN=}z@Ri@2uDEjx#f!lUL?d&*foVL8ifSc`D$*bF= zz#>8~H=rHgAubq(srCxN%gyVsLx0fgeY0ocv*g8tk?q;O>$fB2ZMTXfGR;ytLms26 zQf9Gi4!|3XwxT%G<>Aq9W(mV$9L}lJ(UVF|4S=-9EJL+2^ zPFX&~S5UWzd==pNQYI&^D@^Kg^iuJSX;b=JiXVrQhxX3ECMKQ!vQ!9mjX@^?($;v3 zuINq}H1jNR01u4^2SX09cSXK?oWIyF7!j9Es^Q?BB%HEm<%DoI_ie>--Gn8Do$)k$ zOFOw6Kh@hEm~m@%WN2NLDvRy&EbYwdkA2=S8Z?uH1J7|+A@A&dOm8oG0ya3=fwq_T z6dO?QILh6#ttTYm*v;h=jj|4#b~>jQg35muyBK?Ts9Kv`n%Q1#9UEBYY*-AyzMH*^ z^kREG#BxwJ=d=kWoVNE(aa!Q!QrS~}W9G^!vG%nD)2=Wyx=@>e`Lx13L~buL1BXMn zi9O!?KyM#)6Nwl41Qq#dW@@#7vH3rG8EpYA&FuzxNpCZrE>&N8EIh&nHoT(xw@;t5x`o$wi{42o@K5- zBCT3ql!ZI@dQVM*VQ4s-dqWM^4QY!|jtFko-Uuw?0ylxucDK|;cjj7cnt zlUzl5JhpNzGMvsZ&Tf4l8(g|7aTF)yKkK#auAc!~?LDr|MMJDrlylJO4Dd!r4WLrI z=0fbpU~(6~ST9(w*E17@ZWD$Z1>hAQRSlhF*S$=rP&W+ligXdf6j!x<7WJPoy}-LEjQ1B{E@Ro9uNHo&UkRcQ8K0slv#@vsi_h?szK=M; z!=mPbpm)Y@v--hdj_PQq?oloXAYAj=P(fso(;n3biidb==3C;%`(bIm8S;07yYP7Q zZ@=JQX_vOo3WcY3qKLAKnbrj(oZ)J5P51}P&4!)qnn4sYhV-HuRlS%SXr6?z53_{R zen9e&%`PcnmVhy?`Nh-4D>ub@bK=#nkD) zU88zIr#gUMp{C81(OOhp^RkhVKY=2_7~4-A_$8PFN=v$rK2#Br^d2|5HGy*AQ?^K)5{Uu*x?FU`o9&D(hJCl*UJ&tudWSmp;bP2OA5K}* z3}*=X4ymc?K*O5y08G`3SFoguuXdqK3LPMjHZ*b7_u4<9lZjzUqqeJ<%FLPUWjC(W~xNj75{Yz-lWFN(e%RPaA0?gE5>lx9#!(l8_ms; z)os|NlgsFSA^399#`so!Z*l5w@;ktvjRs2yH~o11C@r^9tx=Lc|D#faik#H20Ca9J zZM_sW`2k}OUy(~WsP~JeaV2u9)0?2d0AL0 zgoYi*-1DMY?yTFZ#gmAYOpou!6c5+_6z^7>@$(P4E<5uHMEmv^v1bx&JdCedK6&ZS zgFN;)?iNnmUkJlQaqp`CB&*46;ef#@7ATQaH{`cMnaPRhj|scSLQ2cZPfhFc&G_-n z2&J|1TS0ujvJuB~Mp1MmH1w~Bhy_h<8UXX62~rvS;vaKeh&-K?{@9km)H0B6wI6zh zy;V)PCop)O!-QL}JnOTv>W(#0_(yGBjvi_kNc5)6>}=D{zkgZ1I(QjkQH9&~wko=( zW6uGGgOOYh`Z?H7;Q= z@bftD+>2oh^KO;RjF9)Zx)fQ-{aSqOnu-IbudMcZKXv+Gh5=(hl{&elS~2;g%s6pd z`3Gl1G9R%B!g|%hzrsR!`RN(kXYCD?7P9u8@2A(G%)5_AtT@w+Fz>#M;sW;jzWAt+_qP3JS_k^ia1dxsCYm2;l*L8m{H90*xEa_ z0Wu_6WXqoWF6)}>T9g^9RAC+nBPcD!pF_eY3ir744iqh9m9Zx-e!1-5KQe2`OXS=T%Bc2NMP_hQ8-CgOL(=GwWw`c@06JWMNoA2;d$W6mOJ zll>)3!qA*-?}TC4ERoLW&(ive&wSAQj&QQJ#@sg%aG_k5mi60^3%j!m(l-HD?EOE^ zxpZ@D+fPXI`Q-|f#d#6cZ^{S->9;YT5*Gcu4sx#b80iLrg{16Vw8)^*67!%E)_0i{u0 zUn3ZKk;o3bwW!v=+m`Ov&LV^4FwK`^oLr9lkTsKcswEaf-s|vRD5}Ayw##mi^N_b#+q)T-^<*nRHc=) zvjkF@P-%;6p3E*$=|0_PVq?Cq|g3{mdseieC{Nurb z2w?qB;+cP3hWwZG+s@ySmLLEA^Y3r;ALPlWznqAEFY|AS|NWOo zsb9bTN9MooNRQwDDfiE(f7y+|8fud z!wi3T1(E77fjBrGBQSG=y>dY8;5Tm8SBU?MTM#G5{~m$)FTbn*xCL?k6Mz{J_~`oc zD*!Xt?dl(&peON{0j$4r2mKR(`4{r{rT*6?=vQu;|GETmast50{x6Rp5AEksYSYOA zJ1dLjdwYAwd&jQhV~Zy^j@QljW|^i2E(lrFF>62s2*?-gW&NrhCgDmJab&b**yT~L zhD;H(LO7HWcRCWv+pTQp;@Kdih3l&FV3`Khd6CXw!KD(#P+u*k~?A%{3z5RLq zbMt%R?HrhgA1F^OEo$hL^YM1k{{4CMoX6Oi8wD7UTD$t zNWl1A+`VpjdtkdU`iis>S}Xeu#Sy z7!U1vv0xW}xfnfiHZt54Jub0}W&XD8i{y3Gmh(;2y>H0Im+K!+xXJGNiiNni@Gls(*in-Rc6Dmals=p?*p(2`nnC)RNGl5d@GO=j zPSbWwOtlW}aUQ0qbUlR+1gDYcf?9Y{@1VR5g58^Z4&`T5)3lSy@5kG?`llkcg^JiT~qqx=*I-psxrV~iW+ynhOe4kO)4c5F7PWp|3_eeu74JFDkF0MU_ z0ntmZ;AU=Np#iZDIr(R@2FO}Qqo~Uh;RE&v19sH#17vt^*iEN)s%~FnK5~z%F)L6{ za!)>AlZkv5G$DYJCaM!Nx~x22Hgo({U=L#M##o4?;)Pb^n_2r7lsTC4PfOIf_@3@K z58%u^Gb@*(xZXq?f<(PPE;GLVY$y#J(r>l&#RX`dv5PWXn$yWz2giSomaNH0|B{eW z&r{>f3;zSXG+WZYrxS2iQKOC@gSqUr@0HzVdehHkWL2DEeaueMW#Mx{f0w7XtXwiF zwB50chYH8K(0+CeG4mN|qwW~i%|g1FYQ1%j4NqLNolnIIQxl>tT4g@1i+Pcv>eX?4 z{~Y!N|5atxs?yu@vD8b1e9}D0I3yzl9?@o#2U_9@3>@Nv5*k~JQJ4hCmUfEvNS}vx zz#67KcKx%Y#CQAseHQN_;D@xOM)&Z@kHg%P>O=CBzCdeGSD>qAC59Bab9EwVi@cXO2UbcKcz2! zv`(q{Vi}8L#NT6N==x<_?Z-+3?-@DA(ejy~7w>?7fsyi;oXmSG?j&1 zijl#eqxQCAdMXYml}O?3@0{7*Id$Vc+i=5U4i;@}ToTizUIb)neTXf3ex5~BZK#kDr zd%f2jyCwNy14^%)&hzJOfM#-X**_onvwOft1fgjf!gC@I(^ppaeO!!DZ{PiZidGUG z&i$x?6b*~p4-F(`E(s>E4(tj00c&0hFZ;ou7G5Ltk2ftNFl;Pqg8B2s-8ZIZM7?Af zTqck~a%)itgls;d)b{<*$jR;h0h18%$@DrT#q2h=RQ%Q0Q8X|_v$#HNc1(gj>cBZ# zE1lRq&Mk(aj^Sn?7QG9+q1wlMW_`RMm?8NP1%)?cg|v`S;L!AIN8}CiNMT``!fTH} z!n?~=#(fE;yv{cK9$SNZQ(iZI7xfMg+nR%3%6FQaMMnqmjvTgmOpdh#G_Kjvt)ktjm1 z1bL=e8K~%iBPT`_8a4OJu&b!=S4V}5vxyTJ7i1aU_ zf}BE)`mZmwU-O0OFEM;HPpi*qPryOrgd9-W%qIL|*b(d3YuPLq?!4(xK;X&@DIwM# z43CQjK|yXH1|1gcjfTcb{tNsLeS==X%CA_0>RRV&c8~aO|>>p!-*OJ-{+kVqoKo#!`?E59@R$}t%HFfNV zY8z2X6ryvB&+7|=n_A(?eHk$5vVpFp@mlpxEu@8bOng4d(qZ^M=I4|Hre?#SCL7HV zZTU|X2QR3Tl*#Pbe5yOgW0MWoFe(yxLb?m$>qUmg6>1ol#_bBXUA>^2DtSH>DdUs$ zwBU|IR)_;?7wkvaCAkpewv4p|#fdP&>@=X5%s%dtz&TShMbAF>z_E5vjey-0TiGr; zkqh!0%vgw(KUXQMo^j+!E^{q+c9sOqXXzpZOr|^O=?LJxAWV;`9=Gn?wkM^OY%xx4 zIa8uUjr`tyvf{}QQsnR68v#px_AGM9yjhtW6aGBKn8hk0QHq}~tdmsgRhp}f@T4j0 z&j%Gx!qqpDU#)zIkfWSRST%{z>`dM^eu!VF-1GHcg6zECSg{{3zW*$ke4!f3iF-xd zhG*|CsvLRDt*Ca@=*L(4 z#L;k(FKoc6LX#5kbzq}4>M&>8$Am3&=@eG9bwfq&%4Mr?X9iXgxOsU4cr}(TC>`Xa zhc3}-MG~-h39qLy=sIr1fj3mw#g#2RF6X}}&Z^Z(ghHc=*tkGi>$67}7F!8aH6%fPoS#MjRMi+3TAE#-Tp@&tnlwbDC2N8a35ijA^UvD)?TfN zwl}}`pUWJ_6v)A8>_vm0<1w>u%t@d$kZkct+t_*t;5kT1T~I0x@O3+LP4u`O_~F$m zYWuVC*h2E)VI*XZJHy?9nFuKiskPC4ZCd|m{_exjyVXvrFW-*aRQr5gFWz{O6FN7x z3Qz_qFSKy5-rTP6(LWah#;@cmIn>#YPa!}V&%i(3@ z`S3!C9u9X|TV#97JJ>-;jGHu@5LFpDm=;bf4vKsb0_{I=W;{zq<{4G{(g3Ex_nw}; zEIj?dv#pr3%Fj7d;DmmQJ_SC0?Qfp#k@O}SzQ^l@`|8n8W5@yNS}RpQfDqXp%m)LJ zRiLn8E$ETN)6(kVFqpD>>6QCl!7sy8=zz^*X3;u;e+EPF{WnYx%hfUBP`Bp!Tixdz zx{V^%CsC@ZIZbu^*BI0JTtw2#jq&gkxZ0e)f)K<-07kKJwHb%vTDWpRQ=#37!f z_AC`ue}1_oDr7#5k@I89xqx|U=ny#cl8D(;8_wqvL{@qe`!QzP8<;3gS@fW4)JVtC zFr=wbEy_5&CcH?RSm3n+IacIU@FGjPr@d9axg|M|S#%eI7Z)KSA}>XjQsF2B$+e+v zWGIa^+!1aMa~X|l)&i|XmiaVhw!nzyK(;46VWD5lpWeDjP*( zQ!zs6lpq&EM$w@75!Hz#l#6E`?aphKYozvsxR5I_0?=B)v!qD9->Y!FUTHZHp+a^( zW7wjmI6Ti|#hFX?CsC1^D8r&9`ZBuF!ifFF@xfeD6P(UOd+&R`2-HhkFP@IRg*Zf zw<6K?INSz!4(-@y>Zi3IJ#|W@x?j9F!%KP>DoODIfD{fRgtiMu4C4$H`L(j(7%mI~h)c%(unvo(h)WE1(rlPjWL9syh0-Z8zc@sW zyWb4Yiw2F}zr1O1_&!xUXo}zJ7B5lP{C2H9nH>(fRX*{RQkm*v!1FJkbb}P)*4!&< zAkGrW1&w-=lg+WQ)#p|X2_$%Etk5}J6WHGI=eT=_``;wD>yzQG63Rj-L#uYiJ=J=fEF9b-%;U>JH6ka2c*zYn5g@aPKYzkF0BxT(x{X>mJar=lQi%%{IJzI z%WBqb6R5pOd~olJ4Hra1Y)E5yy~yMB(qthFX|eK{clKB}dUS)qigy_(#uKHCE(E8c zgG5w}r&BBi2^S`|M;ZG}2gAS`(nj996rq9xqK>+K26`^#7x+=Wy&AzYM%tj4QGxBS z>tjm#C$cw%G%Rp)2hZGz49cMnoxEF+D`p|Cc-r-Tfn2WK?PQXBn_EJjj;FD+l*7dz zeU`Q~;7`m>@P)C}bjBfGr|XUSEJ*Nv{5Co4wTJlAQvHX^n^<#-Z;V})JyP9+M15Cg z6l3qgSr;kQj^_ezTF?#!^OPA?#l30Ahd((*XS_Gg-Zv_>=^;+Zw&QS}Zkix2r#UN? za~IQUsHP`I>9ZwCjfvKZOIgfza=zOXx?^!p*{HPdyqXi_$lW&SVyam5%ZX!{u)F0Jn*41Fe*3Jj--Glr&UWqo9^NU;|+;bvbX(WhgfjvT}A)$TAxiL zT*g#KIOMpgs%ph^+sJ)Ul=r(nDEy~cZ~_X`&~dp{(u_OpM3?ReVlORlVJ!JZ#^ zqoq?$x2e^>R1Z$_<-Q#Zn`ku(W2f%8i7GT0cIl?jsTj*RvJq=)l>XEQUB{9z`HVM0 z!xLEp&!BnR?ft7X8AJbDDOZB93I3J* zq6Uqv6qc2lueq-V7deh%Q+G0~hqiRWaK;+)>r595HzPT=_yrH>zZxvzfZYXarXw+2;eAmAqC_i|hDi|U+En!-);w7mCEK8boDswtHR}WAk~?NC z`9W;X7Wh5t-8;-0OQ(vnS_F5xeKSK>?=)fcOSLblvZQ8b2*f79p3tw#%hKfu%tkzW zSH^D^QsxH>s+&I6f`EwE3LXY~kZmYlCrG4xm2l@7-}rF9CweNtSC*2SYDGz<9e#o7 z8|&@`WiXPjLf^H?Cq$n_soxuZ@<=n5RFBg>-)1?|TS`x9`TBL_!J5-JJx?D6-}>3r z&zq0W^qxgP()j2@wZRBL+L1!YdSk#tF$v+EG8rIMV7aVETy?pq*s1z=EkE4&3;Zn6 z2MBO~^>h{y{!L#PF@YsD-JaqrR$zsi`Bg zwW%xJKazjvSfd4RhmtC)l4^nX0cNdD?2L(n3?(bBDMR4v9hsqfWU3|4Fj=r za)TBAhJnAZfhmT+Vc@bruqB>gAg-tNv9tcgJN>sD0LTpjb60;W3*h1ca6Hiq-~a-^ zEYaWP*ja&(Jkx*I2Tpaz3Z_f`h5@)afsc&U-{jcXxxsYJ-!K3tknK-7PL9WX?Efwc z0CKPbpR^flejs?e^f$S`zC6*v3Icc4)3P9TZXkG%_TM_d_TUCHNq@&c9Bfbfg%tz@ zU)@tVFh}&yz6W>A6FD~EU*yo=^>T7>{vpQ+0&{GCZwuI#z^B*C4tV6?{w~J>0v`fa|Fau+<(9Sboz10%r$(+Akd7 z2L6G8NxDzGw8+@6@OF8+f4rPk~S5z-tBQ$=C(2A6(#ksK3_-o?o8Y=y5K2jFa&A z`rAfe4D@u2gE8=e`KKKC81HFW?x(hVTxXuHp;gnHbJL literal 0 HcmV?d00001 diff --git a/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx b/doc/cheatsheet/Pandas_Cheat_Sheet_JP.pptx new file mode 100644 index 0000000000000000000000000000000000000000..f8b98a6f1f8e4afab4de9892cd8cd8920c6538f7 GIT binary patch literal 105265 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 z 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 F{U7 Date: Mon, 30 Jul 2018 06:06:32 -0400 Subject: [PATCH 09/47] DOC: Fix spaces and brackets in ValueError message for option_context. (#22121) --- pandas/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/config.py b/pandas/core/config.py index 369e0568346ef1..abcdbfa12e4e96 100644 --- a/pandas/core/config.py +++ b/pandas/core/config.py @@ -391,7 +391,7 @@ class option_context(object): def __init__(self, *args): if not (len(args) % 2 == 0 and len(args) >= 2): raise ValueError('Need to invoke as' - 'option_context(pat, val, [(pat, val), ...)).') + ' option_context(pat, val, [(pat, val), ...]).') self.ops = list(zip(args[::2], args[1::2])) From cf14366d1653472c8b0fd9e6e735c6d6275db5b5 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 30 Jul 2018 03:11:48 -0700 Subject: [PATCH 10/47] CLN: Unused varables pt2 (#22115) --- pandas/core/arrays/interval.py | 1 - pandas/core/dtypes/dtypes.py | 1 - pandas/core/groupby/generic.py | 1 - pandas/core/indexes/base.py | 6 +++--- pandas/core/indexing.py | 8 -------- pandas/core/ops.py | 2 +- pandas/core/panel.py | 2 +- pandas/core/sparse/series.py | 6 ++++-- pandas/io/excel.py | 2 +- pandas/io/formats/format.py | 2 +- pandas/io/formats/html.py | 2 +- 11 files changed, 12 insertions(+), 21 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 928483005786a9..60464bcfda1e7b 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -814,7 +814,6 @@ def _format_data(self): summary = '[{head} ... {tail}]'.format( head=', '.join(head), tail=', '.join(tail)) else: - head = [] tail = [formatter(x) for x in self] summary = '[{tail}]'.format(tail=', '.join(tail)) diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 57b1d81d94754d..cf771a127a6966 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -305,7 +305,6 @@ def _hash_categories(categories, ordered=True): # everything to a str first, which means we treat # {'1', '2'} the same as {'1', 2} # find a better solution - cat_array = np.array([hash(x) for x in categories]) hashed = hash((tuple(categories), ordered)) return hashed cat_array = hash_array(np.asarray(categories), categorize=False) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 4c87f6122b9566..5b2590cfcf0100 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -134,7 +134,6 @@ def _cython_agg_blocks(self, how, alt=None, numeric_only=True, obj = self.obj[data.items[locs]] s = groupby(obj, self.grouper) result = s.aggregate(lambda x: alt(x, axis=self.axis)) - newb = result._data.blocks[0] finally: diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 8ad058c001bba6..2a191ef76473b0 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1647,11 +1647,11 @@ def is_int(v): # if we are mixed and have integers try: if is_positional and self.is_mixed(): - # TODO: i, j are not used anywhere + # Validate start & stop if start is not None: - i = self.get_loc(start) # noqa + self.get_loc(start) if stop is not None: - j = self.get_loc(stop) # noqa + self.get_loc(stop) is_positional = False except KeyError: if self.inferred_type == 'mixed-integer-float': diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 13c019dea469ac..80b3d579d54475 100755 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -788,11 +788,6 @@ def _align_frame(self, indexer, df): if isinstance(indexer, tuple): - aligners = [not com.is_null_slice(idx) for idx in indexer] - sum_aligners = sum(aligners) - # TODO: single_aligner is not used - single_aligner = sum_aligners == 1 # noqa - idx, cols = None, None sindexers = [] for i, ix in enumerate(indexer): @@ -865,9 +860,6 @@ def _align_frame(self, indexer, df): raise ValueError('Incompatible indexer with DataFrame') def _align_panel(self, indexer, df): - # TODO: is_frame, is_panel are unused - is_frame = self.obj.ndim == 2 # noqa - is_panel = self.obj.ndim >= 3 # noqa raise NotImplementedError("cannot set using an indexer with a Panel " "yet!") diff --git a/pandas/core/ops.py b/pandas/core/ops.py index c65d2dcdc478cc..6d407c41daea64 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -1789,7 +1789,7 @@ def na_op(x, y): def f(self, other, axis=None): # Validate the axis parameter if axis is not None: - axis = self._get_axis_number(axis) + self._get_axis_number(axis) if isinstance(other, self._constructor): return self._compare_constructor(other, na_op, try_cast=False) diff --git a/pandas/core/panel.py b/pandas/core/panel.py index 38b84ab685c3bf..fd27e3ba650ead 100644 --- a/pandas/core/panel.py +++ b/pandas/core/panel.py @@ -296,7 +296,7 @@ def _getitem_multilevel(self, key): if isinstance(loc, (slice, np.ndarray)): new_index = info[loc] result_index = maybe_droplevels(new_index, key) - slices = [loc] + [slice(None) for x in range(self._AXIS_LEN - 1)] + slices = [loc] + [slice(None)] * (self._AXIS_LEN - 1) new_values = self.values[slices] d = self._construct_axes_dict(self._AXIS_ORDERS[1:]) diff --git a/pandas/core/sparse/series.py b/pandas/core/sparse/series.py index 1a92a27bfb3906..8ac5d81f23bb20 100644 --- a/pandas/core/sparse/series.py +++ b/pandas/core/sparse/series.py @@ -624,8 +624,9 @@ def cumsum(self, axis=0, *args, **kwargs): cumsum : SparseSeries """ nv.validate_cumsum(args, kwargs) + # Validate axis if axis is not None: - axis = self._get_axis_number(axis) + self._get_axis_number(axis) new_array = self.values.cumsum() @@ -654,7 +655,8 @@ def dropna(self, axis=0, inplace=False, **kwargs): Analogous to Series.dropna. If fill_value=NaN, returns a dense Series """ # TODO: make more efficient - axis = self._get_axis_number(axis or 0) + # Validate axis + self._get_axis_number(axis or 0) dense_valid = self.to_dense().dropna() if inplace: raise NotImplementedError("Cannot perform inplace dropna" diff --git a/pandas/io/excel.py b/pandas/io/excel.py index 39131d390c69f3..e2db6643c5ef05 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -647,7 +647,7 @@ def _parse_cell(cell_contents, cell_typ): if header is not None: if is_list_like(header): header_names = [] - control_row = [True for x in data[0]] + control_row = [True] * len(data[0]) for row in header: if is_integer(skiprows): row += skiprows diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py index c6ca59aa08bf91..1ff06138768380 100644 --- a/pandas/io/formats/format.py +++ b/pandas/io/formats/format.py @@ -1567,7 +1567,7 @@ def get_level_lengths(levels, sentinel=''): if len(levels) == 0: return [] - control = [True for x in levels[0]] + control = [True] * len(levels[0]) result = [] for level in levels: diff --git a/pandas/io/formats/html.py b/pandas/io/formats/html.py index 3ea5cb95b9c5a3..a6b03c9c6dd236 100644 --- a/pandas/io/formats/html.py +++ b/pandas/io/formats/html.py @@ -369,7 +369,7 @@ def _write_regular_rows(self, fmt_values, indent): for i in range(nrows): if truncate_v and i == (self.fmt.tr_row_num): - str_sep_row = ['...' for ele in row] + str_sep_row = ['...'] * len(row) self.write_tr(str_sep_row, indent, self.indent_delta, tags=None, nindex_levels=1) From 9a8cebcba87af2e14bfea1fba5986eb5da2217de Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Mon, 30 Jul 2018 03:30:01 -0700 Subject: [PATCH 11/47] BUG/API: to_datetime preserves UTC offsets when parsing datetime strings (#21822) --- asv_bench/benchmarks/timeseries.py | 19 ++ doc/source/whatsnew/v0.24.0.txt | 57 ++++++ pandas/_libs/tslib.pyx | 174 +++++++++++++++--- pandas/core/dtypes/cast.py | 2 +- pandas/core/tools/datetimes.py | 20 +- pandas/tests/frame/test_to_csv.py | 13 +- .../indexes/datetimes/test_arithmetic.py | 6 +- .../tests/indexes/datetimes/test_timezones.py | 4 +- pandas/tests/indexes/datetimes/test_tools.py | 94 +++++++--- pandas/tests/reshape/test_concat.py | 4 +- pandas/tests/test_algos.py | 3 +- pandas/tests/test_base.py | 13 +- pandas/tests/test_resample.py | 4 +- pandas/tests/tslibs/test_array_to_datetime.py | 56 ++++-- 14 files changed, 369 insertions(+), 100 deletions(-) diff --git a/asv_bench/benchmarks/timeseries.py b/asv_bench/benchmarks/timeseries.py index eada401d2930b7..2c98cc16595199 100644 --- a/asv_bench/benchmarks/timeseries.py +++ b/asv_bench/benchmarks/timeseries.py @@ -343,6 +343,25 @@ def time_iso8601_tz_spaceformat(self): to_datetime(self.strings_tz_space) +class ToDatetimeNONISO8601(object): + + goal_time = 0.2 + + def setup(self): + N = 10000 + half = int(N / 2) + ts_string_1 = 'March 1, 2018 12:00:00+0400' + ts_string_2 = 'March 1, 2018 12:00:00+0500' + self.same_offset = [ts_string_1] * N + self.diff_offset = [ts_string_1] * half + [ts_string_2] * half + + def time_same_offset(self): + to_datetime(self.same_offset) + + def time_different_offset(self): + to_datetime(self.diff_offset) + + class ToDatetimeFormat(object): goal_time = 0.2 diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 42e286f487a7d7..d2d5d40393b62d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -224,6 +224,62 @@ For situations where you need an ``ndarray`` of ``Interval`` objects, use np.asarray(idx) idx.values.astype(object) +.. _whatsnew_0240.api.timezone_offset_parsing: + +Parsing Datetime Strings with Timezone Offsets +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, parsing datetime strings with UTC offsets with :func:`to_datetime` +or :class:`DatetimeIndex` would automatically convert the datetime to UTC +without timezone localization. This is inconsistent from parsing the same +datetime string with :class:`Timestamp` which would preserve the UTC +offset in the ``tz`` attribute. Now, :func:`to_datetime` preserves the UTC +offset in the ``tz`` attribute when all the datetime strings have the same +UTC offset (:issue:`17697`, :issue:`11736`) + +*Previous Behavior*: + +.. code-block:: ipython + + + In [2]: pd.to_datetime("2015-11-18 15:30:00+05:30") + Out[2]: Timestamp('2015-11-18 10:00:00') + + In [3]: pd.Timestamp("2015-11-18 15:30:00+05:30") + Out[3]: Timestamp('2015-11-18 15:30:00+0530', tz='pytz.FixedOffset(330)') + + # Different UTC offsets would automatically convert the datetimes to UTC (without a UTC timezone) + In [4]: pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"]) + Out[4]: DatetimeIndex(['2015-11-18 10:00:00', '2015-11-18 10:00:00'], dtype='datetime64[ns]', freq=None) + +*Current Behavior*: + +.. ipython:: python + + pd.to_datetime("2015-11-18 15:30:00+05:30") + pd.Timestamp("2015-11-18 15:30:00+05:30") + +Parsing datetime strings with the same UTC offset will preserve the UTC offset in the ``tz`` + +.. ipython:: python + + pd.to_datetime(["2015-11-18 15:30:00+05:30"] * 2) + +Parsing datetime strings with different UTC offsets will now create an Index of +``datetime.datetime`` objects with different UTC offsets + +.. ipython:: python + + idx = pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"]) + idx + idx[0] + idx[1] + +Passing ``utc=True`` will mimic the previous behavior but will correctly indicate +that the dates have been converted to UTC + +.. ipython:: python + pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) .. _whatsnew_0240.api.datetimelike.normalize: @@ -439,6 +495,7 @@ Datetimelike - Fixed bug where two :class:`DateOffset` objects with different ``normalize`` attributes could evaluate as equal (:issue:`21404`) - Fixed bug where :meth:`Timestamp.resolution` incorrectly returned 1-microsecond ``timedelta`` instead of 1-nanosecond :class:`Timedelta` (:issue:`21336`,:issue:`21365`) +- Bug in :func:`to_datetime` that did not consistently return an :class:`Index` when ``box=True`` was specified (:issue:`21864`) Timedelta ^^^^^^^^^ diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index acf6cd4b743624..76e3d6e92d31ef 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -19,6 +19,7 @@ import numpy as np cnp.import_array() import pytz +from dateutil.tz import tzlocal, tzutc as dateutil_utc from util cimport (is_integer_object, is_float_object, is_string_object, @@ -328,7 +329,7 @@ cpdef array_with_unit_to_datetime(ndarray values, unit, errors='coerce'): if unit == 'ns': if issubclass(values.dtype.type, np.integer): return values.astype('M8[ns]') - return array_to_datetime(values.astype(object), errors=errors) + return array_to_datetime(values.astype(object), errors=errors)[0] m = cast_from_unit(None, unit) @@ -457,9 +458,43 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', dayfirst=False, yearfirst=False, format=None, utc=None, require_iso8601=False): + """ + Converts a 1D array of date-like values to a numpy array of either: + 1) datetime64[ns] data + 2) datetime.datetime objects, if OutOfBoundsDatetime or TypeError + is encountered + + Also returns a pytz.FixedOffset if an array of strings with the same + timezone offset is passed and utc=True is not passed. Otherwise, None + is returned + + Handles datetime.date, datetime.datetime, np.datetime64 objects, numeric, + strings + + Parameters + ---------- + values : ndarray of object + date-like objects to convert + errors : str, default 'raise' + error behavior when parsing + dayfirst : bool, default False + dayfirst parsing behavior when encountering datetime strings + yearfirst : bool, default False + yearfirst parsing behavior when encountering datetime strings + format : str, default None + format of the string to parse + utc : bool, default None + indicator whether the dates should be UTC + require_iso8601 : bool, default False + indicator whether the datetime string should be iso8601 + + Returns + ------- + tuple (ndarray, tzoffset) + """ cdef: Py_ssize_t i, n = len(values) - object val, py_dt + object val, py_dt, tz, tz_out = None ndarray[int64_t] iresult ndarray[object] oresult npy_datetimestruct dts @@ -467,11 +502,14 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', bint seen_integer = 0 bint seen_string = 0 bint seen_datetime = 0 + bint seen_datetime_offset = 0 bint is_raise = errors=='raise' bint is_ignore = errors=='ignore' bint is_coerce = errors=='coerce' _TSObject _ts int out_local=0, out_tzoffset=0 + float offset_seconds + set out_tzoffset_vals = set() # specify error conditions assert is_raise or is_ignore or is_coerce @@ -584,7 +622,7 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', raise ValueError("time data {val} doesn't match " "format specified" .format(val=val)) - return values + return values, tz_out try: py_dt = parse_datetime_string(val, dayfirst=dayfirst, @@ -595,6 +633,30 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', continue raise TypeError("invalid string coercion to datetime") + # If the dateutil parser returned tzinfo, capture it + # to check if all arguments have the same tzinfo + tz = py_dt.tzinfo + if tz is not None: + seen_datetime_offset = 1 + if tz == dateutil_utc(): + # dateutil.tz.tzutc has no offset-like attribute + # Just add the 0 offset explicitly + out_tzoffset_vals.add(0) + elif tz == tzlocal(): + # is comparison fails unlike other dateutil.tz + # objects. Also, dateutil.tz.tzlocal has no + # _offset attribute like tzoffset + offset_seconds = tz._dst_offset.total_seconds() + out_tzoffset_vals.add(offset_seconds) + else: + # dateutil.tz.tzoffset objects cannot be hashed + # store the total_seconds() instead + offset_seconds = tz._offset.total_seconds() + out_tzoffset_vals.add(offset_seconds) + else: + # Add a marker for naive string, to track if we are + # parsing mixed naive and aware strings + out_tzoffset_vals.add('naive') try: _ts = convert_datetime_to_tsobject(py_dt, None) iresult[i] = _ts.value @@ -614,8 +676,17 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', # where we left off value = dtstruct_to_dt64(&dts) if out_local == 1: + seen_datetime_offset = 1 + # Store the out_tzoffset in seconds + # since we store the total_seconds of + # dateutil.tz.tzoffset objects + out_tzoffset_vals.add(out_tzoffset * 60.) tz = pytz.FixedOffset(out_tzoffset) value = tz_convert_single(value, tz, 'UTC') + else: + # Add a marker for naive string, to track if we are + # parsing mixed naive and aware strings + out_tzoffset_vals.add('naive') iresult[i] = value try: check_dts_bounds(&dts) @@ -631,7 +702,7 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', raise ValueError("time data {val} doesn't " "match format specified" .format(val=val)) - return values + return values, tz_out raise else: @@ -657,7 +728,21 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', else: raise TypeError - return result + if seen_datetime_offset and not utc_convert: + # GH 17697 + # 1) If all the offsets are equal, return one offset for + # the parsed dates to (maybe) pass to DatetimeIndex + # 2) If the offsets are different, then force the parsing down the + # object path where an array of datetimes + # (with individual dateutil.tzoffsets) are returned + is_same_offsets = len(out_tzoffset_vals) == 1 + if not is_same_offsets: + return array_to_datetime_object(values, is_raise, + dayfirst, yearfirst) + else: + tz_offset = out_tzoffset_vals.pop() + tz_out = pytz.FixedOffset(tz_offset / 60.) + return result, tz_out except OutOfBoundsDatetime: if is_raise: raise @@ -679,36 +764,67 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', oresult[i] = val.item() else: oresult[i] = val - return oresult + return oresult, tz_out except TypeError: - oresult = np.empty(n, dtype=object) + return array_to_datetime_object(values, is_raise, dayfirst, yearfirst) - for i in range(n): - val = values[i] - if checknull_with_nat(val): - oresult[i] = val - elif is_string_object(val): - if len(val) == 0 or val in nat_strings: - oresult[i] = 'NaT' - continue +cdef array_to_datetime_object(ndarray[object] values, bint is_raise, + dayfirst=False, yearfirst=False): + """ + Fall back function for array_to_datetime - try: - oresult[i] = parse_datetime_string(val, dayfirst=dayfirst, - yearfirst=yearfirst) - pydatetime_to_dt64(oresult[i], &dts) - check_dts_bounds(&dts) - except Exception: - if is_raise: - raise - return values - # oresult[i] = val - else: + Attempts to parse datetime strings with dateutil to return an array + of datetime objects + + Parameters + ---------- + values : ndarray of object + date-like objects to convert + is_raise : bool + error behavior when parsing + dayfirst : bool, default False + dayfirst parsing behavior when encountering datetime strings + yearfirst : bool, default False + yearfirst parsing behavior when encountering datetime strings + + Returns + ------- + tuple (ndarray, None) + """ + cdef: + Py_ssize_t i, n = len(values) + object val, + ndarray[object] oresult + npy_datetimestruct dts + + oresult = np.empty(n, dtype=object) + + # We return an object array and only attempt to parse: + # 1) NaT or NaT-like values + # 2) datetime strings, which we return as datetime.datetime + for i in range(n): + val = values[i] + if checknull_with_nat(val): + oresult[i] = val + elif is_string_object(val): + if len(val) == 0 or val in nat_strings: + oresult[i] = 'NaT' + continue + try: + oresult[i] = parse_datetime_string(val, dayfirst=dayfirst, + yearfirst=yearfirst) + pydatetime_to_dt64(oresult[i], &dts) + check_dts_bounds(&dts) + except (ValueError, OverflowError): if is_raise: raise - return values - - return oresult + return values, None + else: + if is_raise: + raise + return values, None + return oresult, None cdef inline bint _parse_today_now(str val, int64_t* iresult): diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index ead7b39309f5e8..e369679d2146f2 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -918,7 +918,7 @@ def try_datetime(v): # GH19671 v = tslib.array_to_datetime(v, require_iso8601=True, - errors='raise') + errors='raise')[0] except ValueError: # we might have a sequence of the same-datetimes with tz's diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py index be042c9bf8ab01..90a083557a6624 100644 --- a/pandas/core/tools/datetimes.py +++ b/pandas/core/tools/datetimes.py @@ -23,7 +23,8 @@ is_float, is_list_like, is_scalar, - is_numeric_dtype) + is_numeric_dtype, + is_object_dtype) from pandas.core.dtypes.generic import ( ABCIndexClass, ABCSeries, ABCDataFrame) @@ -266,7 +267,7 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None, result = arg if result is None and (format is None or infer_datetime_format): - result = tslib.array_to_datetime( + result, tz_parsed = tslib.array_to_datetime( arg, errors=errors, utc=tz == 'utc', @@ -274,9 +275,16 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None, yearfirst=yearfirst, require_iso8601=require_iso8601 ) + if tz_parsed is not None and box: + return DatetimeIndex._simple_new(result, name=name, + tz=tz_parsed) - if is_datetime64_dtype(result) and box: - result = DatetimeIndex(result, tz=tz, name=name) + if box: + if is_datetime64_dtype(result): + return DatetimeIndex(result, tz=tz, name=name) + elif is_object_dtype(result): + from pandas import Index + return Index(result, name=name) return result except ValueError as e: @@ -404,7 +412,7 @@ def to_datetime(arg, errors='raise', dayfirst=False, yearfirst=False, datetime.datetime objects as well). box : boolean, default True - - If True returns a DatetimeIndex + - If True returns a DatetimeIndex or Index-like object - If False returns ndarray of values. format : string, default None strftime to parse time, eg "%d/%m/%Y", note that "%f" will parse @@ -696,7 +704,7 @@ def calc(carg): parsed = parsing.try_parse_year_month_day(carg / 10000, carg / 100 % 100, carg % 100) - return tslib.array_to_datetime(parsed, errors=errors) + return tslib.array_to_datetime(parsed, errors=errors)[0] def calc_with_mask(carg, mask): result = np.empty(carg.shape, dtype='M8[ns]') diff --git a/pandas/tests/frame/test_to_csv.py b/pandas/tests/frame/test_to_csv.py index 3ad25ae73109ee..9e3b606f319738 100644 --- a/pandas/tests/frame/test_to_csv.py +++ b/pandas/tests/frame/test_to_csv.py @@ -154,7 +154,7 @@ def test_to_csv_from_csv5(self): self.tzframe.to_csv(path) result = pd.read_csv(path, index_col=0, parse_dates=['A']) - converter = lambda c: to_datetime(result[c]).dt.tz_localize( + converter = lambda c: to_datetime(result[c]).dt.tz_convert( 'UTC').dt.tz_convert(self.tzframe[c].dt.tz) result['B'] = converter('B') result['C'] = converter('C') @@ -1027,12 +1027,11 @@ def test_to_csv_with_dst_transitions(self): time_range = np.array(range(len(i)), dtype='int64') df = DataFrame({'A': time_range}, index=i) df.to_csv(path, index=True) - # we have to reconvert the index as we # don't parse the tz's result = read_csv(path, index_col=0) - result.index = to_datetime(result.index).tz_localize( - 'UTC').tz_convert('Europe/London') + result.index = to_datetime(result.index, utc=True).tz_convert( + 'Europe/London') assert_frame_equal(result, df) # GH11619 @@ -1043,9 +1042,9 @@ def test_to_csv_with_dst_transitions(self): with ensure_clean('csv_date_format_with_dst') as path: df.to_csv(path, index=True) result = read_csv(path, index_col=0) - result.index = to_datetime(result.index).tz_localize( - 'UTC').tz_convert('Europe/Paris') - result['idx'] = to_datetime(result['idx']).astype( + result.index = to_datetime(result.index, utc=True).tz_convert( + 'Europe/Paris') + result['idx'] = to_datetime(result['idx'], utc=True).astype( 'datetime64[ns, Europe/Paris]') assert_frame_equal(result, df) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 4ce2b1dd4fd862..1e54e6563d5983 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -423,13 +423,13 @@ def test_dti_shift_tzaware(self, tz_naive_fixture): tm.assert_index_equal(idx.shift(0, freq='H'), idx) tm.assert_index_equal(idx.shift(3, freq='H'), idx) - idx = pd.DatetimeIndex(['2011-01-01 10:00', '2011-01-01 11:00' + idx = pd.DatetimeIndex(['2011-01-01 10:00', '2011-01-01 11:00', '2011-01-01 12:00'], name='xxx', tz=tz) tm.assert_index_equal(idx.shift(0, freq='H'), idx) - exp = pd.DatetimeIndex(['2011-01-01 13:00', '2011-01-01 14:00' + exp = pd.DatetimeIndex(['2011-01-01 13:00', '2011-01-01 14:00', '2011-01-01 15:00'], name='xxx', tz=tz) tm.assert_index_equal(idx.shift(3, freq='H'), exp) - exp = pd.DatetimeIndex(['2011-01-01 07:00', '2011-01-01 08:00' + exp = pd.DatetimeIndex(['2011-01-01 07:00', '2011-01-01 08:00', '2011-01-01 09:00'], name='xxx', tz=tz) tm.assert_index_equal(idx.shift(-3, freq='H'), exp) diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 3697d183d2fc68..67eb81336f6480 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -317,8 +317,8 @@ def test_dti_tz_localize_nonexistent_raise_coerce(self): result = index.tz_localize(tz=tz, errors='coerce') test_times = ['2015-03-08 01:00-05:00', 'NaT', '2015-03-08 03:00-04:00'] - dti = DatetimeIndex(test_times) - expected = dti.tz_localize('UTC').tz_convert('US/Eastern') + dti = to_datetime(test_times, utc=True) + expected = dti.tz_convert('US/Eastern') tm.assert_index_equal(result, expected) @pytest.mark.parametrize('tz', [pytz.timezone('US/Eastern'), diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py index fa9f9fc90387a3..72e5358f219667 100644 --- a/pandas/tests/indexes/datetimes/test_tools.py +++ b/pandas/tests/indexes/datetimes/test_tools.py @@ -7,6 +7,7 @@ import dateutil import numpy as np from dateutil.parser import parse +from dateutil.tz.tz import tzoffset from datetime import datetime, time from distutils.version import LooseVersion @@ -483,7 +484,7 @@ def test_to_datetime_tz_psycopg2(self, cache): # dtype coercion i = pd.DatetimeIndex([ - '2000-01-01 08:00:00+00:00' + '2000-01-01 08:00:00' ], tz=psycopg2.tz.FixedOffsetTimezone(offset=-300, name=None)) assert is_datetime64_ns_dtype(i) @@ -577,6 +578,48 @@ def test_week_without_day_and_calendar_year(self, date, format): with tm.assert_raises_regex(ValueError, msg): pd.to_datetime(date, format=format) + def test_iso_8601_strings_with_same_offset(self): + # GH 17697, 11736 + ts_str = "2015-11-18 15:30:00+05:30" + result = to_datetime(ts_str) + expected = Timestamp(ts_str) + assert result == expected + + expected = DatetimeIndex([Timestamp(ts_str)] * 2) + result = to_datetime([ts_str] * 2) + tm.assert_index_equal(result, expected) + + result = DatetimeIndex([ts_str] * 2) + tm.assert_index_equal(result, expected) + + def test_iso_8601_strings_with_different_offsets(self): + # GH 17697, 11736 + ts_strings = ["2015-11-18 15:30:00+05:30", + "2015-11-18 16:30:00+06:30", + NaT] + result = to_datetime(ts_strings) + expected = np.array([datetime(2015, 11, 18, 15, 30, + tzinfo=tzoffset(None, 19800)), + datetime(2015, 11, 18, 16, 30, + tzinfo=tzoffset(None, 23400)), + NaT], + dtype=object) + # GH 21864 + expected = Index(expected) + tm.assert_index_equal(result, expected) + + result = to_datetime(ts_strings, utc=True) + expected = DatetimeIndex([Timestamp(2015, 11, 18, 10), + Timestamp(2015, 11, 18, 10), + NaT], tz='UTC') + tm.assert_index_equal(result, expected) + + def test_non_iso_strings_with_tz_offset(self): + result = to_datetime(['March 1, 2018 12:00:00+0400'] * 2) + expected = DatetimeIndex([datetime(2018, 3, 1, 12, + tzinfo=pytz.FixedOffset(240))] * 2) + tm.assert_index_equal(result, expected) + class TestToDatetimeUnit(object): @pytest.mark.parametrize('cache', [True, False]) @@ -978,14 +1021,19 @@ def test_to_datetime_types(self, cache): # assert result == expected @pytest.mark.parametrize('cache', [True, False]) - def test_to_datetime_unprocessable_input(self, cache): + @pytest.mark.parametrize('box, klass, assert_method', [ + [True, Index, 'assert_index_equal'], + [False, np.array, 'assert_numpy_array_equal'] + ]) + def test_to_datetime_unprocessable_input(self, cache, box, klass, + assert_method): # GH 4928 - tm.assert_numpy_array_equal( - to_datetime([1, '1'], errors='ignore', cache=cache), - np.array([1, '1'], dtype='O') - ) + # GH 21864 + result = to_datetime([1, '1'], errors='ignore', cache=cache, box=box) + expected = klass(np.array([1, '1'], dtype='O')) + getattr(tm, assert_method)(result, expected) pytest.raises(TypeError, to_datetime, [1, '1'], errors='raise', - cache=cache) + cache=cache, box=box) def test_to_datetime_other_datetime64_units(self): # 5/25/2012 @@ -1031,7 +1079,7 @@ def test_string_na_nat_conversion(self, cache): else: expected[i] = parse_date(val) - result = tslib.array_to_datetime(strings) + result = tslib.array_to_datetime(strings)[0] tm.assert_almost_equal(result, expected) result2 = to_datetime(strings, cache=cache) @@ -1046,7 +1094,9 @@ def test_string_na_nat_conversion(self, cache): cache=cache)) result = to_datetime(malformed, errors='ignore', cache=cache) - tm.assert_numpy_array_equal(result, malformed) + # GH 21864 + expected = Index(malformed) + tm.assert_index_equal(result, expected) pytest.raises(ValueError, to_datetime, malformed, errors='raise', cache=cache) @@ -1495,23 +1545,19 @@ def test_parsers_time(self): assert res == expected_arr @pytest.mark.parametrize('cache', [True, False]) - def test_parsers_timezone_minute_offsets_roundtrip(self, cache): + @pytest.mark.parametrize('dt_string, tz, dt_string_repr', [ + ('2013-01-01 05:45+0545', pytz.FixedOffset(345), + "Timestamp('2013-01-01 05:45:00+0545', tz='pytz.FixedOffset(345)')"), + ('2013-01-01 05:30+0530', pytz.FixedOffset(330), + "Timestamp('2013-01-01 05:30:00+0530', tz='pytz.FixedOffset(330)')")]) + def test_parsers_timezone_minute_offsets_roundtrip(self, cache, dt_string, + tz, dt_string_repr): # GH11708 base = to_datetime("2013-01-01 00:00:00", cache=cache) - dt_strings = [ - ('2013-01-01 05:45+0545', - "Asia/Katmandu", - "Timestamp('2013-01-01 05:45:00+0545', tz='Asia/Katmandu')"), - ('2013-01-01 05:30+0530', - "Asia/Kolkata", - "Timestamp('2013-01-01 05:30:00+0530', tz='Asia/Kolkata')") - ] - - for dt_string, tz, dt_string_repr in dt_strings: - dt_time = to_datetime(dt_string, cache=cache) - assert base == dt_time - converted_time = dt_time.tz_localize('UTC').tz_convert(tz) - assert dt_string_repr == repr(converted_time) + base = base.tz_localize('UTC').tz_convert(tz) + dt_time = to_datetime(dt_string, cache=cache) + assert base == dt_time + assert dt_string_repr == repr(dt_time) @pytest.fixture(params=['D', 's', 'ms', 'us', 'ns']) diff --git a/pandas/tests/reshape/test_concat.py b/pandas/tests/reshape/test_concat.py index a59836eb70d24a..762b04cc3bd4fb 100644 --- a/pandas/tests/reshape/test_concat.py +++ b/pandas/tests/reshape/test_concat.py @@ -2324,7 +2324,7 @@ def test_concat_datetime_timezone(self): '2011-01-01 01:00:00+01:00', '2011-01-01 02:00:00+01:00'], freq='H' - ).tz_localize('UTC').tz_convert('Europe/Paris') + ).tz_convert('UTC').tz_convert('Europe/Paris') expected = pd.DataFrame([[1, 1], [2, 2], [3, 3]], index=exp_idx, columns=['a', 'b']) @@ -2342,7 +2342,7 @@ def test_concat_datetime_timezone(self): '2010-12-31 23:00:00+00:00', '2011-01-01 00:00:00+00:00', '2011-01-01 01:00:00+00:00'] - ).tz_localize('UTC') + ) expected = pd.DataFrame([[np.nan, 1], [np.nan, 2], [np.nan, 3], [1, np.nan], [2, np.nan], [3, np.nan]], diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 7ce2aaf7d7fbbd..796c6374343538 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -332,7 +332,8 @@ def test_datetime64_dtype_array_returned(self): dt_index = pd.to_datetime(['2015-01-03T00:00:00.000000000+0000', '2015-01-01T00:00:00.000000000+0000', - '2015-01-01T00:00:00.000000000+0000']) + '2015-01-01T00:00:00.000000000+0000'], + box=False) result = algos.unique(dt_index) tm.assert_numpy_array_equal(result, expected) assert result.dtype == expected.dtype diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py index 31e5bd88523d2c..b7530da36ed8bc 100644 --- a/pandas/tests/test_base.py +++ b/pandas/tests/test_base.py @@ -668,16 +668,15 @@ def test_value_counts_datetime64(self, klass): s = klass(df['dt'].copy()) s.name = None - - idx = pd.to_datetime(['2010-01-01 00:00:00Z', - '2008-09-09 00:00:00Z', - '2009-01-01 00:00:00Z']) + idx = pd.to_datetime(['2010-01-01 00:00:00', + '2008-09-09 00:00:00', + '2009-01-01 00:00:00']) expected_s = Series([3, 2, 1], index=idx) tm.assert_series_equal(s.value_counts(), expected_s) - expected = np_array_datetime64_compat(['2010-01-01 00:00:00Z', - '2009-01-01 00:00:00Z', - '2008-09-09 00:00:00Z'], + expected = np_array_datetime64_compat(['2010-01-01 00:00:00', + '2009-01-01 00:00:00', + '2008-09-09 00:00:00'], dtype='datetime64[ns]') if isinstance(s, Index): tm.assert_index_equal(s.unique(), DatetimeIndex(expected)) diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index d664a9060b6840..1f70d09e43b378 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -2681,8 +2681,8 @@ def test_resample_with_dst_time_change(self): '2016-03-14 13:00:00-05:00', '2016-03-15 01:00:00-05:00', '2016-03-15 13:00:00-05:00'] - index = pd.DatetimeIndex(expected_index_values, - tz='UTC').tz_convert('America/Chicago') + index = pd.to_datetime(expected_index_values, utc=True).tz_convert( + 'America/Chicago') expected = pd.DataFrame([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0], index=index) diff --git a/pandas/tests/tslibs/test_array_to_datetime.py b/pandas/tests/tslibs/test_array_to_datetime.py index eb77e52e7c91d1..915687304bfe2b 100644 --- a/pandas/tests/tslibs/test_array_to_datetime.py +++ b/pandas/tests/tslibs/test_array_to_datetime.py @@ -3,6 +3,8 @@ import numpy as np import pytest +import pytz +from dateutil.tz.tz import tzoffset from pandas._libs import tslib from pandas.compat.numpy import np_array_datetime64_compat @@ -52,7 +54,7 @@ def test_parsers_iso8601_invalid(self, date_str): class TestArrayToDatetime(object): def test_parsing_valid_dates(self): arr = np.array(['01-01-2013', '01-02-2013'], dtype=object) - result = tslib.array_to_datetime(arr) + result, _ = tslib.array_to_datetime(arr) expected = ['2013-01-01T00:00:00.000000000-0000', '2013-01-02T00:00:00.000000000-0000'] tm.assert_numpy_array_equal( @@ -60,38 +62,60 @@ def test_parsing_valid_dates(self): np_array_datetime64_compat(expected, dtype='M8[ns]')) arr = np.array(['Mon Sep 16 2013', 'Tue Sep 17 2013'], dtype=object) - result = tslib.array_to_datetime(arr) + result, _ = tslib.array_to_datetime(arr) expected = ['2013-09-16T00:00:00.000000000-0000', '2013-09-17T00:00:00.000000000-0000'] tm.assert_numpy_array_equal( result, np_array_datetime64_compat(expected, dtype='M8[ns]')) - @pytest.mark.parametrize('dt_string', [ - '01-01-2013 08:00:00+08:00', - '2013-01-01T08:00:00.000000000+0800', - '2012-12-31T16:00:00.000000000-0800', - '12-31-2012 23:00:00-01:00']) - def test_parsing_timezone_offsets(self, dt_string): + @pytest.mark.parametrize('dt_string, expected_tz', [ + ['01-01-2013 08:00:00+08:00', pytz.FixedOffset(480)], + ['2013-01-01T08:00:00.000000000+0800', pytz.FixedOffset(480)], + ['2012-12-31T16:00:00.000000000-0800', pytz.FixedOffset(-480)], + ['12-31-2012 23:00:00-01:00', pytz.FixedOffset(-60)]]) + def test_parsing_timezone_offsets(self, dt_string, expected_tz): # All of these datetime strings with offsets are equivalent # to the same datetime after the timezone offset is added arr = np.array(['01-01-2013 00:00:00'], dtype=object) - expected = tslib.array_to_datetime(arr) + expected, _ = tslib.array_to_datetime(arr) arr = np.array([dt_string], dtype=object) - result = tslib.array_to_datetime(arr) + result, result_tz = tslib.array_to_datetime(arr) tm.assert_numpy_array_equal(result, expected) + assert result_tz is expected_tz + + def test_parsing_non_iso_timezone_offset(self): + dt_string = '01-01-2013T00:00:00.000000000+0000' + arr = np.array([dt_string], dtype=object) + result, result_tz = tslib.array_to_datetime(arr) + expected = np.array([np.datetime64('2013-01-01 00:00:00.000000000')]) + tm.assert_numpy_array_equal(result, expected) + assert result_tz is pytz.FixedOffset(0) + + def test_parsing_different_timezone_offsets(self): + # GH 17697 + data = ["2015-11-18 15:30:00+05:30", "2015-11-18 15:30:00+06:30"] + data = np.array(data, dtype=object) + result, result_tz = tslib.array_to_datetime(data) + expected = np.array([datetime(2015, 11, 18, 15, 30, + tzinfo=tzoffset(None, 19800)), + datetime(2015, 11, 18, 15, 30, + tzinfo=tzoffset(None, 23400))], + dtype=object) + tm.assert_numpy_array_equal(result, expected) + assert result_tz is None def test_number_looking_strings_not_into_datetime(self): # GH#4601 # These strings don't look like datetimes so they shouldn't be # attempted to be converted arr = np.array(['-352.737091', '183.575577'], dtype=object) - result = tslib.array_to_datetime(arr, errors='ignore') + result, _ = tslib.array_to_datetime(arr, errors='ignore') tm.assert_numpy_array_equal(result, arr) arr = np.array(['1', '2', '3', '4', '5'], dtype=object) - result = tslib.array_to_datetime(arr, errors='ignore') + result, _ = tslib.array_to_datetime(arr, errors='ignore') tm.assert_numpy_array_equal(result, arr) @pytest.mark.parametrize('invalid_date', [ @@ -105,13 +129,13 @@ def test_coerce_outside_ns_bounds(self, invalid_date): with pytest.raises(ValueError): tslib.array_to_datetime(arr, errors='raise') - result = tslib.array_to_datetime(arr, errors='coerce') + result, _ = tslib.array_to_datetime(arr, errors='coerce') expected = np.array([tslib.iNaT], dtype='M8[ns]') tm.assert_numpy_array_equal(result, expected) def test_coerce_outside_ns_bounds_one_valid(self): arr = np.array(['1/1/1000', '1/1/2000'], dtype=object) - result = tslib.array_to_datetime(arr, errors='coerce') + result, _ = tslib.array_to_datetime(arr, errors='coerce') expected = [tslib.iNaT, '2000-01-01T00:00:00.000000000-0000'] tm.assert_numpy_array_equal( @@ -123,11 +147,11 @@ def test_coerce_of_invalid_datetimes(self): # Without coercing, the presence of any invalid dates prevents # any values from being converted - result = tslib.array_to_datetime(arr, errors='ignore') + result, _ = tslib.array_to_datetime(arr, errors='ignore') tm.assert_numpy_array_equal(result, arr) # With coercing, the invalid dates becomes iNaT - result = tslib.array_to_datetime(arr, errors='coerce') + result, _ = tslib.array_to_datetime(arr, errors='coerce') expected = ['2013-01-01T00:00:00.000000000-0000', tslib.iNaT, tslib.iNaT] From 26b3e7de56fdebaf1842b01d747a1a30a126c54a Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Mon, 30 Jul 2018 03:31:35 -0700 Subject: [PATCH 12/47] Remove unused cimports, fix #22067 (#22087) --- pandas/_libs/hashing.pyx | 2 +- pandas/_libs/tslibs/frequencies.pyx | 2 -- pandas/_libs/tslibs/parsing.pyx | 4 +--- pandas/_libs/tslibs/period.pyx | 4 ++-- pandas/_libs/tslibs/resolution.pyx | 2 -- pandas/_libs/tslibs/strptime.pyx | 2 -- pandas/_libs/window.pyx | 10 +++++----- pandas/_libs/writers.pyx | 2 -- pandas/io/msgpack/_packer.pyx | 16 +++++++++++----- pandas/io/msgpack/_unpacker.pyx | 16 ++++++++++++---- pandas/tests/indexes/datetimes/test_datetime.py | 11 +++++++++++ 11 files changed, 43 insertions(+), 28 deletions(-) diff --git a/pandas/_libs/hashing.pyx b/pandas/_libs/hashing.pyx index 4489847518a1d5..ff92ee306288a8 100644 --- a/pandas/_libs/hashing.pyx +++ b/pandas/_libs/hashing.pyx @@ -3,7 +3,7 @@ # at https://github.com/veorq/SipHash import cython -cimport numpy as cnp + import numpy as np from numpy cimport ndarray, uint8_t, uint32_t, uint64_t diff --git a/pandas/_libs/tslibs/frequencies.pyx b/pandas/_libs/tslibs/frequencies.pyx index 7803595badee1e..5c8efa8c037125 100644 --- a/pandas/_libs/tslibs/frequencies.pyx +++ b/pandas/_libs/tslibs/frequencies.pyx @@ -2,8 +2,6 @@ # cython: profile=False import re -cimport cython - cimport numpy as cnp cnp.import_array() diff --git a/pandas/_libs/tslibs/parsing.pyx b/pandas/_libs/tslibs/parsing.pyx index 580d155f87fa82..ffa3d8df44be80 100644 --- a/pandas/_libs/tslibs/parsing.pyx +++ b/pandas/_libs/tslibs/parsing.pyx @@ -14,9 +14,7 @@ from cpython.datetime cimport datetime import time import numpy as np -cimport numpy as cnp -from numpy cimport int64_t, ndarray -cnp.import_array() +from numpy cimport ndarray # Avoid import from outside _libs if sys.version_info.major == 2: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 4054154cd285b9..65fb0f331d039f 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1920,8 +1920,8 @@ class Period(_Period): return cls._from_ordinal(ordinal, freq) -cdef int64_t _ordinal_from_fields(year, month, quarter, day, - hour, minute, second, freq): +cdef int64_t _ordinal_from_fields(int year, int month, quarter, int day, + int hour, int minute, int second, freq): base, mult = get_freq_code(freq) if quarter is not None: year, month = quarter_to_myear(year, quarter, freq) diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 4b90c669eebba3..0659e2a553e7e9 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -5,9 +5,7 @@ cimport cython from cython cimport Py_ssize_t import numpy as np -cimport numpy as cnp from numpy cimport ndarray, int64_t, int32_t -cnp.import_array() from util cimport is_string_object, get_nat diff --git a/pandas/_libs/tslibs/strptime.pyx b/pandas/_libs/tslibs/strptime.pyx index a843a8e2b56124..de2b7440156a76 100644 --- a/pandas/_libs/tslibs/strptime.pyx +++ b/pandas/_libs/tslibs/strptime.pyx @@ -25,8 +25,6 @@ import pytz from cython cimport Py_ssize_t from cpython cimport PyFloat_Check -cimport cython - import numpy as np from numpy cimport ndarray, int64_t diff --git a/pandas/_libs/window.pyx b/pandas/_libs/window.pyx index 6453b5ed2ab3a4..efc8a02014bc06 100644 --- a/pandas/_libs/window.pyx +++ b/pandas/_libs/window.pyx @@ -9,7 +9,7 @@ from libc.stdlib cimport malloc, free import numpy as np cimport numpy as cnp -from numpy cimport ndarray, double_t, int64_t, float64_t +from numpy cimport ndarray, double_t, int64_t, float64_t, float32_t cnp.import_array() @@ -25,11 +25,11 @@ from skiplist cimport (skiplist_t, skiplist_init, skiplist_destroy, skiplist_get, skiplist_insert, skiplist_remove) -cdef cnp.float32_t MINfloat32 = np.NINF -cdef cnp.float64_t MINfloat64 = np.NINF +cdef float32_t MINfloat32 = np.NINF +cdef float64_t MINfloat64 = np.NINF -cdef cnp.float32_t MAXfloat32 = np.inf -cdef cnp.float64_t MAXfloat64 = np.inf +cdef float32_t MAXfloat32 = np.inf +cdef float64_t MAXfloat64 = np.inf cdef double NaN = np.NaN diff --git a/pandas/_libs/writers.pyx b/pandas/_libs/writers.pyx index 77d8ca81258a05..041eb59812ae3d 100644 --- a/pandas/_libs/writers.pyx +++ b/pandas/_libs/writers.pyx @@ -12,9 +12,7 @@ except ImportError: from cpython cimport PyUnicode_GET_SIZE as PyString_GET_SIZE import numpy as np -cimport numpy as cnp from numpy cimport ndarray, uint8_t -cnp.import_array() ctypedef fused pandas_string: diff --git a/pandas/io/msgpack/_packer.pyx b/pandas/io/msgpack/_packer.pyx index c81069c8e04c05..d67c632188e629 100644 --- a/pandas/io/msgpack/_packer.pyx +++ b/pandas/io/msgpack/_packer.pyx @@ -1,10 +1,16 @@ # coding: utf-8 # cython: embedsignature=True -from cpython cimport * -from libc.stdlib cimport * -from libc.string cimport * -from libc.limits cimport * +from cpython cimport ( + PyFloat_Check, PyLong_Check, PyInt_Check, + PyDict_CheckExact, PyDict_Check, + PyTuple_Check, PyList_Check, + PyCallable_Check, + PyUnicode_Check, PyBytes_Check, + PyBytes_AsString, + PyBytes_FromStringAndSize, + PyUnicode_AsEncodedString) +from libc.stdlib cimport free, malloc from pandas.io.msgpack.exceptions import PackValueError from pandas.io.msgpack import ExtType @@ -74,7 +80,7 @@ cdef class Packer(object): cdef object _berrors cdef char *encoding cdef char *unicode_errors - cdef bool use_float + cdef bint use_float cdef bint autoreset def __cinit__(self): diff --git a/pandas/io/msgpack/_unpacker.pyx b/pandas/io/msgpack/_unpacker.pyx index 427414b80dfe49..0c50aa5e68103d 100644 --- a/pandas/io/msgpack/_unpacker.pyx +++ b/pandas/io/msgpack/_unpacker.pyx @@ -1,15 +1,23 @@ # coding: utf-8 # cython: embedsignature=True -from cpython cimport * +from cython cimport Py_ssize_t + +from cpython cimport ( + PyCallable_Check, + PyBUF_SIMPLE, PyObject_GetBuffer, PyBuffer_Release, + PyBytes_Size, + PyBytes_FromStringAndSize, + PyBytes_AsString) + cdef extern from "Python.h": ctypedef struct PyObject cdef int PyObject_AsReadBuffer(object o, const void** buff, Py_ssize_t* buf_len) except -1 -from libc.stdlib cimport * -from libc.string cimport * -from libc.limits cimport * +from libc.stdlib cimport free, malloc +from libc.string cimport memcpy, memmove +from libc.limits cimport INT_MAX from pandas.io.msgpack.exceptions import (BufferFull, OutOfData, UnpackValueError, ExtraData) diff --git a/pandas/tests/indexes/datetimes/test_datetime.py b/pandas/tests/indexes/datetimes/test_datetime.py index 1a5f12103595c7..2adf09924a5091 100644 --- a/pandas/tests/indexes/datetimes/test_datetime.py +++ b/pandas/tests/indexes/datetimes/test_datetime.py @@ -1,4 +1,5 @@ import warnings +import sys import pytest @@ -126,6 +127,16 @@ def test_map(self): exp = Index([f(x) for x in rng], dtype=' Date: Mon, 30 Jul 2018 15:08:22 -0400 Subject: [PATCH 13/47] DOC: read_html typo for displayed_only arg (#22134) --- pandas/io/html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/html.py b/pandas/io/html.py index 45fe3b017e4f69..cca27db00f48d0 100644 --- a/pandas/io/html.py +++ b/pandas/io/html.py @@ -1029,7 +1029,7 @@ def read_html(io, match='.+', flavor=None, header=None, index_col=None, .. versionadded:: 0.19.0 - display_only : bool, default True + displayed_only : bool, default True Whether elements with "display: none" should be parsed .. versionadded:: 0.23.0 From 7c67d9c990465953cac19427a44cdfc0210747a3 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 31 Jul 2018 05:23:41 -0700 Subject: [PATCH 14/47] Centralize m8[ns] Arithmetic Tests (#22118) --- .../indexes/timedeltas/test_arithmetic.py | 72 -- pandas/tests/series/test_arithmetic.py | 349 +------- pandas/tests/test_arithmetic.py | 753 +++++++++++++++++- 3 files changed, 743 insertions(+), 431 deletions(-) diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index d47d75d2f3485d..a5e75de2a267ec 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import operator import pytest import numpy as np @@ -13,7 +12,6 @@ Series, Timestamp, Timedelta) from pandas.errors import PerformanceWarning, NullFrequencyError -from pandas.core import ops @pytest.fixture(params=[pd.offsets.Hour(2), timedelta(hours=2), @@ -270,53 +268,6 @@ def test_tdi_floordiv_timedelta_scalar(self, scalar_td): class TestTimedeltaIndexArithmetic(object): # Addition and Subtraction Operations - # ------------------------------------------------------------- - # Invalid Operations - - @pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])]) - @pytest.mark.parametrize('op', [operator.add, ops.radd, - operator.sub, ops.rsub]) - def test_tdi_add_sub_float(self, op, other): - dti = DatetimeIndex(['2011-01-01', '2011-01-02'], freq='D') - tdi = dti - dti.shift(1) - with pytest.raises(TypeError): - op(tdi, other) - - def test_tdi_add_str_invalid(self): - # GH 13624 - tdi = TimedeltaIndex(['1 day', '2 days']) - - with pytest.raises(TypeError): - tdi + 'a' - with pytest.raises(TypeError): - 'a' + tdi - - @pytest.mark.parametrize('freq', [None, 'H']) - def test_tdi_sub_period(self, freq): - # GH#13078 - # not supported, check TypeError - p = pd.Period('2011-01-01', freq='D') - - idx = pd.TimedeltaIndex(['1 hours', '2 hours'], freq=freq) - - with pytest.raises(TypeError): - idx - p - - with pytest.raises(TypeError): - p - idx - - @pytest.mark.parametrize('op', [operator.add, ops.radd, - operator.sub, ops.rsub]) - @pytest.mark.parametrize('pi_freq', ['D', 'W', 'Q', 'H']) - @pytest.mark.parametrize('tdi_freq', [None, 'H']) - def test_dti_sub_pi(self, tdi_freq, pi_freq, op): - # GH#20049 subtracting PeriodIndex should raise TypeError - tdi = pd.TimedeltaIndex(['1 hours', '2 hours'], freq=tdi_freq) - dti = pd.Timestamp('2018-03-07 17:16:40') + tdi - pi = dti.to_period(pi_freq) - with pytest.raises(TypeError): - op(dti, pi) - # ------------------------------------------------------------- # TimedeltaIndex.shift is used by __add__/__sub__ @@ -626,29 +577,6 @@ def test_tdi_isub_timedeltalike(self, delta): rng -= delta tm.assert_index_equal(rng, expected) - # ------------------------------------------------------------- - # Binary operations TimedeltaIndex and datetime-like - - def test_tdi_sub_timestamp_raises(self): - idx = TimedeltaIndex(['1 day', '2 day']) - msg = "cannot subtract a datelike from a TimedeltaIndex" - with tm.assert_raises_regex(TypeError, msg): - idx - Timestamp('2011-01-01') - - def test_tdi_add_timestamp(self): - idx = TimedeltaIndex(['1 day', '2 day']) - - result = idx + Timestamp('2011-01-01') - expected = DatetimeIndex(['2011-01-02', '2011-01-03']) - tm.assert_index_equal(result, expected) - - def test_tdi_radd_timestamp(self): - idx = TimedeltaIndex(['1 day', '2 day']) - - result = Timestamp('2011-01-01') + idx - expected = DatetimeIndex(['2011-01-02', '2011-01-03']) - tm.assert_index_equal(result, expected) - # ------------------------------------------------------------- # __add__/__sub__ with ndarray[datetime64] and ndarray[timedelta64] diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index 2571498ca802ce..c091df63fcfc7c 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -6,21 +6,13 @@ import numpy as np import pytest -from pandas import Series, Timestamp, Timedelta, Period, NaT +from pandas import Series, Timestamp, Period from pandas._libs.tslibs.period import IncompatibleFrequency import pandas as pd import pandas.util.testing as tm -@pytest.fixture -def tdser(): - """ - Return a Series with dtype='timedelta64[ns]', including a NaT. - """ - return Series(['59 Days', '59 Days', 'NaT'], dtype='timedelta64[ns]') - - # ------------------------------------------------------------------ # Comparisons @@ -552,342 +544,3 @@ def test_dt64ser_sub_datetime_dtype(self): ser = Series([ts]) result = pd.to_timedelta(np.abs(ser - dt)) assert result.dtype == 'timedelta64[ns]' - - -class TestTimedeltaSeriesAdditionSubtraction(object): - # Tests for Series[timedelta64[ns]] __add__, __sub__, __radd__, __rsub__ - - # ------------------------------------------------------------------ - # Operations with int-like others - - def test_td64series_add_int_series_invalid(self, tdser): - with pytest.raises(TypeError): - tdser + Series([2, 3, 4]) - - @pytest.mark.xfail(reason='GH#19123 integer interpreted as nanoseconds') - def test_td64series_radd_int_series_invalid(self, tdser): - with pytest.raises(TypeError): - Series([2, 3, 4]) + tdser - - def test_td64series_sub_int_series_invalid(self, tdser): - with pytest.raises(TypeError): - tdser - Series([2, 3, 4]) - - @pytest.mark.xfail(reason='GH#19123 integer interpreted as nanoseconds') - def test_td64series_rsub_int_series_invalid(self, tdser): - with pytest.raises(TypeError): - Series([2, 3, 4]) - tdser - - def test_td64_series_add_intlike(self): - # GH#19123 - tdi = pd.TimedeltaIndex(['59 days', '59 days', 'NaT']) - ser = Series(tdi) - - other = Series([20, 30, 40], dtype='uint8') - - pytest.raises(TypeError, ser.__add__, 1) - pytest.raises(TypeError, ser.__sub__, 1) - - pytest.raises(TypeError, ser.__add__, other) - pytest.raises(TypeError, ser.__sub__, other) - - pytest.raises(TypeError, ser.__add__, other.values) - pytest.raises(TypeError, ser.__sub__, other.values) - - pytest.raises(TypeError, ser.__add__, pd.Index(other)) - pytest.raises(TypeError, ser.__sub__, pd.Index(other)) - - @pytest.mark.parametrize('scalar', [1, 1.5, np.array(2)]) - def test_td64series_add_sub_numeric_scalar_invalid(self, scalar, tdser): - with pytest.raises(TypeError): - tdser + scalar - with pytest.raises(TypeError): - scalar + tdser - with pytest.raises(TypeError): - tdser - scalar - with pytest.raises(TypeError): - scalar - tdser - - @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', - 'uint64', 'uint32', 'uint16', 'uint8', - 'float64', 'float32', 'float16']) - @pytest.mark.parametrize('vector', [ - np.array([1, 2, 3]), - pd.Index([1, 2, 3]), - pytest.param(Series([1, 2, 3]), - marks=pytest.mark.xfail(reason='GH#19123 integer ' - 'interpreted as nanos')) - ]) - def test_td64series_add_sub_numeric_array_invalid(self, vector, - dtype, tdser): - vector = vector.astype(dtype) - with pytest.raises(TypeError): - tdser + vector - with pytest.raises(TypeError): - vector + tdser - with pytest.raises(TypeError): - tdser - vector - with pytest.raises(TypeError): - vector - tdser - - # ------------------------------------------------------------------ - # Operations with datetime-like others - - def test_td64series_add_sub_timestamp(self): - # GH#11925 - tdser = Series(pd.timedelta_range('1 day', periods=3)) - ts = Timestamp('2012-01-01') - expected = Series(pd.date_range('2012-01-02', periods=3)) - tm.assert_series_equal(ts + tdser, expected) - tm.assert_series_equal(tdser + ts, expected) - - expected2 = Series(pd.date_range('2011-12-31', periods=3, freq='-1D')) - tm.assert_series_equal(ts - tdser, expected2) - tm.assert_series_equal(ts + (-tdser), expected2) - - with pytest.raises(TypeError): - tdser - ts - - # ------------------------------------------------------------------ - # Operations with timedelta-like others (including DateOffsets) - - @pytest.mark.parametrize('names', [(None, None, None), - ('Egon', 'Venkman', None), - ('NCC1701D', 'NCC1701D', 'NCC1701D')]) - def test_td64_series_with_tdi(self, names): - # GH#17250 make sure result dtype is correct - # GH#19043 make sure names are propagated correctly - tdi = pd.TimedeltaIndex(['0 days', '1 day'], name=names[0]) - ser = Series([Timedelta(hours=3), Timedelta(hours=4)], name=names[1]) - expected = Series([Timedelta(hours=3), Timedelta(days=1, hours=4)], - name=names[2]) - - result = tdi + ser - tm.assert_series_equal(result, expected) - assert result.dtype == 'timedelta64[ns]' - - result = ser + tdi - tm.assert_series_equal(result, expected) - assert result.dtype == 'timedelta64[ns]' - - expected = Series([Timedelta(hours=-3), Timedelta(days=1, hours=-4)], - name=names[2]) - - result = tdi - ser - tm.assert_series_equal(result, expected) - assert result.dtype == 'timedelta64[ns]' - - result = ser - tdi - tm.assert_series_equal(result, -expected) - assert result.dtype == 'timedelta64[ns]' - - def test_td64_sub_NaT(self): - # GH#18808 - ser = Series([NaT, Timedelta('1s')]) - res = ser - NaT - expected = Series([NaT, NaT], dtype='timedelta64[ns]') - tm.assert_series_equal(res, expected) - - -class TestTimedeltaSeriesMultiplicationDivision(object): - # Tests for Series[timedelta64[ns]] - # __mul__, __rmul__, __div__, __rdiv__, __floordiv__, __rfloordiv__ - - # ------------------------------------------------------------------ - # __floordiv__, __rfloordiv__ - - @pytest.mark.parametrize('scalar_td', [ - timedelta(minutes=5, seconds=4), - Timedelta('5m4s'), - Timedelta('5m4s').to_timedelta64()]) - def test_timedelta_floordiv(self, scalar_td): - # GH#18831 - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - - result = td1 // scalar_td - expected = Series([0, 0, np.nan]) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('scalar_td', [ - timedelta(minutes=5, seconds=4), - Timedelta('5m4s'), - Timedelta('5m4s').to_timedelta64()]) - def test_timedelta_rfloordiv(self, scalar_td): - # GH#18831 - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - result = scalar_td // td1 - expected = Series([1, 1, np.nan]) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('scalar_td', [ - timedelta(minutes=5, seconds=4), - Timedelta('5m4s'), - Timedelta('5m4s').to_timedelta64()]) - def test_timedelta_rfloordiv_explicit(self, scalar_td): - # GH#18831 - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - - # We can test __rfloordiv__ using this syntax, - # see `test_timedelta_rfloordiv` - result = td1.__rfloordiv__(scalar_td) - expected = Series([1, 1, np.nan]) - tm.assert_series_equal(result, expected) - - # ------------------------------------------------------------------ - # Operations with int-like others - - @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', - 'uint64', 'uint32', 'uint16', 'uint8', - 'float64', 'float32', 'float16']) - @pytest.mark.parametrize('vector', [np.array([20, 30, 40]), - pd.Index([20, 30, 40]), - Series([20, 30, 40])]) - def test_td64series_div_numeric_array(self, vector, dtype, tdser): - # GH#4521 - # divide/multiply by integers - vector = vector.astype(dtype) - expected = Series(['2.95D', '1D 23H 12m', 'NaT'], - dtype='timedelta64[ns]') - - result = tdser / vector - tm.assert_series_equal(result, expected) - - with pytest.raises(TypeError): - vector / tdser - - @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', - 'uint64', 'uint32', 'uint16', 'uint8', - 'float64', 'float32', 'float16']) - @pytest.mark.parametrize('vector', [np.array([20, 30, 40]), - pd.Index([20, 30, 40]), - Series([20, 30, 40])]) - def test_td64series_mul_numeric_array(self, vector, dtype, tdser): - # GH#4521 - # divide/multiply by integers - vector = vector.astype(dtype) - - expected = Series(['1180 Days', '1770 Days', 'NaT'], - dtype='timedelta64[ns]') - - result = tdser * vector - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', - 'uint64', 'uint32', 'uint16', 'uint8', - 'float64', 'float32', 'float16']) - @pytest.mark.parametrize('vector', [ - np.array([20, 30, 40]), - pytest.param(pd.Index([20, 30, 40]), - marks=pytest.mark.xfail(reason='__mul__ raises ' - 'instead of returning ' - 'NotImplemented')), - Series([20, 30, 40]) - ]) - def test_td64series_rmul_numeric_array(self, vector, dtype, tdser): - # GH#4521 - # divide/multiply by integers - vector = vector.astype(dtype) - - expected = Series(['1180 Days', '1770 Days', 'NaT'], - dtype='timedelta64[ns]') - - result = vector * tdser - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('one', [1, np.array(1), 1.0, np.array(1.0)]) - def test_td64series_mul_numeric_scalar(self, one, tdser): - # GH#4521 - # divide/multiply by integers - expected = Series(['-59 Days', '-59 Days', 'NaT'], - dtype='timedelta64[ns]') - - result = tdser * (-one) - tm.assert_series_equal(result, expected) - result = (-one) * tdser - tm.assert_series_equal(result, expected) - - expected = Series(['118 Days', '118 Days', 'NaT'], - dtype='timedelta64[ns]') - - result = tdser * (2 * one) - tm.assert_series_equal(result, expected) - result = (2 * one) * tdser - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('two', [ - 2, 2.0, - pytest.param(np.array(2), - marks=pytest.mark.xfail(reason='GH#19011 is_list_like ' - 'incorrectly True.')), - pytest.param(np.array(2.0), - marks=pytest.mark.xfail(reason='GH#19011 is_list_like ' - 'incorrectly True.')), - ]) - def test_td64series_div_numeric_scalar(self, two, tdser): - # GH#4521 - # divide/multiply by integers - expected = Series(['29.5D', '29.5D', 'NaT'], dtype='timedelta64[ns]') - - result = tdser / two - tm.assert_series_equal(result, expected) - - # ------------------------------------------------------------------ - # Operations with timedelta-like others - - @pytest.mark.parametrize('names', [(None, None, None), - ('Egon', 'Venkman', None), - ('NCC1701D', 'NCC1701D', 'NCC1701D')]) - def test_tdi_mul_int_series(self, names): - # GH#19042 - tdi = pd.TimedeltaIndex(['0days', '1day', '2days', '3days', '4days'], - name=names[0]) - ser = Series([0, 1, 2, 3, 4], dtype=np.int64, name=names[1]) - - expected = Series(['0days', '1day', '4days', '9days', '16days'], - dtype='timedelta64[ns]', - name=names[2]) - - result = ser * tdi - tm.assert_series_equal(result, expected) - - # The direct operation tdi * ser still needs to be fixed. - result = ser.__rmul__(tdi) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('names', [(None, None, None), - ('Egon', 'Venkman', None), - ('NCC1701D', 'NCC1701D', 'NCC1701D')]) - def test_float_series_rdiv_tdi(self, names): - # GH#19042 - # TODO: the direct operation TimedeltaIndex / Series still - # needs to be fixed. - tdi = pd.TimedeltaIndex(['0days', '1day', '2days', '3days', '4days'], - name=names[0]) - ser = Series([1.5, 3, 4.5, 6, 7.5], dtype=np.float64, name=names[1]) - - expected = Series([tdi[n] / ser[n] for n in range(len(ser))], - dtype='timedelta64[ns]', - name=names[2]) - - result = ser.__rdiv__(tdi) - tm.assert_series_equal(result, expected) - - @pytest.mark.parametrize('scalar_td', [ - timedelta(minutes=5, seconds=4), - Timedelta('5m4s'), - Timedelta('5m4s').to_timedelta64()]) - def test_td64series_mul_timedeltalike_invalid(self, scalar_td): - td1 = Series([timedelta(minutes=5, seconds=3)] * 3) - td1.iloc[2] = np.nan - - # check that we are getting a TypeError - # with 'operate' (from core/ops.py) for the ops that are not - # defined - pattern = 'operate|unsupported|cannot|not supported' - with tm.assert_raises_regex(TypeError, pattern): - td1 * scalar_td - with tm.assert_raises_regex(TypeError, pattern): - scalar_td * td1 diff --git a/pandas/tests/test_arithmetic.py b/pandas/tests/test_arithmetic.py index f15b629f15ae38..8ee0bf9ec874ad 100644 --- a/pandas/tests/test_arithmetic.py +++ b/pandas/tests/test_arithmetic.py @@ -2,6 +2,7 @@ # Arithmetc tests for DataFrame/Series/Index/Array classes that should # behave identically. from datetime import timedelta +import operator import pytest import numpy as np @@ -9,7 +10,22 @@ import pandas as pd import pandas.util.testing as tm -from pandas import Timedelta +from pandas.core import ops +from pandas.errors import NullFrequencyError +from pandas._libs.tslibs import IncompatibleFrequency +from pandas import ( + Timedelta, Timestamp, NaT, Series, TimedeltaIndex, DatetimeIndex) + + +# ------------------------------------------------------------------ +# Fixtures + +@pytest.fixture +def tdser(): + """ + Return a Series with dtype='timedelta64[ns]', including a NaT. + """ + return Series(['59 Days', '59 Days', 'NaT'], dtype='timedelta64[ns]') # ------------------------------------------------------------------ @@ -19,7 +35,7 @@ class TestNumericArraylikeArithmeticWithTimedeltaScalar(object): @pytest.mark.parametrize('box', [ pd.Index, - pd.Series, + Series, pytest.param(pd.DataFrame, marks=pytest.mark.xfail(reason="block.eval incorrect", strict=True)) @@ -35,10 +51,10 @@ class TestNumericArraylikeArithmeticWithTimedeltaScalar(object): Timedelta(days=1).to_timedelta64(), Timedelta(days=1).to_pytimedelta()], ids=lambda x: type(x).__name__) - def test_index_mul_timedelta(self, scalar_td, index, box): + def test_numeric_arr_mul_tdscalar(self, scalar_td, index, box): # GH#19333 - if (box is pd.Series and + if (box is Series and type(scalar_td) is timedelta and index.dtype == 'f8'): raise pytest.xfail(reason="Cannot multiply timedelta by float") @@ -53,7 +69,7 @@ def test_index_mul_timedelta(self, scalar_td, index, box): commute = scalar_td * index tm.assert_equal(commute, expected) - @pytest.mark.parametrize('box', [pd.Index, pd.Series, pd.DataFrame]) + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame]) @pytest.mark.parametrize('index', [ pd.Int64Index(range(1, 3)), pd.UInt64Index(range(1, 3)), @@ -65,14 +81,14 @@ def test_index_mul_timedelta(self, scalar_td, index, box): Timedelta(days=1).to_timedelta64(), Timedelta(days=1).to_pytimedelta()], ids=lambda x: type(x).__name__) - def test_index_rdiv_timedelta(self, scalar_td, index, box): + def test_numeric_arr_rdiv_tdscalar(self, scalar_td, index, box): - if box is pd.Series and type(scalar_td) is timedelta: + if box is Series and type(scalar_td) is timedelta: raise pytest.xfail(reason="TODO: Figure out why this case fails") if box is pd.DataFrame and isinstance(scalar_td, timedelta): raise pytest.xfail(reason="TODO: Figure out why this case fails") - expected = pd.TimedeltaIndex(['1 Day', '12 Hours']) + expected = TimedeltaIndex(['1 Day', '12 Hours']) index = tm.box_expected(index, box) expected = tm.box_expected(expected, box) @@ -87,12 +103,727 @@ def test_index_rdiv_timedelta(self, scalar_td, index, box): # ------------------------------------------------------------------ # Timedelta64[ns] dtype Arithmetic Operations +class TestTimedeltaArraylikeAddSubOps(object): + # Tests for timedelta64[ns] __add__, __sub__, __radd__, __rsub__ + + # ------------------------------------------------------------- + # Invalid Operations + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + def test_td64arr_add_str_invalid(self, box): + # GH#13624 + tdi = TimedeltaIndex(['1 day', '2 days']) + tdi = tm.box_expected(tdi, box) + + with pytest.raises(TypeError): + tdi + 'a' + with pytest.raises(TypeError): + 'a' + tdi + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + @pytest.mark.parametrize('other', [3.14, np.array([2.0, 3.0])]) + @pytest.mark.parametrize('op', [operator.add, ops.radd, + operator.sub, ops.rsub], + ids=lambda x: x.__name__) + def test_td64arr_add_sub_float(self, box, op, other): + tdi = TimedeltaIndex(['-1 days', '-1 days']) + tdi = tm.box_expected(tdi, box) + + if box is pd.DataFrame and op in [operator.add, operator.sub]: + pytest.xfail(reason="Tries to align incorrectly, " + "raises ValueError") + + with pytest.raises(TypeError): + op(tdi, other) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Tries to cast df to " + "Period", + strict=True, + raises=IncompatibleFrequency)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('freq', [None, 'H']) + def test_td64arr_sub_period(self, box, freq): + # GH#13078 + # not supported, check TypeError + p = pd.Period('2011-01-01', freq='D') + idx = TimedeltaIndex(['1 hours', '2 hours'], freq=freq) + idx = tm.box_expected(idx, box) + + with pytest.raises(TypeError): + idx - p + + with pytest.raises(TypeError): + p - idx + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="broadcasts along " + "wrong axis", + raises=ValueError, + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('pi_freq', ['D', 'W', 'Q', 'H']) + @pytest.mark.parametrize('tdi_freq', [None, 'H']) + def test_td64arr_sub_pi(self, box, tdi_freq, pi_freq): + # GH#20049 subtracting PeriodIndex should raise TypeError + tdi = TimedeltaIndex(['1 hours', '2 hours'], freq=tdi_freq) + dti = Timestamp('2018-03-07 17:16:40') + tdi + pi = dti.to_period(pi_freq) + + # TODO: parametrize over box for pi? + tdi = tm.box_expected(tdi, box) + with pytest.raises(TypeError): + tdi - pi + + # ------------------------------------------------------------- + # Binary operations td64 arraylike and datetime-like + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + def test_td64arr_sub_timestamp_raises(self, box): + idx = TimedeltaIndex(['1 day', '2 day']) + idx = tm.box_expected(idx, box) + + msg = "cannot subtract a datelike from|Could not operate" + with tm.assert_raises_regex(TypeError, msg): + idx - Timestamp('2011-01-01') + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns object dtype", + strict=True)) + ], ids=lambda x: x.__name__) + def test_td64arr_add_timestamp(self, box): + idx = TimedeltaIndex(['1 day', '2 day']) + expected = DatetimeIndex(['2011-01-02', '2011-01-03']) + + idx = tm.box_expected(idx, box) + expected = tm.box_expected(expected, box) + + result = idx + Timestamp('2011-01-01') + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns object dtype", + strict=True)) + ], ids=lambda x: x.__name__) + def test_td64_radd_timestamp(self, box): + idx = TimedeltaIndex(['1 day', '2 day']) + expected = DatetimeIndex(['2011-01-02', '2011-01-03']) + + idx = tm.box_expected(idx, box) + expected = tm.box_expected(expected, box) + + # TODO: parametrize over scalar datetime types? + result = Timestamp('2011-01-01') + idx + tm.assert_equal(result, expected) + + # ------------------------------------------------------------------ + # Operations with int-like others + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Attempts to broadcast " + "incorrectly", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + def test_td64arr_add_int_series_invalid(self, box, tdser): + tdser = tm.box_expected(tdser, box) + err = TypeError if box is not pd.Index else NullFrequencyError + with pytest.raises(err): + tdser + Series([2, 3, 4]) + + @pytest.mark.parametrize('box', [ + pd.Index, + pytest.param(Series, + marks=pytest.mark.xfail(reason="GH#19123 integer " + "interpreted as " + "nanoseconds", + strict=True)), + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Attempts to broadcast " + "incorrectly", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + def test_td64arr_radd_int_series_invalid(self, box, tdser): + tdser = tm.box_expected(tdser, box) + err = TypeError if box is not pd.Index else NullFrequencyError + with pytest.raises(err): + Series([2, 3, 4]) + tdser + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Attempts to broadcast " + "incorrectly", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + def test_td64arr_sub_int_series_invalid(self, box, tdser): + tdser = tm.box_expected(tdser, box) + err = TypeError if box is not pd.Index else NullFrequencyError + with pytest.raises(err): + tdser - Series([2, 3, 4]) + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + @pytest.mark.xfail(reason='GH#19123 integer interpreted as nanoseconds', + strict=True) + def test_td64arr_rsub_int_series_invalid(self, box, tdser): + tdser = tm.box_expected(tdser, box) + with pytest.raises(TypeError): + Series([2, 3, 4]) - tdser + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Tries to broadcast " + "incorrectly", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + def test_td64arr_add_intlike(self, box): + # GH#19123 + tdi = TimedeltaIndex(['59 days', '59 days', 'NaT']) + ser = tm.box_expected(tdi, box) + err = TypeError if box is not pd.Index else NullFrequencyError + + other = Series([20, 30, 40], dtype='uint8') + + # TODO: separate/parametrize + with pytest.raises(err): + ser + 1 + with pytest.raises(err): + ser - 1 + + with pytest.raises(err): + ser + other + with pytest.raises(err): + ser - other + + with pytest.raises(err): + ser + np.array(other) + with pytest.raises(err): + ser - np.array(other) + + with pytest.raises(err): + ser + pd.Index(other) + with pytest.raises(err): + ser - pd.Index(other) + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + @pytest.mark.parametrize('scalar', [1, 1.5, np.array(2)]) + def test_td64arr_add_sub_numeric_scalar_invalid(self, box, scalar, tdser): + + if box is pd.DataFrame and isinstance(scalar, np.ndarray): + # raises ValueError + pytest.xfail(reason="DataFrame to broadcast incorrectly") + + tdser = tm.box_expected(tdser, box) + err = TypeError + if box is pd.Index and not isinstance(scalar, float): + err = NullFrequencyError + + with pytest.raises(err): + tdser + scalar + with pytest.raises(err): + scalar + tdser + with pytest.raises(err): + tdser - scalar + with pytest.raises(err): + scalar - tdser + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Tries to broadcast " + "incorrectly", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', + 'uint64', 'uint32', 'uint16', 'uint8', + 'float64', 'float32', 'float16']) + @pytest.mark.parametrize('vec', [ + np.array([1, 2, 3]), + pd.Index([1, 2, 3]), + Series([1, 2, 3]) + # TODO: Add DataFrame in here? + ], ids=lambda x: type(x).__name__) + def test_td64arr_add_sub_numeric_arr_invalid(self, box, vec, dtype, tdser): + if type(vec) is Series and not dtype.startswith('float'): + pytest.xfail(reason='GH#19123 integer interpreted as nanos') + + tdser = tm.box_expected(tdser, box) + err = TypeError + if box is pd.Index and not dtype.startswith('float'): + err = NullFrequencyError + + vector = vec.astype(dtype) + # TODO: parametrize over these four ops? + with pytest.raises(err): + tdser + vector + with pytest.raises(err): + vector + tdser + with pytest.raises(err): + tdser - vector + with pytest.raises(err): + vector - tdser + + # ------------------------------------------------------------------ + # Operations with datetime-like others + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns object dtype " + "instead of " + "datetime64[ns]", + strict=True)) + ], ids=lambda x: x.__name__) + def test_td64arr_add_sub_timestamp(self, box): + # GH#11925 + ts = Timestamp('2012-01-01') + # TODO: parametrize over types of datetime scalar? + + tdser = Series(pd.timedelta_range('1 day', periods=3)) + expected = Series(pd.date_range('2012-01-02', periods=3)) + + tdser = tm.box_expected(tdser, box) + expected = tm.box_expected(expected, box) + + tm.assert_equal(ts + tdser, expected) + tm.assert_equal(tdser + ts, expected) + + expected2 = Series(pd.date_range('2011-12-31', + periods=3, freq='-1D')) + expected2 = tm.box_expected(expected2, box) + + tm.assert_equal(ts - tdser, expected2) + tm.assert_equal(ts + (-tdser), expected2) + + with pytest.raises(TypeError): + tdser - ts + + # ------------------------------------------------------------------ + # Operations with timedelta-like others (including DateOffsets) + + # TODO: parametrize over [add, sub, radd, rsub]? + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Tries to broadcast " + "incorrectly leading " + "to alignment error", + strict=True, raises=ValueError)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('names', [(None, None, None), + ('Egon', 'Venkman', None), + ('NCC1701D', 'NCC1701D', 'NCC1701D')]) + def test_td64arr_add_sub_tdi(self, box, names): + # GH#17250 make sure result dtype is correct + # GH#19043 make sure names are propagated correctly + tdi = TimedeltaIndex(['0 days', '1 day'], name=names[0]) + ser = Series([Timedelta(hours=3), Timedelta(hours=4)], name=names[1]) + expected = Series([Timedelta(hours=3), Timedelta(days=1, hours=4)], + name=names[2]) + + ser = tm.box_expected(ser, box) + expected = tm.box_expected(expected, box) + + result = tdi + ser + tm.assert_equal(result, expected) + if box is not pd.DataFrame: + assert result.dtype == 'timedelta64[ns]' + else: + assert result.dtypes[0] == 'timedelta64[ns]' + + result = ser + tdi + tm.assert_equal(result, expected) + if box is not pd.DataFrame: + assert result.dtype == 'timedelta64[ns]' + else: + assert result.dtypes[0] == 'timedelta64[ns]' + + expected = Series([Timedelta(hours=-3), Timedelta(days=1, hours=-4)], + name=names[2]) + expected = tm.box_expected(expected, box) + + result = tdi - ser + tm.assert_equal(result, expected) + if box is not pd.DataFrame: + assert result.dtype == 'timedelta64[ns]' + else: + assert result.dtypes[0] == 'timedelta64[ns]' + + result = ser - tdi + tm.assert_equal(result, -expected) + if box is not pd.DataFrame: + assert result.dtype == 'timedelta64[ns]' + else: + assert result.dtypes[0] == 'timedelta64[ns]' + + @pytest.mark.parametrize('box', [pd.Index, Series, pd.DataFrame], + ids=lambda x: x.__name__) + def test_td64arr_sub_NaT(self, box): + # GH#18808 + ser = Series([NaT, Timedelta('1s')]) + expected = Series([NaT, NaT], dtype='timedelta64[ns]') + + ser = tm.box_expected(ser, box) + expected = tm.box_expected(expected, box) + + res = ser - NaT + tm.assert_equal(res, expected) + + +class TestTimedeltaArraylikeMulDivOps(object): + # Tests for timedelta64[ns] + # __mul__, __rmul__, __div__, __rdiv__, __floordiv__, __rfloordiv__ + + # ------------------------------------------------------------------ + # __floordiv__, __rfloordiv__ + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Incorrectly returns " + "m8[ns] instead of f8", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + Timedelta('5m4s'), + Timedelta('5m4s').to_timedelta64()]) + def test_td64arr_floordiv_tdscalar(self, box, scalar_td): + # GH#18831 + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + + expected = Series([0, 0, np.nan]) + + td1 = tm.box_expected(td1, box) + expected = tm.box_expected(expected, box) + + result = td1 // scalar_td + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Incorrectly casts to f8", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + Timedelta('5m4s'), + Timedelta('5m4s').to_timedelta64()]) + def test_td64arr_rfloordiv_tdscalar(self, box, scalar_td): + # GH#18831 + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + + expected = Series([1, 1, np.nan]) + + td1 = tm.box_expected(td1, box) + expected = tm.box_expected(expected, box) + + result = scalar_td // td1 + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns m8[ns] dtype " + "instead of f8", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + Timedelta('5m4s'), + Timedelta('5m4s').to_timedelta64()]) + def test_td64arr_rfloordiv_tdscalar_explicit(self, box, scalar_td): + # GH#18831 + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + + expected = Series([1, 1, np.nan]) + + td1 = tm.box_expected(td1, box) + expected = tm.box_expected(expected, box) + + # We can test __rfloordiv__ using this syntax, + # see `test_timedelta_rfloordiv` + result = td1.__rfloordiv__(scalar_td) + tm.assert_equal(result, expected) + + # ------------------------------------------------------------------ + # Operations with timedelta-like others + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="__mul__ op treats " + "timedelta other as i8; " + "rmul OK", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + Timedelta('5m4s'), + Timedelta('5m4s').to_timedelta64()]) + def test_td64arr_mul_tdscalar_invalid(self, box, scalar_td): + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + + td1 = tm.box_expected(td1, box) + + # check that we are getting a TypeError + # with 'operate' (from core/ops.py) for the ops that are not + # defined + pattern = 'operate|unsupported|cannot|not supported' + with tm.assert_raises_regex(TypeError, pattern): + td1 * scalar_td + with tm.assert_raises_regex(TypeError, pattern): + scalar_td * td1 + + # ------------------------------------------------------------------ + # Operations with numeric others + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns object-dtype", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('one', [1, np.array(1), 1.0, np.array(1.0)]) + def test_td64arr_mul_numeric_scalar(self, box, one, tdser): + # GH#4521 + # divide/multiply by integers + expected = Series(['-59 Days', '-59 Days', 'NaT'], + dtype='timedelta64[ns]') + + tdser = tm.box_expected(tdser, box) + expected = tm.box_expected(expected, box) + + result = tdser * (-one) + tm.assert_equal(result, expected) + result = (-one) * tdser + tm.assert_equal(result, expected) + + expected = Series(['118 Days', '118 Days', 'NaT'], + dtype='timedelta64[ns]') + expected = tm.box_expected(expected, box) + + result = tdser * (2 * one) + tm.assert_equal(result, expected) + result = (2 * one) * tdser + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="Returns object-dtype", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('two', [2, 2.0, np.array(2), np.array(2.0)]) + def test_td64arr_div_numeric_scalar(self, box, two, tdser): + # GH#4521 + # divide/multiply by integers + expected = Series(['29.5D', '29.5D', 'NaT'], dtype='timedelta64[ns]') + + tdser = tm.box_expected(tdser, box) + expected = tm.box_expected(expected, box) + + result = tdser / two + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="broadcasts along " + "wrong axis", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', + 'uint64', 'uint32', 'uint16', 'uint8', + 'float64', 'float32', 'float16']) + @pytest.mark.parametrize('vector', [np.array([20, 30, 40]), + pd.Index([20, 30, 40]), + Series([20, 30, 40])]) + def test_td64arr_mul_numeric_array(self, box, vector, dtype, tdser): + # GH#4521 + # divide/multiply by integers + vector = vector.astype(dtype) + + expected = Series(['1180 Days', '1770 Days', 'NaT'], + dtype='timedelta64[ns]') + + tdser = tm.box_expected(tdser, box) + # TODO: Make this up-casting more systematic? + box = Series if (box is pd.Index and type(vector) is Series) else box + expected = tm.box_expected(expected, box) + + result = tdser * vector + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="broadcasts along " + "wrong axis", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', + 'uint64', 'uint32', 'uint16', 'uint8', + 'float64', 'float32', 'float16']) + @pytest.mark.parametrize('vector', [np.array([20, 30, 40]), + pd.Index([20, 30, 40]), + Series([20, 30, 40])], + ids=lambda x: type(x).__name__) + def test_td64arr_rmul_numeric_array(self, box, vector, dtype, tdser): + # GH#4521 + # divide/multiply by integers + vector = vector.astype(dtype) + + expected = Series(['1180 Days', '1770 Days', 'NaT'], + dtype='timedelta64[ns]') + + tdser = tm.box_expected(tdser, box) + box = Series if (box is pd.Index and type(vector) is Series) else box + expected = tm.box_expected(expected, box) + + result = vector * tdser + tm.assert_equal(result, expected) + + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="broadcasts along " + "wrong axis", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('dtype', ['int64', 'int32', 'int16', + 'uint64', 'uint32', 'uint16', 'uint8', + 'float64', 'float32', 'float16']) + @pytest.mark.parametrize('vector', [np.array([20, 30, 40]), + pd.Index([20, 30, 40]), + Series([20, 30, 40])]) + def test_td64arr_div_numeric_array(self, box, vector, dtype, tdser): + # GH#4521 + # divide/multiply by integers + vector = vector.astype(dtype) + expected = Series(['2.95D', '1D 23H 12m', 'NaT'], + dtype='timedelta64[ns]') + + tdser = tm.box_expected(tdser, box) + box = Series if (box is pd.Index and type(vector) is Series) else box + expected = tm.box_expected(expected, box) + + result = tdser / vector + tm.assert_equal(result, expected) + + with pytest.raises(TypeError): + vector / tdser + + # TODO: Should we be parametrizing over types for `ser` too? + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pytest.param(pd.DataFrame, + marks=pytest.mark.xfail(reason="broadcasts along " + "wrong axis", + strict=True)) + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('names', [(None, None, None), + ('Egon', 'Venkman', None), + ('NCC1701D', 'NCC1701D', 'NCC1701D')]) + def test_td64arr_mul_int_series(self, box, names): + # GH#19042 test for correct name attachment + tdi = TimedeltaIndex(['0days', '1day', '2days', '3days', '4days'], + name=names[0]) + ser = Series([0, 1, 2, 3, 4], dtype=np.int64, name=names[1]) + + expected = Series(['0days', '1day', '4days', '9days', '16days'], + dtype='timedelta64[ns]', + name=names[2]) + + tdi = tm.box_expected(tdi, box) + box = Series if (box is pd.Index and type(ser) is Series) else box + expected = tm.box_expected(expected, box) + + result = ser * tdi + tm.assert_equal(result, expected) + + # The direct operation tdi * ser still needs to be fixed. + result = ser.__rmul__(tdi) + tm.assert_equal(result, expected) + + # TODO: Should we be parametrizing over types for `ser` too? + @pytest.mark.parametrize('box', [ + pd.Index, + Series, + pd.DataFrame + ], ids=lambda x: x.__name__) + @pytest.mark.parametrize('names', [(None, None, None), + ('Egon', 'Venkman', None), + ('NCC1701D', 'NCC1701D', 'NCC1701D')]) + def test_float_series_rdiv_td64arr(self, box, names): + # GH#19042 test for correct name attachment + # TODO: the direct operation TimedeltaIndex / Series still + # needs to be fixed. + tdi = TimedeltaIndex(['0days', '1day', '2days', '3days', '4days'], + name=names[0]) + ser = Series([1.5, 3, 4.5, 6, 7.5], dtype=np.float64, name=names[1]) + + expected = Series([tdi[n] / ser[n] for n in range(len(ser))], + dtype='timedelta64[ns]', + name=names[2]) + + tdi = tm.box_expected(tdi, box) + box = Series if (box is pd.Index and type(ser) is Series) else box + expected = tm.box_expected(expected, box) + + result = ser.__rdiv__(tdi) + if box is pd.DataFrame: + # TODO: Should we skip this case sooner or test something else? + assert result is NotImplemented + else: + tm.assert_equal(result, expected) + class TestTimedeltaArraylikeInvalidArithmeticOps(object): @pytest.mark.parametrize('box', [ pd.Index, - pd.Series, + Series, pytest.param(pd.DataFrame, marks=pytest.mark.xfail(reason="raises ValueError " "instead of TypeError", @@ -102,8 +833,8 @@ class TestTimedeltaArraylikeInvalidArithmeticOps(object): timedelta(minutes=5, seconds=4), Timedelta('5m4s'), Timedelta('5m4s').to_timedelta64()]) - def test_td64series_pow_invalid(self, scalar_td, box): - td1 = pd.Series([timedelta(minutes=5, seconds=3)] * 3) + def test_td64arr_pow_invalid(self, scalar_td, box): + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan td1 = tm.box_expected(td1, box) From f76a3f352a0d32f0fb42959e38fe80669add6822 Mon Sep 17 00:00:00 2001 From: Paul Reidy Date: Tue, 31 Jul 2018 14:03:02 +0100 Subject: [PATCH 15/47] BUG: Adjust time values with Period objects in Series.dt.end_time (#18952) --- doc/source/whatsnew/v0.24.0.txt | 37 +++++++++++++++++++ pandas/_libs/tslibs/period.pyx | 5 +++ pandas/core/arrays/datetimes.py | 2 - pandas/core/indexes/period.py | 12 +++++- pandas/tests/frame/test_period.py | 10 ++++- pandas/tests/indexes/period/test_period.py | 13 +++++++ .../indexes/period/test_scalar_compat.py | 3 +- pandas/tests/indexes/period/test_tools.py | 11 ++++++ pandas/tests/scalar/period/test_period.py | 17 +++++---- pandas/tests/series/test_period.py | 23 +++++++++++- pandas/tests/test_resample.py | 8 +++- pandas/tseries/offsets.py | 2 +- 12 files changed, 127 insertions(+), 16 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index d2d5d40393b62d..e3e1b35f89cbb9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -281,6 +281,43 @@ that the dates have been converted to UTC .. ipython:: python pd.to_datetime(["2015-11-18 15:30:00+05:30", "2015-11-18 16:30:00+06:30"], utc=True) +.. _whatsnew_0240.api_breaking.period_end_time: + +Time values in ``dt.end_time`` and ``to_timestamp(how='end')`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The time values in :class:`Period` and :class:`PeriodIndex` objects are now set +to '23:59:59.999999999' when calling :attr:`Series.dt.end_time`, :attr:`Period.end_time`, +:attr:`PeriodIndex.end_time`, :func:`Period.to_timestamp()` with ``how='end'``, +or :func:`PeriodIndex.to_timestamp()` with ``how='end'`` (:issue:`17157`) + +Previous Behavior: + +.. code-block:: ipython + + In [2]: p = pd.Period('2017-01-01', 'D') + In [3]: pi = pd.PeriodIndex([p]) + + In [4]: pd.Series(pi).dt.end_time[0] + Out[4]: Timestamp(2017-01-01 00:00:00) + + In [5]: p.end_time + Out[5]: Timestamp(2017-01-01 23:59:59.999999999) + +Current Behavior: + +Calling :attr:`Series.dt.end_time` will now result in a time of '23:59:59.999999999' as +is the case with :attr:`Period.end_time`, for example + +.. ipython:: python + + p = pd.Period('2017-01-01', 'D') + pi = pd.PeriodIndex([p]) + + pd.Series(pi).dt.end_time[0] + + p.end_time + .. _whatsnew_0240.api.datetimelike.normalize: Tick DateOffset Normalize Restrictions diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 65fb0f331d039f..96d7994bdc822d 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -34,6 +34,7 @@ cdef extern from "../src/datetime/np_datetime.h": cimport util from util cimport is_period_object, is_string_object, INT32_MIN +from pandas._libs.tslibs.timedeltas import Timedelta from timestamps import Timestamp from timezones cimport is_utc, is_tzlocal, get_dst_info from timedeltas cimport delta_to_nanoseconds @@ -1221,6 +1222,10 @@ cdef class _Period(object): freq = self._maybe_convert_freq(freq) how = _validate_end_alias(how) + end = how == 'E' + if end: + return (self + 1).to_timestamp(how='start') - Timedelta(1, 'ns') + if freq is None: base, mult = get_freq_code(self.freq) freq = get_to_timestamp_base(base) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 00d53ad82b2dca..26aaab2b1b237c 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -1235,11 +1235,9 @@ def _generate_regular_range(cls, start, end, periods, freq): tz = None if isinstance(start, Timestamp): tz = start.tz - start = start.to_pydatetime() if isinstance(end, Timestamp): tz = end.tz - end = end.to_pydatetime() xdr = generate_range(start=start, end=end, periods=periods, offset=freq) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b315e3ec20830a..32aa89010b2060 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -25,7 +25,7 @@ from pandas.core.tools.datetimes import parse_time_string from pandas._libs.lib import infer_dtype -from pandas._libs import tslib, index as libindex +from pandas._libs import tslib, index as libindex, Timedelta from pandas._libs.tslibs.period import (Period, IncompatibleFrequency, DIFFERENT_FREQ_INDEX, _validate_end_alias) @@ -501,6 +501,16 @@ def to_timestamp(self, freq=None, how='start'): """ how = _validate_end_alias(how) + end = how == 'E' + if end: + if freq == 'B': + # roll forward to ensure we land on B date + adjust = Timedelta(1, 'D') - Timedelta(1, 'ns') + return self.to_timestamp(how='start') + adjust + else: + adjust = Timedelta(1, 'ns') + return (self + 1).to_timestamp(how='start') - adjust + if freq is None: base, mult = _gfc(self.freq) freq = frequencies.get_to_timestamp_base(base) diff --git a/pandas/tests/frame/test_period.py b/pandas/tests/frame/test_period.py index 482210966fe6ba..d56df2371b2e3a 100644 --- a/pandas/tests/frame/test_period.py +++ b/pandas/tests/frame/test_period.py @@ -5,7 +5,7 @@ import pandas as pd import pandas.util.testing as tm from pandas import (PeriodIndex, period_range, DataFrame, date_range, - Index, to_datetime, DatetimeIndex) + Index, to_datetime, DatetimeIndex, Timedelta) def _permute(obj): @@ -51,6 +51,7 @@ def test_frame_to_time_stamp(self): df['mix'] = 'a' exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC') + exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns') result = df.to_timestamp('D', 'end') tm.assert_index_equal(result.index, exp_index) tm.assert_numpy_array_equal(result.values, df.values) @@ -66,22 +67,26 @@ def _get_with_delta(delta, freq='A-DEC'): delta = timedelta(hours=23) result = df.to_timestamp('H', 'end') exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) delta = timedelta(hours=23, minutes=59) result = df.to_timestamp('T', 'end') exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) result = df.to_timestamp('S', 'end') delta = timedelta(hours=23, minutes=59, seconds=59) exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) # columns df = df.T exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC') + exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns') result = df.to_timestamp('D', 'end', axis=1) tm.assert_index_equal(result.columns, exp_index) tm.assert_numpy_array_equal(result.values, df.values) @@ -93,16 +98,19 @@ def _get_with_delta(delta, freq='A-DEC'): delta = timedelta(hours=23) result = df.to_timestamp('H', 'end', axis=1) exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns') tm.assert_index_equal(result.columns, exp_index) delta = timedelta(hours=23, minutes=59) result = df.to_timestamp('T', 'end', axis=1) exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns') tm.assert_index_equal(result.columns, exp_index) result = df.to_timestamp('S', 'end', axis=1) delta = timedelta(hours=23, minutes=59, seconds=59) exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns') tm.assert_index_equal(result.columns, exp_index) # invalid axis diff --git a/pandas/tests/indexes/period/test_period.py b/pandas/tests/indexes/period/test_period.py index 923d826fe1a5e8..405edba83dc7a6 100644 --- a/pandas/tests/indexes/period/test_period.py +++ b/pandas/tests/indexes/period/test_period.py @@ -366,6 +366,19 @@ def test_periods_number_check(self): with pytest.raises(ValueError): period_range('2011-1-1', '2012-1-1', 'B') + def test_start_time(self): + # GH 17157 + index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31') + expected_index = date_range('2016-01-01', end='2016-05-31', freq='MS') + tm.assert_index_equal(index.start_time, expected_index) + + def test_end_time(self): + # GH 17157 + index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31') + expected_index = date_range('2016-01-01', end='2016-05-31', freq='M') + expected_index = expected_index.shift(1, freq='D').shift(-1, freq='ns') + tm.assert_index_equal(index.end_time, expected_index) + def test_index_duplicate_periods(self): # monotonic idx = PeriodIndex([2000, 2007, 2007, 2009, 2009], freq='A-JUN') diff --git a/pandas/tests/indexes/period/test_scalar_compat.py b/pandas/tests/indexes/period/test_scalar_compat.py index 56bd2adf587195..a66a81fe99cd46 100644 --- a/pandas/tests/indexes/period/test_scalar_compat.py +++ b/pandas/tests/indexes/period/test_scalar_compat.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Tests for PeriodIndex behaving like a vectorized Period scalar""" -from pandas import PeriodIndex, date_range +from pandas import PeriodIndex, date_range, Timedelta import pandas.util.testing as tm @@ -14,4 +14,5 @@ def test_start_time(self): def test_end_time(self): index = PeriodIndex(freq='M', start='2016-01-01', end='2016-05-31') expected_index = date_range('2016-01-01', end='2016-05-31', freq='M') + expected_index += Timedelta(1, 'D') - Timedelta(1, 'ns') tm.assert_index_equal(index.end_time, expected_index) diff --git a/pandas/tests/indexes/period/test_tools.py b/pandas/tests/indexes/period/test_tools.py index 16b558916df2df..c4ed07d98413f0 100644 --- a/pandas/tests/indexes/period/test_tools.py +++ b/pandas/tests/indexes/period/test_tools.py @@ -3,6 +3,7 @@ import pytest import pandas as pd +from pandas import Timedelta import pandas.util.testing as tm import pandas.core.indexes.period as period from pandas.compat import lrange @@ -60,6 +61,7 @@ def test_to_timestamp(self): exp_index = date_range('1/1/2001', end='12/31/2009', freq='A-DEC') result = series.to_timestamp(how='end') + exp_index = exp_index + Timedelta(1, 'D') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) assert result.name == 'foo' @@ -74,16 +76,19 @@ def _get_with_delta(delta, freq='A-DEC'): delta = timedelta(hours=23) result = series.to_timestamp('H', 'end') exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'h') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) delta = timedelta(hours=23, minutes=59) result = series.to_timestamp('T', 'end') exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 'm') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) result = series.to_timestamp('S', 'end') delta = timedelta(hours=23, minutes=59, seconds=59) exp_index = _get_with_delta(delta) + exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) index = PeriodIndex(freq='H', start='1/1/2001', end='1/2/2001') @@ -92,6 +97,7 @@ def _get_with_delta(delta, freq='A-DEC'): exp_index = date_range('1/1/2001 00:59:59', end='1/2/2001 00:59:59', freq='H') result = series.to_timestamp(how='end') + exp_index = exp_index + Timedelta(1, 's') - Timedelta(1, 'ns') tm.assert_index_equal(result.index, exp_index) assert result.name == 'foo' @@ -284,6 +290,7 @@ def test_to_timestamp_pi_mult(self): result = idx.to_timestamp(how='E') expected = DatetimeIndex(['2011-02-28', 'NaT', '2011-03-31'], name='idx') + expected = expected + Timedelta(1, 'D') - Timedelta(1, 'ns') tm.assert_index_equal(result, expected) def test_to_timestamp_pi_combined(self): @@ -298,11 +305,13 @@ def test_to_timestamp_pi_combined(self): expected = DatetimeIndex(['2011-01-02 00:59:59', '2011-01-03 01:59:59'], name='idx') + expected = expected + Timedelta(1, 's') - Timedelta(1, 'ns') tm.assert_index_equal(result, expected) result = idx.to_timestamp(how='E', freq='H') expected = DatetimeIndex(['2011-01-02 00:00', '2011-01-03 01:00'], name='idx') + expected = expected + Timedelta(1, 'h') - Timedelta(1, 'ns') tm.assert_index_equal(result, expected) def test_period_astype_to_timestamp(self): @@ -312,6 +321,7 @@ def test_period_astype_to_timestamp(self): tm.assert_index_equal(pi.astype('datetime64[ns]'), exp) exp = pd.DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31']) + exp = exp + Timedelta(1, 'D') - Timedelta(1, 'ns') tm.assert_index_equal(pi.astype('datetime64[ns]', how='end'), exp) exp = pd.DatetimeIndex(['2011-01-01', '2011-02-01', '2011-03-01'], @@ -321,6 +331,7 @@ def test_period_astype_to_timestamp(self): exp = pd.DatetimeIndex(['2011-01-31', '2011-02-28', '2011-03-31'], tz='US/Eastern') + exp = exp + Timedelta(1, 'D') - Timedelta(1, 'ns') res = pi.astype('datetime64[ns, US/Eastern]', how='end') tm.assert_index_equal(res, exp) diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index eccd86a888fb98..4a17b2efd1dece 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -5,6 +5,7 @@ from datetime import datetime, date, timedelta import pandas as pd +from pandas import Timedelta import pandas.util.testing as tm import pandas.core.indexes.period as period from pandas.compat import text_type, iteritems @@ -274,12 +275,14 @@ def test_timestamp_tz_arg_dateutil_from_string(self): def test_timestamp_mult(self): p = pd.Period('2011-01', freq='M') - assert p.to_timestamp(how='S') == pd.Timestamp('2011-01-01') - assert p.to_timestamp(how='E') == pd.Timestamp('2011-01-31') + assert p.to_timestamp(how='S') == Timestamp('2011-01-01') + expected = Timestamp('2011-02-01') - Timedelta(1, 'ns') + assert p.to_timestamp(how='E') == expected p = pd.Period('2011-01', freq='3M') - assert p.to_timestamp(how='S') == pd.Timestamp('2011-01-01') - assert p.to_timestamp(how='E') == pd.Timestamp('2011-03-31') + assert p.to_timestamp(how='S') == Timestamp('2011-01-01') + expected = Timestamp('2011-04-01') - Timedelta(1, 'ns') + assert p.to_timestamp(how='E') == expected def test_construction(self): i1 = Period('1/1/2005', freq='M') @@ -611,19 +614,19 @@ def _ex(p): p = Period('1985', freq='A') result = p.to_timestamp('H', how='end') - expected = datetime(1985, 12, 31, 23) + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') assert result == expected result = p.to_timestamp('3H', how='end') assert result == expected result = p.to_timestamp('T', how='end') - expected = datetime(1985, 12, 31, 23, 59) + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') assert result == expected result = p.to_timestamp('2T', how='end') assert result == expected result = p.to_timestamp(how='end') - expected = datetime(1985, 12, 31) + expected = Timestamp(1986, 1, 1) - Timedelta(1, 'ns') assert result == expected expected = datetime(1985, 1, 1) diff --git a/pandas/tests/series/test_period.py b/pandas/tests/series/test_period.py index 63726f27914f3d..90dbe26a2f0ea3 100644 --- a/pandas/tests/series/test_period.py +++ b/pandas/tests/series/test_period.py @@ -3,7 +3,8 @@ import pandas as pd import pandas.util.testing as tm import pandas.core.indexes.period as period -from pandas import Series, period_range, DataFrame +from pandas import Series, period_range, DataFrame, Period +import pytest def _permute(obj): @@ -167,3 +168,23 @@ def test_truncate(self): pd.Period('2017-09-02') ]) tm.assert_series_equal(result2, pd.Series([2], index=expected_idx2)) + + @pytest.mark.parametrize('input_vals', [ + [Period('2016-01', freq='M'), Period('2016-02', freq='M')], + [Period('2016-01-01', freq='D'), Period('2016-01-02', freq='D')], + [Period('2016-01-01 00:00:00', freq='H'), + Period('2016-01-01 01:00:00', freq='H')], + [Period('2016-01-01 00:00:00', freq='M'), + Period('2016-01-01 00:01:00', freq='M')], + [Period('2016-01-01 00:00:00', freq='S'), + Period('2016-01-01 00:00:01', freq='S')] + ]) + def test_end_time_timevalues(self, input_vals): + # GH 17157 + # Check that the time part of the Period is adjusted by end_time + # when using the dt accessor on a Series + + s = Series(input_vals) + result = s.dt.end_time + expected = s.apply(lambda x: x.end_time) + tm.assert_series_equal(result, expected) diff --git a/pandas/tests/test_resample.py b/pandas/tests/test_resample.py index 1f70d09e43b378..de4dc2bcf25a47 100644 --- a/pandas/tests/test_resample.py +++ b/pandas/tests/test_resample.py @@ -21,7 +21,7 @@ import pandas as pd from pandas import (Series, DataFrame, Panel, Index, isna, - notna, Timestamp) + notna, Timestamp, Timedelta) from pandas.compat import range, lrange, zip, OrderedDict from pandas.errors import UnsupportedFunctionCall @@ -1702,12 +1702,14 @@ def test_resample_anchored_intraday(self): result = df.resample('M').mean() expected = df.resample( 'M', kind='period').mean().to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') tm.assert_frame_equal(result, expected) result = df.resample('M', closed='left').mean() exp = df.tshift(1, freq='D').resample('M', kind='period').mean() exp = exp.to_timestamp(how='end') + exp.index = exp.index + Timedelta(1, 'ns') - Timedelta(1, 'D') tm.assert_frame_equal(result, exp) rng = date_range('1/1/2012', '4/1/2012', freq='100min') @@ -1716,12 +1718,14 @@ def test_resample_anchored_intraday(self): result = df.resample('Q').mean() expected = df.resample( 'Q', kind='period').mean().to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') tm.assert_frame_equal(result, expected) result = df.resample('Q', closed='left').mean() expected = df.tshift(1, freq='D').resample('Q', kind='period', closed='left').mean() expected = expected.to_timestamp(how='end') + expected.index += Timedelta(1, 'ns') - Timedelta(1, 'D') tm.assert_frame_equal(result, expected) ts = _simple_ts('2012-04-29 23:00', '2012-04-30 5:00', freq='h') @@ -2473,7 +2477,7 @@ def test_resample_to_timestamps(self): ts = _simple_pts('1/1/1990', '12/31/1995', freq='M') result = ts.resample('A-DEC', kind='timestamp').mean() - expected = ts.to_timestamp(how='end').resample('A-DEC').mean() + expected = ts.to_timestamp(how='start').resample('A-DEC').mean() assert_series_equal(result, expected) def test_resample_to_quarterly(self): diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 60981f41ec716f..9d41401a7eefc4 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1321,7 +1321,7 @@ def _end_apply_index(self, dtindex): roll = self.n base = (base_period + roll).to_timestamp(how='end') - return base + off + return base + off + Timedelta(1, 'ns') - Timedelta(1, 'D') def onOffset(self, dt): if self.normalize and not _is_normalized(dt): From 1dc77338d21bc7880081f9362690d700cc284791 Mon Sep 17 00:00:00 2001 From: Redonnet Louis Date: Tue, 31 Jul 2018 15:12:53 +0200 Subject: [PATCH 16/47] Bug in date_range with semi-open interval and freq='B' (#22095) --- .../indexes/datetimes/test_date_range.py | 344 +++++++++--------- 1 file changed, 178 insertions(+), 166 deletions(-) diff --git a/pandas/tests/indexes/datetimes/test_date_range.py b/pandas/tests/indexes/datetimes/test_date_range.py index 47d4d15420f1df..22fb8b2942bea6 100644 --- a/pandas/tests/indexes/datetimes/test_date_range.py +++ b/pandas/tests/indexes/datetimes/test_date_range.py @@ -301,172 +301,6 @@ def test_construct_with_different_start_end_string_format(self): Timestamp('2013-01-01 02:00:00+09:00')]) tm.assert_index_equal(result, expected) - -class TestGenRangeGeneration(object): - - def test_generate(self): - rng1 = list(generate_range(START, END, offset=BDay())) - rng2 = list(generate_range(START, END, time_rule='B')) - assert rng1 == rng2 - - def test_generate_cday(self): - rng1 = list(generate_range(START, END, offset=CDay())) - rng2 = list(generate_range(START, END, time_rule='C')) - assert rng1 == rng2 - - def test_1(self): - rng = list(generate_range(start=datetime(2009, 3, 25), periods=2)) - expected = [datetime(2009, 3, 25), datetime(2009, 3, 26)] - assert rng == expected - - def test_2(self): - rng = list(generate_range(start=datetime(2008, 1, 1), - end=datetime(2008, 1, 3))) - expected = [datetime(2008, 1, 1), - datetime(2008, 1, 2), - datetime(2008, 1, 3)] - assert rng == expected - - def test_3(self): - rng = list(generate_range(start=datetime(2008, 1, 5), - end=datetime(2008, 1, 6))) - expected = [] - assert rng == expected - - def test_precision_finer_than_offset(self): - # GH 9907 - result1 = DatetimeIndex(start='2015-04-15 00:00:03', - end='2016-04-22 00:00:00', freq='Q') - result2 = DatetimeIndex(start='2015-04-15 00:00:03', - end='2015-06-22 00:00:04', freq='W') - expected1_list = ['2015-06-30 00:00:03', '2015-09-30 00:00:03', - '2015-12-31 00:00:03', '2016-03-31 00:00:03'] - expected2_list = ['2015-04-19 00:00:03', '2015-04-26 00:00:03', - '2015-05-03 00:00:03', '2015-05-10 00:00:03', - '2015-05-17 00:00:03', '2015-05-24 00:00:03', - '2015-05-31 00:00:03', '2015-06-07 00:00:03', - '2015-06-14 00:00:03', '2015-06-21 00:00:03'] - expected1 = DatetimeIndex(expected1_list, dtype='datetime64[ns]', - freq='Q-DEC', tz=None) - expected2 = DatetimeIndex(expected2_list, dtype='datetime64[ns]', - freq='W-SUN', tz=None) - tm.assert_index_equal(result1, expected1) - tm.assert_index_equal(result2, expected2) - - dt1, dt2 = '2017-01-01', '2017-01-01' - tz1, tz2 = 'US/Eastern', 'Europe/London' - - @pytest.mark.parametrize("start,end", [ - (pd.Timestamp(dt1, tz=tz1), pd.Timestamp(dt2)), - (pd.Timestamp(dt1), pd.Timestamp(dt2, tz=tz2)), - (pd.Timestamp(dt1, tz=tz1), pd.Timestamp(dt2, tz=tz2)), - (pd.Timestamp(dt1, tz=tz2), pd.Timestamp(dt2, tz=tz1)) - ]) - def test_mismatching_tz_raises_err(self, start, end): - # issue 18488 - with pytest.raises(TypeError): - pd.date_range(start, end) - with pytest.raises(TypeError): - pd.DatetimeIndex(start, end, freq=BDay()) - - -class TestBusinessDateRange(object): - - def test_constructor(self): - bdate_range(START, END, freq=BDay()) - bdate_range(START, periods=20, freq=BDay()) - bdate_range(end=START, periods=20, freq=BDay()) - - msg = 'periods must be a number, got B' - with tm.assert_raises_regex(TypeError, msg): - date_range('2011-1-1', '2012-1-1', 'B') - - with tm.assert_raises_regex(TypeError, msg): - bdate_range('2011-1-1', '2012-1-1', 'B') - - msg = 'freq must be specified for bdate_range; use date_range instead' - with tm.assert_raises_regex(TypeError, msg): - bdate_range(START, END, periods=10, freq=None) - - def test_naive_aware_conflicts(self): - naive = bdate_range(START, END, freq=BDay(), tz=None) - aware = bdate_range(START, END, freq=BDay(), tz="Asia/Hong_Kong") - - msg = 'tz-naive.*tz-aware' - with tm.assert_raises_regex(TypeError, msg): - naive.join(aware) - - with tm.assert_raises_regex(TypeError, msg): - aware.join(naive) - - def test_cached_range(self): - DatetimeIndex._cached_range(START, END, freq=BDay()) - DatetimeIndex._cached_range(START, periods=20, freq=BDay()) - DatetimeIndex._cached_range(end=START, periods=20, freq=BDay()) - - with tm.assert_raises_regex(TypeError, "freq"): - DatetimeIndex._cached_range(START, END) - - with tm.assert_raises_regex(TypeError, "specify period"): - DatetimeIndex._cached_range(START, freq=BDay()) - - with tm.assert_raises_regex(TypeError, "specify period"): - DatetimeIndex._cached_range(end=END, freq=BDay()) - - with tm.assert_raises_regex(TypeError, "start or end"): - DatetimeIndex._cached_range(periods=20, freq=BDay()) - - def test_cached_range_bug(self): - rng = date_range('2010-09-01 05:00:00', periods=50, - freq=DateOffset(hours=6)) - assert len(rng) == 50 - assert rng[0] == datetime(2010, 9, 1, 5) - - def test_timezone_comparaison_bug(self): - # smoke test - start = Timestamp('20130220 10:00', tz='US/Eastern') - result = date_range(start, periods=2, tz='US/Eastern') - assert len(result) == 2 - - def test_timezone_comparaison_assert(self): - start = Timestamp('20130220 10:00', tz='US/Eastern') - msg = 'Inferred time zone not equal to passed time zone' - with tm.assert_raises_regex(AssertionError, msg): - date_range(start, periods=2, tz='Europe/Berlin') - - def test_misc(self): - end = datetime(2009, 5, 13) - dr = bdate_range(end=end, periods=20) - firstDate = end - 19 * BDay() - - assert len(dr) == 20 - assert dr[0] == firstDate - assert dr[-1] == end - - def test_date_parse_failure(self): - badly_formed_date = '2007/100/1' - - with pytest.raises(ValueError): - Timestamp(badly_formed_date) - - with pytest.raises(ValueError): - bdate_range(start=badly_formed_date, periods=10) - - with pytest.raises(ValueError): - bdate_range(end=badly_formed_date, periods=10) - - with pytest.raises(ValueError): - bdate_range(badly_formed_date, badly_formed_date) - - def test_daterange_bug_456(self): - # GH #456 - rng1 = bdate_range('12/5/2011', '12/5/2011') - rng2 = bdate_range('12/2/2011', '12/5/2011') - rng2.freq = BDay() - - result = rng1.union(rng2) - assert isinstance(result, DatetimeIndex) - def test_error_with_zero_monthends(self): msg = r'Offset <0 \* MonthEnds> did not increment date' with tm.assert_raises_regex(ValueError, msg): @@ -658,6 +492,184 @@ def test_freq_divides_end_in_nanos(self): tm.assert_index_equal(result_1, expected_1) tm.assert_index_equal(result_2, expected_2) + def test_cached_range_bug(self): + rng = date_range('2010-09-01 05:00:00', periods=50, + freq=DateOffset(hours=6)) + assert len(rng) == 50 + assert rng[0] == datetime(2010, 9, 1, 5) + + def test_timezone_comparaison_bug(self): + # smoke test + start = Timestamp('20130220 10:00', tz='US/Eastern') + result = date_range(start, periods=2, tz='US/Eastern') + assert len(result) == 2 + + def test_timezone_comparaison_assert(self): + start = Timestamp('20130220 10:00', tz='US/Eastern') + msg = 'Inferred time zone not equal to passed time zone' + with tm.assert_raises_regex(AssertionError, msg): + date_range(start, periods=2, tz='Europe/Berlin') + + +class TestGenRangeGeneration(object): + + def test_generate(self): + rng1 = list(generate_range(START, END, offset=BDay())) + rng2 = list(generate_range(START, END, time_rule='B')) + assert rng1 == rng2 + + def test_generate_cday(self): + rng1 = list(generate_range(START, END, offset=CDay())) + rng2 = list(generate_range(START, END, time_rule='C')) + assert rng1 == rng2 + + def test_1(self): + rng = list(generate_range(start=datetime(2009, 3, 25), periods=2)) + expected = [datetime(2009, 3, 25), datetime(2009, 3, 26)] + assert rng == expected + + def test_2(self): + rng = list(generate_range(start=datetime(2008, 1, 1), + end=datetime(2008, 1, 3))) + expected = [datetime(2008, 1, 1), + datetime(2008, 1, 2), + datetime(2008, 1, 3)] + assert rng == expected + + def test_3(self): + rng = list(generate_range(start=datetime(2008, 1, 5), + end=datetime(2008, 1, 6))) + expected = [] + assert rng == expected + + def test_precision_finer_than_offset(self): + # GH 9907 + result1 = DatetimeIndex(start='2015-04-15 00:00:03', + end='2016-04-22 00:00:00', freq='Q') + result2 = DatetimeIndex(start='2015-04-15 00:00:03', + end='2015-06-22 00:00:04', freq='W') + expected1_list = ['2015-06-30 00:00:03', '2015-09-30 00:00:03', + '2015-12-31 00:00:03', '2016-03-31 00:00:03'] + expected2_list = ['2015-04-19 00:00:03', '2015-04-26 00:00:03', + '2015-05-03 00:00:03', '2015-05-10 00:00:03', + '2015-05-17 00:00:03', '2015-05-24 00:00:03', + '2015-05-31 00:00:03', '2015-06-07 00:00:03', + '2015-06-14 00:00:03', '2015-06-21 00:00:03'] + expected1 = DatetimeIndex(expected1_list, dtype='datetime64[ns]', + freq='Q-DEC', tz=None) + expected2 = DatetimeIndex(expected2_list, dtype='datetime64[ns]', + freq='W-SUN', tz=None) + tm.assert_index_equal(result1, expected1) + tm.assert_index_equal(result2, expected2) + + dt1, dt2 = '2017-01-01', '2017-01-01' + tz1, tz2 = 'US/Eastern', 'Europe/London' + + @pytest.mark.parametrize("start,end", [ + (pd.Timestamp(dt1, tz=tz1), pd.Timestamp(dt2)), + (pd.Timestamp(dt1), pd.Timestamp(dt2, tz=tz2)), + (pd.Timestamp(dt1, tz=tz1), pd.Timestamp(dt2, tz=tz2)), + (pd.Timestamp(dt1, tz=tz2), pd.Timestamp(dt2, tz=tz1)) + ]) + def test_mismatching_tz_raises_err(self, start, end): + # issue 18488 + with pytest.raises(TypeError): + pd.date_range(start, end) + with pytest.raises(TypeError): + pd.DatetimeIndex(start, end, freq=BDay()) + + +class TestBusinessDateRange(object): + + def test_constructor(self): + bdate_range(START, END, freq=BDay()) + bdate_range(START, periods=20, freq=BDay()) + bdate_range(end=START, periods=20, freq=BDay()) + + msg = 'periods must be a number, got B' + with tm.assert_raises_regex(TypeError, msg): + date_range('2011-1-1', '2012-1-1', 'B') + + with tm.assert_raises_regex(TypeError, msg): + bdate_range('2011-1-1', '2012-1-1', 'B') + + msg = 'freq must be specified for bdate_range; use date_range instead' + with tm.assert_raises_regex(TypeError, msg): + bdate_range(START, END, periods=10, freq=None) + + def test_naive_aware_conflicts(self): + naive = bdate_range(START, END, freq=BDay(), tz=None) + aware = bdate_range(START, END, freq=BDay(), tz="Asia/Hong_Kong") + + msg = 'tz-naive.*tz-aware' + with tm.assert_raises_regex(TypeError, msg): + naive.join(aware) + + with tm.assert_raises_regex(TypeError, msg): + aware.join(naive) + + def test_cached_range(self): + DatetimeIndex._cached_range(START, END, freq=BDay()) + DatetimeIndex._cached_range(START, periods=20, freq=BDay()) + DatetimeIndex._cached_range(end=START, periods=20, freq=BDay()) + + with tm.assert_raises_regex(TypeError, "freq"): + DatetimeIndex._cached_range(START, END) + + with tm.assert_raises_regex(TypeError, "specify period"): + DatetimeIndex._cached_range(START, freq=BDay()) + + with tm.assert_raises_regex(TypeError, "specify period"): + DatetimeIndex._cached_range(end=END, freq=BDay()) + + with tm.assert_raises_regex(TypeError, "start or end"): + DatetimeIndex._cached_range(periods=20, freq=BDay()) + + def test_misc(self): + end = datetime(2009, 5, 13) + dr = bdate_range(end=end, periods=20) + firstDate = end - 19 * BDay() + + assert len(dr) == 20 + assert dr[0] == firstDate + assert dr[-1] == end + + def test_date_parse_failure(self): + badly_formed_date = '2007/100/1' + + with pytest.raises(ValueError): + Timestamp(badly_formed_date) + + with pytest.raises(ValueError): + bdate_range(start=badly_formed_date, periods=10) + + with pytest.raises(ValueError): + bdate_range(end=badly_formed_date, periods=10) + + with pytest.raises(ValueError): + bdate_range(badly_formed_date, badly_formed_date) + + def test_daterange_bug_456(self): + # GH #456 + rng1 = bdate_range('12/5/2011', '12/5/2011') + rng2 = bdate_range('12/2/2011', '12/5/2011') + rng2.freq = BDay() + + result = rng1.union(rng2) + assert isinstance(result, DatetimeIndex) + + @pytest.mark.parametrize('closed', ['left', 'right']) + def test_bdays_and_open_boundaries(self, closed): + # GH 6673 + start = '2018-07-21' # Saturday + end = '2018-07-29' # Sunday + result = pd.date_range(start, end, freq='B', closed=closed) + + bday_start = '2018-07-23' # Monday + bday_end = '2018-07-27' # Friday + expected = pd.date_range(bday_start, bday_end, freq='D') + tm.assert_index_equal(result, expected) + class TestCustomDateRange(object): From 857910180e64f30c0fd784c97058f67487b640f0 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 31 Jul 2018 06:18:11 -0700 Subject: [PATCH 17/47] [CLN] parametrize and cleanup a bunch of tests (#22093) --- pandas/tests/frame/test_analytics.py | 50 +++--- pandas/tests/frame/test_apply.py | 17 +- .../tests/frame/test_axis_select_reindex.py | 29 +--- pandas/tests/frame/test_operators.py | 74 +++++---- pandas/tests/frame/test_query_eval.py | 13 +- pandas/tests/frame/test_replace.py | 14 +- pandas/tests/frame/test_reshape.py | 28 ++-- pandas/tests/scalar/period/test_period.py | 5 +- pandas/tests/series/test_alter_axes.py | 32 ++-- pandas/tests/series/test_datetime_values.py | 19 ++- pandas/tests/series/test_operators.py | 151 ++++++++++-------- pandas/tests/series/test_period.py | 16 +- pandas/tests/series/test_quantile.py | 49 +++--- pandas/tests/series/test_timeseries.py | 98 ++++++------ 14 files changed, 306 insertions(+), 289 deletions(-) diff --git a/pandas/tests/frame/test_analytics.py b/pandas/tests/frame/test_analytics.py index b48395efaf5c8a..f72cf8cdaafe9d 100644 --- a/pandas/tests/frame/test_analytics.py +++ b/pandas/tests/frame/test_analytics.py @@ -74,29 +74,29 @@ def test_corr_non_numeric(self): tm.assert_frame_equal(result, expected) @td.skip_if_no_scipy - def test_corr_nooverlap(self): + @pytest.mark.parametrize('meth', ['pearson', 'kendall', 'spearman']) + def test_corr_nooverlap(self, meth): # nothing in common - for meth in ['pearson', 'kendall', 'spearman']: - df = DataFrame({'A': [1, 1.5, 1, np.nan, np.nan, np.nan], - 'B': [np.nan, np.nan, np.nan, 1, 1.5, 1], - 'C': [np.nan, np.nan, np.nan, np.nan, - np.nan, np.nan]}) - rs = df.corr(meth) - assert isna(rs.loc['A', 'B']) - assert isna(rs.loc['B', 'A']) - assert rs.loc['A', 'A'] == 1 - assert rs.loc['B', 'B'] == 1 - assert isna(rs.loc['C', 'C']) + df = DataFrame({'A': [1, 1.5, 1, np.nan, np.nan, np.nan], + 'B': [np.nan, np.nan, np.nan, 1, 1.5, 1], + 'C': [np.nan, np.nan, np.nan, np.nan, + np.nan, np.nan]}) + rs = df.corr(meth) + assert isna(rs.loc['A', 'B']) + assert isna(rs.loc['B', 'A']) + assert rs.loc['A', 'A'] == 1 + assert rs.loc['B', 'B'] == 1 + assert isna(rs.loc['C', 'C']) @td.skip_if_no_scipy - def test_corr_constant(self): + @pytest.mark.parametrize('meth', ['pearson', 'spearman']) + def test_corr_constant(self, meth): # constant --> all NA - for meth in ['pearson', 'spearman']: - df = DataFrame({'A': [1, 1, 1, np.nan, np.nan, np.nan], - 'B': [np.nan, np.nan, np.nan, 1, 1, 1]}) - rs = df.corr(meth) - assert isna(rs.values).all() + df = DataFrame({'A': [1, 1, 1, np.nan, np.nan, np.nan], + 'B': [np.nan, np.nan, np.nan, 1, 1, 1]}) + rs = df.corr(meth) + assert isna(rs.values).all() def test_corr_int(self): # dtypes other than float64 #1761 @@ -658,21 +658,21 @@ def test_numeric_only_flag(self, meth): pytest.raises(TypeError, lambda: getattr(df2, meth)( axis=1, numeric_only=False)) - def test_mixed_ops(self): + @pytest.mark.parametrize('op', ['mean', 'std', 'var', + 'skew', 'kurt', 'sem']) + def test_mixed_ops(self, op): # GH 16116 df = DataFrame({'int': [1, 2, 3, 4], 'float': [1., 2., 3., 4.], 'str': ['a', 'b', 'c', 'd']}) - for op in ['mean', 'std', 'var', 'skew', - 'kurt', 'sem']: + result = getattr(df, op)() + assert len(result) == 2 + + with pd.option_context('use_bottleneck', False): result = getattr(df, op)() assert len(result) == 2 - with pd.option_context('use_bottleneck', False): - result = getattr(df, op)() - assert len(result) == 2 - def test_cumsum(self): self.tsframe.loc[5:10, 0] = nan self.tsframe.loc[10:15, 1] = nan diff --git a/pandas/tests/frame/test_apply.py b/pandas/tests/frame/test_apply.py index 344838493f0b1c..f18163d51c7210 100644 --- a/pandas/tests/frame/test_apply.py +++ b/pandas/tests/frame/test_apply.py @@ -120,16 +120,15 @@ def test_apply_standard_nonunique(self): rs = df.T.apply(lambda s: s[0], axis=0) assert_series_equal(rs, xp) - def test_with_string_args(self): - - for arg in ['sum', 'mean', 'min', 'max', 'std']: - result = self.frame.apply(arg) - expected = getattr(self.frame, arg)() - tm.assert_series_equal(result, expected) + @pytest.mark.parametrize('arg', ['sum', 'mean', 'min', 'max', 'std']) + def test_with_string_args(self, arg): + result = self.frame.apply(arg) + expected = getattr(self.frame, arg)() + tm.assert_series_equal(result, expected) - result = self.frame.apply(arg, axis=1) - expected = getattr(self.frame, arg)(axis=1) - tm.assert_series_equal(result, expected) + result = self.frame.apply(arg, axis=1) + expected = getattr(self.frame, arg)(axis=1) + tm.assert_series_equal(result, expected) def test_apply_broadcast_deprecated(self): with tm.assert_produces_warning(FutureWarning): diff --git a/pandas/tests/frame/test_axis_select_reindex.py b/pandas/tests/frame/test_axis_select_reindex.py index 004fb4eb0c128a..0bc74c6890ee9e 100644 --- a/pandas/tests/frame/test_axis_select_reindex.py +++ b/pandas/tests/frame/test_axis_select_reindex.py @@ -674,29 +674,12 @@ def _check_align(self, a, b, axis, fill_axis, how, method, limit=None): assert_frame_equal(aa, ea) assert_frame_equal(ab, eb) - def test_align_fill_method_inner(self): - for meth in ['pad', 'bfill']: - for ax in [0, 1, None]: - for fax in [0, 1]: - self._check_align_fill('inner', meth, ax, fax) - - def test_align_fill_method_outer(self): - for meth in ['pad', 'bfill']: - for ax in [0, 1, None]: - for fax in [0, 1]: - self._check_align_fill('outer', meth, ax, fax) - - def test_align_fill_method_left(self): - for meth in ['pad', 'bfill']: - for ax in [0, 1, None]: - for fax in [0, 1]: - self._check_align_fill('left', meth, ax, fax) - - def test_align_fill_method_right(self): - for meth in ['pad', 'bfill']: - for ax in [0, 1, None]: - for fax in [0, 1]: - self._check_align_fill('right', meth, ax, fax) + @pytest.mark.parametrize('meth', ['pad', 'bfill']) + @pytest.mark.parametrize('ax', [0, 1, None]) + @pytest.mark.parametrize('fax', [0, 1]) + @pytest.mark.parametrize('how', ['inner', 'outer', 'left', 'right']) + def test_align_fill_method(self, how, meth, ax, fax): + self._check_align_fill(how, meth, ax, fax) def _check_align_fill(self, kind, meth, ax, fax): left = self.frame.iloc[0:4, :10] diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index fdf50805ad8184..1702b2e7d29a44 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -72,18 +72,18 @@ def test_operators(self): assert (df + df).equals(df) assert_frame_equal(df + df, df) - def test_ops_np_scalar(self): - vals, xs = np.random.rand(5, 3), [nan, 7, -23, 2.718, -3.14, np.inf] + @pytest.mark.parametrize('other', [nan, 7, -23, 2.718, -3.14, np.inf]) + def test_ops_np_scalar(self, other): + vals = np.random.randn(5, 3) f = lambda x: DataFrame(x, index=list('ABCDE'), columns=['jim', 'joe', 'jolie']) df = f(vals) - for x in xs: - assert_frame_equal(df / np.array(x), f(vals / x)) - assert_frame_equal(np.array(x) * df, f(vals * x)) - assert_frame_equal(df + np.array(x), f(vals + x)) - assert_frame_equal(np.array(x) - df, f(x - vals)) + assert_frame_equal(df / np.array(other), f(vals / other)) + assert_frame_equal(np.array(other) * df, f(vals * other)) + assert_frame_equal(df + np.array(other), f(vals + other)) + assert_frame_equal(np.array(other) - df, f(other - vals)) def test_operators_boolean(self): @@ -116,41 +116,40 @@ def test_operators_boolean(self): True, index=[1], columns=['A']) assert_frame_equal(result, DataFrame(1, index=[1], columns=['A'])) - def f(): - DataFrame(1.0, index=[1], columns=['A']) | DataFrame( - True, index=[1], columns=['A']) - pytest.raises(TypeError, f) + df1 = DataFrame(1.0, index=[1], columns=['A']) + df2 = DataFrame(True, index=[1], columns=['A']) + with pytest.raises(TypeError): + df1 | df2 - def f(): - DataFrame('foo', index=[1], columns=['A']) | DataFrame( - True, index=[1], columns=['A']) - pytest.raises(TypeError, f) + df1 = DataFrame('foo', index=[1], columns=['A']) + df2 = DataFrame(True, index=[1], columns=['A']) + with pytest.raises(TypeError): + df1 | df2 - def test_operators_none_as_na(self): + @pytest.mark.parametrize('op', [operator.add, operator.sub, + operator.mul, operator.truediv]) + def test_operators_none_as_na(self, op): df = DataFrame({"col1": [2, 5.0, 123, None], "col2": [1, 2, 3, 4]}, dtype=object) - ops = [operator.add, operator.sub, operator.mul, operator.truediv] - # since filling converts dtypes from object, changed expected to be # object - for op in ops: - filled = df.fillna(np.nan) - result = op(df, 3) - expected = op(filled, 3).astype(object) - expected[com.isna(expected)] = None - assert_frame_equal(result, expected) + filled = df.fillna(np.nan) + result = op(df, 3) + expected = op(filled, 3).astype(object) + expected[com.isna(expected)] = None + assert_frame_equal(result, expected) - result = op(df, df) - expected = op(filled, filled).astype(object) - expected[com.isna(expected)] = None - assert_frame_equal(result, expected) + result = op(df, df) + expected = op(filled, filled).astype(object) + expected[com.isna(expected)] = None + assert_frame_equal(result, expected) - result = op(df, df.fillna(7)) - assert_frame_equal(result, expected) + result = op(df, df.fillna(7)) + assert_frame_equal(result, expected) - result = op(df.fillna(7), df) - assert_frame_equal(result, expected, check_dtype=False) + result = op(df.fillna(7), df) + assert_frame_equal(result, expected, check_dtype=False) def test_comparison_invalid(self): @@ -978,8 +977,11 @@ def test_boolean_comparison(self): result = df.values > b_r assert_numpy_array_equal(result, expected.values) - pytest.raises(ValueError, df.__gt__, b_c) - pytest.raises(ValueError, df.values.__gt__, b_c) + with pytest.raises(ValueError): + df > b_c + + with pytest.raises(ValueError): + df.values > b_c # == expected = DataFrame([[False, False], [True, False], [False, False]]) @@ -998,7 +1000,9 @@ def test_boolean_comparison(self): result = df.values == b_r assert_numpy_array_equal(result, expected.values) - pytest.raises(ValueError, lambda: df == b_c) + with pytest.raises(ValueError): + df == b_c + assert df.values.shape != b_c.shape # with alignment diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index a226f8de3c8bd9..a02f78bfaf8a5c 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -1029,11 +1029,10 @@ def test_bool_arith_expr(self, parser, engine): expect = self.frame.a[self.frame.a < 1] + self.frame.b assert_series_equal(res, expect) - def test_invalid_type_for_operator_raises(self, parser, engine): + @pytest.mark.parametrize('op', ['+', '-', '*', '/']) + def test_invalid_type_for_operator_raises(self, parser, engine, op): df = DataFrame({'a': [1, 2], 'b': ['c', 'd']}) - ops = '+', '-', '*', '/' - for op in ops: - with tm.assert_raises_regex(TypeError, - r"unsupported operand type\(s\) " - "for .+: '.+' and '.+'"): - df.eval('a {0} b'.format(op), engine=engine, parser=parser) + with tm.assert_raises_regex(TypeError, + r"unsupported operand type\(s\) " + "for .+: '.+' and '.+'"): + df.eval('a {0} b'.format(op), engine=engine, parser=parser) diff --git a/pandas/tests/frame/test_replace.py b/pandas/tests/frame/test_replace.py index dd83a94b7062a4..68d799c55637cb 100644 --- a/pandas/tests/frame/test_replace.py +++ b/pandas/tests/frame/test_replace.py @@ -547,14 +547,12 @@ def test_regex_replace_numeric_to_object_conversion(self): assert_frame_equal(res, expec) assert res.a.dtype == np.object_ - def test_replace_regex_metachar(self): - metachars = '[]', '()', r'\d', r'\w', r'\s' - - for metachar in metachars: - df = DataFrame({'a': [metachar, 'else']}) - result = df.replace({'a': {metachar: 'paren'}}) - expected = DataFrame({'a': ['paren', 'else']}) - assert_frame_equal(result, expected) + @pytest.mark.parametrize('metachar', ['[]', '()', r'\d', r'\w', r'\s']) + def test_replace_regex_metachar(self, metachar): + df = DataFrame({'a': [metachar, 'else']}) + result = df.replace({'a': {metachar: 'paren'}}) + expected = DataFrame({'a': ['paren', 'else']}) + assert_frame_equal(result, expected) def test_replace(self): self.tsframe['A'][:5] = nan diff --git a/pandas/tests/frame/test_reshape.py b/pandas/tests/frame/test_reshape.py index ebf6c5e37b9162..2f90d24f652cad 100644 --- a/pandas/tests/frame/test_reshape.py +++ b/pandas/tests/frame/test_reshape.py @@ -855,21 +855,21 @@ def _test_stack_with_multiindex(multiindex): dtype=df.dtypes[0]) assert_frame_equal(result, expected) - def test_stack_preserve_categorical_dtype(self): + @pytest.mark.parametrize('ordered', [False, True]) + @pytest.mark.parametrize('labels', [list("yxz"), list("yxy")]) + def test_stack_preserve_categorical_dtype(self, ordered, labels): # GH13854 - for ordered in [False, True]: - for labels in [list("yxz"), list("yxy")]: - cidx = pd.CategoricalIndex(labels, categories=list("xyz"), - ordered=ordered) - df = DataFrame([[10, 11, 12]], columns=cidx) - result = df.stack() - - # `MutliIndex.from_product` preserves categorical dtype - - # it's tested elsewhere. - midx = pd.MultiIndex.from_product([df.index, cidx]) - expected = Series([10, 11, 12], index=midx) - - tm.assert_series_equal(result, expected) + cidx = pd.CategoricalIndex(labels, categories=list("xyz"), + ordered=ordered) + df = DataFrame([[10, 11, 12]], columns=cidx) + result = df.stack() + + # `MutliIndex.from_product` preserves categorical dtype - + # it's tested elsewhere. + midx = pd.MultiIndex.from_product([df.index, cidx]) + expected = Series([10, 11, 12], index=midx) + + tm.assert_series_equal(result, expected) @pytest.mark.parametrize("level", [0, 'baz']) def test_unstack_swaplevel_sortlevel(self, level): diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 4a17b2efd1dece..7a97d4ecaa8d57 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -1041,9 +1041,10 @@ def test_add_raises(self): dt1 + dt2 boxes = [lambda x: x, lambda x: pd.Series([x]), lambda x: pd.Index([x])] + ids = ['identity', 'Series', 'Index'] - @pytest.mark.parametrize('lbox', boxes) - @pytest.mark.parametrize('rbox', boxes) + @pytest.mark.parametrize('lbox', boxes, ids=ids) + @pytest.mark.parametrize('rbox', boxes, ids=ids) def test_add_timestamp_raises(self, rbox, lbox): # GH # 17983 ts = pd.Timestamp('2017') diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py index 840c80d6775a55..ed3191cf849c0d 100644 --- a/pandas/tests/series/test_alter_axes.py +++ b/pandas/tests/series/test_alter_axes.py @@ -237,6 +237,23 @@ def test_rename_axis_inplace(self): assert no_return is None assert_series_equal(result, expected) + def test_set_axis_inplace_axes(self, axis_series): + # GH14636 + ser = Series(np.arange(4), index=[1, 3, 5, 7], dtype='int64') + + expected = ser.copy() + expected.index = list('abcd') + + # inplace=True + # The FutureWarning comes from the fact that we would like to have + # inplace default to False some day + for inplace, warn in [(None, FutureWarning), (True, None)]: + result = ser.copy() + kwargs = {'inplace': inplace} + with tm.assert_produces_warning(warn): + result.set_axis(list('abcd'), axis=axis_series, **kwargs) + tm.assert_series_equal(result, expected) + def test_set_axis_inplace(self): # GH14636 @@ -245,17 +262,6 @@ def test_set_axis_inplace(self): expected = s.copy() expected.index = list('abcd') - for axis in 0, 'index': - # inplace=True - # The FutureWarning comes from the fact that we would like to have - # inplace default to False some day - for inplace, warn in (None, FutureWarning), (True, None): - result = s.copy() - kwargs = {'inplace': inplace} - with tm.assert_produces_warning(warn): - result.set_axis(list('abcd'), axis=axis, **kwargs) - tm.assert_series_equal(result, expected) - # inplace=False result = s.set_axis(list('abcd'), axis=0, inplace=False) tm.assert_series_equal(expected, result) @@ -266,7 +272,7 @@ def test_set_axis_inplace(self): tm.assert_series_equal(result, expected) # wrong values for the "axis" parameter - for axis in 2, 'foo': + for axis in [2, 'foo']: with tm.assert_raises_regex(ValueError, 'No axis named'): s.set_axis(list('abcd'), axis=axis, inplace=False) @@ -276,7 +282,7 @@ def test_set_axis_prior_to_deprecation_signature(self): expected = s.copy() expected.index = list('abcd') - for axis in 0, 'index': + for axis in [0, 'index']: with tm.assert_produces_warning(FutureWarning): result = s.set_axis(0, list('abcd'), inplace=False) tm.assert_series_equal(result, expected) diff --git a/pandas/tests/series/test_datetime_values.py b/pandas/tests/series/test_datetime_values.py index 47798d0ddd7f5e..7a02ce3a1fb2e7 100644 --- a/pandas/tests/series/test_datetime_values.py +++ b/pandas/tests/series/test_datetime_values.py @@ -255,12 +255,9 @@ def get_dir(s): # trying to set a copy with pd.option_context('chained_assignment', 'raise'): - - def f(): + with pytest.raises(com.SettingWithCopyError): s.dt.hour[0] = 5 - pytest.raises(com.SettingWithCopyError, f) - def test_dt_namespace_accessor_categorical(self): # GH 19468 dti = DatetimeIndex(['20171111', '20181212']).repeat(2) @@ -420,12 +417,14 @@ def test_dt_accessor_api(self): s = Series(date_range('2000-01-01', periods=3)) assert isinstance(s.dt, DatetimeProperties) - for s in [Series(np.arange(5)), Series(list('abcde')), - Series(np.random.randn(5))]: - with tm.assert_raises_regex(AttributeError, - "only use .dt accessor"): - s.dt - assert not hasattr(s, 'dt') + @pytest.mark.parametrize('ser', [Series(np.arange(5)), + Series(list('abcde')), + Series(np.random.randn(5))]) + def test_dt_accessor_invalid(self, ser): + # GH#9322 check that series with incorrect dtypes don't have attr + with tm.assert_raises_regex(AttributeError, "only use .dt accessor"): + ser.dt + assert not hasattr(ser, 'dt') def test_between(self): s = Series(bdate_range('1/1/2000', periods=20).astype(object)) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index ecb74622edf10d..fad2b025dd3e42 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -136,10 +136,14 @@ def test_categorical_comparisons(self): assert ((~(f == a) == (f != a)).all()) # non-equality is not comparable - pytest.raises(TypeError, lambda: a < b) - pytest.raises(TypeError, lambda: b < a) - pytest.raises(TypeError, lambda: a > b) - pytest.raises(TypeError, lambda: b > a) + with pytest.raises(TypeError): + a < b + with pytest.raises(TypeError): + b < a + with pytest.raises(TypeError): + a > b + with pytest.raises(TypeError): + b > a def test_comparison_tuples(self): # GH11339 @@ -204,20 +208,21 @@ def test_comparison_operators_with_nas(self): # expected = f(val, s.dropna()).reindex(s.index) # assert_series_equal(result, expected) - # boolean &, |, ^ should work with object arrays and propagate NAs + @pytest.mark.parametrize('bool_op', [operator.and_, + operator.or_, operator.xor]) + def test_bool_operators_with_nas(self, bool_op): + # boolean &, |, ^ should work with object arrays and propagate NAs + ser = Series(bdate_range('1/1/2000', periods=10), dtype=object) + ser[::2] = np.nan - ops = ['and_', 'or_', 'xor'] mask = ser.isna() - for bool_op in ops: - func = getattr(operator, bool_op) - - filled = ser.fillna(ser[0]) + filled = ser.fillna(ser[0]) - result = func(ser < ser[9], ser > ser[3]) + result = bool_op(ser < ser[9], ser > ser[3]) - expected = func(filled < filled[9], filled > filled[3]) - expected[mask] = False - assert_series_equal(result, expected) + expected = bool_op(filled < filled[9], filled > filled[3]) + expected[mask] = False + assert_series_equal(result, expected) def test_comparison_object_numeric_nas(self): ser = Series(np.random.randn(10), dtype=object) @@ -248,27 +253,26 @@ def test_comparison_invalid(self): def test_unequal_categorical_comparison_raises_type_error(self): # unequal comparison should raise for unordered cats cat = Series(Categorical(list("abc"))) - - def f(): + with pytest.raises(TypeError): cat > "b" - pytest.raises(TypeError, f) cat = Series(Categorical(list("abc"), ordered=False)) - - def f(): + with pytest.raises(TypeError): cat > "b" - pytest.raises(TypeError, f) - # https://github.com/pandas-dev/pandas/issues/9836#issuecomment-92123057 # and following comparisons with scalars not in categories should raise # for unequal comps, but not for equal/not equal cat = Series(Categorical(list("abc"), ordered=True)) - pytest.raises(TypeError, lambda: cat < "d") - pytest.raises(TypeError, lambda: cat > "d") - pytest.raises(TypeError, lambda: "d" < cat) - pytest.raises(TypeError, lambda: "d" > cat) + with pytest.raises(TypeError): + cat < "d" + with pytest.raises(TypeError): + cat > "d" + with pytest.raises(TypeError): + "d" < cat + with pytest.raises(TypeError): + "d" > cat tm.assert_series_equal(cat == "d", Series([False, False, False])) tm.assert_series_equal(cat != "d", Series([True, True, True])) @@ -365,11 +369,13 @@ def test_nat_comparisons_scalar(self, dtype, data): def test_comparison_different_length(self): a = Series(['a', 'b', 'c']) b = Series(['b', 'a']) - pytest.raises(ValueError, a.__lt__, b) + with pytest.raises(ValueError): + a < b a = Series([1, 2]) b = Series([2, 3, 4]) - pytest.raises(ValueError, a.__eq__, b) + with pytest.raises(ValueError): + a == b def test_comparison_label_based(self): @@ -448,7 +454,8 @@ def test_comparison_label_based(self): assert_series_equal(result, expected) for v in [np.nan, 'foo']: - pytest.raises(TypeError, lambda: t | v) + with pytest.raises(TypeError): + t | v for v in [False, 0]: result = Series([True, False, True], index=index) | v @@ -465,7 +472,8 @@ def test_comparison_label_based(self): expected = Series([False, False, False], index=index) assert_series_equal(result, expected) for v in [np.nan]: - pytest.raises(TypeError, lambda: t & v) + with pytest.raises(TypeError): + t & v def test_comparison_flex_basic(self): left = pd.Series(np.random.randn(10)) @@ -930,12 +938,14 @@ def test_operators_datetimelike_with_timezones(self): result = dt1 - td1[0] exp = (dt1.dt.tz_localize(None) - td1[0]).dt.tz_localize(tz) assert_series_equal(result, exp) - pytest.raises(TypeError, lambda: td1[0] - dt1) + with pytest.raises(TypeError): + td1[0] - dt1 result = dt2 - td2[0] exp = (dt2.dt.tz_localize(None) - td2[0]).dt.tz_localize(tz) assert_series_equal(result, exp) - pytest.raises(TypeError, lambda: td2[0] - dt2) + with pytest.raises(TypeError): + td2[0] - dt2 result = dt1 + td1 exp = (dt1.dt.tz_localize(None) + td1).dt.tz_localize(tz) @@ -953,8 +963,10 @@ def test_operators_datetimelike_with_timezones(self): exp = (dt2.dt.tz_localize(None) - td2).dt.tz_localize(tz) assert_series_equal(result, exp) - pytest.raises(TypeError, lambda: td1 - dt1) - pytest.raises(TypeError, lambda: td2 - dt2) + with pytest.raises(TypeError): + td1 - dt1 + with pytest.raises(TypeError): + td2 - dt2 def test_sub_single_tz(self): # GH12290 @@ -1483,11 +1495,16 @@ def test_operators_bitwise(self): expected = Series([1, 1, 3, 3], dtype='int32') assert_series_equal(res, expected) - pytest.raises(TypeError, lambda: s_1111 & 'a') - pytest.raises(TypeError, lambda: s_1111 & ['a', 'b', 'c', 'd']) - pytest.raises(TypeError, lambda: s_0123 & np.NaN) - pytest.raises(TypeError, lambda: s_0123 & 3.14) - pytest.raises(TypeError, lambda: s_0123 & [0.1, 4, 3.14, 2]) + with pytest.raises(TypeError): + s_1111 & 'a' + with pytest.raises(TypeError): + s_1111 & ['a', 'b', 'c', 'd'] + with pytest.raises(TypeError): + s_0123 & np.NaN + with pytest.raises(TypeError): + s_0123 & 3.14 + with pytest.raises(TypeError): + s_0123 & [0.1, 4, 3.14, 2] # s_0123 will be all false now because of reindexing like s_tft if compat.PY3: @@ -1530,14 +1547,16 @@ def test_scalar_na_cmp_corners(self): def tester(a, b): return a & b - pytest.raises(TypeError, tester, s, datetime(2005, 1, 1)) + with pytest.raises(TypeError): + s & datetime(2005, 1, 1) s = Series([2, 3, 4, 5, 6, 7, 8, 9, datetime(2005, 1, 1)]) s[::2] = np.nan expected = Series(True, index=s.index) expected[::2] = False - assert_series_equal(tester(s, list(s)), expected) + result = s & list(s) + assert_series_equal(result, expected) d = DataFrame({'A': s}) # TODO: Fix this exception - needs to be fixed! (see GH5035) @@ -1587,7 +1606,25 @@ def test_operators_reverse_object(self, op): expected = op(1., arr.astype(float)) assert_series_equal(result.astype(float), expected) - def test_operators_combine(self): + pairings = [] + for op in ['add', 'sub', 'mul', 'pow', 'truediv', 'floordiv']: + fv = 0 + lop = getattr(Series, op) + lequiv = getattr(operator, op) + rop = getattr(Series, 'r' + op) + # bind op at definition time... + requiv = lambda x, y, op=op: getattr(operator, op)(y, x) + pairings.append((lop, lequiv, fv)) + pairings.append((rop, requiv, fv)) + if compat.PY3: + pairings.append((Series.div, operator.truediv, 1)) + pairings.append((Series.rdiv, lambda x, y: operator.truediv(y, x), 1)) + else: + pairings.append((Series.div, operator.div, 1)) + pairings.append((Series.rdiv, lambda x, y: operator.div(y, x), 1)) + + @pytest.mark.parametrize('op, equiv_op, fv', pairings) + def test_operators_combine(self, op, equiv_op, fv): def _check_fill(meth, op, a, b, fill_value=0): exp_index = a.index.union(b.index) a = a.reindex(exp_index) @@ -1619,32 +1656,12 @@ def _check_fill(meth, op, a, b, fill_value=0): a = Series([nan, 1., 2., 3., nan], index=np.arange(5)) b = Series([nan, 1, nan, 3, nan, 4.], index=np.arange(6)) - pairings = [] - for op in ['add', 'sub', 'mul', 'pow', 'truediv', 'floordiv']: - fv = 0 - lop = getattr(Series, op) - lequiv = getattr(operator, op) - rop = getattr(Series, 'r' + op) - # bind op at definition time... - requiv = lambda x, y, op=op: getattr(operator, op)(y, x) - pairings.append((lop, lequiv, fv)) - pairings.append((rop, requiv, fv)) - - if compat.PY3: - pairings.append((Series.div, operator.truediv, 1)) - pairings.append((Series.rdiv, lambda x, y: operator.truediv(y, x), - 1)) - else: - pairings.append((Series.div, operator.div, 1)) - pairings.append((Series.rdiv, lambda x, y: operator.div(y, x), 1)) - - for op, equiv_op, fv in pairings: - result = op(a, b) - exp = equiv_op(a, b) - assert_series_equal(result, exp) - _check_fill(op, equiv_op, a, b, fill_value=fv) - # should accept axis=0 or axis='rows' - op(a, b, axis=0) + result = op(a, b) + exp = equiv_op(a, b) + assert_series_equal(result, exp) + _check_fill(op, equiv_op, a, b, fill_value=fv) + # should accept axis=0 or axis='rows' + op(a, b, axis=0) def test_operators_na_handling(self): from decimal import Decimal diff --git a/pandas/tests/series/test_period.py b/pandas/tests/series/test_period.py index 90dbe26a2f0ea3..b8b7a29082867b 100644 --- a/pandas/tests/series/test_period.py +++ b/pandas/tests/series/test_period.py @@ -1,4 +1,5 @@ import numpy as np +import pytest import pandas as pd import pandas.util.testing as tm @@ -73,22 +74,23 @@ def test_between(self): # --------------------------------------------------------------------- # NaT support - """ - # ToDo: Enable when support period dtype + @pytest.mark.xfail(reason="PeriodDtype Series not supported yet", + strict=True) def test_NaT_scalar(self): - series = Series([0, 1000, 2000, iNaT], dtype='period[D]') + series = Series([0, 1000, 2000, pd._libs.iNaT], dtype='period[D]') val = series[3] - assert isna(val) + assert pd.isna(val) series[2] = val - assert isna(series[2]) + assert pd.isna(series[2]) + @pytest.mark.xfail(reason="PeriodDtype Series not supported yet", + strict=True) def test_NaT_cast(self): result = Series([np.nan]).astype('period[D]') - expected = Series([NaT]) + expected = Series([pd.NaT]) tm.assert_series_equal(result, expected) - """ def test_set_none_nan(self): # currently Period is stored as object dtype, not as NaT diff --git a/pandas/tests/series/test_quantile.py b/pandas/tests/series/test_quantile.py index 3c93ff1d3f31eb..df8799cf5c9007 100644 --- a/pandas/tests/series/test_quantile.py +++ b/pandas/tests/series/test_quantile.py @@ -1,6 +1,8 @@ # coding=utf-8 # pylint: disable-msg=E1101,W0612 +import pytest + import numpy as np import pandas as pd @@ -113,31 +115,30 @@ def test_quantile_nan(self): tm.assert_series_equal(res, pd.Series([np.nan, np.nan], index=[0.2, 0.3])) - def test_quantile_box(self): - cases = [[pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.Timestamp('2011-01-03')], - [pd.Timestamp('2011-01-01', tz='US/Eastern'), - pd.Timestamp('2011-01-02', tz='US/Eastern'), - pd.Timestamp('2011-01-03', tz='US/Eastern')], - [pd.Timedelta('1 days'), pd.Timedelta('2 days'), - pd.Timedelta('3 days')], - # NaT - [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), - pd.Timestamp('2011-01-03'), pd.NaT], - [pd.Timestamp('2011-01-01', tz='US/Eastern'), - pd.Timestamp('2011-01-02', tz='US/Eastern'), - pd.Timestamp('2011-01-03', tz='US/Eastern'), pd.NaT], - [pd.Timedelta('1 days'), pd.Timedelta('2 days'), - pd.Timedelta('3 days'), pd.NaT]] - - for case in cases: - s = pd.Series(case, name='XXX') - res = s.quantile(0.5) - assert res == case[1] + @pytest.mark.parametrize('case', [ + [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), + pd.Timestamp('2011-01-03')], + [pd.Timestamp('2011-01-01', tz='US/Eastern'), + pd.Timestamp('2011-01-02', tz='US/Eastern'), + pd.Timestamp('2011-01-03', tz='US/Eastern')], + [pd.Timedelta('1 days'), pd.Timedelta('2 days'), + pd.Timedelta('3 days')], + # NaT + [pd.Timestamp('2011-01-01'), pd.Timestamp('2011-01-02'), + pd.Timestamp('2011-01-03'), pd.NaT], + [pd.Timestamp('2011-01-01', tz='US/Eastern'), + pd.Timestamp('2011-01-02', tz='US/Eastern'), + pd.Timestamp('2011-01-03', tz='US/Eastern'), pd.NaT], + [pd.Timedelta('1 days'), pd.Timedelta('2 days'), + pd.Timedelta('3 days'), pd.NaT]]) + def test_quantile_box(self, case): + s = pd.Series(case, name='XXX') + res = s.quantile(0.5) + assert res == case[1] - res = s.quantile([0.5]) - exp = pd.Series([case[1]], index=[0.5], name='XXX') - tm.assert_series_equal(res, exp) + res = s.quantile([0.5]) + exp = pd.Series([case[1]], index=[0.5], name='XXX') + tm.assert_series_equal(res, exp) def test_datetime_timedelta_quantiles(self): # covers #9694 diff --git a/pandas/tests/series/test_timeseries.py b/pandas/tests/series/test_timeseries.py index 376b4d71f81e8a..72492de4b12473 100644 --- a/pandas/tests/series/test_timeseries.py +++ b/pandas/tests/series/test_timeseries.py @@ -78,7 +78,8 @@ def test_shift(self): assert_series_equal(shifted2, shifted3) assert_series_equal(ps, shifted2.shift(-1, 'B')) - pytest.raises(ValueError, ps.shift, freq='D') + with pytest.raises(ValueError): + ps.shift(freq='D') # legacy support shifted4 = ps.shift(1, freq='B') @@ -109,7 +110,8 @@ def test_shift(self): # incompat tz s2 = Series(date_range('2000-01-01 09:00:00', periods=5, tz='CET'), name='foo') - pytest.raises(TypeError, lambda: s - s2) + with pytest.raises(TypeError): + s - s2 def test_shift2(self): ts = Series(np.random.randn(5), @@ -168,7 +170,8 @@ def test_tshift(self): shifted3 = ps.tshift(freq=BDay()) assert_series_equal(shifted, shifted3) - pytest.raises(ValueError, ps.tshift, freq='M') + with pytest.raises(ValueError): + ps.tshift(freq='M') # DatetimeIndex shifted = self.ts.tshift(1) @@ -187,7 +190,8 @@ def test_tshift(self): assert_series_equal(unshifted, inferred_ts) no_freq = self.ts[[0, 5, 7]] - pytest.raises(ValueError, no_freq.tshift) + with pytest.raises(ValueError): + no_freq.tshift() def test_truncate(self): offset = BDay() @@ -459,7 +463,8 @@ def test_empty_series_ops(self): assert_series_equal(a, a + b) assert_series_equal(a, a - b) assert_series_equal(a, b + a) - pytest.raises(TypeError, lambda x, y: x - y, b, a) + with pytest.raises(TypeError): + b - a def test_contiguous_boolean_preserve_freq(self): rng = date_range('1/1/2000', '3/1/2000', freq='B') @@ -791,16 +796,19 @@ def test_between_time_raises(self): def test_between_time_types(self): # GH11818 rng = date_range('1/1/2000', '1/5/2000', freq='5min') - pytest.raises(ValueError, rng.indexer_between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + with pytest.raises(ValueError): + rng.indexer_between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) frame = DataFrame({'A': 0}, index=rng) - pytest.raises(ValueError, frame.between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + with pytest.raises(ValueError): + frame.between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) series = Series(0, index=rng) - pytest.raises(ValueError, series.between_time, - datetime(2010, 1, 2, 1), datetime(2010, 1, 2, 5)) + with pytest.raises(ValueError): + series.between_time(datetime(2010, 1, 2, 1), + datetime(2010, 1, 2, 5)) @td.skip_if_has_locale def test_between_time_formats(self): @@ -921,40 +929,40 @@ def test_pickle(self): idx_p = tm.round_trip_pickle(idx) tm.assert_index_equal(idx, idx_p) - def test_setops_preserve_freq(self): - for tz in [None, 'Asia/Tokyo', 'US/Eastern']: - rng = date_range('1/1/2000', '1/1/2002', name='idx', tz=tz) - - result = rng[:50].union(rng[50:100]) - assert result.name == rng.name - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].union(rng[30:100]) - assert result.name == rng.name - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].union(rng[60:100]) - assert result.name == rng.name - assert result.freq is None - assert result.tz == rng.tz - - result = rng[:50].intersection(rng[25:75]) - assert result.name == rng.name - assert result.freqstr == 'D' - assert result.tz == rng.tz - - nofreq = DatetimeIndex(list(rng[25:75]), name='other') - result = rng[:50].union(nofreq) - assert result.name is None - assert result.freq == rng.freq - assert result.tz == rng.tz - - result = rng[:50].intersection(nofreq) - assert result.name is None - assert result.freq == rng.freq - assert result.tz == rng.tz + @pytest.mark.parametrize('tz', [None, 'Asia/Tokyo', 'US/Eastern']) + def test_setops_preserve_freq(self, tz): + rng = date_range('1/1/2000', '1/1/2002', name='idx', tz=tz) + + result = rng[:50].union(rng[50:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].union(rng[30:100]) + assert result.name == rng.name + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].union(rng[60:100]) + assert result.name == rng.name + assert result.freq is None + assert result.tz == rng.tz + + result = rng[:50].intersection(rng[25:75]) + assert result.name == rng.name + assert result.freqstr == 'D' + assert result.tz == rng.tz + + nofreq = DatetimeIndex(list(rng[25:75]), name='other') + result = rng[:50].union(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz + + result = rng[:50].intersection(nofreq) + assert result.name is None + assert result.freq == rng.freq + assert result.tz == rng.tz def test_min_max(self): rng = date_range('1/1/2000', '12/31/2000') From 4f0392d3de7e507eeefd1cd624a5051b1576ad95 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 31 Jul 2018 08:22:47 -0500 Subject: [PATCH 18/47] REF/API: Stricter extension checking. (#22031) --- pandas/core/dtypes/common.py | 21 +++++++-------------- pandas/core/dtypes/dtypes.py | 13 ++++++++----- pandas/tests/dtypes/test_dtypes.py | 24 +++++++++++++++++------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 905073645fcb39..4a0bf67f47bae1 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -9,7 +9,8 @@ from pandas.core.dtypes.dtypes import ( registry, CategoricalDtype, CategoricalDtypeType, DatetimeTZDtype, DatetimeTZDtypeType, PeriodDtype, PeriodDtypeType, IntervalDtype, - IntervalDtypeType, ExtensionDtype) + IntervalDtypeType, PandasExtensionDtype, ExtensionDtype, + _pandas_registry) from pandas.core.dtypes.generic import ( ABCCategorical, ABCPeriodIndex, ABCDatetimeIndex, ABCSeries, ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex, ABCIndexClass, @@ -1709,17 +1710,9 @@ def is_extension_array_dtype(arr_or_dtype): Third-party libraries may implement arrays or types satisfying this interface as well. """ - from pandas.core.arrays import ExtensionArray - - if isinstance(arr_or_dtype, (ABCIndexClass, ABCSeries)): - arr_or_dtype = arr_or_dtype._values - - try: - arr_or_dtype = pandas_dtype(arr_or_dtype) - except TypeError: - pass - - return isinstance(arr_or_dtype, (ExtensionDtype, ExtensionArray)) + dtype = getattr(arr_or_dtype, 'dtype', arr_or_dtype) + return (isinstance(dtype, ExtensionDtype) or + registry.find(dtype) is not None) def is_complex_dtype(arr_or_dtype): @@ -1999,12 +1992,12 @@ def pandas_dtype(dtype): return dtype # registered extension types - result = registry.find(dtype) + result = _pandas_registry.find(dtype) or registry.find(dtype) if result is not None: return result # un-registered extension types - elif isinstance(dtype, ExtensionDtype): + elif isinstance(dtype, (PandasExtensionDtype, ExtensionDtype)): return dtype # try a numpy dtype diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index cf771a127a6966..f53ccc86fc4ff6 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -22,9 +22,9 @@ class Registry(object): -------- registry.register(MyExtensionDtype) """ - dtypes = [] + def __init__(self): + self.dtypes = [] - @classmethod def register(self, dtype): """ Parameters @@ -50,7 +50,7 @@ def find(self, dtype): dtype_type = dtype if not isinstance(dtype, type): dtype_type = type(dtype) - if issubclass(dtype_type, (PandasExtensionDtype, ExtensionDtype)): + if issubclass(dtype_type, ExtensionDtype): return dtype return None @@ -65,6 +65,9 @@ def find(self, dtype): registry = Registry() +# TODO(Extension): remove the second registry once all internal extension +# dtypes are real extension dtypes. +_pandas_registry = Registry() class PandasExtensionDtype(_DtypeOpsMixin): @@ -822,7 +825,7 @@ def is_dtype(cls, dtype): # register the dtypes in search order -registry.register(DatetimeTZDtype) -registry.register(PeriodDtype) registry.register(IntervalDtype) registry.register(CategoricalDtype) +_pandas_registry.register(DatetimeTZDtype) +_pandas_registry.register(PeriodDtype) diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 02ac7fc7d5ed7c..55c841ba1fc46b 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -9,7 +9,7 @@ from pandas.core.dtypes.dtypes import ( DatetimeTZDtype, PeriodDtype, - IntervalDtype, CategoricalDtype, registry) + IntervalDtype, CategoricalDtype, registry, _pandas_registry) from pandas.core.dtypes.common import ( is_categorical_dtype, is_categorical, is_datetime64tz_dtype, is_datetimetz, @@ -775,21 +775,31 @@ def test_update_dtype_errors(self, bad_dtype): @pytest.mark.parametrize( 'dtype', - [DatetimeTZDtype, CategoricalDtype, - PeriodDtype, IntervalDtype]) + [CategoricalDtype, IntervalDtype]) def test_registry(dtype): assert dtype in registry.dtypes +@pytest.mark.parametrize('dtype', [DatetimeTZDtype, PeriodDtype]) +def test_pandas_registry(dtype): + assert dtype not in registry.dtypes + assert dtype in _pandas_registry.dtypes + + @pytest.mark.parametrize( 'dtype, expected', [('int64', None), ('interval', IntervalDtype()), ('interval[int64]', IntervalDtype()), ('interval[datetime64[ns]]', IntervalDtype('datetime64[ns]')), - ('category', CategoricalDtype()), - ('period[D]', PeriodDtype('D')), - ('datetime64[ns, US/Eastern]', DatetimeTZDtype('ns', 'US/Eastern'))]) + ('category', CategoricalDtype())]) def test_registry_find(dtype, expected): - assert registry.find(dtype) == expected + + +@pytest.mark.parametrize( + 'dtype, expected', + [('period[D]', PeriodDtype('D')), + ('datetime64[ns, US/Eastern]', DatetimeTZDtype('ns', 'US/Eastern'))]) +def test_pandas_registry_find(dtype, expected): + assert _pandas_registry.find(dtype) == expected From 48d4a1c47921e1251f26a2f0d70d2f0fb56a9e5e Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 31 Jul 2018 06:24:32 -0700 Subject: [PATCH 19/47] Remove unused, avoid uses of deprecated api (#22071) --- pandas/_libs/algos.pyx | 2 +- pandas/_libs/groupby.pyx | 4 +- pandas/_libs/index.pyx | 6 +- pandas/_libs/src/numpy_helper.h | 14 +- pandas/_libs/tslibs/conversion.pyx | 2 +- pandas/_libs/tslibs/util.pxd | 243 +++++++++++++++++------------ pandas/_libs/util.pxd | 80 ++++++++++ 7 files changed, 237 insertions(+), 114 deletions(-) diff --git a/pandas/_libs/algos.pyx b/pandas/_libs/algos.pyx index ecfc7355dddfcd..124792638e3df2 100644 --- a/pandas/_libs/algos.pyx +++ b/pandas/_libs/algos.pyx @@ -129,7 +129,7 @@ def is_lexsorted(list list_of_arrays): for i in range(nlevels): arr = list_of_arrays[i] assert arr.dtype.name == 'int64' - vecs[i] = arr.data + vecs[i] = cnp.PyArray_DATA(arr) # Assume uniqueness?? with nogil: diff --git a/pandas/_libs/groupby.pyx b/pandas/_libs/groupby.pyx index 5e4a431caca003..5681d01c6bb25c 100644 --- a/pandas/_libs/groupby.pyx +++ b/pandas/_libs/groupby.pyx @@ -7,10 +7,12 @@ from cython cimport Py_ssize_t from libc.stdlib cimport malloc, free import numpy as np +cimport numpy as cnp from numpy cimport (ndarray, double_t, int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t, float32_t, float64_t) +cnp.import_array() from util cimport numeric, get_nat @@ -118,7 +120,7 @@ def group_median_float64(ndarray[float64_t, ndim=2] out, counts[:] = _counts[1:] data = np.empty((K, N), dtype=np.float64) - ptr = data.data + ptr = cnp.PyArray_DATA(data) take_2d_axis1_float64_float64(values.T, indexer, out=data) diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 31ef4b7a3e807a..5918560cf14365 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -37,7 +37,7 @@ cdef inline bint is_definitely_invalid_key(object val): return True # we have a _data, means we are a NDFrame - return (PySlice_Check(val) or cnp.PyArray_Check(val) + return (PySlice_Check(val) or util.is_array(val) or PyList_Check(val) or hasattr(val, '_data')) @@ -104,7 +104,7 @@ cdef class IndexEngine: void* data_ptr loc = self.get_loc(key) - if PySlice_Check(loc) or cnp.PyArray_Check(loc): + if PySlice_Check(loc) or util.is_array(loc): return arr[loc] else: return get_value_at(arr, loc, tz=tz) @@ -120,7 +120,7 @@ cdef class IndexEngine: loc = self.get_loc(key) value = convert_scalar(arr, value) - if PySlice_Check(loc) or cnp.PyArray_Check(loc): + if PySlice_Check(loc) or util.is_array(loc): arr[loc] = value else: util.set_value_at(arr, loc, value) diff --git a/pandas/_libs/src/numpy_helper.h b/pandas/_libs/src/numpy_helper.h index 98eca92fd1ab2c..753cba6ce62aa0 100644 --- a/pandas/_libs/src/numpy_helper.h +++ b/pandas/_libs/src/numpy_helper.h @@ -16,8 +16,6 @@ The full license is in the LICENSE file, distributed with this software. #include "numpy/arrayscalars.h" -PANDAS_INLINE npy_int64 get_nat(void) { return NPY_MIN_INT64; } - PANDAS_INLINE int assign_value_1d(PyArrayObject* ap, Py_ssize_t _i, PyObject* v) { npy_intp i = (npy_intp)_i; @@ -40,16 +38,10 @@ PANDAS_INLINE const char* get_c_string(PyObject* obj) { #endif } -PANDAS_INLINE PyObject* char_to_string(const char* data) { -#if PY_VERSION_HEX >= 0x03000000 - return PyUnicode_FromString(data); -#else - return PyString_FromString(data); -#endif -} - void set_array_not_contiguous(PyArrayObject* ao) { - ao->flags &= ~(NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS); + // Numpy>=1.8-compliant equivalent to: + // ao->flags &= ~(NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS); + PyArray_CLEARFLAGS(ao, (NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS)); } #endif // PANDAS__LIBS_SRC_NUMPY_HELPER_H_ diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 7621ac912d4d59..4335e7baeafe96 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -888,7 +888,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, trans, deltas, typ = get_dst_info(tz) - tdata = trans.data + tdata = cnp.PyArray_DATA(trans) ntrans = len(trans) result_a = np.empty(n, dtype=np.int64) diff --git a/pandas/_libs/tslibs/util.pxd b/pandas/_libs/tslibs/util.pxd index efdb1570ed8786..624ed7ced26543 100644 --- a/pandas/_libs/tslibs/util.pxd +++ b/pandas/_libs/tslibs/util.pxd @@ -1,10 +1,18 @@ -from numpy cimport ndarray -cimport numpy as cnp -cnp.import_array() -cimport cpython from cpython cimport PyTypeObject +cdef extern from *: + """ + PyObject* char_to_string(const char* data) { + #if PY_VERSION_HEX >= 0x03000000 + return PyUnicode_FromString(data); + #else + return PyString_FromString(data); + #endif + } + """ + object char_to_string(const char* data) + cdef extern from "Python.h": # Note: importing extern-style allows us to declare these as nogil @@ -19,6 +27,8 @@ cdef extern from "Python.h": cdef extern from "numpy/arrayobject.h": PyTypeObject PyFloatingArrType_Type + ctypedef signed long long int64_t + int _import_array() except -1 cdef extern from "numpy/ndarrayobject.h": PyTypeObject PyTimedeltaArrType_Type @@ -29,142 +39,177 @@ cdef extern from "numpy/ndarrayobject.h": bint PyArray_IsIntegerScalar(obj) nogil bint PyArray_Check(obj) nogil +cdef extern from "numpy/npy_common.h": + int64_t NPY_MIN_INT64 + + +cdef extern from "../src/headers/stdint.h": + enum: UINT8_MAX + enum: UINT16_MAX + enum: UINT32_MAX + enum: UINT64_MAX + enum: INT8_MIN + enum: INT8_MAX + enum: INT16_MIN + enum: INT16_MAX + enum: INT32_MAX + enum: INT32_MIN + enum: INT64_MAX + enum: INT64_MIN + + +cdef inline int64_t get_nat(): + return NPY_MIN_INT64 + + +cdef inline int import_array() except -1: + _import_array() + + # -------------------------------------------------------------------- # Type Checking cdef inline bint is_string_object(object obj) nogil: + """ + Cython equivalent of `isinstance(val, compat.string_types)` + + Parameters + ---------- + val : object + + Returns + ------- + is_string : bool + """ return PyString_Check(obj) or PyUnicode_Check(obj) cdef inline bint is_integer_object(object obj) nogil: + """ + Cython equivalent of + + `isinstance(val, (int, long, np.integer)) and not isinstance(val, bool)` + + Parameters + ---------- + val : object + + Returns + ------- + is_integer : bool + + Notes + ----- + This counts np.timedelta64 objects as integers. + """ return not PyBool_Check(obj) and PyArray_IsIntegerScalar(obj) cdef inline bint is_float_object(object obj) nogil: + """ + Cython equivalent of `isinstance(val, (float, np.complex_))` + + Parameters + ---------- + val : object + + Returns + ------- + is_float : bool + """ return (PyFloat_Check(obj) or (PyObject_TypeCheck(obj, &PyFloatingArrType_Type))) cdef inline bint is_complex_object(object obj) nogil: + """ + Cython equivalent of `isinstance(val, (complex, np.complex_))` + + Parameters + ---------- + val : object + + Returns + ------- + is_complex : bool + """ return (PyComplex_Check(obj) or PyObject_TypeCheck(obj, &PyComplexFloatingArrType_Type)) cdef inline bint is_bool_object(object obj) nogil: + """ + Cython equivalent of `isinstance(val, (bool, np.bool_))` + + Parameters + ---------- + val : object + + Returns + ------- + is_bool : bool + """ return (PyBool_Check(obj) or PyObject_TypeCheck(obj, &PyBoolArrType_Type)) cdef inline bint is_timedelta64_object(object obj) nogil: - return PyObject_TypeCheck(obj, &PyTimedeltaArrType_Type) - - -cdef inline bint is_datetime64_object(object obj) nogil: - return PyObject_TypeCheck(obj, &PyDatetimeArrType_Type) - -# -------------------------------------------------------------------- - -cdef extern from "../src/numpy_helper.h": - void set_array_not_contiguous(ndarray ao) - - int assign_value_1d(ndarray, Py_ssize_t, object) except -1 - cnp.int64_t get_nat() - object get_value_1d(ndarray, Py_ssize_t) - const char *get_c_string(object) except NULL - object char_to_string(char*) - -ctypedef fused numeric: - cnp.int8_t - cnp.int16_t - cnp.int32_t - cnp.int64_t - - cnp.uint8_t - cnp.uint16_t - cnp.uint32_t - cnp.uint64_t - - cnp.float32_t - cnp.float64_t - -cdef extern from "../src/headers/stdint.h": - enum: UINT8_MAX - enum: UINT16_MAX - enum: UINT32_MAX - enum: UINT64_MAX - enum: INT8_MIN - enum: INT8_MAX - enum: INT16_MIN - enum: INT16_MAX - enum: INT32_MAX - enum: INT32_MIN - enum: INT64_MAX - enum: INT64_MIN - - -cdef inline object get_value_at(ndarray arr, object loc): - cdef: - Py_ssize_t i, sz - int casted + """ + Cython equivalent of `isinstance(val, np.timedelta64)` - if is_float_object(loc): - casted = int(loc) - if casted == loc: - loc = casted - i = loc - sz = cnp.PyArray_SIZE(arr) + Parameters + ---------- + val : object - if i < 0 and sz > 0: - i += sz - elif i >= sz or sz == 0: - raise IndexError('index out of bounds') + Returns + ------- + is_timedelta64 : bool + """ + return PyObject_TypeCheck(obj, &PyTimedeltaArrType_Type) - return get_value_1d(arr, i) +cdef inline bint is_datetime64_object(object obj) nogil: + """ + Cython equivalent of `isinstance(val, np.datetime64)` -cdef inline set_value_at_unsafe(ndarray arr, object loc, object value): - """Sets a value into the array without checking the writeable flag. + Parameters + ---------- + val : object - This should be used when setting values in a loop, check the writeable - flag above the loop and then eschew the check on each iteration. + Returns + ------- + is_datetime64 : bool """ - cdef: - Py_ssize_t i, sz - if is_float_object(loc): - casted = int(loc) - if casted == loc: - loc = casted - i = loc - sz = cnp.PyArray_SIZE(arr) - - if i < 0: - i += sz - elif i >= sz: - raise IndexError('index out of bounds') + return PyObject_TypeCheck(obj, &PyDatetimeArrType_Type) - assign_value_1d(arr, i, value) -cdef inline set_value_at(ndarray arr, object loc, object value): - """Sets a value into the array after checking that the array is mutable. +cdef inline bint is_array(object val): """ - if not cnp.PyArray_ISWRITEABLE(arr): - raise ValueError('assignment destination is read-only') - - set_value_at_unsafe(arr, loc, value) + Cython equivalent of `isinstance(val, np.ndarray)` + Parameters + ---------- + val : object -cdef inline is_array(object o): - return cnp.PyArray_Check(o) + Returns + ------- + is_ndarray : bool + """ + return PyArray_Check(val) -cdef inline bint _checknull(object val): - try: - return val is None or (cpython.PyFloat_Check(val) and val != val) - except ValueError: - return False +cdef inline bint is_period_object(object val): + """ + Cython equivalent of `isinstance(val, pd.Period)` + Parameters + ---------- + val : object -cdef inline bint is_period_object(object val): + Returns + ------- + is_period : bool + """ return getattr(val, '_typ', '_typ') == 'period' @@ -181,3 +226,7 @@ cdef inline bint is_offset_object(object val): is_date_offset : bool """ return getattr(val, '_typ', None) == "dateoffset" + + +cdef inline bint _checknull(object val): + return val is None or (PyFloat_Check(val) and val != val) diff --git a/pandas/_libs/util.pxd b/pandas/_libs/util.pxd index 0b7e66902cbb1c..134f34330d8aa6 100644 --- a/pandas/_libs/util.pxd +++ b/pandas/_libs/util.pxd @@ -1 +1,81 @@ from tslibs.util cimport * + +from cython cimport Py_ssize_t + +cimport numpy as cnp +from numpy cimport ndarray + + +cdef extern from "src/numpy_helper.h": + void set_array_not_contiguous(ndarray ao) + + int assign_value_1d(ndarray, Py_ssize_t, object) except -1 + object get_value_1d(ndarray, Py_ssize_t) + const char *get_c_string(object) except NULL + + +ctypedef fused numeric: + cnp.int8_t + cnp.int16_t + cnp.int32_t + cnp.int64_t + + cnp.uint8_t + cnp.uint16_t + cnp.uint32_t + cnp.uint64_t + + cnp.float32_t + cnp.float64_t + + +cdef inline object get_value_at(ndarray arr, object loc): + cdef: + Py_ssize_t i, sz + int casted + + if is_float_object(loc): + casted = int(loc) + if casted == loc: + loc = casted + i = loc + sz = cnp.PyArray_SIZE(arr) + + if i < 0 and sz > 0: + i += sz + elif i >= sz or sz == 0: + raise IndexError('index out of bounds') + + return get_value_1d(arr, i) + + +cdef inline set_value_at_unsafe(ndarray arr, object loc, object value): + """Sets a value into the array without checking the writeable flag. + + This should be used when setting values in a loop, check the writeable + flag above the loop and then eschew the check on each iteration. + """ + cdef: + Py_ssize_t i, sz + if is_float_object(loc): + casted = int(loc) + if casted == loc: + loc = casted + i = loc + sz = cnp.PyArray_SIZE(arr) + + if i < 0: + i += sz + elif i >= sz: + raise IndexError('index out of bounds') + + assign_value_1d(arr, i, value) + + +cdef inline set_value_at(ndarray arr, object loc, object value): + """Sets a value into the array after checking that the array is mutable. + """ + if not cnp.PyArray_ISWRITEABLE(arr): + raise ValueError('assignment destination is read-only') + + set_value_at_unsafe(arr, loc, value) From b7aefc9158df9a3f5d94222e4e3276737dfe4562 Mon Sep 17 00:00:00 2001 From: Jeff Reback Date: Tue, 31 Jul 2018 10:06:02 -0400 Subject: [PATCH 20/47] STYLE: lint fix --- pandas/tests/series/test_period.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/tests/series/test_period.py b/pandas/tests/series/test_period.py index b8b7a29082867b..24c2f30bef5692 100644 --- a/pandas/tests/series/test_period.py +++ b/pandas/tests/series/test_period.py @@ -5,7 +5,6 @@ import pandas.util.testing as tm import pandas.core.indexes.period as period from pandas import Series, period_range, DataFrame, Period -import pytest def _permute(obj): From 20f7ae82d815328a62a0ff1edd37b5f12cf47e64 Mon Sep 17 00:00:00 2001 From: Dean Langsam Date: Tue, 31 Jul 2018 18:44:06 +0300 Subject: [PATCH 21/47] DOC: Clarify check_like behavior in assert_frame_equal (#22106) --- pandas/util/testing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/util/testing.py b/pandas/util/testing.py index 6dffbcb0b4f010..2225daf10d90fb 100644 --- a/pandas/util/testing.py +++ b/pandas/util/testing.py @@ -1348,7 +1348,9 @@ def assert_frame_equal(left, right, check_dtype=True, check_categorical : bool, default True Whether to compare internal Categorical exactly. check_like : bool, default False - If true, ignore the order of rows & columns + If True, ignore the order of index & columns. + Note: index labels must match their respective rows + (same as in columns) - same labels must be with the same data obj : str, default 'DataFrame' Specify object name being compared, internally used to show appropriate assertion message From 59c313238f88659302f910afffdf14eb9d5b42fa Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 31 Jul 2018 18:05:57 -0700 Subject: [PATCH 22/47] Fix zillion deprecation warnings in core.dtypes.common (#22142) --- pandas/core/dtypes/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/dtypes/common.py b/pandas/core/dtypes/common.py index 4a0bf67f47bae1..b8cbb41501dd19 100644 --- a/pandas/core/dtypes/common.py +++ b/pandas/core/dtypes/common.py @@ -2016,7 +2016,9 @@ def pandas_dtype(dtype): # also catch some valid dtypes such as object, np.object_ and 'object' # which we safeguard against by catching them earlier and returning # np.dtype(valid_dtype) before this condition is evaluated. - if dtype in [object, np.object_, 'object', 'O']: + if is_hashable(dtype) and dtype in [object, np.object_, 'object', 'O']: + # check hashability to avoid errors/DeprecationWarning when we get + # here and `dtype` is an array return npdtype elif npdtype.kind == 'O': raise TypeError("dtype '{}' not understood".format(dtype)) From b62c324775ab9453cf62a6cacecae886a38096fe Mon Sep 17 00:00:00 2001 From: Jeremy Schendel Date: Tue, 31 Jul 2018 19:07:53 -0600 Subject: [PATCH 23/47] DEPR: deprecate IntervalIndex.itemsize and remove IntervalArray.itemsize (#22149) --- pandas/core/arrays/interval.py | 4 ---- pandas/core/indexes/interval.py | 10 ++++++++-- pandas/tests/indexes/interval/test_interval.py | 6 ++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 60464bcfda1e7b..76614454e5a101 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -688,10 +688,6 @@ def size(self): def shape(self): return self.left.shape - @property - def itemsize(self): - return self.left.itemsize + self.right.itemsize - def take(self, indices, allow_fill=False, fill_value=None, axis=None, **kwargs): """ diff --git a/pandas/core/indexes/interval.py b/pandas/core/indexes/interval.py index 0b467760d82d92..838b12468e85e7 100644 --- a/pandas/core/indexes/interval.py +++ b/pandas/core/indexes/interval.py @@ -369,8 +369,14 @@ def shape(self): @property def itemsize(self): - # Avoid materializing ndarray[Interval] - return self._data.itemsize + msg = ('IntervalIndex.itemsize is deprecated and will be removed in ' + 'a future version') + warnings.warn(msg, FutureWarning, stacklevel=2) + + # supress the warning from the underlying left/right itemsize + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + return self.left.itemsize + self.right.itemsize def __len__(self): return len(self.left) diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py index e179286e839db2..71f56c5bc11645 100644 --- a/pandas/tests/indexes/interval/test_interval.py +++ b/pandas/tests/indexes/interval/test_interval.py @@ -989,9 +989,11 @@ def test_itemsize(self): # GH 19209 left = np.arange(0, 4, dtype='i8') right = np.arange(1, 5, dtype='i8') - - result = IntervalIndex.from_arrays(left, right).itemsize expected = 16 # 8 * 2 + + with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + result = IntervalIndex.from_arrays(left, right).itemsize + assert result == expected @pytest.mark.parametrize('new_closed', [ From 57c7daa3251e8fed3f3f709ab5bc8e707db99e98 Mon Sep 17 00:00:00 2001 From: Ming Li <14131823+minggli@users.noreply.github.com> Date: Wed, 1 Aug 2018 02:09:53 +0100 Subject: [PATCH 24/47] BUG: DataFrame.replace with out of bound datetime causing RecursionError (#22108) --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/dtypes/cast.py | 10 ++++- pandas/core/internals/blocks.py | 10 +++-- pandas/tests/frame/test_replace.py | 65 ++++++++++++++---------------- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index e3e1b35f89cbb9..e9d4225c3dbd94 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -652,7 +652,7 @@ Reshaping - Bug in :meth:`Series.combine_first` with ``datetime64[ns, tz]`` dtype which would return tz-naive result (:issue:`21469`) - Bug in :meth:`Series.where` and :meth:`DataFrame.where` with ``datetime64[ns, tz]`` dtype (:issue:`21546`) - Bug in :meth:`Series.mask` and :meth:`DataFrame.mask` with ``list`` conditionals (:issue:`21891`) -- +- Bug in :meth:`DataFrame.replace` raises RecursionError when converting OutOfBounds ``datetime64[ns, tz]`` (:issue:`20380`) - Build Changes diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index e369679d2146f2..3971e90e64a14e 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -6,7 +6,7 @@ import warnings from pandas._libs import tslib, lib, tslibs -from pandas._libs.tslibs import iNaT +from pandas._libs.tslibs import iNaT, OutOfBoundsDatetime from pandas.compat import string_types, text_type, PY3 from .common import (ensure_object, is_bool, is_integer, is_float, is_complex, is_datetimetz, is_categorical_dtype, @@ -838,7 +838,13 @@ def soft_convert_objects(values, datetime=True, numeric=True, timedelta=True, # Soft conversions if datetime: - values = lib.maybe_convert_objects(values, convert_datetime=datetime) + # GH 20380, when datetime is beyond year 2262, hence outside + # bound of nanosecond-resolution 64-bit integers. + try: + values = lib.maybe_convert_objects(values, + convert_datetime=datetime) + except OutOfBoundsDatetime: + pass if timedelta and is_object_dtype(values.dtype): # Object check to ensure only run if previous did not convert diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py index 0f3ffb8055330b..8ee91ded4ab7a2 100644 --- a/pandas/core/internals/blocks.py +++ b/pandas/core/internals/blocks.py @@ -802,12 +802,14 @@ def replace(self, to_replace, value, inplace=False, filter=None, copy=not inplace) for b in blocks] return blocks except (TypeError, ValueError): - # try again with a compatible block block = self.astype(object) - return block.replace( - to_replace=original_to_replace, value=value, inplace=inplace, - filter=filter, regex=regex, convert=convert) + return block.replace(to_replace=original_to_replace, + value=value, + inplace=inplace, + filter=filter, + regex=regex, + convert=convert) def _replace_single(self, *args, **kwargs): """ no-op on a non-ObjectBlock """ diff --git a/pandas/tests/frame/test_replace.py b/pandas/tests/frame/test_replace.py index 68d799c55637cb..227484abb82c13 100644 --- a/pandas/tests/frame/test_replace.py +++ b/pandas/tests/frame/test_replace.py @@ -755,40 +755,37 @@ def test_replace_for_new_dtypes(self): result = tsframe.fillna(method='bfill') assert_frame_equal(result, tsframe.fillna(method='bfill')) - def test_replace_dtypes(self): - # int - df = DataFrame({'ints': [1, 2, 3]}) - result = df.replace(1, 0) - expected = DataFrame({'ints': [0, 2, 3]}) - assert_frame_equal(result, expected) - - df = DataFrame({'ints': [1, 2, 3]}, dtype=np.int32) - result = df.replace(1, 0) - expected = DataFrame({'ints': [0, 2, 3]}, dtype=np.int32) - assert_frame_equal(result, expected) - - df = DataFrame({'ints': [1, 2, 3]}, dtype=np.int16) - result = df.replace(1, 0) - expected = DataFrame({'ints': [0, 2, 3]}, dtype=np.int16) - assert_frame_equal(result, expected) - - # bools - df = DataFrame({'bools': [True, False, True]}) - result = df.replace(False, True) - assert result.values.all() - - # complex blocks - df = DataFrame({'complex': [1j, 2j, 3j]}) - result = df.replace(1j, 0j) - expected = DataFrame({'complex': [0j, 2j, 3j]}) - assert_frame_equal(result, expected) - - # datetime blocks - prev = datetime.today() - now = datetime.today() - df = DataFrame({'datetime64': Index([prev, now, prev])}) - result = df.replace(prev, now) - expected = DataFrame({'datetime64': Index([now] * 3)}) + @pytest.mark.parametrize('frame, to_replace, value, expected', [ + (DataFrame({'ints': [1, 2, 3]}), 1, 0, + DataFrame({'ints': [0, 2, 3]})), + (DataFrame({'ints': [1, 2, 3]}, dtype=np.int32), 1, 0, + DataFrame({'ints': [0, 2, 3]}, dtype=np.int32)), + (DataFrame({'ints': [1, 2, 3]}, dtype=np.int16), 1, 0, + DataFrame({'ints': [0, 2, 3]}, dtype=np.int16)), + (DataFrame({'bools': [True, False, True]}), False, True, + DataFrame({'bools': [True, True, True]})), + (DataFrame({'complex': [1j, 2j, 3j]}), 1j, 0, + DataFrame({'complex': [0j, 2j, 3j]})), + (DataFrame({'datetime64': Index([datetime(2018, 5, 28), + datetime(2018, 7, 28), + datetime(2018, 5, 28)])}), + datetime(2018, 5, 28), datetime(2018, 7, 28), + DataFrame({'datetime64': Index([datetime(2018, 7, 28)] * 3)})), + # GH 20380 + (DataFrame({'dt': [datetime(3017, 12, 20)], 'str': ['foo']}), + 'foo', 'bar', + DataFrame({'dt': [datetime(3017, 12, 20)], 'str': ['bar']})), + (DataFrame({'A': date_range('20130101', periods=3, tz='US/Eastern'), + 'B': [0, np.nan, 2]}), + Timestamp('20130102', tz='US/Eastern'), + Timestamp('20130104', tz='US/Eastern'), + DataFrame({'A': [Timestamp('20130101', tz='US/Eastern'), + Timestamp('20130104', tz='US/Eastern'), + Timestamp('20130103', tz='US/Eastern')], + 'B': [0, np.nan, 2]})) + ]) + def test_replace_dtypes(self, frame, to_replace, value, expected): + result = getattr(frame, 'replace')(to_replace, value) assert_frame_equal(result, expected) def test_replace_input_formats_listlike(self): From 8d5c51bf2db07d34e03d06d0e758d1a59f366147 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 1 Aug 2018 02:19:43 -0700 Subject: [PATCH 25/47] [Bug] Fix various DatetimeIndex comparison bugs (#22074) --- doc/source/whatsnew/v0.24.0.txt | 5 + pandas/core/arrays/datetimes.py | 40 ++++-- pandas/core/ops.py | 31 ++++- pandas/tests/frame/test_operators.py | 16 ++- pandas/tests/frame/test_query_eval.py | 8 +- .../indexes/datetimes/test_arithmetic.py | 121 +++++++++++++++++- pandas/tests/series/test_operators.py | 11 +- 7 files changed, 208 insertions(+), 24 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index e9d4225c3dbd94..8b89618cd0d88f 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -533,6 +533,9 @@ Datetimelike - Fixed bug where two :class:`DateOffset` objects with different ``normalize`` attributes could evaluate as equal (:issue:`21404`) - Fixed bug where :meth:`Timestamp.resolution` incorrectly returned 1-microsecond ``timedelta`` instead of 1-nanosecond :class:`Timedelta` (:issue:`21336`,:issue:`21365`) - Bug in :func:`to_datetime` that did not consistently return an :class:`Index` when ``box=True`` was specified (:issue:`21864`) +- Bug in :class:`DatetimeIndex` comparisons where string comparisons incorrectly raises ``TypeError`` (:issue:`22074`) +- Bug in :class:`DatetimeIndex` comparisons when comparing against ``timedelta64[ns]`` dtyped arrays; in some cases ``TypeError`` was incorrectly raised, in others it incorrectly failed to raise (:issue:`22074`) +- Bug in :class:`DatetimeIndex` comparisons when comparing against object-dtyped arrays (:issue:`22074`) Timedelta ^^^^^^^^^ @@ -555,6 +558,7 @@ Timezones - Bug in :class:`Index` with ``datetime64[ns, tz]`` dtype that did not localize integer data correctly (:issue:`20964`) - Bug in :class:`DatetimeIndex` where constructing with an integer and tz would not localize correctly (:issue:`12619`) - Fixed bug where :meth:`DataFrame.describe` and :meth:`Series.describe` on tz-aware datetimes did not show `first` and `last` result (:issue:`21328`) +- Bug in :class:`DatetimeIndex` comparisons failing to raise ``TypeError`` when comparing timezone-aware ``DatetimeIndex`` against ``np.datetime64`` (:issue:`22074`) Offsets ^^^^^^^ @@ -572,6 +576,7 @@ Numeric - Bug in :meth:`DataFrame.agg`, :meth:`DataFrame.transform` and :meth:`DataFrame.apply` where, when supplied with a list of functions and ``axis=1`` (e.g. ``df.apply(['sum', 'mean'], axis=1)``), a ``TypeError`` was wrongly raised. For all three methods such calculation are now done correctly. (:issue:`16679`). +- Bug in :class:`Series` comparison against datetime-like scalars and arrays (:issue:`22074`) - Strings diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 26aaab2b1b237c..ee0677f7607053 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -5,7 +5,7 @@ import numpy as np from pytz import utc -from pandas._libs import tslib +from pandas._libs import lib, tslib from pandas._libs.tslib import Timestamp, NaT, iNaT from pandas._libs.tslibs import ( normalize_date, @@ -18,7 +18,7 @@ from pandas.core.dtypes.common import ( _NS_DTYPE, - is_datetimelike, + is_object_dtype, is_datetime64tz_dtype, is_datetime64_dtype, is_timedelta64_dtype, @@ -29,6 +29,7 @@ import pandas.core.common as com from pandas.core.algorithms import checked_add_with_arr +from pandas.core import ops from pandas.tseries.frequencies import to_offset from pandas.tseries.offsets import Tick, Day, generate_range @@ -99,31 +100,40 @@ def wrapper(self, other): meth = getattr(dtl.DatetimeLikeArrayMixin, opname) if isinstance(other, (datetime, np.datetime64, compat.string_types)): - if isinstance(other, datetime): + if isinstance(other, (datetime, np.datetime64)): # GH#18435 strings get a pass from tzawareness compat self._assert_tzawareness_compat(other) - other = _to_m8(other, tz=self.tz) + try: + other = _to_m8(other, tz=self.tz) + except ValueError: + # string that cannot be parsed to Timestamp + return ops.invalid_comparison(self, other, op) + result = meth(self, other) if isna(other): result.fill(nat_result) + elif lib.is_scalar(other): + return ops.invalid_comparison(self, other, op) else: if isinstance(other, list): + # FIXME: This can break for object-dtype with mixed types other = type(self)(other) elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries)): # Following Timestamp convention, __eq__ is all-False # and __ne__ is all True, others raise TypeError. - if opname == '__eq__': - return np.zeros(shape=self.shape, dtype=bool) - elif opname == '__ne__': - return np.ones(shape=self.shape, dtype=bool) - raise TypeError('%s type object %s' % - (type(other), str(other))) - - if is_datetimelike(other): + return ops.invalid_comparison(self, other, op) + + if is_object_dtype(other): + result = op(self.astype('O'), np.array(other)) + elif not (is_datetime64_dtype(other) or + is_datetime64tz_dtype(other)): + # e.g. is_timedelta64_dtype(other) + return ops.invalid_comparison(self, other, op) + else: self._assert_tzawareness_compat(other) + result = meth(self, np.asarray(other)) - result = meth(self, np.asarray(other)) result = com.values_from_object(result) # Make sure to pass an array to result[...]; indexing with @@ -152,6 +162,10 @@ class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin): 'is_year_end', 'is_leap_year'] _object_ops = ['weekday_name', 'freq', 'tz'] + # dummy attribute so that datetime.__eq__(DatetimeArray) defers + # by returning NotImplemented + timetuple = None + # ----------------------------------------------------------------- # Constructors diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 6d407c41daea64..f7d863bba82a75 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -788,6 +788,35 @@ def mask_cmp_op(x, y, op, allowed_types): return result +def invalid_comparison(left, right, op): + """ + If a comparison has mismatched types and is not necessarily meaningful, + follow python3 conventions by: + + - returning all-False for equality + - returning all-True for inequality + - raising TypeError otherwise + + Parameters + ---------- + left : array-like + right : scalar, array-like + op : operator.{eq, ne, lt, le, gt} + + Raises + ------ + TypeError : on inequality comparisons + """ + if op is operator.eq: + res_values = np.zeros(left.shape, dtype=bool) + elif op is operator.ne: + res_values = np.ones(left.shape, dtype=bool) + else: + raise TypeError("Invalid comparison between dtype={dtype} and {typ}" + .format(dtype=left.dtype, typ=type(right).__name__)) + return res_values + + # ----------------------------------------------------------------------------- # Functions that add arithmetic methods to objects, given arithmetic factory # methods @@ -1259,7 +1288,7 @@ def na_op(x, y): result = _comp_method_OBJECT_ARRAY(op, x, y) elif is_datetimelike_v_numeric(x, y): - raise TypeError("invalid type comparison") + return invalid_comparison(x, y, op) else: diff --git a/pandas/tests/frame/test_operators.py b/pandas/tests/frame/test_operators.py index 1702b2e7d29a44..a11d673fd5d7fd 100644 --- a/pandas/tests/frame/test_operators.py +++ b/pandas/tests/frame/test_operators.py @@ -156,8 +156,20 @@ def test_comparison_invalid(self): def check(df, df2): for (x, y) in [(df, df2), (df2, df)]: - pytest.raises(TypeError, lambda: x == y) - pytest.raises(TypeError, lambda: x != y) + # we expect the result to match Series comparisons for + # == and !=, inequalities should raise + result = x == y + expected = DataFrame({col: x[col] == y[col] + for col in x.columns}, + index=x.index, columns=x.columns) + assert_frame_equal(result, expected) + + result = x != y + expected = DataFrame({col: x[col] != y[col] + for col in x.columns}, + index=x.index, columns=x.columns) + assert_frame_equal(result, expected) + pytest.raises(TypeError, lambda: x >= y) pytest.raises(TypeError, lambda: x > y) pytest.raises(TypeError, lambda: x < y) diff --git a/pandas/tests/frame/test_query_eval.py b/pandas/tests/frame/test_query_eval.py index a02f78bfaf8a5c..3be7ad12db883b 100644 --- a/pandas/tests/frame/test_query_eval.py +++ b/pandas/tests/frame/test_query_eval.py @@ -463,9 +463,13 @@ def test_date_query_with_non_date(self): df = DataFrame({'dates': date_range('1/1/2012', periods=n), 'nondate': np.arange(n)}) - ops = '==', '!=', '<', '>', '<=', '>=' + result = df.query('dates == nondate', parser=parser, engine=engine) + assert len(result) == 0 - for op in ops: + result = df.query('dates != nondate', parser=parser, engine=engine) + assert_frame_equal(result, df) + + for op in ['<', '>', '<=', '>=']: with pytest.raises(TypeError): df.query('dates %s nondate' % op, parser=parser, engine=engine) diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 1e54e6563d5983..f54cb32b0a0366 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -275,6 +275,20 @@ def test_comparison_tzawareness_compat(self, op): with pytest.raises(TypeError): op(ts, dz) + @pytest.mark.parametrize('op', [operator.eq, operator.ne, + operator.gt, operator.ge, + operator.lt, operator.le]) + @pytest.mark.parametrize('other', [datetime(2016, 1, 1), + Timestamp('2016-01-01'), + np.datetime64('2016-01-01')]) + def test_scalar_comparison_tzawareness(self, op, other, tz_aware_fixture): + tz = tz_aware_fixture + dti = pd.date_range('2016-01-01', periods=2, tz=tz) + with pytest.raises(TypeError): + op(dti, other) + with pytest.raises(TypeError): + op(other, dti) + @pytest.mark.parametrize('op', [operator.eq, operator.ne, operator.gt, operator.ge, operator.lt, operator.le]) @@ -290,12 +304,60 @@ def test_nat_comparison_tzawareness(self, op): result = op(dti.tz_localize('US/Pacific'), pd.NaT) tm.assert_numpy_array_equal(result, expected) - def test_dti_cmp_int_raises(self): - rng = date_range('1/1/2000', periods=10) + def test_dti_cmp_str(self, tz_naive_fixture): + # GH#22074 + # regardless of tz, we expect these comparisons are valid + tz = tz_naive_fixture + rng = date_range('1/1/2000', periods=10, tz=tz) + other = '1/1/2000' + + result = rng == other + expected = np.array([True] + [False] * 9) + tm.assert_numpy_array_equal(result, expected) + + result = rng != other + expected = np.array([False] + [True] * 9) + tm.assert_numpy_array_equal(result, expected) + + result = rng < other + expected = np.array([False] * 10) + tm.assert_numpy_array_equal(result, expected) + + result = rng <= other + expected = np.array([True] + [False] * 9) + tm.assert_numpy_array_equal(result, expected) + + result = rng > other + expected = np.array([False] + [True] * 9) + tm.assert_numpy_array_equal(result, expected) + + result = rng >= other + expected = np.array([True] * 10) + tm.assert_numpy_array_equal(result, expected) + + @pytest.mark.parametrize('other', ['foo', 99, 4.0, + object(), timedelta(days=2)]) + def test_dti_cmp_scalar_invalid(self, other, tz_naive_fixture): + # GH#22074 + tz = tz_naive_fixture + rng = date_range('1/1/2000', periods=10, tz=tz) + + result = rng == other + expected = np.array([False] * 10) + tm.assert_numpy_array_equal(result, expected) + + result = rng != other + expected = np.array([True] * 10) + tm.assert_numpy_array_equal(result, expected) - # raise TypeError for now with pytest.raises(TypeError): - rng < rng[3].value + rng < other + with pytest.raises(TypeError): + rng <= other + with pytest.raises(TypeError): + rng > other + with pytest.raises(TypeError): + rng >= other def test_dti_cmp_list(self): rng = date_range('1/1/2000', periods=10) @@ -304,6 +366,57 @@ def test_dti_cmp_list(self): expected = rng == rng tm.assert_numpy_array_equal(result, expected) + @pytest.mark.parametrize('other', [ + pd.timedelta_range('1D', periods=10), + pd.timedelta_range('1D', periods=10).to_series(), + pd.timedelta_range('1D', periods=10).asi8.view('m8[ns]') + ], ids=lambda x: type(x).__name__) + def test_dti_cmp_tdi_tzawareness(self, other): + # GH#22074 + # reversion test that we _don't_ call _assert_tzawareness_compat + # when comparing against TimedeltaIndex + dti = date_range('2000-01-01', periods=10, tz='Asia/Tokyo') + + result = dti == other + expected = np.array([False] * 10) + tm.assert_numpy_array_equal(result, expected) + + result = dti != other + expected = np.array([True] * 10) + tm.assert_numpy_array_equal(result, expected) + + with pytest.raises(TypeError): + dti < other + with pytest.raises(TypeError): + dti <= other + with pytest.raises(TypeError): + dti > other + with pytest.raises(TypeError): + dti >= other + + def test_dti_cmp_object_dtype(self): + # GH#22074 + dti = date_range('2000-01-01', periods=10, tz='Asia/Tokyo') + + other = dti.astype('O') + + result = dti == other + expected = np.array([True] * 10) + tm.assert_numpy_array_equal(result, expected) + + other = dti.tz_localize(None) + with pytest.raises(TypeError): + # tzawareness failure + dti != other + + other = np.array(list(dti[:5]) + [Timedelta(days=1)] * 5) + result = dti == other + expected = np.array([True] * 5 + [False] * 5) + tm.assert_numpy_array_equal(result, expected) + + with pytest.raises(TypeError): + dti >= other + class TestDatetimeIndexArithmetic(object): diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index fad2b025dd3e42..df52b4cabc77cf 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -243,8 +243,15 @@ def test_comparison_invalid(self): s2 = Series(date_range('20010101', periods=5)) for (x, y) in [(s, s2), (s2, s)]: - pytest.raises(TypeError, lambda: x == y) - pytest.raises(TypeError, lambda: x != y) + + result = x == y + expected = Series([False] * 5) + assert_series_equal(result, expected) + + result = x != y + expected = Series([True] * 5) + assert_series_equal(result, expected) + pytest.raises(TypeError, lambda: x >= y) pytest.raises(TypeError, lambda: x > y) pytest.raises(TypeError, lambda: x < y) From 5d661c8ee06dc361ae49a63ba1e51d18e44e2255 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 1 Aug 2018 03:25:13 -0700 Subject: [PATCH 26/47] [TST] make xfails strict (#22139) --- .../arrays/categorical/test_constructors.py | 3 ++- pandas/tests/extension/base/setitem.py | 3 ++- pandas/tests/extension/integer/test_integer.py | 10 +++++++--- pandas/tests/extension/json/test_json.py | 5 ++++- pandas/tests/frame/test_arithmetic.py | 3 ++- pandas/tests/frame/test_duplicates.py | 3 ++- pandas/tests/groupby/aggregate/test_other.py | 12 +++++++++++- pandas/tests/groupby/test_apply.py | 7 ++++--- pandas/tests/indexes/interval/test_astype.py | 4 ++-- pandas/tests/indexes/multi/test_missing.py | 2 +- pandas/tests/indexes/period/test_arithmetic.py | 3 ++- pandas/tests/indexes/test_base.py | 8 +++++--- pandas/tests/indexes/test_numeric.py | 3 ++- pandas/tests/io/formats/test_to_csv.py | 2 +- pandas/tests/io/json/test_json_table_schema.py | 12 +++++++++--- pandas/tests/io/test_excel.py | 3 ++- pandas/tests/io/test_parquet.py | 4 +++- pandas/tests/plotting/test_frame.py | 3 ++- pandas/tests/plotting/test_misc.py | 1 - pandas/tests/reshape/test_pivot.py | 18 +++++++++++------- pandas/tests/scalar/period/test_asfreq.py | 3 ++- pandas/tests/scalar/period/test_period.py | 3 ++- pandas/tests/series/test_analytics.py | 2 +- pandas/tests/series/test_rank.py | 3 ++- pandas/tests/sparse/frame/test_analytics.py | 8 ++++---- pandas/tests/sparse/frame/test_frame.py | 11 ++++++----- pandas/tests/sparse/frame/test_indexing.py | 16 ++++++++-------- pandas/tests/sparse/series/test_indexing.py | 8 ++++---- pandas/tests/test_algos.py | 6 ++---- pandas/tests/test_base.py | 3 ++- pandas/tests/test_downstream.py | 2 +- pandas/tests/test_window.py | 5 +++-- 32 files changed, 111 insertions(+), 68 deletions(-) diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py index e082629a5433d5..e5d620df96493d 100644 --- a/pandas/tests/arrays/categorical/test_constructors.py +++ b/pandas/tests/arrays/categorical/test_constructors.py @@ -511,7 +511,8 @@ def test_construction_with_ordered(self): cat = Categorical([0, 1, 2], ordered=True) assert cat.ordered - @pytest.mark.xfail(reason="Imaginary values not supported in Categorical") + @pytest.mark.xfail(reason="Imaginary values not supported in Categorical", + strict=True) def test_constructor_imaginary(self): values = [1, 2, 3 + 1j] c1 = Categorical(values) diff --git a/pandas/tests/extension/base/setitem.py b/pandas/tests/extension/base/setitem.py index 4e27f1eca538f9..307543eca2b3e3 100644 --- a/pandas/tests/extension/base/setitem.py +++ b/pandas/tests/extension/base/setitem.py @@ -159,7 +159,8 @@ def test_setitem_frame_invalid_length(self, data): with tm.assert_raises_regex(ValueError, xpr): df['B'] = data[:5] - @pytest.mark.xfail(reason="GH-20441: setitem on extension types.") + @pytest.mark.xfail(reason="GH#20441: setitem on extension types.", + strict=True) def test_setitem_tuple_index(self, data): s = pd.Series(data[:2], index=[(0, 0), (0, 1)]) expected = pd.Series(data.take([1, 1]), index=s.index) diff --git a/pandas/tests/extension/integer/test_integer.py b/pandas/tests/extension/integer/test_integer.py index 451f7488bd38af..5e0f5bf0a5dcfe 100644 --- a/pandas/tests/extension/integer/test_integer.py +++ b/pandas/tests/extension/integer/test_integer.py @@ -599,13 +599,17 @@ def test_construct_cast_invalid(self, dtype): class TestGroupby(BaseInteger, base.BaseGroupbyTests): - @pytest.mark.xfail(reason="groupby not working") + @pytest.mark.xfail(reason="groupby not working", strict=True) def test_groupby_extension_no_sort(self, data_for_grouping): super(TestGroupby, self).test_groupby_extension_no_sort( data_for_grouping) - @pytest.mark.xfail(reason="groupby not working") - @pytest.mark.parametrize('as_index', [True, False]) + @pytest.mark.parametrize('as_index', [ + pytest.param(True, + marks=pytest.mark.xfail(reason="groupby not working", + strict=True)), + False + ]) def test_groupby_extension_agg(self, as_index, data_for_grouping): super(TestGroupby, self).test_groupby_extension_agg( as_index, data_for_grouping) diff --git a/pandas/tests/extension/json/test_json.py b/pandas/tests/extension/json/test_json.py index 520c303f1990b4..b9cc3c431528fb 100644 --- a/pandas/tests/extension/json/test_json.py +++ b/pandas/tests/extension/json/test_json.py @@ -142,6 +142,7 @@ def test_custom_asserts(self): class TestConstructors(BaseJSON, base.BaseConstructorsTests): + # TODO: Should this be pytest.mark.skip? @pytest.mark.xfail(reason="not implemented constructor from dtype") def test_from_dtype(self, data): # construct from our dtype & string dtype @@ -157,10 +158,12 @@ class TestGetitem(BaseJSON, base.BaseGetitemTests): class TestMissing(BaseJSON, base.BaseMissingTests): + # TODO: Should this be pytest.mark.skip? @pytest.mark.xfail(reason="Setting a dict as a scalar") def test_fillna_series(self): """We treat dictionaries as a mapping in fillna, not a scalar.""" + # TODO: Should this be pytest.mark.skip? @pytest.mark.xfail(reason="Setting a dict as a scalar") def test_fillna_frame(self): """We treat dictionaries as a mapping in fillna, not a scalar.""" @@ -212,7 +215,7 @@ def test_combine_add(self, data_repeated): class TestCasting(BaseJSON, base.BaseCastingTests): - + # TODO: Should this be pytest.mark.skip? @pytest.mark.xfail(reason="failing on np.array(self, dtype=str)") def test_astype_str(self): """This currently fails in NumPy on np.array(self, dtype=str) with diff --git a/pandas/tests/frame/test_arithmetic.py b/pandas/tests/frame/test_arithmetic.py index fb381a56405191..3d03a70553d2d6 100644 --- a/pandas/tests/frame/test_arithmetic.py +++ b/pandas/tests/frame/test_arithmetic.py @@ -201,7 +201,8 @@ def test_df_div_zero_series_does_not_commute(self): class TestFrameArithmetic(object): - @pytest.mark.xfail(reason='GH#7996 datetime64 units not converted to nano') + @pytest.mark.xfail(reason='GH#7996 datetime64 units not converted to nano', + strict=True) def test_df_sub_datetime64_not_ns(self): df = pd.DataFrame(pd.date_range('20130101', periods=3)) dt64 = np.datetime64('2013-01-01') diff --git a/pandas/tests/frame/test_duplicates.py b/pandas/tests/frame/test_duplicates.py index 289170527dea73..940692ec5b46a0 100644 --- a/pandas/tests/frame/test_duplicates.py +++ b/pandas/tests/frame/test_duplicates.py @@ -55,7 +55,8 @@ def test_duplicated_keep(keep, expected): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(reason="GH21720; nan/None falsely considered equal") +@pytest.mark.xfail(reason="GH#21720; nan/None falsely considered equal", + strict=True) @pytest.mark.parametrize('keep, expected', [ ('first', Series([False, False, True, False, True])), ('last', Series([True, True, False, False, False])), diff --git a/pandas/tests/groupby/aggregate/test_other.py b/pandas/tests/groupby/aggregate/test_other.py index 34489051efc18a..606539a5643237 100644 --- a/pandas/tests/groupby/aggregate/test_other.py +++ b/pandas/tests/groupby/aggregate/test_other.py @@ -487,7 +487,17 @@ def test_agg_structs_series(structure, expected): tm.assert_series_equal(result, expected) -@pytest.mark.xfail(reason="GH-18869: agg func not called on empty groups.") +@pytest.mark.parametrize('observed', [ + True, + pytest.param(False, + marks=pytest.mark.xfail(reason="GH#18869: agg func not " + "called on empty groups.", + strict=True)), + pytest.param(None, + marks=pytest.mark.xfail(reason="GH#18869: agg func not " + "called on empty groups.", + strict=True)) +]) def test_agg_category_nansum(observed): categories = ['a', 'b', 'c'] df = pd.DataFrame({"A": pd.Categorical(['a', 'a', 'b'], diff --git a/pandas/tests/groupby/test_apply.py b/pandas/tests/groupby/test_apply.py index 07eef2d87feb30..7c90d359a40549 100644 --- a/pandas/tests/groupby/test_apply.py +++ b/pandas/tests/groupby/test_apply.py @@ -58,9 +58,10 @@ def test_apply_trivial(): tm.assert_frame_equal(result, expected) -@pytest.mark.xfail(reason=("GH 20066; function passed into apply " - "returns a DataFrame with the same index " - "as the one to create GroupBy object.")) +@pytest.mark.xfail(reason="GH#20066; function passed into apply " + "returns a DataFrame with the same index " + "as the one to create GroupBy object.", + strict=True) def test_apply_trivial_fail(): # GH 20066 # trivial apply fails if the constant dataframe has the same index diff --git a/pandas/tests/indexes/interval/test_astype.py b/pandas/tests/indexes/interval/test_astype.py index 1e96ac730a0eba..6bbc938c346f79 100644 --- a/pandas/tests/indexes/interval/test_astype.py +++ b/pandas/tests/indexes/interval/test_astype.py @@ -95,7 +95,7 @@ def test_subtype_integer(self, subtype_start, subtype_end): closed=index.closed) tm.assert_index_equal(result, expected) - @pytest.mark.xfail(reason='GH 15832') + @pytest.mark.xfail(reason='GH#15832', strict=True) def test_subtype_integer_errors(self): # int64 -> uint64 fails with negative values index = interval_range(-10, 10) @@ -133,7 +133,7 @@ def test_subtype_integer(self, subtype): with tm.assert_raises_regex(ValueError, msg): index.insert(0, np.nan).astype(dtype) - @pytest.mark.xfail(reason='GH 15832') + @pytest.mark.xfail(reason='GH#15832', strict=True) def test_subtype_integer_errors(self): # float64 -> uint64 fails with negative values index = interval_range(-10.0, 10.0) diff --git a/pandas/tests/indexes/multi/test_missing.py b/pandas/tests/indexes/multi/test_missing.py index 79fcff965e7250..bedacf84f4f9a0 100644 --- a/pandas/tests/indexes/multi/test_missing.py +++ b/pandas/tests/indexes/multi/test_missing.py @@ -83,7 +83,7 @@ def test_nulls(idx): idx.isna() -@pytest.mark.xfail +@pytest.mark.xfail(strict=True) def test_hasnans_isnans(idx): # GH 11343, added tests for hasnans / isnans index = idx.copy() diff --git a/pandas/tests/indexes/period/test_arithmetic.py b/pandas/tests/indexes/period/test_arithmetic.py index 1d3c8b94a64904..d7dbb1423a9a30 100644 --- a/pandas/tests/indexes/period/test_arithmetic.py +++ b/pandas/tests/indexes/period/test_arithmetic.py @@ -356,7 +356,8 @@ def test_pi_add_sub_td64_array_non_tick_raises(self): with pytest.raises(period.IncompatibleFrequency): tdarr - rng - @pytest.mark.xfail(reason='op with TimedeltaIndex raises, with ndarray OK') + @pytest.mark.xfail(reason='op with TimedeltaIndex raises, with ndarray OK', + strict=True) def test_pi_add_sub_td64_array_tick(self): rng = pd.period_range('1/1/2000', freq='Q', periods=3) dti = pd.date_range('2016-01-01', periods=3) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 754703dfc4bee1..ef3d41126d3736 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -491,8 +491,9 @@ def test_constructor_overflow_int64(self): with tm.assert_raises_regex(OverflowError, msg): Index([np.iinfo(np.uint64).max - 1], dtype="int64") - @pytest.mark.xfail(reason="see gh-21311: Index " - "doesn't enforce dtype argument") + @pytest.mark.xfail(reason="see GH#21311: Index " + "doesn't enforce dtype argument", + strict=True) def test_constructor_cast(self): msg = "could not convert string to float" with tm.assert_raises_regex(ValueError, msg): @@ -1455,7 +1456,8 @@ def test_slice_float_locs(self): assert index2.slice_locs(8.5, 1.5) == (2, 6) assert index2.slice_locs(10.5, -1) == (0, n) - @pytest.mark.xfail(reason="Assertions were not correct - see GH 20915") + @pytest.mark.xfail(reason="Assertions were not correct - see GH#20915", + strict=True) def test_slice_ints_with_floats_raises(self): # int slicing with floats # GH 4892, these are all TypeErrors diff --git a/pandas/tests/indexes/test_numeric.py b/pandas/tests/indexes/test_numeric.py index 71b2774a926124..01ff038c4dd1c9 100644 --- a/pandas/tests/indexes/test_numeric.py +++ b/pandas/tests/indexes/test_numeric.py @@ -150,7 +150,8 @@ def test_rpow_float(self): result = 2.0**idx tm.assert_index_equal(result, expected) - @pytest.mark.xfail(reason='GH#19252 Series has no __rdivmod__') + @pytest.mark.xfail(reason='GH#19252 Series has no __rdivmod__', + strict=True) def test_divmod_series(self): idx = self.create_index() diff --git a/pandas/tests/io/formats/test_to_csv.py b/pandas/tests/io/formats/test_to_csv.py index 5fb356e48289ff..ea0b5f5cc0c660 100644 --- a/pandas/tests/io/formats/test_to_csv.py +++ b/pandas/tests/io/formats/test_to_csv.py @@ -274,7 +274,7 @@ def test_to_csv_string_array_ascii(self): with open(path, 'r') as f: assert f.read() == expected_ascii - @pytest.mark.xfail + @pytest.mark.xfail(strict=True) def test_to_csv_string_array_utf8(self): # GH 10813 str_array = [{'names': ['foo', 'bar']}, {'names': ['baz', 'qux']}] diff --git a/pandas/tests/io/json/test_json_table_schema.py b/pandas/tests/io/json/test_json_table_schema.py index b6483d0e978bab..829953c144caa8 100644 --- a/pandas/tests/io/json/test_json_table_schema.py +++ b/pandas/tests/io/json/test_json_table_schema.py @@ -495,7 +495,10 @@ def test_mi_falsey_name(self): class TestTableOrientReader(object): @pytest.mark.parametrize("index_nm", [ - None, "idx", pytest.param("index", marks=pytest.mark.xfail), + None, + "idx", + pytest.param("index", + marks=pytest.mark.xfail(strict=True)), 'level_0']) @pytest.mark.parametrize("vals", [ {'ints': [1, 2, 3, 4]}, @@ -504,7 +507,8 @@ class TestTableOrientReader(object): {'categoricals': pd.Series(pd.Categorical(['a', 'b', 'c', 'c']))}, {'ordered_cats': pd.Series(pd.Categorical(['a', 'b', 'c', 'c'], ordered=True))}, - pytest.param({'floats': [1., 2., 3., 4.]}, marks=pytest.mark.xfail), + pytest.param({'floats': [1., 2., 3., 4.]}, + marks=pytest.mark.xfail(strict=True)), {'floats': [1.1, 2.2, 3.3, 4.4]}, {'bools': [True, False, False, True]}]) def test_read_json_table_orient(self, index_nm, vals, recwarn): @@ -562,7 +566,9 @@ def test_multiindex(self, index_names): tm.assert_frame_equal(df, result) @pytest.mark.parametrize("strict_check", [ - pytest.param(True, marks=pytest.mark.xfail), False]) + pytest.param(True, marks=pytest.mark.xfail(strict=True)), + False + ]) def test_empty_frame_roundtrip(self, strict_check): # GH 21287 df = pd.DataFrame([], columns=['a', 'b', 'c']) diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index e51780891534fc..fa5a8f6a1900c5 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -2227,7 +2227,8 @@ def check_called(func): pytest.param('xlwt', marks=pytest.mark.xfail(reason='xlwt does not support ' 'openpyxl-compatible ' - 'style dicts')), + 'style dicts', + strict=True)), 'xlsxwriter', 'openpyxl', ]) diff --git a/pandas/tests/io/test_parquet.py b/pandas/tests/io/test_parquet.py index 11cbea8ce63314..fefbe8afb59cbf 100644 --- a/pandas/tests/io/test_parquet.py +++ b/pandas/tests/io/test_parquet.py @@ -216,7 +216,8 @@ def test_options_get_engine(fp, pa): @pytest.mark.xfail(is_platform_windows() or is_platform_mac(), - reason="reading pa metadata failing on Windows/mac") + reason="reading pa metadata failing on Windows/mac", + strict=True) def test_cross_engine_pa_fp(df_cross_compat, pa, fp): # cross-compat with differing reading/writing engines @@ -383,6 +384,7 @@ def test_basic(self, pa, df_full): check_round_trip(df, pa) + # TODO: This doesn't fail on all systems; track down which @pytest.mark.xfail(reason="pyarrow fails on this (ARROW-1883)") def test_basic_subset_columns(self, pa, df_full): # GH18628 diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py index db10ea15f6e9c8..47a93ba82d77be 100644 --- a/pandas/tests/plotting/test_frame.py +++ b/pandas/tests/plotting/test_frame.py @@ -496,7 +496,8 @@ def test_subplots_timeseries_y_axis(self): testdata.plot(y="text") @pytest.mark.xfail(reason='not support for period, categorical, ' - 'datetime_mixed_tz') + 'datetime_mixed_tz', + strict=True) def test_subplots_timeseries_y_axis_not_supported(self): """ This test will fail for: diff --git a/pandas/tests/plotting/test_misc.py b/pandas/tests/plotting/test_misc.py index 0473610ea2f8ff..e80443954a4343 100644 --- a/pandas/tests/plotting/test_misc.py +++ b/pandas/tests/plotting/test_misc.py @@ -212,7 +212,6 @@ def test_parallel_coordinates(self, iris): with tm.assert_produces_warning(FutureWarning): parallel_coordinates(df, 'Name', colors=colors) - @pytest.mark.xfail(reason="unreliable test") def test_parallel_coordinates_with_sorted_labels(self): """ For #15908 """ from pandas.plotting import parallel_coordinates diff --git a/pandas/tests/reshape/test_pivot.py b/pandas/tests/reshape/test_pivot.py index 7e7e0814085347..e3d5880eebd482 100644 --- a/pandas/tests/reshape/test_pivot.py +++ b/pandas/tests/reshape/test_pivot.py @@ -458,7 +458,8 @@ def test_pivot_with_list_like_values_nans(self, values): tm.assert_frame_equal(result, expected) @pytest.mark.xfail(reason='MultiIndexed unstack with tuple names fails' - 'with KeyError #19966') + 'with KeyError GH#19966', + strict=True) def test_pivot_with_multiindex(self): # issue #17160 index = Index(data=[0, 1, 2, 3, 4, 5]) @@ -617,8 +618,9 @@ def test_margins_dtype(self): tm.assert_frame_equal(expected, result) - @pytest.mark.xfail(reason='GH 17035 (len of floats is casted back to ' - 'floats)') + @pytest.mark.xfail(reason='GH#17035 (len of floats is casted back to ' + 'floats)', + strict=True) def test_margins_dtype_len(self): mi_val = list(product(['bar', 'foo'], ['one', 'two'])) + [('All', '')] mi = MultiIndex.from_tuples(mi_val, names=('A', 'B')) @@ -1102,8 +1104,9 @@ def test_pivot_table_margins_name_with_aggfunc_list(self): expected = pd.DataFrame(table.values, index=ix, columns=cols) tm.assert_frame_equal(table, expected) - @pytest.mark.xfail(reason='GH 17035 (np.mean of ints is casted back to ' - 'ints)') + @pytest.mark.xfail(reason='GH#17035 (np.mean of ints is casted back to ' + 'ints)', + strict=True) def test_categorical_margins(self, observed): # GH 10989 df = pd.DataFrame({'x': np.arange(8), @@ -1117,8 +1120,9 @@ def test_categorical_margins(self, observed): table = df.pivot_table('x', 'y', 'z', dropna=observed, margins=True) tm.assert_frame_equal(table, expected) - @pytest.mark.xfail(reason='GH 17035 (np.mean of ints is casted back to ' - 'ints)') + @pytest.mark.xfail(reason='GH#17035 (np.mean of ints is casted back to ' + 'ints)', + strict=True) def test_categorical_margins_category(self, observed): df = pd.DataFrame({'x': np.arange(8), 'y': np.arange(8) // 4, diff --git a/pandas/tests/scalar/period/test_asfreq.py b/pandas/tests/scalar/period/test_asfreq.py index 8fde9a417f3b7a..2e3867db65604f 100644 --- a/pandas/tests/scalar/period/test_asfreq.py +++ b/pandas/tests/scalar/period/test_asfreq.py @@ -32,7 +32,8 @@ def test_asfreq_near_zero_weekly(self): assert week2.asfreq('D', 'S') <= per2 @pytest.mark.xfail(reason='GH#19643 period_helper asfreq functions fail ' - 'to check for overflows') + 'to check for overflows', + strict=True) def test_to_timestamp_out_of_bounds(self): # GH#19643, currently gives Timestamp('1754-08-30 22:43:41.128654848') per = Period('0001-01-01', freq='B') diff --git a/pandas/tests/scalar/period/test_period.py b/pandas/tests/scalar/period/test_period.py index 7a97d4ecaa8d57..c4c9a5f8452dea 100644 --- a/pandas/tests/scalar/period/test_period.py +++ b/pandas/tests/scalar/period/test_period.py @@ -1447,7 +1447,8 @@ def test_period_immutable(): per.freq = 2 * freq -@pytest.mark.xfail(reason='GH#19834 Period parsing error') +# TODO: This doesn't fail on all systems; track down which +@pytest.mark.xfail(reason="Parses as Jan 1, 0007 on some systems") def test_small_year_parsing(): per1 = Period('0001-01-07', 'D') assert per1.year == 1 diff --git a/pandas/tests/series/test_analytics.py b/pandas/tests/series/test_analytics.py index 69969bd090b9b8..09e89115e120ec 100644 --- a/pandas/tests/series/test_analytics.py +++ b/pandas/tests/series/test_analytics.py @@ -2070,7 +2070,7 @@ def test_value_counts_with_nan(self): "dtype", ["int_", "uint", "float_", "unicode_", "timedelta64[h]", pytest.param("datetime64[D]", - marks=pytest.mark.xfail(reason="issue7996"))] + marks=pytest.mark.xfail(reason="GH#7996", strict=True))] ) @pytest.mark.parametrize("is_ordered", [True, False]) def test_drop_duplicates_categorical_non_bool(self, dtype, is_ordered): diff --git a/pandas/tests/series/test_rank.py b/pandas/tests/series/test_rank.py index d0e001cbfcd88f..42f2d45df2def4 100644 --- a/pandas/tests/series/test_rank.py +++ b/pandas/tests/series/test_rank.py @@ -223,7 +223,8 @@ def test_rank_signature(self): 'int64', marks=pytest.mark.xfail( reason="iNaT is equivalent to minimum value of dtype" - "int64 pending issue #16674")), + "int64 pending issue GH#16674", + strict=True)), ([NegInfinity(), '1', 'A', 'BA', 'Ba', 'C', Infinity()], 'object') ]) diff --git a/pandas/tests/sparse/frame/test_analytics.py b/pandas/tests/sparse/frame/test_analytics.py index ccb30502b862e7..54e3ddbf2f1cfb 100644 --- a/pandas/tests/sparse/frame/test_analytics.py +++ b/pandas/tests/sparse/frame/test_analytics.py @@ -4,8 +4,8 @@ from pandas.util import testing as tm -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_quantile(): # GH 17386 data = [[1, 1], [2, 10], [3, 100], [np.nan, np.nan]] @@ -22,8 +22,8 @@ def test_quantile(): tm.assert_sp_series_equal(result, sparse_expected) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_quantile_multi(): # GH 17386 data = [[1, 1], [2, 10], [3, 100], [np.nan, np.nan]] diff --git a/pandas/tests/sparse/frame/test_frame.py b/pandas/tests/sparse/frame/test_frame.py index 9cc615e15564fe..be5a1710119ee4 100644 --- a/pandas/tests/sparse/frame/test_frame.py +++ b/pandas/tests/sparse/frame/test_frame.py @@ -1131,7 +1131,8 @@ def test_as_blocks(self): tm.assert_frame_equal(df_blocks['float64'], df) @pytest.mark.xfail(reason='nan column names in _init_dict problematic ' - '(GH 16894)') + '(GH#16894)', + strict=True) def test_nan_columnname(self): # GH 8822 nan_colname = DataFrame(Series(1.0, index=[0]), columns=[nan]) @@ -1257,8 +1258,8 @@ def test_numpy_func_call(self): for func in funcs: getattr(np, func)(self.frame) - @pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_quantile(self): # GH 17386 data = [[1, 1], [2, 10], [3, 100], [nan, nan]] @@ -1274,8 +1275,8 @@ def test_quantile(self): tm.assert_series_equal(result, dense_expected) tm.assert_sp_series_equal(result, sparse_expected) - @pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') + @pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_quantile_multi(self): # GH 17386 data = [[1, 1], [2, 10], [3, 100], [nan, nan]] diff --git a/pandas/tests/sparse/frame/test_indexing.py b/pandas/tests/sparse/frame/test_indexing.py index 1c27d44015c2b8..607eb2da6ded09 100644 --- a/pandas/tests/sparse/frame/test_indexing.py +++ b/pandas/tests/sparse/frame/test_indexing.py @@ -18,8 +18,8 @@ [np.nan, np.nan] ] ]) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_numeric_data(data): # GH 17386 lower_bound = 1.5 @@ -52,8 +52,8 @@ def test_where_with_numeric_data(data): 0.1, 100.0 + 100.0j ]) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_numeric_data_and_other(data, other): # GH 17386 lower_bound = 1.5 @@ -70,8 +70,8 @@ def test_where_with_numeric_data_and_other(data, other): tm.assert_sp_frame_equal(result, sparse_expected) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_bool_data(): # GH 17386 data = [[False, False], [True, True], [False, False]] @@ -94,8 +94,8 @@ def test_where_with_bool_data(): 0.1, 100.0 + 100.0j ]) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_bool_data_and_other(other): # GH 17386 data = [[False, False], [True, True], [False, False]] diff --git a/pandas/tests/sparse/series/test_indexing.py b/pandas/tests/sparse/series/test_indexing.py index de01b065a9fa0a..998285d9334921 100644 --- a/pandas/tests/sparse/series/test_indexing.py +++ b/pandas/tests/sparse/series/test_indexing.py @@ -18,8 +18,8 @@ np.nan, np.nan ] ]) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_numeric_data(data): # GH 17386 lower_bound = 1.5 @@ -70,8 +70,8 @@ def test_where_with_numeric_data_and_other(data, other): tm.assert_sp_series_equal(result, sparse_expected) -@pytest.mark.xfail(reason='Wrong SparseBlock initialization ' - '(GH 17386)') +@pytest.mark.xfail(reason='Wrong SparseBlock initialization (GH#17386)', + strict=True) def test_where_with_bool_data(): # GH 17386 data = [False, False, True, True, False, False] diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 796c6374343538..58f2f41f3681cf 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -858,10 +858,8 @@ def test_duplicated_with_nas(self): 2, 4, 1, 5, 6]), np.array([1.1, 2.2, 1.1, np.nan, 3.3, 2.2, 4.4, 1.1, np.nan, 6.6]), - pytest.param(np.array([1 + 1j, 2 + 2j, 1 + 1j, 5 + 5j, 3 + 3j, - 2 + 2j, 4 + 4j, 1 + 1j, 5 + 5j, 6 + 6j]), - marks=pytest.mark.xfail(reason="Complex bug. GH 16399") - ), + np.array([1 + 1j, 2 + 2j, 1 + 1j, 5 + 5j, 3 + 3j, + 2 + 2j, 4 + 4j, 1 + 1j, 5 + 5j, 6 + 6j]), np.array(['a', 'b', 'a', 'e', 'c', 'b', 'd', 'a', 'e', 'f'], dtype=object), np.array([1, 2**63, 1, 3**5, 10, 2**63, 39, 1, 3**5, 7], diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py index b7530da36ed8bc..bbc5bd96bad550 100644 --- a/pandas/tests/test_base.py +++ b/pandas/tests/test_base.py @@ -1235,7 +1235,8 @@ def test_values_consistent(array, expected_type, dtype): pytest.param( pd.PeriodIndex(['2017', '2018'], freq='D'), np.array([17167, 17532]), - marks=pytest.mark.xfail(reason="PeriodArray Not implemented") + marks=pytest.mark.xfail(reason="PeriodArray Not implemented", + strict=True) ), ]) def test_ndarray_values(array, expected): diff --git a/pandas/tests/test_downstream.py b/pandas/tests/test_downstream.py index cf98cff97669a2..70973801d7cda9 100644 --- a/pandas/tests/test_downstream.py +++ b/pandas/tests/test_downstream.py @@ -95,7 +95,7 @@ def test_pandas_gbq(df): pandas_gbq = import_module('pandas_gbq') # noqa -@pytest.mark.xfail(reason="0.7.0 pending") +@pytest.mark.xfail(reason="0.7.0 pending", strict=True) @tm.network def test_pandas_datareader(): diff --git a/pandas/tests/test_window.py b/pandas/tests/test_window.py index 397da2fa40cd85..fc3b13a37fcdb5 100644 --- a/pandas/tests/test_window.py +++ b/pandas/tests/test_window.py @@ -621,8 +621,9 @@ def test_numpy_compat(self, method): @pytest.mark.parametrize( 'expander', [1, pytest.param('ls', marks=pytest.mark.xfail( - reason='GH 16425 expanding with ' - 'offset not supported'))]) + reason='GH#16425 expanding with ' + 'offset not supported', + strict=True))]) def test_empty_df_expanding(self, expander): # GH 15819 Verifies that datetime and integer expanding windows can be # applied to empty DataFrames From 9c118668256d2e36d2d310d46e604392a92bbee7 Mon Sep 17 00:00:00 2001 From: Pietro Battiston Date: Wed, 1 Aug 2018 16:43:14 +0200 Subject: [PATCH 27/47] DOC: clarify default behavior for categories ordering (#22081) --- pandas/core/arrays/categorical.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 204e800b932a98..3d9f1ca4027fd2 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -241,10 +241,14 @@ class Categorical(ExtensionArray, PandasObject): categories will be replaced with NaN. categories : Index-like (unique), optional The unique categories for this categorical. If not given, the - categories are assumed to be the unique values of values. + categories are assumed to be the unique values of `values` (sorted, if + possible, otherwise in the order in which they appear). ordered : boolean, (default False) Whether or not this categorical is treated as a ordered categorical. - If not given, the resulting categorical will not be ordered. + If True, the resulting categorical will be ordered. + An ordered categorical respects, when sorted, the order of its + `categories` attribute (which in turn is the `categories` argument, if + provided). dtype : CategoricalDtype An instance of ``CategoricalDtype`` to use for this categorical From 93f154cd5cca4cbd0ad0725c83700ffa61c6527c Mon Sep 17 00:00:00 2001 From: Daniel Himmelstein Date: Wed, 1 Aug 2018 17:23:34 -0400 Subject: [PATCH 28/47] API: Default to_* methods to compression='infer' (#22011) Closes gh-22004. --- doc/source/io.rst | 2 +- doc/source/whatsnew/v0.24.0.txt | 3 +- pandas/core/frame.py | 8 ++- pandas/core/generic.py | 9 +-- pandas/core/series.py | 11 ++-- pandas/io/formats/csvs.py | 41 ++++++------ pandas/io/json/json.py | 2 +- pandas/tests/io/test_common.py | 61 +++++++++--------- pandas/tests/io/test_compression.py | 99 +++++++++++++++++++++++++++++ pandas/tests/test_common.py | 69 ++------------------ 10 files changed, 180 insertions(+), 125 deletions(-) create mode 100644 pandas/tests/io/test_compression.py diff --git a/doc/source/io.rst b/doc/source/io.rst index 9fe578524c8e0d..c2c8c1c17700f3 100644 --- a/doc/source/io.rst +++ b/doc/source/io.rst @@ -298,7 +298,7 @@ compression : {``'infer'``, ``'gzip'``, ``'bz2'``, ``'zip'``, ``'xz'``, ``None`` Set to ``None`` for no decompression. .. versionadded:: 0.18.1 support for 'zip' and 'xz' compression. - + .. versionchanged:: 0.24.0 'infer' option added and set to default. thousands : str, default ``None`` Thousands separator. decimal : str, default ``'.'`` diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 8b89618cd0d88f..2e0d9ed2bf3f0d 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -177,7 +177,8 @@ Other Enhancements - :func:`read_html` copies cell data across ``colspan`` and ``rowspan``, and it treats all-``th`` table rows as headers if ``header`` kwarg is not given and there is no ``thead`` (:issue:`17054`) - :meth:`Series.nlargest`, :meth:`Series.nsmallest`, :meth:`DataFrame.nlargest`, and :meth:`DataFrame.nsmallest` now accept the value ``"all"`` for the ``keep`` argument. This keeps all ties for the nth largest/smallest value (:issue:`16818`) - :class:`IntervalIndex` has gained the :meth:`~IntervalIndex.set_closed` method to change the existing ``closed`` value (:issue:`21670`) -- :func:`~DataFrame.to_csv` and :func:`~DataFrame.to_json` now support ``compression='infer'`` to infer compression based on filename (:issue:`15008`) +- :func:`~DataFrame.to_csv`, :func:`~Series.to_csv`, :func:`~DataFrame.to_json`, and :func:`~Series.to_json` now support ``compression='infer'`` to infer compression based on filename extension (:issue:`15008`). + The default compression for ``to_csv``, ``to_json``, and ``to_pickle`` methods has been updated to ``'infer'`` (:issue:`22004`). - :func:`to_timedelta` now supports iso-formated timedelta strings (:issue:`21877`) - :class:`Series` and :class:`DataFrame` now support :class:`Iterable` in constructor (:issue:`2193`) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 834cc3d188b396..ebd35cb1a6a1ae 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1715,7 +1715,7 @@ def to_panel(self): def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, columns=None, header=True, index=True, index_label=None, - mode='w', encoding=None, compression=None, quoting=None, + mode='w', encoding=None, compression='infer', quoting=None, quotechar='"', line_terminator='\n', chunksize=None, tupleize_cols=None, date_format=None, doublequote=True, escapechar=None, decimal='.'): @@ -1750,10 +1750,14 @@ def to_csv(self, path_or_buf=None, sep=",", na_rep='', float_format=None, encoding : string, optional A string representing the encoding to use in the output file, defaults to 'ascii' on Python 2 and 'utf-8' on Python 3. - compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, default None + compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, + default 'infer' If 'infer' and `path_or_buf` is path-like, then detect compression from the following extensions: '.gz', '.bz2', '.zip' or '.xz' (otherwise no compression). + + .. versionchanged:: 0.24.0 + 'infer' option added and set to default line_terminator : string, default ``'\n'`` The newline character or character sequence to use in the output file diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 7a12ce0e1385e3..f62605c3427025 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1933,7 +1933,7 @@ def _repr_latex_(self): def to_json(self, path_or_buf=None, orient=None, date_format=None, double_precision=10, force_ascii=True, date_unit='ms', - default_handler=None, lines=False, compression=None, + default_handler=None, lines=False, compression='infer', index=True): """ Convert the object to a JSON string. @@ -1999,13 +1999,14 @@ def to_json(self, path_or_buf=None, orient=None, date_format=None, like. .. versionadded:: 0.19.0 - - compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, default None + compression : {'infer', 'gzip', 'bz2', 'zip', 'xz', None}, + default 'infer' A string representing the compression to use in the output file, only used when the first argument is a filename. .. versionadded:: 0.21.0 - + .. versionchanged:: 0.24.0 + 'infer' option added and set to default index : boolean, default True Whether to include the index values in the JSON string. Not including the index (``index=False``) is only supported when diff --git a/pandas/core/series.py b/pandas/core/series.py index 8f9fe5ee516e69..21dea15772cc07 100644 --- a/pandas/core/series.py +++ b/pandas/core/series.py @@ -3767,7 +3767,7 @@ def from_csv(cls, path, sep=',', parse_dates=True, header=None, def to_csv(self, path=None, index=True, sep=",", na_rep='', float_format=None, header=False, index_label=None, - mode='w', encoding=None, compression=None, date_format=None, + mode='w', encoding=None, compression='infer', date_format=None, decimal='.'): """ Write Series to a comma-separated values (csv) file @@ -3795,10 +3795,13 @@ def to_csv(self, path=None, index=True, sep=",", na_rep='', encoding : string, optional a string representing the encoding to use if the contents are non-ascii, for python versions prior to 3 - compression : string, optional + compression : None or string, default 'infer' A string representing the compression to use in the output file. - Allowed values are 'gzip', 'bz2', 'zip', 'xz'. This input is only - used when the first argument is a filename. + Allowed values are None, 'gzip', 'bz2', 'zip', 'xz', and 'infer'. + This input is only used when the first argument is a filename. + + .. versionchanged:: 0.24.0 + 'infer' option added and set to default date_format: string, default None Format string for datetime objects. decimal: string, default '.' diff --git a/pandas/io/formats/csvs.py b/pandas/io/formats/csvs.py index 0796888554a46a..6fabd2573a7b40 100644 --- a/pandas/io/formats/csvs.py +++ b/pandas/io/formats/csvs.py @@ -21,8 +21,13 @@ from pandas.core.dtypes.generic import ( ABCMultiIndex, ABCPeriodIndex, ABCDatetimeIndex, ABCIndexClass) -from pandas.io.common import (_get_handle, UnicodeWriter, _expand_user, - _stringify_path) +from pandas.io.common import ( + _expand_user, + _get_handle, + _infer_compression, + _stringify_path, + UnicodeWriter, +) class CSVFormatter(object): @@ -30,7 +35,7 @@ class CSVFormatter(object): def __init__(self, obj, path_or_buf=None, sep=",", na_rep='', float_format=None, cols=None, header=True, index=True, index_label=None, mode='w', nanRep=None, encoding=None, - compression=None, quoting=None, line_terminator='\n', + compression='infer', quoting=None, line_terminator='\n', chunksize=None, tupleize_cols=False, quotechar='"', date_format=None, doublequote=True, escapechar=None, decimal='.'): @@ -50,8 +55,10 @@ def __init__(self, obj, path_or_buf=None, sep=",", na_rep='', self.index = index self.index_label = index_label self.mode = mode + if encoding is None: + encoding = 'ascii' if compat.PY2 else 'utf-8' self.encoding = encoding - self.compression = compression + self.compression = _infer_compression(self.path_or_buf, compression) if quoting is None: quoting = csvlib.QUOTE_MINIMAL @@ -124,16 +131,10 @@ def __init__(self, obj, path_or_buf=None, sep=",", na_rep='', self.nlevels = 0 def save(self): - # create the writer & save - if self.encoding is None: - if compat.PY2: - encoding = 'ascii' - else: - encoding = 'utf-8' - else: - encoding = self.encoding - - # GH 21227 internal compression is not used when file-like passed. + """ + Create the writer & save + """ + # GH21227 internal compression is not used when file-like passed. if self.compression and hasattr(self.path_or_buf, 'write'): msg = ("compression has no effect when passing file-like " "object as input.") @@ -147,7 +148,7 @@ def save(self): if is_zip: # zipfile doesn't support writing string to archive. uses string # buffer to receive csv writing and dump into zip compression - # file handle. GH 21241, 21118 + # file handle. GH21241, GH21118 f = StringIO() close = False elif hasattr(self.path_or_buf, 'write'): @@ -155,7 +156,7 @@ def save(self): close = False else: f, handles = _get_handle(self.path_or_buf, self.mode, - encoding=encoding, + encoding=self.encoding, compression=self.compression) close = True @@ -165,23 +166,23 @@ def save(self): doublequote=self.doublequote, escapechar=self.escapechar, quotechar=self.quotechar) - if encoding == 'ascii': + if self.encoding == 'ascii': self.writer = csvlib.writer(f, **writer_kwargs) else: - writer_kwargs['encoding'] = encoding + writer_kwargs['encoding'] = self.encoding self.writer = UnicodeWriter(f, **writer_kwargs) self._save() finally: if is_zip: - # GH 17778 handles zip compression separately. + # GH17778 handles zip compression separately. buf = f.getvalue() if hasattr(self.path_or_buf, 'write'): self.path_or_buf.write(buf) else: f, handles = _get_handle(self.path_or_buf, self.mode, - encoding=encoding, + encoding=self.encoding, compression=self.compression) f.write(buf) close = True diff --git a/pandas/io/json/json.py b/pandas/io/json/json.py index 629e00ebfa7d01..c5f8872f93d944 100644 --- a/pandas/io/json/json.py +++ b/pandas/io/json/json.py @@ -28,7 +28,7 @@ # interface to/from def to_json(path_or_buf, obj, orient=None, date_format='epoch', double_precision=10, force_ascii=True, date_unit='ms', - default_handler=None, lines=False, compression=None, + default_handler=None, lines=False, compression='infer', index=True): if not index and orient not in ['split', 'table']: diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index 5c9739be733932..ceaac9818354a9 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -1,19 +1,20 @@ """ - Tests for the pandas.io.common functionalities +Tests for the pandas.io.common functionalities """ import mmap -import pytest import os -from os.path import isabs + +import pytest import pandas as pd -import pandas.util.testing as tm +import pandas.io.common as icom import pandas.util._test_decorators as td - -from pandas.io import common -from pandas.compat import is_platform_windows, StringIO, FileNotFoundError - -from pandas import read_csv, concat +import pandas.util.testing as tm +from pandas.compat import ( + is_platform_windows, + StringIO, + FileNotFoundError, +) class CustomFSPath(object): @@ -55,24 +56,24 @@ class TestCommonIOCapabilities(object): def test_expand_user(self): filename = '~/sometest' - expanded_name = common._expand_user(filename) + expanded_name = icom._expand_user(filename) assert expanded_name != filename - assert isabs(expanded_name) + assert os.path.isabs(expanded_name) assert os.path.expanduser(filename) == expanded_name def test_expand_user_normal_path(self): filename = '/somefolder/sometest' - expanded_name = common._expand_user(filename) + expanded_name = icom._expand_user(filename) assert expanded_name == filename assert os.path.expanduser(filename) == expanded_name @td.skip_if_no('pathlib') def test_stringify_path_pathlib(self): - rel_path = common._stringify_path(Path('.')) + rel_path = icom._stringify_path(Path('.')) assert rel_path == '.' - redundant_path = common._stringify_path(Path('foo//bar')) + redundant_path = icom._stringify_path(Path('foo//bar')) assert redundant_path == os.path.join('foo', 'bar') @td.skip_if_no('py.path') @@ -80,11 +81,11 @@ def test_stringify_path_localpath(self): path = os.path.join('foo', 'bar') abs_path = os.path.abspath(path) lpath = LocalPath(path) - assert common._stringify_path(lpath) == abs_path + assert icom._stringify_path(lpath) == abs_path def test_stringify_path_fspath(self): p = CustomFSPath('foo/bar.csv') - result = common._stringify_path(p) + result = icom._stringify_path(p) assert result == 'foo/bar.csv' @pytest.mark.parametrize('extension,expected', [ @@ -97,36 +98,36 @@ def test_stringify_path_fspath(self): @pytest.mark.parametrize('path_type', path_types) def test_infer_compression_from_path(self, extension, expected, path_type): path = path_type('foo/bar.csv' + extension) - compression = common._infer_compression(path, compression='infer') + compression = icom._infer_compression(path, compression='infer') assert compression == expected def test_get_filepath_or_buffer_with_path(self): filename = '~/sometest' - filepath_or_buffer, _, _, should_close = common.get_filepath_or_buffer( + filepath_or_buffer, _, _, should_close = icom.get_filepath_or_buffer( filename) assert filepath_or_buffer != filename - assert isabs(filepath_or_buffer) + assert os.path.isabs(filepath_or_buffer) assert os.path.expanduser(filename) == filepath_or_buffer assert not should_close def test_get_filepath_or_buffer_with_buffer(self): input_buffer = StringIO() - filepath_or_buffer, _, _, should_close = common.get_filepath_or_buffer( + filepath_or_buffer, _, _, should_close = icom.get_filepath_or_buffer( input_buffer) assert filepath_or_buffer == input_buffer assert not should_close def test_iterator(self): - reader = read_csv(StringIO(self.data1), chunksize=1) - result = concat(reader, ignore_index=True) - expected = read_csv(StringIO(self.data1)) + reader = pd.read_csv(StringIO(self.data1), chunksize=1) + result = pd.concat(reader, ignore_index=True) + expected = pd.read_csv(StringIO(self.data1)) tm.assert_frame_equal(result, expected) # GH12153 - it = read_csv(StringIO(self.data1), chunksize=1) + it = pd.read_csv(StringIO(self.data1), chunksize=1) first = next(it) tm.assert_frame_equal(first, expected.iloc[[0]]) - tm.assert_frame_equal(concat(it), expected.iloc[1:]) + tm.assert_frame_equal(pd.concat(it), expected.iloc[1:]) @pytest.mark.parametrize('reader, module, error_class, fn_ext', [ (pd.read_csv, 'os', FileNotFoundError, 'csv'), @@ -246,18 +247,18 @@ def test_constructor_bad_file(self, mmap_file): msg = "[Errno 22]" err = mmap.error - tm.assert_raises_regex(err, msg, common.MMapWrapper, non_file) + tm.assert_raises_regex(err, msg, icom.MMapWrapper, non_file) target = open(mmap_file, 'r') target.close() msg = "I/O operation on closed file" tm.assert_raises_regex( - ValueError, msg, common.MMapWrapper, target) + ValueError, msg, icom.MMapWrapper, target) def test_get_attr(self, mmap_file): with open(mmap_file, 'r') as target: - wrapper = common.MMapWrapper(target) + wrapper = icom.MMapWrapper(target) attrs = dir(wrapper.mmap) attrs = [attr for attr in attrs @@ -271,7 +272,7 @@ def test_get_attr(self, mmap_file): def test_next(self, mmap_file): with open(mmap_file, 'r') as target: - wrapper = common.MMapWrapper(target) + wrapper = icom.MMapWrapper(target) lines = target.readlines() for line in lines: @@ -285,4 +286,4 @@ def test_unknown_engine(self): df = tm.makeDataFrame() df.to_csv(path) with tm.assert_raises_regex(ValueError, 'Unknown engine'): - read_csv(path, engine='pyt') + pd.read_csv(path, engine='pyt') diff --git a/pandas/tests/io/test_compression.py b/pandas/tests/io/test_compression.py new file mode 100644 index 00000000000000..76788ced44e846 --- /dev/null +++ b/pandas/tests/io/test_compression.py @@ -0,0 +1,99 @@ +import os + +import pytest + +import pandas as pd +import pandas.io.common as icom +import pandas.util.testing as tm + + +@pytest.mark.parametrize('obj', [ + pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']), + pd.Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) +@pytest.mark.parametrize('method', ['to_pickle', 'to_json', 'to_csv']) +def test_compression_size(obj, method, compression_only): + with tm.ensure_clean() as path: + getattr(obj, method)(path, compression=compression_only) + compressed_size = os.path.getsize(path) + getattr(obj, method)(path, compression=None) + uncompressed_size = os.path.getsize(path) + assert uncompressed_size > compressed_size + + +@pytest.mark.parametrize('obj', [ + pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']), + pd.Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) +@pytest.mark.parametrize('method', ['to_csv', 'to_json']) +def test_compression_size_fh(obj, method, compression_only): + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=compression_only) + with f: + getattr(obj, method)(f) + assert not f.closed + assert f.closed + compressed_size = os.path.getsize(path) + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=None) + with f: + getattr(obj, method)(f) + assert not f.closed + assert f.closed + uncompressed_size = os.path.getsize(path) + assert uncompressed_size > compressed_size + + +@pytest.mark.parametrize('write_method, write_kwargs, read_method', [ + ('to_csv', {'index': False}, pd.read_csv), + ('to_json', {}, pd.read_json), + ('to_pickle', {}, pd.read_pickle), +]) +def test_dataframe_compression_defaults_to_infer( + write_method, write_kwargs, read_method, compression_only): + # GH22004 + input = pd.DataFrame([[1.0, 0, -4], [3.4, 5, 2]], columns=['X', 'Y', 'Z']) + extension = icom._compression_to_extension[compression_only] + with tm.ensure_clean('compressed' + extension) as path: + getattr(input, write_method)(path, **write_kwargs) + output = read_method(path, compression=compression_only) + tm.assert_frame_equal(output, input) + + +@pytest.mark.parametrize('write_method,write_kwargs,read_method,read_kwargs', [ + ('to_csv', {'index': False, 'header': True}, + pd.read_csv, {'squeeze': True}), + ('to_json', {}, pd.read_json, {'typ': 'series'}), + ('to_pickle', {}, pd.read_pickle, {}), +]) +def test_series_compression_defaults_to_infer( + write_method, write_kwargs, read_method, read_kwargs, + compression_only): + # GH22004 + input = pd.Series([0, 5, -2, 10], name='X') + extension = icom._compression_to_extension[compression_only] + with tm.ensure_clean('compressed' + extension) as path: + getattr(input, write_method)(path, **write_kwargs) + output = read_method(path, compression=compression_only, **read_kwargs) + tm.assert_series_equal(output, input, check_names=False) + + +def test_compression_warning(compression_only): + # Assert that passing a file object to to_csv while explicitly specifying a + # compression protocol triggers a RuntimeWarning, as per GH21227. + # Note that pytest has an issue that causes assert_produces_warning to fail + # in Python 2 if the warning has occurred in previous tests + # (see https://git.io/fNEBm & https://git.io/fNEBC). Hence, should this + # test fail in just Python 2 builds, it likely indicates that other tests + # are producing RuntimeWarnings, thereby triggering the pytest bug. + df = pd.DataFrame(100 * [[0.123456, 0.234567, 0.567567], + [12.32112, 123123.2, 321321.2]], + columns=['X', 'Y', 'Z']) + with tm.ensure_clean() as path: + f, handles = icom._get_handle(path, 'w', compression=compression_only) + with tm.assert_produces_warning(RuntimeWarning, + check_stacklevel=False): + with f: + df.to_csv(f, compression=compression_only) diff --git a/pandas/tests/test_common.py b/pandas/tests/test_common.py index e1c92021899723..868525e818b62c 100644 --- a/pandas/tests/test_common.py +++ b/pandas/tests/test_common.py @@ -1,17 +1,16 @@ # -*- coding: utf-8 -*- -import pytest -import os import collections from functools import partial import numpy as np +import pytest -from pandas import Series, DataFrame, Timestamp -import pandas.core.common as com -from pandas.core import ops -from pandas.io.common import _get_handle -import pandas.util.testing as tm +from pandas import Series, Timestamp +from pandas.core import ( + common as com, + ops, +) def test_get_callable_name(): @@ -20,7 +19,7 @@ def test_get_callable_name(): def fn(x): return x - lambda_ = lambda x: x + lambda_ = lambda x: x # noqa: E731 part1 = partial(fn) part2 = partial(part1) @@ -111,57 +110,3 @@ def test_standardize_mapping(): dd = collections.defaultdict(list) assert isinstance(com.standardize_mapping(dd), partial) - - -@pytest.mark.parametrize('obj', [ - DataFrame(100 * [[0.123456, 0.234567, 0.567567], - [12.32112, 123123.2, 321321.2]], - columns=['X', 'Y', 'Z']), - Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) -@pytest.mark.parametrize('method', ['to_pickle', 'to_json', 'to_csv']) -def test_compression_size(obj, method, compression_only): - - with tm.ensure_clean() as filename: - getattr(obj, method)(filename, compression=compression_only) - compressed = os.path.getsize(filename) - getattr(obj, method)(filename, compression=None) - uncompressed = os.path.getsize(filename) - assert uncompressed > compressed - - -@pytest.mark.parametrize('obj', [ - DataFrame(100 * [[0.123456, 0.234567, 0.567567], - [12.32112, 123123.2, 321321.2]], - columns=['X', 'Y', 'Z']), - Series(100 * [0.123456, 0.234567, 0.567567], name='X')]) -@pytest.mark.parametrize('method', ['to_csv', 'to_json']) -def test_compression_size_fh(obj, method, compression_only): - - with tm.ensure_clean() as filename: - f, _handles = _get_handle(filename, 'w', compression=compression_only) - with f: - getattr(obj, method)(f) - assert not f.closed - assert f.closed - compressed = os.path.getsize(filename) - with tm.ensure_clean() as filename: - f, _handles = _get_handle(filename, 'w', compression=None) - with f: - getattr(obj, method)(f) - assert not f.closed - assert f.closed - uncompressed = os.path.getsize(filename) - assert uncompressed > compressed - - -# GH 21227 -def test_compression_warning(compression_only): - df = DataFrame(100 * [[0.123456, 0.234567, 0.567567], - [12.32112, 123123.2, 321321.2]], - columns=['X', 'Y', 'Z']) - with tm.ensure_clean() as filename: - f, _handles = _get_handle(filename, 'w', compression=compression_only) - with tm.assert_produces_warning(RuntimeWarning, - check_stacklevel=False): - with f: - df.to_csv(f, compression=compression_only) From 599631c4309322d47b7556abdf2ed983a17298b5 Mon Sep 17 00:00:00 2001 From: topper-123 Date: Wed, 1 Aug 2018 21:49:57 +0000 Subject: [PATCH 29/47] Remove depr. warning in SeriesGroupBy.count (#22155) --- pandas/core/groupby/generic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/groupby/generic.py b/pandas/core/groupby/generic.py index 5b2590cfcf0100..685635fb6854dc 100644 --- a/pandas/core/groupby/generic.py +++ b/pandas/core/groupby/generic.py @@ -46,6 +46,7 @@ from pandas.core.index import Index, MultiIndex, CategoricalIndex from pandas.core.arrays.categorical import Categorical from pandas.core.internals import BlockManager, make_block +from pandas.compat.numpy import _np_version_under1p13 from pandas.plotting._core import boxplot_frame_groupby @@ -1206,7 +1207,8 @@ def count(self): mask = (ids != -1) & ~isna(val) ids = ensure_platform_int(ids) - out = np.bincount(ids[mask], minlength=ngroups or None) + minlength = ngroups or (None if _np_version_under1p13 else 0) + out = np.bincount(ids[mask], minlength=minlength) return Series(out, index=self.grouper.result_index, From d010469c10d23f1dd8544ede42ae856b4760003e Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 1 Aug 2018 14:53:23 -0700 Subject: [PATCH 30/47] use memoryviews instead of ndarrays (#22147) --- pandas/_libs/hashing.pyx | 8 ++--- pandas/_libs/tslib.pyx | 11 +++---- pandas/_libs/tslibs/conversion.pyx | 50 ++++++++++++++++-------------- pandas/_libs/tslibs/fields.pyx | 5 ++- pandas/_libs/tslibs/parsing.pyx | 37 +++++++++++----------- pandas/_libs/tslibs/period.pyx | 39 +++++++++++------------ pandas/_libs/tslibs/resolution.pyx | 7 +++-- pandas/_libs/tslibs/strptime.pyx | 10 +++--- pandas/_libs/tslibs/timedeltas.pxd | 4 +-- pandas/_libs/tslibs/timedeltas.pyx | 14 ++++----- pandas/_libs/tslibs/timestamps.pyx | 6 ++-- pandas/_libs/tslibs/timezones.pyx | 7 ++--- 12 files changed, 100 insertions(+), 98 deletions(-) diff --git a/pandas/_libs/hashing.pyx b/pandas/_libs/hashing.pyx index ff92ee306288a8..16cfde620d269d 100644 --- a/pandas/_libs/hashing.pyx +++ b/pandas/_libs/hashing.pyx @@ -5,7 +5,7 @@ import cython import numpy as np -from numpy cimport ndarray, uint8_t, uint32_t, uint64_t +from numpy cimport uint8_t, uint32_t, uint64_t from util cimport _checknull from cpython cimport (PyBytes_Check, @@ -17,7 +17,7 @@ DEF dROUNDS = 4 @cython.boundscheck(False) -def hash_object_array(ndarray[object] arr, object key, object encoding='utf8'): +def hash_object_array(object[:] arr, object key, object encoding='utf8'): """ Parameters ---------- @@ -37,7 +37,7 @@ def hash_object_array(ndarray[object] arr, object key, object encoding='utf8'): """ cdef: Py_ssize_t i, l, n - ndarray[uint64_t] result + uint64_t[:] result bytes data, k uint8_t *kb uint64_t *lens @@ -89,7 +89,7 @@ def hash_object_array(ndarray[object] arr, object key, object encoding='utf8'): free(vecs) free(lens) - return result + return result.base # .base to retrieve underlying np.ndarray cdef inline uint64_t _rotl(uint64_t x, uint64_t b) nogil: diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 76e3d6e92d31ef..eba553bfaeb48e 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # cython: profile=False -cimport cython from cython cimport Py_ssize_t from cpython cimport PyFloat_Check, PyUnicode_Check @@ -37,8 +36,7 @@ from tslibs.np_datetime import OutOfBoundsDatetime from tslibs.parsing import parse_datetime_string from tslibs.timedeltas cimport cast_from_unit -from tslibs.timezones cimport (is_utc, is_tzlocal, is_fixed_offset, - treat_tz_as_pytz, get_dst_info) +from tslibs.timezones cimport is_utc, is_tzlocal, get_dst_info from tslibs.conversion cimport (tz_convert_single, _TSObject, convert_datetime_to_tsobject, get_datetime64_nanos, @@ -77,8 +75,7 @@ cdef inline object create_time_from_ts( return time(dts.hour, dts.min, dts.sec, dts.us) -def ints_to_pydatetime(ndarray[int64_t] arr, tz=None, freq=None, - box="datetime"): +def ints_to_pydatetime(int64_t[:] arr, tz=None, freq=None, box="datetime"): """ Convert an i8 repr to an ndarray of datetimes, date, time or Timestamp @@ -102,7 +99,9 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None, freq=None, cdef: Py_ssize_t i, n = len(arr) - ndarray[int64_t] trans, deltas + ndarray[int64_t] trans + int64_t[:] deltas + Py_ssize_t pos npy_datetimestruct dts object dt int64_t value, delta diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 4335e7baeafe96..a459b185fa48c6 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -91,7 +91,7 @@ def ensure_datetime64ns(ndarray arr, copy=True): """ cdef: Py_ssize_t i, n = arr.size - ndarray[int64_t] ivalues, iresult + int64_t[:] ivalues, iresult NPY_DATETIMEUNIT unit npy_datetimestruct dts @@ -139,7 +139,7 @@ def ensure_timedelta64ns(ndarray arr, copy=True): return arr.astype(TD_DTYPE, copy=copy) -def datetime_to_datetime64(ndarray[object] values): +def datetime_to_datetime64(object[:] values): """ Convert ndarray of datetime-like objects to int64 array representing nanosecond timestamps. @@ -156,7 +156,7 @@ def datetime_to_datetime64(ndarray[object] values): cdef: Py_ssize_t i, n = len(values) object val, inferred_tz = None - ndarray[int64_t] iresult + int64_t[:] iresult npy_datetimestruct dts _TSObject _ts @@ -525,7 +525,8 @@ cdef inline void localize_tso(_TSObject obj, tzinfo tz): Sets obj.tzinfo inplace, alters obj.dts inplace. """ cdef: - ndarray[int64_t] trans, deltas + ndarray[int64_t] trans + int64_t[:] deltas int64_t local_val Py_ssize_t pos @@ -631,15 +632,16 @@ cdef inline int64_t[:] _tz_convert_dst(ndarray[int64_t] values, tzinfo tz, cdef: Py_ssize_t n = len(values) Py_ssize_t i, j, pos - ndarray[int64_t] result = np.empty(n, dtype=np.int64) - ndarray[int64_t] tt, trans, deltas - ndarray[Py_ssize_t] posn + int64_t[:] result = np.empty(n, dtype=np.int64) + ndarray[int64_t] tt, trans + int64_t[:] deltas + Py_ssize_t[:] posn int64_t v trans, deltas, typ = get_dst_info(tz) if not to_utc: # We add `offset` below instead of subtracting it - deltas = -1 * deltas + deltas = -1 * np.array(deltas, dtype='i8') tt = values[values != NPY_NAT] if not len(tt): @@ -728,7 +730,7 @@ cpdef int64_t tz_convert_single(int64_t val, object tz1, object tz2): converted: int64 """ cdef: - ndarray[int64_t] trans, deltas + int64_t[:] deltas Py_ssize_t pos int64_t v, offset, utc_date npy_datetimestruct dts @@ -756,7 +758,7 @@ cpdef int64_t tz_convert_single(int64_t val, object tz1, object tz2): else: # Convert UTC to other timezone arr = np.array([utc_date]) - # Note: at least with cython 0.28.3, doing a looking `[0]` in the next + # Note: at least with cython 0.28.3, doing a lookup `[0]` in the next # line is sensitive to the declared return type of _tz_convert_dst; # if it is declared as returning ndarray[int64_t], a compile-time error # is raised. @@ -781,10 +783,9 @@ def tz_convert(ndarray[int64_t] vals, object tz1, object tz2): """ cdef: - ndarray[int64_t] utc_dates, tt, result, trans, deltas + ndarray[int64_t] utc_dates, result Py_ssize_t i, j, pos, n = len(vals) - int64_t v, offset, delta - npy_datetimestruct dts + int64_t v if len(vals) == 0: return np.array([], dtype=np.int64) @@ -843,7 +844,8 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, localized : ndarray[int64_t] """ cdef: - ndarray[int64_t] trans, deltas, idx_shifted + ndarray[int64_t] trans + int64_t[:] deltas, idx_shifted ndarray ambiguous_array Py_ssize_t i, idx, pos, ntrans, n = len(vals) int64_t *tdata @@ -1069,7 +1071,7 @@ def normalize_date(object dt): @cython.wraparound(False) @cython.boundscheck(False) -def normalize_i8_timestamps(ndarray[int64_t] stamps, tz=None): +def normalize_i8_timestamps(int64_t[:] stamps, tz=None): """ Normalize each of the (nanosecond) timestamps in the given array by rounding down to the beginning of the day (i.e. midnight). If `tz` @@ -1087,7 +1089,7 @@ def normalize_i8_timestamps(ndarray[int64_t] stamps, tz=None): cdef: Py_ssize_t i, n = len(stamps) npy_datetimestruct dts - ndarray[int64_t] result = np.empty(n, dtype=np.int64) + int64_t[:] result = np.empty(n, dtype=np.int64) if tz is not None: tz = maybe_get_tz(tz) @@ -1101,12 +1103,12 @@ def normalize_i8_timestamps(ndarray[int64_t] stamps, tz=None): dt64_to_dtstruct(stamps[i], &dts) result[i] = _normalized_stamp(&dts) - return result + return result.base # .base to access underlying np.ndarray @cython.wraparound(False) @cython.boundscheck(False) -cdef ndarray[int64_t] _normalize_local(ndarray[int64_t] stamps, object tz): +cdef int64_t[:] _normalize_local(int64_t[:] stamps, object tz): """ Normalize each of the (nanosecond) timestamps in the given array by rounding down to the beginning of the day (i.e. midnight) for the @@ -1123,8 +1125,9 @@ cdef ndarray[int64_t] _normalize_local(ndarray[int64_t] stamps, object tz): """ cdef: Py_ssize_t n = len(stamps) - ndarray[int64_t] result = np.empty(n, dtype=np.int64) - ndarray[int64_t] trans, deltas + int64_t[:] result = np.empty(n, dtype=np.int64) + ndarray[int64_t] trans + int64_t[:] deltas Py_ssize_t[:] pos npy_datetimestruct dts int64_t delta @@ -1190,7 +1193,7 @@ cdef inline int64_t _normalized_stamp(npy_datetimestruct *dts) nogil: return dtstruct_to_dt64(dts) -def is_date_array_normalized(ndarray[int64_t] stamps, tz=None): +def is_date_array_normalized(int64_t[:] stamps, tz=None): """ Check if all of the given (nanosecond) timestamps are normalized to midnight, i.e. hour == minute == second == 0. If the optional timezone @@ -1206,8 +1209,9 @@ def is_date_array_normalized(ndarray[int64_t] stamps, tz=None): is_normalized : bool True if all stamps are normalized """ cdef: - Py_ssize_t i, n = len(stamps) - ndarray[int64_t] trans, deltas + Py_ssize_t pos, i, n = len(stamps) + ndarray[int64_t] trans + int64_t[:] deltas npy_datetimestruct dts int64_t local_val, delta diff --git a/pandas/_libs/tslibs/fields.pyx b/pandas/_libs/tslibs/fields.pyx index a298f521ef8530..96f023f7fdafe1 100644 --- a/pandas/_libs/tslibs/fields.pyx +++ b/pandas/_libs/tslibs/fields.pyx @@ -85,8 +85,7 @@ def build_field_sarray(ndarray[int64_t] dtindex): @cython.wraparound(False) @cython.boundscheck(False) -def get_date_name_field(ndarray[int64_t] dtindex, object field, - object locale=None): +def get_date_name_field(int64_t[:] dtindex, object field, object locale=None): """ Given a int64-based datetime index, return array of strings of date name based on requested field (e.g. weekday_name) @@ -134,7 +133,7 @@ def get_date_name_field(ndarray[int64_t] dtindex, object field, @cython.wraparound(False) -def get_start_end_field(ndarray[int64_t] dtindex, object field, +def get_start_end_field(int64_t[:] dtindex, object field, object freqstr=None, int month_kw=12): """ Given an int64-based datetime index return array of indicators diff --git a/pandas/_libs/tslibs/parsing.pyx b/pandas/_libs/tslibs/parsing.pyx index ffa3d8df44be80..afda2046fd12df 100644 --- a/pandas/_libs/tslibs/parsing.pyx +++ b/pandas/_libs/tslibs/parsing.pyx @@ -14,7 +14,6 @@ from cpython.datetime cimport datetime import time import numpy as np -from numpy cimport ndarray # Avoid import from outside _libs if sys.version_info.major == 2: @@ -381,11 +380,11 @@ cpdef object _get_rule_month(object source, object default='DEC'): # Parsing for type-inference -def try_parse_dates(ndarray[object] values, parser=None, +def try_parse_dates(object[:] values, parser=None, dayfirst=False, default=None): cdef: Py_ssize_t i, n - ndarray[object] result + object[:] result n = len(values) result = np.empty(n, dtype='O') @@ -420,15 +419,15 @@ def try_parse_dates(ndarray[object] values, parser=None, # raise if passed parser and it failed raise - return result + return result.base # .base to access underlying ndarray -def try_parse_date_and_time(ndarray[object] dates, ndarray[object] times, +def try_parse_date_and_time(object[:] dates, object[:] times, date_parser=None, time_parser=None, dayfirst=False, default=None): cdef: Py_ssize_t i, n - ndarray[object] result + object[:] result n = len(dates) if len(times) != n: @@ -457,14 +456,14 @@ def try_parse_date_and_time(ndarray[object] dates, ndarray[object] times, result[i] = datetime(d.year, d.month, d.day, t.hour, t.minute, t.second) - return result + return result.base # .base to access underlying ndarray -def try_parse_year_month_day(ndarray[object] years, ndarray[object] months, - ndarray[object] days): +def try_parse_year_month_day(object[:] years, object[:] months, + object[:] days): cdef: Py_ssize_t i, n - ndarray[object] result + object[:] result n = len(years) if len(months) != n or len(days) != n: @@ -474,19 +473,19 @@ def try_parse_year_month_day(ndarray[object] years, ndarray[object] months, for i in range(n): result[i] = datetime(int(years[i]), int(months[i]), int(days[i])) - return result + return result.base # .base to access underlying ndarray -def try_parse_datetime_components(ndarray[object] years, - ndarray[object] months, - ndarray[object] days, - ndarray[object] hours, - ndarray[object] minutes, - ndarray[object] seconds): +def try_parse_datetime_components(object[:] years, + object[:] months, + object[:] days, + object[:] hours, + object[:] minutes, + object[:] seconds): cdef: Py_ssize_t i, n - ndarray[object] result + object[:] result int secs double float_secs double micros @@ -509,7 +508,7 @@ def try_parse_datetime_components(ndarray[object] years, int(hours[i]), int(minutes[i]), secs, int(micros)) - return result + return result.base # .base to access underlying ndarray # ---------------------------------------------------------------------- diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 96d7994bdc822d..811f0d25c38383 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -423,13 +423,13 @@ cdef inline int month_to_quarter(int month): @cython.wraparound(False) @cython.boundscheck(False) -def dt64arr_to_periodarr(ndarray[int64_t] dtarr, int freq, tz=None): +def dt64arr_to_periodarr(int64_t[:] dtarr, int freq, tz=None): """ Convert array of datetime64 values (passed in as 'i8' dtype) to a set of periods corresponding to desired frequency, per period convention. """ cdef: - ndarray[int64_t] out + int64_t[:] out Py_ssize_t i, l npy_datetimestruct dts @@ -447,18 +447,18 @@ def dt64arr_to_periodarr(ndarray[int64_t] dtarr, int freq, tz=None): out[i] = get_period_ordinal(&dts, freq) else: out = localize_dt64arr_to_period(dtarr, freq, tz) - return out + return out.base # .base to access underlying np.ndarray @cython.wraparound(False) @cython.boundscheck(False) -def periodarr_to_dt64arr(ndarray[int64_t] periodarr, int freq): +def periodarr_to_dt64arr(int64_t[:] periodarr, int freq): """ Convert array to datetime64 values from a set of ordinals corresponding to periods per period convention. """ cdef: - ndarray[int64_t] out + int64_t[:] out Py_ssize_t i, l l = len(periodarr) @@ -472,7 +472,7 @@ def periodarr_to_dt64arr(ndarray[int64_t] periodarr, int freq): continue out[i] = period_ordinal_to_dt64(periodarr[i], freq) - return out + return out.base # .base to access underlying np.ndarray cpdef int64_t period_asfreq(int64_t ordinal, int freq1, int freq2, bint end): @@ -556,7 +556,7 @@ def period_asfreq_arr(ndarray[int64_t] arr, int freq1, int freq2, bint end): if upsampling, choose to use start ('S') or end ('E') of period. """ cdef: - ndarray[int64_t] result + int64_t[:] result Py_ssize_t i, n freq_conv_func func asfreq_info af_info @@ -584,7 +584,7 @@ def period_asfreq_arr(ndarray[int64_t] arr, int freq1, int freq2, bint end): raise ValueError("Unable to convert to desired frequency.") result[i] = val - return result + return result.base # .base to access underlying np.ndarray cpdef int64_t period_ordinal(int y, int m, int d, int h, int min, @@ -825,10 +825,10 @@ cdef int pdays_in_month(int64_t ordinal, int freq): return ccalendar.get_days_in_month(dts.year, dts.month) -def get_period_field_arr(int code, ndarray[int64_t] arr, int freq): +def get_period_field_arr(int code, int64_t[:] arr, int freq): cdef: Py_ssize_t i, sz - ndarray[int64_t] out + int64_t[:] out accessor f func = _get_accessor_func(code) @@ -844,7 +844,7 @@ def get_period_field_arr(int code, ndarray[int64_t] arr, int freq): continue out[i] = func(arr[i], freq) - return out + return out.base # .base to access underlying np.ndarray cdef accessor _get_accessor_func(int code): @@ -875,10 +875,10 @@ cdef accessor _get_accessor_func(int code): return NULL -def extract_ordinals(ndarray[object] values, freq): +def extract_ordinals(object[:] values, freq): cdef: Py_ssize_t i, n = len(values) - ndarray[int64_t] ordinals = np.empty(n, dtype=np.int64) + int64_t[:] ordinals = np.empty(n, dtype=np.int64) object p freqstr = Period._maybe_convert_freq(freq).freqstr @@ -904,10 +904,10 @@ def extract_ordinals(ndarray[object] values, freq): else: ordinals[i] = p.ordinal - return ordinals + return ordinals.base # .base to access underlying np.ndarray -def extract_freq(ndarray[object] values): +def extract_freq(object[:] values): cdef: Py_ssize_t i, n = len(values) object p @@ -930,12 +930,13 @@ def extract_freq(ndarray[object] values): @cython.wraparound(False) @cython.boundscheck(False) -cdef ndarray[int64_t] localize_dt64arr_to_period(ndarray[int64_t] stamps, - int freq, object tz): +cdef int64_t[:] localize_dt64arr_to_period(int64_t[:] stamps, + int freq, object tz): cdef: Py_ssize_t n = len(stamps) - ndarray[int64_t] result = np.empty(n, dtype=np.int64) - ndarray[int64_t] trans, deltas + int64_t[:] result = np.empty(n, dtype=np.int64) + ndarray[int64_t] trans + int64_t[:] deltas Py_ssize_t[:] pos npy_datetimestruct dts int64_t local_val diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index 0659e2a553e7e9..18cc21ccd59e0c 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -31,7 +31,7 @@ cdef int RESO_DAY = 6 # ---------------------------------------------------------------------- -cpdef resolution(ndarray[int64_t] stamps, tz=None): +cpdef resolution(int64_t[:] stamps, tz=None): cdef: Py_ssize_t i, n = len(stamps) npy_datetimestruct dts @@ -42,11 +42,12 @@ cpdef resolution(ndarray[int64_t] stamps, tz=None): return _reso_local(stamps, tz) -cdef _reso_local(ndarray[int64_t] stamps, object tz): +cdef _reso_local(int64_t[:] stamps, object tz): cdef: Py_ssize_t i, n = len(stamps) int reso = RESO_DAY, curr_reso - ndarray[int64_t] trans, deltas + ndarray[int64_t] trans + int64_t[:] deltas Py_ssize_t[:] pos npy_datetimestruct dts int64_t local_val, delta diff --git a/pandas/_libs/tslibs/strptime.pyx b/pandas/_libs/tslibs/strptime.pyx index de2b7440156a76..59d673881bb407 100644 --- a/pandas/_libs/tslibs/strptime.pyx +++ b/pandas/_libs/tslibs/strptime.pyx @@ -26,7 +26,7 @@ from cython cimport Py_ssize_t from cpython cimport PyFloat_Check import numpy as np -from numpy cimport ndarray, int64_t +from numpy cimport int64_t from datetime import date as datetime_date @@ -60,7 +60,7 @@ cdef dict _parse_code_table = {'y': 0, 'z': 19} -def array_strptime(ndarray[object] values, object fmt, +def array_strptime(object[:] values, object fmt, bint exact=True, errors='raise'): """ Calculates the datetime structs represented by the passed array of strings @@ -76,8 +76,8 @@ def array_strptime(ndarray[object] values, object fmt, cdef: Py_ssize_t i, n = len(values) npy_datetimestruct dts - ndarray[int64_t] iresult - ndarray[object] result_timezone + int64_t[:] iresult + object[:] result_timezone int year, month, day, minute, hour, second, weekday, julian int week_of_year, week_of_year_start, parse_code, ordinal int64_t us, ns @@ -320,7 +320,7 @@ def array_strptime(ndarray[object] values, object fmt, result_timezone[i] = timezone - return result, result_timezone + return result, result_timezone.base """_getlang, LocaleTime, TimeRE, _calc_julian_from_U_or_W are vendored diff --git a/pandas/_libs/tslibs/timedeltas.pxd b/pandas/_libs/tslibs/timedeltas.pxd index 3e7b88b208e893..2413c281e0a52d 100644 --- a/pandas/_libs/tslibs/timedeltas.pxd +++ b/pandas/_libs/tslibs/timedeltas.pxd @@ -3,11 +3,11 @@ from cpython.datetime cimport timedelta -from numpy cimport int64_t, ndarray +from numpy cimport int64_t # Exposed for tslib, not intended for outside use. cdef parse_timedelta_string(object ts) cpdef int64_t cast_from_unit(object ts, object unit) except? -1 cpdef int64_t delta_to_nanoseconds(delta) except? -1 cpdef convert_to_timedelta64(object ts, object unit) -cpdef array_to_timedelta64(ndarray[object] values, unit=*, errors=*) +cpdef array_to_timedelta64(object[:] values, unit=*, errors=*) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index f7a6cf0c6dafc2..9e7f1d94934ba3 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -13,7 +13,7 @@ from cpython cimport PyUnicode_Check, Py_NE, Py_EQ, PyObject_RichCompare import numpy as np cimport numpy as cnp -from numpy cimport int64_t, ndarray +from numpy cimport int64_t cnp.import_array() from cpython.datetime cimport (datetime, timedelta, @@ -83,7 +83,7 @@ _no_input = object() # ---------------------------------------------------------------------- # API -def ints_to_pytimedelta(ndarray[int64_t] arr, box=False): +def ints_to_pytimedelta(int64_t[:] arr, box=False): """ convert an i8 repr to an ndarray of timedelta or Timedelta (if box == True) @@ -101,7 +101,7 @@ def ints_to_pytimedelta(ndarray[int64_t] arr, box=False): cdef: Py_ssize_t i, n = len(arr) int64_t value - ndarray[object] result = np.empty(n, dtype=object) + object[:] result = np.empty(n, dtype=object) for i in range(n): @@ -114,7 +114,7 @@ def ints_to_pytimedelta(ndarray[int64_t] arr, box=False): else: result[i] = timedelta(microseconds=int(value) / 1000) - return result + return result.base # .base to access underlying np.ndarray # ---------------------------------------------------------------------- @@ -199,7 +199,7 @@ cpdef convert_to_timedelta64(object ts, object unit): return ts.astype('timedelta64[ns]') -cpdef array_to_timedelta64(ndarray[object] values, unit='ns', errors='raise'): +cpdef array_to_timedelta64(object[:] values, unit='ns', errors='raise'): """ Convert an ndarray to an array of timedeltas. If errors == 'coerce', coerce non-convertible objects to NaT. Otherwise, raise. @@ -207,7 +207,7 @@ cpdef array_to_timedelta64(ndarray[object] values, unit='ns', errors='raise'): cdef: Py_ssize_t i, n - ndarray[int64_t] iresult + int64_t[:] iresult if errors not in ('ignore', 'raise', 'coerce'): raise ValueError("errors must be one of 'ignore', " @@ -233,7 +233,7 @@ cpdef array_to_timedelta64(ndarray[object] values, unit='ns', errors='raise'): else: raise - return iresult + return iresult.base # .base to access underlying np.ndarray cpdef inline int64_t cast_from_unit(object ts, object unit) except? -1: diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index be988e7247e59e..eb5c0076a868a3 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -7,7 +7,7 @@ from cpython cimport (PyObject_RichCompareBool, PyObject_RichCompare, import numpy as np cimport numpy as cnp -from numpy cimport int64_t, int32_t, ndarray +from numpy cimport int64_t, int32_t, int8_t cnp.import_array() from datetime import time as datetime_time @@ -342,7 +342,7 @@ cdef class _Timestamp(datetime): cdef: int64_t val dict kwds - ndarray out + int8_t out[1] int month_kw freq = self.freq @@ -362,7 +362,7 @@ cdef class _Timestamp(datetime): cpdef _get_date_name_field(self, object field, object locale): cdef: int64_t val - ndarray out + object[:] out val = self._maybe_convert_value_to_local() out = get_date_name_field(np.array([val], dtype=np.int64), diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index 2e3b07252d45eb..a787452d90c074 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # cython: profile=False -cimport cython from cython cimport Py_ssize_t # dateutil compat @@ -19,7 +18,7 @@ UTC = pytz.utc import numpy as np cimport numpy as cnp -from numpy cimport ndarray, int64_t +from numpy cimport int64_t cnp.import_array() # ---------------------------------------------------------------------- @@ -188,10 +187,10 @@ cdef object get_utc_trans_times_from_dateutil_tz(object tz): return new_trans -cpdef ndarray[int64_t, ndim=1] unbox_utcoffsets(object transinfo): +cpdef int64_t[:] unbox_utcoffsets(object transinfo): cdef: Py_ssize_t i, sz - ndarray[int64_t] arr + int64_t[:] arr sz = len(transinfo) arr = np.empty(sz, dtype='i8') From a8836f328a31dd29a5b2b667b0675f38ba9e6468 Mon Sep 17 00:00:00 2001 From: Wenhuan Date: Thu, 2 Aug 2018 06:10:53 +0800 Subject: [PATCH 31/47] BUG: Better handling of invalid na_option argument for groupby.rank(#22124) (#22125) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/groupby/groupby.py | 3 ++ pandas/tests/groupby/test_rank.py | 62 ++++++++++++++++++------------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 2e0d9ed2bf3f0d..ca60dea241d88b 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -659,6 +659,7 @@ Reshaping - Bug in :meth:`Series.where` and :meth:`DataFrame.where` with ``datetime64[ns, tz]`` dtype (:issue:`21546`) - Bug in :meth:`Series.mask` and :meth:`DataFrame.mask` with ``list`` conditionals (:issue:`21891`) - Bug in :meth:`DataFrame.replace` raises RecursionError when converting OutOfBounds ``datetime64[ns, tz]`` (:issue:`20380`) +- :func:`pandas.core.groupby.GroupBy.rank` now raises a ``ValueError`` when an invalid value is passed for argument ``na_option`` (:issue:`22124`) - Build Changes diff --git a/pandas/core/groupby/groupby.py b/pandas/core/groupby/groupby.py index 4b0143b3e1cede..3f84fa0f0670e8 100644 --- a/pandas/core/groupby/groupby.py +++ b/pandas/core/groupby/groupby.py @@ -1705,6 +1705,9 @@ def rank(self, method='average', ascending=True, na_option='keep', ----- DataFrame with ranking of values within each group """ + if na_option not in {'keep', 'top', 'bottom'}: + msg = "na_option must be one of 'keep', 'top', or 'bottom'" + raise ValueError(msg) return self._cython_transform('rank', numeric_only=False, ties_method=method, ascending=ascending, na_option=na_option, pct=pct, axis=axis) diff --git a/pandas/tests/groupby/test_rank.py b/pandas/tests/groupby/test_rank.py index 0628f9c79a154c..f0dcf768e36071 100644 --- a/pandas/tests/groupby/test_rank.py +++ b/pandas/tests/groupby/test_rank.py @@ -172,35 +172,35 @@ def test_infs_n_nans(grps, vals, ties_method, ascending, na_option, exp): [3., 3., np.nan, 1., 3., 2., np.nan, np.nan]), ('dense', False, 'keep', True, [3. / 3., 3. / 3., np.nan, 1. / 3., 3. / 3., 2. / 3., np.nan, np.nan]), - ('average', True, 'no_na', False, [2., 2., 7., 5., 2., 4., 7., 7.]), - ('average', True, 'no_na', True, + ('average', True, 'bottom', False, [2., 2., 7., 5., 2., 4., 7., 7.]), + ('average', True, 'bottom', True, [0.25, 0.25, 0.875, 0.625, 0.25, 0.5, 0.875, 0.875]), - ('average', False, 'no_na', False, [4., 4., 7., 1., 4., 2., 7., 7.]), - ('average', False, 'no_na', True, + ('average', False, 'bottom', False, [4., 4., 7., 1., 4., 2., 7., 7.]), + ('average', False, 'bottom', True, [0.5, 0.5, 0.875, 0.125, 0.5, 0.25, 0.875, 0.875]), - ('min', True, 'no_na', False, [1., 1., 6., 5., 1., 4., 6., 6.]), - ('min', True, 'no_na', True, + ('min', True, 'bottom', False, [1., 1., 6., 5., 1., 4., 6., 6.]), + ('min', True, 'bottom', True, [0.125, 0.125, 0.75, 0.625, 0.125, 0.5, 0.75, 0.75]), - ('min', False, 'no_na', False, [3., 3., 6., 1., 3., 2., 6., 6.]), - ('min', False, 'no_na', True, + ('min', False, 'bottom', False, [3., 3., 6., 1., 3., 2., 6., 6.]), + ('min', False, 'bottom', True, [0.375, 0.375, 0.75, 0.125, 0.375, 0.25, 0.75, 0.75]), - ('max', True, 'no_na', False, [3., 3., 8., 5., 3., 4., 8., 8.]), - ('max', True, 'no_na', True, + ('max', True, 'bottom', False, [3., 3., 8., 5., 3., 4., 8., 8.]), + ('max', True, 'bottom', True, [0.375, 0.375, 1., 0.625, 0.375, 0.5, 1., 1.]), - ('max', False, 'no_na', False, [5., 5., 8., 1., 5., 2., 8., 8.]), - ('max', False, 'no_na', True, + ('max', False, 'bottom', False, [5., 5., 8., 1., 5., 2., 8., 8.]), + ('max', False, 'bottom', True, [0.625, 0.625, 1., 0.125, 0.625, 0.25, 1., 1.]), - ('first', True, 'no_na', False, [1., 2., 6., 5., 3., 4., 7., 8.]), - ('first', True, 'no_na', True, + ('first', True, 'bottom', False, [1., 2., 6., 5., 3., 4., 7., 8.]), + ('first', True, 'bottom', True, [0.125, 0.25, 0.75, 0.625, 0.375, 0.5, 0.875, 1.]), - ('first', False, 'no_na', False, [3., 4., 6., 1., 5., 2., 7., 8.]), - ('first', False, 'no_na', True, + ('first', False, 'bottom', False, [3., 4., 6., 1., 5., 2., 7., 8.]), + ('first', False, 'bottom', True, [0.375, 0.5, 0.75, 0.125, 0.625, 0.25, 0.875, 1.]), - ('dense', True, 'no_na', False, [1., 1., 4., 3., 1., 2., 4., 4.]), - ('dense', True, 'no_na', True, + ('dense', True, 'bottom', False, [1., 1., 4., 3., 1., 2., 4., 4.]), + ('dense', True, 'bottom', True, [0.25, 0.25, 1., 0.75, 0.25, 0.5, 1., 1.]), - ('dense', False, 'no_na', False, [3., 3., 4., 1., 3., 2., 4., 4.]), - ('dense', False, 'no_na', True, + ('dense', False, 'bottom', False, [3., 3., 4., 1., 3., 2., 4., 4.]), + ('dense', False, 'bottom', True, [0.75, 0.75, 1., 0.25, 0.75, 0.5, 1., 1.]) ]) def test_rank_args_missing(grps, vals, ties_method, ascending, @@ -252,14 +252,24 @@ def test_rank_object_raises(ties_method, ascending, na_option, with tm.assert_raises_regex(TypeError, "not callable"): df.groupby('key').rank(method=ties_method, ascending=ascending, - na_option='bad', pct=pct) + na_option=na_option, pct=pct) - with tm.assert_raises_regex(TypeError, "not callable"): - df.groupby('key').rank(method=ties_method, - ascending=ascending, - na_option=True, pct=pct) - with tm.assert_raises_regex(TypeError, "not callable"): +@pytest.mark.parametrize("na_option", [True, "bad", 1]) +@pytest.mark.parametrize("ties_method", [ + 'average', 'min', 'max', 'first', 'dense']) +@pytest.mark.parametrize("ascending", [True, False]) +@pytest.mark.parametrize("pct", [True, False]) +@pytest.mark.parametrize("vals", [ + ['bar', 'bar', 'foo', 'bar', 'baz'], + ['bar', np.nan, 'foo', np.nan, 'baz'], + [1, np.nan, 2, np.nan, 3] +]) +def test_rank_naoption_raises(ties_method, ascending, na_option, pct, vals): + df = DataFrame({'key': ['foo'] * 5, 'val': vals}) + msg = "na_option must be one of 'keep', 'top', or 'bottom'" + + with tm.assert_raises_regex(ValueError, msg): df.groupby('key').rank(method=ties_method, ascending=ascending, na_option=na_option, pct=pct) From 647f3f091eedd715b7966829a36ef345de4d7a4d Mon Sep 17 00:00:00 2001 From: miker985 Date: Wed, 1 Aug 2018 15:12:53 -0700 Subject: [PATCH 32/47] Fix categorical from codes nan 21767 (#21775) --- doc/source/whatsnew/v0.24.0.txt | 5 ++--- pandas/core/arrays/categorical.py | 17 +++++++++++++++- .../arrays/categorical/test_constructors.py | 20 +++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index ca60dea241d88b..61119089fdb426 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -477,6 +477,7 @@ Deprecations - :meth:`MultiIndex.to_hierarchical` is deprecated and will be removed in a future version (:issue:`21613`) - :meth:`Series.ptp` is deprecated. Use ``numpy.ptp`` instead (:issue:`21614`) - :meth:`Series.compress` is deprecated. Use ``Series[condition]`` instead (:issue:`18262`) +- :meth:`Categorical.from_codes` has deprecated providing float values for the ``codes`` argument. (:issue:`21767`) .. _whatsnew_0240.prior_deprecations: @@ -524,9 +525,7 @@ Bug Fixes Categorical ^^^^^^^^^^^ -- -- -- +- Bug in :meth:`Categorical.from_codes` where ``NaN`` values in `codes` were silently converted to ``0`` (:issue:`21767`). In the future this will raise a ``ValueError``. Also changes the behavior of `.from_codes([1.1, 2.0])`. Datetimelike ^^^^^^^^^^^^ diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 3d9f1ca4027fd2..eebdfe8a54a9de 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -27,6 +27,8 @@ is_timedelta64_dtype, is_categorical, is_categorical_dtype, + is_float_dtype, + is_integer_dtype, is_list_like, is_sequence, is_scalar, is_iterator, is_dict_like) @@ -633,8 +635,21 @@ def from_codes(cls, codes, categories, ordered=False): categorical. If not given, the resulting categorical will be unordered. """ + codes = np.asarray(codes) # #21767 + if not is_integer_dtype(codes): + msg = "codes need to be array-like integers" + if is_float_dtype(codes): + icodes = codes.astype('i8') + if (icodes == codes).all(): + msg = None + codes = icodes + warn(("float codes will be disallowed in the future and " + "raise a ValueError"), FutureWarning, stacklevel=2) + if msg: + raise ValueError(msg) + try: - codes = coerce_indexer_dtype(np.asarray(codes), categories) + codes = coerce_indexer_dtype(codes, categories) except (ValueError, TypeError): raise ValueError( "codes need to be convertible to an arrays of integers") diff --git a/pandas/tests/arrays/categorical/test_constructors.py b/pandas/tests/arrays/categorical/test_constructors.py index e5d620df96493d..b5f499ba273239 100644 --- a/pandas/tests/arrays/categorical/test_constructors.py +++ b/pandas/tests/arrays/categorical/test_constructors.py @@ -468,6 +468,26 @@ def test_from_codes_with_categorical_categories(self): with pytest.raises(ValueError): Categorical.from_codes([0, 1], Categorical(['a', 'b', 'a'])) + def test_from_codes_with_nan_code(self): + # GH21767 + codes = [1, 2, np.nan] + categories = ['a', 'b', 'c'] + with pytest.raises(ValueError): + Categorical.from_codes(codes, categories) + + def test_from_codes_with_float(self): + # GH21767 + codes = [1.0, 2.0, 0] # integer, but in float dtype + categories = ['a', 'b', 'c'] + + with tm.assert_produces_warning(FutureWarning): + cat = Categorical.from_codes(codes, categories) + tm.assert_numpy_array_equal(cat.codes, np.array([1, 2, 0], dtype='i1')) + + codes = [1.1, 2.0, 0] # non-integer + with pytest.raises(ValueError): + Categorical.from_codes(codes, categories) + @pytest.mark.parametrize('dtype', [None, 'category']) def test_from_inferred_categories(self, dtype): cats = ['a', 'b'] From 4cf95ba86571ce971ab475dc5f28842593d8f53f Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 2 Aug 2018 03:18:15 -0700 Subject: [PATCH 33/47] implement tslibs/src to make tslibs self-contained (#22152) --- pandas/_libs/src/ujson/python/objToJSON.c | 4 +- .../{ => tslibs}/src/datetime/np_datetime.c | 4 ++ .../{ => tslibs}/src/datetime/np_datetime.h | 4 ++ .../src/datetime/np_datetime_strings.c | 4 ++ .../src/datetime/np_datetime_strings.h | 4 ++ pandas/_libs/{ => tslibs}/src/period_helper.c | 24 ++++---- pandas/_libs/{ => tslibs}/src/period_helper.h | 8 ++- setup.py | 55 ++++++++++++++----- 8 files changed, 76 insertions(+), 31 deletions(-) rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime.c (99%) rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime.h (96%) rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime_strings.c (99%) rename pandas/_libs/{ => tslibs}/src/datetime/np_datetime_strings.h (96%) rename pandas/_libs/{ => tslibs}/src/period_helper.c (96%) rename pandas/_libs/{ => tslibs}/src/period_helper.h (97%) diff --git a/pandas/_libs/src/ujson/python/objToJSON.c b/pandas/_libs/src/ujson/python/objToJSON.c index 8c7b92ddeaa81a..a5e93640742aaa 100644 --- a/pandas/_libs/src/ujson/python/objToJSON.c +++ b/pandas/_libs/src/ujson/python/objToJSON.c @@ -47,8 +47,8 @@ Numeric decoder derived from from TCL library #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) +#include <../../../tslibs/src/datetime/np_datetime.h> // NOLINT(build/include_order) +#include <../../../tslibs/src/datetime/np_datetime_strings.h> // NOLINT(build/include_order) #include "datetime.h" static PyObject *type_decimal; diff --git a/pandas/_libs/src/datetime/np_datetime.c b/pandas/_libs/tslibs/src/datetime/np_datetime.c similarity index 99% rename from pandas/_libs/src/datetime/np_datetime.c rename to pandas/_libs/tslibs/src/datetime/np_datetime.c index 663ec66a35db21..1b33f38441253b 100644 --- a/pandas/_libs/src/datetime/np_datetime.c +++ b/pandas/_libs/tslibs/src/datetime/np_datetime.c @@ -16,6 +16,10 @@ This file is derived from NumPy 1.7. See NUMPY_LICENSE.txt #define NO_IMPORT +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + #include #include diff --git a/pandas/_libs/src/datetime/np_datetime.h b/pandas/_libs/tslibs/src/datetime/np_datetime.h similarity index 96% rename from pandas/_libs/src/datetime/np_datetime.h rename to pandas/_libs/tslibs/src/datetime/np_datetime.h index 04009c6581ac0b..9fa85b18dd2197 100644 --- a/pandas/_libs/src/datetime/np_datetime.h +++ b/pandas/_libs/tslibs/src/datetime/np_datetime.h @@ -17,6 +17,10 @@ This file is derived from NumPy 1.7. See NUMPY_LICENSE.txt #ifndef PANDAS__LIBS_SRC_DATETIME_NP_DATETIME_H_ #define PANDAS__LIBS_SRC_DATETIME_NP_DATETIME_H_ +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + #include #include diff --git a/pandas/_libs/src/datetime/np_datetime_strings.c b/pandas/_libs/tslibs/src/datetime/np_datetime_strings.c similarity index 99% rename from pandas/_libs/src/datetime/np_datetime_strings.c rename to pandas/_libs/tslibs/src/datetime/np_datetime_strings.c index fa96cce1756c88..19ade6fa5add9a 100644 --- a/pandas/_libs/src/datetime/np_datetime_strings.c +++ b/pandas/_libs/tslibs/src/datetime/np_datetime_strings.c @@ -22,6 +22,10 @@ This file implements string parsing and creation for NumPy datetime. #define PY_SSIZE_T_CLEAN #define NO_IMPORT +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + #include #include diff --git a/pandas/_libs/src/datetime/np_datetime_strings.h b/pandas/_libs/tslibs/src/datetime/np_datetime_strings.h similarity index 96% rename from pandas/_libs/src/datetime/np_datetime_strings.h rename to pandas/_libs/tslibs/src/datetime/np_datetime_strings.h index 821bb79b345bd2..e9a7fd74b05e51 100644 --- a/pandas/_libs/src/datetime/np_datetime_strings.h +++ b/pandas/_libs/tslibs/src/datetime/np_datetime_strings.h @@ -22,6 +22,10 @@ This file implements string parsing and creation for NumPy datetime. #ifndef PANDAS__LIBS_SRC_DATETIME_NP_DATETIME_STRINGS_H_ #define PANDAS__LIBS_SRC_DATETIME_NP_DATETIME_STRINGS_H_ +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + /* * Parses (almost) standard ISO 8601 date strings. The differences are: * diff --git a/pandas/_libs/src/period_helper.c b/pandas/_libs/tslibs/src/period_helper.c similarity index 96% rename from pandas/_libs/src/period_helper.c rename to pandas/_libs/tslibs/src/period_helper.c index 7dab77131c1a0b..4bf3774e35a681 100644 --- a/pandas/_libs/src/period_helper.c +++ b/pandas/_libs/tslibs/src/period_helper.c @@ -13,8 +13,12 @@ frequency conversion routines. See end of file for stuff pandas uses (search for 'pandas'). */ +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + #include "period_helper.h" -#include "../datetime/np_datetime.h" +#include "datetime/np_datetime.h" /* ------------------------------------------------------------------ * Code derived from scikits.timeseries @@ -79,9 +83,9 @@ static npy_int64 daytime_conversion_factor_matrix[7][7] = { int max_value(int a, int b) { return a > b ? a : b; } -PANDAS_INLINE int min_value(int a, int b) { return a < b ? a : b; } +static int min_value(int a, int b) { return a < b ? a : b; } -PANDAS_INLINE int get_freq_group(int freq) { return (freq / 1000) * 1000; } +static int get_freq_group(int freq) { return (freq / 1000) * 1000; } npy_int64 get_daytime_conversion_factor(int from_index, int to_index) { @@ -97,8 +101,7 @@ npy_int64 get_daytime_conversion_factor(int from_index, int to_index) { return daytime_conversion_factor_matrix[row - 6][col - 6]; } -PANDAS_INLINE npy_int64 upsample_daytime(npy_int64 ordinal, - asfreq_info *af_info) { +static npy_int64 upsample_daytime(npy_int64 ordinal, asfreq_info *af_info) { if (af_info->is_end) { return (ordinal + 1) * af_info->intraday_conversion_factor - 1; } else { @@ -106,15 +109,14 @@ PANDAS_INLINE npy_int64 upsample_daytime(npy_int64 ordinal, } } -PANDAS_INLINE npy_int64 downsample_daytime(npy_int64 ordinal, - asfreq_info *af_info) { +static npy_int64 downsample_daytime(npy_int64 ordinal, asfreq_info *af_info) { return ordinal / (af_info->intraday_conversion_factor); } -PANDAS_INLINE npy_int64 transform_via_day(npy_int64 ordinal, - asfreq_info *af_info, - freq_conv_func first_func, - freq_conv_func second_func) { +static npy_int64 transform_via_day(npy_int64 ordinal, + asfreq_info *af_info, + freq_conv_func first_func, + freq_conv_func second_func) { npy_int64 result; result = (*first_func)(ordinal, af_info); diff --git a/pandas/_libs/src/period_helper.h b/pandas/_libs/tslibs/src/period_helper.h similarity index 97% rename from pandas/_libs/src/period_helper.h rename to pandas/_libs/tslibs/src/period_helper.h index 8f538b261db9e2..f0198935bd4213 100644 --- a/pandas/_libs/src/period_helper.h +++ b/pandas/_libs/tslibs/src/period_helper.h @@ -14,9 +14,11 @@ frequency conversion routines. #ifndef PANDAS__LIBS_SRC_PERIOD_HELPER_H_ #define PANDAS__LIBS_SRC_PERIOD_HELPER_H_ +#ifndef NPY_NO_DEPRECATED_API +#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION +#endif + #include -#include "headers/stdint.h" -#include "helper.h" #include "limits.h" #include "numpy/ndarraytypes.h" @@ -74,7 +76,7 @@ frequency conversion routines. #define FR_UND -10000 /* Undefined */ -#define INT_ERR_CODE INT32_MIN +#define INT_ERR_CODE NPY_MIN_INT32 typedef struct asfreq_info { int is_end; diff --git a/setup.py b/setup.py index f058c8a6e3c99a..3289f1e99b87fd 100755 --- a/setup.py +++ b/setup.py @@ -226,15 +226,15 @@ def initialize_options(self): self._clean_trees = [] base = pjoin('pandas', '_libs', 'src') - dt = pjoin(base, 'datetime') - src = base + tsbase = pjoin('pandas', '_libs', 'tslibs', 'src') + dt = pjoin(tsbase, 'datetime') util = pjoin('pandas', 'util') parser = pjoin(base, 'parser') ujson_python = pjoin(base, 'ujson', 'python') ujson_lib = pjoin(base, 'ujson', 'lib') self._clean_exclude = [pjoin(dt, 'np_datetime.c'), pjoin(dt, 'np_datetime_strings.c'), - pjoin(src, 'period_helper.c'), + pjoin(tsbase, 'period_helper.c'), pjoin(parser, 'tokenizer.c'), pjoin(parser, 'io.c'), pjoin(ujson_python, 'ujson.c'), @@ -498,16 +498,19 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): lib_depends = [] common_include = ['pandas/_libs/src/klib', 'pandas/_libs/src'] +ts_include = ['pandas/_libs/tslibs/src'] lib_depends = lib_depends + ['pandas/_libs/src/numpy_helper.h', 'pandas/_libs/src/parse_helper.h', 'pandas/_libs/src/compat_helper.h'] -np_datetime_headers = ['pandas/_libs/src/datetime/np_datetime.h', - 'pandas/_libs/src/datetime/np_datetime_strings.h'] -np_datetime_sources = ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c'] +np_datetime_headers = [ + 'pandas/_libs/tslibs/src/datetime/np_datetime.h', + 'pandas/_libs/tslibs/src/datetime/np_datetime_strings.h'] +np_datetime_sources = [ + 'pandas/_libs/tslibs/src/datetime/np_datetime.c', + 'pandas/_libs/tslibs/src/datetime/np_datetime_strings.c'] tseries_depends = np_datetime_headers @@ -520,13 +523,16 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): 'pyxfile': '_libs/groupby', 'depends': _pxi_dep['groupby']}, '_libs.hashing': { - 'pyxfile': '_libs/hashing'}, + 'pyxfile': '_libs/hashing', + 'include': [], + 'depends': []}, '_libs.hashtable': { 'pyxfile': '_libs/hashtable', 'depends': (['pandas/_libs/src/klib/khash_python.h'] + _pxi_dep['hashtable'])}, '_libs.index': { 'pyxfile': '_libs/index', + 'include': common_include + ts_include, 'depends': _pxi_dep['index'], 'sources': np_datetime_sources}, '_libs.indexing': { @@ -541,9 +547,11 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): 'depends': _pxi_dep['join']}, '_libs.lib': { 'pyxfile': '_libs/lib', + 'include': common_include + ts_include, 'depends': lib_depends + tseries_depends}, '_libs.missing': { 'pyxfile': '_libs/missing', + 'include': common_include + ts_include, 'depends': tseries_depends}, '_libs.parsers': { 'pyxfile': '_libs/parsers', @@ -570,54 +578,71 @@ def srcpath(name=None, suffix='.pyx', subdir='src'): 'depends': _pxi_dep['sparse']}, '_libs.tslib': { 'pyxfile': '_libs/tslib', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.ccalendar': { - 'pyxfile': '_libs/tslibs/ccalendar'}, + 'pyxfile': '_libs/tslibs/ccalendar', + 'include': []}, '_libs.tslibs.conversion': { 'pyxfile': '_libs/tslibs/conversion', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.fields': { 'pyxfile': '_libs/tslibs/fields', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.frequencies': { - 'pyxfile': '_libs/tslibs/frequencies'}, + 'pyxfile': '_libs/tslibs/frequencies', + 'include': []}, '_libs.tslibs.nattype': { - 'pyxfile': '_libs/tslibs/nattype'}, + 'pyxfile': '_libs/tslibs/nattype', + 'include': []}, '_libs.tslibs.np_datetime': { 'pyxfile': '_libs/tslibs/np_datetime', + 'include': ts_include, 'depends': np_datetime_headers, 'sources': np_datetime_sources}, '_libs.tslibs.offsets': { 'pyxfile': '_libs/tslibs/offsets', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.parsing': { - 'pyxfile': '_libs/tslibs/parsing'}, + 'pyxfile': '_libs/tslibs/parsing', + 'include': []}, '_libs.tslibs.period': { 'pyxfile': '_libs/tslibs/period', - 'depends': tseries_depends + ['pandas/_libs/src/period_helper.h'], - 'sources': np_datetime_sources + ['pandas/_libs/src/period_helper.c']}, + 'include': ts_include, + 'depends': tseries_depends + [ + 'pandas/_libs/tslibs/src/period_helper.h'], + 'sources': np_datetime_sources + [ + 'pandas/_libs/tslibs/src/period_helper.c']}, '_libs.tslibs.resolution': { 'pyxfile': '_libs/tslibs/resolution', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.strptime': { 'pyxfile': '_libs/tslibs/strptime', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.timedeltas': { 'pyxfile': '_libs/tslibs/timedeltas', + 'include': ts_include, 'depends': np_datetime_headers, 'sources': np_datetime_sources}, '_libs.tslibs.timestamps': { 'pyxfile': '_libs/tslibs/timestamps', + 'include': ts_include, 'depends': tseries_depends, 'sources': np_datetime_sources}, '_libs.tslibs.timezones': { - 'pyxfile': '_libs/tslibs/timezones'}, + 'pyxfile': '_libs/tslibs/timezones', + 'include': []}, '_libs.testing': { 'pyxfile': '_libs/testing'}, '_libs.window': { From d23c6174fb56d1e10539dfd33c8ac57095c39391 Mon Sep 17 00:00:00 2001 From: Matthew Roeschke Date: Thu, 2 Aug 2018 03:29:48 -0700 Subject: [PATCH 34/47] CLN: Use public method to capture UTC offsets (#22164) --- pandas/_libs/tslib.pyx | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index eba553bfaeb48e..12089f1617f5da 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -634,24 +634,12 @@ cpdef array_to_datetime(ndarray[object] values, errors='raise', # If the dateutil parser returned tzinfo, capture it # to check if all arguments have the same tzinfo - tz = py_dt.tzinfo + tz = py_dt.utcoffset() if tz is not None: seen_datetime_offset = 1 - if tz == dateutil_utc(): - # dateutil.tz.tzutc has no offset-like attribute - # Just add the 0 offset explicitly - out_tzoffset_vals.add(0) - elif tz == tzlocal(): - # is comparison fails unlike other dateutil.tz - # objects. Also, dateutil.tz.tzlocal has no - # _offset attribute like tzoffset - offset_seconds = tz._dst_offset.total_seconds() - out_tzoffset_vals.add(offset_seconds) - else: - # dateutil.tz.tzoffset objects cannot be hashed - # store the total_seconds() instead - offset_seconds = tz._offset.total_seconds() - out_tzoffset_vals.add(offset_seconds) + # dateutil timezone objects cannot be hashed, so store + # the UTC offsets in seconds instead + out_tzoffset_vals.add(tz.total_seconds()) else: # Add a marker for naive string, to track if we are # parsing mixed naive and aware strings From 115724a04dde1e0479fbaf884ba8e81e4a9041c5 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 2 Aug 2018 05:42:50 -0500 Subject: [PATCH 35/47] BUG: Matplotlib scatter datetime (#22039) --- doc/source/whatsnew/v0.24.0.txt | 4 ++-- pandas/plotting/_converter.py | 6 +++++- pandas/tests/plotting/test_datetimelike.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 61119089fdb426..ceb03ce06a0eca 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -629,8 +629,8 @@ I/O Plotting ^^^^^^^^ -- Bug in :func:'DataFrame.plot.scatter' and :func:'DataFrame.plot.hexbin' caused x-axis label and ticklabels to disappear when colorbar was on in IPython inline backend (:issue:`10611`, :issue:`10678`, and :issue:`20455`) -- +- Bug in :func:`DataFrame.plot.scatter` and :func:`DataFrame.plot.hexbin` caused x-axis label and ticklabels to disappear when colorbar was on in IPython inline backend (:issue:`10611`, :issue:`10678`, and :issue:`20455`) +- Bug in plotting a Series with datetimes using :func:`matplotlib.axes.Axes.scatter` (:issue:`22039`) Groupby/Resample/Rolling ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pandas/plotting/_converter.py b/pandas/plotting/_converter.py index 3bb0b988512348..96ea8a542a4519 100644 --- a/pandas/plotting/_converter.py +++ b/pandas/plotting/_converter.py @@ -320,7 +320,11 @@ def try_parse(values): return values elif isinstance(values, compat.string_types): return try_parse(values) - elif isinstance(values, (list, tuple, np.ndarray, Index)): + elif isinstance(values, (list, tuple, np.ndarray, Index, ABCSeries)): + if isinstance(values, ABCSeries): + # https://github.com/matplotlib/matplotlib/issues/11391 + # Series was skipped. Convert to DatetimeIndex to get asi8 + values = Index(values) if isinstance(values, Index): values = values.values if not isinstance(values, np.ndarray): diff --git a/pandas/tests/plotting/test_datetimelike.py b/pandas/tests/plotting/test_datetimelike.py index e3d502cd373e49..0abe82d138e5e5 100644 --- a/pandas/tests/plotting/test_datetimelike.py +++ b/pandas/tests/plotting/test_datetimelike.py @@ -1483,6 +1483,17 @@ def test_add_matplotlib_datetime64(self): l1, l2 = ax.lines tm.assert_numpy_array_equal(l1.get_xydata(), l2.get_xydata()) + def test_matplotlib_scatter_datetime64(self): + # https://github.com/matplotlib/matplotlib/issues/11391 + df = DataFrame(np.random.RandomState(0).rand(10, 2), + columns=["x", "y"]) + df["time"] = date_range("2018-01-01", periods=10, freq="D") + fig, ax = self.plt.subplots() + ax.scatter(x="time", y="y", data=df) + fig.canvas.draw() + label = ax.get_xticklabels()[0] + assert label.get_text() == '2017-12-12' + def _check_plot_works(f, freq=None, series=None, *args, **kwargs): import matplotlib.pyplot as plt From cf1462767e61466746c9bba5e71ebd0f1e87d5e3 Mon Sep 17 00:00:00 2001 From: Marc Garcia Date: Thu, 2 Aug 2018 11:45:03 +0100 Subject: [PATCH 36/47] DEPR: Removing previously deprecated datetools module (#6581) (#19119) --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/api.py | 11 ------- pandas/core/datetools.py | 55 --------------------------------- pandas/tests/api/test_api.py | 15 +-------- 4 files changed, 2 insertions(+), 81 deletions(-) delete mode 100644 pandas/core/datetools.py diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index ceb03ce06a0eca..3cff1522274ef4 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -486,7 +486,7 @@ Removal of prior version deprecations/changes - The ``LongPanel`` and ``WidePanel`` classes have been removed (:issue:`10892`) - Several private functions were removed from the (non-public) module ``pandas.core.common`` (:issue:`22001`) -- +- Removal of the previously deprecated module ``pandas.core.datetools`` (:issue:`14105`, :issue:`14094`) - .. _whatsnew_0240.performance: diff --git a/pandas/core/api.py b/pandas/core/api.py index 92586235df93c2..32df317a602a9c 100644 --- a/pandas/core/api.py +++ b/pandas/core/api.py @@ -32,17 +32,6 @@ from pandas.core.tools.datetimes import to_datetime from pandas.core.tools.timedeltas import to_timedelta -# see gh-14094. -from pandas.util._depr_module import _DeprecatedModule - -_removals = ['day', 'bday', 'businessDay', 'cday', 'customBusinessDay', - 'customBusinessMonthEnd', 'customBusinessMonthBegin', - 'monthEnd', 'yearEnd', 'yearBegin', 'bmonthEnd', 'bmonthBegin', - 'cbmonthEnd', 'cbmonthBegin', 'bquarterEnd', 'quarterEnd', - 'byearEnd', 'week'] -datetools = _DeprecatedModule(deprmod='pandas.core.datetools', - removals=_removals) - from pandas.core.config import (get_option, set_option, reset_option, describe_option, option_context, options) diff --git a/pandas/core/datetools.py b/pandas/core/datetools.py deleted file mode 100644 index 83167a45369c43..00000000000000 --- a/pandas/core/datetools.py +++ /dev/null @@ -1,55 +0,0 @@ -"""A collection of random tools for dealing with dates in Python. - -.. deprecated:: 0.19.0 - Use pandas.tseries module instead. -""" - -# flake8: noqa - -import warnings - -from pandas.core.tools.datetimes import * -from pandas.tseries.offsets import * -from pandas.tseries.frequencies import * - -warnings.warn("The pandas.core.datetools module is deprecated and will be " - "removed in a future version. Please use the pandas.tseries " - "module instead.", FutureWarning, stacklevel=2) - -day = DateOffset() -bday = BDay() -businessDay = bday -try: - cday = CDay() - customBusinessDay = CustomBusinessDay() - customBusinessMonthEnd = CBMonthEnd() - customBusinessMonthBegin = CBMonthBegin() -except NotImplementedError: - cday = None - customBusinessDay = None - customBusinessMonthEnd = None - customBusinessMonthBegin = None -monthEnd = MonthEnd() -yearEnd = YearEnd() -yearBegin = YearBegin() -bmonthEnd = BMonthEnd() -bmonthBegin = BMonthBegin() -cbmonthEnd = customBusinessMonthEnd -cbmonthBegin = customBusinessMonthBegin -bquarterEnd = BQuarterEnd() -quarterEnd = QuarterEnd() -byearEnd = BYearEnd() -week = Week() - -# Functions/offsets to roll dates forward -thisMonthEnd = MonthEnd(0) -thisBMonthEnd = BMonthEnd(0) -thisYearEnd = YearEnd(0) -thisYearBegin = YearBegin(0) -thisBQuarterEnd = BQuarterEnd(0) -thisQuarterEnd = QuarterEnd(0) - -# Functions to check where a date lies -isBusinessDay = BDay().onOffset -isMonthEnd = MonthEnd().onOffset -isBMonthEnd = BMonthEnd().onOffset diff --git a/pandas/tests/api/test_api.py b/pandas/tests/api/test_api.py index 2aa875d1e095a6..bf9e14b4270158 100644 --- a/pandas/tests/api/test_api.py +++ b/pandas/tests/api/test_api.py @@ -35,7 +35,7 @@ class TestPDApi(Base): 'util', 'options', 'io'] # these are already deprecated; awaiting removal - deprecated_modules = ['datetools', 'parser', 'json', 'lib', 'tslib'] + deprecated_modules = ['parser', 'json', 'lib', 'tslib'] # misc misc = ['IndexSlice', 'NaT'] @@ -127,19 +127,6 @@ def test_testing(self): self.check(testing, self.funcs) -class TestDatetoolsDeprecation(object): - - def test_deprecation_access_func(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - pd.datetools.to_datetime('2016-01-01') - - def test_deprecation_access_obj(self): - with tm.assert_produces_warning(FutureWarning, - check_stacklevel=False): - pd.datetools.monthEnd - - class TestTopLevelDeprecations(object): # top-level API deprecations From 83f6be50073a48c447d02ac21593c31217055272 Mon Sep 17 00:00:00 2001 From: dahlbaek <30782351+dahlbaek@users.noreply.github.com> Date: Thu, 2 Aug 2018 12:49:06 +0200 Subject: [PATCH 37/47] DEPR: pd.read_table (#21954) * DEPR: pd.read_table - pd.read_table is deprecated and replaced by pd.read_csv. - add whatsnew note - change tests to test for warning messages - change DataFrame.from_csv to use pandas.read_csv instead of pandas.read_table - Change pandas.read_clipboard to use pandas.read_csv instead of pandas.read_table * Add sep note to whatsnew --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/frame.py | 10 +++---- pandas/io/clipboards.py | 8 +++--- pandas/io/parsers.py | 31 ++++++++++++++++++---- pandas/tests/io/conftest.py | 4 +-- pandas/tests/io/formats/test_format.py | 6 ++--- pandas/tests/io/parser/test_network.py | 4 +-- pandas/tests/io/parser/test_parsers.py | 12 ++++++--- pandas/tests/io/parser/test_unsupported.py | 24 ++++++++--------- pandas/tests/io/test_common.py | 24 +++++++++++++++-- pandas/tests/test_multilevel.py | 8 +++--- 11 files changed, 89 insertions(+), 43 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 3cff1522274ef4..8a92db4c66fb59 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -478,6 +478,7 @@ Deprecations - :meth:`Series.ptp` is deprecated. Use ``numpy.ptp`` instead (:issue:`21614`) - :meth:`Series.compress` is deprecated. Use ``Series[condition]`` instead (:issue:`18262`) - :meth:`Categorical.from_codes` has deprecated providing float values for the ``codes`` argument. (:issue:`21767`) +- :func:`pandas.read_table` is deprecated. Instead, use :func:`pandas.read_csv` passing ``sep='\t'`` if necessary (:issue:`21948`) .. _whatsnew_0240.prior_deprecations: diff --git a/pandas/core/frame.py b/pandas/core/frame.py index ebd35cb1a6a1ae..bbe84110fd0190 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -1594,11 +1594,11 @@ def from_csv(cls, path, header=0, sep=',', index_col=0, parse_dates=True, "for from_csv when changing your function calls", FutureWarning, stacklevel=2) - from pandas.io.parsers import read_table - return read_table(path, header=header, sep=sep, - parse_dates=parse_dates, index_col=index_col, - encoding=encoding, tupleize_cols=tupleize_cols, - infer_datetime_format=infer_datetime_format) + from pandas.io.parsers import read_csv + return read_csv(path, header=header, sep=sep, + parse_dates=parse_dates, index_col=index_col, + encoding=encoding, tupleize_cols=tupleize_cols, + infer_datetime_format=infer_datetime_format) def to_sparse(self, fill_value=None, kind='block'): """ diff --git a/pandas/io/clipboards.py b/pandas/io/clipboards.py index 141a5d2389db50..0d564069c681fa 100644 --- a/pandas/io/clipboards.py +++ b/pandas/io/clipboards.py @@ -9,7 +9,7 @@ def read_clipboard(sep=r'\s+', **kwargs): # pragma: no cover r""" - Read text from clipboard and pass to read_table. See read_table for the + Read text from clipboard and pass to read_csv. See read_csv for the full argument list Parameters @@ -31,7 +31,7 @@ def read_clipboard(sep=r'\s+', **kwargs): # pragma: no cover 'reading from clipboard only supports utf-8 encoding') from pandas.io.clipboard import clipboard_get - from pandas.io.parsers import read_table + from pandas.io.parsers import read_csv text = clipboard_get() # try to decode (if needed on PY3) @@ -51,7 +51,7 @@ def read_clipboard(sep=r'\s+', **kwargs): # pragma: no cover # that this came from excel and set 'sep' accordingly lines = text[:10000].split('\n')[:-1][:10] - # Need to remove leading white space, since read_table + # Need to remove leading white space, since read_csv # accepts: # a b # 0 1 2 @@ -80,7 +80,7 @@ def read_clipboard(sep=r'\s+', **kwargs): # pragma: no cover if kwargs.get('engine') == 'python' and PY2: text = text.encode('utf-8') - return read_table(StringIO(text), sep=sep, **kwargs) + return read_csv(StringIO(text), sep=sep, **kwargs) def to_clipboard(obj, excel=True, sep=None, **kwargs): # pragma: no cover diff --git a/pandas/io/parsers.py b/pandas/io/parsers.py index 88358ff392cb65..4b3fa08e5e4af8 100755 --- a/pandas/io/parsers.py +++ b/pandas/io/parsers.py @@ -331,6 +331,10 @@ """ % (_parser_params % (_sep_doc.format(default="','"), _engine_doc)) _read_table_doc = """ + +.. deprecated:: 0.24.0 + Use :func:`pandas.read_csv` instead, passing ``sep='\t'`` if necessary. + Read general delimited file into DataFrame %s @@ -540,9 +544,13 @@ def _read(filepath_or_buffer, kwds): } -def _make_parser_function(name, sep=','): +def _make_parser_function(name, default_sep=','): - default_sep = sep + # prepare read_table deprecation + if name == "read_table": + sep = False + else: + sep = default_sep def parser_f(filepath_or_buffer, sep=sep, @@ -611,11 +619,24 @@ def parser_f(filepath_or_buffer, memory_map=False, float_precision=None): + # deprecate read_table GH21948 + if name == "read_table": + if sep is False and delimiter is None: + warnings.warn("read_table is deprecated, use read_csv " + "instead, passing sep='\\t'.", + FutureWarning, stacklevel=2) + else: + warnings.warn("read_table is deprecated, use read_csv " + "instead.", + FutureWarning, stacklevel=2) + if sep is False: + sep = default_sep + # Alias sep -> delimiter. if delimiter is None: delimiter = sep - if delim_whitespace and delimiter is not default_sep: + if delim_whitespace and delimiter != default_sep: raise ValueError("Specified a delimiter with both sep and" " delim_whitespace=True; you can only" " specify one.") @@ -687,10 +708,10 @@ def parser_f(filepath_or_buffer, return parser_f -read_csv = _make_parser_function('read_csv', sep=',') +read_csv = _make_parser_function('read_csv', default_sep=',') read_csv = Appender(_read_csv_doc)(read_csv) -read_table = _make_parser_function('read_table', sep='\t') +read_table = _make_parser_function('read_table', default_sep='\t') read_table = Appender(_read_table_doc)(read_table) diff --git a/pandas/tests/io/conftest.py b/pandas/tests/io/conftest.py index 7623587803b418..b0cdbe2b5bedbc 100644 --- a/pandas/tests/io/conftest.py +++ b/pandas/tests/io/conftest.py @@ -1,5 +1,5 @@ import pytest -from pandas.io.parsers import read_table +from pandas.io.parsers import read_csv @pytest.fixture @@ -17,7 +17,7 @@ def jsonl_file(datapath): @pytest.fixture def salaries_table(datapath): """DataFrame with the salaries dataset""" - return read_table(datapath('io', 'parser', 'data', 'salaries.csv')) + return read_csv(datapath('io', 'parser', 'data', 'salaries.csv'), sep='\t') @pytest.fixture diff --git a/pandas/tests/io/formats/test_format.py b/pandas/tests/io/formats/test_format.py index 191e3f37f1c37a..3218742aa76361 100644 --- a/pandas/tests/io/formats/test_format.py +++ b/pandas/tests/io/formats/test_format.py @@ -21,7 +21,7 @@ import numpy as np import pandas as pd from pandas import (DataFrame, Series, Index, Timestamp, MultiIndex, - date_range, NaT, read_table) + date_range, NaT, read_csv) from pandas.compat import (range, zip, lrange, StringIO, PY3, u, lzip, is_platform_windows, is_platform_32bit) @@ -1225,8 +1225,8 @@ def test_to_string(self): lines = result.split('\n') header = lines[0].strip().split() joined = '\n'.join(re.sub(r'\s+', ' ', x).strip() for x in lines[1:]) - recons = read_table(StringIO(joined), names=header, - header=None, sep=' ') + recons = read_csv(StringIO(joined), names=header, + header=None, sep=' ') tm.assert_series_equal(recons['B'], biggie['B']) assert recons['A'].count() == biggie['A'].count() assert (np.abs(recons['A'].dropna() - diff --git a/pandas/tests/io/parser/test_network.py b/pandas/tests/io/parser/test_network.py index f6a31008bca5c3..a7cc3ad989ea16 100644 --- a/pandas/tests/io/parser/test_network.py +++ b/pandas/tests/io/parser/test_network.py @@ -12,7 +12,7 @@ import pandas.util.testing as tm import pandas.util._test_decorators as td from pandas import DataFrame -from pandas.io.parsers import read_csv, read_table +from pandas.io.parsers import read_csv from pandas.compat import BytesIO, StringIO @@ -44,7 +44,7 @@ def check_compressed_urls(salaries_table, compression, extension, mode, if mode != 'explicit': compression = mode - url_table = read_table(url, compression=compression, engine=engine) + url_table = read_csv(url, sep='\t', compression=compression, engine=engine) tm.assert_frame_equal(url_table, salaries_table) diff --git a/pandas/tests/io/parser/test_parsers.py b/pandas/tests/io/parser/test_parsers.py index b6f13039641a27..8535a51657abf5 100644 --- a/pandas/tests/io/parser/test_parsers.py +++ b/pandas/tests/io/parser/test_parsers.py @@ -70,7 +70,9 @@ def read_table(self, *args, **kwds): kwds = kwds.copy() kwds['engine'] = self.engine kwds['low_memory'] = self.low_memory - return read_table(*args, **kwds) + with tm.assert_produces_warning(FutureWarning): + df = read_table(*args, **kwds) + return df class TestCParserLowMemory(BaseParser, CParserTests): @@ -88,7 +90,9 @@ def read_table(self, *args, **kwds): kwds = kwds.copy() kwds['engine'] = self.engine kwds['low_memory'] = True - return read_table(*args, **kwds) + with tm.assert_produces_warning(FutureWarning): + df = read_table(*args, **kwds) + return df class TestPythonParser(BaseParser, PythonParserTests): @@ -103,7 +107,9 @@ def read_csv(self, *args, **kwds): def read_table(self, *args, **kwds): kwds = kwds.copy() kwds['engine'] = self.engine - return read_table(*args, **kwds) + with tm.assert_produces_warning(FutureWarning): + df = read_table(*args, **kwds) + return df class TestUnsortedUsecols(object): diff --git a/pandas/tests/io/parser/test_unsupported.py b/pandas/tests/io/parser/test_unsupported.py index 3117f6fae55da0..1c64c1516077d9 100644 --- a/pandas/tests/io/parser/test_unsupported.py +++ b/pandas/tests/io/parser/test_unsupported.py @@ -14,7 +14,7 @@ from pandas.compat import StringIO from pandas.errors import ParserError -from pandas.io.parsers import read_csv, read_table +from pandas.io.parsers import read_csv import pytest @@ -43,24 +43,24 @@ def test_c_engine(self): # specify C engine with unsupported options (raise) with tm.assert_raises_regex(ValueError, msg): - read_table(StringIO(data), engine='c', - sep=None, delim_whitespace=False) + read_csv(StringIO(data), engine='c', + sep=None, delim_whitespace=False) with tm.assert_raises_regex(ValueError, msg): - read_table(StringIO(data), engine='c', sep=r'\s') + read_csv(StringIO(data), engine='c', sep=r'\s') with tm.assert_raises_regex(ValueError, msg): - read_table(StringIO(data), engine='c', quotechar=chr(128)) + read_csv(StringIO(data), engine='c', sep='\t', quotechar=chr(128)) with tm.assert_raises_regex(ValueError, msg): - read_table(StringIO(data), engine='c', skipfooter=1) + read_csv(StringIO(data), engine='c', skipfooter=1) # specify C-unsupported options without python-unsupported options with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), sep=None, delim_whitespace=False) + read_csv(StringIO(data), sep=None, delim_whitespace=False) with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), quotechar=chr(128)) + read_csv(StringIO(data), sep=r'\s') with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), sep=r'\s') + read_csv(StringIO(data), sep='\t', quotechar=chr(128)) with tm.assert_produces_warning(parsers.ParserWarning): - read_table(StringIO(data), skipfooter=1) + read_csv(StringIO(data), skipfooter=1) text = """ A B C D E one two three four @@ -70,9 +70,9 @@ def test_c_engine(self): msg = 'Error tokenizing data' with tm.assert_raises_regex(ParserError, msg): - read_table(StringIO(text), sep='\\s+') + read_csv(StringIO(text), sep='\\s+') with tm.assert_raises_regex(ParserError, msg): - read_table(StringIO(text), engine='c', sep='\\s+') + read_csv(StringIO(text), engine='c', sep='\\s+') msg = "Only length-1 thousands markers supported" data = """A|B|C diff --git a/pandas/tests/io/test_common.py b/pandas/tests/io/test_common.py index ceaac9818354a9..991b8ee5087609 100644 --- a/pandas/tests/io/test_common.py +++ b/pandas/tests/io/test_common.py @@ -131,7 +131,6 @@ def test_iterator(self): @pytest.mark.parametrize('reader, module, error_class, fn_ext', [ (pd.read_csv, 'os', FileNotFoundError, 'csv'), - (pd.read_table, 'os', FileNotFoundError, 'csv'), (pd.read_fwf, 'os', FileNotFoundError, 'txt'), (pd.read_excel, 'xlrd', FileNotFoundError, 'xlsx'), (pd.read_feather, 'feather', Exception, 'feather'), @@ -149,9 +148,14 @@ def test_read_non_existant(self, reader, module, error_class, fn_ext): with pytest.raises(error_class): reader(path) + def test_read_non_existant_read_table(self): + path = os.path.join(HERE, 'data', 'does_not_exist.' + 'csv') + with pytest.raises(FileNotFoundError): + with tm.assert_produces_warning(FutureWarning): + pd.read_table(path) + @pytest.mark.parametrize('reader, module, path', [ (pd.read_csv, 'os', ('io', 'data', 'iris.csv')), - (pd.read_table, 'os', ('io', 'data', 'iris.csv')), (pd.read_fwf, 'os', ('io', 'data', 'fixed_width_format.txt')), (pd.read_excel, 'xlrd', ('io', 'data', 'test1.xlsx')), (pd.read_feather, 'feather', ('io', 'data', 'feather-0_3_1.feather')), @@ -170,6 +174,22 @@ def test_read_fspath_all(self, reader, module, path, datapath): mypath = CustomFSPath(path) result = reader(mypath) expected = reader(path) + + if path.endswith('.pickle'): + # categorical + tm.assert_categorical_equal(result, expected) + else: + tm.assert_frame_equal(result, expected) + + def test_read_fspath_all_read_table(self, datapath): + path = datapath('io', 'data', 'iris.csv') + + mypath = CustomFSPath(path) + with tm.assert_produces_warning(FutureWarning): + result = pd.read_table(mypath) + with tm.assert_produces_warning(FutureWarning): + expected = pd.read_table(path) + if path.endswith('.pickle'): # categorical tm.assert_categorical_equal(result, expected) diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index 3caee2b44c5798..dcfeab55f94fc6 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -10,7 +10,7 @@ import numpy as np from pandas.core.index import Index, MultiIndex -from pandas import Panel, DataFrame, Series, notna, isna, Timestamp +from pandas import Panel, DataFrame, Series, notna, isna, Timestamp, read_csv from pandas.core.dtypes.common import is_float_dtype, is_integer_dtype import pandas.core.common as com @@ -512,14 +512,13 @@ def f(x): pytest.raises(com.SettingWithCopyError, f, result) def test_xs_level_multiple(self): - from pandas import read_table text = """ A B C D E one two three four a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" - df = read_table(StringIO(text), sep=r'\s+', engine='python') + df = read_csv(StringIO(text), sep=r'\s+', engine='python') result = df.xs(('a', 4), level=['one', 'four']) expected = df.xs('a').xs(4, level='four') @@ -547,14 +546,13 @@ def f(x): tm.assert_frame_equal(rs, xp) def test_xs_level0(self): - from pandas import read_table text = """ A B C D E one two three four a b 10.0032 5 -0.5109 -2.3358 -0.4645 0.05076 0.3640 a q 20 4 0.4473 1.4152 0.2834 1.00661 0.1744 x q 30 3 -0.6662 -0.5243 -0.3580 0.89145 2.5838""" - df = read_table(StringIO(text), sep=r'\s+', engine='python') + df = read_csv(StringIO(text), sep=r'\s+', engine='python') result = df.xs('a', level=0) expected = df.xs('a') From 615615a5fd733d7b915947386f3f4df77293e786 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 2 Aug 2018 05:57:34 -0500 Subject: [PATCH 38/47] Fixed py36-only syntax [ci skip] (#22167) Closes https://github.com/pandas-dev/pandas/issues/22162 [ci skip] --- asv_bench/benchmarks/reshape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/asv_bench/benchmarks/reshape.py b/asv_bench/benchmarks/reshape.py index 07634811370c71..3cf9a32dab3984 100644 --- a/asv_bench/benchmarks/reshape.py +++ b/asv_bench/benchmarks/reshape.py @@ -141,7 +141,7 @@ class GetDummies(object): def setup(self): categories = list(string.ascii_letters[:12]) - s = pd.Series(np.random.choice(categories, size=1_000_000), + s = pd.Series(np.random.choice(categories, size=1000000), dtype=pd.api.types.CategoricalDtype(categories)) self.s = s From 5076ebe5490635e6bc46bc47b74a787a30e5112e Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 2 Aug 2018 19:21:36 +0200 Subject: [PATCH 39/47] BUG: Fix get dummies unicode error (#22131) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/reshape/reshape.py | 24 +++++++++++++++++------- pandas/tests/reshape/test_reshape.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 8a92db4c66fb59..b7692f51c4ddff 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -660,6 +660,7 @@ Reshaping - Bug in :meth:`Series.mask` and :meth:`DataFrame.mask` with ``list`` conditionals (:issue:`21891`) - Bug in :meth:`DataFrame.replace` raises RecursionError when converting OutOfBounds ``datetime64[ns, tz]`` (:issue:`20380`) - :func:`pandas.core.groupby.GroupBy.rank` now raises a ``ValueError`` when an invalid value is passed for argument ``na_option`` (:issue:`22124`) +- Bug in :func:`get_dummies` with Unicode attributes in Python 2 (:issue:`22084`) - Build Changes diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py index f9ab813855f470..bd5ce4897e9da3 100644 --- a/pandas/core/reshape/reshape.py +++ b/pandas/core/reshape/reshape.py @@ -1,6 +1,6 @@ # pylint: disable=E1101,E1103 # pylint: disable=W0703,W0622,W0613,W0201 -from pandas.compat import range, text_type, zip +from pandas.compat import range, text_type, zip, u, PY2 from pandas import compat from functools import partial import itertools @@ -923,13 +923,23 @@ def get_empty_Frame(data, sparse): number_of_cols = len(levels) - if prefix is not None: - dummy_strs = [u'{prefix}{sep}{level}' if isinstance(v, text_type) - else '{prefix}{sep}{level}' for v in levels] - dummy_cols = [dummy_str.format(prefix=prefix, sep=prefix_sep, level=v) - for dummy_str, v in zip(dummy_strs, levels)] - else: + if prefix is None: dummy_cols = levels + else: + + # PY2 embedded unicode, gh-22084 + def _make_col_name(prefix, prefix_sep, level): + fstr = '{prefix}{prefix_sep}{level}' + if PY2 and (isinstance(prefix, text_type) or + isinstance(prefix_sep, text_type) or + isinstance(level, text_type)): + fstr = u(fstr) + return fstr.format(prefix=prefix, + prefix_sep=prefix_sep, + level=level) + + dummy_cols = [_make_col_name(prefix, prefix_sep, level) + for level in levels] if isinstance(data, Series): index = data.index diff --git a/pandas/tests/reshape/test_reshape.py b/pandas/tests/reshape/test_reshape.py index 295801f3e8def0..3f4ccd7693a8f6 100644 --- a/pandas/tests/reshape/test_reshape.py +++ b/pandas/tests/reshape/test_reshape.py @@ -302,6 +302,24 @@ def test_dataframe_dummies_with_categorical(self, df, sparse, dtype): expected.sort_index(axis=1) assert_frame_equal(result, expected) + @pytest.mark.parametrize('get_dummies_kwargs,expected', [ + ({'data': pd.DataFrame(({u'ä': ['a']}))}, + pd.DataFrame({u'ä_a': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'ä']})}, + pd.DataFrame({u'x_ä': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'a']}), 'prefix':u'ä'}, + pd.DataFrame({u'ä_a': [1]}, dtype=np.uint8)), + + ({'data': pd.DataFrame({'x': [u'a']}), 'prefix_sep':u'ä'}, + pd.DataFrame({u'xäa': [1]}, dtype=np.uint8))]) + def test_dataframe_dummies_unicode(self, get_dummies_kwargs, expected): + # GH22084 pd.get_dummies incorrectly encodes unicode characters + # in dataframe column names + result = get_dummies(**get_dummies_kwargs) + assert_frame_equal(result, expected) + def test_basic_drop_first(self, sparse): # GH12402 Add a new parameter `drop_first` to avoid collinearity # Basic case From 6e1e1e40f7e474886f46b88edfd4c5bc22a35b14 Mon Sep 17 00:00:00 2001 From: Dylan Dmitri Gray Date: Thu, 2 Aug 2018 10:27:22 -0700 Subject: [PATCH 40/47] fix: scalar timestamp assignment (#19843) (#19973) --- doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/frame.py | 9 +++++++-- pandas/tests/frame/test_indexing.py | 8 ++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index b7692f51c4ddff..5c15c7b6a742f9 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -560,6 +560,7 @@ Timezones - Bug in :class:`DatetimeIndex` where constructing with an integer and tz would not localize correctly (:issue:`12619`) - Fixed bug where :meth:`DataFrame.describe` and :meth:`Series.describe` on tz-aware datetimes did not show `first` and `last` result (:issue:`21328`) - Bug in :class:`DatetimeIndex` comparisons failing to raise ``TypeError`` when comparing timezone-aware ``DatetimeIndex`` against ``np.datetime64`` (:issue:`22074`) +- Bug in ``DataFrame`` assignment with a timezone-aware scalar (:issue:`19843`) Offsets ^^^^^^^ diff --git a/pandas/core/frame.py b/pandas/core/frame.py index bbe84110fd0190..cb251d46489255 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -27,6 +27,7 @@ maybe_upcast, cast_scalar_to_array, construct_1d_arraylike_from_scalar, + infer_dtype_from_scalar, maybe_cast_to_datetime, maybe_infer_to_datetimelike, maybe_convert_platform, @@ -3507,9 +3508,13 @@ def reindexer(value): value = maybe_infer_to_datetimelike(value) else: - # upcast the scalar + # cast ignores pandas dtypes. so save the dtype first + infer_dtype, _ = infer_dtype_from_scalar( + value, pandas_dtype=True) + + # upcast value = cast_scalar_to_array(len(self.index), value) - value = maybe_cast_to_datetime(value, value.dtype) + value = maybe_cast_to_datetime(value, infer_dtype) # return internal types directly if is_extension_type(value) or is_extension_array_dtype(value): diff --git a/pandas/tests/frame/test_indexing.py b/pandas/tests/frame/test_indexing.py index 5f229aca5c25b8..d885df76967b84 100644 --- a/pandas/tests/frame/test_indexing.py +++ b/pandas/tests/frame/test_indexing.py @@ -3135,6 +3135,14 @@ def test_transpose(self): expected.index = ['A', 'B'] assert_frame_equal(result, expected) + def test_scalar_assignment(self): + # issue #19843 + df = pd.DataFrame(index=(0, 1, 2)) + df['now'] = pd.Timestamp('20130101', tz='UTC') + expected = pd.DataFrame( + {'now': pd.Timestamp('20130101', tz='UTC')}, index=[0, 1, 2]) + tm.assert_frame_equal(df, expected) + class TestDataFrameIndexingUInt64(TestData): From e4381b6e7c3cf1c6f424d01e3dc2613710d79b0d Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Thu, 2 Aug 2018 15:26:40 -0500 Subject: [PATCH 41/47] 0.23.4 whatsnew (#22177) --- doc/source/whatsnew/v0.23.4.txt | 36 ++------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/doc/source/whatsnew/v0.23.4.txt b/doc/source/whatsnew/v0.23.4.txt index c17f4ffdd6b8e3..9a3ad3f61ee49b 100644 --- a/doc/source/whatsnew/v0.23.4.txt +++ b/doc/source/whatsnew/v0.23.4.txt @@ -1,7 +1,7 @@ .. _whatsnew_0234: -v0.23.4 -------- +v0.23.4 (August 3, 2018) +------------------------ This is a minor bug-fix release in the 0.23.x series and includes some small regression fixes and bug fixes. We recommend that all users upgrade to this version. @@ -21,7 +21,6 @@ Fixed Regressions ~~~~~~~~~~~~~~~~~ - Python 3.7 with Windows gave all missing values for rolling variance calculations (:issue:`21813`) -- .. _whatsnew_0234.bug_fixes: @@ -32,37 +31,6 @@ Bug Fixes - Bug where calling :func:`DataFrameGroupBy.agg` with a list of functions including ``ohlc`` as the non-initial element would raise a ``ValueError`` (:issue:`21716`) - Bug in ``roll_quantile`` caused a memory leak when calling ``.rolling(...).quantile(q)`` with ``q`` in (0,1) (:issue:`21965`) -- - -**Conversion** - -- -- - -**Indexing** - -- -- - -**I/O** - -- -- - -**Categorical** - -- -- - -**Timezones** - -- -- - -**Timedelta** - -- -- **Missing** From 103c5eeb67cab192d9f0b2f1975831b3d0ef4ddb Mon Sep 17 00:00:00 2001 From: nicolab100 <40072822+nicolab100@users.noreply.github.com> Date: Thu, 2 Aug 2018 21:33:02 +0100 Subject: [PATCH 42/47] DOC: updated Series.str.contains see also section (#22176) --- pandas/core/strings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 6349af4d2e0ac5..8317473a99f670 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -293,6 +293,8 @@ def str_contains(arr, pat, case=True, flags=0, na=np.nan, regex=True): See Also -------- match : analogous, but stricter, relying on re.match instead of re.search + Series.str.startswith : Test if the start of each string element matches a pattern. + Series.str.endswith : Same as startswith, but tests the end of string. Examples -------- From 7725fa028aad7a8817c10b7fe67a34cf7fe8ee5c Mon Sep 17 00:00:00 2001 From: ym-pett Date: Thu, 2 Aug 2018 21:35:07 +0100 Subject: [PATCH 43/47] DOC: added .join to 'see also' in Series.str.cat (#22175) --- pandas/core/strings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index 8317473a99f670..b5b44a361a98d6 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -2065,6 +2065,7 @@ def cat(self, others=None, sep=None, na_rep=None, join=None): See Also -------- split : Split each string in the Series/Index + join : Join lists contained as elements in the Series/Index Examples -------- From 6753068b92e3cab8a6e6e47565c55fa41b69e545 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Thu, 2 Aug 2018 18:26:46 -0500 Subject: [PATCH 44/47] Documentation: typo fixes in MultiIndex / Advanced Indexing (#22179) --- doc/source/advanced.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/source/advanced.rst b/doc/source/advanced.rst index e530ece2e12c58..2be1a53aa6c93a 100644 --- a/doc/source/advanced.rst +++ b/doc/source/advanced.rst @@ -21,7 +21,7 @@ See the :ref:`Indexing and Selecting Data ` for general indexing docum .. warning:: - Whether a copy or a reference is returned for a setting operation, may + Whether a copy or a reference is returned for a setting operation may depend on the context. This is sometimes called ``chained assignment`` and should be avoided. See :ref:`Returning a View versus Copy `. @@ -172,7 +172,7 @@ Defined Levels ~~~~~~~~~~~~~~ The repr of a ``MultiIndex`` shows all the defined levels of an index, even -if the they are not actually used. When slicing an index, you may notice this. +if they are not actually used. When slicing an index, you may notice this. For example: .. ipython:: python @@ -379,7 +379,7 @@ slicers on a single axis. dfmi.loc(axis=0)[:, :, ['C1', 'C3']] -Furthermore you can *set* the values using the following methods. +Furthermore, you can *set* the values using the following methods. .. ipython:: python @@ -559,7 +559,7 @@ return a copy of the data rather than a view: .. _advanced.unsorted: -Furthermore if you try to index something that is not fully lexsorted, this can raise: +Furthermore, if you try to index something that is not fully lexsorted, this can raise: .. code-block:: ipython @@ -659,7 +659,7 @@ Index Types We have discussed ``MultiIndex`` in the previous sections pretty extensively. ``DatetimeIndex`` and ``PeriodIndex`` are shown :ref:`here `, and information about -`TimedeltaIndex`` is found :ref:`here `. +``TimedeltaIndex`` is found :ref:`here `. In the following sub-sections we will highlight some other index types. @@ -835,8 +835,8 @@ In non-float indexes, slicing using floats will raise a ``TypeError``. Here is a typical use-case for using this type of indexing. Imagine that you have a somewhat -irregular timedelta-like indexing scheme, but the data is recorded as floats. This could for -example be millisecond offsets. +irregular timedelta-like indexing scheme, but the data is recorded as floats. This could, for +example, be millisecond offsets. .. ipython:: python From 35dd15ba150175e8522e2d443ee4118b0d5c57ce Mon Sep 17 00:00:00 2001 From: Jeremy Schendel Date: Thu, 2 Aug 2018 19:58:32 -0600 Subject: [PATCH 45/47] CI: Fix Travis failures due to lint.sh on pandas/core/strings.py (#22184) --- pandas/core/strings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/strings.py b/pandas/core/strings.py index b5b44a361a98d6..39ecf7f49bc2ef 100644 --- a/pandas/core/strings.py +++ b/pandas/core/strings.py @@ -293,7 +293,8 @@ def str_contains(arr, pat, case=True, flags=0, na=np.nan, regex=True): See Also -------- match : analogous, but stricter, relying on re.match instead of re.search - Series.str.startswith : Test if the start of each string element matches a pattern. + Series.str.startswith : Test if the start of each string element matches a + pattern. Series.str.endswith : Same as startswith, but tests the end of string. Examples From 75ad42ef89e028550456005cc91ba59f841b522b Mon Sep 17 00:00:00 2001 From: gfyoung Date: Fri, 3 Aug 2018 08:09:49 -0700 Subject: [PATCH 46/47] TST: Check DatetimeIndex.drop on DST boundary (#22165) Closes gh-18031. --- .../tests/indexes/datetimes/test_timezones.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pandas/tests/indexes/datetimes/test_timezones.py b/pandas/tests/indexes/datetimes/test_timezones.py index 67eb81336f6480..b83645414c8fff 100644 --- a/pandas/tests/indexes/datetimes/test_timezones.py +++ b/pandas/tests/indexes/datetimes/test_timezones.py @@ -738,6 +738,28 @@ def test_dti_drop_dont_lose_tz(self): assert ind.tz is not None + def test_drop_dst_boundary(self): + # see gh-18031 + tz = "Europe/Brussels" + freq = "15min" + + start = pd.Timestamp("201710290100", tz=tz) + end = pd.Timestamp("201710290300", tz=tz) + index = pd.date_range(start=start, end=end, freq=freq) + + expected = DatetimeIndex(["201710290115", "201710290130", + "201710290145", "201710290200", + "201710290215", "201710290230", + "201710290245", "201710290200", + "201710290215", "201710290230", + "201710290245", "201710290300"], + tz=tz, freq=freq, + ambiguous=[True, True, True, True, + True, True, True, False, + False, False, False, False]) + result = index.drop(index[0]) + tm.assert_index_equal(result, expected) + def test_date_range_localize(self): rng = date_range('3/11/2012 03:00', periods=15, freq='H', tz='US/Eastern') From 776fed3ab63d74ddef6e5af1a702b10c2a30bbb6 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Fri, 3 Aug 2018 11:50:13 -0500 Subject: [PATCH 47/47] Run tests in conda build [ci skip] (#22190) * Run tests in conda build [ci skip] [ci skip] * update requirements --- conda.recipe/meta.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 2bc42c1bd2dec1..f92090fecccf35 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -29,8 +29,11 @@ requirements: - pytz test: - imports: - - pandas + requires: + - pytest + commands: + - python -c "import pandas; pandas.test()" + about: home: http://pandas.pydata.org