diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index ed03d58c..00000000 --- a/.gitattributes +++ /dev/null @@ -1,21 +0,0 @@ -# Auto detect text files and perform LF normalization -* text eol=lf - -# Custom for Visual Studio -*.cs diff=csharp - -# Standard to msysgit -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain - -*.png binary -*.jpg binary -*.pdf binary -*.exe binary diff --git a/Boot1/Boot1.py b/Boot1/Boot1.py new file mode 100644 index 00000000..86c98023 --- /dev/null +++ b/Boot1/Boot1.py @@ -0,0 +1,724 @@ +#pylint: disable=invalid-name, missing-docstring, line-too-long, bad-continuation +""" + Router SDK Boot1 Application. + + Copyright © 2016 Cradlepoint, Inc. . All rights reserved. + + This file contains confidential information of Cradlepoint, Inc. and your + use of this file is subject to the Cradlepoint Software License Agreement + distributed with this file. Unauthorized reproduction or distribution of + this file is subject to civil and criminal penalties. + + Desc: + Determine "fastest" wireless wan connection by loading both SIMs, + running speedtest. Disable the not-fastest SIM. + +""" + +import sys +import json +import socket +import time +#import Email + +app_name = "Boot1" + +#_STDERR=False +_STDERR = True + +#tmplog = "/tmp/log.txt" +tmplog = None + + +def rawlog(fmt, *args): + if _STDERR: + msg = fmt % args + print(msg, file=sys.stderr) + + if tmplog: + with open(tmplog, "a") as outfile: + msg = fmt % args + print(time.ctime(), file=outfile) + print(msg, file=outfile) + +class Boot1Exception(Exception): + pass + +class Timeout(Boot1Exception): + pass + +class SocketLost(Boot1Exception): + pass + +class OneModem(Boot1Exception): + pass + +class ReadLine(object): + """State machine to read CR/LF style line. Input controlled from outside. """ + + # Keeping the read or recv or whatever outside the class allows me to handle + # weird conditions around sockets, serial ports, etc. + STATE_RECV_LINE = 1 + STATE_WAIT_LF = 2 + STATE_WAIT_SOL = 3 + + CR = b'\x0d' + LF = b'\x0a' + + def __init__(self, maxlen=256): + self.maxlen = maxlen + self.state = self.STATE_RECV_LINE + self.s = bytes() + self.len_s = 0 + + def recv(self, c): + return_s = None + + if self.state == self.STATE_RECV_LINE: + if c == self.CR: + # CR; could be a bare CR or a CRLF + return_s = self.s + # restart capture + self.s = bytes() + self.len_s = 0 + self.state = self.STATE_WAIT_LF + elif c == self.LF: + # bare LF (unusual) + return_s = self.s + + # restart capture + self.s = bytes() + self.len_s = 0 + else: + #rawlog("c={} s={}".format(c,self.s)) + self.s += c + self.len_s += 1 + + # protection from evil input; if we don't see a CRLF before + # maxlen, throw away our current input and start over + if self.len_s >= self.maxlen: + # throw away current input; start over + self.s = bytes() + self.len_s = 0 + + elif self.state == self.STATE_WAIT_LF: + if c == self.LF: + # saw CRLF; capture was restarted in the previous state + assert self.len_s == 0, self.len_s + else: + # raw CR! save what we've seen and start parsing again + # (note: this won't handle weird cases like CRCRCR) + self.s = c + self.len_s = 1 + + # start capturing line again + self.state = self.STATE_RECV_LINE + + else: + # WTF? + assert 0, self.state + + return return_s + +class CSClient(object): + """Wrapper for the TCP interface to the router config store.""" + + def __init__(self): + self.getline = ReadLine() + + def _read_line(self, sock): + while True: + try: + c = sock.recv(1) + except socket.timeout: + break + + if not c: + raise SocketLost("connection lost sock={}".format(sock)) + + buf = self.getline.recv(c) + if buf is not None: + s = buf.decode("utf-8") + rawlog("read_line line=%s"%s) + return s + +# logger.info("socket timeout {}".format(sock)) + return None + + def get(self, base, query='', tree=0): + """Send a get request.""" + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + + def delete(self, base, query=''): + """Send a delete request.""" + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + + def alert(self, value=''): + """Send a request to create an alert.""" + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + + def log(self, value=''): + """Send a request to create a log entry.""" + cmd = "log\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + + def paranoid_sendall(self, cmd, sock): + message_list = cmd.split("\n") + for msg in message_list: + rawlog("send >>>%s<<<",msg) + sock.sendall(bytes(msg+"\n", 'ascii')) + time.sleep(0.1) + + def safe_dispatch(self, cmd): + """Send the command and return the response.""" + resl = '' + rawlog("_dispatch %s", cmd) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + # davep 20160706 ; disable Nagle because we're talking localhost + # and other side is using sock.readline() + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + sock.connect(('localhost', 1337)) + sock.sendall(bytes(cmd, 'ascii')) +# self.paranoid_sendall(cmd, sock) + + line = self._read_line(sock) + resl = "" + if line and line.strip() == 'status: ok': + line = self._read_line(sock) + if line and line.startswith("content-length"): + mlen = int(line.strip().split(' ')[1]) + # eat \r\n\r\n + sock.recv(1) + sock.recv(1) + sock.recv(1) + sock.recv(1) + # read message, pray that sender has accurate content-length field + while mlen > 0: + c = sock.recv(1) + if not c: + raise SocketLost("connection lost sock={}".format(sock)) + resl += c.decode('utf8') + mlen -= 1 + + rawlog("_dispatch resl=\"%s\"", resl) + return resl + + def _dispatch(self, cmd): + errmsg = None + resl = "" + try: + resl = self.safe_dispatch(cmd) + except Boot1Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + + if errmsg is not None: + rawlog(errmsg) + + return resl + +default_speedtest = { + "input": { + "options": { + "limit": { + "size":0, + "time":15 + }, + "port":None, + "host":"", + "ifc_wan":None, + "tcp":True, + "udp":False, + "send":True, + "recv":True, + "rr":False + }, + "tests":None + }, + "run":1 +} + +class SIMSpeedTest(object): + STATUS_DEVS_PATH = '/status/wan/devices' + CFG_RULES2_PATH = '/config/wan/rules2' + CTRL_WAN_DEVS_PATH = '/control/wan/devices' + MIN_UPLOAD_SPD = 1.0 # Mbps + MIN_DOWNLOAD_SPD = 10.0 + CONNECTION_STATE_TIMEOUT = 7 * 60 # 7 Min + NETPERF_TIMEOUT = 4 * 60 # 4 Min + + def __init__(self): + self.client = CSClient() + + def log(self, fmt, *args): + rawlog(fmt, *args) + msg = fmt % args + self.client.log(msg) + + def find_modems(self, verbose): + dsdm = self.client.get('/config/wan/dual_sim_disable_mask') + self.log("dsdm: %s" % dsdm) + while True: + devs = json.loads(self.client.get(self.STATUS_DEVS_PATH)) + modems_list = [x for x in devs if x.startswith("mdm-")] + self.log('modems_list: %s', modems_list) + num_modems = len(modems_list) + if not num_modems: + rawlog('No Modems found at all yet') + time.sleep(10) + continue + + if num_modems < 2: + status = devs[modems_list[0]].get('status') or None + ready = None if not status else status.get('ready') + if -1 != ready.find('unconfigured'): + rawlog('Modems not yet finished configuring') + time.sleep(10) + continue + + if verbose: + self.log("Looks like we have been run before!") + raise OneModem("Only one Modem found") + else: + break + + modems = {} + for mdm in modems_list: + err_txt = devs[mdm]['status']['error_text'] + if err_txt and -1 != err_txt.find('NOSIM'): + continue + self.log('mdm: %s', mdm) + key = devs[mdm]['info'].get('sim') + self.log('key: %s %s', key, type(key)) + modems[key] = mdm + self.log('modems dict: %s', modems) + return modems + + def find_sims(self): + modems = self.find_modems(True) + sim1 = modems.get('sim1') or None + sim2 = modems.get('sim2') or None + self.log('Sim1: %s Sim2: %s', sim1, sim2) + return (sim1, sim2) + + def find_device(self, devs, devname): + return [idx for (idx, dev) in enumerate(devs) if dev['trigger_string'].startswith(devname)] + + def enable_dev_mode(self, dev, mode, state): + devices = json.loads(self.client.get(self.CFG_RULES2_PATH)) + mdms = self.find_device(devices, dev) + if state != None: + [self.client.put(self.CFG_RULES2_PATH + '/%d' % mdm, {mode:state}) for mdm in mdms] + else: + for mdm in mdms: + key = '%s/%d/%s' % (self.CFG_RULES2_PATH,mdm,mode) + if self.client.get(key) != None: + self.client.delete(key) + + def enable_mdm_mode(self, mode, state): + self.enable_dev_mode('type|is|mdm', mode, state) + + def enable_eth_mode(self, mode, state): + self.enable_dev_mode('type|is|ethernet', mode, state) + + def set_wan_dev_disabled(self, dev, state): + s = self.client.put(self.CFG_RULES2_PATH + '/%d' % dev, {"disabled":state}) + self.log("set_wan_dev_disabled - put s=%s", s) + + def modem_state(self, sim, state): + ''' Blocking call that will wait until a given state is shown as the modem's status ''' + timeout_counter = 0 + sleep_seconds = 0 + conn_path = '%s/%s/status/connection_state' % (self.STATUS_DEVS_PATH, sim) + self.log("modem_state waiting sim=%s state=%s", sim, state) + while True: + sleep_seconds += 5 + conn_state = self.client.get(conn_path) + # TODO add checking for mdm error states + self.log('waiting for state=%s on sim=%s curr state=%s', state, sim, conn_state) + if conn_state.replace('"', '') == state: + break + if timeout_counter > self.CONNECTION_STATE_TIMEOUT: + self.log("timeout waiting on sim=%s", sim) + raise Timeout(conn_path) + time.sleep(min(sleep_seconds,45)) + timeout_counter += sleep_seconds + self.log("sim=%s connected", sim) + return True + + def connect_sim(self, sim, state): + self.client.put('%s/%s/testmode' % (self.CTRL_WAN_DEVS_PATH, sim), {"ready":state}) + + def reset_sim(self, sim, state): + self.log("Reset SIM called") + self.client.put('%s/%s' % (self.CTRL_WAN_DEVS_PATH, sim), {"reset":state}) + while True: + devs = json.loads(self.client.get(self.STATUS_DEVS_PATH)) + modems_list = [x for x in devs if x.startswith("mdm-")] + self.log('Modems_list: %s', modems_list) + if len(modems_list): + time.sleep(10) + continue + else: + break + self.log("Modem is offline") + try: + modems = self.find_modems(False) + except OneModem: + pass + self.log("Modem is back online") + + def reset_spdtest_cnt(self): + self.client.put('/state/system/netperf', {"run_count":0}) + + def iface(self, sim): + iface = self.client.get('%s/%s/info/iface' % (self.STATUS_DEVS_PATH, sim)) + return iface + + def run_speedtest(self, speedtest): + # TODO verify the put was successful + res = self.client.put("/control/netperf", speedtest) + self.log("put netperf res=%s", res) + + timeout_counter = 0 + + # wait for results + delay = speedtest['input']['options']['limit']['time'] + 2 + status = None + status_path = "/control/netperf/output/status" + while True: + self.log("waiting for netperf results...") + status = self.client.get(status_path).replace('"', '') + self.log("status=%s", status) + if status == 'complete': + break + # add timeout + if timeout_counter > self.NETPERF_TIMEOUT: + self.log("timeout waiting on netperf") + raise Timeout(status_path) + time.sleep(delay) + timeout_counter += delay + + if status != 'complete': + self.log("error: status=%s expected 'complete'", status) + return None + + # now get the result + res = self.client.get("/control/netperf/output/results_path") + self.log("results_path=%s", res) + + # TODO verify retrieved the string successfully + # strip and remove the leading/trailing quotes + results = self.client.get("%s" % res.strip()[1:-1]) or {} + #client.log("results=%s (%s)" % (results, type(results))) + return results + + def do_speedtest(self, sim): + mdm_ifc = self.iface(sim).replace('"', '') + self.log('Sim iface: %s', mdm_ifc) + default_speedtest['input']['options']['ifc_wan'] = mdm_ifc + + # run speedtest w/send & recv and attempt to parse as JSON + results = json.loads(self.run_speedtest(default_speedtest)) + + tcp_up = results.get('tcp_up') or None + tcp_down = results.get('tcp_down') or None + + if not tcp_up: + self.log('do_speedtest tcp_up results missing!') + default_speedtest['input']['options']['send'] = True + default_speedtest['input']['options']['recv'] = False + results = json.loads(self.run_speedtest(default_speedtest)) + tcp_up = results.get('tcp_up') or None + if not tcp_down: + self.log('do_speedtest tcp_down results missing!') + default_speedtest['input']['options']['send'] = False + default_speedtest['input']['options']['recv'] = True + results = json.loads(self.run_speedtest(default_speedtest)) + tcp_down = results.get('tcp_down') or None + + up = float(tcp_up.get('THROUGHPUT') or 0) if tcp_up else 0 + down = float(tcp_down.get('THROUGHPUT') or 0) if tcp_down else 0 + self.log('do_speedtest returning: %s down, %s up', down, up) + + self.reset_spdtest_cnt() + + return (up, down) + + def send_email(self, message): + rawlog("send_email:%s",message) + return + # Example of how to send email + server = "mail..com" + port = 587 + username = ".com" + password = '' + from_addr = '%s@.com' % username + to_addr = '@.com' + + email = Email.Email(server, port, username, password) + email.message('Information Message', from_addr, to_addr, message) + email.send() + + + def meets_minimums(self, up, down): + return up >= self.MIN_UPLOAD_SPD and down >= self.MIN_DOWNLOAD_SPD + + def percent_diff(self, a, b): + if a == 0 or b == 0: return 100.0 + return (abs(a-b)/min(a,b))*100.0 + + def gt_percent_diff(self, a, b, percent): + return self.percent_diff(a, b) >= percent + + def ten_prcnt_diff(self, a,b): + return self.gt_percent_diff(a, b, 10.0) + + def select_sim(self, sim1, s1_up, s1_down, sim2, s2_up, s2_down): + rawlog('select_sim') + s1 = {'slot':sim1, 'slot_name':'sim1', 'slot_num':1, 'up':s1_up, 'down':s1_down} + s2 = {'slot':sim2, 'slot_name':'sim2', 'slot_num':2, 'up':s2_up, 'down':s2_down} + + if self.meets_minimums(s1_up, s1_down): + return (s1,s2) + + if self.meets_minimums(s2_up, s2_down): + return (s2,s1) + + # Neither meet minimums, but > 10% diff on each, defer to DL speed + if self.ten_prcnt_diff(s1_up, s2_up) and self.ten_prcnt_diff(s1_down, s2_down): + return (s1,s2) if s1_down > s2_down else (s2,s1) + + # Neither meet minimums, but > 10% diff on upload, defer to UL speed + if self.ten_prcnt_diff(s1_up, s2_up) and not self.ten_prcnt_diff(s1_down, s2_down): + return (s1,s2) if s1_up > s2_up else (s2,s1) + + # Neither meet minimums, but > 10% diff on download, defer to DL speed + if not self.ten_prcnt_diff(s1_up, s2_up) and self.ten_prcnt_diff(s1_down, s2_down): + return (s1,s2) if s1_down > s2_down else (s2,s1) + + # Neither meet minimums and < 10% diff on both, defer to biggest delta + if not self.ten_prcnt_diff(s1_up, s2_up) and not self.ten_prcnt_diff(s1_down, s2_down): + d_up = self.percent_diff(s1_up, s2_up) + d_down = self.percent_diff(s1_down, s2_down) + + if d_up > d_down: + return (s1,s2) if s1_up > s2_up else (s2,s1) + else: + return (s1,s2) if s1_down > s2_down else (s2,s1) + + def log_results(self, product_name, system_id, selected_sim, rejected_sim): + rawlog("log_results") + sdiag= json.loads(self.client.get('%s/%s/diagnostics' % (self.STATUS_DEVS_PATH, selected_sim['slot']))) + rdiag= json.loads(self.client.get('%s/%s/diagnostics' % (self.STATUS_DEVS_PATH, rejected_sim['slot']))) + + msg1 = 'The results of the SIM test on product={} id={} are as follows:'.format(product_name, system_id) + msg2 = 'Selected Sim: slot={} carrier={} ICCID={} down={:.4f} up={:.4f}'.format( \ + selected_sim['slot_name'], sdiag['HOMECARRID'], sdiag['ICCID'], selected_sim['down'], selected_sim['up']) + msg3 = 'Rejected Sim: slot={} carrier={} ICCID={} down={:.4f} up={:.4f}'.format( \ + rejected_sim['slot_name'], rdiag['HOMECARRID'], rdiag['ICCID'], rejected_sim['down'], rejected_sim['up']) + + msg = msg1 + ' ' + msg2 + ', ' + msg3 + + self.log(msg1); self.log(msg2); self.log(msg3) + self.client.alert(msg) + self.send_email(msg) + + def lock_sim(self,sim): + port = self.client.get('/%s/%s/info/port' % (self.STATUS_DEVS_PATH, sim['slot'])).replace('"', '') + sim_disable_mask = '%s,%s' % (port, sim['slot_num']) + self.log("Writing dual_sim_disable_mask to: %s", sim_disable_mask) + self.client.put('/config/wan', {'dual_sim_disable_mask':sim_disable_mask}) + + def NTP_time_updated(self): + return time.time() > 1467416418 + + def ECM_resume(self): + self.log('Resuming ECM') + self.client.put('/control/ecm', {'restart': 'true'}) + timeout_count = 500 + while not 'connected' == self.client.get('/status/ecm/state').replace('"', ''): + timeout_count -= 1 + if not timeout_count: + raise Timeout('ECM not connecting') + time.sleep(2) + + def ECM_suspend(self): + self.log('Suspending ECM') + timeout_count = 500 + while not 'ready' == self.client.get('/status/ecm/sync').replace('"', ''): + timeout_count -= 1 + if not timeout_count: + raise Timeout('ECM sync ready') + time.sleep(2) + self.client.put('/control/ecm', {'stop':'stop'}) + timeout_count = 500 + while not 'stopped' == self.client.get('/status/ecm/state').replace('"', ''): + timeout_count -= 1 + if not timeout_count: + raise Timeout('ECM not stopping') + time.sleep(2) + + def ECM_config_ver(self): + return self.client.get('/config/ecm/config_version').replace('"', '') + + def ECM_updated(self): + return self.ECM_config_ver > 0 + + def ECM_connected(self): + ecm_state = self.client.get("/status/ecm/state") + #TODO Remove unmanaged test below after dev done. + return '"connected"' == ecm_state or '"unmanaged"' == ecm_state + + def min_fw_version_check(self, major, minor, patch=0): + fw_major = int(self.client.get("/status/fw_info/major_version").replace('"', '')) + fw_minor = int(self.client.get("/status/fw_info/minor_version").replace('"', '')) + fw_patch = int(self.client.get("/status/fw_info/patch_version").replace('"', '')) + self.log("Current FW Version - major: %s, minor: %s, patch: %s", + fw_major, fw_minor, fw_patch) + return fw_major >= major and fw_minor >= minor and fw_patch >= patch + + def wait_to_start(self): + while not self.NTP_time_updated(): + self.log("waiting for NTP time set now=%s", time.ctime()) + time.sleep(5) + + while not self.ECM_connected(): + self.log("waiting for ECM update now=%s", time.ctime()) + time.sleep(5) + + def run(self): + if not self.min_fw_version_check(6, 2): + self.log("Boot1 FW version check failed!") + return + else: + self.log("Boot1 FW version check passed") + + if 'running' == self.client.get('/status/sdk/Boot1').replace('"', ''): + self.log("Boot1 SIM test already running!") + return + else: + self.client.put('/status/sdk', {'Boot1':'running'}) + + self.log("Boot1 SIM test starting") + self.wait_to_start() + + product_name = self.client.get("/status/product_info/product_name") + system_id = self.client.get("/config/system/system_id") + + sim1, sim2 = self.find_sims() + + self.client.log('Sending alert to ECM.') + message = "Hello from BOOT1 SIM Speedtest product={} id={}".format(product_name, system_id) + self.client.alert('Transmitting message: %s' % message) + self.send_email(message) + + self.ECM_suspend() + + self.enable_eth_mode('loadbalance', True) + self.enable_mdm_mode('loadbalance', True) + self.connect_sim(sim1, True) + self.connect_sim(sim2, False) + + sim1_upload_speed = sim1_download_speed = 0.0 + if sim1: + try: + if self.modem_state(sim1, 'connected'): + sim1_upload_speed, sim1_download_speed = self.do_speedtest(sim1) + except Timeout: + self.log("timeout on sim=%s", sim1) + self.client.alert("netperf failed on sim1=%s" % sim1) + # continue, try sim2 + + sim2_upload_speed = sim2_download_speed = 0.0 + if sim2 and not (sim1_upload_speed >= self.MIN_UPLOAD_SPD and sim1_download_speed >= self.MIN_DOWNLOAD_SPD): + self.connect_sim(sim2, True) + self.connect_sim(sim1, False) + try: + if self.modem_state(sim2, 'connected'): + sim2_upload_speed, sim2_download_speed = self.do_speedtest(sim2) + except Timeout: + self.log("timeout on sim=%s", sim2) + self.client.alert("netperf failed on sim2=%s" % sim2) + self.connect_sim(sim1, True) + elif not sim2: + rawlog("Error with Sim2:%s", sim2) + self.log("Error with Sim2:%s", sim2) + + self.enable_mdm_mode('loadbalance', False) + self.enable_eth_mode('loadbalance', False) + + rawlog('\n\nSpeeds - Sim1: %f down, %f up Sim2: %f down, %f up' % \ + (sim1_download_speed, sim1_upload_speed, \ + sim2_download_speed, sim2_upload_speed)) + + # Check for abject failure + if sim1_download_speed == 0.0 and sim1_upload_speed == 0.0 and \ + sim2_download_speed == 0.0 and sim2_upload_speed == 0.0: + self.client.alert('Was not able to get any modem speed results. Aborting!') + self.log('Was not able to get any modem speed results. Aborting!') + self.client.put('/status/sdk', {'Boot1':'failed'}) + self.ECM_resume() + return + + # Run the selection algorithm + selected_sim, rejected_sim = self.select_sim(sim1, sim1_upload_speed, sim1_download_speed, sim2, sim2_upload_speed, sim2_download_speed) + + self.log_results(product_name, system_id, selected_sim, rejected_sim) + + self.enable_mdm_mode('loadbalance', None) + self.enable_eth_mode('loadbalance', None) + + self.lock_sim(selected_sim) + self.reset_sim(selected_sim['slot'], True) + + self.ECM_resume() + + self.client.put('/status/sdk', {'Boot1':'completed'}) + self.log("Boot1 SIM test completed") + +if __name__ == '__main__': + errmsg = None + try: + boot1 = SIMSpeedTest() + boot1.run() + except OneModem: + # non-local goto :-( + boot1.ECM_resume() + except Exception as err: + boot1.ECM_resume() + errmsg = "failed with exception={} err={}".format(type(err), str(err)) + + # not doing this inside the exception above in case we can't write to the + # filesystem; don't want to throw inside a except + if errmsg is not None: + rawlog("failed with exception={}".format(errmsg)) + try: + with open("/tmp/boot1.fatal.log","w") as outfile: + outfile.write(errmsg) + except IOError as err: + # unable to open the log file + if _STDERR: + print("failed with exception={}".format(err), file=sys.stderr) + pass + + while True: + # as of this writing (20160707) we cannot exit the SDK app without + # being restarted by the firmware SDK service. So we suspend. + time.sleep(2147483647) diff --git a/Boot1/Email.py b/Boot1/Email.py new file mode 100644 index 00000000..dfa90be5 --- /dev/null +++ b/Boot1/Email.py @@ -0,0 +1,98 @@ +""" + + Email.py -- Email from SDK App + + Copyright © 2015 Cradlepoint, Inc. . All rights reserved. + + This file contains confidential information of Cradlepoint, Inc. and your + use of this file is subject to the Cradlepoint Software License Agreement + distributed with this file. Unauthorized reproduction or distribution of + this file is subject to civil and criminal penalties. + + Desc: + +""" + +import sys +import smtplib +import socket +import time + +class Email(object): + """Email class to send emails""" + +# USER = "nrf2016cp@gmail.com" +# PASS = "Welcome2cp" +# +# SERVER = "smtp.gmail.com" +# PORT = 587 + DATE_FMT= "%a, %d %b %Y %H:%M:%S %z" + + def __init__(self, server, port, user, password): + self.server = server + self.port = port + self.user = user + self.password = password + + self.msg = '' + self.from_addr = 'test1@test.org' + self.to_addr = 'test2@test.org' + + def message(self, subject, from_addr, to_addr, body): + self.from_addr = from_addr + self.to_addr = to_addr + + template = 'Content-Type: text/plain; charset="us-ascii"\n' + template += 'MIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\n' + template += 'Date: {}\n' + template += 'Subject: {}\nFrom: {}\nTo: {}\n\n{}' + + self.msg = template.format(time.strftime(self.DATE_FMT), subject, from_addr, to_addr, body) + + def _send_tls(self): + try: + host = socket.gethostname() + mail_server = smtplib.SMTP(self.server, self.port, host) + mail_server.ehlo() + mail_server.starttls() + mail_server.ehlo() + + mail_server.login(self.user, self.password) + mail_server.sendmail(self.from_addr, self.to_addr, self.msg) + + mail_server.quit() + except smtplib.SMTPException as err: + print("_send_tls failed: err=%s" % err, file=sys.stderr) + raise + + def _send(self): + try: + host = socket.gethostname() + mail_server = smtplib.SMTP(self.server, self.port, host) + mail_server.ehlo() + mail_server.sendmail(self.from_addr, self.to_addr, self.msg) + mail_server.quit() + + except smtplib.SMTPException as err: + print("_send failed: err=%s" % err, file=sys.stderr) + raise + + def send(self): + try: + self._send_tls() + except smtplib.SMTPException: + # _send_tls() will log error + pass + else: + # succes! + return + + return + + # fall back to insecure email + print("send falling back to unauthenticated SMTP", file=sys.stderr) + try: + self._send() + except smtplib.SMTPException: + # _send() will log error + pass diff --git a/Boot1/LogLister.py b/Boot1/LogLister.py new file mode 100644 index 00000000..84894aae --- /dev/null +++ b/Boot1/LogLister.py @@ -0,0 +1,51 @@ +""" + + LogLister.py -- Sample SDK App + + Copyright © 2015 Cradlepoint, Inc. . All rights reserved. + + This file contains confidential information of Cradlepoint, Inc. and your + use of this file is subject to the Cradlepoint Software License Agreement + distributed with this file. Unauthorized reproduction or distribution of + this file is subject to civil and criminal penalties. + + Desc: + +""" + +import serial +import time + + +class LogLister(object): + """Crappy class connects to MIPS/ARM CP router""" + + DEV = '/dev/ttyUSB0' + MIPS = 57600 + ARM = 115200 + + def __init__(self): + self.serial = serial.Serial() + self.serial.port = self.DEV + self.serial.baudrate = self.MIPS + self.serial.bytesize = 8 + self.serial.parity = 'N' + self.serial.stopbits = 1 + self.serial.timeout = 1 + + def dump(self, cmd): + if not self.serial.isOpen(): + self.serial.open() + + self.serial.write(cmd.encode()+b'\r\n') + time.sleep(0.5) + + out = '' + resp = self.serial.readline() + + while resp != b'': + out += resp.decode() + resp = self.serial.readline() + self.serial.close() + + return out diff --git a/Boot1/install.sh b/Boot1/install.sh new file mode 100644 index 00000000..4230e61e --- /dev/null +++ b/Boot1/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION for Boot1 on:" >> install.log +date >> install.log diff --git a/Boot1/package.ini b/Boot1/package.ini new file mode 100644 index 00000000..f6a3fba9 --- /dev/null +++ b/Boot1/package.ini @@ -0,0 +1,12 @@ +[Boot1] +uuid=cde20cbb-b146-40ef-9f60-c162a0e4c562 +vendor=Cradlepoint +notes=Router SDK Boot1 SIM Speedtest +firmware_major=6 +firmware_minor=2 +restart=true +reboot=true +auto_start=true +app_type=0 +version_major=1 +version_minor=10 diff --git a/Boot1/start.sh b/Boot1/start.sh new file mode 100644 index 00000000..676eb9a3 --- /dev/null +++ b/Boot1/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +#echo foo > /tmp/foo.txt +cppython Boot1.py diff --git a/GNU_Make_README.html b/GNU_Make_README.html new file mode 100644 index 00000000..440a5551 --- /dev/null +++ b/GNU_Make_README.html @@ -0,0 +1,428 @@ + + + +GNU_Make_README + + + + +

Router SDK reference application and development tools.

+

Available GNU make targets:

+

default: + Build and test the router SDK reference app and create the archive file suitable for deployment to a router DEV mode or for uploading to ECM.

+

clean: + Clean all project artifacts. If a package returns with 'nothing to be done', a clean will cause the next package to occur.

+

package: + Create the application archive tar.gz file.

+

status: + Fetch and print current SDK app status from the locally connected router.

+

install: + Secure copy the application archive to a locally connected router. The router must already be in DEV mode via the ECM.

+

start: + Start the application on the locally connected router.

+

stop: + Stop the application on the locally connected router.

+

uninstall: + Uninstall the application from the locally connected router.

+

purge: + Purge all applications from the locally connected router.

+
+

HOW-TO Steps for running the reference application on your router.

+
    +
  1. +

    Register your router with ECM.

    +
  2. +
  3. +

    Put your router into DEV mode via ECM.

    +
  4. +
  5. +

    Export the following variables in your environment:

    +
    APP_NAME - The directory that contains the app
    +APP_UUID - Each app must have its own UUID
    +DEV_CLIENT_MAC - The mac address of your router
    +DEV_CLIENT_IP  - The lan ip address of your router
    +DEV_CLIENT_PASSWORD - The router password. Only needed if the
    +default password has been changed.
    +
    + +

    Example:

    +
    $ export APP_NAME=ping
    +$ export APP_UUID=38858989-af8b-4e5d-8e2e-1cbc79acdde4
    +$ export DEV_CLIENT_MAC=44224267
    +$ export DEV_CLIENT_IP=192.168.0.1
    +
    + +
  6. +
  7. +

    Create the application package (i.e. <app_name>.tar.gz).

    +
    $ make
    +
    + +
  8. +
  9. +

    Test connectivity with your router via the 'status' target.

    +
    $ make status
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X GET http://192.168.0.1/api/status/system/sdk | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": {},
    +    "success": true
    +}
    +
    + +
  10. +
  11. +

    Install the application in the router.

    +
    $ make install
    +scp /home/sfisher/dev/sdk/hspt.tar.gz admin@192.168.0.1:/app_upload
    +admin@192.168.0.1's password: 
    +hspt.tar.gz                          100% 1439     1.4KB/s   00:00    
    +Received disconnect from 192.168.0.1: 11: Bye Bye
    +lost connection
    +
    + +
  12. +
  13. +

    Get application execution status from your router.

    +
    $ make status
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X GET http://192.168.0.1/api/status/system/sdk | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": {
    +        "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c": {
    +            "app": {
    +                "date": "2015-12-04T09:30:39.656151",
    +                "name": "hspt",
    +                "restart": true,
    +                "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +                "vendor": "Cradlebox",
    +                "version_major": 1,
    +                "version_minor": 1
    +            },
    +            "base_directory": "/var/mnt/sdk/apps/7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +            "directory": hspt",
    +            "filename": "dist/tmp_znv2t",
    +            "state": "started",
    +            "summary": "Package started successfully",
    +            "type": "development",
    +            "url": "file:///var/tmp/tmpg1385l",
    +            "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"
    +        }
    +    },
    +    "success": true
    +}
    +
    + +
  14. +
  15. +

    Uninstall the application in the router.

    +
    $ make uninstall
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X PUT http://192.168.0.1/api/control/system/sdk/action \
    +                -d data='"uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"' | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": "uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +    "success": true
    +}
    +
    + +
  16. +
+
+

Published Date: 5-9-2017

+

This article not have what you need? Not find what you were looking for? Think this article can be improved? Please let us know at suggestions@cradlepoint.com.

+ + + + diff --git a/gnu_apps/extensible_ui_ref_app/Makefile b/Makefile similarity index 100% rename from gnu_apps/extensible_ui_ref_app/Makefile rename to Makefile diff --git a/README.md b/README.md index 2228bae9..34094876 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,84 @@ -# Router App/SDK sample application design tools. +# Router SDK Design Tools and sample applications. +---------- +This directory contains the Router SDK tools and sample applications. Below is a desciption of each. The Router Applications Development Guide is the best document to read first. + +## *** IMPORTANT - PLEASE READ *** + +This is version 2.0 of the Router SDK and applications. The SDK has been simplified from the previous SDK to decrease the learning curve to allow more focus on application development. The router application infrastructure and packaging is unchanged. That is, an 'tar.gz' application package built with the previous SDK can still be installed into the router using SDK version 2.0. However, the coding of an application version 1.0 may need to be re-factored in order for continued development with SDK version 2.0. Please see document SDK\_version\_1.0\_app_refactor.html in this directory for details. + +## Documents + +- **README.html** + - This README file. +- **Router\_Application\_Development_Guide.html** + - The main document that describes application development. +- **Router\_APIs\_for_Applications.html** + - The router config store API in the router. +- **GNU\_Make_README.html** + - The Linux GNU make instructions for the SDK. + +## Sample Application Directories + +- **app_template** + - A skeleton template for the creation of a new application. +- **Boot1** + - On bootup, this application will select test the connection of each sim in a dual sim modem and enable the best. +- **email** + - Sends an email. +- **ftp_client** + - Creates a file and uploads it to an FTP server. +- **ftp_server** + - Creates an FTP server in the router. A USB memory device is used as the FTP directory. +- **gps_localhost** + - Assuming the Cradlepoint router is configured to forward NMEA sentences to a localhost port, open the port as a server and receive the streaming GSP data. +- **gps_probe** + - Probe the GPS hardware and log the results. +- **hello_world** + - Outputs a 'Hello World!' log every 10 seconds. +- **hspt** + - Sets up a custom Hot Spot landing page. +- **list\_serial_ports** + - Lists out the serial ports in the logs. +- **loglevel** + - Changes the router log level. +- **modbus_poll** + - Poll a single range of Modbus registers from an attached serial Modbus/RTU PLC or slave device. +- **modbus\_simple_bridge** + - A basic Modbus/TCP to RTU bridge. +- **ping** + - Ping an address and log the results. +- **power_gpio** + - Query the 2x2 power connector GPIO. +- **send_alert** + - Sends an alert to the ECM when the application is started and stopped. +- **send\_to_server** + - Gets the '/status' from the reouter config store and send it to a test server. +- **serial_echo** + - Waits for data to enter the serial port, then echo back out. +- **simple\_web_server** + - A simple web server to receive messages. Note that any 'server function' requires the router firewall to be correctly changed to allow client access to the router. + + + + +## SDK Directories + +- **common** + - Contains the cs.py file which should be copied into an application folder. It is a wrapper for the TCP interface to the router config store. +- **config** + - Contains the settings.mk file for Linux users that want to use GNU make for application development instead of python make.py. +- **tools** + - Contains support files for the SDK. There is also a simple python syslog server that can be used during application development. + +## Files + +- **make.py** + - The main python tool used to build application packages and install, uninstall, start, stop, or purge from a locally connected router that is in DEV mode. +- **Makefile** + - The Makefile for Linux users that want to use GNU make for application development instead of python make.py. +- **sdk_settings.ini** + - This is the ini file that contains the settings used by python make.py. -## File Make.py -This is the main build tool - see for how-to-use -documentation. -## Directory config -Hold various shared configuration files, plus the default main.py - -## Directory cp_lib - -Common library modules. Most are designed to work on the Cradlepoint router, -although most can run on either a router or (for testing) on a PC - -## Directory data - SAMPLES - -Code Sample related to 'data' usage and movement. -* json_settings - simple JSON server on TCP port 9901, which allows -a JSON RPC client to query the CradlepointAppBase 'settings' data - -## Directory demo - SAMPLES - -Larger code applications related to customer demos. -* gpio_power_loss - monitor the IBR11X0 2x2 power connector input to detect -power loss. Email an alert when power is lost or restored. -* hoops_counter - run both a JSON RPC server and web server. -They share a counter value (app_base.data["counter"]). -JSON RPC server assumes a remote client puts (writes) new counter values. -The web server shows as 5 large graphic digits on a page. - -## Directory gpio - SAMPLES - -Code Samples related to router input/output pins: -* power - read the 2x2 power connector gpio on the IBR11X0 -* serial_gpio - read 3 inputs on the IBR11X0's RS-232 port - -## Directory gps - SAMPLES - -Code Samples related to router GPS: -* probe_gps - query the router STATUS tree, showing if the router supports -GPS, and if any active modems show GPS data - -## Directory network - SAMPLES - -Code Samples related to common TCP/IP network tasks: -* **warning: any 'server function' requires the router firewall to be -correctly changed to allow client access to the router.** -* digit_web - display a web page of a 5-digit number, as JPG images per digit. -This demonstrates support for a simple web page with embedded objects, \ -which means the remote browser makes multiple requests. -* send_email - send a single email to GMAIL with SSL/TLS -* send_ping - use Router API control tree to send a ping. -Sadly, is fixed to 40 pings, so takes about 40 seconds! -* simple_web - display a web page with a short text message. -* tcp_echo - accept a raw client, echoing back (repeating) any text received. - -## Directory serial_port - SAMPLES - -Code Samples related to common TCP/IP network tasks: -* list_port - tests ports on IBR11X0 - firmware does NOT yet support -* serial_echo - open IBR11X0 RS-232 port, echo by bytes received -- firmware does NOT yet support properly - -## Directory simple - SAMPLES - -Code Samples related to common tasks: -* hello_world - single python file running, sending text message to Syslog. -Naming the code file 'main.py' -prevents make.py from including the default main.py and many cplib files. -* hello_world_1task - send text message to Syslog, but creates 1 sub-task -to do the sending. On a PC, a KeyboardInterrupt shows how to gracefully -stop children tasks. -* hello_world_2task - send 3 text messages to Syslog, by creating 3 sub-tasks -to do the sending. 2 sub-tasks run forever, while the third exists frequently -and is restarted. -* hello_world-app - send a text message to Syslog. uses the CradlepointAppBase. -* send_alert - send an ECM alert. - -## Directory test - -Unittest scripts - -## Directory tools - -Shared modules NEVER designed to run on the router. -Most of the tools here are used by MAKE.PY diff --git a/Router_API_for_Applications.html b/Router_API_for_Applications.html new file mode 100644 index 00000000..c5a910ec --- /dev/null +++ b/Router_API_for_Applications.html @@ -0,0 +1,1053 @@ + + + +Router_API_for_Applications + + + + +

Router API for Applications

+
+

Quick Links

+

Overview

+

Router Info

+

Router SDK and Application Info

+

Logging

+

Ping

+

GPIO

+

GPS

+

Modem

+

+

Overview

+

The router trees, also referred to as the router config store, are essentially the APIs to router query and control. One can read items to determine status or results and write to control and configure. All data is exchanged in the JSON format. The JSON format is described at http://www.json.org/. The data can be accessed via the router CLI, a python application, or using curl.

+

The router config store consist of the following trees:

+
    +
  • +control +
      +
    • Elements used for controlling functions of the router.
    • +
    +
  • +
  • +config +
      +
    • Elements used for router configuration.
    • +
    +
  • +
  • +status +
      +
    • Elements to get the status of the router.
    • +
    +
  • +
  • +state +
      +
    • Should not to be used by applications.
    • +
    +
  • +
+

This document does not describe every item in the router config store. There are sections that cover functionality and how the config store can be used for that function (i.e. Ping, GPIO, GPS, Modem, etc.). Many functional areas will span multiple tree types depending on the use case. Anything which is done via the router UI, utilizes the router trees.

+

CLI Example

+

Below is an example of how to query the router system logging information using the cat or get command and then changing the logging level to info with the set command. Note the JSON format in the output. These trees can also be traversed using linux change directory command (i.e. cd /config/system/logging). This can be very useful to see what is contained in each branch of the tree.

+
    [admin@IBR900-267: /$ cat /config/system/logging
+    {
+        "console": false,
+        "enabled": true,
+        "firewall": false,
+        "level": "debug",
+        "max_logs": 1000,
+        "modem_debug": {
+            "lvl_error": false,
+            "lvl_info": false,
+            "lvl_trace": false,
+            "lvl_warn": false
+        },
+        "remoteLogging": {
+            "enabled": false,
+            "serverAddr": "192.168.20.171",
+            "server_port": 514,
+            "system_id": false,
+            "utf8_bom": false
+        },
+        "services": []
+    }
+    [admin@IBR900-267: /$ set /config/system/logging/level "info"
+
+    [admin@IBR900-267: /$ cat /config/system/logging
+    {
+        "console": false,
+        "enabled": true,
+        "firewall": false,
+        "level": "info",
+        "max_logs": 1000,
+        "modem_debug": {
+            "lvl_error": false,
+            "lvl_info": false,
+            "lvl_trace": false,
+            "lvl_warn": false
+        },
+        "remoteLogging": {
+            "enabled": false,
+            "serverAddr": "192.168.20.171",
+            "server_port": 514,
+            "system_id": false,
+            "utf8_bom": false
+        },
+        "services": []
+    }
+
+ +

Python Example

+

All sample apps include a cs.py file which contains the CSClient class. This class is a wrapper for the TCP interface to the router trees. Below is an example of how to query the router system logging information and then change the logging level to debug using the CSClient class.

+
    # Import the CSClient file which contains the necessary functions
+    # for router config store access
+    import cs
+
+    appName = 'ref_app'
+
+    cs.CSClient().log(appName, 'action({})'.format(command))
+
+    # Read the logging level
+    ret_value = cs.CSClient().get('/config/system/logging/level')
+
+    # Output a syslog for the current logging level
+    cs.CSClient().log(appName, 'Current Logging level = {}'.format(ret_value))
+    ret_value = ''
+
+    # Set the logging level to debug when the app is stopped.
+    ret_value = cs.CSClient().put('/config/system/logging', {'level': 'debug'})
+
+    # Output a syslog for the new current logging level
+    cs.CSClient().log(appName, 'New Logging level = {}'.format(ret_value))
+
+ +

Curl Example

+

On Linux, the curl command can also be used to access items in the router trees. Use curl 'GET' to read and 'PUT' to write. Below is an example of getting the SDK status.

+

The user specific information in the curl command is:

+
    +
  • router user name: admin
  • +
  • router password: 44224267
  • +
  • +

    router IP address: 192.168.0.1

    +
        $ curl -s --digest --insecure -u admin:44224267 -H "Accept: application/json" -X GET http://192.168.0.1/api/status/system/sdk | /usr/bin/env python3 -m json.tool
    +    {
    +        "success": true,
    +        "data": {
    +            "service": "started",
    +            "mode": "devmode",
    +            "summary": "Service started"
    +        }
    +    }
    +
    +
    +    curl -s --digest --insecure -u admin:44224267 -H "Accept: application/json" -X GET http://192.168.0.1/api/config/system/logging/level | /usr/bin/env python3 -m json.tool
    +    {
    +        "success": true,
    +        "data": "debug"
    +    }
    +
    +
    +    curl -s --digest --insecure -u admin:44224267 -H "Accept: application/json" -X PUT http://192.168.0.1/api/config/system/logging/level -d data='"info"' | /usr/bin/env python3 -m json.tool
    +    {
    +        "success": true,
    +        "data": "info"
    +    }        
    +
    + +
  • +
+

+

Router Info

+

There are three different trees that provide router information. Most are self explanatory.

+

Product Info /status/product_info/:

+
    cat /status/product_info/
+    {
+        "company_name": "Cradlepoint, Inc.",
+        "company_url": "http://cradlepoint.com",
+        "copyright": "Cradlepoint, Inc. 2017",
+        "mac0": "00:30:44:22:42:67",
+        "manufacturing": {
+            "board_ID": "909507",
+            "mftr_date": "1643",
+            "serial_num": "WA164300000518"
+        },
+        "product_name": "IBR900LP6"
+    }
+
+ +

Firmware Info /status/fw_info/:

+
    cat /status/fw_info/
+    {
+        "build_date": "Wed Mar  8 18:57:21 MST 2017",
+        "build_type": "RELEASE",
+        "build_version": "cc2619b",
+        "custom_defaults": false,
+        "fw_update_available": false,
+        "major_version": 6,
+        "manufacturing_upgrade": false,
+        "minor_version": 3,
+        "patch_version": 1,
+        "upgrade_major_version": 0,
+        "upgrade_minor_version": 0,
+        "upgrade_patch_version": 0
+    }  
+
+ +

Feature Info /status/feature:

+

The feature information provides a list of feature license. The two SDK license will only exist if the features have been enabled via the ECM. The 'Router SDK' means that router apps have been enabled while the 'Router SDK - Dev Mode' means the the development mode is enabled.

+
    cat /status/feature
+    {
+        "db": [
+            [
+                "46a03bd2-91ae-11e2-a305-002564cb1fdc",
+                "Extended Enterprise License",
+                1123,
+                1064
+            ],
+            [
+                "b7b3eb09-53c5-4a5c-88d8-f9456870087c",
+                "Router SDK - Dev Mode",
+                7606,
+                7597
+            ],
+            [
+                "9568fa81-d8e0-4223-a18c-0b1784f21f2a",
+                "Router SDK",
+                7607,
+                7597
+            ]
+        ],
+        "expiring_alert": false
+    }
+
+ +

System ID /config/system/system_id:

+

This is a customizable identity that will be used in router reporting and alerting. The default value is the product name and the last three characters in the MAC address of the router. It is used as a hostname to DHCP, so it must conform to standard hostname conventions.

+
    cat /config/system/system_id
+    "IBR900-267"
+
+ +

Ethernet /status/ethernet:

+

Shows current state of the Ethernet ports and their speed.

+
    cat /status/ethernet
+    [
+        {
+            "link": "up",
+            "link_speed": "1000FD",
+            "port": 0
+        },
+        {
+            "link": "up",
+            "link_speed": "1000FD",
+            "port": 1
+        },
+        {
+            "link": "down",
+            "link_speed": "Unknown",
+            "port": 2
+        }
+    ]
+
+ +

+

SDK and Application Info

+

SDK and application information can be found in /status/system/sdk. It contains the state of the Router SDK service and any application. If there are any error with installing or starting an application, it will be contained in this tree.

+

/status/system/sdk:

+

The main items in SDK status tree are:

+
    +
  • +mode +
      +
    • +This indicates the route SDK mode which affects how apps can be installed. +
        +
      • standard - Normal router mode when apps can be installed via ECM.
      • +
      • devmode - Development mode which allows apps to be installed directly from a computer. This is set from the Tools menu of ECM.
      • +
      +
    • +
    +
  • +
  • +service +
      +
    • The state of the Router SDK service in the router.
    • +
    +
  • +
  • +summary +
      +
    • A more detailed version of the Router SDK service state.
    • +
    +
  • +
  • +apps +
      +
    • A list of any apps that are installed with details about each app. Most are items self explanatory. However, note the 'state' which indicated the current state of the app. This can be installing, started, error, etc.
    • +
    +
  • +
+

No App installed:

+
    cat /status/system/sdk
+    {
+        "mode": "devmode",
+        "service": "started",
+        "summary": "Service started"
+    }
+
+ +

After App installed:

+
    cat /status/system/sdk
+    {
+        "apps": [
+            {
+                "_id_": "616acd0c-0475-479e-a33b-f7054843c971",
+                "app": {
+                    "auto_start": true,
+                    "date": "2017-03-31T15:49:17.537335",
+                    "name": "hello_world",
+                    "reboot": true,
+                    "restart": false,
+                    "uuid": "616acd0c-0475-479e-a33b-f7054843c971",
+                    "vendor": "Cradlebpoint",
+                    "version_major": 1,
+                    "version_minor": 0
+                },
+                "state": "started",
+                "summary": "Started Application",
+                "type": "developer"
+            }
+        ],
+        "mode": "devmode",
+        "service": "started",
+        "summary": "Service started"
+    }
+
+ +

+

Logging

+

Router logging can be queried or configured via /config/system/logging. This includes setting the log level along with sending logs to a syslog server.

+
    cat /config/system/logging/
+    {
+        "console": false,
+        "enabled": true,
+        "firewall": false,
+        "level": "debug",
+        "max_logs": 1000,
+        "modem_debug": {
+            "lvl_error": false,
+            "lvl_info": false,
+            "lvl_trace": false,
+            "lvl_warn": false
+        },
+        "remoteLogging": {
+            "enabled": false,
+            "serverAddr": "192.168.0.171",
+            "server_port": 514,
+            "system_id": false,
+            "utf8_bom": true
+        },
+        "services": []
+    }
+
+ +
    +
  • +console +
      +
    • Not Used.
    • +
    +
  • +
  • +enabled +
      +
    • Not Used.
    • +
    +
  • +
  • +firewall +
      +
    • Not Used.
    • +
    +
  • +
  • +level +
      +
    • Setting the log level controls which messages are stored or filtered out. A log level of Debug will record the most information while a log level of Critical will only record the most urgent messages.
    • +
    +
  • +
  • +max_logs +
      +
    • This is the maximum number of log messages the service will store. After this limit is reached, the oldest are lost as new messages come in. However, based on the platform, the limit may be smaller.
    • +
    +
  • +
  • +modem_debug +
      +
    • Used to set the modem logging level. Not recommended for application use.
    • +
    +
  • +
  • +remoteLogging +
      +
    • +enabled +
        +
      • Set true to send log messages to the specified Syslog Server.
      • +
      +
    • +
    • +serverAddr +
        +
      • The Hostname or IP address of the Syslog Server.
      • +
      +
    • +
    • +server_port +
        +
      • The Port Number of the Syslog Server.
      • +
      +
    • +
    • +system_id +
        +
      • Set true to include the routers "System ID" at the beginning of every log message. This is often useful when a single remote syslog server is handling logs for several routers.
      • +
      +
    • +
    • +utf8_bom +
        +
      • The log message is sent using a UTF-8 encoding. By default the router will preprend the Unicode Byte Order Mark (BOM) to the syslog message in compliance with the Syslog protocol, RFC5424. Some syslog servers may not support, fully, RFC5424 and will treat the BOM as ASCII text which will appear as garbled characters in the log. If this occurs, set this option to false.
      • +
      +
    • +
    +
  • +
  • +services +
      +
    • Not recommended for application use.
    • +
    +
  • +
+

+

Ping

+

Ping can be used to test communication to another host on the network. All ping operations and results are found in the /control/ping tree.

+
    cat /control/ping
+    {
+        "result": "",
+        "start": {
+            "bind_ip": false,
+            "deadline": "Same as timeout",
+            "df": "do",
+            "family": "inet",
+            "fwmark": null,
+            "host": null,
+            "iface": null,
+            "interval": 1,
+            "num": 4,
+            "size": 56,
+            "srcaddr": null,
+            "timeout": 11
+        },
+        "status": "",
+        "stop": ""
+    }
+
+ +

Starting a Ping:

+

Write operations to control/ping/start will initiate a ping. The following will start a basic ping when written: {"host": "www.google.com", "size": 64}. An IP address can also be used.

+

NOTE: Concurrent ping operations are not supported.

+

Ping Options for control/ping/start:

+
    +
  • +iface: device UID, interface name, or Network name +
      +
    • Allows ping to be sourced from an interface
    • +
    +
  • +
  • +bind_ip: boolean +
      +
    • Causes ping to bind to an IP on the interface specified by "iface". +
    • +
    +
  • +
  • +srcaddr: IP Address +
      +
    • Causes ping to be sourced from the specified IP address.
    • +
    +
  • +
  • deadline: seconds
  • +
  • +timeout: seconds +
      +
    • If specified, ping will continue until either "num" packets are sent, or "timeout" seconds have elapsed. The two options are equivalent, and if both are specified, the lesser of the two is used
    • +
    +
  • +
  • +df: {do, want, or dont} +
      +
    • +Specify the Path MTU discovery option: +
        +
      • do: Asserts the "Don't Fragment" flag in the ICMP requests, prohibiting fragmentation
      • +
      • want: Allows local fragmentation, and will fragment the ICMP request if the ICMP request exceeds the outbound interfaces' MTU
      • +
      • dont: Do not assert the "Don't Fragment" flag, allowing fragmentation, and will fragment the ICMP Requests in response to ICMP fragmentation responses
      • +
      +
    • +
    +
  • +
  • +fwmark: null/non-null +
      +
    • If non-null, then the SO_MARK socket option is set, causing the packetssent through the socket to be marked.
    • +
    +
  • +
  • +family: {inet or inet6} +
      +
    • If inet, then ping for IPv4 is used; if inet6, then the equivalent of ping6 is used.
    • +
    +
  • +
  • +host: Hostname, or IP address +
      +
    • Specifies the destination for the ICMP requests
    • +
    +
  • +
  • +interval: seconds +
      +
    • The number of seconds to wait between each ICMP request
    • +
    +
  • +
  • +num: integer +
      +
    • The number of ICMP requests to send before exiting. If "deadline" or "timeout" is also specified, then ping will exit after "num" packets have been sent, or min(deadline, timeout") seconds have elapsed.
    • +
    +
  • +
  • +size: bytes +
      +
    • Size of the ICMP request
    • +
    +
  • +
+

Stopping a Ping:

+

Write operations to control/ping/stop will halt the ping in progress, if any. If a ping is not in progress, this is ignored.

+

Retrieving Results and Status:

+

Reading from control/ping/results will return the additional results since the last read, or the start of the ping, whichever is most recent. Reading the results will clear out the results immediately after they are read; the reader will need to concatenate the results if desired.

+

NOTE: If more than 10 results accumulate between reads, the oldest results will be truncated such that the maximum number of result lines will never exceed 10.

+

Reading from control/ping/status will return the current state of the ping in progress. Unlike the results, this will persist through multiple reads. The status can be one of 'running', 'done', 'stopped', or 'error'. If any packets were transmitted during the Ping, then when the ping stops, the result summary will be posted to control/ping/results (and cleared when read)

+

+

GPIO

+

Some of the Cradlepoint routers have GPIO connectors as well as GPIO pins built into the power connectors Below is information related to GPIO router config store tree items.

+

/config/system/gpio_actions

+

Includes configuration data for all user configurable pins - if the pin is enabled, it's direction, and what action it is currently assigned to. The available actions are:

+

if value of direction is in, available values for "current_action" are:

+
    +
  • +in_action_sensing Input Sensing: +
      +
    • In this mode the logic state (high or low) is automatically sensed by the router and is readable as the Current Value.
    • +
    +
  • +
  • +in_action_ignition Ignition Sensing: +
      +
    • (available on power connector only) In this mode the router will turn off after the input has been held low for the timeout period in seconds. The router will then reboot when the input is returned to high. If the input is held low for less than the timeout period before returning to high, no action is taken.
    • +
    +
  • +
  • +in_action_reset Router Reset: +
      +
    • (available on power connector only) In this mode an external device can reset the router by holding the input low for 10-seconds.
    • +
    +
  • +
+

if value of direction is out, available values for "current_action" are:

+
    +
  • +out_action_low Default/Open: +
      +
    • In this mode the output pin is not used and is at 0V (ground potential).
    • +
    +
  • +
  • +out_action_high or out_action_router_running Set Closed/Router Running: +
      +
    • In these modes the output pin is logic low while the router is booting and transitions to logic high when the router is fully running. If the router is reset, the output returns to low until the router has fully rebooted.
    • +
    +
  • +
  • +out_action_modem_connected Modem Connected: +
      +
    • In this mode the output pin is logic low until the modem has connected to the tower. If the connection drops, this output is set low until the connection is restored.
    • +
    +
  • +
+

The "pin" value is for internal purposes only. Please see /status/gpio_action for more information.

+

+

/status/gpio_actions

+

Used to view capabilities of the GPIOs. Here you can see what actions a pin supports, and what direction it can be set to (some pins are fixed direction and some are bidirectional). You can also get the "pretty_value" string, which outputs the current state of the GPIO in human readable form. It is updated every time this field is accessed.

+

/control/gpio

+

Used to bypass the GPIO service and set values to gpio pins manually. Not recommended

+

/status/gpio

+

Used to read the GPIO values directly from the pins.

+

/config/system/connector_gpio

+

Deprecated - DO NOT USE.

+

+

GPS

+

GNSS vs GPS

+

GNSS stands for Global Navigation Satellite Systems and GPS stand for Global Positioning System. GNSS is an umbrella term for all the satellite-based positioning systems, including GPS. GPS itself is the US satellite system and, however, GPS is used interchangeably for all satellite positioning systems. See https://en.wikipedia.org/wiki/Satellite_navigation for more details about satellite navigation.

+

The most common assumption is that all modems are capable of GPS. In order for a cellular modem to support GPS, GPS must be a function of both the modem and carrier. GPS requires the following four elements:

+
    +
  • A cellular modem that supports GPS.
  • +
  • A cellular carrier that allows the GPS functionality on that modem.
  • +
  • Sufficient GPS signal level at the modem deployment location.
  • +
  • GPS antenna (depending on the model of modem or Cradlepoint).
  • +
+

From an app access perspective, most use cases only needs to verify that GPS is enabled and get the GPS location data. This is described below.

+

config/system/gps/enabled

+

This is either true or false and indicates gps enablement.

+

config/system/gps/enable_gps_keepalive

+

When "GPS Keepalive" is enabled, the GPS service will poll the GPS hardware every 10 seconds. The GPS hardware goes into a low power state after a certain interval of inactivity. Setting this flag will prevent the GPS from going into that low power state. Coming out of a low power state takes extra time to regain the GPS lock so set this flag to keep the hardware from falling asleep. Note this flag has no effect if the GPS service itself is not enabled.

+

Get GPS location data

+

The current GPS data can be found in /status/gps/.

+

This contains two fields: fix and nmea.

+
    +
  • +fix: +
      +
    • a struct containing parsed information about the current GPS fix.
    • +
    +
  • +
  • +nmea +
      +
    • an array of National Marine Electronics Association (NMEA) strings.
    • +
    +
  • +
+

An example output is:

+
    cat /status/gps
+    {
+    'fix': {'age': 1.2596200000261888,
+             'altitude_meters': 850.8,
+             'ground_speed_knots': 0.0,
+             'heading': 323.4,
+             'latitude': {'degree': 43.0,
+                          'minute': 37.0,
+                          'second': 10.84808349609375},
+             'lock': True,
+             'longitude': {'degree': -116.0,
+                           'minute': 12.0,
+                           'second': 20.53081512451172},
+             'satellites': 9,
+             'time': 204921},
+    'nmea': ['$GPGSV,4,1,16,05,14,275,24,07,61,082,30,08,40,063,28,09,29,167,26*71\r\n',
+              '$GPGSV,4,2,16,11,18,116,27,13,17,316,27,17,02,189,23,27,12,036,24*77\r\n',
+              '$GPGSV,4,3,16,28,53,243,25,30,67,317,32,33,,,35,38,,,34*7B\r\n',
+              '$GPGSV,4,4,16,39,,,34,40,,,34,41,,,34,42,,,34*73\r\n',
+              '$GPGGA,204920.0,4337.180827,N,11612.342184,W,1,09,0.9,850.8,M,-11.0,M,,*61\r\n',
+              '$GPVTG,323.4,T,308.9,M,0.0,N,0.0,K,A*27\r\n',
+              '$GPRMC,204920.0,A,4337.180827,N,11612.342184,W,0.0,323.4,110417,14.6,E,A*27\r\n',
+              '$GPGSA,A,2,05,07,08,09,11,13,27,28,30,,,,1.2,0.9,0.8*3C\r\n']
+    }
+
+ +

+

Modem

+

The modem status and control information is part of the wan tree. This is a very critical and complicated aspect of the router functionality. Please note that there is no direct access to the modem AT commands. For now, this section will only describe how to enable and disable a modem, along with some status information, as this has been requested by several application developers. More information will be added as needed.

+

The list of modems in the system can be found here: get /status/wan/devices. While this will list all the wan devices, not all modems may be configured with a SIM card.

+
    get status/wan/devices
+    {
+        "ethernet-wan": {
+            ...
+        },
+        "mdm-feb80139": {
+            ...
+        },
+        "mdm-fee5c7f2": {
+            ...
+        }
+    }
+
+ +

To determine if a modem does not have SIM card inserted, see here: get status/wan/devices/mdm-/status/error_text. Where is the modem's unique identifier. The response will be 'null' if the card exist.

+
    get status/wan/devices/mdm-fee5c7f2/status/error_text
+    "SIM error: NOSIM"
+
+ +

Here is an example of the diagnostic information for a modem with a SIM. This contains much data about the modem including its signal quality. Note that the "DBM" field is the modems RSSI.

+
    get status/wan/devices/mdm-fee5c7f2/diagnostics/
+    {
+        "BANDDLFRQ": "2110-2155",
+        "BANDULFRQ": "1710-1755",
+        "CARRID": "Verizon Wireless",
+        "CARRIER_SWITCH_DEFAULTS": "VERIZON,ATT,SPRINT,GENERIC",
+        "CELL_ID": "2934137 (0x2cc579)",
+        "CFGAPNMASK": "8",
+        "CGSN": "356526070342666",
+        "CHIPSET": "9X30C",
+        "CUR_PLMN": "311480",
+        "DBM": "-60",
+        "DEFAPN": "3",
+        "DEFAPNTYPE": "IP",
+        "DISP_IMEI": "356526070342666",
+        "DISP_MEID": "35652607034266",
+        "EMMCOMMSTATE": "RRC Idle",
+        "EMMSTATE": "Registered",
+        "EMMSUBSTATE": "Normal Service",
+        "FW_CARRIER_LOAD": "VERIZON",
+        "GSN": "356526070342666",
+        "HM_PLMN": "311480",
+        "HOMECARRID": "Verizon",
+        "HW_VER": "1.0",
+        "ICCID": "89148000003136071391",
+        "IMSI": "311480313413488",
+        "LAST_PIN": "",
+        "LAST_PIN_VALID": "False",
+        "LTEBANDWIDTH": "10 MHz",
+        "MDL": "Internal LP6 (SIM1)",
+        "MDM_CONTROL_TYPE": "NORMAL",
+        "MDM_DRIVER_CAPABILITIES": "1547313",
+        "MDM_MODE_CAPABILITIES": "21",
+        "MDN": "+12086310191",
+        "MFG": "CradlePoint Inc.",
+        "MFG_MDL": "MC7455-CP",
+        "MODEMIMSSTATE": "Full Service",
+        "MODEMOPMODE": "Online",
+        "MODEMPSSTATE": "Attached",
+        "MODEMSYSMODE": "LTE",
+        "MODEMTEMP": "33",
+        "MSID": "2086310191",
+        "PIN_RETRIES": "3",
+        "PIN_STATUS": "READY",
+        "PRD": "Internal LP6 (SIM1)",
+        "PRI_ID": "9905117",
+        "PRI_VER": "000.005",
+        "PRLV": "15504",
+        "PUK_RETRIES": "10",
+        "RFBAND": "Band 4",
+        "ROAM": "1",
+        "RSRP": "-84",
+        "RSRQ": "-7",
+        "RXCHANNEL": "2000",
+        "SCELLACTIVE": "Not Registered",
+        "SELAPN": "3",
+        "SIM_LOCK": "FALSE",
+        "SINR": "9.2",
+        "SS": "100",
+        "SUPPORTED_TECH": "lte/3g",
+        "TXCHANNEL": "20000",
+        "VER": "SWI9X30C_02.05.07.00 r5154 CARMD-EV-FRMWR2 2015/12/04 22:23:15",
+        "VER_PKG": "02.05.07.00_VERIZON,002.008_002",
+        "VER_PREF_PKG": "02.05.07.00_VERIZON,002.008_002",
+        "VER_PRETTY": "2.5.7.0"
+    }
+
+ +

The modem can be enabled and disabled by changing the testmode/ready flag in the control tree (true to enable and false to diasable). Setting the reset flag to true will reset the modem.

+
    get control/wan/devices/mdm-fee5c7f2/testmode
+    {
+        "ready": true,
+        "reset": false
+    }
+
+ +
+

Published Date: 5-30-2017

+

This article not have what you need? Not find what you were looking for? Think this article can be improved? Please let us know at suggestions@cradlepoint.com.

+ + + + diff --git a/Router_Application_Development_Guide.html b/Router_Application_Development_Guide.html new file mode 100644 index 00000000..5b9dcba5 --- /dev/null +++ b/Router_Application_Development_Guide.html @@ -0,0 +1,930 @@ + + + +Router_Application_Development_Guide + + + + +

Cradlepoint Router Application Development

+
+

Quick Links

+

Overview

+

Developer Community

+

Cradlepoint Knowledge Base

+

Router Python Environment

+

Computer Setup Instructions

+

Router Development Mode

+

Application Directory Structure

+

Application Package Anatomy

+

SDK Instructions Overview

+

Router Syslog for Debugging

+

NCM Application Deployment

+

Sample Application Walk Through

+

+

Overview

+

Cradlepoint’s Router Application Framework provides the ability to add intelligence in the router. Applications written in Python can be securely downloaded to the router via Network Cloud Manager (NCM). This allows for extended router features, FOG Computing, and IoT management.

+

At a high level, the Cradlepoint Router Apps/SDK is a mechanism to package a collection of files – including executable files – in an archive, which can be transferred securely via NCM, hidden within a Cradlepoint router, and executed as an extension to normal firmware.

+

What is Supported?

+

For the scope of this document, Router Apps are limited to the non-privileged Python scripts. Supported functionality:

+
    +
  • Standard TCP/UDP/SSL socket servers function on ports higher than 1024.
  • +
  • Standard TCP/UDP/SSL socket client to other devices (or the router as 127.0.0.1/localhost).
  • +
  • Access to serial ports via PySerial module, including native and USB-serial ports.
  • +
  • Ability to PING external devices.
  • +
  • UI Extensibility (i.e. Hot Spot splash page or other UI WEB pages)
  • +
  • Access to the Router API (aka: status and control tree data).
  • +
  • USB Memory device file access (USB Memory device support varies based on router).
  • +
+

What is not Supported?

+
    +
  • Any form of natively compiled or kernel linked code.
  • +
  • Any function requiring privileged (or root) permissions.
  • +
  • Access to shared resources (for example: no ability to issue custom AT commands to cell modems).
  • +
  • Modifications of routing or security behavior.
  • +
+

Supported Routers

+

The supported set of routers is:

+
    +
  • AER – 1600/1650, 2100, 3100/3150
  • +
  • COR – IBR1100/1150, IBR900/IBR950, IBR600B/IBR650B, IBR350
  • +
  • ARC - CBA850
  • +
+

New routers products will support Python applications unless they are a special low-function, low-cost model.

+

Application Development

+

During development, an application can be directly installed into a 'DEV Mode' router. This makes it easier to debug and work through the development process. Once the application has been fully debugged and is ready for deployment, it can be installed via NCM at the group level.

+

SDK Toolset

+

Cradlepoint has a simplified SDK, written in python, which builds and creates an app package. The SDK, along with sample applications is located here.

+

For app development, the SDK is used to install, start, stop, uninstall, and check status of the application in a locally connected development router. The application package is the same for local debugging or for uploading to the NCM for production deployment. Application development can be done on Linux, OS X, and Windows operating systems with the same SDK.

+

This document is specifically written for SDN version 2.0 and above.

+

+

Developer Community

+

Cradlepoint has a Developer Community Portal to leverage knowledge, share, and collaborate with other developers. This forum is also actively monitored by Cradlepoint to answer questions.

+

+

Cradlepoint Knowledge Base

+

The existing Cradlepoint Knowledge Base also has many articles related to router applications and the SDK.

+

+

Router Python Environment

+

Application are written in python. However, the router only contains a subset of a typical python installation on a computer. The list of python modules in the router are listed here: Router FW 6.1.0 Modules. New python files can be added to you application but they must also adhere to this list.

+

+

Computer Setup Instructions

+

The SDK and sample apps can be downloaded from https://github.com/cradlepoint/sdk-samples. Below are the setup instruction for:

+ +

+

Linux

+
    +
  1. +

    Install python 3.5.1 from python.org.

    +
  2. +
  3. +

    Add Linux development libraries.

    +
    sudo apt-get install libffi-dev
    +sudo apt-get install libssl-dev
    +sudo apt-get install sshpass
    +
    + +
  4. +
  5. +

    Install python libraries.

    +
    sudo apt-get install python3-pip
    +pip3 install requests
    +pip3 install pyopenssl
    +pip3 install requests
    +pip3 install cryptography
    +
    + +
  6. +
  7. +

    Useful tools

    +

    PyCharm (community version is free): https://www.jetbrains.com/pycharm/download/#section=linux.

    +
  8. +
+

+

Mac OS X

+
    +
  1. +

    Install python 3.5.1 from python.org.

    +
  2. +
  3. +

    Install HomeBrew for package updates.

    +
    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    +
    + +
  4. +
  5. +

    Install openssl.

    +
    brew install openssl
    +
    + +
  6. +
  7. +

    Install python libraries.

    +
    pip3 install requests
    +pip3 install pyopenssl
    +pip3 install requests
    +pip3 install cryptography
    +
    + +
  8. +
  9. +

    Useful tools

    +

    PyCharm (community version is free): https://www.jetbrains.com/pycharm/download/#section=macOS.

    +
  10. +
+

+

Windows

+
    +
  1. Install python 3.5.1 from https://www.python.org/downloads/release/python-351/.
  2. +
  3. +The SDK requires some OpenSSL tools to generate digital signatures. + +
  4. +
  5. +

    Open a terminal window and use the following commands to install python libraries.

    +
    python -m pip install -U pip
    +python -m pip install -U pyserial
    +python -m pip install -U requests
    +python -m pip install -U pyopenssl
    +
    + +
  6. +
  7. +Useful tools +
      +
    1. Putty: http://www.putty.org/
    2. +
    3. PyCharm (community version is free): https://www.jetbrains.com/pycharm/download/#section=windows.
    4. +
    5. 7-zip: http://www.7-zip.org/
    6. +
    7. MarkdownPad: http://markdownpad.com/
    8. +
    +
  8. +
+

+

SDK/Apps Directory Structure

+

Below is the directory structure for for the SDK and sample applications. The BOLD items are modified or created by the developer. The other files are used by the SDK or are referenced by the other files.

+
    +
  • +Router_Apps (directory) +
      +
    • +app_name (directory) +
        +
      • package.ini - App initialization items.
      • +
      • app_name.py - The app python code. There can be multiple py files based on the app design.
      • +
      • cs.py - This is included with every sample app and should be in your app. It contains a CSClient class which is a wrapper for the TCP interface to the router config store (i.e. the router trees).
      • +
      • install.sh - Runs on app installation. (update with app name)
      • +
      • start.sh - Script that starts an app (i.e. cppython app_name.py start).
      • +
      • stop.sh - Script that stops an app (i.e. cppython app_name.py stop).
      • +
      +
    • +
    • +config (directory) +
        +
      • settings.mk - Build system config settings (i.e. Router MAC, IP, Username, Password, etc.).
      • +
      +
    • +
    • +common +
        +
      • cs.py - This is included with every sample app and can be copied into your app directory. It contains a CSClient class which is a wrapper for the TCP.
      • +
      +
    • +
    • +tools (directory) +
        +
      • +bin (directory) +
          +
        • package_application.py - Used by SDK.
        • +
        • validate_application.py - Used by SDK.
        • +
        • pscp.exe - An executable use on Windows by the SDK.
        • +
        +
      • +
      +
    • +
    • sdk_setting.ini - Used by the SDK and contains the settings for building the app and connecting to the local router.
    • +
    • Router_Application_Development_Guide.md
    • +
    • Router_APIs_for_Applications.md
    • +
    • Makefile_README.md
    • +
    +
  • +
+

Based on the sdk_setting.ini file, the SDK will build all files located in the app_name directory into a tar.gz package that can then been installed into the router. This installation is either directly into the router (if in DEV mode) or via NCM for grouped routers.

+

+

Application Package Anatomy

+

A router application package, which is a tar.gz archive, consists of a set of files that includes the python executable, start/stop scripts, initialization files, along with manifest and signature files. This package of files is built by the SDK base on the sdk_settings.ini. Some of these files, like the manifest and signature files, are created by the Make tool. Others are created by the application developer. Below are the example contents for a tar.gz archive created for a router application.

+
    +
  • +app_name (directory) +
      +
    • +METADATA (directory) +
        +
      • MANIFEST.json - Contains a file list along with hash signatures and other app the package initialization data.
      • +
      • SIGNATURE.DS - A signature file for the app package.
      • +
      +
    • +
    • app_name.py - The application python executable file.
    • +
    • cs.py - Another python file used by the app. There could be multiple python files depending on the application design.
    • +
    • package.ini - The package initialization data.
    • +
    • install.sh - The script run during installation.
    • +
    • start.sh - The script run when the app is started.
    • +
    • stop.sh - The script run when the app is stopped
    • +
    +
  • +
+

package.ini

+

This initialization file contains information and about the application and items that affect installation and execution. This information will stored in /status/system/sdk within the router config store for installed apps.

+

For example:

+
[hello_world]
+uuid=7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c
+vendor=Cradlepoint
+notes=Hello World Demo Application
+firmware_major=6
+firmware_minor=1
+restart=false
+reboot=true
+version_major=1
+version_minor=6
+auto_start=true
+
+ +
    +
  • +

    [hello_world]

    +

    This must contain the name of the application. In this example, hello_world is the application name.

    +
  • +
  • +

    uuid +

    +

    Every app must contain a universally unique identifier (UUID).

    +
  • +
  • +

    vendor

    +

    This is the vendor name for the app.

    +
  • +
  • +

    notes

    +

    Add notes to describe the app or anything else.

    +
  • +
  • +

    **firmware_major and firmware_minor **

    +

    This is the required router firmware version for the app. Not implemented at this time.

    +
  • +
  • +

    restart +If set to 'true', then the application will be restarted if it dies or is not running. If false, the router will not attempt to restart the application.

    +
  • +
  • +

    reboot +If set to 'true', the router will restart the application following a router reboot. Otherwise, it will not be restarted.

    +
  • +
  • +

    version_major and version_minor

    +

    This contains the app version. This must be incremented for any new production app used installed via NCM. It will not re-install the same version that already exist in the router.

    +
  • +
  • +

    auto_start

    +

    If set to 'true', the app will automatically start after installation.

    +
  • +
+

install.sh

+

This script is executed when the application is installed in the router. Typically it will just add logs for the installation.

+

For example:

+
#!/bin/bash
+echo "INSTALLATION hello_world on:" >> install.log
+date >> install.log
+
+ +

start.sh

+

This script is executed when the application is started in the router. It contains the command to start the python script and pass any arguments.

+

For example:

+
#!/bin/bash
+cppython hello_world.py start
+
+ +

stop.sh

+

This script is executed when the application is stopped in the router. It contains the command to stop the python script.

+

For example:

+
#!/bin/bash
+cppython hello_world.py stop
+
+ +

+

SDK Instructions Overview

+

The SDK includes a python make.py file which is compatible for all supported platforms. There is also a GNU Makefile which can only be used with Linux or OS X. Both perform the same actions which are noted below. However, there are minor setup differences between the two. Developers can choose the one they prefer. For usage instructions, see:

+ +

SDK actions are:

+

default (i.e. no action given): + Build and test the router reference app and create the archive file suitable for deployment to a router DEV mode or for uploading to NCM.

+

clean: + Clean all project artifacts. Entails execution of all "-clean" make targets.

+

package: + Create the app archive tar.gz file.

+

status: + Fetch and print current app status from the locally connected router.

+

install: + Secure copy the app archive to a locally connected router. The router must already be in SDK DEV mode via registration and licensing in NCM.

+

start: + Start the app on the locally connected router.

+

stop: + Stop the app on the locally connected router.

+

uninstall: + Uninstall the app from the locally connected router.

+

purge: + Purge all apps from the locally connected router.

+

+

Python SDK Usage

+

All SDK functions are contained in the make.py python file. While this executable is the same regardless of the workstation platform, the python command is not. Use the following python command based on your platform:

+
    +
  • +

    Linux or OS X:

    +
    python3
    +
    + +
  • +
  • +

    Windows:

    +
    python
    +
    + +
  • +
+

The command structure is:

+
python(3) make.py <action>
+
+ +

The make.py usage is as follows:

+
    +
  1. +

    Update the sdk_setting.ini file based on your needs.

    +

    Example:

    +
    [sdk]
    +app_name=ping
    +dev_client_ip=192.168.0.1
    +dev_client_username=admin
    +dev_client_password=44224267
    +
    + +
  2. +
  3. +

    Update the UUID in the package.ini file located in the app directory.

    +

    Example:

    +
    [ping]
    +uuid=dd91c8ea-cd95-4d9d-b08b-cf62de19684f
    +
    + +
  4. +
  5. +

    Build the application package.

    +
    python(3) make.py
    +
    + +
  6. +
  7. +

    Test connectivity with your router via the status target.

    +
    python(3) make.py status
    +{
    +    "data": {},
    +    "success": true
    +}
    +
    + +
  8. +
  9. +

    Install the application on your router.

    +
    python(3) make.py install
    +admin@192.168.0.1's password: 
    +hspt.tar.gz                          100% 1439     1.4KB/s   00:00    
    +Received disconnect from 192.168.0.1: 11: Bye Bye
    +lost connection
    +
    + +
  10. +
  11. +

    Get the application execution status from your router.

    +
    python(3) make.py status
    +{
    +    "data": {
    +        "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c": {
    +            "app": {
    +                "date": "2015-12-04T09:30:39.656151",
    +                "name": "hspt",
    +                "restart": true,
    +                "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +                "vendor": "Cradlebox",
    +                "version_major": 1,
    +                "version_minor": 1
    +            },
    +            "base_directory": "/var/mnt/sdk/apps/7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +            "directory": hspt",
    +            "filename": "dist/tmp_znv2t",
    +            "state": "started",
    +            "summary": "Package started successfully",
    +            "type": "development",
    +            "url": "file:///var/tmp/tmpg1385l",
    +            "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"
    +        }
    +    },
    +    "success": true
    +}
    +
    + +
  12. +
  13. +

    Uninstall the application from your router.

    +
    python(3) make.py uninstall
    +{
    +    "data": "uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +    "success": true
    +}
    +
    + +
  14. +
+

+

GNU Make SDK Usage

+

A GNU Makefile, for Linux or OS X, is also included with the SDK which can perform the same functions as the make.py file. The make targets are identical to the make.py actions. However, environment variable will need to be set in lieu of the sdk_setting.ini file.

+

The GNU make usage is as follows:

+
    +
  1. +

    Export the following variables in your environment.

    +
    APP_NAME - The name of your application.
    +APP_UUID - Each application must have its own UUID.
    +DEV_CLIENT_MAC - The mac address of your router.
    +DEV_CLIENT_IP  - The lan ip address of your router.
    +
    + +

    Example:

    +
    $ export APP_NAME=hello_world
    +$ export APP_UUID=616acd0c-0475-479e-a33b-f7054843c973
    +$ export DEV_CLIENT_MAC=44224267
    +$ export DEV_CLIENT_IP=192.168.20.1
    +
    + +
  2. +
  3. +

    Build the application package.

    +
    $ make
    +
    + +
  4. +
  5. +

    Test connectivity with your router via the status target.

    +
    $ make status
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X GET http://192.168.0.1/api/status/system/sdk | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": {},
    +    "success": true
    +}
    +
    + +
  6. +
  7. +

    Build, test, and install the application on your router.

    +
    $ make install
    +scp /home/sfisher/dev/sdk/hspt.tar.gz admin@192.168.0.1:/app_upload
    +admin@192.168.0.1's password: 
    +hspt.tar.gz                          100% 1439     1.4KB/s   00:00    
    +Received disconnect from 192.168.0.1: 11: Bye Bye
    +lost connection
    +
    + +
  8. +
  9. +

    Get application execution status from your router.

    +
    $ make status
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X GET http://192.168.0.1/api/status/system/sdk | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": {
    +        "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c": {
    +            "app": {
    +                "date": "2015-12-04T09:30:39.656151",
    +                "name": "hspt",
    +                "restart": true,
    +                "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +                "vendor": "Cradlebox",
    +                "version_major": 1,
    +                "version_minor": 1
    +            },
    +            "base_directory": "/var/mnt/sdk/apps/7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +            "directory": hspt",
    +            "filename": "dist/tmp_znv2t",
    +            "state": "started",
    +            "summary": "Package started successfully",
    +            "type": "development",
    +            "url": "file:///var/tmp/tmpg1385l",
    +            "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"
    +        }
    +    },
    +    "success": true
    +}
    +
    + +
  10. +
  11. +

    Uninstall the application from your router.

    +
    $ make uninstall
    +curl -s --digest --insecure -u admin:441dbbec \
    +                -H "Accept: application/json" \
    +                -X PUT http://192.168.0.1/api/control/system/sdk/action \
    +                -d data='"uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"' | \
    +                /usr/bin/env python3 -m json.tool
    +{
    +    "data": "uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c",
    +    "success": true
    +}
    +
    + +
  12. +
+

+

Router Syslog for Debugging

+

Application debugging is accomplished with the use of debug syslogs. However, the default logging level in the router is set to info so this will need to be changed to debug. It is also possible to send the router logs to a syslog server running on another device. For more information, see the Knowledge Base article 'Understanding Router Log Files'.

+

You can also view logs via CLI commands when logged into the router console. This console is available by logging into the router with Secure Shell (i.e. ssh) or by slecting the 'Device Console' from 'System > System Control > Device Options' in the router UI. The logs can be viewed or cleared with the following CLI commands:

+
log (displays logs in the terminal window)
+log -s <text> (search for logs that contain <text> and displays them)
+log -s -i <text> (search for logs that contain <text> but case insensitive)
+log clear (clears the log buffer)
+help log (display the log command options)
+
+ +

+

Router Development Mode

+

In order to install an application directly to the router without using NCM, the router must be placed in DEV mode. One would typically debug and test an application using DEV mode prior to using NCM for installation. DEV mode allows for quicker and easier testing and debugging. Instructions for setting up a router for DEV mode is in Knowledge Base article 'SDK Enable Developer Mode'.

+

+

NCM Application Deployment

+

NCM is used to securely deploy applications to routers at the group level. If an application tar.gz package is uploaded to NCM and then assigned to a router group, NCM will then securely download and install the application to the routers within the group. For security, the application files are not user accessible within NCM or routers. That is, one is not able to download the application from the router or NCM.

+

+

Sample Application Walk Through

+

Cradlepoint has provided several sample applications with the SDK which is located here. Any of these apps can be used as a starting point for your application. The application data structure is described here.

+

When using the SDK make.py file, be sure to invoke the proper python command based on your computer OS.

+
    +
  • +

    Linux or OS X:

    +
    python3
    +
    + +
  • +
  • +

    Windows:

    +
    python
    +
    + +
  • +
+

How to Run the Hello World Sample App

+
    +
  1. Download the SDK and sample apps from here.
  2. +
  3. Ensure your computer has been setup. See Computer Setup Instructions.
  4. +
  5. Connect the router to your computer. This can be done by connecting the LAN port of the router to the USB port of your computer via a USB to Ethernet adapter.
  6. +
  7. Ensure the router is in DEV Mode. See here.
  8. +
  9. Enable Debug logs in the router which is very helpful. See here
  10. +
  11. Open a terminal window.
  12. +
  13. Change directory to 'sample_apps'.
  14. +
  15. +

    Update the sdk_setting.ini file based on your needs and for the sample app you wish to run. The hello_world is a good app to test.

    +

    Example:

    +
    [sdk]
    +app_name=hello_world
    +dev_client_ip=192.168.0.1
    +dev_client_username=admin
    +dev_client_password=44224267
    +
    + +
  16. +
  17. +

    Verify router connectivity via:

    +
    $ python(3) make.py status
    +
    + +
  18. +
  19. +

    Create the application package

    +
    $ python(3) make.py 
    +
    + +
  20. +
  21. +

    Install the application package

    +
    $ python(3) make.py install
    +
    + +
  22. +
  23. +

    Check the application status to ensure it has started.

    +
    $ python(3) make.py status
    +
    + +
  24. +
  25. +

    Also check the logs in the router to ensure the application is creating 'Hello World' logs. In the router console use the 'log' command.

    +
  26. +
+
+

Published Date: 6-30-2017

+

This article not have what you need? Not find what you were looking for? Think this article can be improved? Please let us know at suggestions@cradlepoint.com.

+ + + + diff --git a/SDK_version_1.0_app_refactor.html b/SDK_version_1.0_app_refactor.html new file mode 100644 index 00000000..43537f4f --- /dev/null +++ b/SDK_version_1.0_app_refactor.html @@ -0,0 +1,327 @@ + + + +Convert_Apps_from_Open_Beta_SDK_to_this_SDK + + + + +

Converting Apps from the Open Beta SDK to the New SDK

+
+

Quick Links

+

Overview

+

Reuse the Open Beta SDK App Files

+

Re-factor the Open Beta SDK App Files

+

+

Overview

+

The 'Open BETA' Router SDK is being replaced with a more simplified SDK. This should decrease the SDK learning curve and allow app developers to focus more on their app functionality. Router application packages built using the Open Beta SDK or the New SDK will function in the router. The app package anatomy has not changes and both SDKs comply to this format. However, the directory code structure is different between the two SDKs. This means that the application files between the two SDK have some compatible issues. For example, the Open Beta SDK requires a setting.ini file and the New SDK requires a package.ini file.

+

This document will describe how to utilize the Open Beta SDK app files in the New SDK framework. There are two possibilities:

+
    +
  1. Reuse the Open Beta SDK App Files
  2. +
  3. Re-factor the Open Beta SDK App Files
  4. +
+

+

Reuse the Open Beta SDK App Files

+

The simplest way to utilize the Open Beta SDK application files with the New SDK framework is to:

+
    +
  1. Build the application package with the Open Beta SDK using the 'python make.py -m build <app path>' command.
  2. +
  3. Copy the uncompressed application files directory from the Open Beta SDK build directory to the New SDK applications directory.
  4. +
  5. The New SDK can now build the Open Beta SDK application package.
  6. +
+

However, working with the Open Beta SDK files with the New SDK could be a little more complicated due to their differences. For example, the settings.json file is used by the CradlepintAppBase object in app_base.py and the package.ini file is used by the FW in the router. There is also some redundant data in these files that must be kept in sync. One must keep this in mind if any of the redundant items are modified.

+

+

Re-factor the Open Beta SDK App Files

+

While reusing the python SDK app files with the GNU Make is an option, it can complicate future modification depending on the application design. It may be more beneficial to re-factor the python code. That is, remove any dependencies on any files in cp_lib. Classes and functions in cp_lib were written for convenience and SDK functionality. The New SDK has no dependencies on the cp_lib files.

+

In short, it is recommended to re-factor any existing Open Beta SDK application to be used with the New SDK. The cs.py file in the New SDK contains all the functions required to interface with the router config store (i.e. the config, status, state, and control router trees.) If your app from the Open Beta SDK uses other code from the cp_lib directory, this functionality should be ported to your app to remove the dependency.

+
+

Published Date: 5-9-2017

+

This article not have what you need? Not find what you were looking for? Think this article can be improved? Please let us know at suggestions@cradlepoint.com.

+ + + + diff --git a/app_template/app_template.py b/app_template/app_template.py new file mode 100644 index 00000000..018ddaaf --- /dev/null +++ b/app_template/app_template.py @@ -0,0 +1,67 @@ +""" +A Blank app template as an example +""" +try: + import sys + import traceback + import argparse + import cs +except Exception as ex: + cs.CSClient().log("app_template.py", 'Import failure: {}'.format(ex)) + cs.CSClient().log("app_template.py", 'Traceback: {}'.format(traceback.format_exc())) + sys.exit(-1) + +APP_NAME = "app_template" + + +def start_router_app(): + try: + cs.CSClient().log(APP_NAME, 'start_router_app()') + + except Exception as e: + cs.CSClient().log(APP_NAME, 'Something went wrong in start_router_app()! exception: {}'.format(e)) + raise + + return + + +def stop_router_app(): + try: + cs.CSClient().log(APP_NAME, 'stop_router_app()') + + except Exception as e: + cs.CSClient().log(APP_NAME, 'Something went wrong in stop_router_app()! exception: {}'.format(e)) + raise + + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + # Call the function to start the app. + start_router_app() + + elif command == 'stop': + # Call the function to start the app. + stop_router_app() + + except Exception as e: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + # The start.sh and stop.sh should call this script with a start or stop argument + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/app_template/cs.py b/app_template/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/app_template/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/app_template/install.sh b/app_template/install.sh new file mode 100644 index 00000000..e15c09b5 --- /dev/null +++ b/app_template/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION app_template on:" >> install.log +date >> install.log diff --git a/app_template/package.ini b/app_template/package.ini new file mode 100644 index 00000000..8d496abb --- /dev/null +++ b/app_template/package.ini @@ -0,0 +1,11 @@ +[app_template] +uuid=1687727c-fd6c-48ff-807d-34a32faaedf8 +vendor=Cradlepoint +notes=A blank Reference Application as an example +firmware_major=6 +firmware_minor=3 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true \ No newline at end of file diff --git a/app_template/start.sh b/app_template/start.sh new file mode 100644 index 00000000..d0e850ed --- /dev/null +++ b/app_template/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython app_template.py start diff --git a/app_template/stop.sh b/app_template/stop.sh new file mode 100644 index 00000000..910d5c24 --- /dev/null +++ b/app_template/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython app_template.py stop diff --git a/common/ReadMe.txt b/common/ReadMe.txt new file mode 100644 index 00000000..42d66d4a --- /dev/null +++ b/common/ReadMe.txt @@ -0,0 +1,6 @@ +Common Directory Contents +========================= +cs.py - A wrapper for the TCP interface to the router config store. Includes +functions for log generation, get, put, delete, and append items to config store, +and a function for sending alerts to the ECM. Copy this file to your application +directory and import into your application. \ No newline at end of file diff --git a/common/cs.py b/common/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/common/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/config/README.md b/config/README.md deleted file mode 100644 index 6e32d578..00000000 --- a/config/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# directory: ./config -## Router App/SDK sample applications - -This directory holds global things shared by most Router Apps - -## File: main.py - -This Python script is the default "Router App" start up routine, which make.py -will copy -into your build archive - unless you have your own 'main.py' file in your -application project directory. - -This main.py does a variety of start-up chores, including: - -* locate your settings.json, which make.py should place in the root of your -archive. -* delay until time.time() is valid (or a max delay has pasted) -* delay until WAN connection is true (or a max delay has pasted) -* locate, import, and run your RouterApp module (start your code) -* if your code exits, then it uses an optional delay to prevent rapid -task trashing. For example, it might delay until 30 seconds past the -time your app started. - -## File: settings.ini - -Holds common shared settings, such as how logging is handled, the IP and user -credentials for your local router, and main.py startup behavior. - -This file is required by make.py - -## File: settings.json - -A temporary file used by tools - do not edit, as any edits will be lost. -If it exists, it will have been created from various other settings.ini files. - -## File: target.ini - -An optional settings file use by the tools/target.py script, which is -designed to simplify testing on a PC with more than one router or interface. -It allows you to map a name (like IBR1100 or CBA850) to select one of N -interfaces, assign IP, and so on. diff --git a/config/settings.ini b/config/settings.ini deleted file mode 100644 index a7ca9ef6..00000000 --- a/config/settings.ini +++ /dev/null @@ -1,31 +0,0 @@ -; Global settings - -[logging] -level=debug -; name = silly_toes -; log_file = trace.txt -syslog_ip=192.168.35.6 -; syslog_ip=/dev/log -; syslog_port=514 -; syslog_facility=22 -pc_syslog=false - -[router_api] -user_name=admin -interface=ENet USB-1 -local_ip=192.168.35.1 -password=441b537e - -[application] -name=make -vendor=Sample Code, Inc. -firmware=6.1 -restart=true -reboot=true -auto_start=true - -[startup] -boot_delay_max=5 min -boot_delay_for_time=True -boot_delay_for_wan=True -exit_delay=30 sec diff --git a/gnu_apps/extensible_ui_ref_app/config/settings.mk b/config/settings.mk similarity index 91% rename from gnu_apps/extensible_ui_ref_app/config/settings.mk rename to config/settings.mk index 524e5ba7..bf6fa577 100644 --- a/gnu_apps/extensible_ui_ref_app/config/settings.mk +++ b/config/settings.mk @@ -9,8 +9,8 @@ ROOT := $(shell pwd) SHELL := $(shell which bash) TOOLS := $(abspath $(ROOT)/tools) -APP_NAME ?= hspt -APP_UUID ?= 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c +APP_NAME ?= email_app +APP_UUID ?= 616acd0c-0475-479e-a33b-f7054843c971 APP_ARCHIVE := $(abspath $(APP_NAME).tar.gz) APP_DIR := $(ROOT)/$(APP_NAME) diff --git a/config/target.ini b/config/target.ini deleted file mode 100644 index c1592b25..00000000 --- a/config/target.ini +++ /dev/null @@ -1,42 +0,0 @@ -; think of these as 'flavors' to apply to the settings.ini - -[AER3100] -; user_name=admin -; interface=ENet USB-1 -local_ip=192.168.30.1 -password=franklin805 - -[AER2100] -local_ip=192.168.21.1 -password=4416ec79 - -[AER1600] -local_ip=192.168.16.1 -password=441ec8f9 - -[IBR1100] -local_ip=192.168.1.1 -password=441b1702 - -[IBR1150] -interface=Local Addin ENet -local_ip=192.168.115.1 -password=441a819c - -[CBA850] -local_ip=192.168.85.1 -password=441e0173 - -[IBR600] -; older/not ruby -local_ip=192.168.5.10 -password=4413a421 - -[IBR600B] -local_ip=192.168.60.1 -password=44167976 - -[IBR350] -interface=ENet USB-1 -local_ip=192.168.35.1 -password=441b537e diff --git a/cp_lib/__init__.py b/cp_lib/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cp_lib/clean_ini.py b/cp_lib/clean_ini.py deleted file mode 100644 index bfbb11f5..00000000 --- a/cp_lib/clean_ini.py +++ /dev/null @@ -1,85 +0,0 @@ -import os -import shutil - - -DEF_INI_EOL = "\n" -DEF_BACKUP_EXT = ".BAK" - - -def clean_ini_file(ini_name, eol=None, backup=False): - """ - Given an INI file, walk through and 'clean up' as required. The primary changes: - 1) string unnecessary white space - 2) insure all lines end in consistent end-of-line (is OS default, unless user passes in another form) - 3) force all section headers to lower case - per spec, INI files are NOT case sensitive - 4) strip off leading or trailing empty/blank lines - 5) reduce sequential blank lines to one only - 6) note: the keys and values are NOT affected (at this time) - - :param str ini_name: relative path (with directories) to the INI file - :param str eol: optional end-of-line string. default is OS default, but user can pass in another - :param bool backup: T if you want to save the original file - :return: None - """ - if os.path.exists(ini_name): - # only do if it exists - print("Cleaning INI file {}".format(ini_name)) - - # read the original file, clean up various things - lines = [] - was_blank = False - file_han = open(ini_name, "r") - for line in file_han: - # go through and clean up any lines - line = line.strip() - if not len(line): - # then is blank line, if 2 in a row skip appending! - if was_blank: - continue - else: - was_blank = True - - elif line[0] == "[": - # then is section heading, since is not case sensitive, make lower case - line = line.lower() - was_blank = False - - elif line[0] == "#": - # make Python-like comments into INI standard comments - if line[1] != " ": - line = "; " + line[1:] - else: - line = ";" + line[1:] - was_blank = False - - # else, leave as is - lines.append(line) - file_han.close() - - try: - while len(lines[0]) == 0: - # strip off leading blank lines - lines.pop(0) - - while len(lines[-1]) == 0: - # strip off leading blank lines - lines.pop(-1) - - except IndexError: - # file was too short? Ehh, is okay. - return - - # optionally back up the original (mainly used during testing) - if backup: - shutil.copyfile(ini_name, ini_name + DEF_BACKUP_EXT) - - # rewrite the INI with the new cleaned and EOL-normalized data - if eol is None: - eol = DEF_INI_EOL - - file_han = open(ini_name, "w") - for line in lines: - file_han.write(line + eol) - file_han.close() - - return diff --git a/cp_lib/cp_email.py b/cp_lib/cp_email.py deleted file mode 100644 index bde77c05..00000000 --- a/cp_lib/cp_email.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Send a single email -""" - -# Required keys -# ['smtp_tls] = T/F to use TLS, defaults to True -# ['smtp_url'] = URL, such as 'smtp.gmail.com' -# ['smtp_port'] = TCP port like 587 - be careful, as some servers have more -# than one, with the number defining the security demanded. -# ['username'] = your smtp user name (often your email acct address) -# ['password'] = your smtp acct password -# ['email_to'] = the target email address, as str or list -# ['subject'] = the email subject - -# Optional keys -# ['email_from'] = the from address - any smtp server will ignore, and force -# this to be your user/acct email address; def = ['username'] -# ['body'] = the email body; def = ['subject'] - -EMAIL_REQUIRED_KEYS = ('smtp_url', 'smtp_port', 'smtp_tls', - 'username', 'password', 'email_to', 'subject') - -EMAIL_OPTIONAL_KEYS = ('email_from', 'body', 'logger') - - -def cp_send_email(sets): - """ - - :param dict sets: the various settings - :return: - """ - import smtplib - - for key in EMAIL_REQUIRED_KEYS: - if key not in sets: - raise KeyError('cp_send_email() requires ["%s"] key' % key) - - # handle the two optional keys - if 'email_from' not in sets: - sets['email_from'] = sets['username'] - if 'body' not in sets: - sets['body'] = sets['subject'] - - email_list = sets['email_to'] - # if isinstance(email_list, str): - # # if already string, check if like '["add1@c.com","add2@d.com"] - # email_list = email_list.strip() - # if email_list[0] in ("[", "("): - # email_list = eval(email_list) - - email = smtplib.SMTP(sets['smtp_url'], sets['smtp_port']) - # TODO handle ['smtp_tls'] for optional TLS or not - email.ehlo() - email.starttls() - email.login(sets['username'], sets['password']) - - assert isinstance(email_list, list) - for send_to in email_list: - if 'logger' in sets: - sets['logger'].debug("Send email to {}".format(send_to)) - - email_body = '\r\n'.join(['TO: %s' % send_to, - 'FROM: %s' % sets['email_from'], - 'SUBJECT: %s' % sets['subject'], '', - sets['body']]) - - # try: TODO - better understand hard & soft failure modes. - email.sendmail(sets['email_from'], [send_to], email_body) - - # except: - # logging.error("Email send failed!") - - email.quit() - return 0 diff --git a/cp_lib/cs_ping.py b/cp_lib/cs_ping.py deleted file mode 100644 index 9a4d1d8d..00000000 --- a/cp_lib/cs_ping.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -Issue a ping, via Router API control/ping -""" -import time -from cp_lib.app_base import CradlepointAppBase - -# for now, this does NOT work! -SUPPORT_COUNT = False - - -def cs_ping(app_base, ping_ip, ping_count=40, loop_delay=2.5): - """ - Issue a ping via Cradlepoint Router IP. ping_ip can be like - "192.168.35.6" or "www.google.com". - - return is a dictionary - - if ["status"] == "success", - - ["result'] is an array of lines (as shown below) - - ["transmitted" == 40 (from line "40 packets transmitted, ... ) - - ["received" == 40 (from line "... 40 packets received ... ) - - ["loss" == 40 (from line "... 0% packet loss) - - ["good" == 100 - ["loss"] - - if ["status"] == "error", ["result"] is likely the string explaining the - error, such as "Timed out trying to send a packet to 192.168.115.3" - - if ["status"] == "key_error", - - the cs_client response of GET "control/ping" lacked expected keys - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client, - :param str ping_ip: the IP or DNS name to ping - :param int ping_count: how many times to ping - :param float loop_delay: the delay in the GET loop - :return: - :rtype dict: - """ - - if not SUPPORT_COUNT and ping_count != 40: - app_base.logger.warning( - "PING 'count' not yet supported - forced to 40") - ping_count = 40 - - app_base.logger.info("Send a PING to {}".format(ping_ip)) - put_data = '{"host":"%s", "count": %d}' % (ping_ip, ping_count) - - # we want to PUT control/ping/start {"host": "192.168.115.6"} - result = app_base.cs_client.put("control/ping/start", put_data) - app_base.logger.info("Start Result:{}".format(result)) - - # an initial delay assume 1 second per ping_count - app_base.logger.info("Initial Delay for {} seconds".format(ping_count + 1)) - time.sleep(ping_count + 1) - - report = [] - while True: - result = app_base.cs_client.get("control/ping") - app_base.logger.info("Loop Result:{}".format(result)) - # first result should be something like: - # {'status': 'running', 'start': {'host': 'www.google.com'}, - # 'stop': '', 'result': 'PING www.google.com (216.58.193.100)'} - try: - value = result['status'] - except KeyError: - app_base.logger.error("PING has no ['status'], aborting") - return {"status": "key_error"} - - if value == "error": - # then some error occurred - return result - - if value == "": - # then the cycle is complete - break - - app_base.logger.info("PING Status:{}".format(value)) - - try: - value = result['result'] - except KeyError: - app_base.logger.error("PING has no ['result'], aborting") - return {"status": "key_error"} - - if value != "": - # then we are mid-cycle - lines = value.split('\n') - report.extend(lines) - - for line in lines: - app_base.logger.debug("{}".format(line)) - - if report[-1].startswith("round-trip"): - # then the cycle is complete - break - - # else still wait - for example, when we first start the - # ["status"] == "running" and ["result"] == "" - - # post delay if we didn't delay quite enough - app_base.logger.info("Loop for {} seconds".format(loop_delay)) - time.sleep(loop_delay) - - result = {"status": "success", "result": report} - - for line in report: - if line.find("packet loss") > 0: - # 40 packets transmitted, 40 packets received, 0% packet loss - # [0][1] [2] [3] [4] [5] [6] [7] [8] - value = line.split() - if value[2].startswith('transmit'): - result['transmitted'] = int(value[0]) - app_base.logger.debug("PING transmitted:{}".format( - result['transmitted'])) - if value[5].startswith('receive'): - result['received'] = int(value[3]) - app_base.logger.debug("PING received:{}".format( - result['received'])) - if value[6][-1] == '%': - result['loss'] = int(value[6][:-1]) - result['good'] = 100 - result['loss'] - app_base.logger.debug("PING loss:{}% good:{}%".format( - result['loss'], result['good'])) - - return result - -""" -PUT control/ping/start {"host": "192.168.115.6"}. - -Cyclically GET control/ping, watch ['status'] goes to 'running' (or 'error') -then eventually to 'done', then after repeated GET returns to '' - -I have seen ['status']: -= '' after all GETs done -= 'running' during pinging -= 'error' if -= 'sysstopped' if ?? - -If IP has no response - or Ip is unreachable: -{'start': {'host': '192.168.115.3'}, 'status': 'error', - 'result': 'Timed out trying to send a packet to 192.168.115.3'} - -The entire collection of ['result'] lines will be like (EOL = '\n') - -PING 192.168.115.6 (192.168.115.6) -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=0. time=1.080. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=1. time=0.900. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=2. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=3. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=4. time=1.020. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=5. time=0.960. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=6. time=1.000. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=7. time=1.000. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=8. time=0.980. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=9. time=1.000. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=10. time=1.040. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=11. time=0.980. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=12. time=0.960. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=13. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=14. time=1.080. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=15. time=1.000. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=16. time=1.000. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=17. time=1.040. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=18. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=19. time=0.900. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=20. time=12.440. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=21. time=0.940. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=22. time=0.900. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=23. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=24. time=1.020. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=25. time=0.940. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=26. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=27. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=28. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=29. time=0.900. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=30. time=0.940. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=31. time=0.900. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=32. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=33. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=34. time=1.020. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=35. time=0.940. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=36. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=37. time=0.940. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=38. time=0.920. ms -44 bytes from 192.168.115.6 (192.168.115.6): icmp_seq=39. time=0.900. ms ----Ping statistics--- -40 packets transmitted, 40 packets received, 0% packet loss -round-trip(ms) min/avg/max = 0.900/1.244/12.440 - -(eventually will be "") - -""" diff --git a/cp_lib/data/__init__.py b/cp_lib/data/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cp_lib/data/json_get_put.py b/cp_lib/data/json_get_put.py deleted file mode 100644 index 7ba24799..00000000 --- a/cp_lib/data/json_get_put.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Simple JSON GET and PUT. Geared towards JSON-RPC, but assumes nitty-gritty -details of the protocol were handled externally. -""" -import json - -from cp_lib.data.data_tree import get_item_value, put_item, \ - DataTreeItemNotFound - -JSON_VERSION = "2.0" - - -def jsonrpc_check_request(source, test_params=True): - """ - Given a JSONRPC request, confirm looks valid - - :param source: the JSON RPC request - :type source: str or bytes or dict - :param boot test_params: if True, confirm [params] exists - :return: - :rtype dict: - """ - if isinstance(source, bytes): - source = source.decode('utf-8') - - if isinstance(source, str): - source = json.loads(source) - - assert isinstance(source, dict) - - if "jsonrpc" not in source: - source["error"] = { - "code": -32600, - "message": "Invalid request - [jsonrpc] key missing"} - - elif source["jsonrpc"] != JSON_VERSION: - source["error"] = { - "code": -32600, - "message": "Invalid request - [jsonrpc] != {}".format( - JSON_VERSION)} - - elif "method" not in source: - source["error"] = { - "code": -32600, - "message": "Invalid request - [method] key missing"} - - elif not isinstance(source["method"], str): - source["error"] = { - "code": -32600, - "message": "Invalid request - [method] is not str"} - - else: - if test_params and "params" not in source: - source["error"] = { - "code": -32600, - "message": "Invalid request - [params] key missing"} - - return source - - -def jsonrpc_prep_response(source, encode=True): - """ - Given a JSONRPC response, pop out unwanted keyes, plus confirm - one and only one of the [error] or [result] keys - - :param dict source: the JSON RPC request - :param bool encode: if T, then convert to str() - :return: - :rtype dict: - """ - assert isinstance(source, dict) - - pop_list = [] - for key, value in source.items(): - # remove any unknown keys - if key not in ("jsonrpc", "id", "result", "error"): - pop_list.append(key) - - for key in pop_list: - # expect to pop out [method] and [params] - # print("Pop unwanted key:{}".format(key)) - source.pop(key) - - if "jsonrpc" not in source: - # should be here, but add if missing - source["jsonrpc"] = JSON_VERSION - - if "error" in source: - # we only want error or result, not both - if "result" in source: - raise KeyError("Cannot have both [error] and [result]") - - elif "result" not in source: - raise KeyError("Must have either [error] or [result]") - - if encode: - source = json.dumps(source) - - return source - - -def jsonrpc_get(base, source): - """ - Given a dict acting as data tree, fetch the item in - JSON RPC ["params"]["path"] - - :param dict base: the data tree to search - :param dict source: the parses json rpc request - :return: - """ - - # we ignore keys "jsonrpc", "method", and "id" - - if not isinstance(source, dict): - raise TypeError("jsonrpc source must be dict()") - - if "params" not in source: - raise ValueError('jsonrpc["params"] key missing') - - if "path" not in source["params"]: - raise ValueError('jsonrpc["params"]["path"] key missing') - - try: - result = get_item_value( - base, source["params"]["path"], throw_exception=True) - source["result"] = result - - except DataTreeItemNotFound: - source["error"] = {"code": -32602, "message": "path is not found"} - - # we 'key' into the messages, but we do NOT clean up the 'call' keys - return source - - -def jsonrpc_put(base, source): - """ - Given a dict acting as data tree, fetch the item in - JSON RPC ["params"]["path"] - - :param dict base: the data tree to search - :param dict source: the parses json rpc request - :return: - """ - - # we ignore keys "jsonrpc", "method", and "id" - - if not isinstance(source, dict): - raise TypeError("jsonrpc source must be dict()") - - if "params" not in source: - raise ValueError('jsonrpc["params"] key missing') - - if "path" not in source["params"]: - raise ValueError('jsonrpc["params"]["path"] key missing') - - try: - result = put_item( - base, path=source["params"]["path"], - value=source["params"]["value"], throw_exception=True) - source["result"] = result - - except DataTreeItemNotFound: - source["error"] = {"code": -32602, "message": "path is not found"} - - # we 'key' into the messages, but we do NOT clean up the 'call' keys - return source diff --git a/cp_lib/data/quality.py b/cp_lib/data/quality.py deleted file mode 100644 index ad3c2693..00000000 --- a/cp_lib/data/quality.py +++ /dev/null @@ -1,244 +0,0 @@ -# File: quality.py -# Desc: manage the quality bits, tags, and names - -__version__ = "1.0.0" - -# History: -# -# 1.0.0: 2015-Mar Lynn -# * initial draft, new design -# -# - -""" -The data samples: -""" - -# lowest 8 bits define 'BadData' - generally hardware errors, disabled etc -# such data is probably meaningless; for example a '0' reading of a broken -# sensor doesn't mean it is zero (0) degrees outside -QUALITY_NO_SUPPORTED = 0x00000000 # no support, value is meaningless - -QUAL_DISABLE = 0x00000001 # source is disabled -QUAL_FAULT = 0x00000002 # low-level HW fault; device specific -QUAL_OFFLINE = 0x00000004 # source is unavailable, offline, etc) -QUAL_NOT_INIT = 0x00000008 # the data has never been set -QUAL_OVER_RANGE = 0x00000010 # above permitted range-of-operation -QUAL_UNDER_RANGE = 0x00000020 # below permitted range-of-operation -QUAL_RES_BD = 0x00000040 # (reserved) -QUAL_VALID = 0x00000080 # status is valid if 1 -QUAL_BAD_DATA = 0x0000007F # these are internal data alarms - -QUAL_QUAL_UNK = 0x00000100 # data is of unknown quality -QUAL_QUAL_LOW = 0x00000200 # data is of known low quality -QUAL_MANUAL = 0x00000400 # at least 1 input is in Manual mode -QUAL_CLAMP_HIGH = 0x00001000 # was forced high due to system fault -QUAL_CLAMP_LOW = 0x00002000 # was forced low due to system fault -QUAL_SOSO_DATA = 0x00003700 # internal process 'conditions' - -QUAL_DIGITAL = 0x00010000 # digital NOT in desired state -QUAL_RES_AL = 0x00020000 # (reserved) -QUAL_LO = 0x00040000 # normal/expected low alarm (warning) -QUAL_LOLO = 0x00080000 # abnormal/unexpected too-low alarm (err) -QUAL_ROC_NOR = 0x00100000 # normal/expected rate-of-change alarm -QUAL_ROC_AB = 0x00200000 # abnormal/unexpected rate-of-change alarm -QUAL_HI = 0x00400000 # normal/expected high alarm (warning) -QUAL_HIHI = 0x00800000 # abnormal/unexpected too-high alarm (err) -QUAL_DEV_NOR = 0x01000000 # normal/expected deviation alarm (warning) -QUAL_DEV_AB = 0x02000000 # abnormal/unexpected deviation alarm (err) -QUAL_ALARMS = 0x0FFF0000 # These are External Process Alarms - -QUAL_ABNORM = 0x10000000 # first sample after go to alarm -QUAL_RTNORM = 0x20000000 # first sample after a return to normal -QUAL_RES_EV = 0x40000000 # (reserved) - -QUAL_ALL_BITS = 0x7FFFFFFF # handle py 3.x when all int or long - -QUALITY_GOOD = QUAL_VALID -QUALITY_BAD = QUAL_BAD_DATA - -QUALITY_DEFAULT_ALARM_BITS = (QUAL_FAULT | QUAL_OFFLINE | - QUAL_OVER_RANGE | - QUAL_UNDER_RANGE | QUAL_ALARMS) - -QUALITY_DEFAULT_EVENT_BITS = (QUAL_MANUAL | QUAL_RTNORM | - QUAL_ABNORM) - -QUALITY_TAG_TO_BIT = { - 'dis': QUAL_DISABLE, 'flt': QUAL_FAULT, - 'ofl': QUAL_OFFLINE, - 'ovr': QUAL_OVER_RANGE, 'udr': QUAL_UNDER_RANGE, - 'N/A': QUAL_NOT_INIT, 'n/a': QUAL_NOT_INIT, - 'unq': QUAL_QUAL_UNK, 'loq': QUAL_QUAL_LOW, - 'man': QUAL_MANUAL, 'clh': QUAL_CLAMP_HIGH, - 'cll': QUAL_CLAMP_LOW, 'dig': QUAL_DIGITAL, - 'low': QUAL_LO, 'llo': QUAL_LOLO, - 'roc': QUAL_ROC_NOR, 'rab': QUAL_ROC_AB, - 'hig': QUAL_HI, 'hhi': QUAL_HIHI, - 'dev': QUAL_DEV_NOR, 'dab': QUAL_DEV_AB, - 'abn': QUAL_ABNORM, 'rtn': QUAL_RTNORM, - 'ok': QUAL_VALID -} - -QUALITY_SHORT_NAME = { - QUAL_DISABLE: 'dis', QUAL_FAULT: 'flt', - QUAL_OFFLINE: 'ofl', - QUAL_OVER_RANGE: 'ovr', QUAL_UNDER_RANGE: 'udr', - QUAL_NOT_INIT: 'N/A', - QUAL_QUAL_UNK: 'unq', QUAL_QUAL_LOW: 'loq', - QUAL_MANUAL: 'man', QUAL_CLAMP_HIGH: 'clh', - QUAL_CLAMP_LOW: 'cll', QUAL_DIGITAL: 'dig', - QUAL_LO: 'low', QUAL_LOLO: 'llo', - QUAL_ROC_NOR: 'roc', QUAL_ROC_AB: 'rab', - QUAL_HI: 'hig', QUAL_HIHI: 'hhi', - QUAL_DEV_NOR: 'dev', QUAL_DEV_AB: 'dab', - QUAL_ABNORM: 'abn', QUAL_RTNORM: 'rtn', - QUAL_VALID: 'ok', -} - -QUALITY_FULL_NAME = { - QUAL_DISABLE: 'disabled', QUAL_FAULT: 'fault', - QUAL_OFFLINE: 'offline', QUAL_NOT_INIT: 'not-initialized', - QUAL_OVER_RANGE: 'over-range', - QUAL_UNDER_RANGE: 'under-range', - QUAL_QUAL_UNK: 'unknown-quality', - QUAL_QUAL_LOW: 'low-quality', - QUAL_MANUAL: 'manual', QUAL_CLAMP_HIGH: 'clamp-high', - QUAL_CLAMP_LOW: 'clamp-low', QUAL_DIGITAL: 'digital', - QUAL_LO: 'low', QUAL_LOLO: 'low-low', - QUAL_ROC_NOR: 'rate-of-change', - QUAL_ROC_AB: 'rate-of-change-abnorm', - QUAL_HI: 'high', QUAL_HIHI: 'high-high', - QUAL_DEV_NOR: 'deviation', - QUAL_DEV_AB: 'deviation-abnormal', - QUAL_ABNORM: 'go-abnormal', - QUAL_RTNORM: 'return-to-normal', - QUAL_VALID: 'status-valid', -} - - -def one_bit_to_tag(bit: int): - """Given a single bit-mask, return the tag/mnemonic""" - if bit in QUALITY_SHORT_NAME: - return QUALITY_SHORT_NAME[bit] - raise ValueError("one_bit_to_tag({0}) - bit matches no tag".format(bit)) - - -def one_bit_to_name(bit: int): - """Given a single bit-mask, return the long name""" - if bit in QUALITY_FULL_NAME: - return QUALITY_FULL_NAME[bit] - raise ValueError("one_bit_to_name({0}) - bit matches no tag".format(bit)) - - -def all_bits_to_tag(bits: int, long_name=False): - """Cycle through all bits, returning a tag/mnemonic string""" - - if not (bits & QUAL_VALID): - raise ValueError("all_bits_to_tag(0x%X) - lacks VALID bit tag" % bits) - - if bits == QUAL_VALID: - if long_name: - return "status-valid" - else: - return "ok" - - # else at least one bit is true, so continue - tag_string = '' - n = 1 - first = True - while n & QUAL_ALL_BITS: - if (bits & n) and (n != QUAL_VALID): - # then this bit is true, so add the tag - # but skip adding 'ok' tag! - if long_name: - tag = one_bit_to_name(n) - else: - tag = one_bit_to_tag(n) - if tag is not None: - if first: - first = False - tag_string = tag - else: - tag_string += ',' + tag - # cycle through all the bits. - # On 32-bit, will go zero after 0x80000000, so while cond not true - # On 64-bit/py 3.x, will go to 0x100000000, so while cond not true - n <<= 1 - return tag_string - - -def clr_quality_tags(quality: int, tag): - """ - Use the 3-ch tags to clear some bits - - :param int quality: the bits to mask in - :param tag: the short or tag name - :type tag: str, tuple, or list - :rtype: int - """ - if not isinstance(quality, int): - raise TypeError("clr_quality_tags() quality must be int type") - - quality |= QUAL_VALID - - if isinstance(tag, str): - # if a string was passed in, clear this one bit - quality &= ~tag_to_one_bit(tag) - - else: - try: - # if a list was passed in, we cycle through the list clearing - # each tag one by one - for one_tag in tag: - quality = clr_quality_tags(quality, one_tag) - - except: # any other situation is an error - raise TypeError("clr_quality_tags({0}) - invalid tags".format(tag)) - - return quality - - -def set_quality_tags(quality, tag): - """ - Use the 3-ch tags to set some bits - - :param int quality: the bits to mask in - :param tag: the short or tag name - :type tag: str, tuple, or list - :rtype: int - """ - if not isinstance(quality, int): - raise TypeError("set_quality_tags() quality must be int type") - - quality |= QUAL_VALID - - if isinstance(tag, str): - # if a string was passed in, clear this one bit - quality |= tag_to_one_bit(tag) - - else: - try: - # if a list was passed in, we cycle through the list setting - # each tag one by one - for one_tag in tag: - quality = set_quality_tags(quality, one_tag) - - except: # any other situation is an error - raise TypeError("set_quality_tags({0}) - invalid tags".format(tag)) - - return quality - - -def tag_to_one_bit(tag): - """ - Given string short (or tag) name, return alarm mask - :param str tag: the short or tag name - :rtype: int - """ - if isinstance(tag, str): - tag = tag.lower() - if tag in QUALITY_TAG_TO_BIT: - return QUALITY_TAG_TO_BIT[tag] - - raise ValueError("tag_to_one_bit({0}) - tag not valid".format(tag)) diff --git a/cp_lib/load_active_wan.py b/cp_lib/load_active_wan.py deleted file mode 100644 index 8a5dc587..00000000 --- a/cp_lib/load_active_wan.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -Load Router API "status/wan/devices/mdm-xx" -""" -from cp_lib.app_base import CradlepointAppBase, CradlepointRouterOffline - -KEY_NAME = 'name' -KEY_ACTIVE = "connected" -KEY_LIVE_GPS = 'gps' - - -class ActiveWan(object): - """ - fetch router's WAN data, allowing questions to be asked - - :param CradlepointAppBase app_base: - """ - def __init__(self, app_base): - self.app_base = app_base - self.data = None - self.refresh() - return - - def refresh(self): - self.data = fetch_active_wan(self.app_base) - return - - def get_imei(self): - """ - Scan for the first imei value. - - ['imei'] is IMEI string (or info.serial) - - :return: - """ - if self.data[KEY_ACTIVE]['imei'] not in (None, "", "unset"): - # assume 'active' if it seems valid - return self.data[KEY_ACTIVE]['imei'] - - for key, value in self.data.items(): - # else get first valid seeming one - if 'imei' in value and value['imei'] not in (None, "", "unset"): - return value['imei'] - - def supports_gps(self): - """ - Scan for the first wan claiming 'support_gps' - - ['supports_gps'] is T/F based on modem hw (or info.supports_gps) - - :return: - """ - if self.data[KEY_LIVE_GPS] not in (None, {}): - # then we have a fix - return True - - # slightly special - we might support GPS, but not yet have a fix - for key, value in self.data.items(): - # else get first valid seeming one - if value.get('supports_gps', False): - # then at least 1 claims support - return True - - # else, none claimed to support gps - return False - - def get_live_gps_data(self): - """ - return last-seen GPS data - - :return: - """ - return self.data[KEY_LIVE_GPS] - - -def fetch_active_wan(app_base, return_raw=False): - """ - Load Router API "/status/wan/devices" into settings. - - If return_raw=True, you'' receive the full dict returned via the - status API tree - - Else if return_raw=False, you receive a reduced dict. In the base level, - it will contain the same dict() as "/status/wan/devices", however - there will be 1 extra, as ["active"] which is an alias to the - modem currently serving as the uplink - - Example: - output['mdm-436abc4f'] = {"connection_state": "disconnected", ...} - output['mdm-43988388'] = {"connection_state": "connected", ...} - output['active'] = {"connection_state": "connected", ...} - - But remember that in this example, - output['mdm-43988388'] == output['active'] in every way - even - memory allocation, so changing output['mdm-43988388']['imei'] also - changes output['active']['imei'] !! - - Since the ['name'] key is added, you can see that - output['mdm-436abc4f']['name'] == 'mdm-436abc4f' - output['mdm-43988388']['name'] == 'mdm-43988388' - output['active']['name'] == 'mdm-43988388' - - :param CradlepointAppBase app_base: - :param bool return_raw: if True, return API data as-is, else REDUCE - :return dict: the merged settings - """ - import json - - assert isinstance(app_base, CradlepointAppBase) - - save_state = app_base.cs_client.show_rsp - app_base.cs_client.show_rsp = False - result = app_base.cs_client.get("/status/wan/devices") - app_base.cs_client.show_rsp = save_state - - if result is None: - raise CradlepointRouterOffline( - "Aborting - Router({}) is not accessible".format( - app_base.cs_client.router_ip)) - - if isinstance(result, str): - result = json.loads(result) - - if not return_raw: - # then 'reduce' complexity as defined in make_modem_dict() - - # the base section with have multiple modem and wired/wan sections. - # most cellular products have '2' modems, which is a fake design - # to support 2 SIM - wan_items = [] - for key, value in result.items(): - wan_items.append(key) - - # sort in-place, just to make life easier - wan_items.sort() - - output = dict() - - for modem in wan_items: - output[modem] = _make_modem_dict(modem, result[modem]) - - # the 'first' WAN we find which is 'connected' becomes - # output['active'] - if KEY_ACTIVE not in output: - if output[modem]['connection_state'].lower() == 'connected': - output[KEY_ACTIVE] = output[modem] - - # set up the GPS source - if KEY_LIVE_GPS not in output: - if output[modem].get('supports_gps', False): - # then we claim to support - if 'gps' in output[modem] and \ - len(output[modem]['gps']) > 0: - output[KEY_LIVE_GPS] = output[modem]['gps'] - - # we shouldn't change 'result' until out of the loop - if KEY_ACTIVE not in output: - output[KEY_ACTIVE] = None - if KEY_LIVE_GPS not in output: - output[KEY_LIVE_GPS] = None - result = output - - return result - - -def _make_modem_dict(modem_name, xdct): - """ - Given an API dict, plus modem name, create our simpler reduced dict. If - all goes well, it will contain: - ['imei'] is IMEI string (or info.serial) - ['supports_gps'] is T/F based on modem hw (or info.supports_gps) - ['tech'] is string like 'lte/3G' based on modem hw (or info.tech) - ['summary'] string like 'connected' or 'configure error' (status.summary) - ['connection_state'] string like 'disconnected' or 'connected' - (status.connection_state) - ['uptime'] None or float, seconds 'up' (status.uptime) - ['gps'] dict. Empty if no lock, else last data (status.gps) - ['ipinfo'] dict. Empty if no link, else cellular IP data (status.ipinfo) - - ['gps'] may be like: - {'nmea': - {'GPVTG': '$GPVTG,,T,0.0,M,0.0,N,0.0,K,A*0D\r\n', - 'GPGGA': '$GPGGA,183820.0,4500.815065,N,09320.092759,W,1,06,0.8, - 282.8,M,-33.0,M,,*6B\r\n', - 'GPRMC': '$GPRMC,183820.0,A,4500.815065,N,09320.092759,W,0.0,, - 110516,0.0,E,A*3D\r\n'}, - 'fix': - {'age': 45.68415999998978, - 'lock': True, - 'satellites': 6, - 'time': 183820, - 'longitude': {'minute': 20, 'second': 5.5644001960754395, - 'degree': -93}, - 'latitude': {'minute': 0, 'second': 48.902400970458984, - 'degree':45} - }} - - ['ipinfo'] may be like: - {'ip_address': '10.109.130.98' - 'netmask': '255.255.255.252', - 'gateway': '10.109.130.97', - 'dns': ['172.26.38.1', '172.26.38.2']} - - :param str modem_name: the official name - :param xdct: the dictionary at /status/wan/devices/modem_name - :return: - :rtype: dict - """ - result = dict() - result[KEY_NAME] = modem_name - result['path'] = "/status/wan/devices" + "/" + modem_name - - if "info" in xdct: - temp = xdct['info'] - if "serial" in temp: - if temp['serial'] in (None, "", "unset"): - result['imei'] = None - else: - result['imei'] = temp['serial'] - if "supports_gps" in temp: - result['supports_gps'] = temp['supports_gps'] - else: - result['supports_gps'] = False - if "tech" in temp: - result['tech'] = temp['tech'] - if "status" in xdct: - temp = xdct['status'] - if "summary" in temp: - result['summary'] = temp['summary'] - if "connection_state" in temp: - result['connection_state'] = temp['connection_state'] - if "uptime" in temp: - result['uptime'] = temp['uptime'] - if "gps" in temp: - result['gps'] = temp['gps'] - else: - result['gps'] = {} - if "ipinfo" in temp: - result['ipinfo'] = temp['ipinfo'] - - return result diff --git a/cp_lib/load_gps_config.py b/cp_lib/load_gps_config.py deleted file mode 100644 index 19aa36c1..00000000 --- a/cp_lib/load_gps_config.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -Load Router API "config/system/gps" settings. -""" - -from cp_lib.app_base import CradlepointAppBase, CradlepointRouterOffline - - -class GpsConfig(object): - """ - fetch router's GPS config, allowing questions to be asked - - :param CradlepointAppBase app_base: - """ - def __init__(self, app_base): - self.app_base = app_base - self.data = None - self.refresh() - return - - def refresh(self): - self.data = fetch_gps_config(self.app_base) - return - - def is_enabled(self): - """ - return if GPS is enabled. We say False if data is bad! - - :return bool: T/F - """ - if self.data is None: - return False - - return self.data.get("enabled", False) - - def keepalive_is_enabled(self): - """ - return if GPS Keepalive is enabled. - - Allow KeyError if our data is bad. Technically, do not call - this if self.is_enabled() == False - - :return bool: T/F - """ - return self.data.get("enable_gps_keepalive", False) - - def get_client_info(self): - """ - return the FIRST instance of a client (we send to server) - - Allow KeyError if our data is bad. Technically, do not call - this if self.is_enabled() == False - - :return bool: T/F - :rtype: None or (str, int) - """ - if "connections" in self.data: - for client in self.data["connections"]: - # test each sender / client - # self.app_base.logger.debug("client:{}".format(client)) - if "client" in client: - # then found gps/connections[]/client - if client.get("enabled", False): - # then found gps/connections[]/enabled == True - server_ip = client["client"].get("server", None) - server_port = client["client"].get("port", None) - if server_ip is not None and server_port is not None: - return server_ip, int(server_port) - - # if still here, we don't have, so return None - return None - - -def fetch_gps_config(app_base, return_raw=True): - """ - Load Router API "config/system/gps" and answer things about it. - - $ cat config/system/gps - { - "connections": [ - { - "_id_": "00000000-cb35-39e3-bc26-fd7b4f4c4a", - "client": { - "port": 9999, - "server": "192.168.35.6", - "time_interval": { - "enabled": false, - "end_time": "5:00 PM", - "start_time": "9:00 AM" - } - }, - "distance_interval_meters": 0, - "enabled": true, - "interval": 10, - "language": "nmea", - "name": "my pc", - "stationary_distance_threshold_meters": 20, - "stationary_movement_event_threshold_seconds": 0, - "stationary_time_interval_seconds": 0 - } - ], - "debug": { - "flags": 0, - "log_nmea_to_fs": false - }, - "enable_gps_keepalive": true, - "enable_gps_led": false, - "enabled": true, - "pwd_enabled": false, - "taip_vehicle_id": "0000" - } - - :param CradlepointAppBase app_base: - :param bool return_raw: if True, return API data as-is, else REDUCE - :return dict: the merged settings - """ - import json - - assert isinstance(app_base, CradlepointAppBase) - - save_state = app_base.cs_client.show_rsp - app_base.cs_client.show_rsp = False - result = app_base.cs_client.get("config/system/gps") - app_base.cs_client.show_rsp = save_state - - if result is None: - raise CradlepointRouterOffline( - "Aborting - Router({}) is not accessible".format( - app_base.cs_client.router_ip)) - - if isinstance(result, str): - result = json.loads(result) - - if return_raw: - # for future use, and common module - pass - - return result diff --git a/cp_lib/load_settings_ini.py b/cp_lib/load_settings_ini.py deleted file mode 100644 index 6807a026..00000000 --- a/cp_lib/load_settings_ini.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Simple Load settings, assume is JSON only. Follow this logic: -1) if settings.json exists in root directory, load it and STOP -2) else -2a) load ./config/settings -2b) load ./{project}/settings -""" -import json -import logging -import os -import sys - -from cp_lib.load_settings_json import DEF_GLOBAL_DIRECTORY, \ - DEF_SETTINGS_FILE_NAME, DEF_JSON_EXT, SECTION_NAME_LIST, \ - SECTION_APPLICATION, SECTION_LOGGING, SECTION_ROUTER_API -from make import EXIT_CODE_BAD_FORM - -DEF_INI_EXT = ".ini" -DEF_SAVE_EXT = ".save" - -# set to None to suppress adding comments to the settings.json files -ADD_JSON_COMMENT_KEY = "_comment" - - -def load_sdk_ini_as_dict(app_dir_path=None, file_name=None): - """ - Follow Router SDK Design: - 1) first load ./config/settings.ini - if it exists - 2) second load /{project}/settings.ini - if it exists, and smartly - merge into .config values - - :param str app_dir_path: the base directory of our project, like network/tcp_echo/ - :param str file_name: pass in alternative name - mainly for testing, else use DEF_FILE_NAME - :return dict: - """ - if file_name is None: - file_name = DEF_SETTINGS_FILE_NAME - - # start by loading the globals or ./config/settings.ini - ini_name = os.path.join(DEF_GLOBAL_DIRECTORY, file_name + DEF_INI_EXT) - logging.debug("Load Global Settings from {}".format(ini_name)) - _sets = load_ini_as_dict(ini_name) - - if app_dir_path is not None: - ini_name = os.path.join(app_dir_path, file_name + DEF_INI_EXT) - if os.path.exists(ini_name): - # load the app-specific settings, manually handle file - avoid potential path issues .. - logging.debug("Load App Project Settings from {}".format(ini_name)) - _sets = load_ini_as_dict(ini_name, _sets) - else: - logging.debug("There is no settings file {}".format(ini_name)) - - return _sets - - -def load_ini_as_dict(ini_name, pre_dict=None): - """ - - Locate and read a single INI - - force our expected sections headers to lower case - - if pre_dict exists, walk through and smartly 'merge' new data over old - - :param str ini_name: relative directory path to the INI file (which may NOT exist) - :param dict pre_dict: any existing settings, which we want new INI loaded data to over-write - :return dict: the prepared data as dict - """ - import configparser - - if not os.path.isfile(ini_name): - # if this INI file DOES NOT exist, return - existence is not this - # module's responsibility! - # logging.debug("INI file {} does NOT exist".format(ini_path)) - return pre_dict - - # LOAD IN THE INI FILE, using the Python library - config = configparser.ConfigParser() - # READ indirectly, as config.read() tries to open cp_lib/config/file.ini, - # not config/file.ini - file_han = open(ini_name, "r") - try: - config.read_file(file_han) - - except configparser.DuplicateOptionError as e: - logging.error(str(e)) - logging.error("Aborting MAKE") - sys.exit(EXIT_CODE_BAD_FORM) - - finally: - file_han.close() - # logging.debug(" Sections:{}".format(config.sections())) - - # convert INI/ConfigParser to Python dictionary - settings = {} - for section in config.sections(): - - section_tag = section.lower() - if section_tag not in SECTION_NAME_LIST: - # we make sure 'expected' sections are lower case, otherwise - # allow any case-mix - # example: [application] must be lower-case, but [TcpStuff] is fine - section_tag = section - - settings[section_tag] = {} - # note: 'section' is the old possibly mixed case name; section_tag - # might be lower case - for key, val in config.items(section): - settings[section_tag][key] = val - - # add OPTIONAL comments to file; set ADD_JSON_COMMENT_KEY=None to disable - if ADD_JSON_COMMENT_KEY is not None: - comments = { - SECTION_APPLICATION: "Settings for the application being built.", - SECTION_LOGGING: "Settings for the application debug/syslog/logging function.", - SECTION_ROUTER_API: "Settings to allow accessing router API in development mode." - } - for section in comments: - if section in settings: - settings[section][ADD_JSON_COMMENT_KEY] = comments[section] - - if pre_dict is None: - # no pre-existing data, then return as-is - return settings - - # else smartly merge new data into old, not 'replacing' sections - assert isinstance(pre_dict, dict) - for key, data in settings.items(): - if key in pre_dict: - # update/merge existing section (not replace) - pre_dict[key].update(data) - else: - # set / replace new sections - pre_dict[key] = data - - return pre_dict - - -def copy_config_ini_to_json(): - # copy the globals or ./config/settings.ini to ./config/settings.json - name = os.path.join(DEF_GLOBAL_DIRECTORY, DEF_SETTINGS_FILE_NAME + DEF_INI_EXT) - logging.debug("Copy Global Settings from INI to JSON ") - _sets = load_ini_as_dict(name) - - name = os.path.join(DEF_GLOBAL_DIRECTORY, DEF_SETTINGS_FILE_NAME + DEF_JSON_EXT) - save_root_settings_json(_sets, name) - return - - -def save_root_settings_json(sets, file_name=None): - """ - - :param sets: - :param file_name: - - :param dict sets: the settings as Python dict - :param str file_name: pass in alternative name - mainly for testing, else use DEF_FILE_NAME - :return: - """ - if file_name is None: - file_name = DEF_SETTINGS_FILE_NAME + DEF_JSON_EXT - - logging.info("Save settings to {}".format(file_name)) - lines = json.dumps(sets, indent=4, sort_keys=True) - file_han = open(file_name, 'wb') - for line in lines: - # cooked = line + '\n' - file_han.write(line.encode()) - file_han.close() diff --git a/cp_lib/modbus/__init__.py b/cp_lib/modbus/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/cp_lib/modbus/readme.md b/cp_lib/modbus/readme.md deleted file mode 100644 index f528a0c6..00000000 --- a/cp_lib/modbus/readme.md +++ /dev/null @@ -1,54 +0,0 @@ -# directory: ./cp_lib/modbus -## Simplistic transaction parsing for Modbis - -These files allow simple conversion between Modbus/TCP, Modbus/RTU, and Modbus/ASCII. - -This is actually pretty easy, as they all enclose the same "ADU" message. For example, the block read of 10 coils starting at the 2nd (data[1]) offset is: - -* Modbus/RTU = b'**\x01\x01\x00\x01\x00\x0A**\x44\x2A' -* Modbus/ASCII = b':**01010001000A**66\r\n' -* Modbus/TCP = b'\x00\xA5\x00\x00\x00\x06**\x01\x01\x00\x01\x00\x0A**' - -Notice that they all include the 6-byte value **01 01 00 01 00 0A**! So conversion is very mechanical. - -## File: __init__.py - -Empty - exists to define cp_lib/modbus as a module. - -## File: transaction.py ## - -Defines a class IaTransaction(), which supports splitting up a request and releated response, plus some timing and routing details. - -*Unit testing is in test/test\_cplib\_modbus\_trans\_modbus.py* - -## File: transaction\_modbus.py ## - -Subclasses IaTransaction() as ModbusTransaction(). - -You can use as follows: - -* create the ModbusTransaction() -* call obj.set\_request(data, 'mbtcp'), which causes the Modbus/TCP request to be parsed into the raw Modbus/ADU format. -* call obj.get\_request(data, 'mbrtu'), which causes the raw Modbus/ADU data to be recombined to be Modbus/RTU. - -The set\_response()/get\_response() work in the same manners, so obj.set\_response(data, 'mbrtu') would break up the RTU response, and obj.get\_response(data, 'mbtcp') would reform as Modbus/TCP, including the correct header adjustments. - -*Unit testing is in test/test\_cplib\_modbus\_trans\_modbus.py* - -## File: modbus\_asc.py ## - -The checksum, encode/decode, and end-of-message routines for Modbus/ASCII. - -*Unit testing is in test/test\_cplib\_modbus\_asc.py* - -## File: modbus\_rtu.py ## - -The checksum, encode/decode, and end-of-message routines for Modbus/RTU. Since RTU lacks any formal 'length' test, it also includes routines to estimate size based on Modbus command form. - -*Unit testing is in test/test\_cplib\_modbus\_rtu.py* - -## File: modbus\_tcp.py ## - -The checksum, encode/decode, and end-of-message routines for Modbus/TCP. - -*Unit testing is in test/test\_cplib\_modbus\_tcp.py* \ No newline at end of file diff --git a/cp_lib/readme.md b/cp_lib/readme.md deleted file mode 100644 index ee0409e8..00000000 --- a/cp_lib/readme.md +++ /dev/null @@ -1,130 +0,0 @@ -# Lynn's Sample Design - -Any sample in the system must be created from a sample template. Example templates: - -- Single Temperature Value, with Deg F as unit of measure -- An average Temperature, with an attached time period - such as the average temperature in degree F for the hour from -2:00PM to 2:59PM -- The min/max/avg Temperature, with an attached time period -- The temperatures can be define as +/- 0.5 (so like 70 F only), or like +/- 0.1 (so like 70.2 F), which allows more -accurate packing for transport as XML or JSON. - -This allows more flexible samples data, for example the template can include the ability to convert deg C to deg F. -Assuming the sample includes a reference to the template (instead of the units string), the individual instance -samples are no larger than the old DIA form - -# Files: - -## DataCore -data_core.py holds the base object, and is the tree/handle node for the object. It has no real data, but does include the child/tree structures. It includes attributes: - -- [**data\_name**]: str, is the NON-unique name for this object - example 'level' or 'outdoor', which may be used by other objects -- [**class**]: str, defines how the object can be processed -- [**index**]: int, a simple unique counting number, but the value MAY vary between runs. Technically, it is the order the objects are created during the current run-time. -- [**path**]: str, is the unique name such as 'tank01.level' -- **PATH_SEP**:str, is the symbol used within the path. Default is '.' to avoid the special meanings of a '/' or '\'. -- [**role**]: str, is a subset of 'class', allowing shared processing handlers - -Special internal values include: - -- **\_parent**: DataCore, handle of my parent instance -- **\_children\_list**: list, an unordered list of DataCore objects -- **\_children\_keyed**: dict, keyed on the data_name, so for example in the path example above, the node 'tank01' would have a child named 'level' -- **\_private**: bool, if True, then this is an internal hidden node -- **\_template**: None or DataTemplate reference, is a special shared handler used to, for example, allow a dozen data objects to 'share' data validation and/or alarm processing. - -## DataObject -data_object.py holds the next higher level of data, which are constrained to a specific list of types, including: - -- **base**: *DataObject*, with no data, but might be a parent/container for children. For example a 'tank' may have no data, as one needs to look at the 'level' or 'empty_alert' children. -- **string**: *StringObject* with a user-friendly UTF8 string value, with no other encoding or meaning. For example, a 'tank' may have a contents such as 'aviation fuel grade 2'. -- **digital**: *BooleanObject* is a True/False object with optional alarm handling. -- **analog**: *NumericObject* is a float which may include optional precision filters to force to int or X.0, etc. There is no true in, since most (all?) system input/output shall be JSON, which has a common 'numeric'. -- **gps**: *GpsObject* is a special case object, It rarely will be used by itself, but can be used to 'tag' data samples tied to a GPS locations, such as a police car's lightbar being turned True/On at a specific location and time. - -A base DataObject has the common attributes shared by ALL the other DataObjects, including these static values: - -- [**data\_name**]: *str*, is unique to the 'parent' collection of children. This may NOT the same name as the DataCore's ['data_name']. It is 'indexable', so not UTF8. -- [**display\_name**]: *UTF8*, user-defined display name, without constraints or rules. -- [**description**]: *UTF8*, user-defined descriptive string, without constraints or rules. -- [**display\_color**]: DataColor, has no value in Router, but can be used to manage font & back ground color in simple web or UI displays. The DataColor is a common HTML/CSS sRGB tuple -- [**font\_size**]: float, a simple scaling in (50%, 75%, 100%, 125%, 150%). It is NOT intended to format web pages, but to compensate for UTF8 character sets which may appear unusually small or large on a traditional English-oriented UI. -- [**data\_type**]: str, selects the type, such as digital or analog. -- [**value\_type**]: type, Python info about the data value. -- [**failsafe**]: ??, the startup or fault value - a 'safe' value when the true value is unknown. -- [**attach\_gps**]: bool, T/F, if DataSample time-series data should have GPS info attached. Default is False, so do not attach. -- [**read\_only**]: bool, T/F, if DataObject should not change in real-time. - -The following values may be dynamic: - -- [**value**]: DataSample, None or the appropriate data value. -- [**source**]: ??, defines the source of non-read\_only data. -- [**owner\_token**]: str, a user-defined 'lock' to writing of new data values. -- [**health**]: float, 0-100% for data health. - -## StringObject -See **StringAttributeHandler** in data_attribute.py. A StringObeject has no data attributes, but the [**value**] attribute will be a UTF8 user-friendly string. Many strings will be read\_only, however some might outputs from various real-time state machines such as "I/O is within expected range" or "I/O is wildly bad". - -## DigitalObject -See **BooleanAttributeHandler** in data_attribute.py. A DigitalObeject adds the following attributes. - -- [**display\_name\_true**]: *UTF8*, user-defined display 'tag' which matches [value] = True. For example, it might be 'normal', 'open', or 'snowing'. -- [**display\_name\_false**]: *UTF8*, like [display\_name\_true], but when [value] = False. For example, it might be 'too hot', 'closed', or 'sunny'. -- [**display\_color\_true**]: DataColor, how to display [value] when True. The DataColor is a common HTML/CSS sRGB tuple, and perhaps the background should be white when True, and red when False. -- [**display\_color\_false**]: DataColor, like [display\_color\_true], but when [value] is False. -- [**invert**]: bool, T/F if any 'set' of [value] should be inverted. Default is False or not-present, so no inversion. -- [**abnormal\_when**]: bool, when [value] matches this value, then based upon the application context, this data object is abnormal, in alarm, or not-as-desired. *Note that this does NOT drive alarms or alerts!* It is instead a HINT which helps a consuming-agent to understand good-verse-bad states. Default is False or not-present. -- [**auto\_reset**]: bool, defines if 'repeated' data is new or old. For example, the source defines a new *restart = True* event, when the existing value is True. If auto\_reset is False (default), then the 'repeat' is treated as a redundant or refresh, so may not propagate. If auto\_reset is True, then the 'repeat' is treated as new data and propagated. -- [**delay\_t\_f**]: numeric, seconds to delay (or debounce) a transition from True to False. For example, you might want a door-closed value = True to remain True unless the door is open longer than 10 seconds. -- [**delay\_f\_t**]: numeric, like [delay\_t\_f], but when [value] = False, delay going True. -- TBD - add time of last go-true and last go-false? - -## NumericObject -See **NumericAttributeHandler** in data_attribute.py. A NumericObject adds the following attributes. - -- [**uom**]: *UTF8*, user-defined unit-of-measure such as 'F' or 'miles'. -- TBD - add min/max/avg option? - -## GpsObject -See **GpsAttributeHandler** in data_attribute.py. A GpsObject adds the following attributes (* TBD *). I am still considering. It needs to include the GPS health, as well as lat/long, possibly speed & others. - -## DataTemplate -data_template.py holds shared objects for use with DataObjects and DataSamples. The template does 2 basic functions: - -1. supplies a shared template, such as 'uom' (unit-of-measure) as Gallons for 10 different tank.level DataObjects and related DataSamples. -2. includes a process_value() function which validates a new data value is of the correct type, within permitted ranges, and rounded to a reasonable precision. - -Each DataCore instance can contain a reference to a DataTemplate, although the values are usually copied out into the Data object. For example, if the template has a analog 'failsafe' of 999, this will be copied to the failsafe of a AnalogObject. This means changes to the Template may (TBD) need to manually updated in objects. - -Each DataSample instance also can contain a reference to a DataTemple, and is generally treated as an active helper. For example, if the template has a 'uom' of 'F', this will be fetched on-demand to create a JSON record for export or display. This means changes to the Template automatically cause changes going forward - such as changign a temperature from 'F' to 'C'. - -## DataSample -data_sample.py holds granular time-series data attached to a DataObject. For example, a DigitalObject named 'back\_door' might have a current status, which is False/'open', but it might also hold the last 10 times the door was opened or closed as a small history. These 10 values are called Data Samples, and include these attributes: - -- [**value**] required, is the data value of an appropriate type, such as True or 73.45. -- [**timestamp**] required, is the UTC time() of the data recorded. -- [**quality**] optional, is a numeric bit-mask of status, such as disabled, low-alarm, go-abnormal, and so on. If missing, it is assumed quality is valid. -- **get_uom()** optional, and fetches a UTF8 string from a template. In a JSON sample this will be ['uom']. For Digital data, this will return the appropriate 'display_name_true' or 'display_name_false'. For string or GPS data, it returns None. -- **get_gps()** optional, and fetches ... something (TBD). Returns None if GPS data is not being attached, else it maybe a JSON sub-item or CSV. Format to TBD. -- TBD - how to handle a **stats min/max/avg** value? Is it just a 3-item tuple? - -## DataColor -data_color.py holds a standard HTML/CSS color or color-pair. Generally this is assumed to be what's called CSS3 (see [http://www.w3.org/TR/css3-color/](http://www.w3.org/TR/css3-color/ "CSS Color Module Level 3")). - -It uses the PyPi WebColors module, so can be set with far greater flexibility than I expected to be used! (see [http://pypi.python.org/pypi/webcolors/](http://pypi.python.org/pypi/webcolors/ "webcolors 1.5"). - -In general, it assumes the colors are set via string values, such as 'orchid' or '#da70d6'. Internally, the values will be retained as numeric, but you can fetch the vales as HEX strings, or as CSS3 names if there is a match. Given the flexibility of the Python webcolors module, it should be fairly easy to extend it to handle other notations like RGB percentages. - -A DataColor value can be one of three things: - -- **None**, which means no-effect or as default. -- a **single value**, which is assumed the BACKGROUND value, with the fore/font color remaining as default. So for example, a data value might have background colors of 'green', 'yellow', or 'red' based on an alarm status, always assuming the font remains default such as 'black'. -- a **2-value tuple**, which is assumed (BACKGROUND, FONTCOLOR) such as ('white','navy') for Navy blue text on a white background. - -The router generally ignores a DataColor, but it will be used for any local UI or when sent for remove UI display. - -## DataSetting (TBD) -data_setting.py are special DataObjects which are intended to be saved and restored between runs. - -## DataAlarm (TBD) -data_alarm.py are special filtering processors which validate changes in DataObjects. diff --git a/cp_lib/simple_data.py b/cp_lib/simple_data.py deleted file mode 100644 index 7d187362..00000000 --- a/cp_lib/simple_data.py +++ /dev/null @@ -1,59 +0,0 @@ -import time - -import cp_lib.data.quality as qual - - -class SimpleData(object): - - def __init__(self, value=None, uom=None, now=None, quality=None): - self.value = None - self.unit = None - self.time = None - self.quality = None - - if value is not None: - self.set_value(value, uom, now, quality) - return - - def __repr__(self): - """ - make a fancy string output - :return: - """ - st = str(self.value) - - if self.unit is not None: - st += ' (' + self.unit + ')' - - if self.quality is not None and self.quality != qual.QUALITY_GOOD: - st += ' !Quality:%s' % qual.all_bits_to_tag(self.quality) - - if self.time is not None: - st += ' (%s)' % time.strftime("%Y-%m-%d %H:%M:%S", - time.localtime(self.time)) - return st - - def set_value(self, value, uom=None, now=None, quality=None): - """ - Set the value, with time and quality - - :param value: - :param str uom: - :param float now: - :param int quality: - :return: - """ - self.value = value - self.unit = uom - - if now is None: - self.time = time.time() - else: - self.time = now - - if quality is None: - self.quality = qual.QUALITY_GOOD - else: - self.quality = (qual.QUALITY_GOOD | quality) - - return diff --git a/cp_lib/status_tree_data.py b/cp_lib/status_tree_data.py deleted file mode 100644 index 5c71dcb8..00000000 --- a/cp_lib/status_tree_data.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Read/Write our data in status tree ( /status/system/sdk/apps[0] ) -""" -import json - -from cp_lib.app_base import CradlepointAppBase - - -class StatusTreeData(object): - - def __init__(self, app_base): - """ - - :param CradlepointAppBase app_base: resources: logger, settings, etc - """ - assert isinstance(app_base, CradlepointAppBase) - self.base = app_base - - # start out, we don't know our data - self.slot = None - self.uuid = None - - self.data = dict() - self.clean = False - return - - @staticmethod - def get_url_sdk(): - """Return the URL in status tree for full SDK data""" - return "status/system/sdk" - - def get_url_app_slot(self): - """Return the URL for one specific APPS within status tree""" - return "status/system/sdk/apps/{}".format(self.slot) - - def get_url_app_data(self): - """Return URL for one specific APPS['usr_data'] within status tree""" - return "status/system/sdk/apps/{}/usr_data".format(self.slot) - - def set_uuid(self, uuid): - """ - - :param uuid: the UUID to locate our apps[] slot - """ - assert isinstance(uuid, str) - - result = self.base.cs_client.get(self.get_url_sdk()) - if not isinstance(result, dict): - raise ValueError("SDK status tree not valid") - - # tree always has at least 'apps': [] (empty list) - if 'apps' not in result: - raise KeyError("SDK status tree, lacks ['apps'] data") - - if len(result['apps']) < 1: - raise ValueError("No APPS installed in SDK status tree") - - # force back to None - self.slot = None - self.uuid = None - - try_index = 0 - for app in result['apps']: - if "_id_" in app: - data = string_list_status_apps(try_index, app) - for line in data: - self.base.logger.debug("{}".format(line)) - if uuid == app["_id_"]: - self.slot = try_index - break - - try_index += 1 - - if self.slot is None: - # then NOT found! - raise ValueError("UUID is not installed on router!") - - self.base.logger.debug("Found UUID in ['apps'][{}]".format(self.slot)) - self.uuid = uuid - return - - def set_data_value(self, tag, value): - """ - Set some value into our data block - - :param str tag: - :param value: - :return: - """ - if tag in self.data: - # then already here - if self.data[tag] == value: - # no change - return False - - self.data[tag] = value - self.clean = False - return True - - def clear_data(self): - """ - Delete our data from router, start clean with nothing here - - :return: - """ - result = self.base.cs_client.delete(self.get_url_app_data()) - self.base.logger.debug("DATA={}".format(result)) - self.data = dict() - self.clean = True - return True - - def get_data(self): - """ - Set some value into our data block - - :return: - """ - result = self.base.cs_client.get(self.get_url_app_data()) - if result is None: - self.base.logger.debug("DATA is empty") - self.data = dict() - else: - self.data = result - self.base.logger.debug("DATA={}".format(result)) - return True - - def put_data(self, force=False): - """ - Set some value into our data block - - :param bool force: if T, write even if self.Clean - :return: - """ - if force or not self.clean: - # then need to write our data - if self.data is None: - # then remove ['apps'][slot] from router - raise NotImplementedError - - elif len(self.data) == 0: - # then is empty item - result = self.base.cs_client.put( - self.get_url_app_data(), "{}") - - else: - # else has some data - data = json.dumps(self.data) - result = self.base.cs_client.put( - self.get_url_app_data(), data) - - self.base.logger.debug("RSP={}".format(result)) - self.clean = True - - return True - - -def string_list_status_apps(index, one_app, all_data=False): - """ - Given STATUS return from Router, Make a list of strings to show ONE - entry in APPS array value: - { - "_id_": "ae151650-4ce9-4337-ab6b-a16f886be569", - "app": { - "date": "2016-04-15T21:58:30Z", - "name": "do_it", - "restart": false, - "uuid": "ae151650-4ce9-4337-ab6b-a16f886be569", - "vendor": "Sample Code, Inc.", - "version_major": 1, - "version_minor": 0 - }, - "state": "stopped", - "summary": "Stopped application", - "type": "developer" - } - - Results in the following lines - SDK APP[0] Name:do_it - SDK APP[0] State:stopped - SDK APP[0] Summary:Stopped application - SDK APP[0] Date:2016-04-15T21:58:30Z - SDK APP[0] Version:1.0 - SDK APP[0] UUID:ae151650-4ce9-4337-ab6b-a16f886be569 - - This does NOT enumerate through the APPS list - - :param int index: the index in ['apps'] - :param dict one_app: one entry in the array - :param bool all_data: if T, include RESTART and VENDOR, else ignore - :return list: - """ - result = [] - - app_tag = "SDK APP[%d]" % index - - if '_id_' in one_app: - result.append( - "{0} UUID:{1}".format(app_tag, one_app['_id_'])) - - if 'state' in one_app: - result.append( - "{0} State:{1}".format(app_tag, one_app['state'])) - - if 'summary' in one_app: - result.append( - "{0} Summary:{1}".format(app_tag, one_app['summary'])) - - if 'app' in one_app: - # only if the app is named - note that as of FW 6.1.2, we might have - # 'extra' entries for failed actions. So we need to support un-named - # apps. Example: trying to install 'too many' apps leaves the failure - # as 2nd entry, without name, date, etc. - if 'name' in one_app["app"]: - # only if the app is named - result.append( - "{0} Name:{1}".format(app_tag, one_app['app']['name'])) - - if 'date' in one_app["app"]: - result.append( - "{0} Date:{1}".format(app_tag, one_app['app']['date'])) - - if 'version_major' in one_app["app"]: - result.append("{0} Version:{1}.{2}".format( - app_tag, one_app['app']['version_major'], - one_app['app']['version_minor'])) - - if all_data: - # then include all data - if 'type' in one_app: - result.append( - "{0} type:{1}".format(app_tag, - one_app['type'])) - - if 'restart' in one_app["app"]: - result.append( - "{0} Restart:{1}".format(app_tag, - one_app['app']['restart'])) - - if 'vendor' in one_app["app"]: - result.append( - "{0} Vendor:{1}".format(app_tag, - one_app['app']['vendor'])) - - return result diff --git a/cp_lib/time_period.py b/cp_lib/time_period.py deleted file mode 100644 index a54a3be5..00000000 --- a/cp_lib/time_period.py +++ /dev/null @@ -1,418 +0,0 @@ - -import threading -import time - - -class TimePeriods(threading.Thread): - """ - Allow classes to obtain a callback at predictable time periods, - such as every hour - - To reduce load, the minimum 'period' default is every 5 seconds. - 1 second is perhaps overly 'heavy' - """ - - # for now, this MUST be in set (2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) - # so one/1 is not supported - MINIMUM_PERIOD = 5 - - HANDLE_GARBAGE_COLLECTION = True - - # adds a variable skew seconds to avoid concurrence at midnight on - # first date of month, when - # callbacks for per-hour, per-day, and per-month would all fire - SKEW_MINUTE = 3 - SKEW_HOUR = 17 - SKEW_DAY = 33 - SKEW_MONTH = 47 - SKEW_YEAR = 51 - - NAME_MINUTE = 'min' - NAME_HOUR = 'hr' - NAME_DAY = 'day' - NAME_MONTH = 'mon' - NAME_YEAR = 'yr' - NAME_LIST = (NAME_MINUTE, NAME_HOUR, NAME_DAY, NAME_MONTH, NAME_YEAR) - - def __init__(self): - - # enforce our basic design rules, which allow some basic assumptions - assert isinstance(self.MINIMUM_PERIOD, int) - # we don't support 1! - assert 1 < self.MINIMUM_PERIOD <= 60 - assert is_valid_clean_period_seconds(self.MINIMUM_PERIOD) - - super().__init__(name="TimePeriods", daemon=True) - - # the event starts as False (per specs) - self.shutdown_requested = threading.Event() - - # create a semaphore lock to detect when callbacks take too long! - self._busy = threading.Lock() - self._over_run = 0 - self._good_run = 0 - - # here is our shared values - self.now = time.time() - self.now_struct = time.gmtime(self.now) - - # these will be our 'period lists' - pass in 'self' to allow - # the periods to interact - self.per_minute = self.OnePeriod(self) - self.per_hour = self.OnePeriod(self) - self.per_day = self.OnePeriod(self) - self.per_month = self.OnePeriod(self) - self.per_year = self.OnePeriod(self) - - # now that ALL exist, assign each one a period name (a 'role') - self.per_minute.set_period_name(self.NAME_MINUTE) - self.per_hour.set_period_name(self.NAME_HOUR) - self.per_day.set_period_name(self.NAME_DAY) - self.per_month.set_period_name(self.NAME_MONTH) - self.per_year.set_period_name(self.NAME_YEAR) - - # assume set externally (or run() handles) - self.logger = None - - return - - def run(self): - """ - The THREAD main run loop: - - Check the time - - update the shared struct_time - - call any callbacks - """ - - if self.logger is None: - self.logger = get_project_logger() - assert self.logger is not None - - while True: - - if self.shutdown_requested.is_set(): - # then we are to STOP running - break - - self.now = time.time() - self.now_struct = time.gmtime(self.now) - - # reschedule NEXT callback - assert isinstance(self.now_struct, time.struct_time) - next_delay = next_seconds_period(self.now_struct.tm_sec, - self.MINIMUM_PERIOD) - if next_delay < 1: - # avoid it ever being zero/0 - next_delay = 1 - - if not self._busy.acquire(False): - # then we had an over-run! - self._over_run += 1 - self.logger.warning( - "TimePeriod over-run #{0}( or {1}%)".format( - self._over_run, self._over_run / self._good_run) + - "; call backs taking too long") - - else: - self._good_run += 1 - - # do the callbacks - self.per_minute.check_callbacks(self.now_struct) - - self._busy.release() - - self.logger.debug("Sleep for %d sec" % int(next_delay)) - time.sleep(next_delay) - - return - - def add_periodic_callback(self, cb, period: str): - return - - class OnePeriod(object): - - def __init__(self, parent): - - self._parent = parent - - self.cb_list = [] - self.cb_list_skewed = [] - self.last_seen = 0 - self.do_skewed = False - - # these are fixed by a subsequent set_period_name() call - self._get_my_value = lambda x: x - self.period_name = None - self.my_sub = None - self.skew_seconds = 0 - return - - def __repr__(self): - return "Period:%03s cb:%d skewed:%d" % (str(self.period_name), - len(self.cb_list), - len(self.cb_list_skewed)) - - def get_name(self): - return self.period_name - - def set_period_name(self, name: str): - - if name not in TimePeriods.NAME_LIST: - raise ValueError( - "Period name({0}) is invalid; must be in {1}.".format( - name, TimePeriods.NAME_LIST)) - - self.period_name = name - - assert isinstance(self._parent, TimePeriods) - - if self.period_name == TimePeriods.NAME_MINUTE: - self._get_my_value = lambda x: x.tm_min - self.skew_seconds = TimePeriods.SKEW_MINUTE - self.last_seen = self._parent.now_struct.tm_min - self.my_sub = self._parent.per_hour - assert isinstance(self.my_sub, TimePeriods.OnePeriod) - - elif self.period_name == TimePeriods.NAME_HOUR: - self._get_my_value = lambda x: x.tm_hour - self.skew_seconds = TimePeriods.SKEW_HOUR - self.last_seen = self._parent.now_struct.tm_hour - self.my_sub = self._parent.per_day - assert isinstance(self.my_sub, TimePeriods.OnePeriod) - - elif self.period_name == TimePeriods.NAME_DAY: - self._get_my_value = lambda x: x.tm_mday - self.skew_seconds = TimePeriods.SKEW_DAY - self.last_seen = self._parent.now_struct.tm_mday - self.my_sub = self._parent.per_month - assert isinstance(self.my_sub, TimePeriods.OnePeriod) - - elif self.period_name == TimePeriods.NAME_MONTH: - self._get_my_value = lambda x: x.tm_mon - self.skew_seconds = TimePeriods.SKEW_MONTH - self.last_seen = self._parent.now_struct.tm_mon - self.my_sub = self._parent.per_year - assert isinstance(self.my_sub, TimePeriods.OnePeriod) - - elif self.period_name == TimePeriods.NAME_YEAR: - self._get_my_value = lambda x: x.tm_year - self.skew_seconds = TimePeriods.SKEW_YEAR - self.last_seen = self._parent.now_struct.tm_year - self.my_sub = None # YEAR has no next period - - # else: already handled up ub if name not in ... test - - return - - def add_callback(self, cb, skewed=False): - """ - - :param cb: - :param skewed: - :return: - """ - # make sure we're not adding 'twice' - remove any existing - self.remove_callback(cb) - - if skewed: - self.cb_list_skewed.append(cb) - else: - self.cb_list.append(cb) - return - - def remove_callback(self, cb): - """ - - :param cb: - :return: - """ - if cb in self.cb_list: - self.cb_list.remove(cb) - - if cb in self.cb_list_skewed: - self.cb_list_skewed.remove(cb) - return - - def period_has_changed(self, now_tuple): - """Use our 'lambda' to check is our internal last-seen - implied period change""" - return self.last_seen != self._get_my_value(now_tuple) - - def check_callbacks(self, now_tuple: time.struct_time): - """ - Handle the various per callbacks this period (if any) - """ - if self.period_has_changed(now_tuple): - # then period has changed - self.last_seen = self._get_my_value(now_tuple) - if len(self.cb_list_skewed) > 0: - self.do_skewed = True - - # do the non-skewed callbacks - self.process_callbacks(now_tuple) - - if self.my_sub is not None and \ - self.my_sub.period_has_changed(now_tuple): - # then chain to our sub - for per_year, this is still None - self.my_sub.check_callbacks(now_tuple) - - return - - def process_callbacks(self, now_tuple: time.struct_time, skewed=False): - """ - Handle the various per callbacks this period (if any) - """ - if skewed: - use_list = self.cb_list - else: - use_list = self.cb_list_skewed - - for cb in use_list: - try: - cb(now_tuple) - except: - # self.logger.error("PerMinute CB failed") - raise - return - -""" -What are CLEAN PERIODS? That are time slices which allow predictable -periodic repeats within scope of the next larger (encapsulating) time period. - -For example, doing something every 12 seconds allows 5 periods per 1 minute -(minutes being the next larger time periods encapsulating seconds. However, -doing something every 8 or 13 seconds does NOT support this. - -For example, doing something very 3 hours allows 8 periods per 1 day, -whereas doing something every 5 hours does NOT support such clean periods. -""" - -_CLEAN_PERIOD_SEC_MIN = (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) -CLEAN_PERIOD_SECONDS = _CLEAN_PERIOD_SEC_MIN -CLEAN_PERIOD_MINUTES = _CLEAN_PERIOD_SEC_MIN -CLEAN_PERIOD_HOURS = (1, 2, 3, 4, 6, 8, 12, 24) - -_CLEAN_ROUNDUP_SEC_MIN = { - 2: (0, 2, 4, 6, 8, ), - 3: (0, 3, 6, 9, 12, ) -} - - -def is_valid_clean_period_seconds(value: int): - """ - Given a number of seconds, detect if it is in the correct set - for clean periods, such that doing - something every 'X' seconds allows predictable repeats each minute. - - Example: doing something every 12 seconds allows 5 'clean periods' - per minute, but doing something - every 13 seconds does NOT allow such 'clean periods' - - :param value: the number of seconds (zero-based, not time.time() form! - :rtype: bool - """ - return int(value) in CLEAN_PERIOD_SECONDS - - -def is_valid_clean_period_minutes(value: int): - """ - Given a number of minutes, detect if it is in the correct set - for clean periods, such that doing - something every 'X' minutes allows predictable repeats each hour. - - Example: doing something every 12 minutes allows 5 'clean periods' - per hour, but doing something - every 13 minutes does NOT allow such 'clean periods' - - :param value: the number of minutes (zero-based, not time.time() form! - :rtype: bool - """ - return int(value) in CLEAN_PERIOD_MINUTES - - -def is_valid_clean_period_hours(value: int): - """ - Given a number of hours, detect if it is in the correct set for - clean periods, such that doing - something every 'X' hours allows predictable repeats each day. - - Example: doing something every 3 hours allows 8 'clean periods' - per day, but doing something - every 5 hours does NOT allow such 'clean periods' - - :param value: the number of hours (zero-based, not time.time() form! - :rtype: bool - """ - return int(value) in CLEAN_PERIOD_HOURS - - -def next_seconds_period(source: int, period: int): - """ - Given a time as seconds, return next clean period start. For - example, if source=37 and period=15 (secs), then return 45. - - To know the delta to next period, use delay_to_next_seconds_period() - instead (so 45 - 37 = 8) - - If 60 is returned, then should be NEXT minute or hour (aka: next '0') - Also, the input does NOT have to be < 60, so input 292 returns 300, - and the caller needs to handle any side effects of this. - - :param source: the time as seconds (or minutes) - :param period: the desired clean period - :return: the nex period as sec/minutes - :rtype: int - """ - # this is same for seconds and minutes - assert period in _CLEAN_PERIOD_SEC_MIN - # assert 0 <= source <= 60 - - if period == 1: - # special case #1 - if period is 1, then value is as desired - # (no round up ever) - return source - - # we divide and allow truncation (round-down) So 17 / '5 sec' period = 3 - # (// is special integer division) - value = source // period - - # multiple means 3 * 5 = 15 - value *= period - - # if source % period != 0: - # then add in 1 more period make it 20, which is correct answer - value += period - - return value - - -def next_minutes_period(source: int, period: int): - """ - See docs for next_second_period() - function is the same - - :rtype: int - """ - return next_seconds_period(source, period) - - -def delay_to_next_seconds_period(source: int, period: int): - """ - Given a time as seconds, return HOW MANY seconds to delay to reach - start of next clean period start. - - :return: the seconds to reach start of next clean period start - :rtype: int - """ - next_period = next_seconds_period(source, period) - return next_period - source - - -def delay_to_next_minutes_period(source: int, period: int): - """ - See docs for delay_to_next_seconds_period() - function is the same - - :rtype: int - """ - # both minutes & seconds work the same - next_period = next_seconds_period(source, period) - return next_period - source diff --git a/cp_lib/time_until.py b/cp_lib/time_until.py deleted file mode 100644 index b1756b33..00000000 --- a/cp_lib/time_until.py +++ /dev/null @@ -1,89 +0,0 @@ -import time - - -VALID_NICE_SECOND_PERIODS = (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) -VALID_NICE_MINUTE_PERIODS = (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) -VALID_NICE_HOUR_PERIODS = (1, 2, 3, 4, 6, 8, 12, 24) - - -def seconds_until_next_hour(now=None): - """ - How many seconds until next hour - - :param now: - :return: - """ - now = _prep_time_now(now) - - # how many second until next minute - delay = 60 - now.tm_sec - - # add how many seconds until next hour - delay += (60 - now.tm_min) - - return delay - - -def seconds_until_nice_minute_period(period, now=None): - """ - How many seconds until next minute, when now.tm_sec == 0 - - :param int period: must be in set VALID_NICE_MINUTE_PERIODS - :param now: - :return int: - """ - now = _prep_time_now(now) - - return 60 - now.tm_sec - - -def seconds_until_next_minute(now=None, fudge=2): - """ - How many seconds until next minute, when now.tm_sec == 0 - - :param now: - :param int fudge: to avoid too short of delays, a fudge=2 means when tm_sec > 58, go to NEXT minute (62 seconds) - :return int: - """ - now = _prep_time_now(now) - - if fudge > 0 and (60 - now.tm_sec) <= fudge: - # then round up to next hour - return (60 - now.tm_sec) + 60 - - return 60 - now.tm_sec - - -def seconds_until_nice_second_period(period, now=None): - """ - How many seconds until next seconds period (like 15 means hh:00, hh:15, hh:30, hh:45) - - :param int period: must be in set VALID_NICE_SECOND_PERIODS - :param now: - :return int: - """ - if period not in VALID_NICE_SECOND_PERIODS: - raise ValueError("{} seconds is not valid NICE SECONDS period".format(period)) - - now = _prep_time_now(now) - - return 60 - now.tm_sec - - -def _prep_time_now(now=None): - """ - make sure now is struct_time - - :param now: optional source value - :rtype: time.struct_time - """ - if now is None: - # if none provided, get NOW - now = time.time() - - if not isinstance(now, time.struct_time): - # if now isn't float or suitable, then will throw exception - now = time.localtime(now) - - assert isinstance(now, time.struct_time) - return now diff --git a/data/jsonrpc_settings/README.md b/data/jsonrpc_settings/README.md deleted file mode 100644 index 8f41b5dc..00000000 --- a/data/jsonrpc_settings/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# directory: ./network/simple_jsonrpc -## Router App/SDK sample applications - -A most basic JSON RPC server, using the standard Python 3 SocketServer - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: jsonrpc_server.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [jsonrpc]: - -* host_port=9001, define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. -Also, avoid 8001 or 8080, as router may be using already. - -## File: images - -* The ones named "digit_1.jpg" (etc) are 550x985 - -* are 190x380 (to fit in 200x400 cell? diff --git a/data/jsonrpc_settings/__init__.py b/data/jsonrpc_settings/__init__.py deleted file mode 100644 index 38c22ddd..00000000 --- a/data/jsonrpc_settings/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from data.jsonrpc_settings.jsonrpc_server import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/data/jsonrpc_settings/client.py b/data/jsonrpc_settings/client.py deleted file mode 100644 index 822fb5bb..00000000 --- a/data/jsonrpc_settings/client.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Simple client to run against jsonrpc_server.py -""" -import json -import socket -import sys -import time - -from cp_lib.app_base import CradlepointAppBase -# import cp_lib.data.json_get_put as json_get_put - -# avoid 8080, as the router may have service on it. -DEF_HOST_PORT = 9901 -DEF_HOST_IP = "localhost" - - -def run_client(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - - tests = [ - {"path": "logging.level", "exp": "debug"}, - {"path": "router_api.user_name", "exp": "admin"}, - {"path": "application.firmware", "exp": "6.1"}, - {"path": "application.restart", "exp": "true"}, - - {"path": "router_api", "exp": "admin"}, - ] - - for test in tests: - data = {"jsonrpc": "2.0", "method": "get_setting", - "params": {"path": test["path"]}, "id": 1} - - # then convert to JSON string - data = json.dumps(data).encode() - - app_base.logger.debug("Sent:{}".format(data)) - - server_address = (DEF_HOST_IP, DEF_HOST_PORT) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - # Connect to server and send data - sock.connect(server_address) - sock.sendall(data) - - # Receive data from the server and shut down - received = str(sock.recv(1024), "utf-8") - finally: - sock.close() - - received = json.dumps(received) - - app_base.logger.debug("Recv:{}".format(received)) - - time.sleep(1.0) - - return 0 - - -def do_one_get(app_base, data): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param data: the data - :type data: str or dict or bytes - :return: - """ - if isinstance(data, dict): - # then convert to JSON string - data = json.dumps(data) - - if isinstance(data, str): - # then convert to JSON string - data = data.encode('utf-8') - - app_base.logger.debug("Sent:{}".format(data)) - - server_address = (DEF_HOST_IP, DEF_HOST_PORT) - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - # Connect to server and send data - sock.connect(server_address) - sock.sendall(data) - - # Receive data from the server and shut down - received = str(sock.recv(1024), "utf-8") - finally: - sock.close() - - app_base.logger.debug("Recv:{}".format(received)) - - return json.loads(received) - - -if __name__ == "__main__": - - my_app = CradlepointAppBase("data/jsonrpc_settings") - _result = run_client(my_app) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/data/jsonrpc_settings/jsonrpc_server.py b/data/jsonrpc_settings/jsonrpc_server.py deleted file mode 100644 index c1f92876..00000000 --- a/data/jsonrpc_settings/jsonrpc_server.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -A basic but complete echo server -""" - -import socketserver -import threading -import time - -import cp_lib.data.json_get_put as json_get_put -from cp_lib.app_base import CradlepointAppBase - -# avoid 8080, as the router may have service on it. -DEF_HOST_PORT = 9901 -DEF_HOST_IP = "" - -# hold the CradlepointAppBase for access by the TCP handler -my_base = None - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - global my_base - - my_base = app_base - - my_server = JsonServerThread('Json', app_base) - my_server.start() - - # we need to block the main thread here, because this sample is running - # a SECOND thread for the actual server. This makes no sense in a pure - # sample-code scenario, but doing it this way does allow you to - # import & run the class JsonServerThread() from another demo app - # which requires multiple threads - such as my Counter demo which - # requires both a web server AND a JSON RPC server as 2 threads. - try: - while True: - time.sleep(15.0) - - except KeyboardInterrupt: - app_base.logger.info("Stopping Server, Key Board interrupt") - - return 0 - - -class JsonServerThread(threading.Thread): - - def __init__(self, name, app_base): - """ - prep our thread, but do not start yet - - :param str name: name for the thread - :param CradlepointAppBase app_base: prepared resources: logger, etc - """ - threading.Thread.__init__(self, name=name) - - self.app_base = app_base - self.app_base.logger.info("started INIT") - - return - - def run(self): - """ - Now thread is being asked to start running - """ - if "jsonrpc" in self.app_base.settings: - host_port = int(self.app_base.settings["jsonrpc"].get( - "host_port", DEF_HOST_PORT)) - - host_ip = self.app_base.settings["jsonrpc"].get( - "host_ip", DEF_HOST_IP) - else: - # we create, so WebServerRequestHandler can obtain - self.app_base.settings["jsonrpc"] = dict() - - host_port = DEF_HOST_PORT - self.app_base.settings["jsonrpc"]["host_port"] = host_port - - host_ip = DEF_HOST_IP - self.app_base.settings["jsonrpc"]["host_ip"] = host_ip - - # we want on all interfaces - server_address = (host_ip, host_port) - - self.app_base.logger.info("Starting Server:{}".format(server_address)) - - server = socketserver.TCPServer(server_address, MyHandler) - - # Activate the server; this will keep running until you - # interrupt the program with Ctrl-C - try: - server.serve_forever() - - except KeyboardInterrupt: - self.app_base.logger.info("Stopping Server, Key Board interrupt") - - def please_stop(self): - """ - Now thread is being asked to start running - """ - raise NotImplementedError - - -class MyHandler(socketserver.BaseRequestHandler): - """ - The RequestHandler class for our server. - - It is instantiated once per connection to the server, and must - override the handle() method to implement communication to the - client. - """ - - def handle(self): - """ - Handle a TCP packet - :return: - """ - global my_base - - # self.request is the TCP socket connected to the client - message = self.request.recv(1024).strip() - assert my_base is not None - """ :type my_base: CradlepointAppBase """ - - my_base.logger.debug( - "Client {} asked:".format(self.client_address[0])) - my_base.logger.debug(message) - - # parse & confirm appears valid - message = json_get_put.jsonrpc_check_request(message, - test_params=True) - my_base.logger.debug("checked:{}".format(message)) - # my_base.logger.debug("type:{}".format(type(message["method"]))) - - if "error" not in message: - # then so far, so good - method = message["method"].lower() - - if method == "get_setting": - my_base.logger.debug( - "GetSetting:{}".format(message["params"])) - message = json_get_put.jsonrpc_get( - my_base.settings, message) - - elif method == "get_data": - my_base.logger.debug( - "GetData:{}".format(message["params"])) - message = json_get_put.jsonrpc_get( - my_base.data, message) - - elif method == "put_data": - my_base.logger.debug( - "PutData:{}".format(message["params"])) - message = json_get_put.jsonrpc_put( - my_base.data, message) - - else: - message["error"] = { - "code": -32601, - "message": "Unknown \"method\": {}".format(method)} - - # else, if request["error"] is already true, then return error - - message = json_get_put.jsonrpc_prep_response(message, encode=True) - """ :type message: str """ - my_base.logger.debug("returning:{}".format(message)) - self.request.sendall(message.encode()) - - my_base.logger.debug("") - return - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/simple_jsonrpc") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/data/jsonrpc_settings/send_json.py b/data/jsonrpc_settings/send_json.py deleted file mode 100644 index f585a710..00000000 --- a/data/jsonrpc_settings/send_json.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Simple client to run against jsonrpc_server.py -""" -import json -import socket -import sys -import time - -from cp_lib.app_base import CradlepointAppBase -# import cp_lib.data.json_get_put as json_get_put - -# avoid 8080, as the router may have service on it. -DEF_HOST_PORT = 9901 -DEF_HOST_IP = "192.168.115.1" - - -def run_client(app_base, value): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - - data = {"jsonrpc": "2.0", "method": "put_data", - "params": {"path": "counter", "value": value}, "id": 1} - - # then convert to JSON string - data = json.dumps(data).encode() - - app_base.logger.debug("Sent:{}".format(data)) - - server_address = (DEF_HOST_IP, DEF_HOST_PORT) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - # Connect to server and send data - sock.connect(server_address) - sock.sendall(data) - - # Receive data from the server and shut down - received = str(sock.recv(1024), "utf-8") - finally: - sock.close() - - received = json.dumps(received) - - app_base.logger.debug("Recv:{}".format(received)) - - time.sleep(1.0) - - return 0 - - -def do_one_get(app_base, data): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param data: the data - :type data: str or dict or bytes - :return: - """ - if isinstance(data, dict): - # then convert to JSON string - data = json.dumps(data) - - if isinstance(data, str): - # then convert to JSON string - data = data.encode('utf-8') - - app_base.logger.debug("Sent:{}".format(data)) - - server_address = (DEF_HOST_IP, DEF_HOST_PORT) - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - try: - # Connect to server and send data - sock.connect(server_address) - sock.sendall(data) - - # Receive data from the server and shut down - received = str(sock.recv(1024), "utf-8") - finally: - sock.close() - - app_base.logger.debug("Recv:{}".format(received)) - - return json.loads(received) - - -if __name__ == "__main__": - - my_app = CradlepointAppBase("data/jsonrpc_settings") - _result = run_client(my_app, sys.argv[1]) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/data/jsonrpc_settings/settings.ini b/data/jsonrpc_settings/settings.ini deleted file mode 100644 index f1195a32..00000000 --- a/data/jsonrpc_settings/settings.ini +++ /dev/null @@ -1,12 +0,0 @@ -; one app settings - -[application] -name=jsonrpc_server -description=A very simple JSON RPC server, get/put app_base settings -path = data/jsonrpc_settings -version = 1.0 -uuid=b1bf5883-24db-4518-a787-bb5dea467e4a - -[jsonrpc] -; port cannot be less than 1024 on CP router -host_port=9901 diff --git a/demo/gpio_power_loss/README.md b/demo/gpio_power_loss/README.md deleted file mode 100644 index 5d50267f..00000000 --- a/demo/gpio_power_loss/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# directory: ./demo/gpio_power_loss -## Router App/SDK sample applications - -This demo includes a thread (task) to allow it to be combined with other -demos. The demo runs on an IBR1100, reading the digital input on the 2x2 -power connector. sample application creates 3 sub-tasks (so four total). -The main application starts 3 sub-tasks, which loop, sleeping with a random -delay before printing out a logger INFO message. - -The first 2 sub-tasks will run 'forever' - or on a PC, when you do a ^C or -keyboard interrupt, the main task will abort, using an event() to stop -all three sub-tasks. - -The third task will exit after each loop, and the main task will re-run it -when it notices that it is not running. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: power_loss.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [power_loss]: - -* check_input_delay=5 sec, how often to query the router status tree. -Polling too fast will impact router performance - possibly even prevent -operation. So select a reasonable value: a few seconds for DEMO purposes, -likely '30 sec' or '1 min' for normal operations. -The routine 'parse_duration' is used, so supported time tags include -"x sec", "x min", "x hr" and so on. -* loss_delay=2 sec, after seeing the first POWER LOSS value, we delay and -recheck, confirming it is still lost before sending the alert. -The routine 'parse_duration' is used, so tags like 'sec' and 'min' are -supported. -* restore_delay=2 sec, after seeing the first POWER RESTORE value, we delay -and recheck, confirming it is still restored before sending the alert. -The routine 'parse_duration' is used, so tags like 'sec' and 'min' are -supported. -* match_on_power_loss=False, the 2x2 power connector input is read and -compared to this value. If it matches, then the condition is deemed -to be "Power Loss == True". -False and 0 are interchangeable, as are True and 1. -* led_on_power_loss=False, defines if and how the 2x2 power connector -output is handled. Set to 'null' or 'None' to disable out. -If True or False, that value to set when "Power Loss == True" -* site_name=Quick Serve Restaurant #278A, any user defined name, which is -included in the alert. - -Also in the section [power_loss], see cp_lib.cp_email.py: - -* username, password, smtp_url, smtp_port, email_to - -## Alert Forms - -The general form will be {condition}{setting: site_name} - -### Example when Power is Lost: - -* Bad News! AC Power lost at site: Quick Serve Restaurant #278A - at time: 2016-04-20 19:36:26 -0500 - -### Example when Power is Restored: - -* Good News! AC Power restored at site: Quick Serve Restaurant #278A - at time: 2016-04-20 19:54:47 -0500 - diff --git a/demo/gpio_power_loss/__init__.py b/demo/gpio_power_loss/__init__.py deleted file mode 100644 index ecdcf51a..00000000 --- a/demo/gpio_power_loss/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from demo.gpio_power_loss.power_loss import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/demo/gpio_power_loss/power_loss.py b/demo/gpio_power_loss/power_loss.py deleted file mode 100644 index b5d16f91..00000000 --- a/demo/gpio_power_loss/power_loss.py +++ /dev/null @@ -1,396 +0,0 @@ -import threading -import time - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.cp_email import cp_send_email -from cp_lib.parse_duration import TimeDuration -from cp_lib.parse_data import parse_boolean, parse_none - -power_loss_task = None - - -def run_router_app(app_base, wait_for_child=True): - """ - Start our thread - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param bool wait_for_child: T to wait in loop, F to return immediately - :return: - """ - global power_loss_task - - # confirm we are running on 1100/1150, result should be like "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info( - "Product Model is good:{}".format(result)) - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - power_loss_task = PowerLoss("power_loss", app_base) - power_loss_task.start() - - if wait_for_child: - # we block on this sub task - for testing - try: - while True: - time.sleep(300) - - except KeyboardInterrupt: - # this is only true on a test PC - won't see on router - # must trap here to prevent try/except in __init__.py from avoiding - # the graceful shutdown below. - pass - - # now we need to try & kill off our kids - if we are here - app_base.logger.info("Okay, exiting") - - stop_router_app(app_base) - - else: - # we return ASAP, assume this is 1 of many tasks run by single parent - app_base.logger.info("Exit immediately, leave sub-task run") - - return 0 - - -def stop_router_app(app_base): - """ - Stop the thread - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - global power_loss_task - - if power_loss_task is not None: - app_base.logger.info("Signal PowerLoss sub-task to stop") - - # signal sub task to halt - power_loss_task.please_stop() - - # what until it does - remember, it is sleeping, so will take some time - power_loss_task.join() - - return 0 - - -class PowerLoss(threading.Thread): - - DEF_INPUT_NAME = "status/gpio/CGPIO_CONNECTOR_INPUT" - - def __init__(self, name, app_base): - """ - prep our thread, but do not start yet - - :param str name: name for the thread - :param CradlepointAppBase app_base: prepared resources: logger, etc - """ - threading.Thread.__init__(self, name=name) - - self.app_base = app_base - self.app_base.logger.info("started INIT") - - # how long to delay between checking the GPIO - self.loop_delay = self.app_base.settings["power_loss"].get( - "check_input_delay", 15) - # support things like '1 min' or 15 - duration = TimeDuration(self.loop_delay) - self.loop_delay = float(duration.get_seconds()) - - # how long to wait, to double-check LOSS - self.loss_delay = self.app_base.settings["power_loss"].get( - "loss_delay", 1) - self.loss_delay = duration.parse_time_duration_to_seconds( - self.loss_delay) - - # how long to wait, to double-check RESTORE - self.restore_delay = self.app_base.settings["power_loss"].get( - "restore_delay", 1) - self.restore_delay = duration.parse_time_duration_to_seconds( - self.restore_delay) - - # when GPIO matches this state, then power is lost - self.state_in_alarm = self.app_base.settings["power_loss"].get( - "match_on_power_loss", False) - # support 'true', '1' etc - but finally is True/False - self.state_in_alarm = parse_boolean(self.state_in_alarm) - - # when 'power is lost', send to LED - self.led_in_alarm = self.app_base.settings["power_loss"].get( - "led_on_power_loss", None) - try: - # see if the setting is None, to disable - self.led_in_alarm = parse_none(self.led_in_alarm) - - except ValueError: - # support 'true', '1' etc - but finally is True/False - self.led_in_alarm = parse_boolean(self.led_in_alarm) - - # when GPIO matches this state, then power is lost - self.site_name = self.app_base.settings["power_loss"].get( - "site_name", "My Site") - - # create an event to manage our stopping - # (Note: on CP router, this isn't strictly true, as when the parent is - # stopped/halted, the child dies as well. However, you may want - # your sub task to clean up before it exists - self.keep_running = threading.Event() - self.keep_running.set() - - # hold the .get_power_loss_status() - self.last_state = None - - # special tweak to announce 'first poll' more smartly - self.starting_up = True - - self.email_settings = dict() - self.prep_email_settings() - - return - - def run(self): - """ - Now thread is being asked to start running - """ - - self.app_base.logger.info("Running") - - self.app_base.cs_client.show_rsp = False - - while self.keep_running.is_set(): - - # check the GPIO input status, likely is string - result = self.get_power_loss_status() - if result == self.last_state: - # then no change - pass - # self.app_base.logger.debug( - # "State has not changed, still={}".format(result)) - - elif result is None: - # handle hiccups? Try again? - pass - - else: - # else state has been changed - self.last_state = result - if self.last_state: - # changed state = True, power has been LOST - self.app_base.logger.debug( - "State changed={}, POWER LOST!".format( - self.last_state)) - - # double-check if really lost - self.app_base.logger.debug( - "Delay %d sec to double-check" % int( - self.loss_delay)) - time.sleep(self.loss_delay) - result = self.get_power_loss_status() - if result: - # then power really is lost - self.do_power_lost_event() - else: - self.last_state = result - self.app_base.logger.debug("False Alarm") - - else: - # changed state = False, power has been RESTORED - self.app_base.logger.info( - "State changed={}, POWER OKAY".format( - self.last_state)) - - # double-check if really restored - self.app_base.logger.debug( - "Delay %d sec to double-check" % int( - self.restore_delay)) - time.sleep(self.restore_delay) - result = self.get_power_loss_status() - if not result: - # then power really is restored - self.do_power_restore_event() - else: - self.last_state = result - self.app_base.logger.debug("False Alarm") - - time.sleep(self.loop_delay) - - self.app_base.logger.info("Stopping") - return 0 - - def please_stop(self): - """ - Now thread is being asked to start running - """ - self.keep_running.clear() - return - - def get_power_loss_status(self): - """ - Fetch the GPIO input state, return reading, which may not - be directly related to power status. - - :return bool: return True/False, for condition of 'power is lost' - """ - # check the GPIO input status, likely is string - result = self.app_base.cs_client.get(self.DEF_INPUT_NAME) - if result in (1, '1'): - result = True - elif result in (0, '0'): - result = False - else: - # ?? what if we hiccup & have a bad reading? - return None - - # self.app_base.logger.debug("result:{} alarm:{} is_lost:{}".format( - # result, self.state_in_alarm, self.power_is_lost(result))) - - return self.power_is_lost(result) - - def do_power_lost_event(self): - """ - Do what we need to when power is lost - :return: - """ - if self.starting_up: - # special tweak to announce 'first poll' more smartly - self.starting_up = False - message =\ - "Starting Up: AC Power OFF at site: {}".format(self.site_name) - else: - message =\ - "Bad News! AC Power lost at site: {}".format(self.site_name) - self._do_event(message, alarm=True) - - if self.led_in_alarm is not None: - # then affect LED output, since in alarm, set to led_in_alarm - self.app_base.cs_client.put( - "control/gpio", - {"CGPIO_CONNECTOR_OUTPUT": int(self.led_in_alarm)}) - return - - def do_power_restore_event(self): - """ - Do what we need to when power is restored - :return: - """ - if self.starting_up: - # special tweak to announce 'first poll' more smartly - self.starting_up = False - message =\ - "Starting Up: AC Power ON at site: {}".format(self.site_name) - else: - message =\ - "Good News! AC Power restored at site: {}".format( - self.site_name) - self._do_event(message, alarm=False) - - if self.led_in_alarm is not None: - # affect LED output, since not in alarm, set to NOT led_in_alarm - self.app_base.cs_client.put( - "control/gpio", - {"CGPIO_CONNECTOR_OUTPUT": int(not self.led_in_alarm)}) - return - - def _do_event(self, message, alarm): - """ - Wrap the logging anf email action - - :param str message: the email subject with site_name - :param bool alarm: T/F if this is bad/alarm, of good/return-to-normal - :return: - """ - self.email_settings['subject'] = message - - time_string = self.format_time_message() - self.email_settings['body'] = self.email_settings['subject'] + \ - '\n' + time_string + '\n' - - self.email_settings['logger'] = self.app_base.logger - result = cp_send_email(self.email_settings) - - if alarm: - self.app_base.logger.warning(message) - self.app_base.logger.warning(time_string) - else: - self.app_base.logger.info(message) - self.app_base.logger.info(time_string) - return - - @staticmethod - def format_time_message(now=None): - """ - Produce a string such as "2016-04-20 10:54:23 Mountain Time" - - :param now: optional time ot use - :return: - """ - if now is None: - now = time.time() - - return " at time: {}".format( - time.strftime("%Y-%m-%d %H:%M:%S %z", time.localtime(now))) - - def power_is_lost(self, value): - """ - Given current state, form the string - :param value: - :return: - """ - return bool(value == self.state_in_alarm) - - def prep_email_settings(self): - """ - - :return: - """ - # Required keys - # ['smtp_tls] = T/F to use TLS, defaults to True - # ['smtp_url'] = URL, such as 'smtp.gmail.com' - # ['smtp_port'] = TCP port like 587 - be careful, as some servers - # have more than one, with the number defining the - # security demanded. - # ['username'] = your smtp user name (often your email acct address) - # ['password'] = your smtp acct password - # ['email_to'] = the target email address, as str or list - # ['subject'] = the email subject - - # Optional keys - # ['email_from'] = the from address - any smtp server will ignore, - # and force this to be your user/acct email address; - # def = ['username'] - # ['body'] = the email body; def = ['subject'] - - # we allow KeyError is any of these are missing - self.email_settings['smtp_tls'] = True - self.email_settings['smtp_url'] = \ - self.app_base.settings['power_loss']['smtp_url'] - self.email_settings['smtp_port'] = \ - self.app_base.settings['power_loss']['smtp_port'] - self.email_settings['username'] = \ - self.app_base.settings['power_loss']['username'] - self.email_settings['password'] = \ - self.app_base.settings['power_loss']['password'] - - email_to = self.app_base.settings['power_loss']['email_to'] - self.app_base.logger.debug("email_to:typ:{} {}".format( - type(email_to), email_to)) - - if isinstance(email_to, str): - # if already string, check if like '["add1@c.com","add2@d.com"] - email_to = email_to.strip() - if email_to[0] in ("[", "("): - email_to = list(eval(email_to)) - self.app_base.logger.debug("lister:typ:{} {}".format( - type(email_to), email_to)) - else: - email_to = [email_to] - self.app_base.logger.debug("elslie:typ:{} {}".format( - type(email_to), email_to)) - - self.email_settings['email_to'] = email_to - - return - diff --git a/demo/gpio_power_loss/settings.ini b/demo/gpio_power_loss/settings.ini deleted file mode 100644 index a0edd533..00000000 --- a/demo/gpio_power_loss/settings.ini +++ /dev/null @@ -1,23 +0,0 @@ -[application] -name=power_loss -description=Thread to monitor 1100's GPIO, for loss of AC power -path=demo.gpio_power_loss -version=1.23 -uuid=ab30518a-0f0b-41ca-b60d-e933d33f7959 - -; this section and item is unique to this sample -[power_loss] -check_input_delay=5 sec -match_on_power_loss=False -loss_delay=2 sec -restore_delay=2 sec -site_name=Quick Serve Restaurant #278A -led_on_power_loss=False -username={your name}@gmail.com -password={put your password here} -smtp_url=smtp.gmail.com -smtp_port=587 -email_to={your name}@gmail.com - -[logging] -level=info diff --git a/demo/gps_gate/README.md b/demo/gps_gate/README.md deleted file mode 100644 index 933a3eef..00000000 --- a/demo/gps_gate/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# directory: ./demo/gps_gate -## Router App/SDK sample applications - -Received GPS data on 'localhost' port (as configured in CP Router GPS) -settings, then forward to GpsGate public servers. Only the TCP transport -is used (UDP and HTTP/XML is NOT coded/included - yet). - -To use this, you will need to have arranged a GpsGate server, from which -you are given the URL to use. The code relies upon the IMEI ALREADY existing -in the server - it does not device registration, etc. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: gps_gate.py - -The main files of the application. - -## File: gps_gate_nmea.py - -A custom state-machine for a NMEA stream designed for GpsGate, including the -unit-of-measure and delta-filters they handle. The cp_lib.gps_nema.py -module is also used, but mainly for the NMEA sentence cleanup & creation. - -## File: gps_gate_protocol.py - -A custom state-machine for GpsGate 'TCP' server. It maintains a state, -and returns next-requests as expected by the protocol. Note that it does NOT -do the actual data comm. This code CREATES requests, and PARSES responses, -changing its internal state & settings as appropriate. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [gps_gate]: - -* gps_gate_url=64.46.40.178, defines the URL assigned to you by the -GpsGate people. This is treated as string, so can be IP or FQDN - -* gps_gate_port=30175, is the default TCP port to use. Change if needed. - -* host_ip=192.168.1.6 - ONLY include to ignore Router API value! -Define which 'localhost' port the code waits on for incoming GPS sentences. -This must match what is configured in the CP Router - -* host_port=9999 - ONLY include to ignore Router API value! -Define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. - - -## TODO - - -* have the code fetch the IP/port info from the router config tree. This would -eliminate the need for the host_ip/host_port settings. diff --git a/demo/gps_gate/__init__.py b/demo/gps_gate/__init__.py deleted file mode 100644 index 4cec1af1..00000000 --- a/demo/gps_gate/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from demo.gps_gate.gps_gate import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/demo/gps_gate/gps_gate.py b/demo/gps_gate/gps_gate.py deleted file mode 100644 index 5b0009e5..00000000 --- a/demo/gps_gate/gps_gate.py +++ /dev/null @@ -1,215 +0,0 @@ -""" -Received GPS, assuming the router's GPS function sends new data (sentences) -to a localhost port -""" - -import socket -import time -import gc - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.load_active_wan import ActiveWan -from cp_lib.load_gps_config import GpsConfig -from cp_lib.parse_data import clean_string, parse_integer - -from demo.gps_gate.gps_gate_protocol import GpsGate - - -DEF_BUFFER_SIZE = 1024 - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - # handle the GPS_GATE protocol - gps_gate = GpsGate(app_base.logger) - - # process the SETTINGS file - section = "gps_gate" - if section not in app_base.settings: - # this is unlikely! - app_base.logger.warning( - "Aborting: No [%s] section in settings.ini".format(section)) - return -1 - - else: - # then load dynamic values - temp = app_base.settings[section] - - app_base.logger.debug( - "[{}] section = {}".format(section, temp)) - - # settings for our LISTENER for router GPS output - buffer_size = int(temp.get("buffer_size", DEF_BUFFER_SIZE)) - - # check on our localhost port (not used, but to test) - config = GpsConfig(app_base) - host_ip, host_port = config.get_client_info() - if "host_ip" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_ip") - value = clean_string(temp["host_ip"]) - app_base.logger.warning("was:{} now:{}".format(host_ip, value)) - host_ip = value - - if "host_port" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_port") - value = parse_integer(temp["host_port"]) - app_base.logger.warning("was:{} now:{}".format(host_port, value)) - host_port = value - - app_base.logger.debug("GPS source:({}:{})".format(host_ip, host_port)) - del config - - # settings defining the GpsGate interactions - if "gps_gate_url" in temp: - gps_gate.set_server_url(temp["gps_gate_url"]) - if "gps_gate_port" in temp: - gps_gate.set_server_port(temp["gps_gate_port"]) - if "gps_gate_transport" in temp: - gps_gate.set_server_transport(temp["gps_gate_transport"]) - - if "username" in temp: - gps_gate.set_username(temp["username"]) - if "password" in temp: - gps_gate.set_password(temp["password"]) - if "server_version" in temp: - gps_gate.set_server_version(temp["server_version"]) - if "client" in temp: - gps_gate.set_client_name(temp["client"]) - - if "IMEI" in temp: - gps_gate.set_imei(temp["IMEI"]) - elif "imei" in temp: - # handle upper or lower case, just because ... - gps_gate.set_imei(temp["imei"]) - - # we can pre-set the filters here, but Gpsgate may try to override - if "distance_filter" in temp: - gps_gate.nmea.set_distance_filter(temp["distance_filter"]) - if "time_filter" in temp: - gps_gate.nmea.set_time_filter(temp["time_filter"]) - if "speed_filter" in temp: - gps_gate.nmea.set_speed_filter(temp["speed_filter"]) - if "direction_filter" in temp: - gps_gate.nmea.set_direction_filter(temp["direction_filter"]) - if "direction_threshold" in temp: - gps_gate.nmea.set_direction_threshold(temp["direction_threshold"]) - - # check the cell modem/gps sources - wan_data = ActiveWan(app_base) - # app_base.logger.info("WAN data {}".format(wan_data['active'])) - - value = wan_data.get_imei() - if gps_gate.client_imei is None: - # only take 'live' value if setting.ini was NOT included - gps_gate.set_imei(value) - else: - app_base.logger.warning( - "Using settings.ini IMEI, ignoring cell modem's value") - - if not wan_data.supports_gps(): - app_base.logger.warning( - "cell modem claims no GPS - another source might exist") - - if wan_data.get_live_gps_data() in (None, {}): - app_base.logger.warning( - "cell modem has no last GPS data") - else: - app_base.logger.debug("GPS={}".format(wan_data.get_live_gps_data())) - - while True: - # define the socket resource, including the type (stream == "TCP") - address = (host_ip, host_port) - app_base.logger.info("Preparing GPS Listening on {}".format(address)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # attempt to actually lock resource, which may fail if unavailable - # (see BIND ERROR note) - try: - sock.bind(address) - except OSError as msg: - app_base.logger.error("socket.bind() failed - {}".format(msg)) - - # technically, Python will close when 'sock' goes out of scope, - # but be disciplined and close it yourself. Python may warning - # you of unclosed resource, during runtime. - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors - # are likely permanent. - return -1 - - # only allow 1 client at a time - sock.listen(3) - - while True: - # loop forever - app_base.logger.info("Waiting on TCP socket %d" % host_port) - client, address = sock.accept() - app_base.logger.info("Accepted connection from {}".format(address)) - - # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # set non-blocking so we can do a manual timeout (use of select() - # is better ... but that's another sample) - # client.setblocking(0) - - while True: - app_base.logger.debug("Waiting to receive data") - data = client.recv(buffer_size) - # data is type() bytes, to echo we don't need to convert - # to str to format or return. - if data: - data = data.decode().split() - - # gps.start() - for line in data: - try: - pass - # result = gps.parse_sentence(line) - # if not result: - # break - - except ValueError: - app_base.logger.warning( - "Bad NMEA sentence:[{}]".format(line)) - raise - - # gps.publish() - # app_base.logger.debug( - # "See({})".format(gps.get_attributes())) - # client.send(data) - else: - break - - time.sleep(1.0) - - app_base.logger.info("Client disconnected") - client.close() - - # since this server is expected to run on a small embedded system, - # free up memory ASAP (see MEMORY note) - del client - gc.collect() - - return 0 - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("gps/gps_gate") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/demo/gps_gate/gps_gate_nmea.py b/demo/gps_gate/gps_gate_nmea.py deleted file mode 100644 index 4ae9a24e..00000000 --- a/demo/gps_gate/gps_gate_nmea.py +++ /dev/null @@ -1,733 +0,0 @@ -""" -handle NMEA sentences, in ways customized to Gpsgate expectations - -This file is used by demo.gps_gate.gps_gate_protocol.py! -""" -import time - -from cp_lib.parse_data import parse_float - - -class NmeaCollection(object): - """ - Hold the state of a connected NMEA device. - - We assume the sentences come in a cluster, and some of the sentences - contain duplicate values. - - So the paradigm is: - 1) call NmeaCollection.start() to prep the new data - 2) call NmeaCollection.parse_sentence() repeatedly - 3) call NmeaCollection.end() to 'end' the parsing of data & test filters - if end() returns True, then there has been enough of a change to - warrant uploading - 4) call NmeaCollection.publish() to formalize (confirm) we've uploaded - - """ - - # if True, then just claim an out-of-range setting, don't ignore - CLAMP_SETTINGS = True - - DISTANCE_FILTER_DEF = None - # at some point, GPS is not accurate enough to be like 1 inch! And for - # MAX, roughly 1/2 way around earth is probably silly - DISTANCE_FILTER_LIMITS = (1.0, 6000000.0, "distance") - - TIME_FILTER_DEF = "1 min" - # at most each 10 seconds (not too fast) and at least once a day - TIME_FILTER_LIMITS = (10.0, 86400.0, "time") - - SPEED_FILTER_DEF = None - # at most each 10 seconds (not too fast) and at least once a day - SPEED_FILTER_LIMITS = (1.0, 500.0, "speed") - - DIRECTION_FILTER_DEF = None - # direction/degrees limited to 0 to 360 - DIRECTION_FILTER_LIMITS = (1.0, 360.0, "direction") - - DIRECTION_THRESHOLD_DEF = None - # like distance - DIRECTION_THRESHOLD_LIMITS = (1.0, 6000000.0, "direction threshold") - - def __init__(self, _logger): - - # hold the last RMC data - self.gprmc = dict() - - # hold the last VYG data - self.gpvtg = dict() - - # hold the last GGA data - self.gpgga = dict() - - self.publish_time = None - - self.new_data = dict() - self.last_data = None - self.publish_reason = None - - self.logger = _logger - - # Distance in meters before a new update should be sent to server. - self.distance_filter = None - self.set_distance_filter(self.DISTANCE_FILTER_DEF) - - # Interval in seconds before a new position should be sent to server - self.time_filter = None - self.set_time_filter(self.TIME_FILTER_DEF) - - # Change in speed in meters per second, before a new position update - # should sent to server by client. - self.speed_filter = None - self.set_speed_filter(self.SPEED_FILTER_DEF) - - # Change of heading in degrees, before a new update should be sent. - self.direction_filter = None - self.set_direction_filter(self.DIRECTION_FILTER_DEF) - - # Distance in meters travelled before "DirectionFilter" is considered. - self.direction_threshold = None - self.set_direction_threshold(self.DIRECTION_THRESHOLD_DEF) - - return - - def set_distance_filter(self, value=None): - """ - Distance in meters before a new update should be sent to server. - - Distance must be > 0.0, and although logically it should not be - too large, I'm not sure what MAX would be. - """ - # test None, make float, test Min/Max, clamp or throw ValueError - value = self._test_value_limits(value, self.DISTANCE_FILTER_LIMITS) - - if value is None: - # then disable the distance filter - self.distance_filter = None - self.logger.debug("Disable distance_filter") - elif value != self.distance_filter: - self.distance_filter = value - self.logger.debug("Set distance_filter = {} m".format(value)) - return - - def set_time_filter(self, value): - """ - Interval in seconds before a new position should be sent to server - """ - from cp_lib.parse_duration import TimeDuration - - # handle the "null" or 0 times, as disable - value = self._test_value_as_none(value) - - if value is None: - # then disable the distance filter - if self.time_filter is not None: - self.logger.debug("Disable time_filter") - self.time_filter = None - else: - try: - duration = TimeDuration(value) - except AttributeError: - raise ValueError("Bad Time Filter Value:{}".format(value)) - - value = duration.get_seconds() - - # test Min/Max, clamp or throw ValueError - value = self._test_value_limits(value, self.TIME_FILTER_LIMITS) - if value != self.time_filter: - self.time_filter = value - self.logger.debug("Set time_filter = {} sec".format(value)) - return - - def set_speed_filter(self, value): - """ - Change in speed in meters per second, before a new position update - should sent to server. - """ - # test None, make float, test Min/Max, clamp or throw ValueError - value = self._test_value_limits(value, self.SPEED_FILTER_LIMITS) - - if value is None: - # then disable the distance filter - if self.speed_filter is not None: - self.logger.debug("Disable speed_filter") - self.speed_filter = None - elif value != self.speed_filter: - self.speed_filter = value - self.logger.debug("Set speed_filter = {} m/sec".format(value)) - return - - def set_direction_filter(self, value): - """ - Change in direction in degrees, before a new position update - should sent to server . - """ - # test None, make float, test Min/Max, clamp or throw ValueError - value = self._test_value_limits(value, self.DIRECTION_FILTER_LIMITS) - - if value is None: - # then disable the distance filter - if self.direction_filter is not None: - self.logger.debug("Disable direction_filter") - self.direction_filter = None - elif value != self.direction_filter: - self.direction_filter = value - self.logger.debug("Set direction_filter = {} deg".format(value)) - return - - def set_direction_threshold(self, value): - """ - Distance in meters travelled before "DirectionFilter" should be - considered. - """ - # test None, make float, test Min/Max, clamp or throw ValueError - value = self._test_value_limits(value, self.DIRECTION_THRESHOLD_LIMITS) - - if value is None: - # then disable the distance filter - if self.direction_threshold is not None: - self.logger.debug("Disable direction_threshold") - self.direction_threshold = None - else: - if value != self.direction_threshold: - self.direction_threshold = value - self.logger.debug( - "NMEA.set_direction_threshold = {} m".format(value)) - return - - @staticmethod - def _test_value_as_none(value): - """ - give a value, which might be float or string (etc) but in our - context of 'filters' we want to disable or set None - :param value: - :return: - """ - if value in (None, 0, 0.0): - # none of the filters are usable if '0' - return None - - if isinstance(value, bytes): - # make any string bytes - value = value.decode() - - if isinstance(value, str): - if value.lower() in ("", "0", "0.0", "none", "null"): - # none of the filters are usable if '0' - return None - - return value - - def _test_value_limits(self, value, limits): - """ - We assume 'limits' is like (min, max, name), so confirm the value in - within range. If CLAMP is true, we claim to MIN or MAX, else we - throw ValueError - - :param value: the value to test - like float - :param tuple limits: - :return: - """ - assert isinstance(limits, tuple) - assert len(limits) >= 3 - - value = self._test_value_as_none(value) - if value is None: - return None - - value = parse_float(value) - - if value < limits[0]: - # value is too low - if self.CLAMP_SETTINGS: - return limits[0] - else: - raise ValueError( - "Set {} filter={} is below MIN={}".format( - limits[2], value, limits[0])) - - if value > limits[1]: - # too high - if self.CLAMP_SETTINGS: - return limits[1] - else: - raise ValueError( - "Set {} filter={} is above MAX={}".format( - limits[2], value, limits[1])) - - # if still here, then is within range - return value - - def start(self, now=None): - """ - Start parsing a new cluster of sentences - - :param float now: force in a time-stamp, mainly for testing - :return: - """ - if now is None: - now = time.time() - - self.new_data = dict() - self.new_data['time'] = now - return - - def end(self): - """ - End parsing a cluster of sentences, so check out filters - - :return: (T/F, reason string) - :rtype: bool, str - """ - - if not self.new_data.get('valid', False): - # test if new_data is valid - for now never publish is RMC is bad - # at some point likely need to publish as 'bad' - return False, 'invalid' - - # we use self.filtered to RECORD if there are any filters, or not. - filtered = False - result = False - reason = "error" - - if self.last_data is None or self.last_data == {}: - # if no last data, a special case - self.last_data = self.new_data - return True, "first" - - # place the filter tests in roughly order of computational complexity - - if self.time_filter is not None: - # Interval in seconds before a new data should be sent to server - filtered = True - if self._test_time_filter(): - result = True - reason = "time" - - if not result and self.speed_filter is not None: - # Change in speed in meters per second - # we only test if result isn't True yet - filtered = True - if self._test_speed_filter(): - result = True - reason = "speed" - - if not result and self.distance_filter is not None: - # Distance in meters before a new update should be sent to server - # we only test if result isn't True yet - filtered = True - if self._test_distance_filter(): - result = True - reason = "distance" - - if not result and self.direction_filter is not None: - # Change of heading in degrees, before new update sent. - # we only test if result isn't True yet - filtered = True - if self._test_direction_filter(): - result = True - reason = "direction" - - # self.direction_threshold is handled within _test_direction_filter() - - if not result: - # if still here, one of two situations were true - if filtered: - # a) we have filter conditions, but NONE wanted to publish - # result is already False - reason = "filtered" - else: - # b) we have NO filter conditions, so always publish - result = True - reason = "always" - # else result == True, so assume 'reason' is valid already - - return result, reason - - def publish(self, now=None): - """ - End parsing a cluster of sentences - discard the older data and - shift the new data into its place - - :param float now: force in a time-stamp, mainly for testing - :return: - """ - # else, do not 'lose' the last valid or None - - if now is None: - now = time.time() - - if self.new_data.get("valid", False): - # only save if last was valid! - self.last_data = self.new_data - - self.publish_time = now - return True - - def report_list(self): - """ - return a list of 'details' for a report - :return: - """ - report = [] - if self.time_filter is not None: - report.append("Time Filter = %0.2f sec" % float(self.time_filter)) - if self.speed_filter is not None: - report.append( - "Speed Filter = %0.2f m/sec" % float(self.speed_filter)) - if self.distance_filter is not None: - report.append( - "Distance Filter = %0.1f m" % float(self.distance_filter)) - if self.direction_filter is not None: - report.append( - "Direction Filter = %0.1f deg" % float(self.direction_filter)) - if self.direction_threshold is not None: - report.append( - "Direction Threshold = %0.1f m" % float( - self.direction_threshold)) - - if 'raw' in self.gprmc: - report.append(self.gprmc['raw']) - if 'raw' in self.gprmc: - report.append(self.gpvtg['raw']) - if 'raw' in self.gprmc: - report.append(self.gpgga['raw']) - - if self.new_data.get('valid', False): - report.append("Newest: Lat:%f Long:%f Alt:%0.1f m" % - (self.new_data['latitude'], - self.new_data['longitude'], - self.new_data['altitude'])) - report.append( - "Newest: time:%s satellite_count:%d" % - (time.strftime("%Y-%m-%d %H:%M:%S", - time.localtime(self.new_data['time'])), - self.new_data['satellites'])) - else: - report.append("Newest Data is invalid!") - - if self.publish_time is None: - report.append("Never Published") - else: - report.append( - "Last Publish:%s (%d seconds ago)" % - (time.strftime("%Y-%m-%d %H:%M:%S", - time.localtime(self.publish_time)), - time.time() - self.publish_time)) - - return report - - def parse_sentence(self, raw_sentence): - """ - - :param str raw_sentence: - :return: - """ - from cp_lib.gps_nmea import strip_sentence, calc_checksum - - sentence, seen_csum = strip_sentence(raw_sentence) - - if seen_csum is not None: - if seen_csum != calc_checksum(sentence): - raise ValueError("Invalid NMEA checksum") - - tokens = sentence.split(',') - if len(tokens) <= 1: - raise ValueError("Bad NMEA format") - - # index = 0 - # for x in tokens: - # print("[%02d]=%s" % (index, x)) - # index += 1 - - result = False - if tokens[0] == "GPRMC": - # recommended minimum standard - result = self.parse_rmc(tokens, raw_sentence) - - elif tokens[0] == "GPVTG": - # recommended minimum standard - result = self.parse_vtg(tokens, raw_sentence) - - elif tokens[0] == "GPGGA": - # Global Positioning System Fixed Data - result = self.parse_gga(tokens, raw_sentence) - - return result - - def parse_rmc(self, tokens, raw_sentence): - """ - Recommended minimum standard - - GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0,353.2, - 020516,0.0,E,A - - :param list tokens: - :param str raw_sentence: - :return: - """ - if len(tokens) < 9: - raise ValueError("NMEA RMC is too short.") - assert tokens[0] == "GPRMC" - - self.gprmc = dict() - self.gprmc['time'] = time.time() - self.gprmc['raw'] = raw_sentence - self.gprmc['tokens'] = tokens - - if tokens[2] == 'A': - # then is valid - self.new_data['valid'] = True - else: - self.new_data['valid'] = False - return False - - # convert to full decimal - if 'latitude' not in self.new_data: - self.new_data['latitude'] = self._parse_latitude( - tokens[3], tokens[4]) - - if 'longitude' not in self.new_data: - self.new_data['longitude'] = self._parse_longitude( - tokens[5], tokens[6]) - - if 'knots' not in self.new_data: - # self.new_data['knots'] = float(tokens[7]) - # 0.514444444 × Vkn = Vm/sec - self.new_data['mps'] = float(tokens[7]) * 0.514444444 - - if 'course' not in self.new_data and len(tokens[8]): - # if null, is unknown - self.new_data['course'] = float(tokens[8]) - - return True - - def parse_vtg(self, tokens, raw_sentence): - """ - GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A - - :param list tokens: - :param str raw_sentence: - :return: - """ - if len(tokens) < 8: - raise ValueError("NMEA VTG is too short.") - - assert tokens[0] == "GPVTG" - - self.gpvtg = dict() - self.gpvtg['time'] = time.time() - self.gpvtg['raw'] = raw_sentence - self.gpvtg['tokens'] = tokens - - if 'course' not in self.new_data and len(tokens[1]): - # if null, is unknown - self.new_data['course'] = float(tokens[1]) - - if 'knots' not in self.new_data: - self.new_data['knots'] = float(tokens[5]) - # 0.514444444 × Vkn = Vm/sec - self.new_data['mps'] = self.new_data['knots'] * 0.514444444 - - return True - - def parse_gga(self, tokens, raw_sentence): - """ - Global Positioning System Fixed Data - - GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A - - :param list tokens: - :param str raw_sentence: - :return: - """ - if len(tokens) < 8: - raise ValueError("NMEA GGA is too short.") - - assert tokens[0] == "GPGGA" - - self.gpgga = dict() - self.gpgga['time'] = time.time() - self.gpgga['raw'] = raw_sentence - self.gpgga['tokens'] = tokens - - # convert to full decimal - if 'latitude' not in self.new_data: - self.new_data['latitude'] = self._parse_latitude( - tokens[2], tokens[3]) - - if 'longitude' not in self.new_data: - self.new_data['longitude'] = self._parse_longitude( - tokens[4], tokens[5]) - - # always do the number of satellites - if 'satellites' not in self.new_data: - self.new_data['satellites'] = int(tokens[7]) - - if 'altitude' not in self.new_data: - self.new_data['altitude'] = float(tokens[9]) - - return True - - @staticmethod - def _parse_latitude(latitude, north): - """ - Parse the 2 fields: latitude and N/S indicator - - GPS returns as ddmm.mmm, or 'degree & decimal minutes' - - :param str latitude: - :param str north: - :rtype: float - """ - assert latitude[4] == '.' - value = float(latitude[:2]) + float(latitude[2:]) / 60.0 - if north == 'S': - # 'N' is +, 'S' is negative - value = -value - - return value - - @staticmethod - def _parse_longitude(longitude, west): - """ - Parse the 2 fields: longitude and W/E indicator - - GPS returns as dddmm.mmm, or 'degree & decimal minutes' - - :param str longitude: - :param str west: - :return: - :rtype: float - """ - assert longitude[5] == '.' - value = float(longitude[:3]) + float(longitude[3:]) / 60.0 - if west == 'W': - # 'E' is +, 'W' is negative - value = -value - return value - - @staticmethod - def _distance_meters(lon1, lat1, lon2, lat2): - """ - Calculate the great circle distance between two points - on the earth (specified in decimal degrees) - - :param float lon1: first longitude (as decimal degrees) - :param float lat1: first latitude (as decimal degrees) - :param float lon2: second longitude (as decimal degrees) - :param float lat2: second latitude (as decimal degrees) - :return float: distance in meters - """ - from math import radians, cos, sin, sqrt, atan2 - - # convert decimal degrees to radians, use haversine formula - diff_lon = radians(lon2 - lon1) - diff_lat = radians(lat2 - lat1) - a = sin(diff_lat / 2) * sin(diff_lat / 2) + cos(radians(lat1)) \ - * cos(radians(lat2)) * sin(diff_lon / 2) * sin(diff_lon / 2) - # c = 2 * asin(sqrt(a)) - c = 2 * atan2(sqrt(a), sqrt(1 - a)) - km = 6371 * c - return km * 1000.0 - - def _test_time_filter(self): - """ - Interval in seconds before a new data should be sent to server - - :return: - """ - if 'time' in self.new_data and 'time' in self.last_data: - delta = self.new_data['time'] - self.last_data['time'] - # print("Time Filter={} sec; delta={}".format(self.time_filter, - # delta)) - if delta >= self.time_filter: - return True - - # else if either value missing, skip! - return False - - def _test_distance_filter(self): - """ - Distance in meters before a new update should be sent to server - - :return: - """ - delta = self.__get_raw_distance() - print("Distance Filter={} meters; delta={}".format( - self.distance_filter, delta)) - if delta is not None and delta >= self.distance_filter: - return True - - # else if either value missing, skip! - return False - - def __get_raw_distance(self): - """ - Warp test, confirming all required data exists - - :return: - """ - if 'longitude' in self.new_data and 'latitude' in self.new_data and \ - 'longitude' in self.last_data and \ - 'latitude' in self.last_data: - return self._distance_meters( - self.new_data['longitude'], self.new_data['latitude'], - self.last_data['longitude'], self.last_data['latitude']) - - # else if either value missing, skip! - return None - - def _test_speed_filter(self): - """ - Change in speed in meters per second - - :return: - """ - if 'mps' in self.new_data and 'mps' in self.last_data: - delta = abs(self.new_data['mps'] - self.last_data['mps']) - print("Speed Filter={} mps; delta={}".format( - self.speed_filter, delta)) - if delta >= self.speed_filter: - return True - - # else if either value missing, skip! - return False - - def _test_direction_filter(self): - """ - Change in speed in meters per second - - :return: - """ - if 'course' in self.new_data and 'course' in self.last_data: - # Change of heading in degrees, before new update sent. - - # handle left or right course change, for example turn 10 deg - # one way can be seen as 350 degrees the other way! - if self.new_data['course'] > self.last_data['course']: - delta = self.new_data['course'] - self.last_data['course'] - else: - delta = self.last_data['course'] - self.new_data['course'] - delta = min(delta, 360 - delta) - print("Direction Filter={}; delta={}".format( - self.direction_filter, delta)) - if delta < self.direction_filter: - # then won't apply, not enough change - return False - - # else MIGHT apply, check the threshold - if self.direction_threshold is not None: - # Distance in meters travelled before "DirectionFilter" - # should be considered. - delta = self.__get_raw_distance() - print("Directional Distance Threshold={}; delta={}".format( - self.distance_filter, delta)) - if delta < self.direction_threshold: - # have not travelled far enough - return False - - # had enough change, and threshold is null or satisfied - return True - - return False diff --git a/demo/gps_gate/gps_gate_protocol.py b/demo/gps_gate/gps_gate_protocol.py deleted file mode 100644 index 438ba0e2..00000000 --- a/demo/gps_gate/gps_gate_protocol.py +++ /dev/null @@ -1,641 +0,0 @@ -""" -Received GPS, assuming the router's GPS function sends new data (sentences) -to a localhost port -""" -import copy - -import cp_lib.gps_nmea as gps_nmea -from demo.gps_gate.gps_gate_nmea import NmeaCollection -from cp_lib.parse_data import clean_string, parse_integer -from cp_lib.split_version import split_version_string - - -class GpsGate(object): - - DEF_CLIENT_USERNAME = None - DEF_CLIENT_PASSWORD = None - DEF_CLIENT_IMEI = None - DEF_SERVER_MAJOR_VERSION = 1 - DEF_SERVER_MINOR_VERSION = 1 - - # update these 2 as you desire - # DEF_TRACKER_NAME = "Cradlepoint SDK" - DEF_TRACKER_NAME = "GpsGate TrackerOne" - DEF_CLIENT_NAME = "Cradlepoint" - DEF_CLIENT_VERSION = "1.0" - - OPT_USE_ONLY_TCP = False - - DEF_GPS_GATE_URL = 'online.gpsgate.com' - DEF_GPS_GATE_PORT = 30175 - DEF_GPS_GATE_TRANSPORT = 'tcp' - - # our small state-machine - STATE_OFFLINE = 'offline' - STATE_TRY_LOGIN = 'login' - STATE_HAVE_SESSION = 'session' - STATE_WAIT_SERVER_VERSION = 'wait_ver' - STATE_HAVE_SERVER_VERSION = 'have_ver' - STATE_ASK_UPDATE = 'update' - STATE_FORWARD_READY = 'ready' - STATE_FORWARDING = 'forwarding' - - def __init__(self, _logger): - self.client_username = self.DEF_CLIENT_USERNAME - self.client_password = self.DEF_CLIENT_PASSWORD - self.client_imei = self.DEF_CLIENT_IMEI - self.server_major_version = self.DEF_SERVER_MAJOR_VERSION - self.server_minor_version = self.DEF_SERVER_MINOR_VERSION - self.tracker_name = self.DEF_TRACKER_NAME - self.client_name = self.DEF_CLIENT_NAME - self.client_version = self.DEF_CLIENT_VERSION - - self.gps_gate_url = self.DEF_GPS_GATE_URL - self.gps_gate_port = self.DEF_GPS_GATE_PORT - self.gps_gate_transport = self.DEF_GPS_GATE_TRANSPORT - - self.state = self.STATE_OFFLINE - self.session = None - self.server_title = None - self.return_as_bytes = True - - self.logger = _logger - - # hold the NMEA data and filter-states to drive uploading - self.nmea = NmeaCollection(_logger) - return - - def reset(self): - """ - - :return: - """ - self.state = self.STATE_OFFLINE - self.session = None - return - - def get_next_client_2_server(self): - """ - A simple state machine, as defined in GpsGateServerProtocol200.pdf - on page 18 of 26 - - :return: - """ - self.logger.debug("get_next() entry state:{}".format(self.state)) - - if self.state == self.STATE_OFFLINE: - # then we're first starting - message = self.form_frlin() - self.state = self.STATE_TRY_LOGIN - - elif self.state == self.STATE_TRY_LOGIN: - # hmm, we shouldn't still be here, but start again - message = self.form_frlin() - self.logger.warning("stuck in LOGIN state?") - - elif self.state == self.STATE_HAVE_SESSION: - # we sent login; server replied with session - # we should send our version expectations - message = self.form_frver() - self.state = self.STATE_WAIT_SERVER_VERSION - - elif self.state == self.STATE_WAIT_SERVER_VERSION: - # hmm, we shouldn't still be here, but start again - message = self.form_frver() - self.logger.warning("stuck in WAIT SERVER VERSION state?") - - elif self.state == self.STATE_HAVE_SERVER_VERSION: - # ask the server for update rules - message = self.form_frcmd_update() - self.state = self.STATE_ASK_UPDATE - - elif self.state == self.STATE_FORWARD_READY: - # we sent our version, server replied its version - message = self.form_frwdt() - self.state = self.STATE_FORWARDING - - else: - self.logger.error("unexpected state:{}".format(self.state)) - self.state = self.STATE_OFFLINE - message = None - - self.logger.debug("get_next() exit state:{}".format(self.state)) - - if self.return_as_bytes: - message = message.encode() - - return message - - def parse_message(self, source): - """ - Feed in RESPONSES from GpsGate - - :param source: - :return: - """ - if isinstance(source, bytes): - source = source.decode() - - # source MIGHT be more than one messages! - # source = '$FRVAL,TimeFilter,60.0*42\r\n$FRVAL,SpeedFilter,2.8*' + - # '0C\r\n$FRVAL,DirectionFilter,30.0*37\r\n$FRVAL,' + - # 'DirectionThreshold,10.0*42\r\n' - tokens = source.split('\n') - - result = True - for one in tokens: - if len(one) > 0: - if not self._parse_one_message(one): - result = False - - return result - - def _parse_one_message(self, source): - """ - - :param source: - :return: - """ - # clean off the data - sentence, seen_csum = gps_nmea.strip_sentence(source) - - if seen_csum is not None: - # then we had one, check it - calc_csum = gps_nmea.calc_checksum(sentence) - if seen_csum != calc_csum: - raise ValueError( - "Bad NMEA checksum, saw:%02X expect:%02X" % - (seen_csum, calc_csum)) - - # self.logger.debug("checksum={}".format(seen_csum)) - - # break up the sentence - tokens = sentence.split(',') - - if tokens[0] == "FRVAL": - # setting a name-space value - result = self._parse_frval(tokens) - - elif tokens[0] == "FRRET": - # we have a command response - result = self._parse_frret(tokens) - - elif tokens[0] == "FRSES": - # we have a session! - result = self._parse_frses(tokens) - - elif tokens[0] == "FRVER": - # we have the server version - result = self._parse_frver(tokens) - - # elif tokens[0] == "FRERR": - # # we have a server error - # result = self._parse_frerr(tokens) - # # '$FRERR,AuthError,Wrong username or password*56\r\n' - # - else: - raise ValueError("Unknown response:{}".format(source)) - - return result - - def get_my_identifier(self): - """ - - :return: - """ - if self.client_imei is not None: - return self.client_imei - return self.client_name - - def get_frcmd_id(self, value: str): - """ - - :return: - """ - return "FRCMD," + self.get_my_identifier() + "," + value - - def get_frret_id(self, value: str): - """ - - :return: - """ - return "FRRET," + self.get_my_identifier() + "," + value - - def set_username(self, value: str): - """ - Set the user-name used with GpsGate Server. There don't seem to - be any 'rules' about what makes a valid user-name. - - Insure is string, remove extra quotes, etc. - """ - value = clean_string(value) - if self.client_username != value: - self.client_username = value - self.logger.info("GpsGate: Setting user name:{}".format(value)) - - def set_password(self, value): - """make string, remove extra quotes, etc""" - value = clean_string(value) - if self.client_password != value: - self.client_password = value - self.logger.info("GpsGate: Setting new PASSWORD:****") - - def set_imei(self, value): - """make string, remove extra quotes, etc""" - value = clean_string(value) - if self.client_imei != value: - self.client_imei = value - self.logger.info("GpsGate: Setting IMEI:{}".format(value)) - - def set_client_name(self, name: str, version=None): - """ - Set client name. if version is none, then we assume name is like - "my tool 1.4" 7 try to parse off the ending float. - - TODO At the moment, this routine assumes version is like x.y, with y - being only 1 digit. So 1.2 is okay, but 1.22 is not - """ - change = False - - name = clean_string(name) - if version is None: - # assume version is on end of name! - name = name.split(' ') - if len(name) <= 1: - # then is a single work/token - assume version 1.0 - version = 1.0 - name = name[0] - else: - if name[-1].find('.') >= 0: - version = name[-1] - name.pop(-1) - else: - version = 1.0 - name = ' '.join(name) - - if isinstance(version, int): - version = float(version) - - if isinstance(version, float): - version = str(version) - - if self.client_name != name: - self.client_name = name - change = True - - if self.client_version != version: - self.client_version = version - change = True - - if change and self.logger is not None: - self.logger.info( - "GpsGate: Setting client name:{} version:{}".format( - self.client_name, self.client_version)) - - def set_server_version(self, value): - """ - make string, remove extra quotes, etc. We assume will be like '1.1' - so we need to split into 2 ints for GpsGate protocol - """ - major, minor = split_version_string(value) - if major != self.server_major_version or \ - minor != self.server_minor_version: - self.server_major_version = major - self.server_minor_version = minor - self.logger.info( - "GpsGate: Setting server version:%d.%d" % (major, minor)) - - def set_server_url(self, value): - """save the GpsGate server URL""" - value = clean_string(value) - if self.gps_gate_url != value: - self.gps_gate_url = value - self.logger.info("GpsGate: Setting Server URL:{}".format(value)) - - def set_server_port(self, value): - """save the GpsGate server port""" - value = parse_integer(value) - if self.gps_gate_port != value: - self.gps_gate_port = value - self.logger.info("GpsGate: Setting Server port:{}".format(value)) - - def set_server_transport(self, value): - """save the GpsGate server transport - ('tcp' or 'xml')""" - value = clean_string(value).lower() - if self.gps_gate_transport != value: - if value == 'xml': - raise NotImplementedError("GpsGate XML transport - not yet") - elif value != 'tcp': - raise ValueError("GpsGate - only TCP transport supported") - self.gps_gate_transport = value - self.logger.info("GpsGate: Setting Transport:{}".format(value)) - - def form_frcmd_reset(self): - """ - $FRCMD,IMEI,_DeviceReset - - The return is wrapped with the NMEA checksum - - :return: - :rtype: str - """ - return gps_nmea.wrap_sentence(self.get_frcmd_id("_Device_Reset")) - - @staticmethod - def form_frcmd_update(): - """ - Request server's update rules - - :return: - :rtype: str - """ - return copy.copy("$FRCMD,,_getupdaterules,Inline*1E") - - def form_frcmd_imei(self, gps: dict): - """ - <-- TrackerOne to GpsGate Server GPRS or SMS - $FRCMD,IMEI,_SendMessage,,latitude,hemi,longitude,hemi,alt, - speed,heading,date,time,valid - - The return is wrapped with the NMEA checksum - - :return: - :rtype: str - """ - if not isinstance(gps, dict): - raise TypeError("Invalid GPS INFO type") - - sentence = self.get_frcmd_id("_SendMessage") - - try: - lat = float(gps[gps_nmea.NmeaStatus.LATITUDE]) - if lat < 0.0: - lat_h = 'S' - else: - lat_h = 'N' - - long = float(gps[gps_nmea.NmeaStatus.LONGITUDE]) - if long < 0.0: - long_h = 'W' - else: - long_h = 'E' - - sentence += "%f,%s,%f,%s," % (lat, lat_h, long, long_h) - - except KeyError: - raise ValueError() - - # these are optional - alt = gps.get(gps_nmea.NmeaStatus.ALTITUDE, 0.0) - speed = gps.get(gps_nmea.NmeaStatus.SPEED_KNOTS, 0.0) - heading = gps.get(gps_nmea.NmeaStatus.COURSE_TRUE, "") - raw_date = gps.get(gps_nmea.NmeaStatus.RAW_DATE, "") - raw_time = gps.get(gps_nmea.NmeaStatus.RAW_TIME, "") - sentence += "%f,%f,%s,%s,%s" % (alt, speed, heading, - raw_date, raw_time) - - sentence = gps_nmea.wrap_sentence(sentence) - return sentence - - def form_frlin(self, force_imei=True): - """ - Form the $FRLIN sentence, assuming the SELF values are valid: - * self.server_major_version - * self.server_minor_version - * self.client_name_version - - The return is wrapped with the NMEA checksum - - :return: - :rtype: str - """ - - if not force_imei and self.client_username is not None: - # we prefer the username - when set! - if not isinstance(self.client_username, str): - raise ValueError("FRLIN Error: user name is not string") - if not isinstance(self.client_password, str): - raise ValueError("FRLIN Error: password is not string") - sentence = "FRLIN,,{0},{1}".format( - self.client_username, - self._encrypt_password(self.client_password)) - else: - if not isinstance(self.client_imei, str): - raise ValueError("FRLIN Error: IMEI is not string") - sentence = "FRLIN,IMEI,{0},".format(self.client_imei) - sentence = gps_nmea.wrap_sentence(sentence) - return sentence - - def form_frret_imei(self): - """ - Form the $FRRET,IMEI response for server - - The return is wrapped with the NMEA checksum - - :return: - :rtype: str - """ - sentence = self.get_frret_id( - "_GprsSettings") + ",,{0},{1},{2}".format( - self.DEF_TRACKER_NAME, self.client_name, self.client_version) - - if self.OPT_USE_ONLY_TCP: - sentence += ",tcp" - sentence = gps_nmea.wrap_sentence(sentence) - return sentence - - def form_frver(self): - """ - Form the $FRVER sentence, assuming the SELF values are valid: - * self.server_major_version - * self.server_minor_version - * self.client_name_version - - The return is wrapped with the NMEA checksum - - :return: - :rtype: str - """ - sentence = "FRVER,{0},{1},{2} {3}".format( - self.server_major_version, self.server_minor_version, - self.client_name, self.client_version) - sentence = gps_nmea.wrap_sentence(sentence) - return sentence - - @staticmethod - def form_frwdt(): - """ - Start our NMEA forwarding - is fixed message. We return a COPY just - to prevent someone doing something wonky to 'the original', like - make lower case or affect NMEA wrapping - - :return: - :rtype: str - """ - return copy.copy('$FRWDT,NMEA*78') - - def _parse_frret(self, tokens): - """ - Parse a server response cms - - :param list tokens: - :return: - """ - if len(tokens) < 4: - raise ValueError("FRRET is too short.") - - assert tokens[0] == "FRRET" - - # we'll ignore the user name or context - - value = tokens[2].lower() - - if value == '_getupdaterules': - # then this is server's response to our update query - if self.state == self.STATE_ASK_UPDATE: - # then expected - self.state = self.STATE_FORWARD_READY - - else: # then unexpected! - self.logger.warning( - "Unexpected FRRET _getupdaterules response") - - # ignore - not sure the significance - # value = tokens[3].lower() - # if value != 'nmea': - # self.logger.warning( - # "FRRET: Unexpected server major number, saw:" + - # "{} expect:{}".format(value, self.server_major_version)) - - else: - self.logger.warning("FRRET: Unexpected cmd:{}".format(value)) - return False - - return True - - def _parse_frses(self, tokens): - """ - Parse the SESSION response from server - - :param list tokens: - :return: - """ - if len(tokens) < 2: - raise ValueError("FRSES is too short.") - - assert tokens[0] == "FRSES" - - if self.state == self.STATE_TRY_LOGIN: - # then expected - self.state = self.STATE_HAVE_SESSION - - else: # then unexpected! - if self.logger is not None: - self.logger.warning("Unexpected FRSES response") - - self.session = tokens[1] - if self.logger is not None: - self.logger.debug( - "Recording server SESSION:{}".format(self.session)) - return True - - def _parse_frval(self, tokens): - """ - Parse a value in our name-space - - FRVAL,DistanceFilter,500.0 - - :param list tokens: - :return: - """ - if len(tokens) < 3: - raise ValueError("FRVAL is too short.") - - assert tokens[0] == "FRVAL" - - name = tokens[1].lower() - - if name == "distancefilter": - self.nmea.set_distance_filter(tokens[2]) - - elif name == "timefilter": - self.nmea.set_time_filter(tokens[2]) - - elif name == "speedfilter": - self.nmea.set_speed_filter(tokens[2]) - - elif name == "directionfilter": - self.nmea.set_direction_filter(tokens[2]) - - elif name == "directionthreshold": - self.nmea.set_direction_threshold(tokens[2]) - - return True - - def _parse_frver(self, tokens): - """ - Parse the SESSION response from server - - :param list tokens: - :return: - """ - if len(tokens) < 4: - raise ValueError("FRVER is too short.") - - assert tokens[0] == "FRVER" - - if self.state == self.STATE_WAIT_SERVER_VERSION: - # then expected - self.state = self.STATE_HAVE_SERVER_VERSION - - else: # then unexpected! - self.logger.warning("Unexpected FRVER response") - - value = parse_integer(tokens[1]) - if value != self.server_major_version: - self.logger.warning( - "FRVER: Unexpected server major number, saw:" + - "{} expect:{}".format(value, self.server_major_version)) - self.server_major_version = value - - value = parse_integer(tokens[2]) - if value != self.server_minor_version: - self.logger.warning( - "FRVER: Unexpected server minor number, saw:" + - "{} expect:{}".format(value, self.server_minor_version)) - self.server_minor_version = value - - self.server_title = tokens[3] - self.logger.debug( - "Recording server TITLE:{}".format(self.server_title)) - return True - - @staticmethod - def _encrypt_password(value: str): - """ - Do the simple adjustment per spec - - Encryption sample: "coolness" -> "HHVMOLLX" - - :param value: - :return: - """ - assert isinstance(value, str) - result = "" - - for ch in value: - i = ord(ch) - if '0' <= ch <= '9': - # result += (9 - (i - '0') + '0') - result += chr(9 - (i - 48) + 48) - - elif 'a' <= ch <= 'z': - # (char)(('z' - 'a') - (c - 'a') + 'A')) - result += chr(25 - (i - 97) + 65) - - elif 'A' <= ch <= 'Z': - # (char)(('Z' - 'A') - (c - 'A') + 'a')); - result += chr(25 - (i - 65) + 97) - - # use extended slice to reverse string - return result[::-1] diff --git a/demo/gps_gate/settings.ini b/demo/gps_gate/settings.ini deleted file mode 100644 index b41e3535..00000000 --- a/demo/gps_gate/settings.ini +++ /dev/null @@ -1,55 +0,0 @@ -; one app settings - -[application] -name=gps_gate -description=Receive NMEA sentences on localhost port, send to GpsGate -path = demo/gps_gate -uuid=9246bb79-db63-4243-a872-db0cbce5dcf3 -version=1.1 - -[gps_gate] -; do NOT set these if you desire Router API in config/system/gps to be used! -; these OVER-RIDE the router api values -; host_ip=192.168.1.6 -; host_port=9992 - -; likely, all you need is the URL assigned to you, and the server matches up -; data per the imei, which is technically (I beleive) just a unique -; shared identity (or secret of sorts) -gps_gate_url=64.46.40.178 -; gps_gate_port=30175 -; gps_gate_transport=tcp - -; this code does NOT use username/password, although it does read them -; username=admin -; password={as needed} -; server_version=1.1 -; client=Cradlepoint 1.0 - -; do NOT set if you desire Router API in config/wan to be used! -; imei=353547060660845 - -; can pre-set the filters here, but Gpsgate may try to override -; setting any filter to 0, 0.0, None, or null to disable - -; Distance Filter in meters, how far to travel between uploads -; module clamps to between 1 and 6,000,000 meters (~ half way around earth) -; distance_filter=10.0 - -; Time filter in seconds, how many seconds between min upload rate -; supports tags in ('sec', 'min', 'hr', 'day'), so '5 min' is fine -; module clamps to between 10 and 86400 seconds (1/once a day) -; time_filter=1 min - -; Speed filter in m/sec, how much speed change between uploads -; module clamps to between 1 and 500 m/sec -; speed_filter=4 - -; Direction filter in degrees, how much course change between uploads -; module clamps to between 1 and 360 degrees -; direction_filter=25 - -; Direction threshold in meters, a modifier to direction/course change -; of how many meters must be travelled to consider the change meaningful -; module clamps to between 1 and 6,000,000 meters (~ half way around earth) -; direction_threshold=25 diff --git a/demo/gps_gate/test/connect.py b/demo/gps_gate/test/connect.py deleted file mode 100644 index 0b58dca3..00000000 --- a/demo/gps_gate/test/connect.py +++ /dev/null @@ -1,232 +0,0 @@ -# Test the gps.gps_gate.gps_gate_protocol module - -import socket -import time - -import demo.gps_gate.gps_gate_protocol as protocol -from cp_lib.gps_nmea import fix_time_sentence -from cp_lib.load_gps_config import GpsConfig -from cp_lib.load_active_wan import ActiveWan -from cp_lib.parse_data import clean_string, parse_integer - -# Here is a sample TRACE of this code running -# -# INFO:make:GpsGate: Setting IMEI:353547060660845 -# INFO:make:GpsGate: Setting Server URL:64.46.40.178 -# INFO:make:GpsGate: Setting user name:Admin -# INFO:make:GpsGate: Setting new PASSWORD:**** -# INFO:make:Preparing to connect on TCP socket ('64.46.40.178', 30175) -# DEBUG:make:get_next() entry state:offline -# DEBUG:make:get_next() exit state:login -# DEBUG:make:Req(b'$FRLIN,IMEI,353547060660845,*47\r\n') -# DEBUG:make:Rsp(b'$FRSES,83*76\r\n') -# DEBUG:make:Recording server SESSION:83 -# DEBUG:make:get_next() entry state:session -# DEBUG:make:get_next() exit state:wait_ver -# DEBUG:make:Req(b'$FRVER,1,1,Cradlepoint 1.0*27\r\n') -# DEBUG:make:Rsp(b'$FRVER,1,1,GpsGate Server 3.0.0.2583*3E\r\n') -# DEBUG:make:Recording server TITLE:GpsGate Server 3.0.0.2583 -# DEBUG:make:get_next() entry state:have_ver -# DEBUG:make:get_next() exit state:update -# DEBUG:make:Req(b'$FRCMD,,_getupdaterules,Inline*1E') -# DEBUG:make:Rsp(b'$FRRET,IBR350,_getupdaterules,Nmea,5*6F\r\n') -# DEBUG:make:Rsp(b'$FRVAL,DistanceFilter,500.0*67\r\n$FRVAL,TimeFilter, -# 60.0*42\r\n$FRVAL,SpeedFilter,2.8*0C\r\n$FRVAL,DirectionFilter, -# 30.0*37\r\n$FRVAL,DirectionThreshold,10.0*42\r\n') -# DEBUG:make:NMEA.set_distance_filter = 500.0 m -# DEBUG:make:NMEA.set_time_filter = 60.0 sec -# DEBUG:make:NMEA.set_speed_filter = 2.8 m/sec -# DEBUG:make:NMEA.set_direction_filter = 30.0 deg -# DEBUG:make:NMEA.set_direction_threshold = 10.0 m -# ERROR:make:No response - jump from loop! -# DEBUG:make:get_next() entry state:ready -# DEBUG:make:get_next() exit state:forwarding -# DEBUG:make:Req(b'$FRWDT,NMEA*78') -# DEBUG:make:Req(b'$GPGGA,143736.0,4334.784909,N,11612.766448,W,1,09,0.9, -# 830.6,M,-11.0,M,,*6B\r\n') - - -def run_client(): - global base_app - - obj = protocol.GpsGate(base_app.logger) - - # base_app.logger.debug("sets:{}".format(base_app.settings)) - - # set a few defaults - imei = "353547060660845" - gps_gate_url = "64.46.40.178" - gps_gate_port = 30175 - - if "gps_gate" in base_app.settings: - # then we do have a customer config - temp = base_app.settings["gps_gate"] - - # check on our localhost port (not used, but to test) - config = GpsConfig(base_app) - host_ip, host_port = config.get_client_info() - if "host_ip" in temp: - # then OVER-RIDE what the router told us - base_app.logger.warning("Settings OVER-RIDE router host_ip") - value = clean_string(temp["host_ip"]) - base_app.logger.warning("was:{} now:{}".format(host_ip, value)) - host_ip = value - - if "host_port" in temp: - # then OVER-RIDE what the router told us - base_app.logger.warning("Settings OVER-RIDE router host_port") - value = parse_integer(temp["host_port"]) - base_app.logger.warning("was:{} now:{}".format(host_port, value)) - host_port = value - - base_app.logger.debug("GPS source:({}:{})".format(host_ip, host_port)) - del config - - # check on our cellular details - config = ActiveWan(base_app) - imei = config.get_imei() - if "imei" in temp: - # then OVER-RIDE what the router told us - base_app.logger.warning("Settings OVER-RIDE router IMEI") - value = clean_string(temp["imei"]) - base_app.logger.warning("was:{} now:{}".format(imei, value)) - imei = value - del config - - if "gps_gate_url" in temp: - # load the settings.ini value - gps_gate_url = clean_string(temp["gps_gate_url"]) - - if "gps_gate_port" in temp: - # load the settings.ini value - gps_gate_port = parse_integer(temp["gps_gate_port"]) - - obj.set_imei(imei) - obj.set_server_url(gps_gate_url) - obj.set_server_port(gps_gate_port) - - # we never need these! - # obj.set_username('Admin') - # obj.set_password(':Vm78!!') - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - sock.settimeout(2.0) - address = (obj.gps_gate_url, obj.gps_gate_port) - - base_app.logger.info( - "Preparing to connect on TCP socket {}".format(address)) - - # attempt to actually lock the resource, which may fail if unavailable - # (see CONNECT ERROR note) - try: - sock.connect(address) - except OSError as msg: - base_app.logger.error("socket.connect() failed - {}".format(msg)) - - return -1 - - # first test - try our user name: - # Req = $FRLIN,, Admin,12Ne*14\r\n - # Rsp = $FRERR,AuthError,Wrong username or password*56\r\n - - # second test - try our IMEI: - # Req = $FRLIN,IMEI,353547060660845,*47\r\n - # Rsp = $FRSES,75*7F\r\n - - # expect Req = $FRLIN,IMEI,353547060660845,*47\r\n - request = obj.get_next_client_2_server() - base_app.logger.debug("Req({})".format(request)) - sock.send(request) - - # expect Rsp = $FRSES,75*7F\r\n - try: - response = sock.recv(1024) - base_app.logger.debug("Rsp({})".format(response)) - result = obj.parse_message(response) - if not result: - base_app.logger.debug("parse result({})".format(result)) - except socket.timeout: - base_app.logger.error("No response - one was expected!") - return -1 - - # expect Req = $FRVER,1,1,Cradlepoint 1.0*27\r\n - request = obj.get_next_client_2_server() - base_app.logger.debug("Req({})".format(request)) - sock.send(request) - - # expect Rsp = $FRVER,1,1,GpsGate Server 3.0.0.2583*3E\r\n - try: - response = sock.recv(1024) - base_app.logger.debug("Rsp({})".format(response)) - result = obj.parse_message(response) - if not result: - base_app.logger.debug("parse result({})".format(result)) - except socket.timeout: - base_app.logger.error("No response - one was expected!") - return -1 - - # expect Req = $FRCMD,,_getupdaterules,Inline*1E - request = obj.get_next_client_2_server() - base_app.logger.debug("Req({})".format(request)) - sock.send(request) - - # expect Rsp = $FRRET,User1,_getupdaterules,Nmea,2*07 - try: - response = sock.recv(1024) - base_app.logger.debug("Rsp({})".format(response)) - result = obj.parse_message(response) - if not result: - base_app.logger.debug("parse result({})".format(result)) - except socket.timeout: - base_app.logger.error("No response - one was expected!") - return -1 - - # now we LOOP and process the rules! - while True: - # expect Rsp = $FRVAL,DistanceFilter,500.0*67 - try: - response = sock.recv(1024) - base_app.logger.debug("Rsp({})".format(response)) - result = obj.parse_message(response) - if not result: - base_app.logger.debug("parse result({})".format(result)) - except socket.timeout: - base_app.logger.error("No response - jump from loop!") - break - - # expect Req = b"$FRWDT,NMEA*78" - request = obj.get_next_client_2_server() - base_app.logger.debug("Req({})".format(request)) - sock.send(request) - # this message has NO response! - - # our fake data, time-fixed to me NOW - request = fix_time_sentence("$GPGGA,094013.0,4334.784909,N,11612.7664" + - "48,W,1,09,0.9,830.6,M,-11.0,M,,*60") - request = request.encode() - base_app.logger.debug("Req({})".format(request)) - sock.send(request) - - # this message has NO response! - time.sleep(2.0) - - sock.close() - - return 0 - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - import tools.make_load_settings - - app_name = 'demo.gps_gate' - - # we share the settings.ini in demo/gps_gate/settings.ini - base_app = CradlepointAppBase(full_name=app_name, call_router=False) - - # force a full make/read of {app_path}/settings.ini - base_app.settings = tools.make_load_settings.load_settings( - base_app.app_path) - - run_client() diff --git a/demo/gps_gate/test/rcv_localhost.py b/demo/gps_gate/test/rcv_localhost.py deleted file mode 100644 index 03ef38bb..00000000 --- a/demo/gps_gate/test/rcv_localhost.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Received GPS, assuming the router's GPS function sends new data (sentences) -to a localhost port -""" - -import socket -import time -import gc - -from cp_lib.app_base import CradlepointAppBase -from demo.gps_gate.gps_gate_nmea import NmeaCollection - - -DEF_HOST_IP = '192.168.1.6' -DEF_HOST_PORT = 9999 -DEF_BUFFER_SIZE = 1024 - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - host_ip = DEF_HOST_IP - host_port = DEF_HOST_PORT - buffer_size = DEF_BUFFER_SIZE - - gps = NmeaCollection(app_base.logger) - gps.set_time_filter(30.0) - - while True: - # define the socket resource, including the type (stream == "TCP") - address = (host_ip, host_port) - app_base.logger.info("Preparing GPS Listening on {}".format(address)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # attempt to actually lock resource, which may fail if unavailable - # (see BIND ERROR note) - try: - sock.bind(address) - except OSError as msg: - app_base.logger.error("socket.bind() failed - {}".format(msg)) - - # technically, Python will close when 'sock' goes out of scope, - # but be disciplined and close it yourself. Python may warning - # you of unclosed resource, during runtime. - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors - # are likely permanent. - return -1 - - # only allow 1 client at a time - sock.listen(3) - - while True: - # loop forever - app_base.logger.info("Waiting on TCP socket %d" % host_port) - client, address = sock.accept() - app_base.logger.info("Accepted connection from {}".format(address)) - - # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # set non-blocking so we can do a manual timeout (use of select() - # is better ... but that's another sample) - # client.setblocking(0) - - while True: - app_base.logger.debug("Waiting to receive data") - data = client.recv(buffer_size) - # data is type() bytes, to echo we don't need to convert - # to str to format or return. - if data: - data = data.decode().split() - - gps.start() - for line in data: - result = gps.parse_sentence(line) - if not result: - break - need_publish, reason = gps.end() - if need_publish: - app_base.logger.debug( - "Publish reason:{}".format(reason)) - report = gps.report_list() - for line in report: - app_base.logger.debug("{}".format(line)) - gps.publish() - - # else: - # app_base.logger.debug("No Publish") - - # # client.send(data) - else: - break - - time.sleep(1.0) - - app_base.logger.info("Client disconnected") - client.close() - - # since this server is expected to run on a small embedded system, - # free up memory ASAP (see MEMORY note) - del client - gc.collect() - - return 0 - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("gps/gps_gate") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/demo/gps_gate/test/test_gps_gate_nmea.py b/demo/gps_gate/test/test_gps_gate_nmea.py deleted file mode 100644 index 3b8bda9d..00000000 --- a/demo/gps_gate/test/test_gps_gate_nmea.py +++ /dev/null @@ -1,416 +0,0 @@ -# Test the gps.gps_gate.gps_gate_nmea module -import logging -import unittest - -import demo.gps_gate.gps_gate_nmea as nmea - - -class TestGpsGateNmea(unittest.TestCase): - - def test_distance_meters(self): - global base_app - - # note: had to manually confirm 'import math' works on router - - base_app.logger.info("TEST Haversine formula for distance") - base_app.logger.setLevel(logging.INFO) - - lon1 = -93.334702 - lat1 = 45.013432 - base_app.logger.info("MSP long:{} lat:{}".format(lon1, lat1)) - - lon2 = -116.20599500000003 - lat2 = 43.6194842 - base_app.logger.info("BOI long:{} lat:{}".format(lon2, lat2)) - - obj = nmea.NmeaCollection(base_app.logger) - delta = obj._distance_meters(lon1, lat1, lon2, lat2) - base_app.logger.info( - "Distance MSP to BOI = {} km".format(delta * 1000.0)) - - # delta is in meters, not KM - self.assertEqual(int(delta), 1820118) - - # this returns 1820 KM, by highway is 2347.7 KM - # by cgs network .com = 1845 KM - - return - - def test_set_distance_filter(self): - global base_app - - base_app.logger.info("TEST set_distance_filter()") - base_app.logger.setLevel(logging.INFO) - - obj = nmea.NmeaCollection(base_app.logger) - - min_limit = obj.DISTANCE_FILTER_LIMITS[0] - max_limit = obj.DISTANCE_FILTER_LIMITS[1] - base_app.logger.debug("Min:{} Max:{}".format(min_limit, max_limit)) - # distance tests, in meters - alternate - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': min_limit}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': max_limit}, - - {'inp': 'why?', 'out': ValueError}, - {'inp': b'why?', 'out': ValueError}, - {'inp': (1, 2), 'out': ValueError}, - ] - - self.assertTrue(obj.CLAMP_SETTINGS) - - if obj.DISTANCE_FILTER_DEF is None: - self.assertIsNone(obj.distance_filter) - else: - self.assertIsNotNone(obj.distance_filter) - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_distance_filter(test['inp']) - - else: - obj.set_distance_filter(test['inp']) - self.assertEqual(obj.distance_filter, test['out']) - - # repeat tests with - obj.CLAMP_SETTINGS = False - self.assertFalse(obj.CLAMP_SETTINGS) - - # distance tests, in meters - alternate - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': ValueError}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': ValueError}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': ValueError}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': ValueError}, - ] - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_distance_filter(test['inp']) - - else: - obj.set_distance_filter(test['inp']) - self.assertEqual(obj.distance_filter, test['out']) - - return - - def test_set_speed_filter(self): - global base_app - - base_app.logger.info("TEST set_speed_filter()") - base_app.logger.setLevel(logging.INFO) - - obj = nmea.NmeaCollection(base_app.logger) - - min_limit = obj.SPEED_FILTER_LIMITS[0] - max_limit = obj.SPEED_FILTER_LIMITS[1] - base_app.logger.debug("Min:{} Max:{}".format(min_limit, max_limit)) - # distance tests, in meters - alternate - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': min_limit}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': max_limit}, - - {'inp': 'why?', 'out': ValueError}, - {'inp': b'why?', 'out': ValueError}, - {'inp': (1, 2), 'out': ValueError}, - ] - - self.assertTrue(obj.CLAMP_SETTINGS) - - if obj.SPEED_FILTER_DEF is None: - self.assertIsNone(obj.speed_filter) - else: - self.assertIsNotNone(obj.speed_filter) - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_speed_filter(test['inp']) - - else: - obj.set_speed_filter(test['inp']) - self.assertEqual(obj.speed_filter, test['out']) - - return - - def test_set_direction_filter(self): - global base_app - - base_app.logger.info("TEST set_direction_filter()") - base_app.logger.setLevel(logging.INFO) - - obj = nmea.NmeaCollection(base_app.logger) - - min_limit = obj.DIRECTION_FILTER_LIMITS[0] - max_limit = obj.DIRECTION_FILTER_LIMITS[1] - base_app.logger.debug("Min:{} Max:{}".format(min_limit, max_limit)) - # distance tests, in meters - alternate - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': min_limit}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': max_limit}, - - {'inp': 'why?', 'out': ValueError}, - {'inp': b'why?', 'out': ValueError}, - {'inp': (1, 2), 'out': ValueError}, - ] - - self.assertTrue(obj.CLAMP_SETTINGS) - - if obj.DIRECTION_FILTER_DEF is None: - self.assertIsNone(obj.direction_filter) - else: - self.assertIsNotNone(obj.direction_filter) - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_direction_filter(test['inp']) - - else: - obj.set_direction_filter(test['inp']) - self.assertEqual(obj.direction_filter, test['out']) - - return - - def test_set_direction_threshold(self): - global base_app - - base_app.logger.info("TEST set_direction_threshold()") - base_app.logger.setLevel(logging.INFO) - - obj = nmea.NmeaCollection(base_app.logger) - - min_limit = obj.DIRECTION_THRESHOLD_LIMITS[0] - max_limit = obj.DIRECTION_THRESHOLD_LIMITS[1] - base_app.logger.debug("Min:{} Max:{}".format(min_limit, max_limit)) - # distance tests, in meters - alternate - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': min_limit}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': max_limit}, - - {'inp': 'why?', 'out': ValueError}, - {'inp': b'why?', 'out': ValueError}, - {'inp': (1, 2), 'out': ValueError}, - ] - - self.assertTrue(obj.CLAMP_SETTINGS) - - if obj.DIRECTION_FILTER_DEF is None: - self.assertIsNone(obj.direction_threshold) - else: - self.assertIsNotNone(obj.direction_threshold) - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_direction_threshold(test['inp']) - - else: - obj.set_direction_threshold(test['inp']) - self.assertEqual(obj.direction_threshold, test['out']) - - return - - def test_set_time_filter(self): - global base_app - - base_app.logger.info("TEST set_time_filter()") - base_app.logger.setLevel(logging.DEBUG) - - obj = nmea.NmeaCollection(base_app.logger) - - min_limit = obj.TIME_FILTER_LIMITS[0] - max_limit = obj.TIME_FILTER_LIMITS[1] - base_app.logger.debug("Min:{} Max:{}".format(min_limit, max_limit)) - tests = [ - {'inp': '', 'out': None}, - {'inp': '100', 'out': 100.0}, - {'inp': None, 'out': None}, - {'inp': '100.0', 'out': 100.0}, - {'inp': 0, 'out': None}, - {'inp': 100, 'out': 100.0}, - {'inp': '0', 'out': None}, - {'inp': 100.0, 'out': 100.0}, - {'inp': '0', 'out': None}, - - # test clamps - {'inp': min_limit, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': min_limit - 0.9, 'out': min_limit}, - {'inp': '0', 'out': None}, - {'inp': -1, 'out': min_limit}, - - {'inp': '0', 'out': None}, - {'inp': max_limit, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': max_limit + 0.9, 'out': max_limit}, - {'inp': '0', 'out': None}, - {'inp': 99999999, 'out': max_limit}, - - {'inp': 'why?', 'out': ValueError}, - {'inp': b'why?', 'out': ValueError}, - {'inp': (1, 2), 'out': ValueError}, - - # test the SPECIAL values with time-tags - {'inp': '1 sec', 'out': min_limit}, - {'inp': '5 sec', 'out': min_limit}, - {'inp': '10 sec', 'out': min_limit}, - - {'inp': '15 sec', 'out': 15.0}, - {'inp': '5 min', 'out': 300.0}, - {'inp': '5 hr', 'out': 18000.0}, - {'inp': '20 hr', 'out': 72000.0}, - {'inp': '24 hr', 'out': 86400.0}, - {'inp': '1 day', 'out': 86400.0}, - - {'inp': '25 day', 'out': max_limit}, - {'inp': '2 day', 'out': max_limit}, - ] - - self.assertTrue(obj.CLAMP_SETTINGS) - - if obj.TIME_FILTER_DEF is None: - self.assertIsNone(obj.time_filter) - else: - self.assertIsNotNone(obj.time_filter) - - for test in tests: - - base_app.logger.debug("TEST {}".format(test)) - - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_time_filter(test['inp']) - - else: - obj.set_time_filter(test['inp']) - self.assertEqual(obj.time_filter, test['out']) - - return - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - - base_app = CradlepointAppBase(call_router=False) - unittest.main() diff --git a/demo/gps_gate/test/test_gps_gate_protocol.py b/demo/gps_gate/test/test_gps_gate_protocol.py deleted file mode 100644 index a6e36ad7..00000000 --- a/demo/gps_gate/test/test_gps_gate_protocol.py +++ /dev/null @@ -1,352 +0,0 @@ -# Test the gps.gps_gate.gps_gate_protocol module - -import unittest - -import demo.gps_gate.gps_gate_protocol as protocol - - -class TestGpsGateProtocol(unittest.TestCase): - - def test_sets(self): - global base_app - - print("") # skip paste '.' on line - - base_app.logger.info("TEST set of client details") - - tests = [ - {'inp': '', 'out': ''}, - {'inp': 'User1', 'out': 'User1'}, - {'inp': ' User2 ', 'out': 'User2'}, - {'inp': '\"User3\"', 'out': 'User3'}, - {'inp': '\'User4\'', 'out': 'User4'}, - {'inp': b'', 'out': ''}, - {'inp': b'User1', 'out': 'User1'}, - {'inp': b' User2 ', 'out': 'User2'}, - {'inp': b'\"User3\"', 'out': 'User3'}, - {'inp': b'\'User4\'', 'out': 'User4'}, - {'inp': None, 'out': TypeError}, - {'inp': 10, 'out': TypeError}, - {'inp': 100.998, 'out': TypeError}, - ] - - obj = protocol.GpsGate(base_app.logger) - self.assertIsNone(obj.client_username) - self.assertIsNone(obj.client_password) - self.assertIsNone(obj.client_imei) - - for test in tests: - - if test['out'] == TypeError: - with self.assertRaises(TypeError): - obj.set_username(test['inp']) - with self.assertRaises(TypeError): - obj.set_password(test['inp']) - with self.assertRaises(TypeError): - obj.set_imei(test['inp']) - - else: - obj.set_username(test['inp']) - self.assertEqual(obj.client_username, test['out']) - obj.set_password(test['inp']) - self.assertEqual(obj.client_password, test['out']) - obj.set_imei(test['inp']) - self.assertEqual(obj.client_imei, test['out']) - - base_app.logger.info("TEST set of client name") - tests = [ - {'name_in': 'My Tools', 'ver_in': '9.3', - 'name_out': 'My Tools', 'ver_out': '9.3'}, - {'name_in': 'My Toes', 'ver_in': 7.8, - 'name_out': 'My Toes', 'ver_out': '7.8'}, - {'name_in': 'My Nose', 'ver_in': 14, - 'name_out': 'My Nose', 'ver_out': '14.0'}, - {'name_in': 'Boss', 'ver_in': 13.25, - 'name_out': 'Boss', 'ver_out': '13.25'}, - {'name_in': 'Tammy Gears 7.2', 'ver_in': None, - 'name_out': 'Tammy Gears', 'ver_out': '7.2'}, - {'name_in': 'Ziggy', 'ver_in': None, - 'name_out': 'Ziggy', 'ver_out': '1.0'}, - {'name_in': 'Tammy Gears 9 8.1', 'ver_in': None, - 'name_out': 'Tammy Gears 9', 'ver_out': '8.1'}, - {'name_in': 'Tammy Gears two 4.3', 'ver_in': None, - 'name_out': 'Tammy Gears two', 'ver_out': '4.3'}, - {'name_in': 'Tammy Gears two', 'ver_in': None, - 'name_out': 'Tammy Gears two', 'ver_out': '1.0'}, - ] - - # start with defaults - self.assertEqual(obj.client_name, obj.DEF_CLIENT_NAME) - self.assertEqual(obj.client_version, obj.DEF_CLIENT_VERSION) - - for test in tests: - obj.set_client_name(test['name_in'], test['ver_in']) - self.assertEqual(obj.client_name, test['name_out']) - self.assertEqual(obj.client_version, test['ver_out']) - - base_app.logger.info("TEST set of server URL, Port, and Transport") - tests = [ - {'in': 'gps.linse.org', 'out': 'gps.linse.org'}, - {'in': '\"web.linse.org\"', 'out': 'web.linse.org'}, - ] - - # start with defaults - self.assertEqual(obj.gps_gate_url, obj.DEF_GPS_GATE_URL) - - for test in tests: - obj.set_server_url(test['in']) - self.assertEqual(obj.gps_gate_url, test['out']) - - tests = [ - {'in': '9999', 'out': 9999}, - {'in': 8824, 'out': 8824}, - ] - - # start with defaults - self.assertEqual(obj.gps_gate_port, obj.DEF_GPS_GATE_PORT) - - for test in tests: - obj.set_server_port(test['in']) - self.assertEqual(obj.gps_gate_port, test['out']) - - tests = [ - {'in': 'tcp', 'out': 'tcp'}, - {'in': 'TCP', 'out': 'tcp'}, - {'in': 'Tcp', 'out': 'tcp'}, - {'in': 'silly', 'out': ValueError}, - {'in': 'xml', 'out': NotImplementedError}, - ] - - # start with defaults - self.assertEqual(obj.gps_gate_transport, obj.DEF_GPS_GATE_TRANSPORT) - - for test in tests: - if test['out'] == ValueError: - with self.assertRaises(ValueError): - obj.set_server_transport(test['in']) - elif test['out'] == NotImplementedError: - with self.assertRaises(NotImplementedError): - obj.set_server_transport(test['in']) - else: - obj.set_server_transport(test['in']) - self.assertEqual(obj.gps_gate_transport, test['out']) - - return - - def test_frlin(self): - global base_app - - print("") # skip paste '.' on line - base_app.logger.info("TEST FRLIN") - - obj = protocol.GpsGate(base_app.logger) - self.assertIsNone(obj.client_username) - self.assertIsNone(obj.client_password) - self.assertIsNone(obj.client_imei) - - # a simple encryption test using sample in the spec - expect = "HHVMOLLX" - result = obj._encrypt_password("coolness") - self.assertEqual(result, expect) - - with self.assertRaises(ValueError): - # since user name is None - obj.form_frlin() - - obj.set_username("cradlepoint") - with self.assertRaises(ValueError): - # since password is None - obj.form_frlin() - - obj.set_password("Billy6Boy") - result = obj.form_frlin() - expect = '$FRLIN,,cradlepoint,BLy3BOORy*2F\r\n' - self.assertEqual(result, expect) - - with self.assertRaises(ValueError): - # since imei is None - obj.form_frlin(force_imei=True) - - obj.set_imei("353547060660845") - result = obj.form_frlin() - expect = '$FRLIN,,cradlepoint,BLy3BOORy*2F\r\n' - self.assertEqual(result, expect) - - result = obj.form_frlin(force_imei=True) - expect = '$FRLIN,IMEI,353547060660845,*47\r\n' - self.assertEqual(result, expect) - - return - - def test_frret_imei(self): - global base_app - - print("") # skip paste '.' on line - base_app.logger.info("TEST FRRET IMEI") - - obj = protocol.GpsGate(base_app.logger) - self.assertEqual(obj.tracker_name, obj.DEF_TRACKER_NAME) - self.assertEqual(obj.client_name, obj.DEF_CLIENT_NAME) - self.assertEqual(obj.client_version, obj.DEF_CLIENT_VERSION) - - # a simple encryption test using sample in the spec - expect = '$FRRET,Cradlepoint,_GprsSettings,,GpsGate TrackerOne,' +\ - 'Cradlepoint,1.0*7B\r\n' - result = obj.form_frret_imei() - self.assertEqual(result, expect) - - return - - def test_frver(self): - global base_app - - print("") # skip paste '.' on line - base_app.logger.info("TEST FRVER") - - obj = protocol.GpsGate(base_app.logger) - - # a simple encryption test using sample in the spec - expect = '$FRVER,1,1,Cradlepoint 1.0*27\r\n' - result = obj.form_frver() - self.assertEqual(result, expect) - - return - - def test_parse_frval(self): - global base_app - - print("") # skip paste '.' on line - base_app.logger.info("TEST FRVAL") - - obj = protocol.GpsGate(base_app.logger) - - self.assertIsNone(obj.nmea.distance_filter) - source = "$FRVAL,DistanceFilter,500.0*67" - obj.parse_message(source) - self.assertEqual(obj.nmea.distance_filter, 500.0) - - source = "$FRVAL,DistanceFilter,200.0" - obj.parse_message(source) - self.assertEqual(obj.nmea.distance_filter, 200.0) - - source = "FRVAL,DistanceFilter,100.0" - obj.parse_message(source) - self.assertEqual(obj.nmea.distance_filter, 100.0) - - self.assertIsNone(obj.nmea.time_filter) - source = "$FRVAL,TimeFilter,60.0*42" - obj.parse_message(source) - self.assertEqual(obj.nmea.time_filter, 60.0) - - self.assertIsNone(obj.nmea.direction_filter) - source = "$FRVAL,DirectionFilter,40.0*30" - obj.parse_message(source) - self.assertEqual(obj.nmea.direction_filter, 40.0) - - self.assertIsNone(obj.nmea.direction_threshold) - source = "$FRVAL,DirectionThreshold,10.0*42" - obj.parse_message(source) - self.assertEqual(obj.nmea.direction_threshold, 10.0) - - self.assertIsNone(obj.nmea.speed_filter) - source = "$FRVAL,SpeedFilter,25.0*31" - obj.parse_message(source) - self.assertEqual(obj.nmea.speed_filter, 25.0) - - return - - def test_state_machine(self): - global base_app - - print("") # skip paste '.' on line - base_app.logger.info("TEST server state machine") - - obj = protocol.GpsGate(base_app.logger) - obj.set_imei("353547060660845") - - self.assertEqual(obj.state, obj.STATE_OFFLINE) - - expect = "$FRLIN,IMEI,353547060660845,*47\r\n" - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_TRY_LOGIN) - self.assertEqual(message, expect) - - # repeat, is okay but get warning - expect = "$FRLIN,IMEI,353547060660845,*47\r\n" - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_TRY_LOGIN) - self.assertEqual(message, expect) - - server_response = "$FRSES,1221640*4F" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_HAVE_SESSION) - self.assertTrue(result) - - # repeat, is okay but get warning - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_HAVE_SESSION) - self.assertTrue(result) - - expect = "$FRVER,1,1,Cradlepoint 1.0*27\r\n" - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_WAIT_SERVER_VERSION) - self.assertEqual(message, expect) - - # repeat, is okay but get warning - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_WAIT_SERVER_VERSION) - self.assertEqual(message, expect) - - server_response = "$FRVER,1,1,GpsGate Server 1.1.0.360*04" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_HAVE_SERVER_VERSION) - self.assertTrue(result) - - # repeat, is okay but get warning - server_response = "$FRVER,1,1,GpsGate Server 1.1.0.360*04" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_HAVE_SERVER_VERSION) - self.assertTrue(result) - - obj.set_server_version("0.3") - self.assertEqual(obj.server_major_version, 0) - self.assertEqual(obj.server_minor_version, 3) - - server_response = "$FRVER,1,1,GpsGate Server 1.1.0.360*04" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_HAVE_SERVER_VERSION) - self.assertTrue(result) - - expect = "$FRCMD,,_getupdaterules,Inline*1E" - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_ASK_UPDATE) - self.assertEqual(message, expect) - - server_response = "$FRRET,User1,_getupdaterules,Nmea,2*07" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_FORWARD_READY) - self.assertTrue(result) - - server_response = "$FRVAL,DistanceFilter,500.0*67" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_FORWARD_READY) - self.assertTrue(result) - - server_response = "$FRVAL,TimeFilter,60.0*42" - result = obj.parse_message(server_response) - self.assertEqual(obj.state, obj.STATE_FORWARD_READY) - self.assertTrue(result) - - expect = "$FRWDT,NMEA*78" - message = obj.get_next_client_2_server() - self.assertEqual(obj.state, obj.STATE_FORWARDING) - self.assertEqual(message, expect) - - return - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - - base_app = CradlepointAppBase(call_router=False) - unittest.main() diff --git a/demo/gps_replay/README.md b/demo/gps_replay/README.md deleted file mode 100644 index 75a3b9e1..00000000 --- a/demo/gps_replay/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# directory: ./demo/gps_replay -## Router App/SDK sample applications - -These samples are best run on a PC/notebook, for while one CAN save a file -in the router flash, there is NO way to access that file. - -## File: __init__.py - -Is empty! - -## File: gps_save_replay.py - -Run this directly! It probes your router, obtains the configured GPS-to-IP -server settings, and waits on the configured IP/port. It saves the data like -this: - -[ - {"offset":1, "data":$GPGGA,094013.0,4334.784909,N,11612.766448,W,1 (...) *60}, - {"offset":3, "data":$GPGGA,094023.0,4334.784913,N,11612.766463,W,1 (...) *61}, - {"offset":13, "data":$GPGGA,094034.0,4334.784922,N,11612.766471,W, (...) *67}, - - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [gps]: - -* host_port=9999, define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. diff --git a/demo/gps_replay/__init__.py b/demo/gps_replay/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/demo/gps_replay/gps_log.json b/demo/gps_replay/gps_log.json deleted file mode 100644 index ce364b35..00000000 --- a/demo/gps_replay/gps_log.json +++ /dev/null @@ -1,13 +0,0 @@ -[ -{"offset":1, "data":"$GPGGA,094013.0,4334.784909,N,11612.766448,W,1,09,0.9,830.6,M,-11.0,M,,*60"}, -{"offset":3, "data":"$GPGGA,094023.0,4334.784913,N,11612.766463,W,1,09,0.9,830.6,M,-11.0,M,,*61"}, -{"offset":13, "data":"$GPGGA,094034.0,4334.784922,N,11612.766471,W,1,09,0.9,830.7,M,-11.0,M,,*67"}, -{"offset":24, "data":"$GPGGA,094044.0,4334.784917,N,11612.766476,W,1,08,1.0,830.7,M,-11.0,M,,*68"}, -{"offset":34, "data":"$GPGGA,094054.0,4334.784908,N,11612.766493,W,1,07,1.0,830.7,M,-11.0,M,,*63"}, -{"offset":44, "data":"$GPGGA,094104.0,4334.784894,N,11612.766503,W,1,07,1.0,830.8,M,-11.0,M,,*64"}, -{"offset":54, "data":"$GPGGA,094114.0,4334.784886,N,11612.766524,W,1,08,1.0,830.9,M,-11.0,M,,*6D"}, -{"offset":64, "data":"$GPGGA,094124.0,4334.784888,N,11612.766525,W,1,08,0.9,830.9,M,-11.0,M,,*69"}, -{"offset":74, "data":"$GPGGA,094134.0,4334.784897,N,11612.766529,W,1,08,0.8,831.0,M,-11.0,M,,*63"}, -{"offset":84, "data":"$GPGGA,094145.0,4334.784929,N,11612.766557,W,1,09,0.7,831.1,M,-11.0,M,,*67"}, -{"offset":95, "data":"$GPGGA,094155.0,4334.784939,N,11612.766583,W,1,08,1.0,831.2,M,-11.0,M,,*6A"}, -] \ No newline at end of file diff --git a/demo/gps_replay/run_replay.py b/demo/gps_replay/run_replay.py deleted file mode 100644 index 0cd08de5..00000000 --- a/demo/gps_replay/run_replay.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -This is a standalone script, to be run on a PC. It reads a previously -created file (likely created by save_replay.py), then acts like a cradlepoint -router sending the data in a REPLAY mode. - -The replay file format is pseudo-JSON, as it will be like: -[ -{"offset":1, "data":$GPGGA,094013.0,4334.784909,N,11612.766448,W,1 (...) *60}, -{"offset":3, "data":$GPGGA,094023.0,4334.784913,N,11612.766463,W,1 (...) *61}, -{"offset":13, "data":$GPGGA,094034.0,4334.784922,N,11612.766471,W, (...) *67}, - -"offset" is the number of seconds since the start of the replay save, which -are used during replay to delay and meter out the sentences in a realistic -manner. We'll also need to 'edit' the known sentences to add new TIME and -DATE values. -""" -import os -import socket -import time - -from cp_lib.app_base import CradlepointAppBase, CradlepointRouterOffline -from cp_lib.parse_data import clean_string, parse_integer -from cp_lib.load_gps_config import GpsConfig -import cp_lib.gps_nmea as gps_nmea - -DEF_REPLAY_FILE = 'gps_log.json' - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - replay_file_name = DEF_REPLAY_FILE - - try: - # use Router API to fetch any configured data - config = GpsConfig(app_base) - host_ip, host_port = config.get_client_info() - del config - - except CradlepointRouterOffline: - host_ip = None - host_port = 0 - - section = "gps" - if section in app_base.settings: - # then load dynamic values - temp = app_base.settings[section] - - # check on our localhost port (not used, but to test) - if "host_ip" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_ip") - value = clean_string(temp["host_ip"]) - app_base.logger.warning("was:{} now:{}".format(host_ip, value)) - host_ip = value - - if "host_port" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_port") - value = parse_integer(temp["host_port"]) - app_base.logger.warning("was:{} now:{}".format(host_port, value)) - host_port = value - - if "replay_file" in temp: - replay_file_name = clean_string(temp["replay_file"]) - - app_base.logger.debug("GPS destination:({}:{})".format(host_ip, host_port)) - - if not os.path.isfile(replay_file_name): - app_base.logger.error( - "Replay file({}) not found".format(replay_file_name)) - raise FileNotFoundError - - app_base.logger.info( - "Replay file is named:{}".format(replay_file_name)) - - # define the socket resource, including the type (stream == "TCP") - address = (host_ip, host_port) - app_base.logger.info("Will connect on {}".format(address)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - sock.connect(address) - - file_han = None - start_time = time.time() - - try: - while True: - # loop forever - if file_han is None: - # reopen the file - file_han = open(replay_file_name, "r") - start_time = time.time() - - # this - line_in = read_in_line(file_han) - if line_in is None: - app_base.logger.warning("Close Replay file") - file_han.close() - break - - else: - wait_time = start_time + line_in['offset'] - now = time.time() - if wait_time > now: - delay = wait_time - now - app_base.logger.debug("Delay %0.1f sec" % delay) - time.sleep(delay) - - nmea_out = gps_nmea.fix_time_sentence(line_in['data']).strip() - nmea_out += '\r\n' - app_base.logger.debug("out:{}".format(nmea_out)) - - sock.send(nmea_out.encode()) - - time.sleep(0.1) - - finally: - app_base.logger.info("Closing client socket") - sock.close() - - return 0 - - -def read_in_line(_file_han): - - while True: - line_in = _file_han.readline().strip() - if line_in is None or len(line_in) == 0: - break - - if line_in[0] == '{': - # then assume like {"offset":1, "data":$GPGGA,094013.0 ... }, - if line_in[-1] == ',': - line_in = line_in[:-1] - - return eval(line_in) - - # else, get next line - - return None - -if __name__ == "__main__": - import sys - from cp_lib.load_settings_ini import copy_config_ini_to_json, \ - load_sdk_ini_as_dict - - copy_config_ini_to_json() - - app_path = "demo/gps_replay" - my_app = CradlepointAppBase(app_path, call_router=False) - # force a heavy reload of INI (app base normally only finds JSON) - my_app.settings = load_sdk_ini_as_dict(app_path) - - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/demo/gps_replay/save_replay.py b/demo/gps_replay/save_replay.py deleted file mode 100644 index 1e055bff..00000000 --- a/demo/gps_replay/save_replay.py +++ /dev/null @@ -1,173 +0,0 @@ -""" -This is a standalone script, to be run on a PC. It receives GPS data -on a localhost port & writes to a replay file. - -The format is pseudo-JSON, as it will be like: -[ -{"offset":1, "data":$GPGGA,094013.0,4334.784909,N,11612.766448,W,1 (...) *60}, -{"offset":3, "data":$GPGGA,094023.0,4334.784913,N,11612.766463,W,1 (...) *61}, -{"offset":13, "data":$GPGGA,094034.0,4334.784922,N,11612.766471,W, (...) *67}, - -Notice it starts with a '[', but like won't end with ']' - assuming you -abort the creation uncleanly. You can manually add one if you wish. - -"offset" is the number of seconds since the start of the replay save, which -are used during replay to delay and meter out the sentences in a realistic -manner. We'll also need to 'edit' the known sentences to add new TIME and -DATE values. -""" -import socket -import time -import gc - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.load_gps_config import GpsConfig -from cp_lib.parse_data import clean_string, parse_integer - -DEF_BUFFER_SIZE = 1024 -DEF_REPLAY_FILE = 'gps_log.json' - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - buffer_size = DEF_BUFFER_SIZE - replay_file_name = DEF_REPLAY_FILE - - # use Router API to fetch any configured data - config = GpsConfig(app_base) - host_ip, host_port = config.get_client_info() - del config - - section = "gps" - if section in app_base.settings: - # then load dynamic values - temp = app_base.settings[section] - - # check on our localhost port (not used, but to test) - if "host_ip" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_ip") - value = clean_string(temp["host_ip"]) - app_base.logger.warning("was:{} now:{}".format(host_ip, value)) - host_ip = value - - if "host_port" in temp: - # then OVER-RIDE what the router told us - app_base.logger.warning("Settings OVER-RIDE router host_port") - value = parse_integer(temp["host_port"]) - app_base.logger.warning("was:{} now:{}".format(host_port, value)) - host_port = value - - if "buffer_size" in temp: - buffer_size = parse_integer(temp["buffer_size"]) - - if "replay_file" in temp: - replay_file_name = clean_string(temp["replay_file"]) - - app_base.logger.debug("GPS source:({}:{})".format(host_ip, host_port)) - - # make sure our log file exists & is empty - file_han = open(replay_file_name, "w") - file_han.write("[\n") - file_han.close() - - address = (host_ip, host_port) - - while True: - # define the socket resource, including the type (stream == "TCP") - app_base.logger.info("Preparing GPS Listening on {}".format(address)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # attempt to actually lock resource, which may fail if unavailable - # (see BIND ERROR note) - try: - sock.bind(address) - except OSError as msg: - app_base.logger.error("socket.bind() failed - {}".format(msg)) - - # technically, Python will close when 'sock' goes out of scope, - # but be disciplined and close it yourself. Python may warning - # you of unclosed resource, during runtime. - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors - # are likely permanent. - return -1 - - # only allow 1 client at a time - sock.listen(3) - - while True: - # loop forever - start_time = time.time() - - app_base.logger.info("Waiting on TCP socket %d" % host_port) - client, address = sock.accept() - app_base.logger.info("Accepted connection from {}".format(address)) - - # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # set non-blocking so we can do a manual timeout (use of select() - # is better ... but that's another sample) - # client.setblocking(0) - - while True: - app_base.logger.debug("Waiting to receive data") - data = client.recv(buffer_size) - # data is type() bytes, to echo we don't need to convert - # to str to format or return. - if data: - # assume we have multiple sentences per segment recv'd - data = data.decode().split() - - print("data:{}".format(data)) - - file_han = open(replay_file_name, "a") - offset = int(time.time() - start_time) - - for line in data: - result = '{"offset":%d, "data":"%s"},\n' % (offset, - line) - file_han.write(result) - - app_base.logger.debug("Wrote at offset:{}".format(offset)) - - else: - break - - time.sleep(1.0) - - app_base.logger.info("Client disconnected") - client.close() - - # since this server is expected to run on a small embedded system, - # free up memory ASAP (see MEMORY note) - del client - gc.collect() - - return 0 - - -if __name__ == "__main__": - import sys - from cp_lib.load_settings_ini import copy_config_ini_to_json, \ - load_sdk_ini_as_dict - - copy_config_ini_to_json() - - app_path = "demo/gps_replay" - my_app = CradlepointAppBase(app_path) - # force a heavy reload of INI (app base normally only finds JSON) - my_app.settings = load_sdk_ini_as_dict(app_path) - - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/demo/gps_replay/settings.ini b/demo/gps_replay/settings.ini deleted file mode 100644 index b863cbb1..00000000 --- a/demo/gps_replay/settings.ini +++ /dev/null @@ -1,16 +0,0 @@ -; one app settings - -[application] -name=gps_replay -description=Receive NMEA sentences on localhost port, save to file for replay -path = demo/gps_replay -version=1.1 - -[gps] -; do NOT set these if you desire Router API in config/system/gps to be used! -; these OVER-RIDE the router api values -; host_ip=192.168.1.6 -; host_port=9992 - -; the replay file name - is psuedo JSON, but coud also be called text (.txt) -replay_file=gps_replay.json diff --git a/demo/gps_replay/shift_offset.py b/demo/gps_replay/shift_offset.py deleted file mode 100644 index f49033be..00000000 --- a/demo/gps_replay/shift_offset.py +++ /dev/null @@ -1,121 +0,0 @@ -""" -This is a standalone script, to be run on a PC. It reads a previously -created file (likely created by save_replay.py), renames and creates a -new file with the "offset" field shifted by some time. - -The goal is to prepare a file to be appended to another. -""" -import os - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.parse_data import clean_string, parse_float - -DEF_REPLAY_FILE = 'gps_log.json' - - -def run_router_app(app_base, adjust_seconds): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param float adjust_seconds: - :return: - """ - - replay_file_name = DEF_REPLAY_FILE - - section = "gps" - if section in app_base.settings: - # then load dynamic values - temp = app_base.settings[section] - - if "replay_file" in temp: - replay_file_name = clean_string(temp["replay_file"]) - - if not os.path.isfile(replay_file_name): - app_base.logger.error( - "Replay file({}) not found".format(replay_file_name)) - raise FileNotFoundError - - app_base.logger.info( - "Replay file is named:{}".format(replay_file_name)) - - backup_file_name = replay_file_name + '.bak' - app_base.logger.info( - "Backup file is named:{}".format(backup_file_name)) - - os.rename(src=replay_file_name, dst=backup_file_name) - - file_in = open(backup_file_name, "r") - file_out = open(replay_file_name, "w") - - while True: - # loop forever - - # this is a dictionary - dict_in = read_in_line(file_in) - if dict_in is None: - app_base.logger.warning("Close Replay file") - break - - else: - dict_in["offset"] += adjust_seconds - result = '{"offset":%d, "data":"%s"},\n' % (dict_in["offset"], - dict_in["data"]) - file_out.write(result) - - file_in.close() - file_out.close() - - return 0 - - -def read_in_line(_file_han): - - while True: - line_in = _file_han.readline().strip() - if line_in is None or len(line_in) == 0: - break - - if line_in[0] == '{': - # then assume like {"offset":1, "data":$GPGGA,094013.0 ... }, - if line_in[-1] == ',': - line_in = line_in[:-1] - - return eval(line_in) - - # else, get next line - - return None - -if __name__ == "__main__": - import sys - from cp_lib.load_settings_ini import copy_config_ini_to_json, \ - load_sdk_ini_as_dict - from cp_lib.parse_duration import TimeDuration - - copy_config_ini_to_json() - - app_path = "demo/gps_replay" - my_app = CradlepointAppBase(app_path, call_router=False) - # force a heavy reload of INI (app base normally only finds JSON) - my_app.settings = load_sdk_ini_as_dict(app_path) - - if len(sys.argv) == 2: - # assume is numeric seconds - shifter = parse_float(sys.argv[1]) - - elif len(sys.argv) >= 3: - # assume is tagged time, like "15 min" - period = TimeDuration(sys.argv[1] + ' ' + sys.argv[2]) - shifter = parse_float(period.get_seconds()) - - else: - my_app.logger.warning("You need to append the time in seconds") - sys.exit(-1) - - my_app.logger.info("Time shifter = {} seconds".format(shifter)) - - _result = run_router_app(my_app, shifter) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/demo/hoops_counter/README.md b/demo/hoops_counter/README.md deleted file mode 100644 index 7b739ea8..00000000 --- a/demo/hoops_counter/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# directory: ./demo/hoops_counter -## Router App/SDK sample applications - -Runs a few sub-demos. - -A web server is run as a sub-task: - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: power_loss.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [power_loss]: - -* check_input_delay=5 sec, how often to query the router status tree. -Polling too fast will impact router performance - possibly even prevent -operation. So select a reasonable value: a few seconds for DEMO purposes, -likely '30 sec' or '1 min' for normal operations. -The routine 'parse_duration' is used, so supported time tags include -"x sec", "x min", "x hr" and so on. diff --git a/demo/hoops_counter/__init__.py b/demo/hoops_counter/__init__.py deleted file mode 100644 index 86e6667a..00000000 --- a/demo/hoops_counter/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from demo.hoops_counter.hoops_counter import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/demo/hoops_counter/hoops_counter.py b/demo/hoops_counter/hoops_counter.py deleted file mode 100644 index 694f9f90..00000000 --- a/demo/hoops_counter/hoops_counter.py +++ /dev/null @@ -1,69 +0,0 @@ -import threading -import time - -import network.digit_web -import data.jsonrpc_settings -from cp_lib.app_base import CradlepointAppBase - - -def run_router_app(app_base, wait_for_child=True): - """ - Start our thread - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param bool wait_for_child: T to wait in loop, F to return immediately - :return: - """ - - # confirm we are running on 1100/1150, result should be like "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info( - "Product Model is good:{}".format(result)) - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - app_base.logger.info("STARTING the Web Server Task") - web_task = network.digit_web.run_router_app(app_base) - app_base.logger.info(" Web Server Task started") - - app_base.logger.info("STARTING the JSON Server Task") - json_task = data.jsonrpc_settings.run_router_app(app_base) - app_base.logger.info(" JSON Server Task started") - - if wait_for_child: - # we block on this sub task - for testing - try: - while True: - time.sleep(300) - - except KeyboardInterrupt: - # this is only true on a test PC - won't see on router - # must trap here to prevent try/except in __init__.py from avoiding - # the graceful shutdown below. - pass - - # now we need to try & kill off our kids - if we are here - app_base.logger.info("Okay, exiting") - - stop_router_app(app_base) - - else: - # we return ASAP, assume this is 1 of many tasks run by single parent - app_base.logger.info("Exit immediately, leave sub-task run") - - return 0 - - -def stop_router_app(app_base): - """ - Stop the thread - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - return 0 - - diff --git a/demo/hoops_counter/settings.ini b/demo/hoops_counter/settings.ini deleted file mode 100644 index 50dd86e2..00000000 --- a/demo/hoops_counter/settings.ini +++ /dev/null @@ -1,15 +0,0 @@ -[application] -name=hoops_counter -description=Thread to monitor 1100's GPIO, for loss of AC power -path=demo.hoops_counter -version=1.10 -uuid=de2968de-78d5-4ace-8345-1d93e42f7ceb - -[web_server] -; custom web page to show counter digits -host_port=9001 -start_count=2690 - -[jsonrpc] -; allow external push-in of new count -host_port=9901 diff --git a/demo/redlion_cub5/README.md b/demo/redlion_cub5/README.md deleted file mode 100644 index 5d85adc9..00000000 --- a/demo/redlion_cub5/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# directory: ./demo/redlion_cub5 -## Router App/SDK sample applications - -This demo includes ... - -## hardware Device: -The RedLion CUB5 is a small counter/meter, for panel cut outs 1.30 x 2.68 in. -This demo is specifically using it as a counter - to demo counting something -like people entering a door, motion detection, or Mike tossing a basketball -through a hoop. - -* http://files.redlion.net/filedepot_download/213/3984 -* http://www.redlion.net/products/industrial-automation/hmis-and-panel-meters/panel-meters/cub5-panel-meters - -Pin-outs: 485 with 4-wire JR11: -* Black = D+ -* Red = D- -* Green = SG -* Yellow = No Connection - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -with will be run by main.py diff --git a/demo/redlion_cub5/__init__.py b/demo/redlion_cub5/__init__.py deleted file mode 100644 index e773c1e9..00000000 --- a/demo/redlion_cub5/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from demo.redlion_cub5.cub5 import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/demo/redlion_cub5/cub5.py b/demo/redlion_cub5/cub5.py deleted file mode 100644 index 894780b7..00000000 --- a/demo/redlion_cub5/cub5.py +++ /dev/null @@ -1,56 +0,0 @@ -import threading -import time - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.cp_email import cp_send_email -from cp_lib.parse_duration import TimeDuration -from cp_lib.parse_data import parse_boolean - -power_loss_task = None - - -def run_router_app(app_base, wait_for_child=True): - """ - Start our thread - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param bool wait_for_child: T to wait in loop, F to return immediately - :return: - """ - global power_loss_task - - # confirm we are running on 1100/1150, result should be like "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info( - "Product Model is good:{}".format(result)) - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - power_loss_task = PowerLoss("power_loss", app_base) - power_loss_task.start() - - if wait_for_child: - # we block on this sub task - for testing - try: - while True: - time.sleep(300) - - except KeyboardInterrupt: - # this is only true on a test PC - won't see on router - # must trap here to prevent try/except in __init__.py from avoiding - # the graceful shutdown below. - pass - - # now we need to try & kill off our kids - if we are here - app_base.logger.info("Okay, exiting") - - stop_router_app(app_base) - - else: - # we return ASAP, assume this is 1 of many tasks run by single parent - app_base.logger.info("Exit immediately, leave sub-task run") - - return 0 diff --git a/demo/redlion_cub5/cub5_protocol.py b/demo/redlion_cub5/cub5_protocol.py deleted file mode 100644 index e9bf9e64..00000000 --- a/demo/redlion_cub5/cub5_protocol.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -The Redlion CUB5 protocol module -""" - - -class RedLionCub5(object): - - # will be like "N17" or "N5" - CMD_NODE = "N" - CMD_TRANSMIT = "T" # means read ... - - NODE_MIN = 0 - NODE_MAX = 99 - - MAP_ID = { - "CTA": "A", # counter A - "CTB": "B", # counter B - "RTE": "C", # rate - "SFA": "D", # scale factor A - "SFB": "E", # scale factor B - "SP1": "F", # setpoint 1 (reset output 1) - "SP2": "G", # setpoint 2 (reset output 2) - "CLD": "H", # counter A Count Load Value - } - - # "*" means 50msec; "$" means 2msec - TERMINATOR = "*" - - def __init__(self): - - self.node_address = 0 - # if node address = 0, can avoid sending, set this to True to override - # and send anyway - self.force_use_node_address = False - - # if set, allows normal logger output - self.logger = None - - return - - def set_node_address(self, address): - """ - Set the node address, which must be in value range 0-99 - - :param int address: the new address - :rtype str: - """ - if address is None or not isinstance(address, int): - raise ValueError("Invalid value for Node Address") - - assert self.NODE_MIN <= address <= self.NODE_MAX - self.node_address = address - return - - def format_node_address_string(self, address=None): - """ - Fetch the Node string, which might be "" - - :param int address: optional pass in, else use self.node_address - :rtype str: - """ - if address is None: - address = self.node_address - - address = int(address) - - if address == 0: - # special case, return empty, unless forced - if not self.force_use_node_address: - return "" - # else is being forced, so return as "N0" - - assert self.NODE_MIN <= address <= self.NODE_MAX - # not width is auto, so 1 digit or two - return "N%d" % address - - def format_read_value(self, mnemonic, address=None): - """ - read a value - :param str mnemonic: assume is like "CTA", "SFA" and so on - :param int address: optional pass in, else use self.node_address - :return: - """ - if address is None: - address = self.node_address - - mnemonic = mnemonic.upper() - code = self.MAP_ID[mnemonic] - # throws KeyError if bad mnemonic - return self.format_node_address_string(address) +\ - self.CMD_TRANSMIT + code + self.TERMINATOR - - def parse_response(self, response): - """ - Parse a counter response - :param response: - :rtype dict: - """ - if isinstance(response, bytes): - # Convert bytes to string - response = response.decode() - - if not isinstance(response, str): - raise TypeError("bad response type") - - result = dict() - result['raw'] = response.strip() - result["status"] = True - - # address is bytes 1-2, then #3 = - x = response[:2] - if x == " ": - result["adr"] = 0 - else: - # else assume is a number? - result["adr"] = int(x.strip()) - - # get the mnemonic - x = response[3:6].upper() - if x in self.MAP_ID: - result["id"] = x - else: - result["id"] = 'err?' - result["status"] = False - - # get the data value - # TODO - signed? - x = response[6:18] - try: - result["data"] = int(x.strip()) - except ValueError: - result["data"] = None - result["status"] = False - - return result diff --git a/demo/redlion_cub5/docs/CUB5 Product Manual.pdf b/demo/redlion_cub5/docs/CUB5 Product Manual.pdf deleted file mode 100644 index 1900612b..00000000 Binary files a/demo/redlion_cub5/docs/CUB5 Product Manual.pdf and /dev/null differ diff --git a/demo/redlion_cub5/settings.ini b/demo/redlion_cub5/settings.ini deleted file mode 100644 index a2af5902..00000000 --- a/demo/redlion_cub5/settings.ini +++ /dev/null @@ -1,9 +0,0 @@ -[application] -name=redlion_cub5 -description=Simple serial handling of Redlion Cub5 Counter -path=demo.redlion_cub5 -version=1.0 - -; this section and item is unique to this sample -[redlion] -baud=9600 diff --git a/demo/test/test_redlion_cub_protocol.py b/demo/test/test_redlion_cub_protocol.py deleted file mode 100644 index c8dd3186..00000000 --- a/demo/test/test_redlion_cub_protocol.py +++ /dev/null @@ -1,198 +0,0 @@ -# Test the demo.redlion_cub.cub5_protocol module - -# as of 2016, mu 4-wire RJ11 cable has 4 wires: -# 1) black: rdc D+ -# 2) red: rdc D- -# 3) green: rdc SG -# 4) yellow: N/C - -import logging -import time -import unittest - -from demo.redlion_cub5.cub5_protocol import RedLionCub5 - - -class TestRedLionCub5(unittest.TestCase): - - def test_format_node_address(self): - global _logger - - print("") # skip paste '.' on line - - obj = RedLionCub5() - obj.force_use_node_address = False - - tests = [ - (0, ''), (1, 'N1'), (2, 'N2'), (3, 'N3'), (4, 'N4'), (5, 'N5'), - (6, 'N6'), (7, 'N7'), (8, 'N8'), (9, 'N9'), (10, 'N10'), - (11, 'N11'), (12, 'N12'), (13, 'N13'), (14, 'N14'), (15, 'N15'), - (16, 'N16'), (17, 'N17'), (18, 'N18'), (19, 'N19'), (20, 'N20'), - (21, 'N21'), (22, 'N22'), (23, 'N23'), (24, 'N24'), (25, 'N25'), - (26, 'N26'), (27, 'N27'), (28, 'N28'), (29, 'N29'), (30, 'N30'), - (31, 'N31'), (32, 'N32'), (33, 'N33'), (34, 'N34'), (35, 'N35'), - (36, 'N36'), (37, 'N37'), (38, 'N38'), (39, 'N39'), (40, 'N40'), - (41, 'N41'), (42, 'N42'), (43, 'N43'), (44, 'N44'), (45, 'N45'), - (46, 'N46'), (47, 'N47'), (48, 'N48'), (49, 'N49'), (50, 'N50'), - (51, 'N51'), (52, 'N52'), (53, 'N53'), (54, 'N54'), (55, 'N55'), - (56, 'N56'), (57, 'N57'), (58, 'N58'), (59, 'N59'), (60, 'N60'), - (61, 'N61'), (62, 'N62'), (63, 'N63'), (64, 'N64'), (65, 'N65'), - (66, 'N66'), (67, 'N67'), (68, 'N68'), (69, 'N69'), (70, 'N70'), - (71, 'N71'), (72, 'N72'), (73, 'N73'), (74, 'N74'), (75, 'N75'), - (76, 'N76'), (77, 'N77'), (78, 'N78'), (79, 'N79'), (80, 'N80'), - (81, 'N81'), (82, 'N82'), (83, 'N83'), (84, 'N84'), (85, 'N85'), - (86, 'N86'), (87, 'N87'), (88, 'N88'), (89, 'N89'), (90, 'N90'), - (91, 'N91'), (92, 'N92'), (93, 'N93'), (94, 'N94'), (95, 'N95'), - (96, 'N96'), (97, 'N97'), (98, 'N98'), (99, 'N99') - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = obj.format_node_address_string(source) - self.assertEqual(result, expect) - - # test the 'N0' - obj.force_use_node_address = True - source = 0 - expect = 'N0' - result = obj.format_node_address_string(source) - self.assertEqual(result, expect) - obj.force_use_node_address = False - - # test the range - source = obj.NODE_MIN - 1 - with self.assertRaises(AssertionError): - obj.format_node_address_string(source) - source = obj.NODE_MAX + 1 - with self.assertRaises(AssertionError): - obj.format_node_address_string(source) - - # a few internal tests - tests = [ - (0, ''), (1, 'N1'), (48, 'N48'), (98, 'N98'), (99, 'N99'), - (None, ValueError), ('1', ValueError), ('hello', ValueError), - (98.0, ValueError) - ] - - for test in tests: - source = test[0] - expect = test[1] - # print("test:{}".format(test)) - - if expect == ValueError: - with self.assertRaises(ValueError): - obj.set_node_address(source) - - else: - obj.set_node_address(source) - result = obj.format_node_address_string() - self.assertEqual(result, expect) - - return - - def test_format_read(self): - global _logger - - print("") # skip paste '.' on line - - obj = RedLionCub5() - - tests = [ - {'adr': 0, 'id': 'CTA', 'exp': 'TA*'}, - {'adr': 5, 'id': 'CTA', 'exp': 'N5TA*'}, - {'adr': 5, 'id': 'RTE', 'exp': 'N5TC*'}, - {'adr': 15, 'id': 'SFA', 'exp': 'N15TD*'}, - {'adr': 15, 'id': 'SFB', 'exp': 'N15TE*'}, - {'adr': 5, 'id': 'SP1', 'exp': 'N5TF*'}, - {'adr': 5, 'id': 'SP2', 'exp': 'N5TG*'}, - {'adr': 5, 'id': 'CLD', 'exp': 'N5TH*'}, - ] - - for test in tests: - obj.set_node_address(test['adr']) - result = obj.format_read_value(test['id']) - self.assertEqual(result, test['exp']) - - # test the range - obj.set_node_address(5) - with self.assertRaises(AttributeError): - # due to lack of .upper() - obj.format_read_value(None) - - with self.assertRaises(KeyError): - # due to NOT being in self.MAP_ID - obj.format_read_value('silly') - - return - - def test_parse(self): - global _logger - - print("") # skip paste '.' on line - - obj = RedLionCub5() - - tests = [ - {'src': b' CTA 0\r\n', - 'exp': {'adr': 0, 'id': 'CTA', 'data': 0, 'status': True, - 'raw': 'CTA 0'}}, - {'src': ' CTA 0\r\n', - 'exp': {'adr': 0, 'id': 'CTA', 'data': 0, 'status': True, - 'raw': 'CTA 0'}}, - {'src': b' CTA 25\r\n', - 'exp': {'adr': 0, 'id': 'CTA', 'data': 25, 'status': True, - 'raw': 'CTA 25'}}, - {'src': ' CTA 25\r\n', - 'exp': {'adr': 0, 'id': 'CTA', 'data': 25, 'status': True, - 'raw': 'CTA 25'}}, - ] - - for test in tests: - result = obj.parse_response(test['src']) - # _logger.info("result {}".format(result)) - self.assertEqual(result, test['exp']) - - return - - def test_serial(self): - global _logger - - if False: - _logger.warning("Skip serial test due to flag") - - else: - import serial - - name = "COM5" - baud = 9600 - _logger.info("Open port {}".format(name)) - ser = serial.Serial(name, baudrate=baud, bytesize=8, - parity=serial.PARITY_NONE, timeout=1.0) - - send = b'TA*' - _logger.info("Write {}".format(send)) - ser.write(send) - result = ser.read(50) - _logger.info("Read {}".format(result)) - - time.sleep(1.0) - - send = b'TB*' - _logger.info("Write {}".format(send)) - ser.write(send) - result = ser.read(50) - _logger.info("Read {}".format(result)) - - ser.close() - -if __name__ == '__main__': - - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - - _logger = logging.getLogger('unittest') - _logger.setLevel(logging.DEBUG) - - unittest.main() diff --git a/email/cs.py b/email/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/email/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/email/email.py b/email/email.py new file mode 100644 index 00000000..a8ca67c3 --- /dev/null +++ b/email/email.py @@ -0,0 +1,68 @@ + +import sys +import argparse +import smtplib +import cs + +APP_NAME = 'email' + +# This application will send a single email when it is started. + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + email_username = '' + email_password = '' + + email_from = '' + email_to = '' + email_subject = 'This is the subject line for the test email.' + email_body = ' This is the body of the test email.' + + if command == 'start': + cs.CSClient().log(APP_NAME, 'Logging into email server') + + try: + # If you are using gmail, then the login account will need to + # turn 'Allow less secure apps' in the 'Sign-in & security' + # setting for the Google account. + server = smtplib.SMTP('smtp.gmail.com', 587) + server.ehlo() + server.starttls() + server.login(email_username, email_password) + + the_email = '\r\n'.join(['TO: %s' % email_to, + 'FROM: %s' % email_from, + 'SUBJECT: %s' % email_subject, '', + email_body]) + + cs.CSClient().log(APP_NAME, 'Sending email to: {}'.format(email_to)) + server.sendmail(email_from, email_to, the_email) + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Could not send email! exception: {}'.format(e)) + finally: + if server: + server.quit() + + elif command == 'stop': + # Nothing on stop + pass + except Exception as e: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/email/install.sh b/email/install.sh new file mode 100644 index 00000000..cdafe135 --- /dev/null +++ b/email/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION email on:" >> install.log +date >> install.log diff --git a/email/package.ini b/email/package.ini new file mode 100644 index 00000000..69406eac --- /dev/null +++ b/email/package.ini @@ -0,0 +1,11 @@ +[email] +uuid=dd91c8ea-cd95-4d9d-b08b-cf62de19684f +vendor=Cradlepoint +notes=Router Email Reference Application +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/email/start.sh b/email/start.sh new file mode 100644 index 00000000..ac7a5529 --- /dev/null +++ b/email/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython email.py start diff --git a/email/stop.sh b/email/stop.sh new file mode 100644 index 00000000..109b7bb5 --- /dev/null +++ b/email/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython email.py stop diff --git a/ftp_client/cs.py b/ftp_client/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/ftp_client/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/ftp_client/ftp_client.py b/ftp_client/ftp_client.py new file mode 100644 index 00000000..9200afb8 --- /dev/null +++ b/ftp_client/ftp_client.py @@ -0,0 +1,94 @@ +""" +This app will create a file and then upload it to an FTP server. +The file will be deleted when the app is stopped. +""" + +import sys +import argparse +from ftplib import FTP +import os +import cs + + +APP_NAME = "ftp_client" +TEMP_FILE = '/var/tmp/my_file.txt' + +# A USB Storage device will be mounted at /var/media +# if it is plugged into the USB port of the router. +# Note: Not all USB devices are compatible. +TEMP_FILE_USB = '/var/media/my_file.txt' + + +def start_router_app(): + # Create a temporary file to upload to an FTP server + try: + f = open(TEMP_FILE, 'w') + f.write('This is a test!!') + f.write('This is another test!!') + f.close() + except OSError as msg: + cs.CSClient().log(APP_NAME, 'Failed to open file: {}. error: {}'.format(TEMP_FILE, msg)) + + try: + # Connect to an FTP test server + ftp = FTP('speedtest.tele2.net') + + # Login to the server + reply = ftp.login('anonymous', 'anonymous') + cs.CSClient().log(APP_NAME, 'FTP login reply: {}'.format(reply)) + + # Change to the proper directory for upload + ftp.cwd('/upload/') + + # Open the file and upload it to the server + fh = open(TEMP_FILE, 'rb') + reply = ftp.storlines('STOR a.txt', fh) + cs.CSClient().log(APP_NAME, 'FTP STOR reply: {}'.format(reply)) + + except Exception as e: + cs.CSClient().log(APP_NAME, 'Something went wrong in start_router_app()! exception: {}'.format(e)) + raise + + finally: + if fh: + fh.close() + return + + +def stop_router_app(): + # delete the temporary file if it exists + if os.path.exists(TEMP_FILE): + os.remove(TEMP_FILE) + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + # Call the function to start the app. + start_router_app() + + elif command == 'stop': + # Call the function to start the app. + stop_router_app() + + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + # The start.sh and stop.sh should call this script with a start or stop argument + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/ftp_client/ftplib.py b/ftp_client/ftplib.py new file mode 100644 index 00000000..c416d856 --- /dev/null +++ b/ftp_client/ftplib.py @@ -0,0 +1,984 @@ +"""An FTP client class and some helper functions. + +Based on RFC 959: File Transfer Protocol (FTP), by J. Postel and J. Reynolds + +Example: + +>>> from ftplib import FTP +>>> ftp = FTP('ftp.python.org') # connect to host, default port +>>> ftp.login() # default, i.e.: user anonymous, passwd anonymous@ +'230 Guest login ok, access restrictions apply.' +>>> ftp.retrlines('LIST') # list directory contents +total 9 +drwxr-xr-x 8 root wheel 1024 Jan 3 1994 . +drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .. +drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin +drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc +d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming +drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib +drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub +drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr +-rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg +'226 Transfer complete.' +>>> ftp.quit() +'221 Goodbye.' +>>> + +A nice test that reveals some of the network dialogue would be: +python ftplib.py -d localhost -l -p -l +""" + +# +# Changes and improvements suggested by Steve Majewski. +# Modified by Jack to work on the mac. +# Modified by Siebren to support docstrings and PASV. +# Modified by Phil Schwartz to add storbinary and storlines callbacks. +# Modified by Giampaolo Rodola' to add TLS support. +# + +import os +import sys +import socket +import warnings +from socket import _GLOBAL_DEFAULT_TIMEOUT + +__all__ = ["FTP"] + +# Magic number from +MSG_OOB = 0x1 # Process data out of band + + +# The standard FTP server control port +FTP_PORT = 21 +# The sizehint parameter passed to readline() calls +MAXLINE = 8192 + + +# Exception raised when an error or invalid response is received +class Error(Exception): pass +class error_reply(Error): pass # unexpected [123]xx reply +class error_temp(Error): pass # 4xx errors +class error_perm(Error): pass # 5xx errors +class error_proto(Error): pass # response does not begin with [1-5] + + +# All exceptions (hopefully) that may be raised here and that aren't +# (always) programming errors on our side +all_errors = (Error, OSError, EOFError) + + +# Line terminators (we always output CRLF, but accept any of CRLF, CR, LF) +CRLF = '\r\n' +B_CRLF = b'\r\n' + +# The class itself +class FTP: + + '''An FTP client class. + + To create a connection, call the class using these arguments: + host, user, passwd, acct, timeout + + The first four arguments are all strings, and have default value ''. + timeout must be numeric and defaults to None if not passed, + meaning that no timeout will be set on any ftp socket(s) + If a timeout is passed, then this is now the default timeout for all ftp + socket operations for this instance. + + Then use self.connect() with optional host and port argument. + + To download a file, use ftp.retrlines('RETR ' + filename), + or ftp.retrbinary() with slightly different arguments. + To upload a file, use ftp.storlines() or ftp.storbinary(), + which have an open file as argument (see their definitions + below for details). + The download/upload functions first issue appropriate TYPE + and PORT or PASV commands. + ''' + + debugging = 0 + host = '' + port = FTP_PORT + maxline = MAXLINE + sock = None + file = None + welcome = None + passiveserver = 1 + encoding = "latin-1" + + # Initialization method (called by class instantiation). + # Initialize host to localhost, port to standard ftp port + # Optional arguments are host (for connect()), + # and user, passwd, acct (for login()) + def __init__(self, host='', user='', passwd='', acct='', + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + self.source_address = source_address + self.timeout = timeout + if host: + self.connect(host) + if user: + self.login(user, passwd, acct) + + def __enter__(self): + return self + + # Context management protocol: try to quit() if active + def __exit__(self, *args): + if self.sock is not None: + try: + self.quit() + except (OSError, EOFError): + pass + finally: + if self.sock is not None: + self.close() + + def connect(self, host='', port=0, timeout=-999, source_address=None): + '''Connect to host. Arguments are: + - host: hostname to connect to (string, default previous host) + - port: port to connect to (integer, default previous port) + - timeout: the timeout to set against the ftp socket(s) + - source_address: a 2-tuple (host, port) for the socket to bind + to as its source address before connecting. + ''' + if host != '': + self.host = host + if port > 0: + self.port = port + if timeout != -999: + self.timeout = timeout + if source_address is not None: + self.source_address = source_address + self.sock = socket.create_connection((self.host, self.port), self.timeout, + source_address=self.source_address) + self.af = self.sock.family + self.file = self.sock.makefile('r', encoding=self.encoding) + self.welcome = self.getresp() + return self.welcome + + def getwelcome(self): + '''Get the welcome message from the server. + (this is read and squirreled away by connect())''' + if self.debugging: + print('*welcome*', self.sanitize(self.welcome)) + return self.welcome + + def set_debuglevel(self, level): + '''Set the debugging level. + The required argument level means: + 0: no debugging output (default) + 1: print commands and responses but not body text etc. + 2: also print raw lines read and sent before stripping CR/LF''' + self.debugging = level + debug = set_debuglevel + + def set_pasv(self, val): + '''Use passive or active mode for data transfers. + With a false argument, use the normal PORT mode, + With a true argument, use the PASV command.''' + self.passiveserver = val + + # Internal: "sanitize" a string for printing + def sanitize(self, s): + if s[:5] in {'pass ', 'PASS '}: + i = len(s.rstrip('\r\n')) + s = s[:5] + '*'*(i-5) + s[i:] + return repr(s) + + # Internal: send one line to the server, appending CRLF + def putline(self, line): + line = line + CRLF + if self.debugging > 1: + print('*put*', self.sanitize(line)) + self.sock.sendall(line.encode(self.encoding)) + + # Internal: send one command to the server (through putline()) + def putcmd(self, line): + if self.debugging: print('*cmd*', self.sanitize(line)) + self.putline(line) + + # Internal: return one line from the server, stripping CRLF. + # Raise EOFError if the connection is closed + def getline(self): + line = self.file.readline(self.maxline + 1) + if len(line) > self.maxline: + raise Error("got more than %d bytes" % self.maxline) + if self.debugging > 1: + print('*get*', self.sanitize(line)) + if not line: + raise EOFError + if line[-2:] == CRLF: + line = line[:-2] + elif line[-1:] in CRLF: + line = line[:-1] + return line + + # Internal: get a response from the server, which may possibly + # consist of multiple lines. Return a single string with no + # trailing CRLF. If the response consists of multiple lines, + # these are separated by '\n' characters in the string + def getmultiline(self): + line = self.getline() + if line[3:4] == '-': + code = line[:3] + while 1: + nextline = self.getline() + line = line + ('\n' + nextline) + if nextline[:3] == code and \ + nextline[3:4] != '-': + break + return line + + # Internal: get a response from the server. + # Raise various errors if the response indicates an error + def getresp(self): + resp = self.getmultiline() + if self.debugging: + print('*resp*', self.sanitize(resp)) + self.lastresp = resp[:3] + c = resp[:1] + if c in {'1', '2', '3'}: + return resp + if c == '4': + raise error_temp(resp) + if c == '5': + raise error_perm(resp) + raise error_proto(resp) + + def voidresp(self): + """Expect a response beginning with '2'.""" + resp = self.getresp() + if resp[:1] != '2': + raise error_reply(resp) + return resp + + def abort(self): + '''Abort a file transfer. Uses out-of-band data. + This does not follow the procedure from the RFC to send Telnet + IP and Synch; that doesn't seem to work with the servers I've + tried. Instead, just send the ABOR command as OOB data.''' + line = b'ABOR' + B_CRLF + if self.debugging > 1: + print('*put urgent*', self.sanitize(line)) + self.sock.sendall(line, MSG_OOB) + resp = self.getmultiline() + if resp[:3] not in {'426', '225', '226'}: + raise error_proto(resp) + return resp + + def sendcmd(self, cmd): + '''Send a command and return the response.''' + self.putcmd(cmd) + return self.getresp() + + def voidcmd(self, cmd): + """Send a command and expect a response beginning with '2'.""" + self.putcmd(cmd) + return self.voidresp() + + def sendport(self, host, port): + '''Send a PORT command with the current host and the given + port number. + ''' + hbytes = host.split('.') + pbytes = [repr(port//256), repr(port%256)] + bytes = hbytes + pbytes + cmd = 'PORT ' + ','.join(bytes) + return self.voidcmd(cmd) + + def sendeprt(self, host, port): + '''Send an EPRT command with the current host and the given port number.''' + af = 0 + if self.af == socket.AF_INET: + af = 1 + if self.af == socket.AF_INET6: + af = 2 + if af == 0: + raise error_proto('unsupported address family') + fields = ['', repr(af), host, repr(port), ''] + cmd = 'EPRT ' + '|'.join(fields) + return self.voidcmd(cmd) + + def makeport(self): + '''Create a new socket and send a PORT command for it.''' + err = None + sock = None + for res in socket.getaddrinfo(None, 0, self.af, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): + af, socktype, proto, canonname, sa = res + try: + sock = socket.socket(af, socktype, proto) + sock.bind(sa) + except OSError as _: + err = _ + if sock: + sock.close() + sock = None + continue + break + if sock is None: + if err is not None: + raise err + else: + raise OSError("getaddrinfo returns an empty list") + sock.listen(1) + port = sock.getsockname()[1] # Get proper port + host = self.sock.getsockname()[0] # Get proper host + if self.af == socket.AF_INET: + resp = self.sendport(host, port) + else: + resp = self.sendeprt(host, port) + if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(self.timeout) + return sock + + def makepasv(self): + if self.af == socket.AF_INET: + host, port = parse227(self.sendcmd('PASV')) + else: + host, port = parse229(self.sendcmd('EPSV'), self.sock.getpeername()) + return host, port + + def ntransfercmd(self, cmd, rest=None): + """Initiate a transfer over the data connection. + + If the transfer is active, send a port command and the + transfer command, and accept the connection. If the server is + passive, send a pasv command, connect to it, and start the + transfer command. Either way, return the socket for the + connection and the expected size of the transfer. The + expected size may be None if it could not be determined. + + Optional `rest' argument can be a string that is sent as the + argument to a REST command. This is essentially a server + marker used to tell the server to skip over any data up to the + given marker. + """ + size = None + if self.passiveserver: + host, port = self.makepasv() + conn = socket.create_connection((host, port), self.timeout, + source_address=self.source_address) + try: + if rest is not None: + self.sendcmd("REST %s" % rest) + resp = self.sendcmd(cmd) + # Some servers apparently send a 200 reply to + # a LIST or STOR command, before the 150 reply + # (and way before the 226 reply). This seems to + # be in violation of the protocol (which only allows + # 1xx or error messages for LIST), so we just discard + # this response. + if resp[0] == '2': + resp = self.getresp() + if resp[0] != '1': + raise error_reply(resp) + except: + conn.close() + raise + else: + with self.makeport() as sock: + if rest is not None: + self.sendcmd("REST %s" % rest) + resp = self.sendcmd(cmd) + # See above. + if resp[0] == '2': + resp = self.getresp() + if resp[0] != '1': + raise error_reply(resp) + conn, sockaddr = sock.accept() + if self.timeout is not _GLOBAL_DEFAULT_TIMEOUT: + conn.settimeout(self.timeout) + if resp[:3] == '150': + # this is conditional in case we received a 125 + size = parse150(resp) + return conn, size + + def transfercmd(self, cmd, rest=None): + """Like ntransfercmd() but returns only the socket.""" + return self.ntransfercmd(cmd, rest)[0] + + def login(self, user = '', passwd = '', acct = ''): + '''Login, default anonymous.''' + if not user: + user = 'anonymous' + if not passwd: + passwd = '' + if not acct: + acct = '' + if user == 'anonymous' and passwd in {'', '-'}: + # If there is no anonymous ftp password specified + # then we'll just use anonymous@ + # We don't send any other thing because: + # - We want to remain anonymous + # - We want to stop SPAM + # - We don't want to let ftp sites to discriminate by the user, + # host or country. + passwd = passwd + 'anonymous@' + resp = self.sendcmd('USER ' + user) + if resp[0] == '3': + resp = self.sendcmd('PASS ' + passwd) + if resp[0] == '3': + resp = self.sendcmd('ACCT ' + acct) + if resp[0] != '2': + raise error_reply(resp) + return resp + + def retrbinary(self, cmd, callback, blocksize=8192, rest=None): + """Retrieve data in binary mode. A new port is created for you. + + Args: + cmd: A RETR command. + callback: A single parameter callable to be called on each + block of data read. + blocksize: The maximum number of bytes to read from the + socket at one time. [default: 8192] + rest: Passed to transfercmd(). [default: None] + + Returns: + The response code. + """ + self.voidcmd('TYPE I') + with self.transfercmd(cmd, rest) as conn: + while 1: + data = conn.recv(blocksize) + if not data: + break + callback(data) + # shutdown ssl layer + if _SSLSocket is not None and isinstance(conn, _SSLSocket): + conn.unwrap() + return self.voidresp() + + def retrlines(self, cmd, callback = None): + """Retrieve data in line mode. A new port is created for you. + + Args: + cmd: A RETR, LIST, or NLST command. + callback: An optional single parameter callable that is called + for each line with the trailing CRLF stripped. + [default: print_line()] + + Returns: + The response code. + """ + if callback is None: + callback = print_line + resp = self.sendcmd('TYPE A') + with self.transfercmd(cmd) as conn, \ + conn.makefile('r', encoding=self.encoding) as fp: + while 1: + line = fp.readline(self.maxline + 1) + if len(line) > self.maxline: + raise Error("got more than %d bytes" % self.maxline) + if self.debugging > 2: + print('*retr*', repr(line)) + if not line: + break + if line[-2:] == CRLF: + line = line[:-2] + elif line[-1:] == '\n': + line = line[:-1] + callback(line) + # shutdown ssl layer + if _SSLSocket is not None and isinstance(conn, _SSLSocket): + conn.unwrap() + return self.voidresp() + + def storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None): + """Store a file in binary mode. A new port is created for you. + + Args: + cmd: A STOR command. + fp: A file-like object with a read(num_bytes) method. + blocksize: The maximum data size to read from fp and send over + the connection at once. [default: 8192] + callback: An optional single parameter callable that is called on + each block of data after it is sent. [default: None] + rest: Passed to transfercmd(). [default: None] + + Returns: + The response code. + """ + self.voidcmd('TYPE I') + with self.transfercmd(cmd, rest) as conn: + while 1: + buf = fp.read(blocksize) + if not buf: + break + conn.sendall(buf) + if callback: + callback(buf) + # shutdown ssl layer + if _SSLSocket is not None and isinstance(conn, _SSLSocket): + conn.unwrap() + return self.voidresp() + + def storlines(self, cmd, fp, callback=None): + """Store a file in line mode. A new port is created for you. + + Args: + cmd: A STOR command. + fp: A file-like object with a readline() method. + callback: An optional single parameter callable that is called on + each line after it is sent. [default: None] + + Returns: + The response code. + """ + self.voidcmd('TYPE A') + with self.transfercmd(cmd) as conn: + while 1: + buf = fp.readline(self.maxline + 1) + if len(buf) > self.maxline: + raise Error("got more than %d bytes" % self.maxline) + if not buf: + break + if buf[-2:] != B_CRLF: + if buf[-1] in B_CRLF: buf = buf[:-1] + buf = buf + B_CRLF + conn.sendall(buf) + if callback: + callback(buf) + # shutdown ssl layer + if _SSLSocket is not None and isinstance(conn, _SSLSocket): + conn.unwrap() + return self.voidresp() + + def acct(self, password): + '''Send new account name.''' + cmd = 'ACCT ' + password + return self.voidcmd(cmd) + + def nlst(self, *args): + '''Return a list of files in a given directory (default the current).''' + cmd = 'NLST' + for arg in args: + cmd = cmd + (' ' + arg) + files = [] + self.retrlines(cmd, files.append) + return files + + def dir(self, *args): + '''List a directory in long form. + By default list current directory to stdout. + Optional last argument is callback function; all + non-empty arguments before it are concatenated to the + LIST command. (This *should* only be used for a pathname.)''' + cmd = 'LIST' + func = None + if args[-1:] and type(args[-1]) != type(''): + args, func = args[:-1], args[-1] + for arg in args: + if arg: + cmd = cmd + (' ' + arg) + self.retrlines(cmd, func) + + def mlsd(self, path="", facts=[]): + '''List a directory in a standardized format by using MLSD + command (RFC-3659). If path is omitted the current directory + is assumed. "facts" is a list of strings representing the type + of information desired (e.g. ["type", "size", "perm"]). + + Return a generator object yielding a tuple of two elements + for every file found in path. + First element is the file name, the second one is a dictionary + including a variable number of "facts" depending on the server + and whether "facts" argument has been provided. + ''' + if facts: + self.sendcmd("OPTS MLST " + ";".join(facts) + ";") + if path: + cmd = "MLSD %s" % path + else: + cmd = "MLSD" + lines = [] + self.retrlines(cmd, lines.append) + for line in lines: + facts_found, _, name = line.rstrip(CRLF).partition(' ') + entry = {} + for fact in facts_found[:-1].split(";"): + key, _, value = fact.partition("=") + entry[key.lower()] = value + yield (name, entry) + + def rename(self, fromname, toname): + '''Rename a file.''' + resp = self.sendcmd('RNFR ' + fromname) + if resp[0] != '3': + raise error_reply(resp) + return self.voidcmd('RNTO ' + toname) + + def delete(self, filename): + '''Delete a file.''' + resp = self.sendcmd('DELE ' + filename) + if resp[:3] in {'250', '200'}: + return resp + else: + raise error_reply(resp) + + def cwd(self, dirname): + '''Change to a directory.''' + if dirname == '..': + try: + return self.voidcmd('CDUP') + except error_perm as msg: + if msg.args[0][:3] != '500': + raise + elif dirname == '': + dirname = '.' # does nothing, but could return error + cmd = 'CWD ' + dirname + return self.voidcmd(cmd) + + def size(self, filename): + '''Retrieve the size of a file.''' + # The SIZE command is defined in RFC-3659 + resp = self.sendcmd('SIZE ' + filename) + if resp[:3] == '213': + s = resp[3:].strip() + return int(s) + + def mkd(self, dirname): + '''Make a directory, return its full pathname.''' + resp = self.voidcmd('MKD ' + dirname) + # fix around non-compliant implementations such as IIS shipped + # with Windows server 2003 + if not resp.startswith('257'): + return '' + return parse257(resp) + + def rmd(self, dirname): + '''Remove a directory.''' + return self.voidcmd('RMD ' + dirname) + + def pwd(self): + '''Return current working directory.''' + resp = self.voidcmd('PWD') + # fix around non-compliant implementations such as IIS shipped + # with Windows server 2003 + if not resp.startswith('257'): + return '' + return parse257(resp) + + def quit(self): + '''Quit, and close the connection.''' + resp = self.voidcmd('QUIT') + self.close() + return resp + + def close(self): + '''Close the connection without assuming anything about it.''' + try: + file = self.file + self.file = None + if file is not None: + file.close() + finally: + sock = self.sock + self.sock = None + if sock is not None: + sock.close() + +try: + import ssl +except ImportError: + _SSLSocket = None +else: + _SSLSocket = ssl.SSLSocket + + class FTP_TLS(FTP): + '''A FTP subclass which adds TLS support to FTP as described + in RFC-4217. + + Connect as usual to port 21 implicitly securing the FTP control + connection before authenticating. + + Securing the data connection requires user to explicitly ask + for it by calling prot_p() method. + + Usage example: + >>> from ftplib import FTP_TLS + >>> ftps = FTP_TLS('ftp.python.org') + >>> ftps.login() # login anonymously previously securing control channel + '230 Guest login ok, access restrictions apply.' + >>> ftps.prot_p() # switch to secure data connection + '200 Protection level set to P' + >>> ftps.retrlines('LIST') # list directory content securely + total 9 + drwxr-xr-x 8 root wheel 1024 Jan 3 1994 . + drwxr-xr-x 8 root wheel 1024 Jan 3 1994 .. + drwxr-xr-x 2 root wheel 1024 Jan 3 1994 bin + drwxr-xr-x 2 root wheel 1024 Jan 3 1994 etc + d-wxrwxr-x 2 ftp wheel 1024 Sep 5 13:43 incoming + drwxr-xr-x 2 root wheel 1024 Nov 17 1993 lib + drwxr-xr-x 6 1094 wheel 1024 Sep 13 19:07 pub + drwxr-xr-x 3 root wheel 1024 Jan 3 1994 usr + -rw-r--r-- 1 root root 312 Aug 1 1994 welcome.msg + '226 Transfer complete.' + >>> ftps.quit() + '221 Goodbye.' + >>> + ''' + ssl_version = ssl.PROTOCOL_SSLv23 + + def __init__(self, host='', user='', passwd='', acct='', keyfile=None, + certfile=None, context=None, + timeout=_GLOBAL_DEFAULT_TIMEOUT, source_address=None): + if context is not None and keyfile is not None: + raise ValueError("context and keyfile arguments are mutually " + "exclusive") + if context is not None and certfile is not None: + raise ValueError("context and certfile arguments are mutually " + "exclusive") + self.keyfile = keyfile + self.certfile = certfile + if context is None: + context = ssl._create_stdlib_context(self.ssl_version, + certfile=certfile, + keyfile=keyfile) + self.context = context + self._prot_p = False + FTP.__init__(self, host, user, passwd, acct, timeout, source_address) + + def login(self, user='', passwd='', acct='', secure=True): + if secure and not isinstance(self.sock, ssl.SSLSocket): + self.auth() + return FTP.login(self, user, passwd, acct) + + def auth(self): + '''Set up secure control connection by using TLS/SSL.''' + if isinstance(self.sock, ssl.SSLSocket): + raise ValueError("Already using TLS") + if self.ssl_version >= ssl.PROTOCOL_SSLv23: + resp = self.voidcmd('AUTH TLS') + else: + resp = self.voidcmd('AUTH SSL') + self.sock = self.context.wrap_socket(self.sock, + server_hostname=self.host) + self.file = self.sock.makefile(mode='r', encoding=self.encoding) + return resp + + def ccc(self): + '''Switch back to a clear-text control connection.''' + if not isinstance(self.sock, ssl.SSLSocket): + raise ValueError("not using TLS") + resp = self.voidcmd('CCC') + self.sock = self.sock.unwrap() + return resp + + def prot_p(self): + '''Set up secure data connection.''' + # PROT defines whether or not the data channel is to be protected. + # Though RFC-2228 defines four possible protection levels, + # RFC-4217 only recommends two, Clear and Private. + # Clear (PROT C) means that no security is to be used on the + # data-channel, Private (PROT P) means that the data-channel + # should be protected by TLS. + # PBSZ command MUST still be issued, but must have a parameter of + # '0' to indicate that no buffering is taking place and the data + # connection should not be encapsulated. + self.voidcmd('PBSZ 0') + resp = self.voidcmd('PROT P') + self._prot_p = True + return resp + + def prot_c(self): + '''Set up clear text data connection.''' + resp = self.voidcmd('PROT C') + self._prot_p = False + return resp + + # --- Overridden FTP methods + + def ntransfercmd(self, cmd, rest=None): + conn, size = FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, + server_hostname=self.host) + return conn, size + + def abort(self): + # overridden as we can't pass MSG_OOB flag to sendall() + line = b'ABOR' + B_CRLF + self.sock.sendall(line) + resp = self.getmultiline() + if resp[:3] not in {'426', '225', '226'}: + raise error_proto(resp) + return resp + + __all__.append('FTP_TLS') + all_errors = (Error, OSError, EOFError, ssl.SSLError) + + +_150_re = None + +def parse150(resp): + '''Parse the '150' response for a RETR request. + Returns the expected transfer size or None; size is not guaranteed to + be present in the 150 message. + ''' + if resp[:3] != '150': + raise error_reply(resp) + global _150_re + if _150_re is None: + import re + _150_re = re.compile( + "150 .* \((\d+) bytes\)", re.IGNORECASE | re.ASCII) + m = _150_re.match(resp) + if not m: + return None + return int(m.group(1)) + + +_227_re = None + +def parse227(resp): + '''Parse the '227' response for a PASV request. + Raises error_proto if it does not contain '(h1,h2,h3,h4,p1,p2)' + Return ('host.addr.as.numbers', port#) tuple.''' + + if resp[:3] != '227': + raise error_reply(resp) + global _227_re + if _227_re is None: + import re + _227_re = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)', re.ASCII) + m = _227_re.search(resp) + if not m: + raise error_proto(resp) + numbers = m.groups() + host = '.'.join(numbers[:4]) + port = (int(numbers[4]) << 8) + int(numbers[5]) + return host, port + + +def parse229(resp, peer): + '''Parse the '229' response for an EPSV request. + Raises error_proto if it does not contain '(|||port|)' + Return ('host.addr.as.numbers', port#) tuple.''' + + if resp[:3] != '229': + raise error_reply(resp) + left = resp.find('(') + if left < 0: raise error_proto(resp) + right = resp.find(')', left + 1) + if right < 0: + raise error_proto(resp) # should contain '(|||port|)' + if resp[left + 1] != resp[right - 1]: + raise error_proto(resp) + parts = resp[left + 1:right].split(resp[left+1]) + if len(parts) != 5: + raise error_proto(resp) + host = peer[0] + port = int(parts[3]) + return host, port + + +def parse257(resp): + '''Parse the '257' response for a MKD or PWD request. + This is a response to a MKD or PWD request: a directory name. + Returns the directoryname in the 257 reply.''' + + if resp[:3] != '257': + raise error_reply(resp) + if resp[3:5] != ' "': + return '' # Not compliant to RFC 959, but UNIX ftpd does this + dirname = '' + i = 5 + n = len(resp) + while i < n: + c = resp[i] + i = i+1 + if c == '"': + if i >= n or resp[i] != '"': + break + i = i+1 + dirname = dirname + c + return dirname + + +def print_line(line): + '''Default retrlines callback to print a line.''' + print(line) + + +def ftpcp(source, sourcename, target, targetname = '', type = 'I'): + '''Copy file from one FTP-instance to another.''' + if not targetname: + targetname = sourcename + type = 'TYPE ' + type + source.voidcmd(type) + target.voidcmd(type) + sourcehost, sourceport = parse227(source.sendcmd('PASV')) + target.sendport(sourcehost, sourceport) + # RFC 959: the user must "listen" [...] BEFORE sending the + # transfer request. + # So: STOR before RETR, because here the target is a "user". + treply = target.sendcmd('STOR ' + targetname) + if treply[:3] not in {'125', '150'}: + raise error_proto # RFC 959 + sreply = source.sendcmd('RETR ' + sourcename) + if sreply[:3] not in {'125', '150'}: + raise error_proto # RFC 959 + source.voidresp() + target.voidresp() + + +def test(): + '''Test program. + Usage: ftp [-d] [-r[file]] host [-l[dir]] [-d[dir]] [-p] [file] ... + + -d dir + -l list + -p password + ''' + + if len(sys.argv) < 2: + print(test.__doc__) + sys.exit(0) + + import netrc + + debugging = 0 + rcfile = None + while sys.argv[1] == '-d': + debugging = debugging+1 + del sys.argv[1] + if sys.argv[1][:2] == '-r': + # get name of alternate ~/.netrc file: + rcfile = sys.argv[1][2:] + del sys.argv[1] + host = sys.argv[1] + ftp = FTP(host) + ftp.set_debuglevel(debugging) + userid = passwd = acct = '' + try: + netrcobj = netrc.netrc(rcfile) + except OSError: + if rcfile is not None: + sys.stderr.write("Could not open account file" + " -- using anonymous login.") + else: + try: + userid, acct, passwd = netrcobj.authenticators(host) + except KeyError: + # no account for host + sys.stderr.write( + "No account -- using anonymous login.") + ftp.login(userid, passwd, acct) + for file in sys.argv[2:]: + if file[:2] == '-l': + ftp.dir(file[2:]) + elif file[:2] == '-d': + cmd = 'CWD' + if file[2:]: cmd = cmd + ' ' + file[2:] + resp = ftp.sendcmd(cmd) + elif file == '-p': + ftp.set_pasv(not ftp.passiveserver) + else: + ftp.retrbinary('RETR ' + file, \ + sys.stdout.write, 1024) + ftp.quit() + + +if __name__ == '__main__': + test() diff --git a/ftp_client/install.sh b/ftp_client/install.sh new file mode 100644 index 00000000..83e6a388 --- /dev/null +++ b/ftp_client/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION ftp_client on:" >> install.log +date >> install.log diff --git a/ftp_client/package.ini b/ftp_client/package.ini new file mode 100644 index 00000000..3372106e --- /dev/null +++ b/ftp_client/package.ini @@ -0,0 +1,11 @@ +[ftp_to_server] +uuid=1687727c-fd6c-48ff-807d-34a32faaedf8 +vendor=Cradlepoint +notes=A sample app that will upload a file to an ftp server +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/ftp_client/start.sh b/ftp_client/start.sh new file mode 100644 index 00000000..01dec69d --- /dev/null +++ b/ftp_client/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ftp_client.py start diff --git a/ftp_client/stop.sh b/ftp_client/stop.sh new file mode 100644 index 00000000..5ac6c22a --- /dev/null +++ b/ftp_client/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ftp_client.py stop diff --git a/ftp_server/asynchat.py b/ftp_server/asynchat.py new file mode 100644 index 00000000..f728d1b4 --- /dev/null +++ b/ftp_server/asynchat.py @@ -0,0 +1,336 @@ +# -*- Mode: Python; tab-width: 4 -*- +# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp +# Author: Sam Rushing + +# ====================================================================== +# Copyright 1996 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +r"""A class supporting chat-style (command/response) protocols. + +This class adds support for 'chat' style protocols - where one side +sends a 'command', and the other sends a response (examples would be +the common internet protocols - smtp, nntp, ftp, etc..). + +The handle_read() method looks at the input stream for the current +'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n' +for multi-line output), calling self.found_terminator() on its +receipt. + +for example: +Say you build an async nntp client using this class. At the start +of the connection, you'll have self.terminator set to '\r\n', in +order to process the single-line greeting. Just before issuing a +'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST +command will be accumulated (using your own 'collect_incoming_data' +method) up to the terminator, and then control will be returned to +you - by calling your self.found_terminator() method. +""" +import asyncore +from collections import deque + + +class async_chat(asyncore.dispatcher): + """This is an abstract class. You must derive from this class, and add + the two methods collect_incoming_data() and found_terminator()""" + + # these are overridable defaults + + ac_in_buffer_size = 65536 + ac_out_buffer_size = 65536 + + # we don't want to enable the use of encoding by default, because that is a + # sign of an application bug that we don't want to pass silently + + use_encoding = 0 + encoding = 'latin-1' + + def __init__(self, sock=None, map=None): + # for string terminator matching + self.ac_in_buffer = b'' + + # we use a list here rather than io.BytesIO for a few reasons... + # del lst[:] is faster than bio.truncate(0) + # lst = [] is faster than bio.truncate(0) + self.incoming = [] + + # we toss the use of the "simple producer" and replace it with + # a pure deque, which the original fifo was a wrapping of + self.producer_fifo = deque() + asyncore.dispatcher.__init__(self, sock, map) + + def collect_incoming_data(self, data): + raise NotImplementedError("must be implemented in subclass") + + def _collect_incoming_data(self, data): + self.incoming.append(data) + + def _get_data(self): + d = b''.join(self.incoming) + del self.incoming[:] + return d + + def found_terminator(self): + raise NotImplementedError("must be implemented in subclass") + + def set_terminator(self, term): + """Set the input delimiter. + + Can be a fixed string of any length, an integer, or None. + """ + if isinstance(term, str) and self.use_encoding: + term = bytes(term, self.encoding) + elif isinstance(term, int) and term < 0: + raise ValueError('the number of received bytes must be positive') + self.terminator = term + + def get_terminator(self): + return self.terminator + + # grab some more data from the socket, + # throw it to the collector method, + # check for the terminator, + # if found, transition to the next state. + + def handle_read(self): + + try: + data = self.recv(self.ac_in_buffer_size) + except BlockingIOError: + return + except OSError as why: + self.handle_error() + return + + if isinstance(data, str) and self.use_encoding: + data = bytes(str, self.encoding) + self.ac_in_buffer = self.ac_in_buffer + data + + # Continue to search for self.terminator in self.ac_in_buffer, + # while calling self.collect_incoming_data. The while loop + # is necessary because we might read several data+terminator + # combos with a single recv(4096). + + while self.ac_in_buffer: + lb = len(self.ac_in_buffer) + terminator = self.get_terminator() + if not terminator: + # no terminator, collect it all + self.collect_incoming_data(self.ac_in_buffer) + self.ac_in_buffer = b'' + elif isinstance(terminator, int): + # numeric terminator + n = terminator + if lb < n: + self.collect_incoming_data(self.ac_in_buffer) + self.ac_in_buffer = b'' + self.terminator = self.terminator - lb + else: + self.collect_incoming_data(self.ac_in_buffer[:n]) + self.ac_in_buffer = self.ac_in_buffer[n:] + self.terminator = 0 + self.found_terminator() + else: + # 3 cases: + # 1) end of buffer matches terminator exactly: + # collect data, transition + # 2) end of buffer matches some prefix: + # collect data to the prefix + # 3) end of buffer does not match any prefix: + # collect data + terminator_len = len(terminator) + index = self.ac_in_buffer.find(terminator) + if index != -1: + # we found the terminator + if index > 0: + # don't bother reporting the empty string + # (source of subtle bugs) + self.collect_incoming_data(self.ac_in_buffer[:index]) + self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:] + # This does the Right Thing if the terminator + # is changed here. + self.found_terminator() + else: + # check for a prefix of the terminator + index = find_prefix_at_end(self.ac_in_buffer, terminator) + if index: + if index != lb: + # we found a prefix, collect up to the prefix + self.collect_incoming_data(self.ac_in_buffer[:-index]) + self.ac_in_buffer = self.ac_in_buffer[-index:] + break + else: + # no prefix, collect it all + self.collect_incoming_data(self.ac_in_buffer) + self.ac_in_buffer = b'' + + def handle_write(self): + self.initiate_send() + + def handle_close(self): + self.close() + + def push(self, data): + if not isinstance(data, (bytes, bytearray, memoryview)): + raise TypeError('data argument must be byte-ish (%r)', + type(data)) + sabs = self.ac_out_buffer_size + if len(data) > sabs: + for i in range(0, len(data), sabs): + self.producer_fifo.append(data[i:i+sabs]) + else: + self.producer_fifo.append(data) + self.initiate_send() + + def push_with_producer(self, producer): + self.producer_fifo.append(producer) + self.initiate_send() + + def readable(self): + "predicate for inclusion in the readable for select()" + # cannot use the old predicate, it violates the claim of the + # set_terminator method. + + # return (len(self.ac_in_buffer) <= self.ac_in_buffer_size) + return 1 + + def writable(self): + "predicate for inclusion in the writable for select()" + return self.producer_fifo or (not self.connected) + + def close_when_done(self): + "automatically close this channel once the outgoing queue is empty" + self.producer_fifo.append(None) + + def initiate_send(self): + while self.producer_fifo and self.connected: + first = self.producer_fifo[0] + # handle empty string/buffer or None entry + if not first: + del self.producer_fifo[0] + if first is None: + self.handle_close() + return + + # handle classic producer behavior + obs = self.ac_out_buffer_size + try: + data = first[:obs] + except TypeError: + data = first.more() + if data: + self.producer_fifo.appendleft(data) + else: + del self.producer_fifo[0] + continue + + if isinstance(data, str) and self.use_encoding: + data = bytes(data, self.encoding) + + # send the data + try: + num_sent = self.send(data) + except OSError: + self.handle_error() + return + + if num_sent: + if num_sent < len(data) or obs < len(first): + self.producer_fifo[0] = first[num_sent:] + else: + del self.producer_fifo[0] + # we tried to send some actual data + return + + def discard_buffers(self): + # Emergencies only! + self.ac_in_buffer = b'' + del self.incoming[:] + self.producer_fifo.clear() + + +class simple_producer: + + def __init__(self, data, buffer_size=512): + self.data = data + self.buffer_size = buffer_size + + def more(self): + if len(self.data) > self.buffer_size: + result = self.data[:self.buffer_size] + self.data = self.data[self.buffer_size:] + return result + else: + result = self.data + self.data = b'' + return result + + +class fifo: + def __init__(self, list=None): + import warnings + warnings.warn('fifo class will be removed in Python 3.6', + DeprecationWarning, stacklevel=2) + if not list: + self.list = deque() + else: + self.list = deque(list) + + def __len__(self): + return len(self.list) + + def is_empty(self): + return not self.list + + def first(self): + return self.list[0] + + def push(self, data): + self.list.append(data) + + def pop(self): + if self.list: + return (1, self.list.popleft()) + else: + return (0, None) + + +# Given 'haystack', see if any prefix of 'needle' is at its end. This +# assumes an exact match has already been checked. Return the number of +# characters matched. +# for example: +# f_p_a_e("qwerty\r", "\r\n") => 1 +# f_p_a_e("qwertydkjf", "\r\n") => 0 +# f_p_a_e("qwerty\r\n", "\r\n") => + +# this could maybe be made faster with a computed regex? +# [answer: no; circa Python-2.0, Jan 2001] +# new python: 28961/s +# old python: 18307/s +# re: 12820/s +# regex: 14035/s + +def find_prefix_at_end(haystack, needle): + l = len(needle) - 1 + while l and not haystack.endswith(needle[:l]): + l -= 1 + return l diff --git a/ftp_server/asyncore.py b/ftp_server/asyncore.py new file mode 100644 index 00000000..3b51f0f3 --- /dev/null +++ b/ftp_server/asyncore.py @@ -0,0 +1,643 @@ +# -*- Mode: Python -*- +# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp +# Author: Sam Rushing + +# ====================================================================== +# Copyright 1996 by Sam Rushing +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of Sam +# Rushing not be used in advertising or publicity pertaining to +# distribution of the software without specific, written prior +# permission. +# +# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# ====================================================================== + +"""Basic infrastructure for asynchronous socket service clients and servers. + +There are only two ways to have a program on a single processor do "more +than one thing at a time". Multi-threaded programming is the simplest and +most popular way to do it, but there is another very different technique, +that lets you have nearly all the advantages of multi-threading, without +actually using multiple threads. it's really only practical if your program +is largely I/O bound. If your program is CPU bound, then pre-emptive +scheduled threads are probably what you really need. Network servers are +rarely CPU-bound, however. + +If your operating system supports the select() system call in its I/O +library (and nearly all do), then you can use it to juggle multiple +communication channels at once; doing other work while your I/O is taking +place in the "background." Although this strategy can seem strange and +complex, especially at first, it is in many ways easier to understand and +control than multi-threaded programming. The module documented here solves +many of the difficult problems for you, making the task of building +sophisticated high-performance network servers and clients a snap. +""" + +import select +import socket +import sys +import time +import warnings + +import os +from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, EINVAL, \ + ENOTCONN, ESHUTDOWN, EISCONN, EBADF, ECONNABORTED, EPIPE, EAGAIN, \ + errorcode + +_DISCONNECTED = frozenset({ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EPIPE, + EBADF}) + +try: + socket_map +except NameError: + socket_map = {} + +def _strerror(err): + try: + return os.strerror(err) + except (ValueError, OverflowError, NameError): + if err in errorcode: + return errorcode[err] + return "Unknown error %s" %err + +class ExitNow(Exception): + pass + +_reraised_exceptions = (ExitNow, KeyboardInterrupt, SystemExit) + +def read(obj): + try: + obj.handle_read_event() + except _reraised_exceptions: + raise + except: + obj.handle_error() + +def write(obj): + try: + obj.handle_write_event() + except _reraised_exceptions: + raise + except: + obj.handle_error() + +def _exception(obj): + try: + obj.handle_expt_event() + except _reraised_exceptions: + raise + except: + obj.handle_error() + +def readwrite(obj, flags): + try: + if flags & select.POLLIN: + obj.handle_read_event() + if flags & select.POLLOUT: + obj.handle_write_event() + if flags & select.POLLPRI: + obj.handle_expt_event() + if flags & (select.POLLHUP | select.POLLERR | select.POLLNVAL): + obj.handle_close() + except OSError as e: + if e.args[0] not in _DISCONNECTED: + obj.handle_error() + else: + obj.handle_close() + except _reraised_exceptions: + raise + except: + obj.handle_error() + +def poll(timeout=0.0, map=None): + if map is None: + map = socket_map + if map: + r = []; w = []; e = [] + for fd, obj in list(map.items()): + is_r = obj.readable() + is_w = obj.writable() + if is_r: + r.append(fd) + # accepting sockets should not be writable + if is_w and not obj.accepting: + w.append(fd) + if is_r or is_w: + e.append(fd) + if [] == r == w == e: + time.sleep(timeout) + return + + r, w, e = select.select(r, w, e, timeout) + + for fd in r: + obj = map.get(fd) + if obj is None: + continue + read(obj) + + for fd in w: + obj = map.get(fd) + if obj is None: + continue + write(obj) + + for fd in e: + obj = map.get(fd) + if obj is None: + continue + _exception(obj) + +def poll2(timeout=0.0, map=None): + # Use the poll() support added to the select module in Python 2.0 + if map is None: + map = socket_map + if timeout is not None: + # timeout is in milliseconds + timeout = int(timeout*1000) + pollster = select.poll() + if map: + for fd, obj in list(map.items()): + flags = 0 + if obj.readable(): + flags |= select.POLLIN | select.POLLPRI + # accepting sockets should not be writable + if obj.writable() and not obj.accepting: + flags |= select.POLLOUT + if flags: + pollster.register(fd, flags) + + r = pollster.poll(timeout) + for fd, flags in r: + obj = map.get(fd) + if obj is None: + continue + readwrite(obj, flags) + +poll3 = poll2 # Alias for backward compatibility + +def loop(timeout=30.0, use_poll=False, map=None, count=None): + if map is None: + map = socket_map + + if use_poll and hasattr(select, 'poll'): + poll_fun = poll2 + else: + poll_fun = poll + + if count is None: + while map: + poll_fun(timeout, map) + + else: + while map and count > 0: + poll_fun(timeout, map) + count = count - 1 + +class dispatcher: + + debug = False + connected = False + accepting = False + connecting = False + closing = False + addr = None + ignore_log_types = frozenset({'warning'}) + + def __init__(self, sock=None, map=None): + if map is None: + self._map = socket_map + else: + self._map = map + + self._fileno = None + + if sock: + # Set to nonblocking just to make sure for cases where we + # get a socket from a blocking source. + sock.setblocking(0) + self.set_socket(sock, map) + self.connected = True + # The constructor no longer requires that the socket + # passed be connected. + try: + self.addr = sock.getpeername() + except OSError as err: + if err.args[0] in (ENOTCONN, EINVAL): + # To handle the case where we got an unconnected + # socket. + self.connected = False + else: + # The socket is broken in some unknown way, alert + # the user and remove it from the map (to prevent + # polling of broken sockets). + self.del_channel(map) + raise + else: + self.socket = None + + def __repr__(self): + status = [self.__class__.__module__+"."+self.__class__.__qualname__] + if self.accepting and self.addr: + status.append('listening') + elif self.connected: + status.append('connected') + if self.addr is not None: + try: + status.append('%s:%d' % self.addr) + except TypeError: + status.append(repr(self.addr)) + return '<%s at %#x>' % (' '.join(status), id(self)) + + __str__ = __repr__ + + def add_channel(self, map=None): + #self.log_info('adding channel %s' % self) + if map is None: + map = self._map + map[self._fileno] = self + + def del_channel(self, map=None): + fd = self._fileno + if map is None: + map = self._map + if fd in map: + #self.log_info('closing channel %d:%s' % (fd, self)) + del map[fd] + self._fileno = None + + def create_socket(self, family=socket.AF_INET, type=socket.SOCK_STREAM): + self.family_and_type = family, type + sock = socket.socket(family, type) + sock.setblocking(0) + self.set_socket(sock) + + def set_socket(self, sock, map=None): + self.socket = sock +## self.__dict__['socket'] = sock + self._fileno = sock.fileno() + self.add_channel(map) + + def set_reuse_addr(self): + # try to re-use a server port if possible + try: + self.socket.setsockopt( + socket.SOL_SOCKET, socket.SO_REUSEADDR, + self.socket.getsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR) | 1 + ) + except OSError: + pass + + # ================================================== + # predicates for select() + # these are used as filters for the lists of sockets + # to pass to select(). + # ================================================== + + def readable(self): + return True + + def writable(self): + return True + + # ================================================== + # socket object methods. + # ================================================== + + def listen(self, num): + self.accepting = True + if os.name == 'nt' and num > 5: + num = 5 + return self.socket.listen(num) + + def bind(self, addr): + self.addr = addr + return self.socket.bind(addr) + + def connect(self, address): + self.connected = False + self.connecting = True + err = self.socket.connect_ex(address) + if err in (EINPROGRESS, EALREADY, EWOULDBLOCK) \ + or err == EINVAL and os.name in ('nt', 'ce'): + self.addr = address + return + if err in (0, EISCONN): + self.addr = address + self.handle_connect_event() + else: + raise OSError(err, errorcode[err]) + + def accept(self): + # XXX can return either an address pair or None + try: + conn, addr = self.socket.accept() + except TypeError: + return None + except OSError as why: + if why.args[0] in (EWOULDBLOCK, ECONNABORTED, EAGAIN): + return None + else: + raise + else: + return conn, addr + + def send(self, data): + try: + result = self.socket.send(data) + return result + except OSError as why: + if why.args[0] == EWOULDBLOCK: + return 0 + elif why.args[0] in _DISCONNECTED: + self.handle_close() + return 0 + else: + raise + + def recv(self, buffer_size): + try: + data = self.socket.recv(buffer_size) + if not data: + # a closed connection is indicated by signaling + # a read condition, and having recv() return 0. + self.handle_close() + return b'' + else: + return data + except OSError as why: + # winsock sometimes raises ENOTCONN + if why.args[0] in _DISCONNECTED: + self.handle_close() + return b'' + else: + raise + + def close(self): + self.connected = False + self.accepting = False + self.connecting = False + self.del_channel() + if self.socket is not None: + try: + self.socket.close() + except OSError as why: + if why.args[0] not in (ENOTCONN, EBADF): + raise + + # log and log_info may be overridden to provide more sophisticated + # logging and warning methods. In general, log is for 'hit' logging + # and 'log_info' is for informational, warning and error logging. + + def log(self, message): + sys.stderr.write('log: %s\n' % str(message)) + + def log_info(self, message, type='info'): + if type not in self.ignore_log_types: + print('%s: %s' % (type, message)) + + def handle_read_event(self): + if self.accepting: + # accepting sockets are never connected, they "spawn" new + # sockets that are connected + self.handle_accept() + elif not self.connected: + if self.connecting: + self.handle_connect_event() + self.handle_read() + else: + self.handle_read() + + def handle_connect_event(self): + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + raise OSError(err, _strerror(err)) + self.handle_connect() + self.connected = True + self.connecting = False + + def handle_write_event(self): + if self.accepting: + # Accepting sockets shouldn't get a write event. + # We will pretend it didn't happen. + return + + if not self.connected: + if self.connecting: + self.handle_connect_event() + self.handle_write() + + def handle_expt_event(self): + # handle_expt_event() is called if there might be an error on the + # socket, or if there is OOB data + # check for the error condition first + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + # we can get here when select.select() says that there is an + # exceptional condition on the socket + # since there is an error, we'll go ahead and close the socket + # like we would in a subclassed handle_read() that received no + # data + self.handle_close() + else: + self.handle_expt() + + def handle_error(self): + nil, t, v, tbinfo = compact_traceback() + + # sometimes a user repr method will crash. + try: + self_repr = repr(self) + except: + self_repr = '<__repr__(self) failed for object at %0x>' % id(self) + + self.log_info( + 'uncaptured python exception, closing channel %s (%s:%s %s)' % ( + self_repr, + t, + v, + tbinfo + ), + 'error' + ) + self.handle_close() + + def handle_expt(self): + self.log_info('unhandled incoming priority event', 'warning') + + def handle_read(self): + self.log_info('unhandled read event', 'warning') + + def handle_write(self): + self.log_info('unhandled write event', 'warning') + + def handle_connect(self): + self.log_info('unhandled connect event', 'warning') + + def handle_accept(self): + pair = self.accept() + if pair is not None: + self.handle_accepted(*pair) + + def handle_accepted(self, sock, addr): + sock.close() + self.log_info('unhandled accepted event', 'warning') + + def handle_close(self): + self.log_info('unhandled close event', 'warning') + self.close() + +# --------------------------------------------------------------------------- +# adds simple buffered output capability, useful for simple clients. +# [for more sophisticated usage use asynchat.async_chat] +# --------------------------------------------------------------------------- + +class dispatcher_with_send(dispatcher): + + def __init__(self, sock=None, map=None): + dispatcher.__init__(self, sock, map) + self.out_buffer = b'' + + def initiate_send(self): + num_sent = 0 + num_sent = dispatcher.send(self, self.out_buffer[:65536]) + self.out_buffer = self.out_buffer[num_sent:] + + def handle_write(self): + self.initiate_send() + + def writable(self): + return (not self.connected) or len(self.out_buffer) + + def send(self, data): + if self.debug: + self.log_info('sending %s' % repr(data)) + self.out_buffer = self.out_buffer + data + self.initiate_send() + +# --------------------------------------------------------------------------- +# used for debugging. +# --------------------------------------------------------------------------- + +def compact_traceback(): + t, v, tb = sys.exc_info() + tbinfo = [] + if not tb: # Must have a traceback + raise AssertionError("traceback does not exist") + while tb: + tbinfo.append(( + tb.tb_frame.f_code.co_filename, + tb.tb_frame.f_code.co_name, + str(tb.tb_lineno) + )) + tb = tb.tb_next + + # just to be safe + del tb + + file, function, line = tbinfo[-1] + info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo]) + return (file, function, line), t, v, info + +def close_all(map=None, ignore_all=False): + if map is None: + map = socket_map + for x in list(map.values()): + try: + x.close() + except OSError as x: + if x.args[0] == EBADF: + pass + elif not ignore_all: + raise + except _reraised_exceptions: + raise + except: + if not ignore_all: + raise + map.clear() + +# Asynchronous File I/O: +# +# After a little research (reading man pages on various unixen, and +# digging through the linux kernel), I've determined that select() +# isn't meant for doing asynchronous file i/o. +# Heartening, though - reading linux/mm/filemap.c shows that linux +# supports asynchronous read-ahead. So _MOST_ of the time, the data +# will be sitting in memory for us already when we go to read it. +# +# What other OS's (besides NT) support async file i/o? [VMS?] +# +# Regardless, this is useful for pipes, and stdin/stdout... + +if os.name == 'posix': + class file_wrapper: + # Here we override just enough to make a file + # look like a socket for the purposes of asyncore. + # The passed fd is automatically os.dup()'d + + def __init__(self, fd): + self.fd = os.dup(fd) + + def __del__(self): + if self.fd >= 0: + warnings.warn("unclosed file %r" % self, ResourceWarning) + self.close() + + def recv(self, *args): + return os.read(self.fd, *args) + + def send(self, *args): + return os.write(self.fd, *args) + + def getsockopt(self, level, optname, buflen=None): + if (level == socket.SOL_SOCKET and + optname == socket.SO_ERROR and + not buflen): + return 0 + raise NotImplementedError("Only asyncore specific behaviour " + "implemented.") + + read = recv + write = send + + def close(self): + if self.fd < 0: + return + os.close(self.fd) + self.fd = -1 + + def fileno(self): + return self.fd + + class file_dispatcher(dispatcher): + + def __init__(self, fd, map=None): + dispatcher.__init__(self, None, map) + self.connected = True + try: + fd = fd.fileno() + except AttributeError: + pass + self.set_file(fd) + # set it to non-blocking mode + os.set_blocking(fd, False) + + def set_file(self, fd): + self.socket = file_wrapper(fd) + self._fileno = self.socket.fileno() + self.add_channel() diff --git a/ftp_server/cs.py b/ftp_server/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/ftp_server/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/ftp_server/ftp_server.py b/ftp_server/ftp_server.py new file mode 100644 index 00000000..c3a46a66 --- /dev/null +++ b/ftp_server/ftp_server.py @@ -0,0 +1,93 @@ +""" +This app will start an FTP server. This is done by using +pyftplib and also asynchat.py and asyncore.py. For detail +information about pyftplib, see https://pythonhosted.org/pyftpdlib/. +""" + +import sys +import argparse +import cs +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.servers import FTPServer + +APP_NAME = "ftp_server" + +# This requires a USB compatible storage device plugged into +# the router. It will mount to /var/media. +FTP_DIR = '/var/media' + + +def start_ftp_server(): + try: + authorizer = DummyAuthorizer() + # Define a new user having full r/w permissions and a read-only + # anonymous user + authorizer.add_user('user', '12345', FTP_DIR, perm='elradfmwM') + authorizer.add_anonymous(FTP_DIR) + + # Instantiate FTP handler class + handler = FTPHandler + handler.authorizer = authorizer + + # Define a customized banner (string returned when client connects) + handler.banner = "pyftpdlib based ftpd ready." + + # Instantiate FTP server class and listen on 0.0.0.0:2121. + # Application can only use ports higher that 1024 and the port + # will need to be allowed in the router firewall + address = ('', 2121) + server = FTPServer(address, handler) + + # set a limit for connections + server.max_cons = 256 + server.max_cons_per_ip = 5 + + # start ftp server + cs.CSClient().log(APP_NAME, 'Starting FTP server...') + server.serve_forever() + + except Exception as e: + cs.CSClient().log(APP_NAME, 'Something went wrong in start_ftp_server()! exception: {}'.format(e)) + raise + + return + + +def stop_router_app(): + """ + Perform any cleanup or other actions. + """ + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + # Call the function to start the app. + start_ftp_server() + + elif command == 'stop': + # Call the function to start the app. + stop_router_app() + + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + # The start.sh and stop.sh should call this script with a start or stop argument + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/ftp_server/install.sh b/ftp_server/install.sh new file mode 100644 index 00000000..3603d2f6 --- /dev/null +++ b/ftp_server/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION ftp_server on:" >> install.log +date >> install.log diff --git a/ftp_server/package.ini b/ftp_server/package.ini new file mode 100644 index 00000000..dfa1b966 --- /dev/null +++ b/ftp_server/package.ini @@ -0,0 +1,11 @@ +[ftp_server] +uuid=800a7994-87a5-41c2-b747-a614c0326b16 +vendor=Cradlepoint +notes=Starts and FTP server in the router +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/ftp_server/pyftpdlib/__init__.py b/ftp_server/pyftpdlib/__init__.py new file mode 100644 index 00000000..2bbfdbe6 --- /dev/null +++ b/ftp_server/pyftpdlib/__init__.py @@ -0,0 +1,73 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + + +""" +pyftpdlib: RFC-959 asynchronous FTP server. + +pyftpdlib implements a fully functioning asynchronous FTP server as +defined in RFC-959. A hierarchy of classes outlined below implement +the backend functionality for the FTPd: + + [pyftpdlib.ftpservers.FTPServer] + accepts connections and dispatches them to a handler + + [pyftpdlib.handlers.FTPHandler] + a class representing the server-protocol-interpreter + (server-PI, see RFC-959). Each time a new connection occurs + FTPServer will create a new FTPHandler instance to handle the + current PI session. + + [pyftpdlib.handlers.ActiveDTP] + [pyftpdlib.handlers.PassiveDTP] + base classes for active/passive-DTP backends. + + [pyftpdlib.handlers.DTPHandler] + this class handles processing of data transfer operations (server-DTP, + see RFC-959). + + [pyftpdlib.authorizers.DummyAuthorizer] + an "authorizer" is a class handling FTPd authentications and + permissions. It is used inside FTPHandler class to verify user + passwords, to get user's home directory and to get permissions + when a filesystem read/write occurs. "DummyAuthorizer" is the + base authorizer class providing a platform independent interface + for managing virtual users. + + [pyftpdlib.filesystems.AbstractedFS] + class used to interact with the file system, providing a high level, + cross-platform interface compatible with both Windows and UNIX style + filesystems. + +Usage example: + +>>> from pyftpdlib.authorizers import DummyAuthorizer +>>> from pyftpdlib.handlers import FTPHandler +>>> from pyftpdlib.servers import FTPServer +>>> +>>> authorizer = DummyAuthorizer() +>>> authorizer.add_user("user", "12345", "/home/giampaolo", perm="elradfmw") +>>> authorizer.add_anonymous("/home/nobody") +>>> +>>> handler = FTPHandler +>>> handler.authorizer = authorizer +>>> +>>> server = FTPServer(("127.0.0.1", 21), handler) +>>> server.serve_forever() +[I 13-02-19 10:55:42] >>> starting FTP server on 127.0.0.1:21 <<< +[I 13-02-19 10:55:42] poller: +[I 13-02-19 10:55:42] masquerade (NAT) address: None +[I 13-02-19 10:55:42] passive ports: None +[I 13-02-19 10:55:42] use sendfile(2): True +[I 13-02-19 10:55:45] 127.0.0.1:34178-[] FTP session opened (connect) +[I 13-02-19 10:55:48] 127.0.0.1:34178-[user] USER 'user' logged in. +[I 13-02-19 10:56:27] 127.0.0.1:34179-[user] RETR /home/giampaolo/.vimrc + completed=1 bytes=1700 seconds=0.001 +[I 13-02-19 10:56:39] 127.0.0.1:34179-[user] FTP session closed (disconnect). +""" + + +__ver__ = '1.5.2' +__author__ = "Giampaolo Rodola' " +__web__ = 'https://github.com/giampaolo/pyftpdlib/' diff --git a/ftp_server/pyftpdlib/__main__.py b/ftp_server/pyftpdlib/__main__.py new file mode 100644 index 00000000..bf82a90e --- /dev/null +++ b/ftp_server/pyftpdlib/__main__.py @@ -0,0 +1,100 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +""" +Start a stand alone anonymous FTP server from the command line as in: + +$ python -m pyftpdlib +""" + +import logging +import optparse +import os +import sys + +from . import __ver__ +from ._compat import getcwdu +from .authorizers import DummyAuthorizer +from .handlers import FTPHandler +from .log import config_logging +from .servers import FTPServer + + +class CustomizedOptionFormatter(optparse.IndentedHelpFormatter): + """Formats options shown in help in a prettier way.""" + + def format_option(self, option): + result = [] + opts = self.option_strings[option] + result.append(' %s\n' % opts) + if option.help: + help_text = ' %s\n\n' % self.expand_default(option) + result.append(help_text) + return ''.join(result) + + +def main(): + """Start a stand alone anonymous FTP server.""" + usage = "python -m pyftpdlib [options]" + parser = optparse.OptionParser(usage=usage, description=main.__doc__, + formatter=CustomizedOptionFormatter()) + parser.add_option('-i', '--interface', default=None, metavar="ADDRESS", + help="specify the interface to run on (default all " + "interfaces)") + parser.add_option('-p', '--port', type="int", default=2121, metavar="PORT", + help="specify port number to run on (default 2121)") + parser.add_option('-w', '--write', action="store_true", default=False, + help="grants write access for the anonymous user " + "(default read-only)") + parser.add_option('-d', '--directory', default=getcwdu(), metavar="FOLDER", + help="specify the directory to share (default current " + "directory)") + parser.add_option('-n', '--nat-address', default=None, metavar="ADDRESS", + help="the NAT address to use for passive connections") + parser.add_option('-r', '--range', default=None, metavar="FROM-TO", + help="the range of TCP ports to use for passive " + "connections (e.g. -r 8000-9000)") + parser.add_option('-D', '--debug', action='store_true', + help="enable DEBUG logging evel") + parser.add_option('-v', '--version', action='store_true', + help="print pyftpdlib version and exit") + + options, args = parser.parse_args() + if options.version: + sys.exit("pyftpdlib %s" % __ver__) + if options.debug: + config_logging(level=logging.DEBUG) + + passive_ports = None + if options.range: + try: + start, stop = options.range.split('-') + start = int(start) + stop = int(stop) + except ValueError: + parser.error('invalid argument passed to -r option') + else: + passive_ports = list(range(start, stop + 1)) + # On recent Windows versions, if address is not specified and IPv6 + # is installed the socket will listen on IPv6 by default; in this + # case we force IPv4 instead. + if os.name in ('nt', 'ce') and not options.interface: + options.interface = '0.0.0.0' + + authorizer = DummyAuthorizer() + perm = options.write and "elradfmwM" or "elr" + authorizer.add_anonymous(options.directory, perm=perm) + handler = FTPHandler + handler.authorizer = authorizer + handler.masquerade_address = options.nat_address + handler.passive_ports = passive_ports + ftpd = FTPServer((options.interface, options.port), FTPHandler) + try: + ftpd.serve_forever() + finally: + ftpd.close_all() + + +if __name__ == '__main__': + main() diff --git a/ftp_server/pyftpdlib/_compat.py b/ftp_server/pyftpdlib/_compat.py new file mode 100644 index 00000000..62a8bbb4 --- /dev/null +++ b/ftp_server/pyftpdlib/_compat.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +""" +Compatibility module similar to six which helps maintaining +a single code base working with python from 2.6 to 3.x. +""" + +import os +import sys + +PY3 = sys.version_info[0] == 3 + +if PY3: + def u(s): + return s + + def b(s): + return s.encode("latin-1") + + getcwdu = os.getcwd + unicode = str + xrange = range +else: + def u(s): + return unicode(s) + + def b(s): + return s + + getcwdu = os.getcwdu + unicode = unicode + xrange = xrange + + +# removed in 3.0, reintroduced in 3.2 +try: + callable = callable +except Exception: + def callable(obj): + for klass in type(obj).__mro__: + if "__call__" in klass.__dict__: + return True + return False diff --git a/ftp_server/pyftpdlib/authorizers.py b/ftp_server/pyftpdlib/authorizers.py new file mode 100644 index 00000000..b7e1d515 --- /dev/null +++ b/ftp_server/pyftpdlib/authorizers.py @@ -0,0 +1,880 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +"""An "authorizer" is a class handling authentications and permissions +of the FTP server. It is used by pyftpdlib.handlers.FTPHandler +class for: + +- verifying user password +- getting user home directory +- checking user permissions when a filesystem read/write event occurs +- changing user when accessing the filesystem + +DummyAuthorizer is the main class which handles virtual users. + +UnixAuthorizer and WindowsAuthorizer are platform specific and +interact with UNIX and Windows password database. +""" + + +import errno +import os +import sys +import warnings + +from ._compat import PY3 +from ._compat import unicode +from ._compat import getcwdu + + +__all__ = ['DummyAuthorizer', + # 'BaseUnixAuthorizer', 'UnixAuthorizer', + # 'BaseWindowsAuthorizer', 'WindowsAuthorizer', + ] + + +# =================================================================== +# --- exceptions +# =================================================================== + +class AuthorizerError(Exception): + """Base class for authorizer exceptions.""" + + +class AuthenticationFailed(Exception): + """Exception raised when authentication fails for any reason.""" + + +# =================================================================== +# --- base class +# =================================================================== + +class DummyAuthorizer(object): + """Basic "dummy" authorizer class, suitable for subclassing to + create your own custom authorizers. + + An "authorizer" is a class handling authentications and permissions + of the FTP server. It is used inside FTPHandler class for verifying + user's password, getting users home directory, checking user + permissions when a file read/write event occurs and changing user + before accessing the filesystem. + + DummyAuthorizer is the base authorizer, providing a platform + independent interface for managing "virtual" FTP users. System + dependent authorizers can by written by subclassing this base + class and overriding appropriate methods as necessary. + """ + + read_perms = "elr" + write_perms = "adfmwM" + + def __init__(self): + self.user_table = {} + + def add_user(self, username, password, homedir, perm='elr', + msg_login="Login successful.", msg_quit="Goodbye."): + """Add a user to the virtual users table. + + AuthorizerError exceptions raised on error conditions such as + invalid permissions, missing home directory or duplicate usernames. + + Optional perm argument is a string referencing the user's + permissions explained below: + + Read permissions: + - "e" = change directory (CWD command) + - "l" = list files (LIST, NLST, STAT, MLSD, MLST, SIZE, MDTM commands) + - "r" = retrieve file from the server (RETR command) + + Write permissions: + - "a" = append data to an existing file (APPE command) + - "d" = delete file or directory (DELE, RMD commands) + - "f" = rename file or directory (RNFR, RNTO commands) + - "m" = create directory (MKD command) + - "w" = store a file to the server (STOR, STOU commands) + - "M" = change file mode (SITE CHMOD command) + + Optional msg_login and msg_quit arguments can be specified to + provide customized response strings when user log-in and quit. + """ + if self.has_user(username): + raise ValueError('user %r already exists' % username) + if not isinstance(homedir, unicode): + homedir = homedir.decode('utf8') + if not os.path.isdir(homedir): + raise ValueError('no such directory: %r' % homedir) + homedir = os.path.realpath(homedir) + self._check_permissions(username, perm) + dic = {'pwd': str(password), + 'home': homedir, + 'perm': perm, + 'operms': {}, + 'msg_login': str(msg_login), + 'msg_quit': str(msg_quit) + } + self.user_table[username] = dic + + def add_anonymous(self, homedir, **kwargs): + """Add an anonymous user to the virtual users table. + + AuthorizerError exception raised on error conditions such as + invalid permissions, missing home directory, or duplicate + anonymous users. + + The keyword arguments in kwargs are the same expected by + add_user method: "perm", "msg_login" and "msg_quit". + + The optional "perm" keyword argument is a string defaulting to + "elr" referencing "read-only" anonymous user's permissions. + + Using write permission values ("adfmwM") results in a + RuntimeWarning. + """ + DummyAuthorizer.add_user(self, 'anonymous', '', homedir, **kwargs) + + def remove_user(self, username): + """Remove a user from the virtual users table.""" + del self.user_table[username] + + def override_perm(self, username, directory, perm, recursive=False): + """Override permissions for a given directory.""" + self._check_permissions(username, perm) + if not os.path.isdir(directory): + raise ValueError('no such directory: %r' % directory) + directory = os.path.normcase(os.path.realpath(directory)) + home = os.path.normcase(self.get_home_dir(username)) + if directory == home: + raise ValueError("can't override home directory permissions") + if not self._issubpath(directory, home): + raise ValueError("path escapes user home directory") + self.user_table[username]['operms'][directory] = perm, recursive + + def validate_authentication(self, username, password, handler): + """Raises AuthenticationFailed if supplied username and + password don't match the stored credentials, else return + None. + """ + msg = "Authentication failed." + if not self.has_user(username): + if username == 'anonymous': + msg = "Anonymous access not allowed." + raise AuthenticationFailed(msg) + if username != 'anonymous': + if self.user_table[username]['pwd'] != password: + raise AuthenticationFailed(msg) + + def get_home_dir(self, username): + """Return the user's home directory. + Since this is called during authentication (PASS), + AuthenticationFailed can be freely raised by subclasses in case + the provided username no longer exists. + """ + return self.user_table[username]['home'] + + def impersonate_user(self, username, password): + """Impersonate another user (noop). + + It is always called before accessing the filesystem. + By default it does nothing. The subclass overriding this + method is expected to provide a mechanism to change the + current user. + """ + + def terminate_impersonation(self, username): + """Terminate impersonation (noop). + + It is always called after having accessed the filesystem. + By default it does nothing. The subclass overriding this + method is expected to provide a mechanism to switch back + to the original user. + """ + + def has_user(self, username): + """Whether the username exists in the virtual users table.""" + return username in self.user_table + + def has_perm(self, username, perm, path=None): + """Whether the user has permission over path (an absolute + pathname of a file or a directory). + + Expected perm argument is one of the following letters: + "elradfmwM". + """ + if path is None: + return perm in self.user_table[username]['perm'] + + path = os.path.normcase(path) + for dir in self.user_table[username]['operms'].keys(): + operm, recursive = self.user_table[username]['operms'][dir] + if self._issubpath(path, dir): + if recursive: + return perm in operm + if (path == dir or os.path.dirname(path) == dir and not + os.path.isdir(path)): + return perm in operm + + return perm in self.user_table[username]['perm'] + + def get_perms(self, username): + """Return current user permissions.""" + return self.user_table[username]['perm'] + + def get_msg_login(self, username): + """Return the user's login message.""" + return self.user_table[username]['msg_login'] + + def get_msg_quit(self, username): + """Return the user's quitting message.""" + try: + return self.user_table[username]['msg_quit'] + except KeyError: + return "Goodbye." + + def _check_permissions(self, username, perm): + warned = 0 + for p in perm: + if p not in self.read_perms + self.write_perms: + raise ValueError('no such permission %r' % p) + if (username == 'anonymous' and + p in self.write_perms and not + warned): + warnings.warn("write permissions assigned to anonymous user.", + RuntimeWarning) + warned = 1 + + def _issubpath(self, a, b): + """Return True if a is a sub-path of b or if the paths are equal.""" + p1 = a.rstrip(os.sep).split(os.sep) + p2 = b.rstrip(os.sep).split(os.sep) + return p1[:len(p2)] == p2 + + +def replace_anonymous(callable): + """A decorator to replace anonymous user string passed to authorizer + methods as first argument with the actual user used to handle + anonymous sessions. + """ + + def wrapper(self, username, *args, **kwargs): + if username == 'anonymous': + username = self.anonymous_user or username + return callable(self, username, *args, **kwargs) + return wrapper + + +# =================================================================== +# --- platform specific authorizers +# =================================================================== + +class _Base(object): + """Methods common to both Unix and Windows authorizers. + Not supposed to be used directly. + """ + + msg_no_such_user = "Authentication failed." + msg_wrong_password = "Authentication failed." + msg_anon_not_allowed = "Anonymous access not allowed." + msg_invalid_shell = "User %s doesn't have a valid shell." + msg_rejected_user = "User %s is not allowed to login." + + def __init__(self): + """Check for errors in the constructor.""" + if self.rejected_users and self.allowed_users: + raise AuthorizerError("rejected_users and allowed_users options " + "are mutually exclusive") + + users = self._get_system_users() + for user in (self.allowed_users or self.rejected_users): + if user == 'anonymous': + raise AuthorizerError('invalid username "anonymous"') + if user not in users: + raise AuthorizerError('unknown user %s' % user) + + if self.anonymous_user is not None: + if not self.has_user(self.anonymous_user): + raise AuthorizerError('no such user %s' % self.anonymous_user) + home = self.get_home_dir(self.anonymous_user) + if not os.path.isdir(home): + raise AuthorizerError('no valid home set for user %s' + % self.anonymous_user) + + def override_user(self, username, password=None, homedir=None, perm=None, + msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + if (not password and not homedir and not perm and not msg_login and not + msg_quit): + raise AuthorizerError( + "at least one keyword argument must be specified") + if self.allowed_users and username not in self.allowed_users: + raise AuthorizerError('%s is not an allowed user' % username) + if self.rejected_users and username in self.rejected_users: + raise AuthorizerError('%s is not an allowed user' % username) + if username == "anonymous" and password: + raise AuthorizerError("can't assign password to anonymous user") + if not self.has_user(username): + raise AuthorizerError('no such user %s' % username) + if homedir is not None and not isinstance(homedir, unicode): + homedir = homedir.decode('utf8') + + if username in self._dummy_authorizer.user_table: + # re-set parameters + del self._dummy_authorizer.user_table[username] + self._dummy_authorizer.add_user(username, + password or "", + homedir or getcwdu(), + perm or "", + msg_login or "", + msg_quit or "") + if homedir is None: + self._dummy_authorizer.user_table[username]['home'] = "" + + def get_msg_login(self, username): + return self._get_key(username, 'msg_login') or self.msg_login + + def get_msg_quit(self, username): + return self._get_key(username, 'msg_quit') or self.msg_quit + + def get_perms(self, username): + overridden_perms = self._get_key(username, 'perm') + if overridden_perms: + return overridden_perms + if username == 'anonymous': + return 'elr' + return self.global_perm + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + def _get_key(self, username, key): + if self._dummy_authorizer.has_user(username): + return self._dummy_authorizer.user_table[username][key] + + def _is_rejected_user(self, username): + """Return True if the user has been black listed via + allowed_users or rejected_users options. + """ + if self.allowed_users and username not in self.allowed_users: + return True + if self.rejected_users and username in self.rejected_users: + return True + return False + + +# =================================================================== +# --- UNIX +# =================================================================== + +try: + import crypt + import pwd + import spwd +except ImportError: + pass +else: + __all__.extend(['BaseUnixAuthorizer', 'UnixAuthorizer']) + + # the uid/gid the server runs under + PROCESS_UID = os.getuid() + PROCESS_GID = os.getgid() + + class BaseUnixAuthorizer(object): + """An authorizer compatible with Unix user account and password + database. + This class should not be used directly unless for subclassing. + Use higher-level UnixAuthorizer class instead. + """ + + def __init__(self, anonymous_user=None): + if os.geteuid() != 0 or not spwd.getspall(): + raise AuthorizerError("super user privileges are required") + self.anonymous_user = anonymous_user + + if self.anonymous_user is not None: + try: + pwd.getpwnam(self.anonymous_user).pw_dir + except KeyError: + raise AuthorizerError('no such user %s' % anonymous_user) + + # --- overridden / private API + + def validate_authentication(self, username, password, handler): + """Authenticates against shadow password db; raises + AuthenticationFailed in case of failed authentication. + """ + if username == "anonymous": + if self.anonymous_user is None: + raise AuthenticationFailed(self.msg_anon_not_allowed) + else: + try: + pw1 = spwd.getspnam(username).sp_pwd + pw2 = crypt.crypt(password, pw1) + except KeyError: # no such username + raise AuthenticationFailed(self.msg_no_such_user) + else: + if pw1 != pw2: + raise AuthenticationFailed(self.msg_wrong_password) + + @replace_anonymous + def impersonate_user(self, username, password): + """Change process effective user/group ids to reflect + logged in user. + """ + try: + pwdstruct = pwd.getpwnam(username) + except KeyError: + raise AuthorizerError(self.msg_no_such_user) + else: + os.setegid(pwdstruct.pw_gid) + os.seteuid(pwdstruct.pw_uid) + + def terminate_impersonation(self, username): + """Revert process effective user/group IDs.""" + os.setegid(PROCESS_GID) + os.seteuid(PROCESS_UID) + + @replace_anonymous + def has_user(self, username): + """Return True if user exists on the Unix system. + If the user has been black listed via allowed_users or + rejected_users options always return False. + """ + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + """Return user home directory.""" + try: + home = pwd.getpwnam(username).pw_dir + except KeyError: + raise AuthorizerError(self.msg_no_such_user) + else: + if not PY3: + home = home.decode('utf8') + return home + + @staticmethod + def _get_system_users(): + """Return all users defined on the UNIX system.""" + # there should be no need to convert usernames to unicode + # as UNIX does not allow chars outside of ASCII set + return [entry.pw_name for entry in pwd.getpwall()] + + def get_msg_login(self, username): + return "Login successful." + + def get_msg_quit(self, username): + return "Goodbye." + + def get_perms(self, username): + return "elradfmw" + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + class UnixAuthorizer(_Base, BaseUnixAuthorizer): + """A wrapper on top of BaseUnixAuthorizer providing options + to specify what users should be allowed to login, per-user + options, etc. + + Example usages: + + >>> from pyftpdlib.authorizers import UnixAuthorizer + >>> # accept all except root + >>> auth = UnixAuthorizer(rejected_users=["root"]) + >>> + >>> # accept some users only + >>> auth = UnixAuthorizer(allowed_users=["matt", "jay"]) + >>> + >>> # accept everybody and don't care if they have not a valid shell + >>> auth = UnixAuthorizer(require_valid_shell=False) + >>> + >>> # set specific options for a user + >>> auth.override_user("matt", password="foo", perm="elr") + """ + + # --- public API + + def __init__(self, global_perm="elradfmw", + allowed_users=None, + rejected_users=None, + require_valid_shell=True, + anonymous_user=None, + msg_login="Login successful.", + msg_quit="Goodbye."): + """Parameters: + + - (string) global_perm: + a series of letters referencing the users permissions; + defaults to "elradfmw" which means full read and write + access for everybody (except anonymous). + + - (list) allowed_users: + a list of users which are accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (list) rejected_users: + a list of users which are not accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (bool) require_valid_shell: + Deny access for those users which do not have a valid shell + binary listed in /etc/shells. + If /etc/shells cannot be found this is a no-op. + Anonymous user is not subject to this option, and is free + to not have a valid shell defined. + Defaults to True (a valid shell is required for login). + + - (string) anonymous_user: + specify it if you intend to provide anonymous access. + The value expected is a string representing the system user + to use for managing anonymous sessions; defaults to None + (anonymous access disabled). + + - (string) msg_login: + the string sent when client logs in. + + - (string) msg_quit: + the string sent when client quits. + """ + BaseUnixAuthorizer.__init__(self, anonymous_user) + if allowed_users is None: + allowed_users = [] + if rejected_users is None: + rejected_users = [] + self.global_perm = global_perm + self.allowed_users = allowed_users + self.rejected_users = rejected_users + self.anonymous_user = anonymous_user + self.require_valid_shell = require_valid_shell + self.msg_login = msg_login + self.msg_quit = msg_quit + + self._dummy_authorizer = DummyAuthorizer() + self._dummy_authorizer._check_permissions('', global_perm) + _Base.__init__(self) + if require_valid_shell: + for username in self.allowed_users: + if not self._has_valid_shell(username): + raise AuthorizerError("user %s has not a valid shell" + % username) + + def override_user(self, username, password=None, homedir=None, + perm=None, msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + if self.require_valid_shell and username != 'anonymous': + if not self._has_valid_shell(username): + raise AuthorizerError(self.msg_invalid_shell % username) + _Base.override_user(self, username, password, homedir, perm, + msg_login, msg_quit) + + # --- overridden / private API + + def validate_authentication(self, username, password, handler): + if username == "anonymous": + if self.anonymous_user is None: + raise AuthenticationFailed(self.msg_anon_not_allowed) + return + if self._is_rejected_user(username): + raise AuthenticationFailed(self.msg_rejected_user % username) + overridden_password = self._get_key(username, 'pwd') + if overridden_password: + if overridden_password != password: + raise AuthenticationFailed(self.msg_wrong_password) + else: + BaseUnixAuthorizer.validate_authentication(self, username, + password, handler) + if self.require_valid_shell and username != 'anonymous': + if not self._has_valid_shell(username): + raise AuthenticationFailed( + self.msg_invalid_shell % username) + + @replace_anonymous + def has_user(self, username): + if self._is_rejected_user(username): + return False + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + overridden_home = self._get_key(username, 'home') + if overridden_home: + return overridden_home + return BaseUnixAuthorizer.get_home_dir(self, username) + + @staticmethod + def _has_valid_shell(username): + """Return True if the user has a valid shell binary listed + in /etc/shells. If /etc/shells can't be found return True. + """ + try: + file = open('/etc/shells', 'r') + except IOError as err: + if err.errno == errno.ENOENT: + return True + raise + else: + with file: + try: + shell = pwd.getpwnam(username).pw_shell + except KeyError: # invalid user + return False + for line in file: + if line.startswith('#'): + continue + line = line.strip() + if line == shell: + return True + return False + + +# =================================================================== +# --- Windows +# =================================================================== + +# Note: requires pywin32 extension +try: + import pywintypes + import win32api + import win32con + import win32net + import win32security +except ImportError: + pass +else: + if sys.version_info < (3, 0): + import _winreg as winreg + else: + import winreg + + __all__.extend(['BaseWindowsAuthorizer', 'WindowsAuthorizer']) + + class BaseWindowsAuthorizer(object): + """An authorizer compatible with Windows user account and + password database. + This class should not be used directly unless for subclassing. + Use higher-level WinowsAuthorizer class instead. + """ + + def __init__(self, anonymous_user=None, anonymous_password=None): + # actually try to impersonate the user + self.anonymous_user = anonymous_user + self.anonymous_password = anonymous_password + if self.anonymous_user is not None: + self.impersonate_user(self.anonymous_user, + self.anonymous_password) + self.terminate_impersonation(None) + + def validate_authentication(self, username, password, handler): + if username == "anonymous": + if self.anonymous_user is None: + raise AuthenticationFailed(self.msg_anon_not_allowed) + return + try: + win32security.LogonUser(username, None, password, + win32con.LOGON32_LOGON_INTERACTIVE, + win32con.LOGON32_PROVIDER_DEFAULT) + except pywintypes.error: + raise AuthenticationFailed(self.msg_wrong_password) + + @replace_anonymous + def impersonate_user(self, username, password): + """Impersonate the security context of another user.""" + handler = win32security.LogonUser( + username, None, password, + win32con.LOGON32_LOGON_INTERACTIVE, + win32con.LOGON32_PROVIDER_DEFAULT) + win32security.ImpersonateLoggedOnUser(handler) + handler.Close() + + def terminate_impersonation(self, username): + """Terminate the impersonation of another user.""" + win32security.RevertToSelf() + + @replace_anonymous + def has_user(self, username): + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + """Return the user's profile directory, the closest thing + to a user home directory we have on Windows. + """ + try: + sid = win32security.ConvertSidToStringSid( + win32security.LookupAccountName(None, username)[0]) + except pywintypes.error as err: + raise AuthorizerError(err) + path = r"SOFTWARE\Microsoft\Windows NT" \ + r"\CurrentVersion\ProfileList" + "\\" + sid + try: + key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path) + except WindowsError: + raise AuthorizerError( + "No profile directory defined for user %s" % username) + value = winreg.QueryValueEx(key, "ProfileImagePath")[0] + home = win32api.ExpandEnvironmentStrings(value) + if not PY3 and not isinstance(home, unicode): + home = home.decode('utf8') + return home + + @classmethod + def _get_system_users(cls): + """Return all users defined on the Windows system.""" + # XXX - Does Windows allow usernames with chars outside of + # ASCII set? In that case we need to convert this to unicode. + return [entry['name'] for entry in + win32net.NetUserEnum(None, 0)[0]] + + def get_msg_login(self, username): + return "Login successful." + + def get_msg_quit(self, username): + return "Goodbye." + + def get_perms(self, username): + return "elradfmw" + + def has_perm(self, username, perm, path=None): + return perm in self.get_perms(username) + + class WindowsAuthorizer(_Base, BaseWindowsAuthorizer): + """A wrapper on top of BaseWindowsAuthorizer providing options + to specify what users should be allowed to login, per-user + options, etc. + + Example usages: + + >>> from pyftpdlib.authorizers import WindowsAuthorizer + >>> # accept all except Administrator + >>> auth = WindowsAuthorizer(rejected_users=["Administrator"]) + >>> + >>> # accept some users only + >>> auth = WindowsAuthorizer(allowed_users=["matt", "jay"]) + >>> + >>> # set specific options for a user + >>> auth.override_user("matt", password="foo", perm="elr") + """ + + # --- public API + + def __init__(self, + global_perm="elradfmw", + allowed_users=None, + rejected_users=None, + anonymous_user=None, + anonymous_password=None, + msg_login="Login successful.", + msg_quit="Goodbye."): + """Parameters: + + - (string) global_perm: + a series of letters referencing the users permissions; + defaults to "elradfmw" which means full read and write + access for everybody (except anonymous). + + - (list) allowed_users: + a list of users which are accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (list) rejected_users: + a list of users which are not accepted for authenticating + against the FTP server; defaults to [] (no restrictions). + + - (string) anonymous_user: + specify it if you intend to provide anonymous access. + The value expected is a string representing the system user + to use for managing anonymous sessions. + As for IIS, it is recommended to use Guest account. + The common practice is to first enable the Guest user, which + is disabled by default and then assign an empty password. + Defaults to None (anonymous access disabled). + + - (string) anonymous_password: + the password of the user who has been chosen to manage the + anonymous sessions. Defaults to None (empty password). + + - (string) msg_login: + the string sent when client logs in. + + - (string) msg_quit: + the string sent when client quits. + """ + if allowed_users is None: + allowed_users = [] + if rejected_users is None: + rejected_users = [] + self.global_perm = global_perm + self.allowed_users = allowed_users + self.rejected_users = rejected_users + self.anonymous_user = anonymous_user + self.anonymous_password = anonymous_password + self.msg_login = msg_login + self.msg_quit = msg_quit + self._dummy_authorizer = DummyAuthorizer() + self._dummy_authorizer._check_permissions('', global_perm) + _Base.__init__(self) + # actually try to impersonate the user + if self.anonymous_user is not None: + self.impersonate_user(self.anonymous_user, + self.anonymous_password) + self.terminate_impersonation(None) + + def override_user(self, username, password=None, homedir=None, + perm=None, msg_login=None, msg_quit=None): + """Overrides the options specified in the class constructor + for a specific user. + """ + _Base.override_user(self, username, password, homedir, perm, + msg_login, msg_quit) + + # --- overridden / private API + + def validate_authentication(self, username, password, handler): + """Authenticates against Windows user database; return + True on success. + """ + if username == "anonymous": + if self.anonymous_user is None: + raise AuthenticationFailed(self.msg_anon_not_allowed) + return + if self.allowed_users and username not in self.allowed_users: + raise AuthenticationFailed(self.msg_rejected_user % username) + if self.rejected_users and username in self.rejected_users: + raise AuthenticationFailed(self.msg_rejected_user % username) + + overridden_password = self._get_key(username, 'pwd') + if overridden_password: + if overridden_password != password: + raise AuthenticationFailed(self.msg_wrong_password) + else: + BaseWindowsAuthorizer.validate_authentication( + self, username, password, handler) + + def impersonate_user(self, username, password): + """Impersonate the security context of another user.""" + if username == "anonymous": + username = self.anonymous_user or "" + password = self.anonymous_password or "" + BaseWindowsAuthorizer.impersonate_user(self, username, password) + + @replace_anonymous + def has_user(self, username): + if self._is_rejected_user(username): + return False + return username in self._get_system_users() + + @replace_anonymous + def get_home_dir(self, username): + overridden_home = self._get_key(username, 'home') + if overridden_home: + home = overridden_home + else: + home = BaseWindowsAuthorizer.get_home_dir(self, username) + if not PY3 and not isinstance(home, unicode): + home = home.decode('utf8') + return home diff --git a/ftp_server/pyftpdlib/filesystems.py b/ftp_server/pyftpdlib/filesystems.py new file mode 100644 index 00000000..03f22506 --- /dev/null +++ b/ftp_server/pyftpdlib/filesystems.py @@ -0,0 +1,635 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import os +import stat +import tempfile +import time +try: + from stat import filemode as _filemode # PY 3.3 +except ImportError: + from tarfile import filemode as _filemode +try: + import pwd + import grp +except ImportError: + pwd = grp = None +try: + from os import scandir # py 3.5 +except ImportError: + try: + from scandir import scandir # requires "pip install scandir" + except ImportError: + scandir = None + +from ._compat import PY3 +from ._compat import u +from ._compat import unicode + + +__all__ = ['FilesystemError', 'AbstractedFS'] + + +_months_map = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', + 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'} + + +# =================================================================== +# --- custom exceptions +# =================================================================== + +class FilesystemError(Exception): + """Custom class for filesystem-related exceptions. + You can raise this from an AbstractedFS subclass in order to + send a customized error string to the client. + """ + + +# =================================================================== +# --- base class +# =================================================================== + +class AbstractedFS(object): + """A class used to interact with the file system, providing a + cross-platform interface compatible with both Windows and + UNIX style filesystems where all paths use "/" separator. + + AbstractedFS distinguishes between "real" filesystem paths and + "virtual" ftp paths emulating a UNIX chroot jail where the user + can not escape its home directory (example: real "/home/user" + path will be seen as "/" by the client) + + It also provides some utility methods and wraps around all os.* + calls involving operations against the filesystem like creating + files or removing directories. + + FilesystemError exception can be raised from within any of + the methods below in order to send a customized error string + to the client. + """ + + def __init__(self, root, cmd_channel): + """ + - (str) root: the user "real" home directory (e.g. '/home/user') + - (instance) cmd_channel: the FTPHandler class instance + """ + assert isinstance(root, unicode) + # Set initial current working directory. + # By default initial cwd is set to "/" to emulate a chroot jail. + # If a different behavior is desired (e.g. initial cwd = root, + # to reflect the real filesystem) users overriding this class + # are responsible to set _cwd attribute as necessary. + self._cwd = u('/') + self._root = root + self.cmd_channel = cmd_channel + + @property + def root(self): + """The user home directory.""" + return self._root + + @property + def cwd(self): + """The user current working directory.""" + return self._cwd + + @root.setter + def root(self, path): + assert isinstance(path, unicode), path + self._root = path + + @cwd.setter + def cwd(self, path): + assert isinstance(path, unicode), path + self._cwd = path + + # --- Pathname / conversion utilities + + def ftpnorm(self, ftppath): + """Normalize a "virtual" ftp pathname (typically the raw string + coming from client) depending on the current working directory. + + Example (having "/foo" as current working directory): + >>> ftpnorm('bar') + '/foo/bar' + + Note: directory separators are system independent ("/"). + Pathname returned is always absolutized. + """ + assert isinstance(ftppath, unicode), ftppath + if os.path.isabs(ftppath): + p = os.path.normpath(ftppath) + else: + p = os.path.normpath(os.path.join(self.cwd, ftppath)) + # normalize string in a standard web-path notation having '/' + # as separator. + if os.sep == "\\": + p = p.replace("\\", "/") + # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we + # don't need them. In case we get an UNC path we collapse + # redundant separators appearing at the beginning of the string + while p[:2] == '//': + p = p[1:] + # Anti path traversal: don't trust user input, in the event + # that self.cwd is not absolute, return "/" as a safety measure. + # This is for extra protection, maybe not really necessary. + if not os.path.isabs(p): + p = u("/") + return p + + def ftp2fs(self, ftppath): + """Translate a "virtual" ftp pathname (typically the raw string + coming from client) into equivalent absolute "real" filesystem + pathname. + + Example (having "/home/user" as root directory): + >>> ftp2fs("foo") + '/home/user/foo' + + Note: directory separators are system dependent. + """ + assert isinstance(ftppath, unicode), ftppath + # as far as I know, it should always be path traversal safe... + if os.path.normpath(self.root) == os.sep: + return os.path.normpath(self.ftpnorm(ftppath)) + else: + p = self.ftpnorm(ftppath)[1:] + return os.path.normpath(os.path.join(self.root, p)) + + def fs2ftp(self, fspath): + """Translate a "real" filesystem pathname into equivalent + absolute "virtual" ftp pathname depending on the user's + root directory. + + Example (having "/home/user" as root directory): + >>> fs2ftp("/home/user/foo") + '/foo' + + As for ftpnorm, directory separators are system independent + ("/") and pathname returned is always absolutized. + + On invalid pathnames escaping from user's root directory + (e.g. "/home" when root is "/home/user") always return "/". + """ + assert isinstance(fspath, unicode), fspath + if os.path.isabs(fspath): + p = os.path.normpath(fspath) + else: + p = os.path.normpath(os.path.join(self.root, fspath)) + if not self.validpath(p): + return u('/') + p = p.replace(os.sep, "/") + p = p[len(self.root):] + if not p.startswith('/'): + p = '/' + p + return p + + def validpath(self, path): + """Check whether the path belongs to user's home directory. + Expected argument is a "real" filesystem pathname. + + If path is a symbolic link it is resolved to check its real + destination. + + Pathnames escaping from user's root directory are considered + not valid. + """ + assert isinstance(path, unicode), path + root = self.realpath(self.root) + path = self.realpath(path) + if not root.endswith(os.sep): + root = root + os.sep + if not path.endswith(os.sep): + path = path + os.sep + if path[0:len(root)] == root: + return True + return False + + # --- Wrapper methods around open() and tempfile.mkstemp + + def open(self, filename, mode): + """Open a file returning its handler.""" + assert isinstance(filename, unicode), filename + return open(filename, mode) + + def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'): + """A wrap around tempfile.mkstemp creating a file with a unique + name. Unlike mkstemp it returns an object with a file-like + interface. + """ + class FileWrapper: + + def __init__(self, fd, name): + self.file = fd + self.name = name + + def __getattr__(self, attr): + return getattr(self.file, attr) + + text = 'b' not in mode + # max number of tries to find out a unique file name + tempfile.TMP_MAX = 50 + fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text) + file = os.fdopen(fd, mode) + return FileWrapper(file, name) + + # --- Wrapper methods around os.* calls + + def chdir(self, path): + """Change the current directory.""" + # note: process cwd will be reset by the caller + assert isinstance(path, unicode), path + os.chdir(path) + self._cwd = self.fs2ftp(path) + + def mkdir(self, path): + """Create the specified directory.""" + assert isinstance(path, unicode), path + os.mkdir(path) + + def listdir(self, path): + """List the content of a directory.""" + assert isinstance(path, unicode), path + return os.listdir(path) + + def listdirinfo(self, path): + """List the content of a directory.""" + assert isinstance(path, unicode), path + return os.listdir(path) + + def rmdir(self, path): + """Remove the specified directory.""" + assert isinstance(path, unicode), path + os.rmdir(path) + + def remove(self, path): + """Remove the specified file.""" + assert isinstance(path, unicode), path + os.remove(path) + + def rename(self, src, dst): + """Rename the specified src file to the dst filename.""" + assert isinstance(src, unicode), src + assert isinstance(dst, unicode), dst + os.rename(src, dst) + + def chmod(self, path, mode): + """Change file/directory mode.""" + assert isinstance(path, unicode), path + if not hasattr(os, 'chmod'): + raise NotImplementedError + os.chmod(path, mode) + + def stat(self, path): + """Perform a stat() system call on the given path.""" + # on python 2 we might also get bytes from os.lisdir() + # assert isinstance(path, unicode), path + return os.stat(path) + + if hasattr(os, 'lstat'): + def lstat(self, path): + """Like stat but does not follow symbolic links.""" + # on python 2 we might also get bytes from os.lisdir() + # assert isinstance(path, unicode), path + return os.lstat(path) + else: + lstat = stat + + if hasattr(os, 'readlink'): + def readlink(self, path): + """Return a string representing the path to which a + symbolic link points. + """ + assert isinstance(path, unicode), path + return os.readlink(path) + + # --- Wrapper methods around os.path.* calls + + def isfile(self, path): + """Return True if path is a file.""" + assert isinstance(path, unicode), path + return os.path.isfile(path) + + def islink(self, path): + """Return True if path is a symbolic link.""" + assert isinstance(path, unicode), path + return os.path.islink(path) + + def isdir(self, path): + """Return True if path is a directory.""" + assert isinstance(path, unicode), path + return os.path.isdir(path) + + def getsize(self, path): + """Return the size of the specified file in bytes.""" + assert isinstance(path, unicode), path + return os.path.getsize(path) + + def getmtime(self, path): + """Return the last modified time as a number of seconds since + the epoch.""" + assert isinstance(path, unicode), path + return os.path.getmtime(path) + + def realpath(self, path): + """Return the canonical version of path eliminating any + symbolic links encountered in the path (if they are + supported by the operating system). + """ + assert isinstance(path, unicode), path + return os.path.realpath(path) + + def lexists(self, path): + """Return True if path refers to an existing path, including + a broken or circular symbolic link. + """ + assert isinstance(path, unicode), path + return os.path.lexists(path) + + if pwd is not None: + def get_user_by_uid(self, uid): + """Return the username associated with user id. + If this can't be determined return raw uid instead. + On Windows just return "owner". + """ + try: + return pwd.getpwuid(uid).pw_name + except KeyError: + return uid + else: + def get_user_by_uid(self, uid): + return "owner" + + if grp is not None: + def get_group_by_gid(self, gid): + """Return the groupname associated with group id. + If this can't be determined return raw gid instead. + On Windows just return "group". + """ + try: + return grp.getgrgid(gid).gr_name + except KeyError: + return gid + else: + def get_group_by_gid(self, gid): + return "group" + + # --- Listing utilities + + def format_list(self, basedir, listing, ignore_err=True): + """Return an iterator object that yields the entries of given + directory emulating the "/bin/ls -lA" UNIX command output. + + - (str) basedir: the absolute dirname. + - (list) listing: the names of the entries in basedir + - (bool) ignore_err: when False raise exception if os.lstat() + call fails. + + On platforms which do not support the pwd and grp modules (such + as Windows), ownership is printed as "owner" and "group" as a + default, and number of hard links is always "1". On UNIX + systems, the actual owner, group, and number of links are + printed. + + This is how output appears to client: + + -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3 + drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books + -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py + """ + assert isinstance(basedir, unicode), basedir + if self.cmd_channel.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + SIX_MONTHS = 180 * 24 * 60 * 60 + readlink = getattr(self, 'readlink', None) + now = time.time() + for basename in listing: + if not PY3: + try: + file = os.path.join(basedir, basename) + except UnicodeDecodeError: + # (Python 2 only) might happen on filesystem not + # supporting UTF8 meaning os.listdir() returned a list + # of mixed bytes and unicode strings: + # http://goo.gl/6DLHD + # http://bugs.python.org/issue683592 + file = os.path.join(bytes(basedir), bytes(basename)) + if not isinstance(basename, unicode): + basename = unicode(basename, 'utf8', 'ignore') + else: + file = os.path.join(basedir, basename) + try: + st = self.lstat(file) + except (OSError, FilesystemError): + if ignore_err: + continue + raise + + perms = _filemode(st.st_mode) # permissions + nlinks = st.st_nlink # number of links to inode + if not nlinks: # non-posix system, let's use a bogus value + nlinks = 1 + size = st.st_size # file size + uname = self.get_user_by_uid(st.st_uid) + gname = self.get_group_by_gid(st.st_gid) + mtime = timefunc(st.st_mtime) + # if modification time > 6 months shows "month year" + # else "month hh:mm"; this matches proftpd format, see: + # https://github.com/giampaolo/pyftpdlib/issues/187 + if (now - st.st_mtime) > SIX_MONTHS: + fmtstr = "%d %Y" + else: + fmtstr = "%d %H:%M" + try: + mtimestr = "%s %s" % (_months_map[mtime.tm_mon], + time.strftime(fmtstr, mtime)) + except ValueError: + # It could be raised if last mtime happens to be too + # old (prior to year 1900) in which case we return + # the current time as last mtime. + mtime = timefunc() + mtimestr = "%s %s" % (_months_map[mtime.tm_mon], + time.strftime("%d %H:%M", mtime)) + + # same as stat.S_ISLNK(st.st_mode) but slighlty faster + islink = (st.st_mode & 61440) == stat.S_IFLNK + if islink and readlink is not None: + # if the file is a symlink, resolve it, e.g. + # "symlink -> realfile" + try: + basename = basename + " -> " + readlink(file) + except (OSError, FilesystemError): + if not ignore_err: + raise + + # formatting is matched with proftpd ls output + line = "%s %3s %-8s %-8s %8s %s %s\r\n" % ( + perms, nlinks, uname, gname, size, mtimestr, basename) + yield line.encode('utf8', self.cmd_channel.unicode_errors) + + def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True): + """Return an iterator object that yields the entries of a given + directory or of a single file in a form suitable with MLSD and + MLST commands. + + Every entry includes a list of "facts" referring the listed + element. See RFC-3659, chapter 7, to see what every single + fact stands for. + + - (str) basedir: the absolute dirname. + - (list) listing: the names of the entries in basedir + - (str) perms: the string referencing the user permissions. + - (str) facts: the list of "facts" to be returned. + - (bool) ignore_err: when False raise exception if os.stat() + call fails. + + Note that "facts" returned may change depending on the platform + and on what user specified by using the OPTS command. + + This is how output could appear to the client issuing + a MLSD request: + + type=file;size=156;perm=r;modify=20071029155301;unique=8012; music.mp3 + type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks + type=file;size=211;perm=r;modify=20071103093626;unique=192; module.py + """ + assert isinstance(basedir, unicode), basedir + if self.cmd_channel.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + permdir = ''.join([x for x in perms if x not in 'arw']) + permfile = ''.join([x for x in perms if x not in 'celmp']) + if ('w' in perms) or ('a' in perms) or ('f' in perms): + permdir += 'c' + if 'd' in perms: + permdir += 'p' + show_type = 'type' in facts + show_perm = 'perm' in facts + show_size = 'size' in facts + show_modify = 'modify' in facts + show_create = 'create' in facts + show_mode = 'unix.mode' in facts + show_uid = 'unix.uid' in facts + show_gid = 'unix.gid' in facts + show_unique = 'unique' in facts + for basename in listing: + retfacts = dict() + if not PY3: + try: + file = os.path.join(basedir, basename) + except UnicodeDecodeError: + # (Python 2 only) might happen on filesystem not + # supporting UTF8 meaning os.listdir() returned a list + # of mixed bytes and unicode strings: + # http://goo.gl/6DLHD + # http://bugs.python.org/issue683592 + file = os.path.join(bytes(basedir), bytes(basename)) + if not isinstance(basename, unicode): + basename = unicode(basename, 'utf8', 'ignore') + else: + file = os.path.join(basedir, basename) + # in order to properly implement 'unique' fact (RFC-3659, + # chapter 7.5.2) we are supposed to follow symlinks, hence + # use os.stat() instead of os.lstat() + try: + st = self.stat(file) + except (OSError, FilesystemError): + if ignore_err: + continue + raise + # type + perm + # same as stat.S_ISDIR(st.st_mode) but slightly faster + isdir = (st.st_mode & 61440) == stat.S_IFDIR + if isdir: + if show_type: + if basename == '.': + retfacts['type'] = 'cdir' + elif basename == '..': + retfacts['type'] = 'pdir' + else: + retfacts['type'] = 'dir' + if show_perm: + retfacts['perm'] = permdir + else: + if show_type: + retfacts['type'] = 'file' + if show_perm: + retfacts['perm'] = permfile + if show_size: + retfacts['size'] = st.st_size # file size + # last modification time + if show_modify: + try: + retfacts['modify'] = time.strftime("%Y%m%d%H%M%S", + timefunc(st.st_mtime)) + # it could be raised if last mtime happens to be too old + # (prior to year 1900) + except ValueError: + pass + if show_create: + # on Windows we can provide also the creation time + try: + retfacts['create'] = time.strftime("%Y%m%d%H%M%S", + timefunc(st.st_ctime)) + except ValueError: + pass + # UNIX only + if show_mode: + retfacts['unix.mode'] = oct(st.st_mode & 511) + if show_uid: + retfacts['unix.uid'] = st.st_uid + if show_gid: + retfacts['unix.gid'] = st.st_gid + + # We provide unique fact (see RFC-3659, chapter 7.5.2) on + # posix platforms only; we get it by mixing st_dev and + # st_ino values which should be enough for granting an + # uniqueness for the file listed. + # The same approach is used by pure-ftpd. + # Implementors who want to provide unique fact on other + # platforms should use some platform-specific method (e.g. + # on Windows NTFS filesystems MTF records could be used). + if show_unique: + retfacts['unique'] = "%xg%x" % (st.st_dev, st.st_ino) + + # facts can be in any order but we sort them by name + factstring = "".join(["%s=%s;" % (x, retfacts[x]) + for x in sorted(retfacts.keys())]) + line = "%s %s\r\n" % (factstring, basename) + yield line.encode('utf8', self.cmd_channel.unicode_errors) + + +# =================================================================== +# --- platform specific implementation +# =================================================================== + +if os.name == 'posix': + __all__.append('UnixFilesystem') + + class UnixFilesystem(AbstractedFS): + """Represents the real UNIX filesystem. + + Differently from AbstractedFS the client will login into + /home/ and will be able to escape its home directory + and navigate the real filesystem. + """ + + def __init__(self, root, cmd_channel): + AbstractedFS.__init__(self, root, cmd_channel) + # initial cwd was set to "/" to emulate a chroot jail + self.cwd = root + + def ftp2fs(self, ftppath): + return self.ftpnorm(ftppath) + + def fs2ftp(self, fspath): + return fspath + + def validpath(self, path): + # validpath was used to check symlinks escaping user home + # directory; this is no longer necessary. + return True diff --git a/ftp_server/pyftpdlib/handlers.py b/ftp_server/pyftpdlib/handlers.py new file mode 100644 index 00000000..8c715d42 --- /dev/null +++ b/ftp_server/pyftpdlib/handlers.py @@ -0,0 +1,3552 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import asynchat +import contextlib +import errno +import glob +import logging +import os +import random +import socket +import sys +import time +import traceback +import warnings +try: + import pwd + import grp +except ImportError: + pwd = grp = None + +try: + from OpenSSL import SSL # requires "pip install pyopenssl" +except ImportError: + SSL = None + +try: + from collections import OrderedDict # python >= 2.7 +except ImportError: + OrderedDict = dict + +from . import __ver__ +from ._compat import b +from ._compat import getcwdu +from ._compat import PY3 +from ._compat import u +from ._compat import unicode +from ._compat import xrange +from .authorizers import AuthenticationFailed +from .authorizers import AuthorizerError +from .authorizers import DummyAuthorizer +from .filesystems import AbstractedFS +from .filesystems import FilesystemError +from .ioloop import _ERRNOS_DISCONNECTED +from .ioloop import _ERRNOS_RETRY +from .ioloop import Acceptor +from .ioloop import AsyncChat +from .ioloop import Connector +from .ioloop import RetryError +from .ioloop import timer +from .log import debug +from .log import logger + + +def _import_sendfile(): + # By default attempt to use os.sendfile introduced in Python 3.3: + # http://bugs.python.org/issue10882 + # ...otherwise fallback on using third-party pysendfile module: + # https://github.com/giampaolo/pysendfile/ + if os.name == 'posix': + try: + return os.sendfile # py >= 3.3 + except AttributeError: + try: + import sendfile as sf + # dirty hack to detect whether old 1.2.4 version is installed + if hasattr(sf, 'has_sf_hdtr'): + raise ImportError + return sf.sendfile + except ImportError: + pass + + +sendfile = _import_sendfile() + +proto_cmds = { + 'ABOR': dict( + perm=None, auth=True, arg=False, + help='Syntax: ABOR (abort transfer).'), + 'ALLO': dict( + perm=None, auth=True, arg=True, + help='Syntax: ALLO bytes (noop; allocate storage).'), + 'APPE': dict( + perm='a', auth=True, arg=True, + help='Syntax: APPE file-name (append data to file).'), + 'CDUP': dict( + perm='e', auth=True, arg=False, + help='Syntax: CDUP (go to parent directory).'), + 'CWD': dict( + perm='e', auth=True, arg=None, + help='Syntax: CWD [ dir-name] (change working directory).'), + 'DELE': dict( + perm='d', auth=True, arg=True, + help='Syntax: DELE file-name (delete file).'), + 'EPRT': dict( + perm=None, auth=True, arg=True, + help='Syntax: EPRT |proto|ip|port| (extended active mode).'), + 'EPSV': dict( + perm=None, auth=True, arg=None, + help='Syntax: EPSV [ proto/"ALL"] (extended passive mode).'), + 'FEAT': dict( + perm=None, auth=False, arg=False, + help='Syntax: FEAT (list all new features supported).'), + 'HELP': dict( + perm=None, auth=False, arg=None, + help='Syntax: HELP [ cmd] (show help).'), + 'LIST': dict( + perm='l', auth=True, arg=None, + help='Syntax: LIST [ path] (list files).'), + 'MDTM': dict( + perm='l', auth=True, arg=True, + help='Syntax: MDTM [ path] (file last modification time).'), + 'MLSD': dict( + perm='l', auth=True, arg=None, + help='Syntax: MLSD [ path] (list directory).'), + 'MLST': dict( + perm='l', auth=True, arg=None, + help='Syntax: MLST [ path] (show information about path).'), + 'MODE': dict( + perm=None, auth=True, arg=True, + help='Syntax: MODE mode (noop; set data transfer mode).'), + 'MKD': dict( + perm='m', auth=True, arg=True, + help='Syntax: MKD path (create directory).'), + 'NLST': dict( + perm='l', auth=True, arg=None, + help='Syntax: NLST [ path] (list path in a compact form).'), + 'NOOP': dict( + perm=None, auth=False, arg=False, + help='Syntax: NOOP (just do nothing).'), + 'OPTS': dict( + perm=None, auth=True, arg=True, + help='Syntax: OPTS cmd [ option] (set option for command).'), + 'PASS': dict( + perm=None, auth=False, arg=None, + help='Syntax: PASS [ password] (set user password).'), + 'PASV': dict( + perm=None, auth=True, arg=False, + help='Syntax: PASV (open passive data connection).'), + 'PORT': dict( + perm=None, auth=True, arg=True, + help='Syntax: PORT h,h,h,h,p,p (open active data connection).'), + 'PWD': dict( + perm=None, auth=True, arg=False, + help='Syntax: PWD (get current working directory).'), + 'QUIT': dict( + perm=None, auth=False, arg=False, + help='Syntax: QUIT (quit current session).'), + 'REIN': dict( + perm=None, auth=True, arg=False, + help='Syntax: REIN (flush account).'), + 'REST': dict( + perm=None, auth=True, arg=True, + help='Syntax: REST offset (set file offset).'), + 'RETR': dict( + perm='r', auth=True, arg=True, + help='Syntax: RETR file-name (retrieve a file).'), + 'RMD': dict( + perm='d', auth=True, arg=True, + help='Syntax: RMD dir-name (remove directory).'), + 'RNFR': dict( + perm='f', auth=True, arg=True, + help='Syntax: RNFR file-name (rename (source name)).'), + 'RNTO': dict( + perm='f', auth=True, arg=True, + help='Syntax: RNTO file-name (rename (destination name)).'), + 'SITE': dict( + perm=None, auth=False, arg=True, + help='Syntax: SITE site-command (execute SITE command).'), + 'SITE HELP': dict( + perm=None, auth=False, arg=None, + help='Syntax: SITE HELP [ cmd] (show SITE command help).'), + 'SITE CHMOD': dict( + perm='M', auth=True, arg=True, + help='Syntax: SITE CHMOD mode path (change file mode).'), + 'SIZE': dict( + perm='l', auth=True, arg=True, + help='Syntax: SIZE file-name (get file size).'), + 'STAT': dict( + perm='l', auth=False, arg=None, + help='Syntax: STAT [ path name] (server stats [list files]).'), + 'STOR': dict( + perm='w', auth=True, arg=True, + help='Syntax: STOR file-name (store a file).'), + 'STOU': dict( + perm='w', auth=True, arg=None, + help='Syntax: STOU [ name] (store a file with a unique name).'), + 'STRU': dict( + perm=None, auth=True, arg=True, + help='Syntax: STRU type (noop; set file structure).'), + 'SYST': dict( + perm=None, auth=False, arg=False, + help='Syntax: SYST (get operating system type).'), + 'TYPE': dict( + perm=None, auth=True, arg=True, + help='Syntax: TYPE [A | I] (set transfer type).'), + 'USER': dict( + perm=None, auth=False, arg=True, + help='Syntax: USER user-name (set username).'), + 'XCUP': dict( + perm='e', auth=True, arg=False, + help='Syntax: XCUP (obsolete; go to parent directory).'), + 'XCWD': dict( + perm='e', auth=True, arg=None, + help='Syntax: XCWD [ dir-name] (obsolete; change directory).'), + 'XMKD': dict( + perm='m', auth=True, arg=True, + help='Syntax: XMKD dir-name (obsolete; create directory).'), + 'XPWD': dict( + perm=None, auth=True, arg=False, + help='Syntax: XPWD (obsolete; get current dir).'), + 'XRMD': dict( + perm='d', auth=True, arg=True, + help='Syntax: XRMD dir-name (obsolete; remove directory).'), +} + +if not hasattr(os, 'chmod'): + del proto_cmds['SITE CHMOD'] + + +def _strerror(err): + if isinstance(err, EnvironmentError): + try: + return os.strerror(err.errno) + except AttributeError: + # not available on PythonCE + if not hasattr(os, 'strerror'): + return err.strerror + raise + else: + return str(err) + + +def _is_ssl_sock(sock): + return SSL is not None and isinstance(sock, SSL.Connection) + + +def _support_hybrid_ipv6(): + """Return True if it is possible to use hybrid IPv6/IPv4 sockets + on this platform. + """ + # Note: IPPROTO_IPV6 constant is broken on Windows, see: + # http://bugs.python.org/issue6926 + try: + if not socket.has_ipv6: + return False + with contextlib.closing(socket.socket(socket.AF_INET6)) as sock: + return not sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) + except (socket.error, AttributeError): + return False + + +SUPPORTS_HYBRID_IPV6 = _support_hybrid_ipv6() + + +class _FileReadWriteError(OSError): + """Exception raised when reading or writing a file during a transfer.""" + + +class _GiveUpOnSendfile(Exception): + """Exception raised in case use of sendfile() fails on first try, + in which case send() will be used. + """ + + +# --- DTP classes + +class PassiveDTP(Acceptor): + """Creates a socket listening on a local port, dispatching the + resultant connection to DTPHandler. Used for handling PASV command. + + - (int) timeout: the timeout for a remote client to establish + connection with the listening socket. Defaults to 30 seconds. + + - (int) backlog: the maximum number of queued connections passed + to listen(). If a connection request arrives when the queue is + full the client may raise ECONNRESET. Defaults to 5. + """ + timeout = 30 + backlog = None + + def __init__(self, cmd_channel, extmode=False): + """Initialize the passive data server. + + - (instance) cmd_channel: the command channel class instance. + - (bool) extmode: wheter use extended passive mode response type. + """ + self.cmd_channel = cmd_channel + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + Acceptor.__init__(self, ioloop=cmd_channel.ioloop) + + local_ip = self.cmd_channel.socket.getsockname()[0] + if local_ip in self.cmd_channel.masquerade_address_map: + masqueraded_ip = self.cmd_channel.masquerade_address_map[local_ip] + elif self.cmd_channel.masquerade_address: + masqueraded_ip = self.cmd_channel.masquerade_address + else: + masqueraded_ip = None + + if self.cmd_channel.server.socket.family != socket.AF_INET: + # dual stack IPv4/IPv6 support + af = self.bind_af_unspecified((local_ip, 0)) + self.socket.close() + else: + af = self.cmd_channel.socket.family + + self.create_socket(af, socket.SOCK_STREAM) + + if self.cmd_channel.passive_ports is None: + # By using 0 as port number value we let kernel choose a + # free unprivileged random port. + self.bind((local_ip, 0)) + else: + ports = list(self.cmd_channel.passive_ports) + while ports: + port = ports.pop(random.randint(0, len(ports) - 1)) + self.set_reuse_addr() + try: + self.bind((local_ip, port)) + except socket.error as err: + if err.errno == errno.EADDRINUSE: # port already in use + if ports: + continue + # If cannot use one of the ports in the configured + # range we'll use a kernel-assigned port, and log + # a message reporting the issue. + # By using 0 as port number value we let kernel + # choose a free unprivileged random port. + else: + self.bind((local_ip, 0)) + self.cmd_channel.log( + "Can't find a valid passive port in the " + "configured range. A random kernel-assigned " + "port will be used.", + logfun=logger.warning + ) + else: + raise + else: + break + self.listen(self.backlog or self.cmd_channel.server.backlog) + + port = self.socket.getsockname()[1] + if not extmode: + ip = masqueraded_ip or local_ip + if ip.startswith('::ffff:'): + # In this scenario, the server has an IPv6 socket, but + # the remote client is using IPv4 and its address is + # represented as an IPv4-mapped IPv6 address which + # looks like this ::ffff:151.12.5.65, see: + # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # We truncate the first bytes to make it look like a + # common IPv4 address. + ip = ip[7:] + # The format of 227 response in not standardized. + # This is the most expected: + resp = '227 Entering passive mode (%s,%d,%d).' % ( + ip.replace('.', ','), port // 256, port % 256) + self.cmd_channel.respond(resp) + else: + self.cmd_channel.respond('229 Entering extended passive mode ' + '(|||%d|).' % port) + if self.timeout: + self.call_later(self.timeout, self.handle_timeout) + + # --- connection / overridden + + def handle_accepted(self, sock, addr): + """Called when remote client initiates a connection.""" + if not self.cmd_channel.connected: + return self.close() + + # Check the origin of data connection. If not expressively + # configured we drop the incoming data connection if remote + # IP address does not match the client's IP address. + if self.cmd_channel.remote_ip != addr[0]: + if not self.cmd_channel.permit_foreign_addresses: + try: + sock.close() + except socket.error: + pass + msg = '425 Rejected data connection from foreign address ' \ + '%s:%s.' % (addr[0], addr[1]) + self.cmd_channel.respond_w_warning(msg) + # do not close listening socket: it couldn't be client's blame + return + else: + # site-to-site FTP allowed + msg = 'Established data connection with foreign address ' \ + '%s:%s.' % (addr[0], addr[1]) + self.cmd_channel.log(msg, logfun=logger.warning) + # Immediately close the current channel (we accept only one + # connection at time) and avoid running out of max connections + # limit. + self.close() + # delegate such connection to DTP handler + if self.cmd_channel.connected: + handler = self.cmd_channel.dtp_handler(sock, self.cmd_channel) + if handler.connected: + self.cmd_channel.data_channel = handler + self.cmd_channel._on_dtp_connection() + + def handle_timeout(self): + if self.cmd_channel.connected: + self.cmd_channel.respond("421 Passive data channel timed out.", + logfun=logging.info) + self.close() + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except Exception: + logger.error(traceback.format_exc()) + try: + self.close() + except Exception: + logger.critical(traceback.format_exc()) + + def close(self): + debug("call: close()", inst=self) + Acceptor.close(self) + + +class ActiveDTP(Connector): + """Connects to remote client and dispatches the resulting connection + to DTPHandler. Used for handling PORT command. + + - (int) timeout: the timeout for us to establish connection with + the client's listening data socket. + """ + timeout = 30 + + def __init__(self, ip, port, cmd_channel): + """Initialize the active data channel attemping to connect + to remote data socket. + + - (str) ip: the remote IP address. + - (int) port: the remote port. + - (instance) cmd_channel: the command channel class instance. + """ + Connector.__init__(self, ioloop=cmd_channel.ioloop) + self.cmd_channel = cmd_channel + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + self._idler = None + if self.timeout: + self._idler = self.ioloop.call_later(self.timeout, + self.handle_timeout, + _errback=self.handle_error) + + if ip.count('.') == 4: + self._cmd = "PORT" + self._normalized_addr = "%s:%s" % (ip, port) + else: + self._cmd = "EPRT" + self._normalized_addr = "[%s]:%s" % (ip, port) + + source_ip = self.cmd_channel.socket.getsockname()[0] + # dual stack IPv4/IPv6 support + try: + self.connect_af_unspecified((ip, port), (source_ip, 0)) + except (socket.gaierror, socket.error): + self.handle_close() + + def readable(self): + return False + + def handle_write(self): + # overridden to prevent unhandled read/write event messages to + # be printed by asyncore on Python < 2.6 + pass + + def handle_connect(self): + """Called when connection is established.""" + self.del_channel() + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + if not self.cmd_channel.connected: + return self.close() + # fix for asyncore on python < 2.6, meaning we aren't + # actually connected. + # test_active_conn_error tests this condition + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + raise socket.error(err) + # + msg = 'Active data connection established.' + self.cmd_channel.respond('200 ' + msg) + self.cmd_channel.log_cmd(self._cmd, self._normalized_addr, 200, msg) + # + if not self.cmd_channel.connected: + return self.close() + # delegate such connection to DTP handler + handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel) + self.cmd_channel.data_channel = handler + self.cmd_channel._on_dtp_connection() + + def handle_timeout(self): + if self.cmd_channel.connected: + msg = "Active data channel timed out." + self.cmd_channel.respond("421 " + msg, logfun=logger.info) + self.cmd_channel.log_cmd( + self._cmd, self._normalized_addr, 421, msg) + self.close() + + def handle_close(self): + # With the new IO loop, handle_close() gets called in case + # the fd appears in the list of exceptional fds. + # This means connect() failed. + if not self._closed: + self.close() + if self.cmd_channel.connected: + msg = "Can't connect to specified address." + self.cmd_channel.respond("425 " + msg) + self.cmd_channel.log_cmd( + self._cmd, self._normalized_addr, 425, msg) + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except (socket.gaierror, socket.error): + pass + except Exception: + self.log_exception(self) + try: + self.handle_close() + except Exception: + logger.critical(traceback.format_exc()) + + def close(self): + debug("call: close()", inst=self) + if not self._closed: + Connector.close(self) + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + +class DTPHandler(AsyncChat): + """Class handling server-data-transfer-process (server-DTP, see + RFC-959) managing data-transfer operations involving sending + and receiving data. + + Class attributes: + + - (int) timeout: the timeout which roughly is the maximum time we + permit data transfers to stall for with no progress. If the + timeout triggers, the remote client will be kicked off + (defaults 300). + + - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536) + + - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536) + """ + + timeout = 300 + ac_in_buffer_size = 65536 + ac_out_buffer_size = 65536 + + def __init__(self, sock, cmd_channel): + """Initialize the command channel. + + - (instance) sock: the socket object instance of the newly + established connection. + - (instance) cmd_channel: the command channel class instance. + """ + self.cmd_channel = cmd_channel + self.file_obj = None + self.receive = False + self.transfer_finished = False + self.tot_bytes_sent = 0 + self.tot_bytes_received = 0 + self.cmd = None + self.log = cmd_channel.log + self.log_exception = cmd_channel.log_exception + self._data_wrapper = None + self._lastdata = 0 + self._had_cr = False + self._start_time = timer() + self._resp = () + self._offset = None + self._filefd = None + self._idler = None + self._initialized = False + try: + AsyncChat.__init__(self, sock, ioloop=cmd_channel.ioloop) + except socket.error as err: + # if we get an exception here we want the dispatcher + # instance to set socket attribute before closing, see: + # https://github.com/giampaolo/pyftpdlib/issues/188 + AsyncChat.__init__( + self, socket.socket(), ioloop=cmd_channel.ioloop) + # https://github.com/giampaolo/pyftpdlib/issues/143 + self.close() + if err.errno == errno.EINVAL: + return + self.handle_error() + return + + # remove this instance from IOLoop's socket map + if not self.connected: + self.close() + return + if self.timeout: + self._idler = self.ioloop.call_every(self.timeout, + self.handle_timeout, + _errback=self.handle_error) + + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, + self.cmd_channel.get_repr_info(as_str=True)) + + __str__ = __repr__ + + def use_sendfile(self): + if not self.cmd_channel.use_sendfile: + # as per server config + return False + if self.file_obj is None or not hasattr(self.file_obj, "fileno"): + # direcotry listing or unusual file obj + return False + if self.cmd_channel._current_type != 'i': + # text file transfer (need to transform file content on the fly) + return False + return True + + def push(self, data): + self._initialized = True + self.modify_ioloop_events(self.ioloop.WRITE) + self._wanted_io_events = self.ioloop.WRITE + AsyncChat.push(self, data) + + def push_with_producer(self, producer): + self._initialized = True + self.modify_ioloop_events(self.ioloop.WRITE) + self._wanted_io_events = self.ioloop.WRITE + if self.use_sendfile(): + self._offset = producer.file.tell() + self._filefd = self.file_obj.fileno() + try: + self.initiate_sendfile() + except _GiveUpOnSendfile: + pass + else: + self.initiate_send = self.initiate_sendfile + return + debug("starting transfer using send()", self) + AsyncChat.push_with_producer(self, producer) + + def close_when_done(self): + asynchat.async_chat.close_when_done(self) + + def initiate_send(self): + asynchat.async_chat.initiate_send(self) + + def initiate_sendfile(self): + """A wrapper around sendfile.""" + try: + sent = sendfile(self._fileno, self._filefd, self._offset, + self.ac_out_buffer_size) + except OSError as err: + if err.errno in _ERRNOS_RETRY or err.errno == errno.EBUSY: + return + elif err.errno in _ERRNOS_DISCONNECTED: + self.handle_close() + else: + if self.tot_bytes_sent == 0: + logger.warning( + "sendfile() failed; falling back on using plain send") + raise _GiveUpOnSendfile + else: + raise + else: + if sent == 0: + # this signals the channel that the transfer is completed + self.discard_buffers() + self.handle_close() + else: + self._offset += sent + self.tot_bytes_sent += sent + + # --- utility methods + + def _posix_ascii_data_wrapper(self, chunk): + """The data wrapper used for receiving data in ASCII mode on + systems using a single line terminator, handling those cases + where CRLF ('\r\n') gets delivered in two chunks. + """ + if self._had_cr: + chunk = b'\r' + chunk + + if chunk.endswith(b'\r'): + self._had_cr = True + chunk = chunk[:-1] + else: + self._had_cr = False + + return chunk.replace(b'\r\n', b(os.linesep)) + + def enable_receiving(self, type, cmd): + """Enable receiving of data over the channel. Depending on the + TYPE currently in use it creates an appropriate wrapper for the + incoming data. + + - (str) type: current transfer type, 'a' (ASCII) or 'i' (binary). + """ + self._initialized = True + self.modify_ioloop_events(self.ioloop.READ) + self._wanted_io_events = self.ioloop.READ + self.cmd = cmd + if type == 'a': + if os.linesep == '\r\n': + self._data_wrapper = None + else: + self._data_wrapper = self._posix_ascii_data_wrapper + elif type == 'i': + self._data_wrapper = None + else: + raise TypeError("unsupported type") + self.receive = True + + def get_transmitted_bytes(self): + """Return the number of transmitted bytes.""" + return self.tot_bytes_sent + self.tot_bytes_received + + def get_elapsed_time(self): + """Return the transfer elapsed time in seconds.""" + return timer() - self._start_time + + def transfer_in_progress(self): + """Return True if a transfer is in progress, else False.""" + return self.get_transmitted_bytes() != 0 + + # --- connection + + def send(self, data): + result = AsyncChat.send(self, data) + self.tot_bytes_sent += result + return result + + def refill_buffer(self): # pragma: no cover + """Overridden as a fix around http://bugs.python.org/issue1740572 + (when the producer is consumed, close() was called instead of + handle_close()). + """ + while True: + if len(self.producer_fifo): + p = self.producer_fifo.first() + # a 'None' in the producer fifo is a sentinel, + # telling us to close the channel. + if p is None: + if not self.ac_out_buffer: + self.producer_fifo.pop() + # self.close() + self.handle_close() + return + elif isinstance(p, str): + self.producer_fifo.pop() + self.ac_out_buffer += p + return + data = p.more() + if data: + self.ac_out_buffer = self.ac_out_buffer + data + return + else: + self.producer_fifo.pop() + else: + return + + def handle_read(self): + """Called when there is data waiting to be read.""" + try: + chunk = self.recv(self.ac_in_buffer_size) + except RetryError: + pass + except socket.error: + self.handle_error() + else: + self.tot_bytes_received += len(chunk) + if not chunk: + self.transfer_finished = True + # self.close() # <-- asyncore.recv() already do that... + return + if self._data_wrapper is not None: + chunk = self._data_wrapper(chunk) + try: + self.file_obj.write(chunk) + except OSError as err: + raise _FileReadWriteError(err) + + handle_read_event = handle_read # small speedup + + def readable(self): + """Predicate for inclusion in the readable for select().""" + # It the channel is not supposed to be receiving but yet it's + # in the list of readable events, that means it has been + # disconnected, in which case we explicitly close() it. + # This is necessary as differently from FTPHandler this channel + # is not supposed to be readable/writable at first, meaning the + # upper IOLoop might end up calling readable() repeatedly, + # hogging CPU resources. + if not self.receive and not self._initialized: + return self.close() + return self.receive + + def writable(self): + """Predicate for inclusion in the writable for select().""" + return not self.receive and asynchat.async_chat.writable(self) + + def handle_timeout(self): + """Called cyclically to check if data trasfer is stalling with + no progress in which case the client is kicked off. + """ + if self.get_transmitted_bytes() > self._lastdata: + self._lastdata = self.get_transmitted_bytes() + else: + msg = "Data connection timed out." + self._resp = ("421 " + msg, logger.info) + self.close() + self.cmd_channel.close_when_done() + + def handle_error(self): + """Called when an exception is raised and not otherwise handled.""" + try: + raise + # an error could occur in case we fail reading / writing + # from / to file (e.g. file system gets full) + except _FileReadWriteError as err: + error = _strerror(err.errno) + except Exception: + # some other exception occurred; we don't want to provide + # confidential error messages + self.log_exception(self) + error = "Internal error" + try: + self._resp = ("426 %s; transfer aborted." % error, logger.warning) + self.close() + except Exception: + logger.critical(traceback.format_exc()) + + def handle_close(self): + """Called when the socket is closed.""" + # If we used channel for receiving we assume that transfer is + # finished when client closes the connection, if we used channel + # for sending we have to check that all data has been sent + # (responding with 226) or not (responding with 426). + # In both cases handle_close() is automatically called by the + # underlying asynchat module. + if not self._closed: + if self.receive: + self.transfer_finished = True + else: + self.transfer_finished = len(self.producer_fifo) == 0 + try: + if self.transfer_finished: + self._resp = ("226 Transfer complete.", logger.debug) + else: + tot_bytes = self.get_transmitted_bytes() + self._resp = ("426 Transfer aborted; %d bytes transmitted." + % tot_bytes, logger.debug) + finally: + self.close() + + def close(self): + """Close the data channel, first attempting to close any remaining + file handles.""" + debug("call: close()", inst=self) + if not self._closed: + # RFC-959 says we must close the connection before replying + AsyncChat.close(self) + if self._resp: + self.cmd_channel.respond(self._resp[0], logfun=self._resp[1]) + + if self.file_obj is not None and not self.file_obj.closed: + self.file_obj.close() + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + if self.file_obj is not None: + filename = self.file_obj.name + elapsed_time = round(self.get_elapsed_time(), 3) + self.cmd_channel.log_transfer( + cmd=self.cmd, + filename=self.file_obj.name, + receive=self.receive, + completed=self.transfer_finished, + elapsed=elapsed_time, + bytes=self.get_transmitted_bytes()) + if self.transfer_finished: + if self.receive: + self.cmd_channel.on_file_received(filename) + else: + self.cmd_channel.on_file_sent(filename) + else: + if self.receive: + self.cmd_channel.on_incomplete_file_received(filename) + else: + self.cmd_channel.on_incomplete_file_sent(filename) + self.cmd_channel._on_dtp_close() + + +# dirty hack in order to turn AsyncChat into a new style class in +# python 2.x so that we can use super() +if PY3: + class _AsyncChatNewStyle(AsyncChat): + pass +else: + class _AsyncChatNewStyle(object, AsyncChat): + + def __init__(self, *args, **kwargs): + super(object, self).__init__(*args, **kwargs) # bypass object + + +class ThrottledDTPHandler(_AsyncChatNewStyle, DTPHandler): + """A DTPHandler subclass which wraps sending and receiving in a data + counter and temporarily "sleeps" the channel so that you burst to no + more than x Kb/sec average. + + - (int) read_limit: the maximum number of bytes to read (receive) + in one second (defaults to 0 == no limit). + + - (int) write_limit: the maximum number of bytes to write (send) + in one second (defaults to 0 == no limit). + + - (bool) auto_sized_buffers: this option only applies when read + and/or write limits are specified. When enabled it bumps down + the data buffer sizes so that they are never greater than read + and write limits which results in a less bursty and smoother + throughput (default: True). + """ + read_limit = 0 + write_limit = 0 + auto_sized_buffers = True + + def __init__(self, sock, cmd_channel): + super(ThrottledDTPHandler, self).__init__(sock, cmd_channel) + self._timenext = 0 + self._datacount = 0 + self.sleeping = False + self._throttler = None + if self.auto_sized_buffers: + if self.read_limit: + while self.ac_in_buffer_size > self.read_limit: + self.ac_in_buffer_size /= 2 + if self.write_limit: + while self.ac_out_buffer_size > self.write_limit: + self.ac_out_buffer_size /= 2 + self.ac_in_buffer_size = int(self.ac_in_buffer_size) + self.ac_out_buffer_size = int(self.ac_out_buffer_size) + + def __repr__(self): + return DTPHandler.__repr__(self) + + def use_sendfile(self): + return False + + def recv(self, buffer_size): + chunk = super(ThrottledDTPHandler, self).recv(buffer_size) + if self.read_limit: + self._throttle_bandwidth(len(chunk), self.read_limit) + return chunk + + def send(self, data): + num_sent = super(ThrottledDTPHandler, self).send(data) + if self.write_limit: + self._throttle_bandwidth(num_sent, self.write_limit) + return num_sent + + def _cancel_throttler(self): + if self._throttler is not None and not self._throttler.cancelled: + self._throttler.cancel() + + def _throttle_bandwidth(self, len_chunk, max_speed): + """A method which counts data transmitted so that you burst to + no more than x Kb/sec average. + """ + self._datacount += len_chunk + if self._datacount >= max_speed: + self._datacount = 0 + now = timer() + sleepfor = (self._timenext - now) * 2 + if sleepfor > 0: + # we've passed bandwidth limits + def unsleep(): + if self.receive: + event = self.ioloop.READ + else: + event = self.ioloop.WRITE + self.add_channel(events=event) + + self.del_channel() + self._cancel_throttler() + self._throttler = self.ioloop.call_later( + sleepfor, unsleep, _errback=self.handle_error) + self._timenext = now + 1 + + def close(self): + self._cancel_throttler() + super(ThrottledDTPHandler, self).close() + + +# --- producers + + +class FileProducer(object): + """Producer wrapper for file[-like] objects.""" + + buffer_size = 65536 + + def __init__(self, file, type): + """Initialize the producer with a data_wrapper appropriate to TYPE. + + - (file) file: the file[-like] object. + - (str) type: the current TYPE, 'a' (ASCII) or 'i' (binary). + """ + self.file = file + self.type = type + if type == 'a' and os.linesep != '\r\n': + self._data_wrapper = lambda x: x.replace(b(os.linesep), b'\r\n') + else: + self._data_wrapper = None + + def more(self): + """Attempt a chunk of data of size self.buffer_size.""" + try: + data = self.file.read(self.buffer_size) + except OSError as err: + raise _FileReadWriteError(err) + else: + if self._data_wrapper is not None: + data = self._data_wrapper(data) + return data + + +class BufferedIteratorProducer(object): + """Producer for iterator objects with buffer capabilities.""" + # how many times iterator.next() will be called before + # returning some data + loops = 20 + + def __init__(self, iterator): + self.iterator = iterator + + def more(self): + """Attempt a chunk of data from iterator by calling + its next() method different times. + """ + buffer = [] + for x in xrange(self.loops): + try: + buffer.append(next(self.iterator)) + except StopIteration: + break + return b''.join(buffer) + + +# --- FTP + +class FTPHandler(AsyncChat): + """Implements the FTP server Protocol Interpreter (see RFC-959), + handling commands received from the client on the control channel. + + All relevant session information is stored in class attributes + reproduced below and can be modified before instantiating this + class. + + - (int) timeout: + The timeout which is the maximum time a remote client may spend + between FTP commands. If the timeout triggers, the remote client + will be kicked off. Defaults to 300 seconds. + + - (str) banner: the string sent when client connects. + + - (int) max_login_attempts: + the maximum number of wrong authentications before disconnecting + the client (default 3). + + - (bool)permit_foreign_addresses: + FTP site-to-site transfer feature: also referenced as "FXP" it + permits for transferring a file between two remote FTP servers + without the transfer going through the client's host (not + recommended for security reasons as described in RFC-2577). + Having this attribute set to False means that all data + connections from/to remote IP addresses which do not match the + client's IP address will be dropped (defualt False). + + - (bool) permit_privileged_ports: + set to True if you want to permit active data connections (PORT) + over privileged ports (not recommended, defaulting to False). + + - (str) masquerade_address: + the "masqueraded" IP address to provide along PASV reply when + pyftpdlib is running behind a NAT or other types of gateways. + When configured pyftpdlib will hide its local address and + instead use the public address of your NAT (default None). + + - (dict) masquerade_address_map: + in case the server has multiple IP addresses which are all + behind a NAT router, you may wish to specify individual + masquerade_addresses for each of them. The map expects a + dictionary containing private IP addresses as keys, and their + corresponding public (masquerade) addresses as values. + + - (list) passive_ports: + what ports the ftpd will use for its passive data transfers. + Value expected is a list of integers (e.g. range(60000, 65535)). + When configured pyftpdlib will no longer use kernel-assigned + random ports (default None). + + - (bool) use_gmt_times: + when True causes the server to report all ls and MDTM times in + GMT and not local time (default True). + + - (bool) use_sendfile: when True uses sendfile() system call to + send a file resulting in faster uploads (from server to client). + Works on UNIX only and requires pysendfile module to be + installed separately: + https://github.com/giampaolo/pysendfile/ + Automatically defaults to True if pysendfile module is + installed. + + - (bool) tcp_no_delay: controls the use of the TCP_NODELAY socket + option which disables the Nagle algorithm resulting in + significantly better performances (default True on all systems + where it is supported). + + - (str) unicode_errors: + the error handler passed to ''.encode() and ''.decode(): + http://docs.python.org/library/stdtypes.html#str.decode + (detaults to 'replace'). + + - (str) log_prefix: + the prefix string preceding any log line; all instance + attributes can be used as arguments. + + + All relevant instance attributes initialized when client connects + are reproduced below. You may be interested in them in case you + want to subclass the original FTPHandler. + + - (bool) authenticated: True if client authenticated himself. + - (str) username: the name of the connected user (if any). + - (int) attempted_logins: number of currently attempted logins. + - (str) current_type: the current transfer type (default "a") + - (int) af: the connection's address family (IPv4/IPv6) + - (instance) server: the FTPServer class instance. + - (instance) data_channel: the data channel instance (if any). + """ + # these are overridable defaults + + # default classes + authorizer = DummyAuthorizer() + active_dtp = ActiveDTP + passive_dtp = PassiveDTP + dtp_handler = DTPHandler + abstracted_fs = AbstractedFS + proto_cmds = proto_cmds + + # session attributes (explained in the docstring) + timeout = 300 + banner = "pyftpdlib %s ready." % __ver__ + max_login_attempts = 3 + permit_foreign_addresses = False + permit_privileged_ports = False + masquerade_address = None + masquerade_address_map = {} + passive_ports = None + use_gmt_times = True + use_sendfile = sendfile is not None + tcp_no_delay = hasattr(socket, "TCP_NODELAY") + unicode_errors = 'replace' + log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' + auth_failed_timeout = 3 + + def __init__(self, conn, server, ioloop=None): + """Initialize the command channel. + + - (instance) conn: the socket object instance of the newly + established connection. + - (instance) server: the ftp server class instance. + """ + # public session attributes + self.server = server + self.fs = None + self.authenticated = False + self.username = "" + self.password = "" + self.attempted_logins = 0 + self.data_channel = None + self.remote_ip = "" + self.remote_port = "" + self.started = time.time() + + # private session attributes + self._last_response = "" + self._current_type = 'a' + self._restart_position = 0 + self._quit_pending = False + self._in_buffer = [] + self._in_buffer_len = 0 + self._epsvall = False + self._dtp_acceptor = None + self._dtp_connector = None + self._in_dtp_queue = None + self._out_dtp_queue = None + self._extra_feats = [] + self._current_facts = ['type', 'perm', 'size', 'modify'] + self._rnfr = None + self._idler = None + self._log_debug = logging.getLogger('pyftpdlib').getEffectiveLevel() \ + <= logging.DEBUG + + if os.name == 'posix': + self._current_facts.append('unique') + self._available_facts = self._current_facts[:] + if pwd and grp: + self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid'] + if os.name == 'nt': + self._available_facts.append('create') + + try: + AsyncChat.__init__(self, conn, ioloop=ioloop) + except socket.error as err: + # if we get an exception here we want the dispatcher + # instance to set socket attribute before closing, see: + # https://github.com/giampaolo/pyftpdlib/issues/188 + AsyncChat.__init__(self, socket.socket(), ioloop=ioloop) + self.close() + debug("call: FTPHandler.__init__, err %r" % err, self) + if err.errno == errno.EINVAL: + # https://github.com/giampaolo/pyftpdlib/issues/143 + return + self.handle_error() + return + self.set_terminator(b"\r\n") + + # connection properties + try: + self.remote_ip, self.remote_port = self.socket.getpeername()[:2] + except socket.error as err: + debug("call: FTPHandler.__init__, err on getpeername() %r" % err, + self) + # A race condition may occur if the other end is closing + # before we can get the peername, hence ENOTCONN (see issue + # #100) while EINVAL can occur on OSX (see issue #143). + self.connected = False + if err.errno in (errno.ENOTCONN, errno.EINVAL): + self.close() + else: + self.handle_error() + return + else: + self.log("FTP session opened (connect)") + + # try to handle urgent data inline + try: + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1) + except socket.error as err: + debug("call: FTPHandler.__init__, err on SO_OOBINLINE %r" % err, + self) + + # disable Nagle algorithm for the control socket only, resulting + # in significantly better performances + if self.tcp_no_delay: + try: + self.socket.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + except socket.error as err: + debug( + "call: FTPHandler.__init__, err on TCP_NODELAY %r" % err, + self) + + # remove this instance from IOLoop's socket_map + if not self.connected: + self.close() + return + + if self.timeout: + self._idler = self.ioloop.call_later( + self.timeout, self.handle_timeout, _errback=self.handle_error) + + def get_repr_info(self, as_str=False, extra_info={}): + info = OrderedDict() + info['id'] = id(self) + info['addr'] = "%s:%s" % (self.remote_ip, self.remote_port) + if _is_ssl_sock(self.socket): + info['ssl'] = True + if self.username: + info['user'] = self.username + # If threads are involved sometimes "self" may be None (?!?). + dc = getattr(self, 'data_channel', None) + if dc is not None: + if _is_ssl_sock(dc.socket): + info['ssl-data'] = True + if dc.file_obj: + if self.data_channel.receive: + info['sending-file'] = dc.file_obj + if dc.use_sendfile(): + info['use-sendfile(2)'] = True + else: + info['receiving-file'] = dc.file_obj + info['bytes-trans'] = dc.get_transmitted_bytes() + info.update(extra_info) + if as_str: + return ', '.join(['%s=%r' % (k, v) for (k, v) in info.items()]) + return info + + def __repr__(self): + return '<%s(%s)>' % (self.__class__.__name__, self.get_repr_info(True)) + + __str__ = __repr__ + + def handle(self): + """Return a 220 'ready' response to the client over the command + channel. + """ + self.on_connect() + if not self._closed and not self._closing: + if len(self.banner) <= 75: + self.respond("220 %s" % str(self.banner)) + else: + self.push('220-%s\r\n' % str(self.banner)) + self.respond('220 ') + + def handle_max_cons(self): + """Called when limit for maximum number of connections is reached.""" + msg = "421 Too many connections. Service temporarily unavailable." + self.respond_w_warning(msg) + # If self.push is used, data could not be sent immediately in + # which case a new "loop" will occur exposing us to the risk of + # accepting new connections. Since this could cause asyncore to + # run out of fds in case we're using select() on Windows we + # immediately close the channel by using close() instead of + # close_when_done(). If data has not been sent yet client will + # be silently disconnected. + self.close() + + def handle_max_cons_per_ip(self): + """Called when too many clients are connected from the same IP.""" + msg = "421 Too many connections from the same IP address." + self.respond_w_warning(msg) + self.close_when_done() + + def handle_timeout(self): + """Called when client does not send any command within the time + specified in attribute.""" + msg = "Control connection timed out." + self.respond("421 " + msg, logfun=logger.info) + self.close_when_done() + + # --- asyncore / asynchat overridden methods + + def readable(self): + # Checking for self.connected seems to be necessary as per: + # https://github.com/giampaolo/pyftpdlib/issues/188#c18 + # In contrast to DTPHandler, here we are not interested in + # attempting to receive any further data from a closed socket. + return self.connected and AsyncChat.readable(self) + + def writable(self): + return self.connected and AsyncChat.writable(self) + + def collect_incoming_data(self, data): + """Read incoming data and append to the input buffer.""" + self._in_buffer.append(data) + self._in_buffer_len += len(data) + # Flush buffer if it gets too long (possible DoS attacks). + # RFC-959 specifies that a 500 response could be given in + # such cases + buflimit = 2048 + if self._in_buffer_len > buflimit: + self.respond_w_warning('500 Command too long.') + self._in_buffer = [] + self._in_buffer_len = 0 + + def decode(self, bytes): + return bytes.decode('utf8', self.unicode_errors) + + def found_terminator(self): + r"""Called when the incoming data stream matches the \r\n + terminator. + """ + if self._idler is not None and not self._idler.cancelled: + self._idler.reset() + + line = b''.join(self._in_buffer) + try: + line = self.decode(line) + except UnicodeDecodeError: + # By default we'll never get here as we replace errors + # but user might want to override this behavior. + # RFC-2640 doesn't mention what to do in this case so + # we'll just return 501 (bad arg). + return self.respond("501 Can't decode command.") + + self._in_buffer = [] + self._in_buffer_len = 0 + + cmd = line.split(' ')[0].upper() + arg = line[len(cmd) + 1:] + try: + self.pre_process_command(line, cmd, arg) + except UnicodeEncodeError: + self.respond("501 can't decode path (server filesystem encoding " + "is %s)" % sys.getfilesystemencoding()) + + def pre_process_command(self, line, cmd, arg): + kwargs = {} + if cmd == "SITE" and arg: + cmd = "SITE %s" % arg.split(' ')[0].upper() + arg = line[len(cmd) + 1:] + + if cmd != 'PASS': + self.logline("<- %s" % line) + else: + self.logline("<- %s %s" % (line.split(' ')[0], '*' * 6)) + + # Recognize those commands having a "special semantic". They + # should be sent by following the RFC-959 procedure of sending + # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but + # since many ftp clients don't do it correctly we check the + # last 4 characters only. + if cmd not in self.proto_cmds: + if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'): + cmd = cmd[-4:] + else: + msg = 'Command "%s" not understood.' % cmd + self.respond('500 ' + msg) + if cmd: + self.log_cmd(cmd, arg, 500, msg) + return + + if not arg and self.proto_cmds[cmd]['arg'] == True: # NOQA + msg = "Syntax error: command needs an argument." + self.respond("501 " + msg) + self.log_cmd(cmd, "", 501, msg) + return + if arg and self.proto_cmds[cmd]['arg'] == False: # NOQA + msg = "Syntax error: command does not accept arguments." + self.respond("501 " + msg) + self.log_cmd(cmd, arg, 501, msg) + return + + if not self.authenticated: + if self.proto_cmds[cmd]['auth'] or (cmd == 'STAT' and arg): + msg = "Log in with USER and PASS first." + self.respond("530 " + msg) + self.log_cmd(cmd, arg, 530, msg) + else: + # call the proper ftp_* method + self.process_command(cmd, arg) + return + else: + if (cmd == 'STAT') and not arg: + self.ftp_STAT(u('')) + return + + # for file-system related commands check whether real path + # destination is valid + if self.proto_cmds[cmd]['perm'] and (cmd != 'STOU'): + if cmd in ('CWD', 'XCWD'): + arg = self.fs.ftp2fs(arg or u('/')) + elif cmd in ('CDUP', 'XCUP'): + arg = self.fs.ftp2fs(u('..')) + elif cmd == 'LIST': + if arg.lower() in ('-a', '-l', '-al', '-la'): + arg = self.fs.ftp2fs(self.fs.cwd) + else: + arg = self.fs.ftp2fs(arg or self.fs.cwd) + elif cmd == 'STAT': + if glob.has_magic(arg): + msg = 'Globbing not supported.' + self.respond('550 ' + msg) + self.log_cmd(cmd, arg, 550, msg) + return + arg = self.fs.ftp2fs(arg or self.fs.cwd) + elif cmd == 'SITE CHMOD': + if ' ' not in arg: + msg = "Syntax error: command needs two arguments." + self.respond("501 " + msg) + self.log_cmd(cmd, "", 501, msg) + return + else: + mode, arg = arg.split(' ', 1) + arg = self.fs.ftp2fs(arg) + kwargs = dict(mode=mode) + else: # LIST, NLST, MLSD, MLST + arg = self.fs.ftp2fs(arg or self.fs.cwd) + + if not self.fs.validpath(arg): + line = self.fs.fs2ftp(arg) + msg = '"%s" points to a path which is outside ' \ + "the user's root directory" % line + self.respond("550 %s." % msg) + self.log_cmd(cmd, arg, 550, msg) + return + + # check permission + perm = self.proto_cmds[cmd]['perm'] + if perm is not None and cmd != 'STOU': + if not self.authorizer.has_perm(self.username, perm, arg): + msg = "Not enough privileges." + self.respond("550 " + msg) + self.log_cmd(cmd, arg, 550, msg) + return + + # call the proper ftp_* method + self.process_command(cmd, arg, **kwargs) + + def process_command(self, cmd, *args, **kwargs): + """Process command by calling the corresponding ftp_* class + method (e.g. for received command "MKD pathname", ftp_MKD() + method is called with "pathname" as the argument). + """ + if self._closed: + return + self._last_response = "" + method = getattr(self, 'ftp_' + cmd.replace(' ', '_')) + method(*args, **kwargs) + if self._last_response: + code = int(self._last_response[:3]) + resp = self._last_response[4:] + self.log_cmd(cmd, args[0], code, resp) + + def handle_error(self): + try: + self.log_exception(self) + self.close() + except Exception: + logger.critical(traceback.format_exc()) + + def handle_close(self): + self.close() + + def close(self): + """Close the current channel disconnecting the client.""" + debug("call: close()", inst=self) + if not self._closed: + AsyncChat.close(self) + + self._shutdown_connecting_dtp() + + if self.data_channel is not None: + self.data_channel.close() + del self.data_channel + + if self._out_dtp_queue is not None: + file = self._out_dtp_queue[2] + if file is not None: + file.close() + if self._in_dtp_queue is not None: + file = self._in_dtp_queue[0] + if file is not None: + file.close() + + del self._out_dtp_queue + del self._in_dtp_queue + + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + # remove client IP address from ip map + if self.remote_ip in self.server.ip_map: + self.server.ip_map.remove(self.remote_ip) + + if self.fs is not None: + self.fs.cmd_channel = None + self.fs = None + self.log("FTP session closed (disconnect).") + # Having self.remote_ip not set means that no connection + # actually took place, hence we're not interested in + # invoking the callback. + if self.remote_ip: + self.ioloop.call_later(0, self.on_disconnect, + _errback=self.handle_error) + + def _shutdown_connecting_dtp(self): + """Close any ActiveDTP or PassiveDTP instance waiting to + establish a connection (passive or active). + """ + if self._dtp_acceptor is not None: + self._dtp_acceptor.close() + self._dtp_acceptor = None + if self._dtp_connector is not None: + self._dtp_connector.close() + self._dtp_connector = None + + # --- public callbacks + # Note: to run a time consuming task make sure to use a separate + # process or thread (see FAQs). + + def on_connect(self): + """Called when client connects, *before* sending the initial + 220 reply. + """ + + def on_disconnect(self): + """Called when connection is closed.""" + + def on_login(self, username): + """Called on user login.""" + + def on_login_failed(self, username, password): + """Called on failed login attempt. + At this point client might have already been disconnected if it + failed too many times. + """ + + def on_logout(self, username): + """Called when user "cleanly" logs out due to QUIT or USER + issued twice (re-login). This is not called if the connection + is simply closed by client. + """ + + def on_file_sent(self, file): + """Called every time a file has been succesfully sent. + "file" is the absolute name of the file just being sent. + """ + + def on_file_received(self, file): + """Called every time a file has been succesfully received. + "file" is the absolute name of the file just being received. + """ + + def on_incomplete_file_sent(self, file): + """Called every time a file has not been entirely sent. + (e.g. ABOR during transfer or client disconnected). + "file" is the absolute name of that file. + """ + + def on_incomplete_file_received(self, file): + """Called every time a file has not been entirely received + (e.g. ABOR during transfer or client disconnected). + "file" is the absolute name of that file. + """ + + # --- internal callbacks + + def _on_dtp_connection(self): + """Called every time data channel connects, either active or + passive. + + Incoming and outgoing queues are checked for pending data. + If outbound data is pending, it is pushed into the data channel. + If awaiting inbound data, the data channel is enabled for + receiving. + """ + # Close accepting DTP only. By closing ActiveDTP DTPHandler + # would receive a closed socket object. + # self._shutdown_connecting_dtp() + if self._dtp_acceptor is not None: + self._dtp_acceptor.close() + self._dtp_acceptor = None + + # stop the idle timer as long as the data transfer is not finished + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + + # check for data to send + if self._out_dtp_queue is not None: + data, isproducer, file, cmd = self._out_dtp_queue + self._out_dtp_queue = None + self.data_channel.cmd = cmd + if file: + self.data_channel.file_obj = file + try: + if not isproducer: + self.data_channel.push(data) + else: + self.data_channel.push_with_producer(data) + if self.data_channel is not None: + self.data_channel.close_when_done() + except Exception: + # dealing with this exception is up to DTP (see bug #84) + self.data_channel.handle_error() + + # check for data to receive + elif self._in_dtp_queue is not None: + file, cmd = self._in_dtp_queue + self.data_channel.file_obj = file + self._in_dtp_queue = None + self.data_channel.enable_receiving(self._current_type, cmd) + + def _on_dtp_close(self): + """Called every time the data channel is closed.""" + self.data_channel = None + if self._quit_pending: + self.close() + elif self.timeout: + # data transfer finished, restart the idle timer + if self._idler is not None and not self._idler.cancelled: + self._idler.cancel() + self._idler = self.ioloop.call_later( + self.timeout, self.handle_timeout, _errback=self.handle_error) + + # --- utility + + def push(self, s): + asynchat.async_chat.push(self, s.encode('utf8')) + + def respond(self, resp, logfun=logger.debug): + """Send a response to the client using the command channel.""" + self._last_response = resp + self.push(resp + '\r\n') + if self._log_debug: + self.logline('-> %s' % resp, logfun=logfun) + else: + self.log(resp[4:], logfun=logfun) + + def respond_w_warning(self, resp): + self.respond(resp, logfun=logger.warning) + + def push_dtp_data(self, data, isproducer=False, file=None, cmd=None): + """Pushes data into the data channel. + + It is usually called for those commands requiring some data to + be sent over the data channel (e.g. RETR). + If data channel does not exist yet, it queues the data to send + later; data will then be pushed into data channel when + _on_dtp_connection() will be called. + + - (str/classobj) data: the data to send which may be a string + or a producer object). + - (bool) isproducer: whether treat data as a producer. + - (file) file: the file[-like] object to send (if any). + """ + if self.data_channel is not None: + self.respond( + "125 Data connection already open. Transfer starting.") + if file: + self.data_channel.file_obj = file + try: + if not isproducer: + self.data_channel.push(data) + else: + self.data_channel.push_with_producer(data) + if self.data_channel is not None: + self.data_channel.cmd = cmd + self.data_channel.close_when_done() + except Exception: + # dealing with this exception is up to DTP (see bug #84) + self.data_channel.handle_error() + else: + self.respond( + "150 File status okay. About to open data connection.") + self._out_dtp_queue = (data, isproducer, file, cmd) + + def flush_account(self): + """Flush account information by clearing attributes that need + to be reset on a REIN or new USER command. + """ + self._shutdown_connecting_dtp() + # if there's a transfer in progress RFC-959 states we are + # supposed to let it finish + if self.data_channel is not None: + if not self.data_channel.transfer_in_progress(): + self.data_channel.close() + self.data_channel = None + + username = self.username + if self.authenticated and username: + self.on_logout(username) + self.authenticated = False + self.username = "" + self.password = "" + self.attempted_logins = 0 + self._current_type = 'a' + self._restart_position = 0 + self._quit_pending = False + self._in_dtp_queue = None + self._rnfr = None + self._out_dtp_queue = None + + def run_as_current_user(self, function, *args, **kwargs): + """Execute a function impersonating the current logged-in user.""" + self.authorizer.impersonate_user(self.username, self.password) + try: + return function(*args, **kwargs) + finally: + self.authorizer.terminate_impersonation(self.username) + + # --- logging wrappers + + # this is defined earlier + # log_prefix = '%(remote_ip)s:%(remote_port)s-[%(username)s]' + + def log(self, msg, logfun=logger.info): + """Log a message, including additional identifying session data.""" + prefix = self.log_prefix % self.__dict__ + logfun("%s %s" % (prefix, msg)) + + def logline(self, msg, logfun=logger.debug): + """Log a line including additional indentifying session data. + By default this is disabled unless logging level == DEBUG. + """ + if self._log_debug: + prefix = self.log_prefix % self.__dict__ + logfun("%s %s" % (prefix, msg)) + + def logerror(self, msg): + """Log an error including additional indentifying session data.""" + prefix = self.log_prefix % self.__dict__ + logger.error("%s %s" % (prefix, msg)) + + def log_exception(self, instance): + """Log an unhandled exception. 'instance' is the instance + where the exception was generated. + """ + logger.exception("unhandled exception in instance %r", instance) + + # the list of commands which gets logged when logging level + # is >= logging.INFO + log_cmds_list = ["DELE", "RNFR", "RNTO", "MKD", "RMD", "CWD", + "XMKD", "XRMD", "XCWD", + "REIN", "SITE CHMOD"] + + def log_cmd(self, cmd, arg, respcode, respstr): + """Log commands and responses in a standardized format. + This is disabled in case the logging level is set to DEBUG. + + - (str) cmd: + the command sent by client + + - (str) arg: + the command argument sent by client. + For filesystem commands such as DELE, MKD, etc. this is + already represented as an absolute real filesystem path + like "/home/user/file.ext". + + - (int) respcode: + the response code as being sent by server. Response codes + starting with 4xx or 5xx are returned if the command has + been rejected for some reason. + + - (str) respstr: + the response string as being sent by server. + + By default only DELE, RMD, RNTO, MKD, CWD, ABOR, REIN, SITE CHMOD + commands are logged and the output is redirected to self.log + method. + + Can be overridden to provide alternate formats or to log + further commands. + """ + if not self._log_debug and cmd in self.log_cmds_list: + line = '%s %s' % (' '.join([cmd, arg]).strip(), respcode) + if str(respcode)[0] in ('4', '5'): + line += ' %r' % respstr + self.log(line) + + def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes): + """Log all file transfers in a standardized format. + + - (str) cmd: + the original command who caused the tranfer. + + - (str) filename: + the absolutized name of the file on disk. + + - (bool) receive: + True if the transfer was used for client uploading (STOR, + STOU, APPE), False otherwise (RETR). + + - (bool) completed: + True if the file has been entirely sent, else False. + + - (float) elapsed: + transfer elapsed time in seconds. + + - (int) bytes: + number of bytes transmitted. + """ + line = '%s %s completed=%s bytes=%s seconds=%s' % \ + (cmd, filename, completed and 1 or 0, bytes, elapsed) + self.log(line) + + # --- connection + def _make_eport(self, ip, port): + """Establish an active data channel with remote client which + issued a PORT or EPRT command. + """ + # FTP bounce attacks protection: according to RFC-2577 it's + # recommended to reject PORT if IP address specified in it + # does not match client IP address. + remote_ip = self.remote_ip + if remote_ip.startswith('::ffff:'): + # In this scenario, the server has an IPv6 socket, but + # the remote client is using IPv4 and its address is + # represented as an IPv4-mapped IPv6 address which + # looks like this ::ffff:151.12.5.65, see: + # http://en.wikipedia.org/wiki/IPv6#IPv4-mapped_addresses + # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # We truncate the first bytes to make it look like a + # common IPv4 address. + remote_ip = remote_ip[7:] + if not self.permit_foreign_addresses and ip != remote_ip: + msg = "501 Rejected data connection to foreign address %s:%s." \ + % (ip, port) + self.respond_w_warning(msg) + return + + # ...another RFC-2577 recommendation is rejecting connections + # to privileged ports (< 1024) for security reasons. + if not self.permit_privileged_ports and port < 1024: + msg = '501 PORT against the privileged port "%s" refused.' % port + self.respond_w_warning(msg) + return + + # close establishing DTP instances, if any + self._shutdown_connecting_dtp() + + if self.data_channel is not None: + self.data_channel.close() + self.data_channel = None + + # make sure we are not hitting the max connections limit + if not self.server._accept_new_cons(): + msg = "425 Too many connections. Can't open data channel." + self.respond_w_warning(msg) + return + + # open data channel + self._dtp_connector = self.active_dtp(ip, port, self) + + def _make_epasv(self, extmode=False): + """Initialize a passive data channel with remote client which + issued a PASV or EPSV command. + If extmode argument is True we assume that client issued EPSV in + which case extended passive mode will be used (see RFC-2428). + """ + # close establishing DTP instances, if any + self._shutdown_connecting_dtp() + + # close established data connections, if any + if self.data_channel is not None: + self.data_channel.close() + self.data_channel = None + + # make sure we are not hitting the max connections limit + if not self.server._accept_new_cons(): + msg = "425 Too many connections. Can't open data channel." + self.respond_w_warning(msg) + return + + # open data channel + self._dtp_acceptor = self.passive_dtp(self, extmode) + + def ftp_PORT(self, line): + """Start an active data channel by using IPv4.""" + if self._epsvall: + self.respond("501 PORT not allowed after EPSV ALL.") + return + # Parse PORT request for getting IP and PORT. + # Request comes in as: + # > h1,h2,h3,h4,p1,p2 + # ...where the client's IP address is h1.h2.h3.h4 and the TCP + # port number is (p1 * 256) + p2. + try: + addr = list(map(int, line.split(','))) + if len(addr) != 6: + raise ValueError + for x in addr[:4]: + if not 0 <= x <= 255: + raise ValueError + ip = '%d.%d.%d.%d' % tuple(addr[:4]) + port = (addr[4] * 256) + addr[5] + if not 0 <= port <= 65535: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid PORT format.") + return + self._make_eport(ip, port) + + def ftp_EPRT(self, line): + """Start an active data channel by choosing the network protocol + to use (IPv4/IPv6) as defined in RFC-2428. + """ + if self._epsvall: + self.respond("501 EPRT not allowed after EPSV ALL.") + return + # Parse EPRT request for getting protocol, IP and PORT. + # Request comes in as: + # protoipport + # ...where is an arbitrary delimiter character (usually "|") and + # is the network protocol to use (1 for IPv4, 2 for IPv6). + try: + af, ip, port = line.split(line[0])[1:-1] + port = int(port) + if not 0 <= port <= 65535: + raise ValueError + except (ValueError, IndexError, OverflowError): + self.respond("501 Invalid EPRT format.") + return + + if af == "1": + # test if AF_INET6 and IPV6_V6ONLY + if (self.socket.family == socket.AF_INET6 and not + SUPPORTS_HYBRID_IPV6): + self.respond('522 Network protocol not supported (use 2).') + else: + try: + octs = list(map(int, ip.split('.'))) + if len(octs) != 4: + raise ValueError + for x in octs: + if not 0 <= x <= 255: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid EPRT format.") + else: + self._make_eport(ip, port) + elif af == "2": + if self.socket.family == socket.AF_INET: + self.respond('522 Network protocol not supported (use 1).') + else: + self._make_eport(ip, port) + else: + if self.socket.family == socket.AF_INET: + self.respond('501 Unknown network protocol (use 1).') + else: + self.respond('501 Unknown network protocol (use 2).') + + def ftp_PASV(self, line): + """Start a passive data channel by using IPv4.""" + if self._epsvall: + self.respond("501 PASV not allowed after EPSV ALL.") + return + self._make_epasv(extmode=False) + + def ftp_EPSV(self, line): + """Start a passive data channel by using IPv4 or IPv6 as defined + in RFC-2428. + """ + # RFC-2428 specifies that if an optional parameter is given, + # we have to determine the address family from that otherwise + # use the same address family used on the control connection. + # In such a scenario a client may use IPv4 on the control channel + # and choose to use IPv6 for the data channel. + # But how could we use IPv6 on the data channel without knowing + # which IPv6 address to use for binding the socket? + # Unfortunately RFC-2428 does not provide satisfing information + # on how to do that. The assumption is that we don't have any way + # to know wich address to use, hence we just use the same address + # family used on the control connection. + if not line: + self._make_epasv(extmode=True) + # IPv4 + elif line == "1": + if self.socket.family != socket.AF_INET: + self.respond('522 Network protocol not supported (use 2).') + else: + self._make_epasv(extmode=True) + # IPv6 + elif line == "2": + if self.socket.family == socket.AF_INET: + self.respond('522 Network protocol not supported (use 1).') + else: + self._make_epasv(extmode=True) + elif line.lower() == 'all': + self._epsvall = True + self.respond( + '220 Other commands other than EPSV are now disabled.') + else: + if self.socket.family == socket.AF_INET: + self.respond('501 Unknown network protocol (use 1).') + else: + self.respond('501 Unknown network protocol (use 2).') + + def ftp_QUIT(self, line): + """Quit the current session disconnecting the client.""" + if self.authenticated: + msg_quit = self.authorizer.get_msg_quit(self.username) + else: + msg_quit = "Goodbye." + if len(msg_quit) <= 75: + self.respond("221 %s" % msg_quit) + else: + self.push("221-%s\r\n" % msg_quit) + self.respond("221 ") + + # From RFC-959: + # If file transfer is in progress, the connection must remain + # open for result response and the server will then close it. + # We also stop responding to any further command. + if self.data_channel: + self._quit_pending = True + self.del_channel() + else: + self._shutdown_connecting_dtp() + self.close_when_done() + if self.authenticated and self.username: + self.on_logout(self.username) + + # --- data transferring + + def ftp_LIST(self, path): + """Return a list of files in the specified directory to the + client. + On success return the directory path, else None. + """ + # - If no argument, fall back on cwd as default. + # - Some older FTP clients erroneously issue /bin/ls-like LIST + # formats in which case we fall back on cwd as default. + try: + isdir = self.fs.isdir(path) + if isdir: + listing = self.run_as_current_user(self.fs.listdir, path) + if isinstance(listing, list): + try: + # RFC 959 recommends the listing to be sorted. + listing.sort() + except UnicodeDecodeError: + # (Python 2 only) might happen on filesystem not + # supporting UTF8 meaning os.listdir() returned a list + # of mixed bytes and unicode strings: + # http://goo.gl/6DLHD + # http://bugs.python.org/issue683592 + pass + iterator = self.fs.format_list(path, listing) + else: + basedir, filename = os.path.split(path) + self.fs.lstat(path) # raise exc in case of problems + iterator = self.fs.format_list(basedir, [filename]) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + producer = BufferedIteratorProducer(iterator) + self.push_dtp_data(producer, isproducer=True, cmd="LIST") + return path + + def ftp_NLST(self, path): + """Return a list of files in the specified directory in a + compact form to the client. + On success return the directory path, else None. + """ + try: + if self.fs.isdir(path): + listing = list(self.run_as_current_user(self.fs.listdir, path)) + else: + # if path is a file we just list its name + self.fs.lstat(path) # raise exc in case of problems + listing = [os.path.basename(path)] + except (OSError, FilesystemError) as err: + self.respond('550 %s.' % _strerror(err)) + else: + data = '' + if listing: + try: + listing.sort() + except UnicodeDecodeError: + # (Python 2 only) might happen on filesystem not + # supporting UTF8 meaning os.listdir() returned a list + # of mixed bytes and unicode strings: + # http://goo.gl/6DLHD + # http://bugs.python.org/issue683592 + ls = [] + for x in listing: + if not isinstance(x, unicode): + x = unicode(x, 'utf8') + ls.append(x) + listing = sorted(ls) + data = '\r\n'.join(listing) + '\r\n' + data = data.encode('utf8', self.unicode_errors) + self.push_dtp_data(data, cmd="NLST") + return path + + # --- MLST and MLSD commands + + # The MLST and MLSD commands are intended to standardize the file and + # directory information returned by the server-FTP process. These + # commands differ from the LIST command in that the format of the + # replies is strictly defined although extensible. + + def ftp_MLST(self, path): + """Return information about a pathname in a machine-processable + form as defined in RFC-3659. + On success return the path just listed, else None. + """ + line = self.fs.fs2ftp(path) + basedir, basename = os.path.split(path) + perms = self.authorizer.get_perms(self.username) + try: + iterator = self.run_as_current_user( + self.fs.format_mlsx, basedir, [basename], perms, + self._current_facts, ignore_err=False) + data = b''.join(iterator) + except (OSError, FilesystemError) as err: + self.respond('550 %s.' % _strerror(err)) + else: + data = data.decode('utf8', self.unicode_errors) + # since TVFS is supported (see RFC-3659 chapter 6), a fully + # qualified pathname should be returned + data = data.split(' ')[0] + ' %s\r\n' % line + # response is expected on the command channel + self.push('250-Listing "%s":\r\n' % line) + # the fact set must be preceded by a space + self.push(' ' + data) + self.respond('250 End MLST.') + return path + + def ftp_MLSD(self, path): + """Return contents of a directory in a machine-processable form + as defined in RFC-3659. + On success return the path just listed, else None. + """ + # RFC-3659 requires 501 response code if path is not a directory + if not self.fs.isdir(path): + self.respond("501 No such directory.") + return + try: + listing = self.run_as_current_user(self.fs.listdir, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + perms = self.authorizer.get_perms(self.username) + iterator = self.fs.format_mlsx(path, listing, perms, + self._current_facts) + producer = BufferedIteratorProducer(iterator) + self.push_dtp_data(producer, isproducer=True, cmd="MLSD") + return path + + def ftp_RETR(self, file): + """Retrieve the specified file (transfer from the server to the + client). On success return the file path else None. + """ + rest_pos = self._restart_position + self._restart_position = 0 + try: + fd = self.run_as_current_user(self.fs.open, file, 'rb') + except (EnvironmentError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + return + + try: + if rest_pos: + # Make sure that the requested offset is valid (within the + # size of the file being resumed). + # According to RFC-1123 a 554 reply may result in case that + # the existing file cannot be repositioned as specified in + # the REST. + ok = 0 + try: + if rest_pos > self.fs.getsize(file): + raise ValueError + fd.seek(rest_pos) + ok = 1 + except ValueError: + why = "Invalid REST parameter" + except (EnvironmentError, FilesystemError) as err: + why = _strerror(err) + if not ok: + fd.close() + self.respond('554 %s' % why) + return + producer = FileProducer(fd, self._current_type) + self.push_dtp_data(producer, isproducer=True, file=fd, cmd="RETR") + return file + except Exception: + fd.close() + raise + + def ftp_STOR(self, file, mode='w'): + """Store a file (transfer from the client to the server). + On success return the file path, else None. + """ + # A resume could occur in case of APPE or REST commands. + # In that case we have to open file object in different ways: + # STOR: mode = 'w' + # APPE: mode = 'a' + # REST: mode = 'r+' (to permit seeking on file object) + if 'a' in mode: + cmd = 'APPE' + else: + cmd = 'STOR' + rest_pos = self._restart_position + self._restart_position = 0 + if rest_pos: + mode = 'r+' + try: + fd = self.run_as_current_user(self.fs.open, file, mode + 'b') + except (EnvironmentError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + return + + try: + if rest_pos: + # Make sure that the requested offset is valid (within the + # size of the file being resumed). + # According to RFC-1123 a 554 reply may result in case + # that the existing file cannot be repositioned as + # specified in the REST. + ok = 0 + try: + if rest_pos > self.fs.getsize(file): + raise ValueError + fd.seek(rest_pos) + ok = 1 + except ValueError: + why = "Invalid REST parameter" + except (EnvironmentError, FilesystemError) as err: + why = _strerror(err) + if not ok: + fd.close() + self.respond('554 %s' % why) + return + + if self.data_channel is not None: + resp = "Data connection already open. Transfer starting." + self.respond("125 " + resp) + self.data_channel.file_obj = fd + self.data_channel.enable_receiving(self._current_type, cmd) + else: + resp = "File status okay. About to open data connection." + self.respond("150 " + resp) + self._in_dtp_queue = (fd, cmd) + return file + except Exception: + fd.close() + raise + + def ftp_STOU(self, line): + """Store a file on the server with a unique name. + On success return the file path, else None. + """ + # Note 1: RFC-959 prohibited STOU parameters, but this + # prohibition is obsolete. + # Note 2: 250 response wanted by RFC-959 has been declared + # incorrect in RFC-1123 that wants 125/150 instead. + # Note 3: RFC-1123 also provided an exact output format + # defined to be as follow: + # > 125 FILE: pppp + # ...where pppp represents the unique path name of the + # file that will be written. + + # watch for STOU preceded by REST, which makes no sense. + if self._restart_position: + self.respond("450 Can't STOU while REST request is pending.") + return + + if line: + basedir, prefix = os.path.split(self.fs.ftp2fs(line)) + prefix = prefix + '.' + else: + basedir = self.fs.ftp2fs(self.fs.cwd) + prefix = 'ftpd.' + try: + fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix, + dir=basedir) + except (EnvironmentError, FilesystemError) as err: + # likely, we hit the max number of retries to find out a + # file with a unique name + if getattr(err, "errno", -1) == errno.EEXIST: + why = 'No usable unique file name found' + # something else happened + else: + why = _strerror(err) + self.respond("450 %s." % why) + return + + try: + if not self.authorizer.has_perm(self.username, 'w', fd.name): + try: + fd.close() + self.run_as_current_user(self.fs.remove, fd.name) + except (OSError, FilesystemError): + pass + self.respond("550 Not enough privileges.") + return + + # now just acts like STOR except that restarting isn't allowed + filename = os.path.basename(fd.name) + if self.data_channel is not None: + self.respond("125 FILE: %s" % filename) + self.data_channel.file_obj = fd + self.data_channel.enable_receiving(self._current_type, "STOU") + else: + self.respond("150 FILE: %s" % filename) + self._in_dtp_queue = (fd, "STOU") + return filename + except Exception: + fd.close() + raise + + def ftp_APPE(self, file): + """Append data to an existing file on the server. + On success return the file path, else None. + """ + # watch for APPE preceded by REST, which makes no sense. + if self._restart_position: + self.respond("450 Can't APPE while REST request is pending.") + else: + return self.ftp_STOR(file, mode='a') + + def ftp_REST(self, line): + """Restart a file transfer from a previous mark.""" + if self._current_type == 'a': + self.respond('501 Resuming transfers not allowed in ASCII mode.') + return + try: + marker = int(line) + if marker < 0: + raise ValueError + except (ValueError, OverflowError): + self.respond("501 Invalid parameter.") + else: + self.respond("350 Restarting at position %s." % marker) + self._restart_position = marker + + def ftp_ABOR(self, line): + """Abort the current data transfer.""" + # ABOR received while no data channel exists + if (self._dtp_acceptor is None and + self._dtp_connector is None and + self.data_channel is None): + self.respond("225 No transfer to abort.") + return + else: + # a PASV or PORT was received but connection wasn't made yet + if (self._dtp_acceptor is not None or + self._dtp_connector is not None): + self._shutdown_connecting_dtp() + resp = "225 ABOR command successful; data channel closed." + + # If a data transfer is in progress the server must first + # close the data connection, returning a 426 reply to + # indicate that the transfer terminated abnormally, then it + # must send a 226 reply, indicating that the abort command + # was successfully processed. + # If no data has been transmitted we just respond with 225 + # indicating that no transfer was in progress. + if self.data_channel is not None: + if self.data_channel.transfer_in_progress(): + self.data_channel.close() + self.data_channel = None + self.respond("426 Transfer aborted via ABOR.", + logfun=logging.info) + resp = "226 ABOR command successful." + else: + self.data_channel.close() + self.data_channel = None + resp = "225 ABOR command successful; data channel closed." + self.respond(resp) + + # --- authentication + def ftp_USER(self, line): + """Set the username for the current session.""" + # RFC-959 specifies a 530 response to the USER command if the + # username is not valid. If the username is valid is required + # ftpd returns a 331 response instead. In order to prevent a + # malicious client from determining valid usernames on a server, + # it is suggested by RFC-2577 that a server always return 331 to + # the USER command and then reject the combination of username + # and password for an invalid username when PASS is provided later. + if not self.authenticated: + self.respond('331 Username ok, send password.') + else: + # a new USER command could be entered at any point in order + # to change the access control flushing any user, password, + # and account information already supplied and beginning the + # login sequence again. + self.flush_account() + msg = 'Previous account information was flushed' + self.respond('331 %s, send password.' % msg, logfun=logging.info) + self.username = line + + def handle_auth_failed(self, msg, password): + def callback(username, password, msg): + self.add_channel() + if hasattr(self, '_closed') and not self._closed: + self.attempted_logins += 1 + if self.attempted_logins >= self.max_login_attempts: + msg += " Disconnecting." + self.respond("530 " + msg) + self.close_when_done() + else: + self.respond("530 " + msg) + self.log("USER '%s' failed login." % username) + self.on_login_failed(username, password) + + self.del_channel() + if not msg: + if self.username == 'anonymous': + msg = "Anonymous access not allowed." + else: + msg = "Authentication failed." + else: + # response string should be capitalized as per RFC-959 + msg = msg.capitalize() + self.ioloop.call_later(self.auth_failed_timeout, callback, + self.username, password, msg, + _errback=self.handle_error) + self.username = "" + + def handle_auth_success(self, home, password, msg_login): + if not isinstance(home, unicode): + if PY3: + raise TypeError('type(home) != text') + else: + warnings.warn( + '%s.get_home_dir returned a non-unicode string; now ' + 'casting to unicode' % ( + self.authorizer.__class__.__name__), + RuntimeWarning) + home = home.decode('utf8') + + if len(msg_login) <= 75: + self.respond('230 %s' % msg_login) + else: + self.push("230-%s\r\n" % msg_login) + self.respond("230 ") + self.log("USER '%s' logged in." % self.username) + self.authenticated = True + self.password = password + self.attempted_logins = 0 + + self.fs = self.abstracted_fs(home, self) + self.on_login(self.username) + + def ftp_PASS(self, line): + """Check username's password against the authorizer.""" + if self.authenticated: + self.respond("503 User already authenticated.") + return + if not self.username: + self.respond("503 Login with USER first.") + return + + try: + self.authorizer.validate_authentication(self.username, line, self) + home = self.authorizer.get_home_dir(self.username) + msg_login = self.authorizer.get_msg_login(self.username) + except (AuthenticationFailed, AuthorizerError) as err: + self.handle_auth_failed(str(err), line) + else: + self.handle_auth_success(home, line, msg_login) + + def ftp_REIN(self, line): + """Reinitialize user's current session.""" + # From RFC-959: + # REIN command terminates a USER, flushing all I/O and account + # information, except to allow any transfer in progress to be + # completed. All parameters are reset to the default settings + # and the control connection is left open. This is identical + # to the state in which a user finds himself immediately after + # the control connection is opened. + self.flush_account() + # Note: RFC-959 erroneously mention "220" as the correct response + # code to be given in this case, but this is wrong... + self.respond("230 Ready for new user.") + + # --- filesystem operations + def ftp_PWD(self, line): + """Return the name of the current working directory to the client.""" + # The 257 response is supposed to include the directory + # name and in case it contains embedded double-quotes + # they must be doubled (see RFC-959, chapter 7, appendix 2). + cwd = self.fs.cwd + assert isinstance(cwd, unicode), cwd + self.respond('257 "%s" is the current directory.' + % cwd.replace('"', '""')) + + def ftp_CWD(self, path): + """Change the current working directory. + On success return the new directory path, else None. + """ + # Temporarily join the specified directory to see if we have + # permissions to do so, then get back to original process's + # current working directory. + # Note that if for some reason os.getcwd() gets removed after + # the process is started we'll get into troubles (os.getcwd() + # will fail with ENOENT) but we can't do anything about that + # except logging an error. + init_cwd = getcwdu() + try: + self.run_as_current_user(self.fs.chdir, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + cwd = self.fs.cwd + assert isinstance(cwd, unicode), cwd + self.respond('250 "%s" is the current directory.' % cwd) + if getcwdu() != init_cwd: + os.chdir(init_cwd) + return path + + def ftp_CDUP(self, path): + """Change into the parent directory. + On success return the new directory, else None. + """ + # Note: RFC-959 says that code 200 is required but it also says + # that CDUP uses the same codes as CWD. + return self.ftp_CWD(path) + + def ftp_SIZE(self, path): + """Return size of file in a format suitable for using with + RESTart as defined in RFC-3659.""" + + # Implementation note: properly handling the SIZE command when + # TYPE ASCII is used would require to scan the entire file to + # perform the ASCII translation logic + # (file.read().replace(os.linesep, '\r\n')) and then calculating + # the len of such data which may be different than the actual + # size of the file on the server. Considering that calculating + # such result could be very resource-intensive and also dangerous + # (DoS) we reject SIZE when the current TYPE is ASCII. + # However, clients in general should not be resuming downloads + # in ASCII mode. Resuming downloads in binary mode is the + # recommended way as specified in RFC-3659. + + line = self.fs.fs2ftp(path) + if self._current_type == 'a': + why = "SIZE not allowed in ASCII mode" + self.respond("550 %s." % why) + return + if not self.fs.isfile(self.fs.realpath(path)): + why = "%s is not retrievable" % line + self.respond("550 %s." % why) + return + try: + size = self.run_as_current_user(self.fs.getsize, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("213 %s" % size) + + def ftp_MDTM(self, path): + """Return last modification time of file to the client as an ISO + 3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659. + On success return the file path, else None. + """ + line = self.fs.fs2ftp(path) + if not self.fs.isfile(self.fs.realpath(path)): + self.respond("550 %s is not retrievable" % line) + return + if self.use_gmt_times: + timefunc = time.gmtime + else: + timefunc = time.localtime + try: + secs = self.run_as_current_user(self.fs.getmtime, path) + lmt = time.strftime("%Y%m%d%H%M%S", timefunc(secs)) + except (ValueError, OSError, FilesystemError) as err: + if isinstance(err, ValueError): + # It could happen if file's last modification time + # happens to be too old (prior to year 1900) + why = "Can't determine file's last modification time" + else: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("213 %s" % lmt) + return path + + def ftp_MKD(self, path): + """Create the specified directory. + On success return the directory path, else None. + """ + line = self.fs.fs2ftp(path) + try: + self.run_as_current_user(self.fs.mkdir, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + # The 257 response is supposed to include the directory + # name and in case it contains embedded double-quotes + # they must be doubled (see RFC-959, chapter 7, appendix 2). + self.respond( + '257 "%s" directory created.' % line.replace('"', '""')) + return path + + def ftp_RMD(self, path): + """Remove the specified directory. + On success return the directory path, else None. + """ + if self.fs.realpath(path) == self.fs.realpath(self.fs.root): + msg = "Can't remove root directory." + self.respond("550 %s" % msg) + return + try: + self.run_as_current_user(self.fs.rmdir, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 Directory removed.") + + def ftp_DELE(self, path): + """Delete the specified file. + On success return the file path, else None. + """ + try: + self.run_as_current_user(self.fs.remove, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 File removed.") + return path + + def ftp_RNFR(self, path): + """Rename the specified (only the source name is specified + here, see RNTO command)""" + if not self.fs.lexists(path): + self.respond("550 No such file or directory.") + elif self.fs.realpath(path) == self.fs.realpath(self.fs.root): + self.respond("550 Can't rename home directory.") + else: + self._rnfr = path + self.respond("350 Ready for destination name.") + + def ftp_RNTO(self, path): + """Rename file (destination name only, source is specified with + RNFR). + On success return a (source_path, destination_path) tuple. + """ + if not self._rnfr: + self.respond("503 Bad sequence of commands: use RNFR first.") + return + src = self._rnfr + self._rnfr = None + try: + self.run_as_current_user(self.fs.rename, src, path) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond("250 Renaming ok.") + return (src, path) + + # --- others + def ftp_TYPE(self, line): + """Set current type data type to binary/ascii""" + type = line.upper().replace(' ', '') + if type in ("A", "L7"): + self.respond("200 Type set to: ASCII.") + self._current_type = 'a' + elif type in ("I", "L8"): + self.respond("200 Type set to: Binary.") + self._current_type = 'i' + else: + self.respond('504 Unsupported type "%s".' % line) + + def ftp_STRU(self, line): + """Set file structure ("F" is the only one supported (noop)).""" + stru = line.upper() + if stru == 'F': + self.respond('200 File transfer structure set to: F.') + elif stru in ('P', 'R'): + # R is required in minimum implementations by RFC-959, 5.1. + # RFC-1123, 4.1.2.13, amends this to only apply to servers + # whose file systems support record structures, but also + # suggests that such a server "may still accept files with + # STRU R, recording the byte stream literally". + # Should we accept R but with no operational difference from + # F? proftpd and wu-ftpd don't accept STRU R. We just do + # the same. + # + # RFC-1123 recommends against implementing P. + self.respond('504 Unimplemented STRU type.') + else: + self.respond('501 Unrecognized STRU type.') + + def ftp_MODE(self, line): + """Set data transfer mode ("S" is the only one supported (noop)).""" + mode = line.upper() + if mode == 'S': + self.respond('200 Transfer mode set to: S') + elif mode in ('B', 'C'): + self.respond('504 Unimplemented MODE type.') + else: + self.respond('501 Unrecognized MODE type.') + + def ftp_STAT(self, path): + """Return statistics about current ftp session. If an argument + is provided return directory listing over command channel. + + Implementation note: + + RFC-959 does not explicitly mention globbing but many FTP + servers do support it as a measure of convenience for FTP + clients and users. + + In order to search for and match the given globbing expression, + the code has to search (possibly) many directories, examine + each contained filename, and build a list of matching files in + memory. Since this operation can be quite intensive, both CPU- + and memory-wise, we do not support globbing. + """ + # return STATus information about ftpd + if not path: + s = [] + s.append('Connected to: %s:%s' % self.socket.getsockname()[:2]) + if self.authenticated: + s.append('Logged in as: %s' % self.username) + else: + if not self.username: + s.append("Waiting for username.") + else: + s.append("Waiting for password.") + if self._current_type == 'a': + type = 'ASCII' + else: + type = 'Binary' + s.append("TYPE: %s; STRUcture: File; MODE: Stream" % type) + if self._dtp_acceptor is not None: + s.append('Passive data channel waiting for connection.') + elif self.data_channel is not None: + bytes_sent = self.data_channel.tot_bytes_sent + bytes_recv = self.data_channel.tot_bytes_received + elapsed_time = self.data_channel.get_elapsed_time() + s.append('Data connection open:') + s.append('Total bytes sent: %s' % bytes_sent) + s.append('Total bytes received: %s' % bytes_recv) + s.append('Transfer elapsed time: %s secs' % elapsed_time) + else: + s.append('Data connection closed.') + + self.push('211-FTP server status:\r\n') + self.push(''.join([' %s\r\n' % item for item in s])) + self.respond('211 End of status.') + # return directory LISTing over the command channel + else: + line = self.fs.fs2ftp(path) + try: + isdir = self.fs.isdir(path) + if isdir: + listing = self.run_as_current_user(self.fs.listdir, path) + if isinstance(listing, list): + try: + # RFC 959 recommends the listing to be sorted. + listing.sort() + except UnicodeDecodeError: + # (Python 2 only) might happen on filesystem not + # supporting UTF8 meaning os.listdir() returned a + # list of mixed bytes and unicode strings: + # http://goo.gl/6DLHD + # http://bugs.python.org/issue683592 + pass + iterator = self.fs.format_list(path, listing) + else: + basedir, filename = os.path.split(path) + self.fs.lstat(path) # raise exc in case of problems + iterator = self.fs.format_list(basedir, [filename]) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.push('213-Status of "%s":\r\n' % line) + self.push_with_producer(BufferedIteratorProducer(iterator)) + self.respond('213 End of status.') + return path + + def ftp_FEAT(self, line): + """List all new features supported as defined in RFC-2398.""" + features = set(['UTF8', 'TVFS']) + features.update([feat for feat in ('EPRT', 'EPSV', 'MDTM', 'SIZE') + if feat in self.proto_cmds]) + features.update(self._extra_feats) + if 'MLST' in self.proto_cmds or 'MLSD' in self.proto_cmds: + facts = '' + for fact in self._available_facts: + if fact in self._current_facts: + facts += fact + '*;' + else: + facts += fact + ';' + features.add('MLST ' + facts) + if 'REST' in self.proto_cmds: + features.add('REST STREAM') + features = sorted(features) + self.push("211-Features supported:\r\n") + self.push("".join([" %s\r\n" % x for x in features])) + self.respond('211 End FEAT.') + + def ftp_OPTS(self, line): + """Specify options for FTP commands as specified in RFC-2389.""" + try: + if line.count(' ') > 1: + raise ValueError('Invalid number of arguments') + if ' ' in line: + cmd, arg = line.split(' ') + if ';' not in arg: + raise ValueError('Invalid argument') + else: + cmd, arg = line, '' + # actually the only command able to accept options is MLST + if cmd.upper() != 'MLST' or 'MLST' not in self.proto_cmds: + raise ValueError('Unsupported command "%s"' % cmd) + except ValueError as err: + self.respond('501 %s.' % err) + else: + facts = [x.lower() for x in arg.split(';')] + self._current_facts = \ + [x for x in facts if x in self._available_facts] + f = ''.join([x + ';' for x in self._current_facts]) + self.respond('200 MLST OPTS ' + f) + + def ftp_NOOP(self, line): + """Do nothing.""" + self.respond("200 I successfully done nothin'.") + + def ftp_SYST(self, line): + """Return system type (always returns UNIX type: L8).""" + # This command is used to find out the type of operating system + # at the server. The reply shall have as its first word one of + # the system names listed in RFC-943. + # Since that we always return a "/bin/ls -lA"-like output on + # LIST we prefer to respond as if we would on Unix in any case. + self.respond("215 UNIX Type: L8") + + def ftp_ALLO(self, line): + """Allocate bytes for storage (noop).""" + # not necessary (always respond with 202) + self.respond("202 No storage allocation necessary.") + + def ftp_HELP(self, line): + """Return help text to the client.""" + if line: + line = line.upper() + if line in self.proto_cmds: + self.respond("214 %s" % self.proto_cmds[line]['help']) + else: + self.respond("501 Unrecognized command.") + else: + # provide a compact list of recognized commands + def formatted_help(): + cmds = [] + keys = sorted([x for x in self.proto_cmds.keys() + if not x.startswith('SITE ')]) + while keys: + elems = tuple((keys[0:8])) + cmds.append(' %-6s' * len(elems) % elems + '\r\n') + del keys[0:8] + return ''.join(cmds) + + self.push("214-The following commands are recognized:\r\n") + self.push(formatted_help()) + self.respond("214 Help command successful.") + + # --- site commands + + # The user willing to add support for a specific SITE command must + # update self.proto_cmds dictionary and define a new ftp_SITE_%CMD% + # method in the subclass. + + def ftp_SITE_CHMOD(self, path, mode): + """Change file mode. + On success return a (file_path, mode) tuple. + """ + # Note: although most UNIX servers implement it, SITE CHMOD is not + # defined in any official RFC. + try: + assert len(mode) in (3, 4) + for x in mode: + assert 0 <= int(x) <= 7 + mode = int(mode, 8) + except (AssertionError, ValueError): + self.respond("501 Invalid SITE CHMOD format.") + else: + try: + self.run_as_current_user(self.fs.chmod, path, mode) + except (OSError, FilesystemError) as err: + why = _strerror(err) + self.respond('550 %s.' % why) + else: + self.respond('200 SITE CHMOD successful.') + return (path, mode) + + def ftp_SITE_HELP(self, line): + """Return help text to the client for a given SITE command.""" + if line: + line = line.upper() + if line in self.proto_cmds: + self.respond("214 %s" % self.proto_cmds[line]['help']) + else: + self.respond("501 Unrecognized SITE command.") + else: + self.push("214-The following SITE commands are recognized:\r\n") + site_cmds = [] + for cmd in sorted(self.proto_cmds.keys()): + if cmd.startswith('SITE '): + site_cmds.append(' %s\r\n' % cmd[5:]) + self.push(''.join(site_cmds)) + self.respond("214 Help SITE command successful.") + + # --- support for deprecated cmds + + # RFC-1123 requires that the server treat XCUP, XCWD, XMKD, XPWD + # and XRMD commands as synonyms for CDUP, CWD, MKD, LIST and RMD. + # Such commands are obsoleted but some ftp clients (e.g. Windows + # ftp.exe) still use them. + + def ftp_XCUP(self, line): + "Change to the parent directory. Synonym for CDUP. Deprecated." + return self.ftp_CDUP(line) + + def ftp_XCWD(self, line): + "Change the current working directory. Synonym for CWD. Deprecated." + return self.ftp_CWD(line) + + def ftp_XMKD(self, line): + "Create the specified directory. Synonym for MKD. Deprecated." + return self.ftp_MKD(line) + + def ftp_XPWD(self, line): + "Return the current working directory. Synonym for PWD. Deprecated." + return self.ftp_PWD(line) + + def ftp_XRMD(self, line): + "Remove the specified directory. Synonym for RMD. Deprecated." + return self.ftp_RMD(line) + + +# =================================================================== +# --- FTP over SSL +# =================================================================== + + +if SSL is not None: + + class SSLConnection(_AsyncChatNewStyle): + """An AsyncChat subclass supporting TLS/SSL.""" + + _ssl_accepting = False + _ssl_established = False + _ssl_closing = False + _ssl_requested = False + + def __init__(self, *args, **kwargs): + super(SSLConnection, self).__init__(*args, **kwargs) + self._error = False + self._ssl_want_read = False + self._ssl_want_write = False + + def readable(self): + return self._ssl_want_read or \ + super(SSLConnection, self).readable() + + def writable(self): + return self._ssl_want_write or \ + super(SSLConnection, self).writable() + + def secure_connection(self, ssl_context): + """Secure the connection switching from plain-text to + SSL/TLS. + """ + debug("securing SSL connection", self) + self._ssl_requested = True + try: + self.socket = SSL.Connection(ssl_context, self.socket) + except socket.error as err: + # may happen in case the client connects/disconnects + # very quickly + debug( + "call: secure_connection(); can't secure SSL connection " + "%r; closing" % err, self) + self.close() + except ValueError: + # may happen in case the client connects/disconnects + # very quickly + if self.socket.fileno() == -1: + debug( + "ValueError and fd == -1 on secure_connection()", self) + return + raise + else: + self.socket.set_accept_state() + self._ssl_accepting = True + + @contextlib.contextmanager + def _handle_ssl_want_rw(self): + prev_row_pending = self._ssl_want_read or self._ssl_want_write + try: + yield + except SSL.WantReadError: + # we should never get here; it's just for extra safety + self._ssl_want_read = True + except SSL.WantWriteError: + # we should never get here; it's just for extra safety + self._ssl_want_write = True + + if self._ssl_want_read: + self.modify_ioloop_events( + self._wanted_io_events | self.ioloop.READ, logdebug=True) + elif self._ssl_want_write: + self.modify_ioloop_events( + self._wanted_io_events | self.ioloop.WRITE, logdebug=True) + else: + if prev_row_pending: + self.modify_ioloop_events(self._wanted_io_events) + + def _do_ssl_handshake(self): + self._ssl_accepting = True + self._ssl_want_read = False + self._ssl_want_write = False + try: + self.socket.do_handshake() + except SSL.WantReadError: + self._ssl_want_read = True + debug("call: _do_ssl_handshake, err: ssl-want-read", inst=self) + except SSL.WantWriteError: + self._ssl_want_write = True + debug("call: _do_ssl_handshake, err: ssl-want-write", + inst=self) + except SSL.SysCallError as err: + debug("call: _do_ssl_handshake, err: %r" % err, inst=self) + retval, desc = err.args + if (retval == -1 and desc == 'Unexpected EOF') or retval > 0: + return self.handle_close() + raise + except SSL.Error as err: + debug("call: _do_ssl_handshake, err: %r" % err, inst=self) + return self.handle_failed_ssl_handshake() + else: + debug("SSL connection established", self) + self._ssl_accepting = False + self._ssl_established = True + self.handle_ssl_established() + + def handle_ssl_established(self): + """Called when SSL handshake has completed.""" + pass + + def handle_ssl_shutdown(self): + """Called when SSL shutdown() has completed.""" + super(SSLConnection, self).close() + + def handle_failed_ssl_handshake(self): + raise NotImplementedError("must be implemented in subclass") + + def handle_read_event(self): + if not self._ssl_requested: + super(SSLConnection, self).handle_read_event() + else: + with self._handle_ssl_want_rw(): + self._ssl_want_read = False + if self._ssl_accepting: + self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() + else: + super(SSLConnection, self).handle_read_event() + + def handle_write_event(self): + if not self._ssl_requested: + super(SSLConnection, self).handle_write_event() + else: + with self._handle_ssl_want_rw(): + self._ssl_want_write = False + if self._ssl_accepting: + self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() + else: + super(SSLConnection, self).handle_write_event() + + def handle_error(self): + self._error = True + try: + raise + except Exception: + self.log_exception(self) + # when facing an unhandled exception in here it's better + # to rely on base class (FTPHandler or DTPHandler) + # close() method as it does not imply SSL shutdown logic + try: + super(SSLConnection, self).close() + except Exception: + logger.critical(traceback.format_exc()) + + def send(self, data): + if not isinstance(data, bytes): + data = bytes(data) + try: + return super(SSLConnection, self).send(data) + except SSL.WantReadError: + debug("call: send(), err: ssl-want-read", inst=self) + self._ssl_want_read = True + return 0 + except SSL.WantWriteError: + debug("call: send(), err: ssl-want-write", inst=self) + self._ssl_want_write = True + return 0 + except SSL.ZeroReturnError as err: + debug( + "call: send() -> shutdown(), err: zero-return", inst=self) + super(SSLConnection, self).handle_close() + return 0 + except SSL.SysCallError as err: + debug("call: send(), err: %r" % err, inst=self) + errnum, errstr = err.args + if errnum == errno.EWOULDBLOCK: + return 0 + elif (errnum in _ERRNOS_DISCONNECTED or + errstr == 'Unexpected EOF'): + super(SSLConnection, self).handle_close() + return 0 + else: + raise + + def recv(self, buffer_size): + try: + return super(SSLConnection, self).recv(buffer_size) + except SSL.WantReadError: + debug("call: recv(), err: ssl-want-read", inst=self) + self._ssl_want_read = True + raise RetryError + except SSL.WantWriteError: + debug("call: recv(), err: ssl-want-write", inst=self) + self._ssl_want_write = True + raise RetryError + except SSL.ZeroReturnError as err: + debug("call: recv() -> shutdown(), err: zero-return", + inst=self) + super(SSLConnection, self).handle_close() + return b'' + except SSL.SysCallError as err: + debug("call: recv(), err: %r" % err, inst=self) + errnum, errstr = err.args + if (errnum in _ERRNOS_DISCONNECTED or + errstr == 'Unexpected EOF'): + super(SSLConnection, self).handle_close() + return b'' + else: + raise + + def _do_ssl_shutdown(self): + """Executes a SSL_shutdown() call to revert the connection + back to clear-text. + twisted/internet/tcp.py code has been used as an example. + """ + self._ssl_closing = True + if os.name == 'posix': + # since SSL_shutdown() doesn't report errors, an empty + # write call is done first, to try to detect if the + # connection has gone away + try: + os.write(self.socket.fileno(), b'') + except (OSError, socket.error) as err: + debug( + "call: _do_ssl_shutdown() -> os.write, err: %r" % err, + inst=self) + if err.errno in (errno.EINTR, errno.EWOULDBLOCK, + errno.ENOBUFS): + return + elif err.errno in _ERRNOS_DISCONNECTED: + return super(SSLConnection, self).close() + else: + raise + # Ok, this a mess, but the underlying OpenSSL API simply + # *SUCKS* and I really couldn't do any better. + # + # Here we just want to shutdown() the SSL layer and then + # close() the connection so we're not interested in a + # complete SSL shutdown() handshake, so let's pretend + # we already received a "RECEIVED" shutdown notification + # from the client. + # Once the client received our "SENT" shutdown notification + # then we close() the connection. + # + # Since it is not clear what errors to expect during the + # entire procedure we catch them all and assume the + # following: + # - WantReadError and WantWriteError means "retry" + # - ZeroReturnError, SysCallError[EOF], Error[] are all + # aliases for disconnection + try: + laststate = self.socket.get_shutdown() + self.socket.set_shutdown(laststate | SSL.RECEIVED_SHUTDOWN) + done = self.socket.shutdown() + if not (laststate & SSL.RECEIVED_SHUTDOWN): + self.socket.set_shutdown(SSL.SENT_SHUTDOWN) + except SSL.WantReadError: + self._ssl_want_read = True + debug("call: _do_ssl_shutdown, err: ssl-want-read", inst=self) + except SSL.WantWriteError: + self._ssl_want_write = True + debug("call: _do_ssl_shutdown, err: ssl-want-write", inst=self) + except SSL.ZeroReturnError as err: + debug( + "call: _do_ssl_shutdown() -> shutdown(), err: zero-return", + inst=self) + super(SSLConnection, self).close() + except SSL.SysCallError as err: + debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, + inst=self) + errnum, errstr = err.args + if (errnum in _ERRNOS_DISCONNECTED or + errstr == 'Unexpected EOF'): + super(SSLConnection, self).close() + else: + raise + except SSL.Error as err: + debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, + inst=self) + # see: + # https://github.com/giampaolo/pyftpdlib/issues/171 + # https://bugs.launchpad.net/pyopenssl/+bug/785985 + if err.args and not getattr(err, "errno", None): + pass + else: + raise + except socket.error as err: + debug("call: _do_ssl_shutdown() -> shutdown(), err: %r" % err, + inst=self) + if err.errno in _ERRNOS_DISCONNECTED: + super(SSLConnection, self).close() + else: + raise + else: + if done: + debug("call: _do_ssl_shutdown(), shutdown completed", + inst=self) + self._ssl_established = False + self._ssl_closing = False + self.handle_ssl_shutdown() + else: + debug( + "call: _do_ssl_shutdown(), shutdown not completed yet", + inst=self) + + def close(self): + if self._ssl_established and not self._error: + self._do_ssl_shutdown() + else: + self._ssl_accepting = False + self._ssl_established = False + self._ssl_closing = False + super(SSLConnection, self).close() + + class TLS_DTPHandler(SSLConnection, DTPHandler): + """A DTPHandler subclass supporting TLS/SSL.""" + + def __init__(self, sock, cmd_channel): + super(TLS_DTPHandler, self).__init__(sock, cmd_channel) + if self.cmd_channel._prot: + self.secure_connection(self.cmd_channel.ssl_context) + + def __repr__(self): + return DTPHandler.__repr__(self) + + def use_sendfile(self): + if isinstance(self.socket, SSL.Connection): + return False + else: + return super(TLS_DTPHandler, self).use_sendfile() + + def handle_failed_ssl_handshake(self): + # TLS/SSL handshake failure, probably client's fault which + # used a SSL version different from server's. + # RFC-4217, chapter 10.2 expects us to return 522 over the + # command channel. + self.cmd_channel.respond("522 SSL handshake failed.") + self.cmd_channel.log_cmd("PROT", "P", 522, "SSL handshake failed.") + self.close() + + class TLS_FTPHandler(SSLConnection, FTPHandler): + """A FTPHandler subclass supporting TLS/SSL. + Implements AUTH, PBSZ and PROT commands (RFC-2228 and RFC-4217). + + Configurable attributes: + + - (bool) tls_control_required: + When True requires SSL/TLS to be established on the control + channel, before logging in. This means the user will have + to issue AUTH before USER/PASS (default False). + + - (bool) tls_data_required: + When True requires SSL/TLS to be established on the data + channel. This means the user will have to issue PROT + before PASV or PORT (default False). + + SSL-specific options: + + - (string) certfile: + the path to the file which contains a certificate to be + used to identify the local side of the connection. + This must always be specified, unless context is provided + instead. + + - (string) keyfile: + the path to the file containing the private RSA key; + can be omitted if certfile already contains the private + key (defaults: None). + + - (int) ssl_protocol: + the desired SSL protocol version to use. This defaults to + PROTOCOL_SSLv23 which will negotiate the highest protocol + that both the server and your installation of OpenSSL + support. + + - (int) ssl_options: + specific OpenSSL options. These default to: + SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3| SSL.OP_NO_COMPRESSION + which are all considered insecure features. + Can be set to None in order to improve compatibilty with + older (insecure) FTP clients. + + - (instance) ssl_context: + a SSL Context object previously configured; if specified + all other parameters will be ignored. + (default None). + """ + + # configurable attributes + tls_control_required = False + tls_data_required = False + certfile = None + keyfile = None + ssl_protocol = SSL.SSLv23_METHOD + # - SSLv2 is easily broken and is considered harmful and dangerous + # - SSLv3 has several problems and is now dangerous + # - Disable compression to prevent CRIME attacks for OpenSSL 1.0+ + # (see https://github.com/shazow/urllib3/pull/309) + ssl_options = SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3 + if hasattr(SSL, "OP_NO_COMPRESSION"): + ssl_options |= SSL.OP_NO_COMPRESSION + ssl_context = None + + # overridden attributes + dtp_handler = TLS_DTPHandler + proto_cmds = FTPHandler.proto_cmds.copy() + proto_cmds.update({ + 'AUTH': dict( + perm=None, auth=False, arg=True, + help='Syntax: AUTH TLS|SSL (set up secure control ' + 'channel).'), + 'PBSZ': dict( + perm=None, auth=False, arg=True, + help='Syntax: PBSZ 0 (negotiate TLS buffer).'), + 'PROT': dict( + perm=None, auth=False, arg=True, + help='Syntax: PROT [C|P] (set up un/secure data ' + 'channel).'), + }) + + def __init__(self, conn, server, ioloop=None): + super(TLS_FTPHandler, self).__init__(conn, server, ioloop) + if not self.connected: + return + self._extra_feats = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] + self._pbsz = False + self._prot = False + self.ssl_context = self.get_ssl_context() + + def __repr__(self): + return FTPHandler.__repr__(self) + + @classmethod + def get_ssl_context(cls): + if cls.ssl_context is None: + if cls.certfile is None: + raise ValueError("at least certfile must be specified") + cls.ssl_context = SSL.Context(cls.ssl_protocol) + if cls.ssl_protocol != SSL.SSLv2_METHOD: + cls.ssl_context.set_options(SSL.OP_NO_SSLv2) + else: + warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) + cls.ssl_context.use_certificate_chain_file(cls.certfile) + if not cls.keyfile: + cls.keyfile = cls.certfile + cls.ssl_context.use_privatekey_file(cls.keyfile) + if cls.ssl_options: + cls.ssl_context.set_options(cls.ssl_options) + return cls.ssl_context + + # --- overridden methods + + def flush_account(self): + FTPHandler.flush_account(self) + self._pbsz = False + self._prot = False + + def process_command(self, cmd, *args, **kwargs): + if cmd in ('USER', 'PASS'): + if self.tls_control_required and not self._ssl_established: + msg = "SSL/TLS required on the control channel." + self.respond("550 " + msg) + self.log_cmd(cmd, args[0], 550, msg) + return + elif cmd in ('PASV', 'EPSV', 'PORT', 'EPRT'): + if self.tls_data_required and not self._prot: + msg = "SSL/TLS required on the data channel." + self.respond("550 " + msg) + self.log_cmd(cmd, args[0], 550, msg) + return + FTPHandler.process_command(self, cmd, *args, **kwargs) + + # --- new methods + + def handle_failed_ssl_handshake(self): + # TLS/SSL handshake failure, probably client's fault which + # used a SSL version different from server's. + # We can't rely on the control connection anymore so we just + # disconnect the client without sending any response. + self.log("SSL handshake failed.") + self.close() + + def ftp_AUTH(self, line): + """Set up secure control channel.""" + arg = line.upper() + if isinstance(self.socket, SSL.Connection): + self.respond("503 Already using TLS.") + elif arg in ('TLS', 'TLS-C', 'SSL', 'TLS-P'): + # From RFC-4217: "As the SSL/TLS protocols self-negotiate + # their levels, there is no need to distinguish between SSL + # and TLS in the application layer". + self.respond('234 AUTH %s successful.' % arg) + self.secure_connection(self.ssl_context) + else: + self.respond( + "502 Unrecognized encryption type (use TLS or SSL).") + + def ftp_PBSZ(self, line): + """Negotiate size of buffer for secure data transfer. + For TLS/SSL the only valid value for the parameter is '0'. + Any other value is accepted but ignored. + """ + if not isinstance(self.socket, SSL.Connection): + self.respond( + "503 PBSZ not allowed on insecure control connection.") + else: + self.respond('200 PBSZ=0 successful.') + self._pbsz = True + + def ftp_PROT(self, line): + """Setup un/secure data channel.""" + arg = line.upper() + if not isinstance(self.socket, SSL.Connection): + self.respond( + "503 PROT not allowed on insecure control connection.") + elif not self._pbsz: + self.respond( + "503 You must issue the PBSZ command prior to PROT.") + elif arg == 'C': + self.respond('200 Protection set to Clear') + self._prot = False + elif arg == 'P': + self.respond('200 Protection set to Private') + self._prot = True + elif arg in ('S', 'E'): + self.respond('521 PROT %s unsupported (use C or P).' % arg) + else: + self.respond("502 Unrecognized PROT type (use C or P).") diff --git a/ftp_server/pyftpdlib/ioloop.py b/ftp_server/pyftpdlib/ioloop.py new file mode 100644 index 00000000..0804ad2c --- /dev/null +++ b/ftp_server/pyftpdlib/ioloop.py @@ -0,0 +1,1059 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +""" +A specialized IO loop on top of asyncore adding support for epoll() +on Linux and kqueue() and OSX/BSD, dramatically increasing performances +offered by base asyncore module. + +poll() and select() loops are also reimplemented and are an order of +magnitude faster as they support fd un/registration and modification. + +This module is not supposed to be used directly unless you want to +include a new dispatcher which runs within the main FTP server loop, +in which case: + __________________________________________________________________ + | | | + | INSTEAD OF | ...USE: | + |______________________|___________________________________________| + | | | + | asyncore.dispacher | Acceptor (for servers) | + | asyncore.dispacher | Connector (for clients) | + | asynchat.async_chat | AsyncChat (for a full duplex connection ) | + | asyncore.loop | FTPServer.server_forever() | + |______________________|___________________________________________| + +asyncore.dispatcher_with_send is not supported, same for "map" argument +for asyncore.loop and asyncore.dispatcher and asynchat.async_chat +constructors. + +Follows a server example: + +import socket +from pyftpdlib.ioloop import IOLoop, Acceptor, AsyncChat + +class Handler(AsyncChat): + + def __init__(self, sock): + AsyncChat.__init__(self, sock) + self.push('200 hello\r\n') + self.close_when_done() + +class Server(Acceptor): + + def __init__(self, host, port): + Acceptor.__init__(self) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind((host, port)) + self.listen(5) + + def handle_accepted(self, sock, addr): + Handler(sock) + +server = Server('localhost', 8021) +IOLoop.instance().loop() +""" + +import asynchat +import asyncore +import errno +import heapq +import os +import select +import socket +import sys +import time +import traceback +try: + import threading +except ImportError: + import dummy_threading as threading + +from ._compat import callable +from .log import config_logging +from .log import debug +from .log import is_logging_configured +from .log import logger + + +timer = getattr(time, 'monotonic', time.time) +_read = asyncore.read +_write = asyncore.write + +# These errnos indicate that a connection has been abruptly terminated. +_ERRNOS_DISCONNECTED = set(( + errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN, errno.ECONNABORTED, + errno.EPIPE, errno.EBADF, errno.ETIMEDOUT)) +if hasattr(errno, "WSAECONNRESET"): + _ERRNOS_DISCONNECTED.add(errno.WSAECONNRESET) +if hasattr(errno, "WSAECONNABORTED"): + _ERRNOS_DISCONNECTED.add(errno.WSAECONNABORTED) + +# These errnos indicate that a non-blocking operation must be retried +# at a later time. +_ERRNOS_RETRY = set((errno.EAGAIN, errno.EWOULDBLOCK)) +if hasattr(errno, "WSAEWOULDBLOCK"): + _ERRNOS_RETRY.add(errno.WSAEWOULDBLOCK) + + +class RetryError(Exception): + pass + + +# =================================================================== +# --- scheduler +# =================================================================== + +class _Scheduler(object): + """Run the scheduled functions due to expire soonest (if any).""" + + def __init__(self): + # the heap used for the scheduled tasks + self._tasks = [] + self._cancellations = 0 + + def poll(self): + """Run the scheduled functions due to expire soonest and + return the timeout of the next one (if any, else None). + """ + now = timer() + calls = [] + while self._tasks: + if now < self._tasks[0].timeout: + break + call = heapq.heappop(self._tasks) + if call.cancelled: + self._cancellations -= 1 + else: + calls.append(call) + + for call in calls: + if call._repush: + heapq.heappush(self._tasks, call) + call._repush = False + continue + try: + call.call() + except Exception: + logger.error(traceback.format_exc()) + + # remove cancelled tasks and re-heapify the queue if the + # number of cancelled tasks is more than the half of the + # entire queue + if (self._cancellations > 512 and + self._cancellations > (len(self._tasks) >> 1)): + debug("re-heapifying %s cancelled tasks" % self._cancellations) + self.reheapify() + + try: + return max(0, self._tasks[0].timeout - now) + except IndexError: + pass + + def register(self, what): + """Register a _CallLater instance.""" + heapq.heappush(self._tasks, what) + + def unregister(self, what): + """Unregister a _CallLater instance. + The actual unregistration will happen at a later time though. + """ + self._cancellations += 1 + + def reheapify(self): + """Get rid of cancelled calls and reinitialize the internal heap.""" + self._cancellations = 0 + self._tasks = [x for x in self._tasks if not x.cancelled] + heapq.heapify(self._tasks) + + +class _CallLater(object): + """Container object which instance is returned by ioloop.call_later().""" + + __slots__ = ('_delay', '_target', '_args', '_kwargs', '_errback', '_sched', + '_repush', 'timeout', 'cancelled') + + def __init__(self, seconds, target, *args, **kwargs): + assert callable(target), "%s is not callable" % target + assert sys.maxsize >= seconds >= 0, \ + "%s is not greater than or equal to 0 seconds" % seconds + self._delay = seconds + self._target = target + self._args = args + self._kwargs = kwargs + self._errback = kwargs.pop('_errback', None) + self._sched = kwargs.pop('_scheduler') + self._repush = False + # seconds from the epoch at which to call the function + if not seconds: + self.timeout = 0 + else: + self.timeout = timer() + self._delay + self.cancelled = False + self._sched.register(self) + + def __lt__(self, other): + return self.timeout < other.timeout + + def __le__(self, other): + return self.timeout <= other.timeout + + def __repr__(self): + if self._target is None: + sig = object.__repr__(self) + else: + sig = repr(self._target) + sig += ' args=%s, kwargs=%s, cancelled=%s, secs=%s' % ( + self._args or '[]', self._kwargs or '{}', self.cancelled, + self._delay) + return '<%s>' % sig + + __str__ = __repr__ + + def _post_call(self, exc): + if not self.cancelled: + self.cancel() + + def call(self): + """Call this scheduled function.""" + assert not self.cancelled, "already cancelled" + exc = None + try: + self._target(*self._args, **self._kwargs) + except Exception as _: + exc = _ + if self._errback is not None: + self._errback() + else: + raise + finally: + self._post_call(exc) + + def reset(self): + """Reschedule this call resetting the current countdown.""" + assert not self.cancelled, "already cancelled" + self.timeout = timer() + self._delay + self._repush = True + + def cancel(self): + """Unschedule this call.""" + if not self.cancelled: + self.cancelled = True + self._target = self._args = self._kwargs = self._errback = None + self._sched.unregister(self) + + +class _CallEvery(_CallLater): + """Container object which instance is returned by IOLoop.call_every().""" + + def _post_call(self, exc): + if not self.cancelled: + if exc: + self.cancel() + else: + self.timeout = timer() + self._delay + self._sched.register(self) + + +class _IOLoop(object): + """Base class which will later be referred as IOLoop.""" + + READ = 1 + WRITE = 2 + _instance = None + _lock = threading.Lock() + _started_once = False + + def __init__(self): + self.socket_map = {} + self.sched = _Scheduler() + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + def __repr__(self): + status = [self.__class__.__module__ + "." + self.__class__.__name__] + status.append("(fds=%s, tasks=%s)" % ( + len(self.socket_map), len(self.sched._tasks))) + return '<%s at %#x>' % (' '.join(status), id(self)) + + __str__ = __repr__ + + @classmethod + def instance(cls): + """Return a global IOLoop instance.""" + if cls._instance is None: + with cls._lock: + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def register(self, fd, instance, events): + """Register a fd, handled by instance for the given events.""" + raise NotImplementedError('must be implemented in subclass') + + def unregister(self, fd): + """Register fd.""" + raise NotImplementedError('must be implemented in subclass') + + def modify(self, fd, events): + """Changes the events assigned for fd.""" + raise NotImplementedError('must be implemented in subclass') + + def poll(self, timeout): + """Poll once. The subclass overriding this method is supposed + to poll over the registered handlers and the scheduled functions + and then return. + """ + raise NotImplementedError('must be implemented in subclass') + + def loop(self, timeout=None, blocking=True): + """Start the asynchronous IO loop. + + - (float) timeout: the timeout passed to the underlying + multiplex syscall (select(), epoll() etc.). + + - (bool) blocking: if True poll repeatedly, as long as there + are registered handlers and/or scheduled functions. + If False poll only once and return the timeout of the next + scheduled call (if any, else None). + """ + if not _IOLoop._started_once: + _IOLoop._started_once = True + if not is_logging_configured(): + # If we get to this point it means the user hasn't + # configured logging. We want to log by default so + # we configure logging ourselves so that it will + # print to stderr. + config_logging() + + if blocking: + # localize variable access to minimize overhead + poll = self.poll + socket_map = self.socket_map + sched_poll = self.sched.poll + + if timeout is not None: + while socket_map: + poll(timeout) + sched_poll() + else: + soonest_timeout = None + while socket_map: + poll(soonest_timeout) + soonest_timeout = sched_poll() + else: + sched = self.sched + if self.socket_map: + self.poll(timeout) + if sched._tasks: + return sched.poll() + + def call_later(self, seconds, target, *args, **kwargs): + """Calls a function at a later time. + It can be used to asynchronously schedule a call within the polling + loop without blocking it. The instance returned is an object that + can be used to cancel or reschedule the call. + + - (int) seconds: the number of seconds to wait + - (obj) target: the callable object to call later + - args: the arguments to call it with + - kwargs: the keyword arguments to call it with; a special + '_errback' parameter can be passed: it is a callable + called in case target function raises an exception. + """ + kwargs['_scheduler'] = self.sched + return _CallLater(seconds, target, *args, **kwargs) + + def call_every(self, seconds, target, *args, **kwargs): + """Schedules the given callback to be called periodically.""" + kwargs['_scheduler'] = self.sched + return _CallEvery(seconds, target, *args, **kwargs) + + def close(self): + """Closes the IOLoop, freeing any resources used.""" + debug("closing IOLoop", self) + self.__class__._instance = None + + # free connections + instances = sorted(self.socket_map.values(), key=lambda x: x._fileno) + for inst in instances: + try: + inst.close() + except OSError as err: + if err.errno != errno.EBADF: + logger.error(traceback.format_exc()) + except Exception: + logger.error(traceback.format_exc()) + self.socket_map.clear() + + # free scheduled functions + for x in self.sched._tasks: + try: + if not x.cancelled: + x.cancel() + except Exception: + logger.error(traceback.format_exc()) + del self.sched._tasks[:] + + +# =================================================================== +# --- select() - POSIX / Windows +# =================================================================== + +class Select(_IOLoop): + """select()-based poller.""" + + def __init__(self): + _IOLoop.__init__(self) + self._r = [] + self._w = [] + + def register(self, fd, instance, events): + if fd not in self.socket_map: + self.socket_map[fd] = instance + if events & self.READ: + self._r.append(fd) + if events & self.WRITE: + self._w.append(fd) + + def unregister(self, fd): + try: + del self.socket_map[fd] + except KeyError: + debug("call: unregister(); fd was no longer in socket_map", self) + for l in (self._r, self._w): + try: + l.remove(fd) + except ValueError: + pass + + def modify(self, fd, events): + inst = self.socket_map.get(fd) + if inst is not None: + self.unregister(fd) + self.register(fd, inst, events) + else: + debug("call: modify(); fd was no longer in socket_map", self) + + def poll(self, timeout): + try: + r, w, e = select.select(self._r, self._w, [], timeout) + except select.error as err: + if getattr(err, "errno", None) == errno.EINTR: + return + raise + + smap_get = self.socket_map.get + for fd in r: + obj = smap_get(fd) + if obj is None or not obj.readable(): + continue + _read(obj) + for fd in w: + obj = smap_get(fd) + if obj is None or not obj.writable(): + continue + _write(obj) + + +# =================================================================== +# --- poll() / epoll() +# =================================================================== + +class _BasePollEpoll(_IOLoop): + """This is common to both poll() (UNIX), epoll() (Linux) and + /dev/poll (Solaris) implementations which share almost the same + interface. + Not supposed to be used directly. + """ + + def __init__(self): + _IOLoop.__init__(self) + self._poller = self._poller() + + def register(self, fd, instance, events): + try: + self._poller.register(fd, events) + except EnvironmentError as err: + if err.errno == errno.EEXIST: + debug("call: register(); poller raised EEXIST; ignored", self) + else: + raise + self.socket_map[fd] = instance + + def unregister(self, fd): + try: + del self.socket_map[fd] + except KeyError: + debug("call: unregister(); fd was no longer in socket_map", self) + else: + try: + self._poller.unregister(fd) + except EnvironmentError as err: + if err.errno in (errno.ENOENT, errno.EBADF): + debug("call: unregister(); poller returned %r; " + "ignoring it" % err, self) + else: + raise + + def modify(self, fd, events): + try: + self._poller.modify(fd, events) + except OSError as err: + if err.errno == errno.ENOENT and fd in self.socket_map: + # XXX - see: + # https://github.com/giampaolo/pyftpdlib/issues/329 + instance = self.socket_map[fd] + self.register(fd, instance, events) + else: + raise + + def poll(self, timeout): + try: + events = self._poller.poll(timeout or -1) # -1 waits indefinitely + except (IOError, select.error) as err: + # for epoll() and poll() respectively + if err.errno == errno.EINTR: + return + raise + # localize variable access to minimize overhead + smap_get = self.socket_map.get + for fd, event in events: + inst = smap_get(fd) + if inst is None: + continue + if event & self._ERROR and not event & self.READ: + inst.handle_close() + else: + if event & self.READ: + if inst.readable(): + _read(inst) + if event & self.WRITE: + if inst.writable(): + _write(inst) + + +# =================================================================== +# --- poll() - POSIX +# =================================================================== + +if hasattr(select, 'poll'): + + class Poll(_BasePollEpoll): + """poll() based poller.""" + + READ = select.POLLIN + WRITE = select.POLLOUT + _ERROR = select.POLLERR | select.POLLHUP | select.POLLNVAL + _poller = select.poll + + def modify(self, fd, events): + inst = self.socket_map[fd] + self.unregister(fd) + self.register(fd, inst, events) + + def poll(self, timeout): + # poll() timeout is expressed in milliseconds + if timeout is not None: + timeout = int(timeout * 1000) + _BasePollEpoll.poll(self, timeout) + + +# =================================================================== +# --- /dev/poll - Solaris (introduced in python 3.3) +# =================================================================== + +if hasattr(select, 'devpoll'): # pragma: no cover + + class DevPoll(_BasePollEpoll): + """/dev/poll based poller (introduced in python 3.3).""" + + READ = select.POLLIN + WRITE = select.POLLOUT + _ERROR = select.POLLERR | select.POLLHUP | select.POLLNVAL + _poller = select.devpoll + + # introduced in python 3.4 + if hasattr(select.devpoll, 'fileno'): + def fileno(self): + """Return devpoll() fd.""" + return self._poller.fileno() + + def modify(self, fd, events): + inst = self.socket_map[fd] + self.unregister(fd) + self.register(fd, inst, events) + + def poll(self, timeout): + # /dev/poll timeout is expressed in milliseconds + if timeout is not None: + timeout = int(timeout * 1000) + _BasePollEpoll.poll(self, timeout) + + # introduced in python 3.4 + if hasattr(select.devpoll, 'close'): + def close(self): + _IOLoop.close(self) + self._poller.close() + + +# =================================================================== +# --- epoll() - Linux +# =================================================================== + +if hasattr(select, 'epoll'): + + class Epoll(_BasePollEpoll): + """epoll() based poller.""" + + READ = select.EPOLLIN + WRITE = select.EPOLLOUT + _ERROR = select.EPOLLERR | select.EPOLLHUP + _poller = select.epoll + + def fileno(self): + """Return epoll() fd.""" + return self._poller.fileno() + + def close(self): + _IOLoop.close(self) + self._poller.close() + + +# =================================================================== +# --- kqueue() - BSD / OSX +# =================================================================== + +if hasattr(select, 'kqueue'): # pragma: no cover + + class Kqueue(_IOLoop): + """kqueue() based poller.""" + + def __init__(self): + _IOLoop.__init__(self) + self._kqueue = select.kqueue() + self._active = {} + + def fileno(self): + """Return kqueue() fd.""" + return self._kqueue.fileno() + + def close(self): + _IOLoop.close(self) + self._kqueue.close() + + def register(self, fd, instance, events): + self.socket_map[fd] = instance + try: + self._control(fd, events, select.KQ_EV_ADD) + except EnvironmentError as err: + if err.errno == errno.EEXIST: + debug("call: register(); poller raised EEXIST; ignored", + self) + else: + raise + self._active[fd] = events + + def unregister(self, fd): + try: + del self.socket_map[fd] + events = self._active.pop(fd) + except KeyError: + pass + else: + try: + self._control(fd, events, select.KQ_EV_DELETE) + except EnvironmentError as err: + if err.errno in (errno.ENOENT, errno.EBADF): + debug("call: unregister(); poller returned %r; " + "ignoring it" % err, self) + else: + raise + + def modify(self, fd, events): + instance = self.socket_map[fd] + self.unregister(fd) + self.register(fd, instance, events) + + def _control(self, fd, events, flags): + kevents = [] + if events & self.WRITE: + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_WRITE, flags=flags)) + if events & self.READ or not kevents: + # always read when there is not a write + kevents.append(select.kevent( + fd, filter=select.KQ_FILTER_READ, flags=flags)) + # even though control() takes a list, it seems to return + # EINVAL on Mac OS X (10.6) when there is more than one + # event in the list + for kevent in kevents: + self._kqueue.control([kevent], 0) + + # localize variable access to minimize overhead + def poll(self, + timeout, + _len=len, + _READ=select.KQ_FILTER_READ, + _WRITE=select.KQ_FILTER_WRITE, + _EOF=select.KQ_EV_EOF, + _ERROR=select.KQ_EV_ERROR): + try: + kevents = self._kqueue.control(None, _len(self.socket_map), + timeout) + except OSError as err: + if err.errno == errno.EINTR: + return + raise + for kevent in kevents: + inst = self.socket_map.get(kevent.ident) + if inst is None: + continue + if kevent.filter == _READ: + if inst.readable(): + _read(inst) + if kevent.filter == _WRITE: + if kevent.flags & _EOF: + # If an asynchronous connection is refused, + # kqueue returns a write event with the EOF + # flag set. + # Note that for read events, EOF may be returned + # before all data has been consumed from the + # socket buffer, so we only check for EOF on + # write events. + inst.handle_close() + else: + if inst.writable(): + _write(inst) + if kevent.flags & _ERROR: + inst.handle_close() + + +# =================================================================== +# --- choose the better poller for this platform +# =================================================================== + +if hasattr(select, 'epoll'): # epoll() - Linux + IOLoop = Epoll +elif hasattr(select, 'kqueue'): # kqueue() - BSD / OSX + IOLoop = Kqueue +elif hasattr(select, 'devpoll'): # /dev/poll - Solaris + IOLoop = DevPoll +elif hasattr(select, 'poll'): # poll() - POSIX + IOLoop = Poll +else: # select() - POSIX and Windows + IOLoop = Select + + +# =================================================================== +# --- asyncore dispatchers +# =================================================================== + +# these are overridden in order to register() and unregister() +# file descriptors against the new pollers + + +class AsyncChat(asynchat.async_chat): + """Same as asynchat.async_chat, only working with the new IO poller + and being more clever in avoid registering for read events when + it shouldn't. + """ + + def __init__(self, sock=None, ioloop=None): + self.ioloop = ioloop or IOLoop.instance() + self._wanted_io_events = self.ioloop.READ + self._current_io_events = self.ioloop.READ + self._closed = False + self._closing = False + self._fileno = sock.fileno() if sock else None + self._tasks = [] + asynchat.async_chat.__init__(self, sock) + + # --- IO loop related methods + + def add_channel(self, map=None, events=None): + assert self._fileno, repr(self._fileno) + events = events if events is not None else self.ioloop.READ + self.ioloop.register(self._fileno, self, events) + self._wanted_io_events = events + self._current_io_events = events + + def del_channel(self, map=None): + if self._fileno is not None: + self.ioloop.unregister(self._fileno) + + def modify_ioloop_events(self, events, logdebug=False): + if not self._closed: + assert self._fileno, repr(self._fileno) + if self._fileno not in self.ioloop.socket_map: + debug( + "call: modify_ioloop_events(), fd was no longer in " + "socket_map, had to register() it again", inst=self) + self.add_channel(events=events) + else: + if events != self._current_io_events: + if logdebug: + if events == self.ioloop.READ: + ev = "R" + elif events == self.ioloop.WRITE: + ev = "W" + elif events == self.ioloop.READ | self.ioloop.WRITE: + ev = "RW" + else: + ev = events + debug("call: IOLoop.modify(); setting %r IO events" % ( + ev), self) + self.ioloop.modify(self._fileno, events) + self._current_io_events = events + else: + debug( + "call: modify_ioloop_events(), handler had already been " + "close()d, skipping modify()", inst=self) + + # --- utils + + def call_later(self, seconds, target, *args, **kwargs): + """Same as self.ioloop.call_later but also cancel()s the + scheduled function on close(). + """ + if '_errback' not in kwargs and hasattr(self, 'handle_error'): + kwargs['_errback'] = self.handle_error + callback = self.ioloop.call_later(seconds, target, *args, **kwargs) + self._tasks.append(callback) + return callback + + # --- overridden asynchat methods + + def connect(self, addr): + self.modify_ioloop_events(self.ioloop.WRITE) + asynchat.async_chat.connect(self, addr) + + def connect_af_unspecified(self, addr, source_address=None): + """Same as connect() but guesses address family from addr. + Return the address family just determined. + """ + assert self.socket is None + host, port = addr + err = "getaddrinfo() returned an empty list" + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + for res in info: + self.socket = None + af, socktype, proto, canonname, sa = res + try: + self.create_socket(af, socktype) + if source_address: + if source_address[0].startswith('::ffff:'): + # In this scenario, the server has an IPv6 socket, but + # the remote client is using IPv4 and its address is + # represented as an IPv4-mapped IPv6 address which + # looks like this ::ffff:151.12.5.65, see: + # http://en.wikipedia.org/wiki/IPv6\ + # IPv4-mapped_addresses + # http://tools.ietf.org/html/rfc3493.html#section-3.7 + # We truncate the first bytes to make it look like a + # common IPv4 address. + source_address = (source_address[0][7:], + source_address[1]) + self.bind(source_address) + self.connect((host, port)) + except socket.error as _: + err = _ + if self.socket is not None: + self.socket.close() + self.del_channel() + self.socket = None + continue + break + if self.socket is None: + self.del_channel() + raise socket.error(err) + return af + + # send() and recv() overridden as a fix around various bugs: + # - http://bugs.python.org/issue1736101 + # - https://github.com/giampaolo/pyftpdlib/issues/104 + # - https://github.com/giampaolo/pyftpdlib/issues/109 + + def send(self, data): + try: + return self.socket.send(data) + except socket.error as err: + debug("call: send(), err: %s" % err, inst=self) + if err.errno in _ERRNOS_RETRY: + return 0 + elif err.errno in _ERRNOS_DISCONNECTED: + self.handle_close() + return 0 + else: + raise + + def recv(self, buffer_size): + try: + data = self.socket.recv(buffer_size) + except socket.error as err: + debug("call: recv(), err: %s" % err, inst=self) + if err.errno in _ERRNOS_DISCONNECTED: + self.handle_close() + return b'' + elif err.errno in _ERRNOS_RETRY: + raise RetryError + else: + raise + else: + if not data: + # a closed connection is indicated by signaling + # a read condition, and having recv() return 0. + self.handle_close() + return b'' + else: + return data + + def handle_read(self): + try: + asynchat.async_chat.handle_read(self) + except RetryError: + # This can be raised by (the overridden) recv(). + pass + + def initiate_send(self): + asynchat.async_chat.initiate_send(self) + if not self._closed: + # if there's still data to send we want to be ready + # for writing, else we're only intereseted in reading + if not self.producer_fifo: + wanted = self.ioloop.READ + else: + # In FTPHandler, we also want to listen for user input + # hence the READ. DTPHandler has its own initiate_send() + # which will either READ or WRITE. + wanted = self.ioloop.READ | self.ioloop.WRITE + if self._wanted_io_events != wanted: + self.ioloop.modify(self._fileno, wanted) + self._wanted_io_events = wanted + else: + debug("call: initiate_send(); called with no connection", + inst=self) + + def close_when_done(self): + if len(self.producer_fifo) == 0: + self.handle_close() + else: + self._closing = True + asynchat.async_chat.close_when_done(self) + + def close(self): + if not self._closed: + self._closed = True + try: + asynchat.async_chat.close(self) + finally: + for fun in self._tasks: + try: + fun.cancel() + except Exception: + logger.error(traceback.format_exc()) + self._tasks = [] + self._closed = True + self._closing = False + self.connected = False + + +class Connector(AsyncChat): + """Same as base AsyncChat and supposed to be used for + clients. + """ + + def add_channel(self, map=None, events=None): + AsyncChat.add_channel(self, map=map, events=self.ioloop.WRITE) + + +class Acceptor(AsyncChat): + """Same as base AsyncChat and supposed to be used to + accept new connections. + """ + + def add_channel(self, map=None, events=None): + AsyncChat.add_channel(self, map=map, events=self.ioloop.READ) + + def bind_af_unspecified(self, addr): + """Same as bind() but guesses address family from addr. + Return the address family just determined. + """ + assert self.socket is None + host, port = addr + if host == "": + # When using bind() "" is a symbolic name meaning all + # available interfaces. People might not know we're + # using getaddrinfo() internally, which uses None + # instead of "", so we'll make the conversion for them. + host = None + err = "getaddrinfo() returned an empty list" + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + for res in info: + self.socket = None + self.del_channel() + af, socktype, proto, canonname, sa = res + try: + self.create_socket(af, socktype) + self.set_reuse_addr() + self.bind(sa) + except socket.error as _: + err = _ + if self.socket is not None: + self.socket.close() + self.del_channel() + self.socket = None + continue + break + if self.socket is None: + self.del_channel() + raise socket.error(err) + return af + + def listen(self, num): + AsyncChat.listen(self, num) + # XXX - this seems to be necessary, otherwise kqueue.control() + # won't return listening fd events + try: + if isinstance(self.ioloop, Kqueue): + self.ioloop.modify(self._fileno, self.ioloop.READ) + except NameError: + pass + + def handle_accept(self): + try: + sock, addr = self.accept() + except TypeError: + # sometimes accept() might return None, see: + # https://github.com/giampaolo/pyftpdlib/issues/91 + debug("call: handle_accept(); accept() returned None", self) + return + except socket.error as err: + # ECONNABORTED might be thrown on *BSD, see: + # https://github.com/giampaolo/pyftpdlib/issues/105 + if err.errno != errno.ECONNABORTED: + raise + else: + debug("call: handle_accept(); accept() returned ECONNABORTED", + self) + else: + # sometimes addr == None instead of (ip, port) (see issue 104) + if addr is not None: + self.handle_accepted(sock, addr) + + def handle_accepted(self, sock, addr): + sock.close() + self.log_info('unhandled accepted event', 'warning') + + # overridden for convenience; avoid to reuse address on Windows + if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): + def set_reuse_addr(self): + pass diff --git a/ftp_server/pyftpdlib/log.py b/ftp_server/pyftpdlib/log.py new file mode 100644 index 00000000..1958a662 --- /dev/null +++ b/ftp_server/pyftpdlib/log.py @@ -0,0 +1,159 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +""" +Logging support for pyftpdlib, inspired from Tornado's +(http://www.tornadoweb.org/). + +This is not supposed to be imported/used directly. +Instead you should use logging.basicConfig before serve_forever(). +""" + +import logging +import sys +import time +try: + import curses +except ImportError: + curses = None + +from ._compat import unicode + + +# default logger +logger = logging.getLogger('pyftpdlib') + + +def _stderr_supports_color(): + color = False + if curses is not None and sys.stderr.isatty(): + try: + curses.setupterm() + if curses.tigetnum("colors") > 0: + color = True + except Exception: + pass + return color + + +# configurable options +LEVEL = logging.INFO +PREFIX = '[%(levelname)1.1s %(asctime)s]' +COLOURED = _stderr_supports_color() +TIME_FORMAT = "%Y-%m-%d %H:%M:%S" + + +# taken and adapted from Tornado +class LogFormatter(logging.Formatter): + """Log formatter used in pyftpdlib. + Key features of this formatter are: + + * Color support when logging to a terminal that supports it. + * Timestamps on every log line. + * Robust against str/bytes encoding problems. + """ + + def __init__(self, *args, **kwargs): + logging.Formatter.__init__(self, *args, **kwargs) + self._coloured = COLOURED and _stderr_supports_color() + if self._coloured: + curses.setupterm() + # The curses module has some str/bytes confusion in + # python3. Until version 3.2.3, most methods return + # bytes, but only accept strings. In addition, we want to + # output these strings with the logging module, which + # works with unicode strings. The explicit calls to + # unicode() below are harmless in python2 but will do the + # right conversion in python 3. + fg_color = (curses.tigetstr("setaf") or curses.tigetstr("setf") or + "") + if (3, 0) < sys.version_info < (3, 2, 3): + fg_color = unicode(fg_color, "ascii") + self._colors = { + # blues + logging.DEBUG: unicode(curses.tparm(fg_color, 4), "ascii"), + # green + logging.INFO: unicode(curses.tparm(fg_color, 2), "ascii"), + # yellow + logging.WARNING: unicode(curses.tparm(fg_color, 3), "ascii"), + # red + logging.ERROR: unicode(curses.tparm(fg_color, 1), "ascii") + } + self._normal = unicode(curses.tigetstr("sgr0"), "ascii") + + def format(self, record): + try: + record.message = record.getMessage() + except Exception as err: + record.message = "Bad message (%r): %r" % (err, record.__dict__) + + record.asctime = time.strftime(TIME_FORMAT, + self.converter(record.created)) + prefix = PREFIX % record.__dict__ + if self._coloured: + prefix = (self._colors.get(record.levelno, self._normal) + + prefix + self._normal) + + # Encoding notes: The logging module prefers to work with character + # strings, but only enforces that log messages are instances of + # basestring. In python 2, non-ascii bytestrings will make + # their way through the logging framework until they blow up with + # an unhelpful decoding error (with this formatter it happens + # when we attach the prefix, but there are other opportunities for + # exceptions further along in the framework). + # + # If a byte string makes it this far, convert it to unicode to + # ensure it will make it out to the logs. Use repr() as a fallback + # to ensure that all byte strings can be converted successfully, + # but don't do it by default so we don't add extra quotes to ascii + # bytestrings. This is a bit of a hacky place to do this, but + # it's worth it since the encoding errors that would otherwise + # result are so useless (and tornado is fond of using utf8-encoded + # byte strings wherever possible). + try: + message = unicode(record.message) + except UnicodeDecodeError: + message = repr(record.message) + + formatted = prefix + " " + message + if record.exc_info: + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + if record.exc_text: + formatted = formatted.rstrip() + "\n" + record.exc_text + return formatted.replace("\n", "\n ") + + +def debug(s, inst=None): + s = "[debug] " + s + if inst is not None: + s += " (%r)" % inst + logger.debug(s) + + +def is_logging_configured(): + if logging.getLogger('pyftpdlib').handlers: + return True + if logging.root.handlers: + return True + return False + + +# TODO: write tests +def config_logging(level=LEVEL, prefix=PREFIX, other_loggers=None): + # Little speed up + if "%(process)d" not in prefix: + logging.logProcesses = False + if "%(processName)s" not in prefix: + logging.logMultiprocessing = False + if "%(thread)d" not in prefix and "%(threadName)s" not in prefix: + logging.logThreads = False + handler = logging.StreamHandler() + handler.setFormatter(LogFormatter()) + loggers = [logging.getLogger('pyftpdlib')] + if other_loggers is not None: + loggers.extend(other_loggers) + for logger in loggers: + logger.setLevel(level) + logger.addHandler(handler) diff --git a/ftp_server/pyftpdlib/servers.py b/ftp_server/pyftpdlib/servers.py new file mode 100644 index 00000000..e703af3f --- /dev/null +++ b/ftp_server/pyftpdlib/servers.py @@ -0,0 +1,537 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +""" +This module contains the main FTPServer class which listens on a +host:port and dispatches the incoming connections to a handler. +The concurrency is handled asynchronously by the main process thread, +meaning the handler cannot block otherwise the whole server will hang. + +Other than that we have 2 subclasses changing the asynchronous concurrency +model using multiple threads or processes. + +You might be interested in these in case your code contains blocking +parts which cannot be adapted to the base async model or if the +underlying filesystem is particularly slow, see: + +https://github.com/giampaolo/pyftpdlib/issues/197 +https://github.com/giampaolo/pyftpdlib/issues/212 + +Two classes are provided: + + - ThreadingFTPServer + - MultiprocessFTPServer + +...spawning a new thread or process every time a client connects. + +The main thread will be async-based and be used only to accept new +connections. +Every time a new connection comes in that will be dispatched to a +separate thread/process which internally will run its own IO loop. +This way the handler handling that connections will be free to block +without hanging the whole FTP server. +""" + +import errno +import os +import select +import signal +import sys +import time +import traceback + +from .ioloop import Acceptor +from .ioloop import IOLoop +from .log import config_logging +from .log import debug +from .log import is_logging_configured +from .log import logger + + +__all__ = ['FTPServer'] +_BSD = 'bsd' in sys.platform + + +# =================================================================== +# --- base class +# =================================================================== + +class FTPServer(Acceptor): + """Creates a socket listening on
, dispatching the requests + to a (typically FTPHandler class). + + Depending on the type of address specified IPv4 or IPv6 connections + (or both, depending from the underlying system) will be accepted. + + All relevant session information is stored in class attributes + described below. + + - (int) max_cons: + number of maximum simultaneous connections accepted (defaults + to 512). Can be set to 0 for unlimited but it is recommended + to always have a limit to avoid running out of file descriptors + (DoS). + + - (int) max_cons_per_ip: + number of maximum connections accepted for the same IP address + (defaults to 0 == unlimited). + """ + + max_cons = 512 + max_cons_per_ip = 0 + + def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): + """Creates a socket listening on 'address' dispatching + connections to a 'handler'. + + - (tuple) address_or_socket: the (host, port) pair on which + the command channel will listen for incoming connections or + an existent socket object. + + - (instance) handler: the handler class to use. + + - (instance) ioloop: a pyftpdlib.ioloop.IOLoop instance + + - (int) backlog: the maximum number of queued connections + passed to listen(). If a connection request arrives when + the queue is full the client may raise ECONNRESET. + Defaults to 5. + """ + Acceptor.__init__(self, ioloop=ioloop) + self.handler = handler + self.backlog = backlog + self.ip_map = [] + # in case of FTPS class not properly configured we want errors + # to be raised here rather than later, when client connects + if hasattr(handler, 'get_ssl_context'): + handler.get_ssl_context() + if callable(getattr(address_or_socket, 'listen', None)): + sock = address_or_socket + sock.setblocking(0) + self.set_socket(sock) + else: + self.bind_af_unspecified(address_or_socket) + self.listen(backlog) + + @property + def address(self): + return self.socket.getsockname()[:2] + + def _map_len(self): + return len(self.ioloop.socket_map) + + def _accept_new_cons(self): + """Return True if the server is willing to accept new connections.""" + if not self.max_cons: + return True + else: + return self._map_len() <= self.max_cons + + def _log_start(self): + def get_fqname(obj): + try: + return obj.__module__ + "." + obj.__class__.__name__ + except AttributeError: + try: + return obj.__module__ + "." + obj.__name__ + except AttributeError: + return str(obj) + + if not is_logging_configured(): + # If we get to this point it means the user hasn't + # configured any logger. We want logging to be on + # by default (stderr). + config_logging() + + if self.handler.passive_ports: + pasv_ports = "%s->%s" % (self.handler.passive_ports[0], + self.handler.passive_ports[-1]) + else: + pasv_ports = None + addr = self.address + if hasattr(self.handler, 'ssl_protocol'): + proto = "FTP+SSL" + else: + proto = "FTP" + logger.info(">>> starting %s server on %s:%s, pid=%i <<<" + % (proto, addr[0], addr[1], os.getpid())) + if ('ThreadedFTPServer' in __all__ and + issubclass(self.__class__, ThreadedFTPServer)): + logger.info("concurrency model: multi-thread") + elif ('MultiprocessFTPServer' in __all__ and + issubclass(self.__class__, MultiprocessFTPServer)): + logger.info("concurrency model: multi-process") + elif issubclass(self.__class__, FTPServer): + logger.info("concurrency model: async") + + logger.info("masquerade (NAT) address: %s", + self.handler.masquerade_address) + logger.info("passive ports: %s", pasv_ports) + logger.debug("poller: %r", get_fqname(self.ioloop)) + logger.debug("authorizer: %r", get_fqname(self.handler.authorizer)) + if os.name == 'posix': + logger.debug("use sendfile(2): %s", self.handler.use_sendfile) + logger.debug("handler: %r", get_fqname(self.handler)) + logger.debug("max connections: %s", self.max_cons or "unlimited") + logger.debug("max connections per ip: %s", + self.max_cons_per_ip or "unlimited") + logger.debug("timeout: %s", self.handler.timeout or "unlimited") + logger.debug("banner: %r", self.handler.banner) + logger.debug("max login attempts: %r", self.handler.max_login_attempts) + if getattr(self.handler, 'certfile', None): + logger.debug("SSL certfile: %r", self.handler.certfile) + if getattr(self.handler, 'keyfile', None): + logger.debug("SSL keyfile: %r", self.handler.keyfile) + + def serve_forever(self, timeout=None, blocking=True, handle_exit=True): + """Start serving. + + - (float) timeout: the timeout passed to the underlying IO + loop expressed in seconds. + + - (bool) blocking: if False loop once and then return the + timeout of the next scheduled call next to expire soonest + (if any). + + - (bool) handle_exit: when True catches KeyboardInterrupt and + SystemExit exceptions (generally caused by SIGTERM / SIGINT + signals) and gracefully exits after cleaning up resources. + Also, logs server start and stop. + """ + if handle_exit: + log = handle_exit and blocking + if log: + self._log_start() + try: + self.ioloop.loop(timeout, blocking) + except (KeyboardInterrupt, SystemExit): + logger.info("received interrupt signal") + if blocking: + if log: + logger.info( + ">>> shutting down FTP server (%s active socket " + "fds) <<<", + self._map_len()) + self.close_all() + else: + self.ioloop.loop(timeout, blocking) + + def handle_accepted(self, sock, addr): + """Called when remote client initiates a connection.""" + handler = None + ip = None + try: + handler = self.handler(sock, self, ioloop=self.ioloop) + if not handler.connected: + return + + ip = addr[0] + self.ip_map.append(ip) + + # For performance and security reasons we should always set a + # limit for the number of file descriptors that socket_map + # should contain. When we're running out of such limit we'll + # use the last available channel for sending a 421 response + # to the client before disconnecting it. + if not self._accept_new_cons(): + handler.handle_max_cons() + return + + # accept only a limited number of connections from the same + # source address. + if self.max_cons_per_ip: + if self.ip_map.count(ip) > self.max_cons_per_ip: + handler.handle_max_cons_per_ip() + return + + try: + handler.handle() + except Exception: + handler.handle_error() + else: + return handler + except Exception: + # This is supposed to be an application bug that should + # be fixed. We do not want to tear down the server though + # (DoS). We just log the exception, hoping that someone + # will eventually file a bug. References: + # - https://github.com/giampaolo/pyftpdlib/issues/143 + # - https://github.com/giampaolo/pyftpdlib/issues/166 + # - https://groups.google.com/forum/#!topic/pyftpdlib/h7pPybzAx14 + logger.error(traceback.format_exc()) + if handler is not None: + handler.close() + else: + if ip is not None and ip in self.ip_map: + self.ip_map.remove(ip) + + def handle_error(self): + """Called to handle any uncaught exceptions.""" + try: + raise + except Exception: + logger.error(traceback.format_exc()) + self.close() + + def close_all(self): + """Stop serving and also disconnects all currently connected + clients. + """ + return self.ioloop.close() + + +# =================================================================== +# --- extra implementations +# =================================================================== + +class _SpawnerBase(FTPServer): + """Base class shared by multiple threads/process dispatcher. + Not supposed to be used. + """ + + # how many seconds to wait when join()ing parent's threads + # or processes + join_timeout = 5 + _lock = None + _exit = None + + def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): + FTPServer.__init__(self, address_or_socket, handler, + ioloop=ioloop, backlog=backlog) + self._active_tasks = [] + + def _start_task(self, *args, **kwargs): + raise NotImplementedError('must be implemented in subclass') + + def _current_task(self): + raise NotImplementedError('must be implemented in subclass') + + def _map_len(self): + raise NotImplementedError('must be implemented in subclass') + + def _loop(self, handler): + """Serve handler's IO loop in a separate thread or process.""" + with IOLoop() as ioloop: + handler.ioloop = ioloop + try: + handler.add_channel() + except EnvironmentError as err: + if err.errno == errno.EBADF: + # we might get here in case the other end quickly + # disconnected (see test_quick_connect()) + debug("call: %s._loop(); add_channel() returned EBADF", + self) + return + else: + raise + + # Here we localize variable access to minimize overhead. + poll = ioloop.poll + sched_poll = ioloop.sched.poll + poll_timeout = getattr(self, 'poll_timeout', None) + soonest_timeout = poll_timeout + + while (ioloop.socket_map or ioloop.sched._tasks) and \ + not self._exit.is_set(): + try: + if ioloop.socket_map: + poll(timeout=soonest_timeout) + if ioloop.sched._tasks: + soonest_timeout = sched_poll() + # Handle the case where socket_map is emty but some + # cancelled scheduled calls are still around causing + # this while loop to hog CPU resources. + # In theory this should never happen as all the sched + # functions are supposed to be cancel()ed on close() + # but by using threads we can incur into + # synchronization issues such as this one. + # https://github.com/giampaolo/pyftpdlib/issues/245 + if not ioloop.socket_map: + # get rid of cancel()led calls + ioloop.sched.reheapify() + soonest_timeout = sched_poll() + if soonest_timeout: + time.sleep(min(soonest_timeout, 1)) + else: + soonest_timeout = None + except (KeyboardInterrupt, SystemExit): + # note: these two exceptions are raised in all sub + # processes + self._exit.set() + except select.error as err: + # on Windows we can get WSAENOTSOCK if the client + # rapidly connect and disconnects + if os.name == 'nt' and err[0] == 10038: + for fd in list(ioloop.socket_map.keys()): + try: + select.select([fd], [], [], 0) + except select.error: + try: + logger.info("discarding broken socket %r", + ioloop.socket_map[fd]) + del ioloop.socket_map[fd] + except KeyError: + # dict changed during iteration + pass + else: + raise + else: + if poll_timeout: + if (soonest_timeout is None or + soonest_timeout > poll_timeout): + soonest_timeout = poll_timeout + + def handle_accepted(self, sock, addr): + handler = FTPServer.handle_accepted(self, sock, addr) + if handler is not None: + # unregister the handler from the main IOLoop used by the + # main thread to accept connections + self.ioloop.unregister(handler._fileno) + + t = self._start_task(target=self._loop, args=(handler,)) + t.name = repr(addr) + t.start() + + # it is a different process so free resources here + if hasattr(t, 'pid'): + handler.close() + + with self._lock: + # clean finished tasks + for task in self._active_tasks[:]: + if not task.is_alive(): + self._active_tasks.remove(task) + # add the new task + self._active_tasks.append(t) + + def _log_start(self): + FTPServer._log_start(self) + + def serve_forever(self, timeout=1.0, blocking=True, handle_exit=True): + self._exit.clear() + if handle_exit: + log = handle_exit and blocking + if log: + self._log_start() + try: + self.ioloop.loop(timeout, blocking) + except (KeyboardInterrupt, SystemExit): + pass + if blocking: + if log: + logger.info( + ">>> shutting down FTP server (%s active workers) <<<", + self._map_len()) + self.close_all() + else: + self.ioloop.loop(timeout, blocking) + + def close_all(self): + tasks = self._active_tasks[:] + # this must be set after getting active tasks as it causes + # thread objects to get out of the list too soon + self._exit.set() + if tasks and hasattr(tasks[0], 'terminate'): + # we're dealing with subprocesses + for t in tasks: + try: + if not _BSD: + t.terminate() + else: + # XXX - On FreeBSD using SIGTERM doesn't work + # as the process hangs on kqueue.control() or + # select.select(). Use SIGKILL instead. + os.kill(t.pid, signal.SIGKILL) + except OSError as err: + if err.errno != errno.ESRCH: + raise + + self._wait_for_tasks(tasks) + del self._active_tasks[:] + FTPServer.close_all(self) + + def _wait_for_tasks(self, tasks): + """Wait for threads or subprocesses to terminate.""" + warn = logger.warning + for t in tasks: + t.join(self.join_timeout) + if t.is_alive(): + # Thread or process is still alive. If it's a process + # attempt to send SIGKILL as last resort. + # Set timeout to None so that we will exit immediately + # in case also other threads/processes are hanging. + self.join_timeout = None + if hasattr(t, 'terminate'): + msg = "could not terminate process %r" % t + if not _BSD: + warn(msg + "; sending SIGKILL as last resort") + try: + os.kill(t.pid, signal.SIGKILL) + except OSError as err: + if err.errno != errno.ESRCH: + raise + else: + warn(msg) + else: + warn("thread %r didn't terminate; ignoring it", t) + + +try: + import threading +except ImportError: + pass +else: + __all__ += ['ThreadedFTPServer'] + + # compatibility with python <= 2.6 + if not hasattr(threading.Thread, 'is_alive'): + threading.Thread.is_alive = threading.Thread.isAlive + + class ThreadedFTPServer(_SpawnerBase): + """A modified version of base FTPServer class which spawns a + thread every time a new connection is established. + """ + # The timeout passed to thread's IOLoop.poll() call on every + # loop. Necessary since threads ignore KeyboardInterrupt. + poll_timeout = 1.0 + _lock = threading.Lock() + _exit = threading.Event() + + # compatibility with python <= 2.6 + if not hasattr(_exit, 'is_set'): + _exit.is_set = _exit.isSet + + def _start_task(self, *args, **kwargs): + return threading.Thread(*args, **kwargs) + + def _current_task(self): + return threading.currentThread() + + def _map_len(self): + return threading.activeCount() + + +if os.name == 'posix': + try: + import multiprocessing + except ImportError: + pass + else: + __all__ += ['MultiprocessFTPServer'] + + class MultiprocessFTPServer(_SpawnerBase): + """A modified version of base FTPServer class which spawns a + process every time a new connection is established. + """ + _lock = multiprocessing.Lock() + _exit = multiprocessing.Event() + + def _start_task(self, *args, **kwargs): + return multiprocessing.Process(*args, **kwargs) + + def _current_task(self): + return multiprocessing.current_process() + + def _map_len(self): + return len(multiprocessing.active_children()) diff --git a/ftp_server/pyftpdlib/test/README b/ftp_server/pyftpdlib/test/README new file mode 100644 index 00000000..c79aedf6 --- /dev/null +++ b/ftp_server/pyftpdlib/test/README @@ -0,0 +1,5 @@ +RUNNNING TESTS +============== + +In order to run these tests cd into project root directory then run +"make test" (this is also valid for Windows). diff --git a/ftp_server/pyftpdlib/test/__init__.py b/ftp_server/pyftpdlib/test/__init__.py new file mode 100644 index 00000000..c9956f8d --- /dev/null +++ b/ftp_server/pyftpdlib/test/__init__.py @@ -0,0 +1,394 @@ +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import atexit +import contextlib +import errno +import functools +import logging +import os +import shutil +import socket +import sys +import tempfile +import threading +import time +import warnings +try: + from unittest import mock # py3 +except ImportError: + import mock # NOQA - requires "pip install mock" + +from pyftpdlib._compat import getcwdu +from pyftpdlib._compat import u +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.ioloop import IOLoop +from pyftpdlib.servers import FTPServer + +if sys.version_info < (2, 7): + import unittest2 as unittest # pip install unittest2 +else: + import unittest + +if not hasattr(unittest.TestCase, "assertRaisesRegex"): + unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + +sendfile = None +if os.name == 'posix': + try: + import sendfile + except ImportError: + pass + + +# Attempt to use IP rather than hostname (test suite will run a lot faster) +try: + HOST = socket.gethostbyname('localhost') +except socket.error: + HOST = 'localhost' +USER = 'user' +PASSWD = '12345' +HOME = getcwdu() +TESTFN = 'tmp-pyftpdlib' +TESTFN_UNICODE = TESTFN + '-unicode-' + '\xe2\x98\x83' +TESTFN_UNICODE_2 = TESTFN_UNICODE + '-2' +TIMEOUT = 2 +BUFSIZE = 1024 +INTERRUPTED_TRANSF_SIZE = 32768 +NO_RETRIES = 5 +OSX = sys.platform.startswith("darwin") +POSIX = os.name == 'posix' +WINDOWS = os.name == 'nt' +TRAVIS = bool(os.environ.get('TRAVIS')) +VERBOSITY = 1 if os.getenv('SILENT') else 2 + + +class TestCase(unittest.TestCase): + + def __str__(self): + return "%s.%s.%s" % ( + self.__class__.__module__, self.__class__.__name__, + self._testMethodName) + + +# Hack that overrides default unittest.TestCase in order to print +# a full path representation of the single unit tests being run. +unittest.TestCase = TestCase + + +def try_address(host, port=0, family=socket.AF_INET): + """Try to bind a socket on the given host:port and return True + if that has been possible.""" + try: + with contextlib.closing(socket.socket(family)) as sock: + sock.bind((host, port)) + except (socket.error, socket.gaierror): + return False + else: + return True + + +SUPPORTS_IPV4 = try_address('127.0.0.1') +SUPPORTS_IPV6 = socket.has_ipv6 and try_address('::1', family=socket.AF_INET6) +SUPPORTS_SENDFILE = hasattr(os, 'sendfile') or sendfile is not None + + +def safe_remove(*files): + "Convenience function for removing temporary test files" + for file in files: + try: + os.remove(file) + except OSError as err: + if os.name == 'nt': + return + if err.errno != errno.ENOENT: + raise + + +def safe_rmdir(dir): + "Convenience function for removing temporary test directories" + try: + os.rmdir(dir) + except OSError as err: + if os.name == 'nt': + return + if err.errno != errno.ENOENT: + raise + + +def safe_mkdir(dir): + "Convenience function for creating a directory" + try: + os.mkdir(dir) + except OSError as err: + if err.errno != errno.EEXIST: + raise + + +def touch(name): + """Create a file and return its name.""" + with open(name, 'w') as f: + return f.name + + +def remove_test_files(): + """Remove files and directores created during tests.""" + for name in os.listdir(u('.')): + if name.startswith(tempfile.template): + if os.path.isdir(name): + shutil.rmtree(name) + else: + safe_remove(name) + + +def warn(msg): + """Add warning message to be executed on exit.""" + atexit.register(warnings.warn, str(msg) + " - tests have been skipped", + RuntimeWarning) + + +def configure_logging(): + """Set pyftpdlib logger to "WARNING" level.""" + channel = logging.StreamHandler() + logger = logging.getLogger('pyftpdlib') + logger.setLevel(logging.WARNING) + logger.addHandler(channel) + + +def disable_log_warning(fun): + """Temporarily set FTP server's logging level to ERROR.""" + @functools.wraps(fun) + def wrapper(self, *args, **kwargs): + logger = logging.getLogger('pyftpdlib') + level = logger.getEffectiveLevel() + logger.setLevel(logging.ERROR) + try: + return fun(self, *args, **kwargs) + finally: + logger.setLevel(level) + return wrapper + + +def cleanup(): + """Cleanup function executed on interpreter exit.""" + remove_test_files() + map = IOLoop.instance().socket_map + for x in list(map.values()): + try: + sys.stderr.write("garbage: %s\n" % repr(x)) + x.close() + except Exception: + pass + map.clear() + + +def retry_on_failure(ntimes=None): + """Decorator to retry a test in case of failure.""" + def decorator(fun): + @functools.wraps(fun) + def wrapper(*args, **kwargs): + for x in range(ntimes or NO_RETRIES): + try: + return fun(*args, **kwargs) + except AssertionError as _: + err = _ + raise err + return wrapper + return decorator + + +def call_until(fun, expr, timeout=TIMEOUT): + """Keep calling function for timeout secs and exit if eval() + expression is True. + """ + stop_at = time.time() + timeout + while time.time() < stop_at: + ret = fun() + if eval(expr): + return ret + time.sleep(0.001) + raise RuntimeError('timed out (ret=%r)' % ret) + + +def get_server_handler(): + """Return the first FTPHandler instance running in the IOLoop.""" + ioloop = IOLoop.instance() + for fd in ioloop.socket_map: + instance = ioloop.socket_map[fd] + if isinstance(instance, FTPHandler): + return instance + raise RuntimeError("can't find any FTPHandler instance") + + +# commented out as per bug http://bugs.python.org/issue10354 +# tempfile.template = 'tmp-pyftpdlib' + + +class ThreadWorker(threading.Thread): + """A wrapper on top of threading.Thread. + It lets you define a thread worker class which you can easily + start() and stop(). + Subclass MUST provide a poll() method. Optionally it can also + provide the following methods: + + - before_start + - before_stop + - after_stop + + poll() is supposed to be a non-blocking method so that the + worker can be stop()ped immediately. + + **All method calls are supposed to be thread safe, start(), stop() + and the callback methods.** + + Example: + + class MyWorker(ThreadWorker): + + def poll(self): + do_something() + + def before_start(self): + log("starting") + + def before_stop(self): + log("stopping") + + def after_stop(self): + do_cleanup() + + worker = MyWorker(poll_interval=5) + worker.start() + worker.stop() + """ + + # Makes the thread stop on interpreter exit. + daemon = True + + def __init__(self, poll_interval=1.0): + super(ThreadWorker, self).__init__() + self.poll_interval = poll_interval + self.started = False + self.stopped = False + self.lock = threading.Lock() + self._stop_flag = False + self._event_start = threading.Event() + self._event_stop = threading.Event() + + # --- overridable methods + + def poll(self): + raise NotImplementedError("must be implemented in subclass") + + def before_start(self): + """Called right before start().""" + pass + + def before_stop(self): + """Called right before stop(), before signaling the thread + to stop polling. + """ + pass + + def after_stop(self): + """Called right after stop(), after the thread stopped polling.""" + pass + + # --- internals + + def sleep(self): + # Responsive sleep, so that the interpreter will shut down + # after max 1 sec. + if self.poll_interval: + stop_at = time.time() + self.poll_interval + while True: + time.sleep(min(self.poll_interval, 1)) + if time.time() >= stop_at: + break + + def run(self): + try: + while not self._stop_flag: + with self.lock: + if not self.started: + self._event_start.set() + self.started = True + self.poll() + self.sleep() + finally: + self._event_stop.set() + + # --- external API + + def start(self): + if self.started: + raise RuntimeError("already started") + if self._stop_flag: + # ensure the thread can be restarted + super(ThreadWorker, self).__init__(self, self.poll_interval) + with self.lock: + self.before_start() + threading.Thread.start(self) + self._event_start.wait() + + def stop(self): + # TODO: we might want to specify a timeout arg for join. + if not self.stopped: + with self.lock: + self.before_stop() + self._stop_flag = True # signal the main loop to exit + self.stopped = True + # It is important to exit the lock context here otherwise + # we might hang indefinitively. + self.join() + self._event_stop.wait() + with self.lock: + self.after_stop() + + +class ThreadedTestFTPd(ThreadWorker): + """A threaded FTP server used for running tests. + + This is basically a modified version of the FTPServer class which + wraps the polling loop into a thread. + + The instance returned can be used to start(), stop() and + eventually re-start() the server. + """ + handler = FTPHandler + server_class = FTPServer + shutdown_after = 10 + poll_interval = 0.001 if TRAVIS else 0.000001 + + def __init__(self, addr=None): + super(ThreadedTestFTPd, self).__init__( + poll_interval=self.poll_interval) + self.addr = (HOST, 0) if addr is None else addr + authorizer = DummyAuthorizer() + authorizer.add_user(USER, PASSWD, HOME, perm='elradfmwM') # full perms + authorizer.add_anonymous(HOME) + self.handler.authorizer = authorizer + # lower buffer sizes = more "loops" while transfering data + # = less false positives + self.handler.dtp_handler.ac_in_buffer_size = 4096 + self.handler.dtp_handler.ac_out_buffer_size = 4096 + self.server = self.server_class(self.addr, self.handler) + self.host, self.port = self.server.socket.getsockname()[:2] + self.start_time = None + + def before_start(self): + self.start_time = time.time() + + def poll(self): + self.server.serve_forever(timeout=self.poll_interval, blocking=False) + if (self.shutdown_after and + time.time() >= self.start_time + self.shutdown_after): + now = time.time() + if now <= now + self.shutdown_after: + self.server.close_all() + raise Exception("test FTPd shutdown due to timeout") + + def after_stop(self): + self.server.close_all() diff --git a/ftp_server/pyftpdlib/test/keycert.pem b/ftp_server/pyftpdlib/test/keycert.pem new file mode 100644 index 00000000..2f46fcf1 --- /dev/null +++ b/ftp_server/pyftpdlib/test/keycert.pem @@ -0,0 +1,32 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L +opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH +fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB +AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU +D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA +IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM +oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0 +ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/ +loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j +oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA +z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq +ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV +q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD +VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x +IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT +U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1 +NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl +bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m +dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj +aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh +m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8 +M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn +fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC +AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb +08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx +CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/ +iHkC6gGdBJhogs4= +-----END CERTIFICATE----- diff --git a/ftp_server/pyftpdlib/test/runner.py b/ftp_server/pyftpdlib/test/runner.py new file mode 100644 index 00000000..3ae93767 --- /dev/null +++ b/ftp_server/pyftpdlib/test/runner.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import os +import sys + +from pyftpdlib.test import configure_logging +from pyftpdlib.test import remove_test_files +from pyftpdlib.test import unittest +from pyftpdlib.test import VERBOSITY + + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +def main(): + testmodules = [os.path.splitext(x)[0] for x in os.listdir(HERE) + if x.endswith('.py') and x.startswith('test_')] + configure_logging() + remove_test_files() + suite = unittest.TestSuite() + for t in testmodules: + # ...so that "make test" will print the full test paths + t = "pyftpdlib.test.%s" % t + suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t)) + result = unittest.TextTestRunner(verbosity=VERBOSITY).run(suite) + return result.wasSuccessful() + + +if __name__ == '__main__': + if not main(): + sys.exit(1) diff --git a/ftp_server/pyftpdlib/test/test_authorizers.py b/ftp_server/pyftpdlib/test/test_authorizers.py new file mode 100644 index 00000000..f40dd6d3 --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_authorizers.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import os +import random +import string +import sys +import tempfile +import warnings + +from pyftpdlib._compat import getcwdu +from pyftpdlib._compat import unicode +from pyftpdlib.authorizers import AuthenticationFailed +from pyftpdlib.authorizers import AuthorizerError +from pyftpdlib.authorizers import DummyAuthorizer +from pyftpdlib.test import HOME +from pyftpdlib.test import PASSWD +from pyftpdlib.test import POSIX +from pyftpdlib.test import TESTFN +from pyftpdlib.test import touch +from pyftpdlib.test import unittest +from pyftpdlib.test import USER +from pyftpdlib.test import VERBOSITY +from pyftpdlib.test import WINDOWS + +if POSIX: + import pwd + try: + from pyftpdlib.authorizers import UnixAuthorizer + except ImportError: + UnixAuthorizer = None +else: + UnixAuthorizer = None + +if WINDOWS: + from pywintypes import error as Win32ExtError + from pyftpdlib.authorizers import WindowsAuthorizer +else: + WindowsAuthorizer = None + + +class TestDummyAuthorizer(unittest.TestCase): + """Tests for DummyAuthorizer class.""" + + # temporarily change warnings to exceptions for the purposes of testing + def setUp(self): + self.tempdir = tempfile.mkdtemp(dir=HOME) + self.subtempdir = tempfile.mkdtemp( + dir=os.path.join(HOME, self.tempdir)) + self.tempfile = touch(os.path.join(self.tempdir, TESTFN)) + self.subtempfile = touch(os.path.join(self.subtempdir, TESTFN)) + warnings.filterwarnings("error") + + def tearDown(self): + os.remove(self.tempfile) + os.remove(self.subtempfile) + os.rmdir(self.subtempdir) + os.rmdir(self.tempdir) + warnings.resetwarnings() + + def test_common_methods(self): + auth = DummyAuthorizer() + # create user + auth.add_user(USER, PASSWD, HOME) + auth.add_anonymous(HOME) + # check credentials + auth.validate_authentication(USER, PASSWD, None) + self.assertRaises(AuthenticationFailed, + auth.validate_authentication, USER, 'wrongpwd', None) + auth.validate_authentication('anonymous', 'foo', None) + auth.validate_authentication('anonymous', '', None) # empty passwd + # remove them + auth.remove_user(USER) + auth.remove_user('anonymous') + # raise exc if user does not exists + self.assertRaises(KeyError, auth.remove_user, USER) + # raise exc if path does not exist + self.assertRaisesRegex(ValueError, + 'no such directory', + auth.add_user, USER, PASSWD, '?:\\') + self.assertRaisesRegex(ValueError, + 'no such directory', + auth.add_anonymous, '?:\\') + # raise exc if user already exists + auth.add_user(USER, PASSWD, HOME) + auth.add_anonymous(HOME) + self.assertRaisesRegex(ValueError, + 'user %r already exists' % USER, + auth.add_user, USER, PASSWD, HOME) + self.assertRaisesRegex(ValueError, + "user 'anonymous' already exists", + auth.add_anonymous, HOME) + auth.remove_user(USER) + auth.remove_user('anonymous') + # raise on wrong permission + self.assertRaisesRegex(ValueError, + "no such permission", + auth.add_user, USER, PASSWD, HOME, perm='?') + self.assertRaisesRegex(ValueError, + "no such permission", + auth.add_anonymous, HOME, perm='?') + # expect warning on write permissions assigned to anonymous user + for x in "adfmw": + self.assertRaisesRegex( + RuntimeWarning, + "write permissions assigned to anonymous user.", + auth.add_anonymous, HOME, perm=x) + + def test_override_perm_interface(self): + auth = DummyAuthorizer() + auth.add_user(USER, PASSWD, HOME, perm='elr') + # raise exc if user does not exists + self.assertRaises(KeyError, auth.override_perm, USER + 'w', + HOME, 'elr') + # raise exc if path does not exist or it's not a directory + self.assertRaisesRegex(ValueError, + 'no such directory', + auth.override_perm, USER, '?:\\', 'elr') + self.assertRaisesRegex(ValueError, + 'no such directory', + auth.override_perm, USER, self.tempfile, 'elr') + # raise on wrong permission + self.assertRaisesRegex(ValueError, + "no such permission", auth.override_perm, + USER, HOME, perm='?') + # expect warning on write permissions assigned to anonymous user + auth.add_anonymous(HOME) + for p in "adfmw": + self.assertRaisesRegex( + RuntimeWarning, + "write permissions assigned to anonymous user.", + auth.override_perm, 'anonymous', HOME, p) + # raise on attempt to override home directory permissions + self.assertRaisesRegex(ValueError, + "can't override home directory permissions", + auth.override_perm, USER, HOME, perm='w') + # raise on attempt to override a path escaping home directory + if os.path.dirname(HOME) != HOME: + self.assertRaisesRegex(ValueError, + "path escapes user home directory", + auth.override_perm, USER, + os.path.dirname(HOME), perm='w') + # try to re-set an overridden permission + auth.override_perm(USER, self.tempdir, perm='w') + auth.override_perm(USER, self.tempdir, perm='wr') + + def test_override_perm_recursive_paths(self): + auth = DummyAuthorizer() + auth.add_user(USER, PASSWD, HOME, perm='elr') + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), False) + auth.override_perm(USER, self.tempdir, perm='w', recursive=True) + self.assertEqual(auth.has_perm(USER, 'w', HOME), False) + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), True) + self.assertEqual(auth.has_perm(USER, 'w', self.tempfile), True) + self.assertEqual(auth.has_perm(USER, 'w', self.subtempdir), True) + self.assertEqual(auth.has_perm(USER, 'w', self.subtempfile), True) + + self.assertEqual(auth.has_perm(USER, 'w', HOME + '@'), False) + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir + '@'), False) + path = os.path.join(self.tempdir + '@', + os.path.basename(self.tempfile)) + self.assertEqual(auth.has_perm(USER, 'w', path), False) + # test case-sensitiveness + if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): + self.assertTrue(auth.has_perm(USER, 'w', self.tempdir.upper())) + + def test_override_perm_not_recursive_paths(self): + auth = DummyAuthorizer() + auth.add_user(USER, PASSWD, HOME, perm='elr') + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), False) + auth.override_perm(USER, self.tempdir, perm='w') + self.assertEqual(auth.has_perm(USER, 'w', HOME), False) + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir), True) + self.assertEqual(auth.has_perm(USER, 'w', self.tempfile), True) + self.assertEqual(auth.has_perm(USER, 'w', self.subtempdir), False) + self.assertEqual(auth.has_perm(USER, 'w', self.subtempfile), False) + + self.assertEqual(auth.has_perm(USER, 'w', HOME + '@'), False) + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir + '@'), False) + path = os.path.join(self.tempdir + '@', + os.path.basename(self.tempfile)) + self.assertEqual(auth.has_perm(USER, 'w', path), False) + # test case-sensitiveness + if (os.name in ('nt', 'ce')) or (sys.platform == 'cygwin'): + self.assertEqual(auth.has_perm(USER, 'w', self.tempdir.upper()), + True) + + +class _SharedAuthorizerTests(object): + """Tests valid for both UnixAuthorizer and WindowsAuthorizer for + those parts which share the same API. + """ + authorizer_class = None + # --- utils + + def get_users(self): + return self.authorizer_class._get_system_users() + + def get_current_user(self): + if POSIX: + return pwd.getpwuid(os.getuid()).pw_name + else: + return os.environ['USERNAME'] + + def get_current_user_homedir(self): + if POSIX: + return pwd.getpwuid(os.getuid()).pw_dir + else: + return os.environ['USERPROFILE'] + + def get_nonexistent_user(self): + # return a user which does not exist on the system + users = self.get_users() + letters = string.ascii_lowercase + while True: + user = ''.join([random.choice(letters) for i in range(10)]) + if user not in users: + return user + + def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): + try: + callableObj(*args, **kwargs) + except excClass as err: + if str(err) == msg: + return + raise self.failureException("%s != %s" % (str(err), msg)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException("%s not raised" % excName) + # --- /utils + + def test_get_home_dir(self): + auth = self.authorizer_class() + home = auth.get_home_dir(self.get_current_user()) + self.assertTrue(isinstance(home, unicode)) + nonexistent_user = self.get_nonexistent_user() + self.assertTrue(os.path.isdir(home)) + if auth.has_user('nobody'): + home = auth.get_home_dir('nobody') + self.assertRaises(AuthorizerError, + auth.get_home_dir, nonexistent_user) + + def test_has_user(self): + auth = self.authorizer_class() + current_user = self.get_current_user() + nonexistent_user = self.get_nonexistent_user() + self.assertTrue(auth.has_user(current_user)) + self.assertFalse(auth.has_user(nonexistent_user)) + auth = self.authorizer_class(rejected_users=[current_user]) + self.assertFalse(auth.has_user(current_user)) + + def test_validate_authentication(self): + # can't test for actual success in case of valid authentication + # here as we don't have the user password + if self.authorizer_class.__name__ == 'UnixAuthorizer': + auth = self.authorizer_class(require_valid_shell=False) + else: + auth = self.authorizer_class() + current_user = self.get_current_user() + nonexistent_user = self.get_nonexistent_user() + self.assertRaises( + AuthenticationFailed, + auth.validate_authentication, current_user, 'wrongpasswd', None) + self.assertRaises( + AuthenticationFailed, + auth.validate_authentication, nonexistent_user, 'bar', None) + + def test_impersonate_user(self): + auth = self.authorizer_class() + nonexistent_user = self.get_nonexistent_user() + try: + if self.authorizer_class.__name__ == 'UnixAuthorizer': + auth.impersonate_user(self.get_current_user(), '') + self.assertRaises( + AuthorizerError, + auth.impersonate_user, nonexistent_user, 'pwd') + else: + self.assertRaises( + Win32ExtError, + auth.impersonate_user, nonexistent_user, 'pwd') + self.assertRaises( + Win32ExtError, + auth.impersonate_user, self.get_current_user(), '') + finally: + auth.terminate_impersonation('') + + def test_terminate_impersonation(self): + auth = self.authorizer_class() + auth.terminate_impersonation('') + auth.terminate_impersonation('') + + def test_get_perms(self): + auth = self.authorizer_class(global_perm='elr') + self.assertTrue('r' in auth.get_perms(self.get_current_user())) + self.assertFalse('w' in auth.get_perms(self.get_current_user())) + + def test_has_perm(self): + auth = self.authorizer_class(global_perm='elr') + self.assertTrue(auth.has_perm(self.get_current_user(), 'r')) + self.assertFalse(auth.has_perm(self.get_current_user(), 'w')) + + def test_messages(self): + auth = self.authorizer_class(msg_login="login", msg_quit="quit") + self.assertTrue(auth.get_msg_login, "login") + self.assertTrue(auth.get_msg_quit, "quit") + + def test_error_options(self): + wrong_user = self.get_nonexistent_user() + self.assertRaisesWithMsg( + AuthorizerError, + "rejected_users and allowed_users options are mutually exclusive", + self.authorizer_class, allowed_users=['foo'], + rejected_users=['bar']) + self.assertRaisesWithMsg( + AuthorizerError, + 'invalid username "anonymous"', + self.authorizer_class, allowed_users=['anonymous']) + self.assertRaisesWithMsg( + AuthorizerError, + 'invalid username "anonymous"', + self.authorizer_class, rejected_users=['anonymous']) + self.assertRaisesWithMsg( + AuthorizerError, + 'unknown user %s' % wrong_user, + self.authorizer_class, allowed_users=[wrong_user]) + self.assertRaisesWithMsg(AuthorizerError, + 'unknown user %s' % wrong_user, + self.authorizer_class, + rejected_users=[wrong_user]) + + def test_override_user_password(self): + auth = self.authorizer_class() + user = self.get_current_user() + auth.override_user(user, password='foo') + auth.validate_authentication(user, 'foo', None) + self.assertRaises(AuthenticationFailed(auth.validate_authentication, + user, 'bar', None)) + # make sure other settings keep using default values + self.assertEqual(auth.get_home_dir(user), + self.get_current_user_homedir()) + self.assertEqual(auth.get_perms(user), "elradfmw") + self.assertEqual(auth.get_msg_login(user), "Login successful.") + self.assertEqual(auth.get_msg_quit(user), "Goodbye.") + + def test_override_user_homedir(self): + auth = self.authorizer_class() + user = self.get_current_user() + dir = os.path.dirname(getcwdu()) + auth.override_user(user, homedir=dir) + self.assertEqual(auth.get_home_dir(user), dir) + # make sure other settings keep using default values + # self.assertEqual(auth.get_home_dir(user), + # self.get_current_user_homedir()) + self.assertEqual(auth.get_perms(user), "elradfmw") + self.assertEqual(auth.get_msg_login(user), "Login successful.") + self.assertEqual(auth.get_msg_quit(user), "Goodbye.") + + def test_override_user_perm(self): + auth = self.authorizer_class() + user = self.get_current_user() + auth.override_user(user, perm="elr") + self.assertEqual(auth.get_perms(user), "elr") + # make sure other settings keep using default values + self.assertEqual(auth.get_home_dir(user), + self.get_current_user_homedir()) + # self.assertEqual(auth.get_perms(user), "elradfmw") + self.assertEqual(auth.get_msg_login(user), "Login successful.") + self.assertEqual(auth.get_msg_quit(user), "Goodbye.") + + def test_override_user_msg_login_quit(self): + auth = self.authorizer_class() + user = self.get_current_user() + auth.override_user(user, msg_login="foo", msg_quit="bar") + self.assertEqual(auth.get_msg_login(user), "foo") + self.assertEqual(auth.get_msg_quit(user), "bar") + # make sure other settings keep using default values + self.assertEqual(auth.get_home_dir(user), + self.get_current_user_homedir()) + self.assertEqual(auth.get_perms(user), "elradfmw") + # self.assertEqual(auth.get_msg_login(user), "Login successful.") + # self.assertEqual(auth.get_msg_quit(user), "Goodbye.") + + def test_override_user_errors(self): + if self.authorizer_class.__name__ == 'UnixAuthorizer': + auth = self.authorizer_class(require_valid_shell=False) + else: + auth = self.authorizer_class() + this_user = self.get_current_user() + for x in self.get_users(): + if x != this_user: + another_user = x + break + nonexistent_user = self.get_nonexistent_user() + self.assertRaisesWithMsg( + AuthorizerError, + "at least one keyword argument must be specified", + auth.override_user, this_user) + self.assertRaisesWithMsg(AuthorizerError, + 'no such user %s' % nonexistent_user, + auth.override_user, nonexistent_user, + perm='r') + if self.authorizer_class.__name__ == 'UnixAuthorizer': + auth = self.authorizer_class(allowed_users=[this_user], + require_valid_shell=False) + else: + auth = self.authorizer_class(allowed_users=[this_user]) + auth.override_user(this_user, perm='r') + self.assertRaisesWithMsg(AuthorizerError, + '%s is not an allowed user' % another_user, + auth.override_user, another_user, perm='r') + if self.authorizer_class.__name__ == 'UnixAuthorizer': + auth = self.authorizer_class(rejected_users=[this_user], + require_valid_shell=False) + else: + auth = self.authorizer_class(rejected_users=[this_user]) + auth.override_user(another_user, perm='r') + self.assertRaisesWithMsg(AuthorizerError, + '%s is not an allowed user' % this_user, + auth.override_user, this_user, perm='r') + self.assertRaisesWithMsg(AuthorizerError, + "can't assign password to anonymous user", + auth.override_user, "anonymous", + password='foo') + + +# ===================================================================== +# --- UNIX authorizer +# ===================================================================== + + +@unittest.skipUnless(POSIX, "UNIX only") +@unittest.skipUnless(UnixAuthorizer is not None, + "UnixAuthorizer class not available") +class TestUnixAuthorizer(_SharedAuthorizerTests, unittest.TestCase): + """Unix authorizer specific tests.""" + + authorizer_class = UnixAuthorizer + + def setUp(self): + try: + UnixAuthorizer() + except AuthorizerError: # not root + self.skipTest("need root access") + + def test_get_perms_anonymous(self): + auth = UnixAuthorizer( + global_perm='elr', anonymous_user=self.get_current_user()) + self.assertTrue('e' in auth.get_perms('anonymous')) + self.assertFalse('w' in auth.get_perms('anonymous')) + warnings.filterwarnings("ignore") + auth.override_user('anonymous', perm='w') + warnings.resetwarnings() + self.assertTrue('w' in auth.get_perms('anonymous')) + + def test_has_perm_anonymous(self): + auth = UnixAuthorizer( + global_perm='elr', anonymous_user=self.get_current_user()) + self.assertTrue(auth.has_perm(self.get_current_user(), 'r')) + self.assertFalse(auth.has_perm(self.get_current_user(), 'w')) + self.assertTrue(auth.has_perm('anonymous', 'e')) + self.assertFalse(auth.has_perm('anonymous', 'w')) + warnings.filterwarnings("ignore") + auth.override_user('anonymous', perm='w') + warnings.resetwarnings() + self.assertTrue(auth.has_perm('anonymous', 'w')) + + def test_validate_authentication(self): + # we can only test for invalid credentials + auth = UnixAuthorizer(require_valid_shell=False) + self.assertRaises(AuthenticationFailed, + auth.validate_authentication, '?!foo', '?!foo', None) + auth = UnixAuthorizer(require_valid_shell=True) + self.assertRaises(AuthenticationFailed, + auth.validate_authentication, '?!foo', '?!foo', None) + + def test_validate_authentication_anonymous(self): + current_user = self.get_current_user() + auth = UnixAuthorizer(anonymous_user=current_user, + require_valid_shell=False) + self.assertRaises(AuthenticationFailed, + auth.validate_authentication, 'foo', 'passwd', None) + self.assertRaises( + AuthenticationFailed, + auth.validate_authentication, current_user, 'passwd', None) + auth.validate_authentication('anonymous', 'passwd', None) + + def test_require_valid_shell(self): + + def get_fake_shell_user(): + for user in self.get_users(): + shell = pwd.getpwnam(user).pw_shell + # On linux fake shell is usually /bin/false, on + # freebsd /usr/sbin/nologin; in case of other + # UNIX variants test needs to be adjusted. + if '/false' in shell or '/nologin' in shell: + return user + self.fail("no user found") + + user = get_fake_shell_user() + self.assertRaisesWithMsg( + AuthorizerError, + "user %s has not a valid shell" % user, + UnixAuthorizer, allowed_users=[user]) + # commented as it first fails for invalid home + # self.assertRaisesWithMsg( + # ValueError, + # "user %s has not a valid shell" % user, + # UnixAuthorizer, anonymous_user=user) + auth = UnixAuthorizer() + self.assertTrue(auth._has_valid_shell(self.get_current_user())) + self.assertFalse(auth._has_valid_shell(user)) + self.assertRaisesWithMsg(AuthorizerError, + "User %s doesn't have a valid shell." % user, + auth.override_user, user, perm='r') + + def test_not_root(self): + # UnixAuthorizer is supposed to work only as super user + auth = self.authorizer_class() + try: + auth.impersonate_user('nobody', '') + self.assertRaisesWithMsg(AuthorizerError, + "super user privileges are required", + UnixAuthorizer) + finally: + auth.terminate_impersonation('nobody') + + +# ===================================================================== +# --- Windows authorizer +# ===================================================================== + + +@unittest.skipUnless(WINDOWS, "Windows only") +class TestWindowsAuthorizer(_SharedAuthorizerTests, unittest.TestCase): + """Windows authorizer specific tests.""" + + authorizer_class = WindowsAuthorizer + + def test_wrong_anonymous_credentials(self): + user = self.get_current_user() + self.assertRaises(Win32ExtError, self.authorizer_class, + anonymous_user=user, + anonymous_password='$|1wrongpasswd') + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_filesystems.py b/ftp_server/pyftpdlib/test/test_filesystems.py new file mode 100644 index 00000000..d5ee1a08 --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_filesystems.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import os +import tempfile + +from pyftpdlib._compat import getcwdu +from pyftpdlib._compat import u +from pyftpdlib.filesystems import AbstractedFS +from pyftpdlib.test import HOME +from pyftpdlib.test import POSIX +from pyftpdlib.test import safe_remove +from pyftpdlib.test import TESTFN +from pyftpdlib.test import touch +from pyftpdlib.test import unittest +from pyftpdlib.test import VERBOSITY + +if POSIX: + from pyftpdlib.filesystems import UnixFilesystem + + +class TestAbstractedFS(unittest.TestCase): + """Test for conversion utility methods of AbstractedFS class.""" + + def setUp(self): + safe_remove(TESTFN) + + tearDown = setUp + + def test_ftpnorm(self): + # Tests for ftpnorm method. + ae = self.assertEqual + fs = AbstractedFS(u('/'), None) + + fs._cwd = u('/') + ae(fs.ftpnorm(u('')), u('/')) + ae(fs.ftpnorm(u('/')), u('/')) + ae(fs.ftpnorm(u('.')), u('/')) + ae(fs.ftpnorm(u('..')), u('/')) + ae(fs.ftpnorm(u('a')), u('/a')) + ae(fs.ftpnorm(u('/a')), u('/a')) + ae(fs.ftpnorm(u('/a/')), u('/a')) + ae(fs.ftpnorm(u('a/..')), u('/')) + ae(fs.ftpnorm(u('a/b')), '/a/b') + ae(fs.ftpnorm(u('a/b/..')), u('/a')) + ae(fs.ftpnorm(u('a/b/../..')), u('/')) + fs._cwd = u('/sub') + ae(fs.ftpnorm(u('')), u('/sub')) + ae(fs.ftpnorm(u('/')), u('/')) + ae(fs.ftpnorm(u('.')), u('/sub')) + ae(fs.ftpnorm(u('..')), u('/')) + ae(fs.ftpnorm(u('a')), u('/sub/a')) + ae(fs.ftpnorm(u('a/')), u('/sub/a')) + ae(fs.ftpnorm(u('a/..')), u('/sub')) + ae(fs.ftpnorm(u('a/b')), u('/sub/a/b')) + ae(fs.ftpnorm(u('a/b/')), u('/sub/a/b')) + ae(fs.ftpnorm(u('a/b/..')), u('/sub/a')) + ae(fs.ftpnorm(u('a/b/../..')), u('/sub')) + ae(fs.ftpnorm(u('a/b/../../..')), u('/')) + ae(fs.ftpnorm(u('//')), u('/')) # UNC paths must be collapsed + + def test_ftp2fs(self): + # Tests for ftp2fs method. + def join(x, y): + return os.path.join(x, y.replace('/', os.sep)) + + ae = self.assertEqual + fs = AbstractedFS(u('/'), None) + + def goforit(root): + fs._root = root + fs._cwd = u('/') + ae(fs.ftp2fs(u('')), root) + ae(fs.ftp2fs(u('/')), root) + ae(fs.ftp2fs(u('.')), root) + ae(fs.ftp2fs(u('..')), root) + ae(fs.ftp2fs(u('a')), join(root, u('a'))) + ae(fs.ftp2fs(u('/a')), join(root, u('a'))) + ae(fs.ftp2fs(u('/a/')), join(root, u('a'))) + ae(fs.ftp2fs(u('a/..')), root) + ae(fs.ftp2fs(u('a/b')), join(root, u(r'a/b'))) + ae(fs.ftp2fs(u('/a/b')), join(root, u(r'a/b'))) + ae(fs.ftp2fs(u('/a/b/..')), join(root, u('a'))) + ae(fs.ftp2fs(u('/a/b/../..')), root) + fs._cwd = u('/sub') + ae(fs.ftp2fs(u('')), join(root, u('sub'))) + ae(fs.ftp2fs(u('/')), root) + ae(fs.ftp2fs(u('.')), join(root, u('sub'))) + ae(fs.ftp2fs(u('..')), root) + ae(fs.ftp2fs(u('a')), join(root, u('sub/a'))) + ae(fs.ftp2fs(u('a/')), join(root, u('sub/a'))) + ae(fs.ftp2fs(u('a/..')), join(root, u('sub'))) + ae(fs.ftp2fs(u('a/b')), join(root, 'sub/a/b')) + ae(fs.ftp2fs(u('a/b/..')), join(root, u('sub/a'))) + ae(fs.ftp2fs(u('a/b/../..')), join(root, u('sub'))) + ae(fs.ftp2fs(u('a/b/../../..')), root) + # UNC paths must be collapsed + ae(fs.ftp2fs(u('//a')), join(root, u('a'))) + + if os.sep == '\\': + goforit(u(r'C:\dir')) + goforit(u('C:\\')) + # on DOS-derived filesystems (e.g. Windows) this is the same + # as specifying the current drive directory (e.g. 'C:\\') + goforit(u('\\')) + elif os.sep == '/': + goforit(u('/home/user')) + goforit(u('/')) + else: + # os.sep == ':'? Don't know... let's try it anyway + goforit(getcwdu()) + + def test_fs2ftp(self): + # Tests for fs2ftp method. + def join(x, y): + return os.path.join(x, y.replace('/', os.sep)) + + ae = self.assertEqual + fs = AbstractedFS(u('/'), None) + + def goforit(root): + fs._root = root + ae(fs.fs2ftp(root), u('/')) + ae(fs.fs2ftp(join(root, u('/'))), u('/')) + ae(fs.fs2ftp(join(root, u('.'))), u('/')) + # can't escape from root + ae(fs.fs2ftp(join(root, u('..'))), u('/')) + ae(fs.fs2ftp(join(root, u('a'))), u('/a')) + ae(fs.fs2ftp(join(root, u('a/'))), u('/a')) + ae(fs.fs2ftp(join(root, u('a/..'))), u('/')) + ae(fs.fs2ftp(join(root, u('a/b'))), u('/a/b')) + ae(fs.fs2ftp(join(root, u('a/b'))), u('/a/b')) + ae(fs.fs2ftp(join(root, u('a/b/..'))), u('/a')) + ae(fs.fs2ftp(join(root, u('/a/b/../..'))), u('/')) + fs._cwd = u('/sub') + ae(fs.fs2ftp(join(root, 'a/')), u('/a')) + + if os.sep == '\\': + goforit(u(r'C:\dir')) + goforit(u('C:\\')) + # on DOS-derived filesystems (e.g. Windows) this is the same + # as specifying the current drive directory (e.g. 'C:\\') + goforit(u('\\')) + fs._root = u(r'C:\dir') + ae(fs.fs2ftp(u('C:\\')), u('/')) + ae(fs.fs2ftp(u('D:\\')), u('/')) + ae(fs.fs2ftp(u('D:\\dir')), u('/')) + elif os.sep == '/': + goforit(u('/')) + if os.path.realpath('/__home/user') != '/__home/user': + self.fail('Test skipped (symlinks not allowed).') + goforit(u('/__home/user')) + fs._root = u('/__home/user') + ae(fs.fs2ftp(u('/__home')), u('/')) + ae(fs.fs2ftp(u('/')), u('/')) + ae(fs.fs2ftp(u('/__home/userx')), u('/')) + else: + # os.sep == ':'? Don't know... let's try it anyway + goforit(getcwdu()) + + def test_validpath(self): + # Tests for validpath method. + fs = AbstractedFS(u('/'), None) + fs._root = HOME + self.assertTrue(fs.validpath(HOME)) + self.assertTrue(fs.validpath(HOME + '/')) + self.assertFalse(fs.validpath(HOME + 'bar')) + + if hasattr(os, 'symlink'): + + def test_validpath_validlink(self): + # Test validpath by issuing a symlink pointing to a path + # inside the root directory. + fs = AbstractedFS(u('/'), None) + fs._root = HOME + TESTFN2 = TESTFN + '1' + try: + touch(TESTFN) + os.symlink(TESTFN, TESTFN2) + self.assertTrue(fs.validpath(u(TESTFN))) + finally: + safe_remove(TESTFN, TESTFN2) + + def test_validpath_external_symlink(self): + # Test validpath by issuing a symlink pointing to a path + # outside the root directory. + fs = AbstractedFS(u('/'), None) + fs._root = HOME + # tempfile should create our file in /tmp directory + # which should be outside the user root. If it is + # not we just skip the test. + with tempfile.NamedTemporaryFile() as file: + try: + if HOME == os.path.dirname(file.name): + return + os.symlink(file.name, TESTFN) + self.assertFalse(fs.validpath(u(TESTFN))) + finally: + safe_remove(TESTFN) + + +@unittest.skipUnless(POSIX, "UNIX only") +class TestUnixFilesystem(unittest.TestCase): + + def test_case(self): + root = getcwdu() + fs = UnixFilesystem(root, None) + self.assertEqual(fs.root, root) + self.assertEqual(fs.cwd, root) + cdup = os.path.dirname(root) + self.assertEqual(fs.ftp2fs(u('..')), cdup) + self.assertEqual(fs.fs2ftp(root), root) + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_functional.py b/ftp_server/pyftpdlib/test/test_functional.py new file mode 100644 index 00000000..496cf93c --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_functional.py @@ -0,0 +1,2851 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import contextlib +import errno +import ftplib +import logging +import os +import random +import re +import select +import shutil +import socket +import stat +import sys +import tempfile +import time +import warnings + +from pyftpdlib._compat import b +from pyftpdlib._compat import PY3 +from pyftpdlib._compat import u +from pyftpdlib._compat import unicode +from pyftpdlib.filesystems import AbstractedFS +from pyftpdlib.handlers import DTPHandler +from pyftpdlib.handlers import FTPHandler +from pyftpdlib.handlers import SUPPORTS_HYBRID_IPV6 +from pyftpdlib.handlers import ThrottledDTPHandler +from pyftpdlib.ioloop import IOLoop +from pyftpdlib.servers import FTPServer +from pyftpdlib.test import BUFSIZE +from pyftpdlib.test import call_until +from pyftpdlib.test import configure_logging +from pyftpdlib.test import disable_log_warning +from pyftpdlib.test import get_server_handler +from pyftpdlib.test import HOME +from pyftpdlib.test import HOST +from pyftpdlib.test import INTERRUPTED_TRANSF_SIZE +from pyftpdlib.test import mock +from pyftpdlib.test import OSX +from pyftpdlib.test import PASSWD +from pyftpdlib.test import POSIX +from pyftpdlib.test import remove_test_files +from pyftpdlib.test import retry_on_failure +from pyftpdlib.test import safe_mkdir +from pyftpdlib.test import safe_remove +from pyftpdlib.test import safe_rmdir +from pyftpdlib.test import SUPPORTS_IPV4 +from pyftpdlib.test import SUPPORTS_IPV6 +from pyftpdlib.test import SUPPORTS_SENDFILE +from pyftpdlib.test import TESTFN +from pyftpdlib.test import TESTFN_UNICODE +from pyftpdlib.test import TESTFN_UNICODE_2 +from pyftpdlib.test import ThreadedTestFTPd +from pyftpdlib.test import TIMEOUT +from pyftpdlib.test import touch +from pyftpdlib.test import TRAVIS +from pyftpdlib.test import unittest +from pyftpdlib.test import USER +from pyftpdlib.test import VERBOSITY +from pyftpdlib.test import WINDOWS +import pyftpdlib.__main__ + +try: + from StringIO import StringIO as BytesIO +except ImportError: + from io import BytesIO +try: + import ssl +except ImportError: + ssl = None + +sendfile = None +if POSIX: + try: + import sendfile + except ImportError: + pass + + +class TestFtpAuthentication(unittest.TestCase): + + "test: USER, PASS, REIN." + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + with self.server.lock: + self.server.handler.auth_failed_timeout = 0.001 + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.file = open(TESTFN, 'w+b') + self.dummyfile = BytesIO() + + def tearDown(self): + with self.server.lock: + self.server.handler.auth_failed_timeout = 5 + self.client.close() + self.server.stop() + if not self.file.closed: + self.file.close() + if not self.dummyfile.closed: + self.dummyfile.close() + os.remove(TESTFN) + + def assert_auth_failed(self, user, passwd): + self.assertRaisesRegex(ftplib.error_perm, '530 Authentication failed', + self.client.login, user, passwd) + + def test_auth_ok(self): + self.client.login(user=USER, passwd=PASSWD) + + def test_anon_auth(self): + self.client.login(user='anonymous', passwd='anon@') + self.client.login(user='anonymous', passwd='') + # supposed to be case sensitive + self.assert_auth_failed('AnoNymouS', 'foo') + # empty passwords should be allowed + self.client.sendcmd('user anonymous') + self.client.sendcmd('pass ') + self.client.sendcmd('user anonymous') + self.client.sendcmd('pass') + + def test_auth_failed(self): + self.assert_auth_failed(USER, 'wrong') + self.assert_auth_failed('wrong', PASSWD) + self.assert_auth_failed('wrong', 'wrong') + + def test_wrong_cmds_order(self): + self.assertRaisesRegex(ftplib.error_perm, '503 Login with USER first', + self.client.sendcmd, 'pass ' + PASSWD) + self.client.login(user=USER, passwd=PASSWD) + self.assertRaisesRegex(ftplib.error_perm, + "503 User already authenticated.", + self.client.sendcmd, 'pass ' + PASSWD) + + def test_max_auth(self): + self.assert_auth_failed(USER, 'wrong') + self.assert_auth_failed(USER, 'wrong') + self.assert_auth_failed(USER, 'wrong') + # If authentication fails for 3 times ftpd disconnects the + # client. We can check if that happens by using self.client.sendcmd() + # on the 'dead' socket object. If socket object is really + # closed it should be raised a socket.error exception (Windows) + # or a EOFError exception (Linux). + self.client.sock.settimeout(.1) + self.assertRaises((socket.error, EOFError), self.client.sendcmd, '') + + def test_rein(self): + self.client.login(user=USER, passwd=PASSWD) + self.client.sendcmd('rein') + # user not authenticated, error response expected + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.sendcmd, 'pwd') + # by logging-in again we should be able to execute a + # file-system command + self.client.login(user=USER, passwd=PASSWD) + self.client.sendcmd('pwd') + + @retry_on_failure() + def test_rein_during_transfer(self): + # Test REIN while already authenticated and a transfer is + # in progress. + self.client.login(user=USER, passwd=PASSWD) + data = b'abcde12345' * 1000000 + self.file.write(data) + self.file.close() + + conn = self.client.transfercmd('retr ' + TESTFN) + self.addCleanup(conn.close) + rein_sent = False + bytes_recv = 0 + while True: + chunk = conn.recv(BUFSIZE) + if not chunk: + break + bytes_recv += len(chunk) + self.dummyfile.write(chunk) + if bytes_recv > INTERRUPTED_TRANSF_SIZE and not rein_sent: + rein_sent = True + # flush account, error response expected + self.client.sendcmd('rein') + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.dir) + + # a 226 response is expected once tranfer finishes + self.assertEqual(self.client.voidresp()[:3], '226') + # account is still flushed, error response is still expected + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.sendcmd, 'size ' + TESTFN) + # by logging-in again we should be able to execute a + # filesystem command + self.client.login(user=USER, passwd=PASSWD) + self.client.sendcmd('pwd') + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + + def test_user(self): + # Test USER while already authenticated and no transfer + # is in progress. + self.client.login(user=USER, passwd=PASSWD) + self.client.sendcmd('user ' + USER) # authentication flushed + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.sendcmd, 'pwd') + self.client.sendcmd('pass ' + PASSWD) + self.client.sendcmd('pwd') + + def test_user_during_transfer(self): + # Test USER while already authenticated and a transfer is + # in progress. + self.client.login(user=USER, passwd=PASSWD) + data = b'abcde12345' * 1000000 + self.file.write(data) + self.file.close() + + conn = self.client.transfercmd('retr ' + TESTFN) + self.addCleanup(conn.close) + rein_sent = 0 + bytes_recv = 0 + while True: + chunk = conn.recv(BUFSIZE) + if not chunk: + break + bytes_recv += len(chunk) + self.dummyfile.write(chunk) + # stop transfer while it isn't finished yet + if bytes_recv > INTERRUPTED_TRANSF_SIZE and not rein_sent: + rein_sent = True + # flush account, expect an error response + self.client.sendcmd('user ' + USER) + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.dir) + + # a 226 response is expected once transfer finishes + self.assertEqual(self.client.voidresp()[:3], '226') + # account is still flushed, error response is still expected + self.assertRaisesRegex(ftplib.error_perm, + '530 Log in with USER and PASS first', + self.client.sendcmd, 'pwd') + # by logging-in again we should be able to execute a + # filesystem command + self.client.sendcmd('pass ' + PASSWD) + self.client.sendcmd('pwd') + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + + +class TestFtpDummyCmds(unittest.TestCase): + "test: TYPE, STRU, MODE, NOOP, SYST, ALLO, HELP, SITE HELP" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + self.client.close() + self.server.stop() + + def test_type(self): + self.client.sendcmd('type a') + self.client.sendcmd('type i') + self.client.sendcmd('type l7') + self.client.sendcmd('type l8') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'type ?!?') + + def test_stru(self): + self.client.sendcmd('stru f') + self.client.sendcmd('stru F') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru p') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru r') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'stru ?!?') + + def test_mode(self): + self.client.sendcmd('mode s') + self.client.sendcmd('mode S') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode b') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode c') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'mode ?!?') + + def test_noop(self): + self.client.sendcmd('noop') + + def test_syst(self): + self.client.sendcmd('syst') + + def test_allo(self): + self.client.sendcmd('allo x') + + def test_quit(self): + self.client.sendcmd('quit') + + def test_help(self): + self.client.sendcmd('help') + cmd = random.choice(list(FTPHandler.proto_cmds.keys())) + self.client.sendcmd('help %s' % cmd) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'help ?!?') + + def test_site(self): + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'site ?!?') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site foo bar') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'sitefoo bar') + + def test_site_help(self): + self.client.sendcmd('site help') + self.client.sendcmd('site help help') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site help ?!?') + + def test_rest(self): + # Test error conditions only; resumed data transfers are + # tested later. + self.client.sendcmd('type i') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest str') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest -1') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'rest 10.1') + # REST is not supposed to be allowed in ASCII mode + self.client.sendcmd('type a') + self.assertRaisesRegex(ftplib.error_perm, 'not allowed in ASCII mode', + self.client.sendcmd, 'rest 10') + + def test_feat(self): + resp = self.client.sendcmd('feat') + self.assertTrue('UTF8' in resp) + self.assertTrue('TVFS' in resp) + + def test_opts_feat(self): + self.assertRaises( + ftplib.error_perm, self.client.sendcmd, 'opts mlst bad_fact') + self.assertRaises( + ftplib.error_perm, self.client.sendcmd, 'opts mlst type ;') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'opts not_mlst') + # utility function which used for extracting the MLST "facts" + # string from the FEAT response + + def mlst(): + resp = self.client.sendcmd('feat') + return re.search(r'^\s*MLST\s+(\S+)$', resp, re.MULTILINE).group(1) + # we rely on "type", "perm", "size", and "modify" facts which + # are those available on all platforms + self.assertTrue('type*;perm*;size*;modify*;' in mlst()) + self.assertEqual(self.client.sendcmd( + 'opts mlst type;'), '200 MLST OPTS type;') + self.assertEqual(self.client.sendcmd( + 'opts mLSt TypE;'), '200 MLST OPTS type;') + self.assertTrue('type*;perm;size;modify;' in mlst()) + + self.assertEqual(self.client.sendcmd('opts mlst'), '200 MLST OPTS ') + self.assertTrue('*' not in mlst()) + + self.assertEqual( + self.client.sendcmd('opts mlst fish;cakes;'), '200 MLST OPTS ') + self.assertTrue('*' not in mlst()) + self.assertEqual(self.client.sendcmd('opts mlst fish;cakes;type;'), + '200 MLST OPTS type;') + self.assertTrue('type*;perm;size;modify;' in mlst()) + + +class TestFtpCmdsSemantic(unittest.TestCase): + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + arg_cmds = ['allo', 'appe', 'dele', 'eprt', 'mdtm', 'mode', 'mkd', 'opts', + 'port', 'rest', 'retr', 'rmd', 'rnfr', 'rnto', 'site', 'size', + 'stor', 'stru', 'type', 'user', 'xmkd', 'xrmd', 'site chmod'] + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + self.client.close() + self.server.stop() + + def test_arg_cmds(self): + # Test commands requiring an argument. + expected = "501 Syntax error: command needs an argument." + for cmd in self.arg_cmds: + self.client.putcmd(cmd) + resp = self.client.getmultiline() + self.assertEqual(resp, expected) + + def test_no_arg_cmds(self): + # Test commands accepting no arguments. + expected = "501 Syntax error: command does not accept arguments." + narg_cmds = ['abor', 'cdup', 'feat', 'noop', 'pasv', 'pwd', 'quit', + 'rein', 'syst', 'xcup', 'xpwd'] + for cmd in narg_cmds: + self.client.putcmd(cmd + ' arg') + resp = self.client.getmultiline() + self.assertEqual(resp, expected) + + def test_auth_cmds(self): + # Test those commands requiring client to be authenticated. + expected = "530 Log in with USER and PASS first." + self.client.sendcmd('rein') + for cmd in self.server.handler.proto_cmds: + cmd = cmd.lower() + if cmd in ('feat', 'help', 'noop', 'user', 'pass', 'stat', 'syst', + 'quit', 'site', 'site help', 'pbsz', 'auth', 'prot', + 'ccc'): + continue + if cmd in self.arg_cmds: + cmd = cmd + ' arg' + self.client.putcmd(cmd) + resp = self.client.getmultiline() + self.assertEqual(resp, expected) + + def test_no_auth_cmds(self): + # Test those commands that do not require client to be authenticated. + self.client.sendcmd('rein') + for cmd in ('feat', 'help', 'noop', 'stat', 'syst', 'site help'): + self.client.sendcmd(cmd) + # STAT provided with an argument is equal to LIST hence not allowed + # if not authenticated + self.assertRaisesRegex(ftplib.error_perm, '530 Log in with USER', + self.client.sendcmd, 'stat /') + self.client.sendcmd('quit') + + +class TestFtpFsOperations(unittest.TestCase): + + "test: PWD, CWD, CDUP, SIZE, RNFR, RNTO, DELE, MKD, RMD, MDTM, STAT" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.tempfile = os.path.basename(touch(TESTFN)) + self.tempdir = os.path.basename(tempfile.mkdtemp(dir=HOME)) + + def tearDown(self): + self.client.close() + self.server.stop() + safe_remove(self.tempfile) + if os.path.exists(self.tempdir): + shutil.rmtree(self.tempdir) + + def test_cwd(self): + self.client.cwd(self.tempdir) + self.assertEqual(self.client.pwd(), '/' + self.tempdir) + self.assertRaises(ftplib.error_perm, self.client.cwd, 'subtempdir') + # cwd provided with no arguments is supposed to move us to the + # root directory + self.client.sendcmd('cwd') + self.assertEqual(self.client.pwd(), u('/')) + + def test_pwd(self): + self.assertEqual(self.client.pwd(), u('/')) + self.client.cwd(self.tempdir) + self.assertEqual(self.client.pwd(), '/' + self.tempdir) + + def test_cdup(self): + subfolder = os.path.basename(tempfile.mkdtemp(dir=self.tempdir)) + self.assertEqual(self.client.pwd(), u('/')) + self.client.cwd(self.tempdir) + self.assertEqual(self.client.pwd(), '/%s' % self.tempdir) + self.client.cwd(subfolder) + self.assertEqual(self.client.pwd(), + '/%s/%s' % (self.tempdir, subfolder)) + self.client.sendcmd('cdup') + self.assertEqual(self.client.pwd(), '/%s' % self.tempdir) + self.client.sendcmd('cdup') + self.assertEqual(self.client.pwd(), u('/')) + + # make sure we can't escape from root directory + self.client.sendcmd('cdup') + self.assertEqual(self.client.pwd(), u('/')) + + def test_mkd(self): + tempdir = os.path.basename(tempfile.mktemp(dir=HOME)) + dirname = self.client.mkd(tempdir) + # the 257 response is supposed to include the absolute dirname + self.assertEqual(dirname, '/' + tempdir) + # make sure we can't create directories which already exist + # (probably not really necessary); + # let's use a try/except statement to avoid leaving behind + # orphaned temporary directory in the event of a test failure. + try: + self.client.mkd(tempdir) + except ftplib.error_perm: + os.rmdir(tempdir) # ok + else: + self.fail('ftplib.error_perm not raised.') + + def test_rmd(self): + self.client.rmd(self.tempdir) + self.assertRaises(ftplib.error_perm, self.client.rmd, self.tempfile) + # make sure we can't remove the root directory + self.assertRaisesRegex(ftplib.error_perm, + "Can't remove root directory", + self.client.rmd, u('/')) + + def test_dele(self): + self.client.delete(self.tempfile) + self.assertRaises(ftplib.error_perm, self.client.delete, self.tempdir) + + def test_rnfr_rnto(self): + # rename file + tempname = os.path.basename(tempfile.mktemp(dir=HOME)) + self.client.rename(self.tempfile, tempname) + self.client.rename(tempname, self.tempfile) + # rename dir + tempname = os.path.basename(tempfile.mktemp(dir=HOME)) + self.client.rename(self.tempdir, tempname) + self.client.rename(tempname, self.tempdir) + # rnfr/rnto over non-existing paths + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.rename, bogus, '/x') + self.assertRaises( + ftplib.error_perm, self.client.rename, self.tempfile, u('/')) + # rnto sent without first specifying the source + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'rnto ' + self.tempfile) + + # make sure we can't rename root directory + self.assertRaisesRegex(ftplib.error_perm, + "Can't rename home directory", + self.client.rename, '/', '/x') + + def test_mdtm(self): + self.client.sendcmd('mdtm ' + self.tempfile) + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'mdtm ' + bogus) + # make sure we can't use mdtm against directories + try: + self.client.sendcmd('mdtm ' + self.tempdir) + except ftplib.error_perm as err: + self.assertTrue("not retrievable" in str(err)) + else: + self.fail('Exception not raised') + + def test_unforeseen_mdtm_event(self): + # Emulate a case where the file last modification time is prior + # to year 1900. This most likely will never happen unless + # someone specifically force the last modification time of a + # file in some way. + # To do so we temporarily override os.path.getmtime so that it + # returns a negative value referring to a year prior to 1900. + # It causes time.localtime/gmtime to raise a ValueError exception + # which is supposed to be handled by server. + + # On python 3 it seems that the trick of replacing the original + # method with the lambda doesn't work. + if not PY3: + _getmtime = AbstractedFS.getmtime + try: + AbstractedFS.getmtime = lambda x, y: -9000000000 + self.assertRaisesRegex( + ftplib.error_perm, + "550 Can't determine file's last modification time", + self.client.sendcmd, 'mdtm ' + self.tempfile) + # make sure client hasn't been disconnected + self.client.sendcmd('noop') + finally: + AbstractedFS.getmtime = _getmtime + + def test_size(self): + self.client.sendcmd('type a') + self.assertRaises(ftplib.error_perm, self.client.size, self.tempfile) + self.client.sendcmd('type i') + self.client.size(self.tempfile) + # make sure we can't use size against directories + try: + self.client.sendcmd('size ' + self.tempdir) + except ftplib.error_perm as err: + self.assertTrue("not retrievable" in str(err)) + else: + self.fail('Exception not raised') + + if not hasattr(os, 'chmod'): + def test_site_chmod(self): + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site chmod 777 ' + self.tempfile) + else: + def test_site_chmod(self): + # not enough args + self.assertRaises(ftplib.error_perm, + self.client.sendcmd, 'site chmod 777') + # bad args + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site chmod -177 ' + self.tempfile) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site chmod 778 ' + self.tempfile) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site chmod foo ' + self.tempfile) + + def getmode(): + mode = oct(stat.S_IMODE(os.stat(self.tempfile).st_mode)) + if PY3: + mode = mode.replace('o', '') + return mode + + # on Windows it is possible to set read-only flag only + if WINDOWS: + self.client.sendcmd('site chmod 777 ' + self.tempfile) + self.assertEqual(getmode(), '0666') + self.client.sendcmd('site chmod 444 ' + self.tempfile) + self.assertEqual(getmode(), '0444') + self.client.sendcmd('site chmod 666 ' + self.tempfile) + self.assertEqual(getmode(), '0666') + else: + self.client.sendcmd('site chmod 777 ' + self.tempfile) + self.assertEqual(getmode(), '0777') + self.client.sendcmd('site chmod 755 ' + self.tempfile) + self.assertEqual(getmode(), '0755') + self.client.sendcmd('site chmod 555 ' + self.tempfile) + self.assertEqual(getmode(), '0555') + + +class TestFtpStoreData(unittest.TestCase): + """Test STOR, STOU, APPE, REST, TYPE.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.dummy_recvfile = BytesIO() + self.dummy_sendfile = BytesIO() + + def tearDown(self): + self.client.close() + self.server.stop() + self.dummy_recvfile.close() + self.dummy_sendfile.close() + safe_remove(TESTFN) + + def test_stor(self): + try: + data = b'abcde12345' * 100000 + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) + self.dummy_recvfile.seek(0) + datafile = self.dummy_recvfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(TESTFN): + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + def test_stor_active(self): + # Like test_stor but using PORT + self.client.set_pasv(False) + self.test_stor() + + def test_stor_ascii(self): + # Test STOR in ASCII mode + + def store(cmd, fp, blocksize=8192): + # like storbinary() except it sends "type a" instead of + # "type i" before starting the transfer + self.client.voidcmd('type a') + with contextlib.closing(self.client.transfercmd(cmd)) as conn: + while True: + buf = fp.read(blocksize) + if not buf: + break + conn.sendall(buf) + return self.client.voidresp() + + try: + data = b'abcde12345\r\n' * 100000 + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + store('stor ' + TESTFN, self.dummy_sendfile) + self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) + expected = data.replace(b'\r\n', b(os.linesep)) + self.dummy_recvfile.seek(0) + datafile = self.dummy_recvfile.read() + self.assertEqual(len(expected), len(datafile)) + self.assertEqual(hash(expected), hash(datafile)) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(TESTFN): + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + def test_stor_ascii_2(self): + # Test that no extra extra carriage returns are added to the + # file in ASCII mode in case CRLF gets truncated in two chunks + # (issue 116) + + def store(cmd, fp, blocksize=8192): + # like storbinary() except it sends "type a" instead of + # "type i" before starting the transfer + self.client.voidcmd('type a') + with contextlib.closing(self.client.transfercmd(cmd)) as conn: + while True: + buf = fp.read(blocksize) + if not buf: + break + conn.sendall(buf) + return self.client.voidresp() + + old_buffer = DTPHandler.ac_in_buffer_size + try: + # set a small buffer so that CRLF gets delivered in two + # separate chunks: "CRLF", " f", "oo", " CR", "LF", " b", "ar" + DTPHandler.ac_in_buffer_size = 2 + data = b'\r\n foo \r\n bar' + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + store('stor ' + TESTFN, self.dummy_sendfile) + + expected = data.replace(b'\r\n', b(os.linesep)) + self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) + self.dummy_recvfile.seek(0) + self.assertEqual(expected, self.dummy_recvfile.read()) + finally: + DTPHandler.ac_in_buffer_size = old_buffer + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(TESTFN): + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + def test_stou(self): + data = b'abcde12345' * 100000 + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + + self.client.voidcmd('TYPE I') + # filename comes in as "1xx FILE: " + filename = self.client.sendcmd('stou').split('FILE: ')[1] + try: + with contextlib.closing(self.client.makeport()) as sock: + conn, sockaddr = sock.accept() + with contextlib.closing(conn): + conn.settimeout(TIMEOUT) + if hasattr(self.client_class, 'ssl_version'): + conn = ssl.wrap_socket(conn) + while True: + buf = self.dummy_sendfile.read(8192) + if not buf: + break + conn.sendall(buf) + # transfer finished, a 226 response is expected + self.assertEqual('226', self.client.voidresp()[:3]) + self.client.retrbinary('retr ' + filename, + self.dummy_recvfile.write) + self.dummy_recvfile.seek(0) + datafile = self.dummy_recvfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(filename): + try: + self.client.delete(filename) + except (ftplib.Error, EOFError, socket.error): + safe_remove(filename) + + def test_stou_rest(self): + # Watch for STOU preceded by REST, which makes no sense. + self.client.sendcmd('type i') + self.client.sendcmd('rest 10') + self.assertRaisesRegex(ftplib.error_temp, "Can't STOU while REST", + self.client.sendcmd, 'stou') + + def test_stou_orphaned_file(self): + # Check that no orphaned file gets left behind when STOU fails. + # Even if STOU fails the file is first created and then erased. + # Since we can't know the name of the file the best way that + # we have to test this case is comparing the content of the + # directory before and after STOU has been issued. + # Assuming that TESTFN is supposed to be a "reserved" file + # name we shouldn't get false positives. + safe_remove(TESTFN) + # login as a limited user in order to make STOU fail + self.client.login('anonymous', '@nopasswd') + before = os.listdir(HOME) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'stou ' + TESTFN) + after = os.listdir(HOME) + if before != after: + for file in after: + self.assertFalse(file.startswith(TESTFN)) + + def test_appe(self): + try: + data1 = b'abcde12345' * 100000 + self.dummy_sendfile.write(data1) + self.dummy_sendfile.seek(0) + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + + data2 = b'fghil67890' * 100000 + self.dummy_sendfile.write(data2) + self.dummy_sendfile.seek(len(data1)) + self.client.storbinary('appe ' + TESTFN, self.dummy_sendfile) + + self.client.retrbinary("retr " + TESTFN, self.dummy_recvfile.write) + self.dummy_recvfile.seek(0) + datafile = self.dummy_recvfile.read() + self.assertEqual(len(data1 + data2), len(datafile)) + self.assertEqual(hash(data1 + data2), hash(datafile)) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(TESTFN): + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + def test_appe_rest(self): + # Watch for APPE preceded by REST, which makes no sense. + self.client.sendcmd('type i') + self.client.sendcmd('rest 10') + self.assertRaisesRegex(ftplib.error_temp, "Can't APPE while REST", + self.client.sendcmd, 'appe x') + + def test_rest_on_stor(self): + # Test STOR preceded by REST. + data = b'abcde12345' * 100000 + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + + self.client.voidcmd('TYPE I') + with contextlib.closing( + self.client.transfercmd('stor ' + TESTFN)) as conn: + bytes_sent = 0 + while True: + chunk = self.dummy_sendfile.read(BUFSIZE) + conn.sendall(chunk) + bytes_sent += len(chunk) + # stop transfer while it isn't finished yet + if bytes_sent >= INTERRUPTED_TRANSF_SIZE or not chunk: + break + + # transfer wasn't finished yet but server can't know this, + # hence expect a 226 response + self.assertEqual('226', self.client.voidresp()[:3]) + + # resuming transfer by using a marker value greater than the + # file size stored on the server should result in an error + # on stor + file_size = self.client.size(TESTFN) + self.assertEqual(file_size, bytes_sent) + self.client.sendcmd('rest %s' % ((file_size + 1))) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'stor ' + TESTFN) + self.client.sendcmd('rest %s' % bytes_sent) + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + + self.client.retrbinary('retr ' + TESTFN, self.dummy_recvfile.write) + self.dummy_sendfile.seek(0) + self.dummy_recvfile.seek(0) + + data_sendfile = self.dummy_sendfile.read() + data_recvfile = self.dummy_recvfile.read() + self.assertEqual(len(data_sendfile), len(data_recvfile)) + self.assertEqual(len(data_sendfile), len(data_recvfile)) + self.client.delete(TESTFN) + + def test_failing_rest_on_stor(self): + # Test REST -> STOR against a non existing file. + if os.path.exists(TESTFN): + self.client.delete(TESTFN) + self.client.sendcmd('type i') + self.client.sendcmd('rest 10') + self.assertRaises(ftplib.error_perm, self.client.storbinary, + 'stor ' + TESTFN, lambda x: x) + # if the first STOR failed because of REST, the REST marker + # is supposed to be resetted to 0 + self.dummy_sendfile.write(b'x' * 4096) + self.dummy_sendfile.seek(0) + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + + def test_quit_during_transfer(self): + # RFC-959 states that if QUIT is sent while a transfer is in + # progress, the connection must remain open for result response + # and the server will then close it. + with contextlib.closing( + self.client.transfercmd('stor ' + TESTFN)) as conn: + conn.sendall(b'abcde12345' * 50000) + self.client.sendcmd('quit') + conn.sendall(b'abcde12345' * 50000) + # expect the response (transfer ok) + self.assertEqual('226', self.client.voidresp()[:3]) + # Make sure client has been disconnected. + # socket.error (Windows) or EOFError (Linux) exception is supposed + # to be raised in such a case. + self.client.sock.settimeout(.1) + self.assertRaises((socket.error, EOFError), + self.client.sendcmd, 'noop') + + def test_stor_empty_file(self): + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + self.client.quit() + with open(TESTFN) as f: + self.assertEqual(f.read(), "") + + +@unittest.skipUnless(POSIX, "POSIX only") +@unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, + "pysendfile not installed") +class TestFtpStoreDataNoSendfile(TestFtpStoreData): + """Test STOR, STOU, APPE, REST, TYPE not using sendfile().""" + + def setUp(self): + TestFtpStoreData.setUp(self) + with self.server.lock: + self.server.handler.use_sendfile = False + + def tearDown(self): + TestFtpStoreData.tearDown(self) + with self.server.lock: + self.server.handler.use_sendfile = True + + +@unittest.skipUnless(POSIX, "POSIX only") +@unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, + "pysendfile not installed") +class TestSendfile(unittest.TestCase): + """Sendfile specific tests.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.dummy_recvfile = BytesIO() + self.dummy_sendfile = BytesIO() + + def tearDown(self): + self.client.close() + self.server.stop() + self.dummy_recvfile.close() + self.dummy_sendfile.close() + safe_remove(TESTFN) + + def test_fallback(self): + # Makes sure that if sendfile() fails and no bytes were + # transmitted yet the server falls back on using plain + # send() + data = b'abcde12345' * 100000 + self.dummy_sendfile.write(data) + self.dummy_sendfile.seek(0) + self.client.storbinary('stor ' + TESTFN, self.dummy_sendfile) + with mock.patch('pyftpdlib.handlers.sendfile', + side_effect=OSError(errno.EINVAL)) as fun: + try: + self.client.retrbinary( + 'retr ' + TESTFN, self.dummy_recvfile.write) + assert fun.called + self.dummy_recvfile.seek(0) + datafile = self.dummy_recvfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + if os.path.exists(TESTFN): + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + +class TestFtpRetrieveData(unittest.TestCase): + + "Test RETR, REST, TYPE" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.file = open(TESTFN, 'w+b') + self.dummyfile = BytesIO() + + def tearDown(self): + self.client.close() + self.server.stop() + if not self.file.closed: + self.file.close() + if not self.dummyfile.closed: + self.dummyfile.close() + safe_remove(TESTFN) + + def test_retr(self): + data = b'abcde12345' * 100000 + self.file.write(data) + self.file.close() + self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + + # attempt to retrieve a file which doesn't exist + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.retrbinary, + "retr " + bogus, lambda x: x) + + def test_retr_ascii(self): + # Test RETR in ASCII mode. + + def retrieve(cmd, callback, blocksize=8192, rest=None): + # like retrbinary but uses TYPE A instead + self.client.voidcmd('type a') + with contextlib.closing( + self.client.transfercmd(cmd, rest)) as conn: + conn.settimeout(TIMEOUT) + while True: + data = conn.recv(blocksize) + if not data: + break + callback(data) + return self.client.voidresp() + + data = (b'abcde12345' + b(os.linesep)) * 100000 + self.file.write(data) + self.file.close() + retrieve("retr " + TESTFN, self.dummyfile.write) + expected = data.replace(b(os.linesep), b'\r\n') + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(expected), len(datafile)) + self.assertEqual(hash(expected), hash(datafile)) + + @retry_on_failure() + def test_restore_on_retr(self): + data = b'abcde12345' * 1000000 + self.file.write(data) + self.file.close() + + received_bytes = 0 + self.client.voidcmd('TYPE I') + with contextlib.closing( + self.client.transfercmd('retr ' + TESTFN)) as conn: + conn.settimeout(TIMEOUT) + while True: + chunk = conn.recv(BUFSIZE) + if not chunk: + break + self.dummyfile.write(chunk) + received_bytes += len(chunk) + if received_bytes >= INTERRUPTED_TRANSF_SIZE: + break + + # transfer wasn't finished yet so we expect a 426 response + self.assertEqual(self.client.getline()[:3], "426") + + # resuming transfer by using a marker value greater than the + # file size stored on the server should result in an error + # on retr (RFC-1123) + file_size = self.client.size(TESTFN) + self.client.sendcmd('rest %s' % ((file_size + 1))) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'retr ' + TESTFN) + # test resume + self.client.sendcmd('rest %s' % received_bytes) + self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + + def test_retr_empty_file(self): + self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) + self.dummyfile.seek(0) + self.assertEqual(self.dummyfile.read(), b"") + + +@unittest.skipUnless(POSIX, "POSIX only") +@unittest.skipIf(sys.version_info < (3, 3) and sendfile is None, + "pysendfile not installed") +class TestFtpRetrieveDataNoSendfile(TestFtpRetrieveData): + """Test RETR, REST, TYPE by not using sendfile().""" + + def setUp(self): + TestFtpRetrieveData.setUp(self) + with self.server.lock: + self.server.handler.use_sendfile = False + + def tearDown(self): + TestFtpRetrieveData.tearDown(self) + with self.server.lock: + self.server.handler.use_sendfile = True + + +class TestFtpListingCmds(unittest.TestCase): + """Test LIST, NLST, argumented STAT.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + touch(TESTFN) + + def tearDown(self): + self.client.close() + self.server.stop() + os.remove(TESTFN) + + def _test_listing_cmds(self, cmd): + """Tests common to LIST NLST and MLSD commands.""" + # assume that no argument has the same meaning of "/" + l1 = l2 = [] + self.client.retrlines(cmd, l1.append) + self.client.retrlines(cmd + ' /', l2.append) + self.assertEqual(l1, l2) + if cmd.lower() != 'mlsd': + # if pathname is a file one line is expected + x = [] + self.client.retrlines('%s ' % cmd + TESTFN, x.append) + self.assertEqual(len(x), 1) + self.assertTrue(''.join(x).endswith(TESTFN)) + # non-existent path, 550 response is expected + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.retrlines, + '%s ' % cmd + bogus, lambda x: x) + # for an empty directory we excpect that the data channel is + # opened anyway and that no data is received + x = [] + tempdir = os.path.basename(tempfile.mkdtemp(dir=HOME)) + try: + self.client.retrlines('%s %s' % (cmd, tempdir), x.append) + self.assertEqual(x, []) + finally: + safe_rmdir(tempdir) + + def test_nlst(self): + # common tests + self._test_listing_cmds('nlst') + + def test_list(self): + # common tests + self._test_listing_cmds('list') + # known incorrect pathname arguments (e.g. old clients) are + # expected to be treated as if pathname would be == '/' + l1 = l2 = l3 = l4 = l5 = [] + self.client.retrlines('list /', l1.append) + self.client.retrlines('list -a', l2.append) + self.client.retrlines('list -l', l3.append) + self.client.retrlines('list -al', l4.append) + self.client.retrlines('list -la', l5.append) + tot = (l1, l2, l3, l4, l5) + for x in range(len(tot) - 1): + self.assertEqual(tot[x], tot[x + 1]) + + def test_mlst(self): + # utility function for extracting the line of interest + def mlstline(cmd): + return self.client.voidcmd(cmd).split('\n')[1] + + # the fact set must be preceded by a space + self.assertTrue(mlstline('mlst').startswith(' ')) + # where TVFS is supported, a fully qualified pathname is expected + self.assertTrue(mlstline('mlst ' + TESTFN).endswith('/' + TESTFN)) + self.assertTrue(mlstline('mlst').endswith('/')) + # assume that no argument has the same meaning of "/" + self.assertEqual(mlstline('mlst'), mlstline('mlst /')) + # non-existent path + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'mlst ' + bogus) + # test file/dir notations + self.assertTrue('type=dir' in mlstline('mlst')) + self.assertTrue('type=file' in mlstline('mlst ' + TESTFN)) + # let's add some tests for OPTS command + self.client.sendcmd('opts mlst type;') + self.assertEqual(mlstline('mlst'), ' type=dir; /') + # where no facts are present, two leading spaces before the + # pathname are required (RFC-3659) + self.client.sendcmd('opts mlst') + self.assertEqual(mlstline('mlst'), ' /') + + def test_mlsd(self): + # common tests + self._test_listing_cmds('mlsd') + dir = os.path.basename(tempfile.mkdtemp(dir=HOME)) + self.addCleanup(safe_rmdir, dir) + try: + self.client.retrlines('mlsd ' + TESTFN, lambda x: x) + except ftplib.error_perm as err: + resp = str(err) + # if path is a file a 501 response code is expected + self.assertEqual(str(resp)[0:3], "501") + else: + self.fail("Exception not raised") + + def test_mlsd_all_facts(self): + feat = self.client.sendcmd('feat') + # all the facts + facts = re.search(r'^\s*MLST\s+(\S+)$', feat, re.MULTILINE).group(1) + facts = facts.replace("*;", ";") + self.client.sendcmd('opts mlst ' + facts) + resp = self.client.sendcmd('mlst') + + local = facts[:-1].split(";") + returned = resp.split("\n")[1].strip()[:-3] + returned = [x.split("=")[0] for x in returned.split(";")] + self.assertEqual(sorted(local), sorted(returned)) + + self.assertTrue("type" in resp) + self.assertTrue("size" in resp) + self.assertTrue("perm" in resp) + self.assertTrue("modify" in resp) + if POSIX: + self.assertTrue("unique" in resp) + self.assertTrue("unix.mode" in resp) + self.assertTrue("unix.uid" in resp) + self.assertTrue("unix.gid" in resp) + elif WINDOWS: + self.assertTrue("create" in resp) + + def test_stat(self): + # Test STAT provided with argument which is equal to LIST + self.client.sendcmd('stat /') + self.client.sendcmd('stat ' + TESTFN) + self.client.putcmd('stat *') + resp = self.client.getmultiline() + self.assertEqual(resp, '550 Globbing not supported.') + bogus = os.path.basename(tempfile.mktemp(dir=HOME)) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'stat ' + bogus) + + def test_unforeseen_time_event(self): + # Emulate a case where the file last modification time is prior + # to year 1900. This most likely will never happen unless + # someone specifically force the last modification time of a + # file in some way. + # To do so we temporarily override os.path.getmtime so that it + # returns a negative value referring to a year prior to 1900. + # It causes time.localtime/gmtime to raise a ValueError exception + # which is supposed to be handled by server. + _getmtime = AbstractedFS.getmtime + try: + AbstractedFS.getmtime = lambda x, y: -9000000000 + self.client.sendcmd('stat /') # test AbstractedFS.format_list() + self.client.sendcmd('mlst /') # test AbstractedFS.format_mlsx() + # make sure client hasn't been disconnected + self.client.sendcmd('noop') + finally: + AbstractedFS.getmtime = _getmtime + + +class TestFtpAbort(unittest.TestCase): + + "test: ABOR" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + self.client.close() + self.server.stop() + + def test_abor_no_data(self): + # Case 1: ABOR while no data channel is opened: respond with 225. + resp = self.client.sendcmd('ABOR') + self.assertEqual('225 No transfer to abort.', resp) + self.client.retrlines('list', [].append) + + def test_abor_pasv(self): + # Case 2: user sends a PASV, a data-channel socket is listening + # but not connected, and ABOR is sent: close listening data + # socket, respond with 225. + self.client.makepasv() + respcode = self.client.sendcmd('ABOR')[:3] + self.assertEqual('225', respcode) + self.client.retrlines('list', [].append) + + def test_abor_port(self): + # Case 3: data channel opened with PASV or PORT, but ABOR sent + # before a data transfer has been started: close data channel, + # respond with 225 + self.client.set_pasv(0) + with contextlib.closing(self.client.makeport()): + respcode = self.client.sendcmd('ABOR')[:3] + self.assertEqual('225', respcode) + self.client.retrlines('list', [].append) + + def test_abor_during_transfer(self): + # Case 4: ABOR while a data transfer on DTP channel is in + # progress: close data channel, respond with 426, respond + # with 226. + data = b'abcde12345' * 1000000 + with open(TESTFN, 'w+b') as f: + f.write(data) + try: + self.client.voidcmd('TYPE I') + with contextlib.closing( + self.client.transfercmd('retr ' + TESTFN)) as conn: + bytes_recv = 0 + while bytes_recv < 65536: + chunk = conn.recv(BUFSIZE) + bytes_recv += len(chunk) + + # stop transfer while it isn't finished yet + self.client.putcmd('ABOR') + + # transfer isn't finished yet so ftpd should respond with 426 + self.assertEqual(self.client.getline()[:3], "426") + + # transfer successfully aborted, so should now respond + # with a 226 + self.assertEqual('226', self.client.voidresp()[:3]) + finally: + # We do not use os.remove() because file could still be + # locked by ftpd thread. If DELE through FTP fails try + # os.remove() as last resort. + try: + self.client.delete(TESTFN) + except (ftplib.Error, EOFError, socket.error): + safe_remove(TESTFN) + + @unittest.skipUnless(hasattr(socket, 'MSG_OOB'), "MSG_OOB not available") + @unittest.skipIf(sys.version_info < (2, 6), + "does not work on python < 2.6") + @unittest.skipIf(OSX, "does not work on OSX") + def test_oob_abor(self): + # Send ABOR by following the RFC-959 directives of sending + # Telnet IP/Synch sequence as OOB data. + # On some systems like FreeBSD this happened to be a problem + # due to a different SO_OOBINLINE behavior. + # On some platforms (e.g. Python CE) the test may fail + # although the MSG_OOB constant is defined. + self.client.sock.sendall(b(chr(244)), socket.MSG_OOB) + self.client.sock.sendall(b(chr(255)), socket.MSG_OOB) + self.client.sock.sendall(b'abor\r\n') + self.assertEqual(self.client.getresp()[:3], '225') + + +class TestThrottleBandwidth(unittest.TestCase): + """Test ThrottledDTPHandler class.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + class CustomDTPHandler(ThrottledDTPHandler): + # overridden so that the "awake" callback is executed + # immediately; this way we won't introduce any slowdown + # and still test the code of interest + + def _throttle_bandwidth(self, *args, **kwargs): + ThrottledDTPHandler._throttle_bandwidth(self, *args, **kwargs) + if (self._throttler is not None and not + self._throttler.cancelled): + self._throttler.call() + self._throttler = None + + self.server = self.server_class() + with self.server.lock: + self.server.handler.dtp_handler = CustomDTPHandler + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.dummyfile = BytesIO() + + def tearDown(self): + self.client.close() + with self.server.lock: + self.server.handler.dtp_handler.read_limit = 0 + self.server.handler.dtp_handler.write_limit = 0 + self.server.handler.dtp_handler = DTPHandler + self.server.stop() + if not self.dummyfile.closed: + self.dummyfile.close() + if os.path.exists(TESTFN): + os.remove(TESTFN) + + def test_throttle_send(self): + # This test doesn't test the actual speed accuracy, just + # awakes all that code which implements the throttling. + with self.server.lock: + self.server.handler.dtp_handler.write_limit = 32768 + data = b'abcde12345' * 100000 + with open(TESTFN, 'wb') as file: + file.write(data) + self.client.retrbinary("retr " + TESTFN, self.dummyfile.write) + self.dummyfile.seek(0) + datafile = self.dummyfile.read() + self.assertEqual(len(data), len(datafile)) + self.assertEqual(hash(data), hash(datafile)) + + def test_throttle_recv(self): + # This test doesn't test the actual speed accuracy, just + # awakes all that code which implements the throttling. + with self.server.lock: + self.server.handler.dtp_handler.read_limit = 32768 + data = b'abcde12345' * 100000 + self.dummyfile.write(data) + self.dummyfile.seek(0) + self.client.storbinary("stor " + TESTFN, self.dummyfile) + self.client.quit() # needed to fix occasional failures + with open(TESTFN, 'rb') as file: + file_data = file.read() + self.assertEqual(len(data), len(file_data)) + self.assertEqual(hash(data), hash(file_data)) + + +class TestTimeouts(unittest.TestCase): + """Test idle-timeout capabilities of control and data channels. + Some tests may fail on slow machines. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = None + self.client = None + + def _setUp(self, idle_timeout=300, data_timeout=300, pasv_timeout=30, + port_timeout=30): + self.server = self.server_class() + with self.server.lock: + self.server.handler.timeout = idle_timeout + self.server.handler.dtp_handler.timeout = data_timeout + self.server.handler.passive_dtp.timeout = pasv_timeout + self.server.handler.active_dtp.timeout = port_timeout + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + if self.client is not None and self.server is not None: + self.client.close() + with self.server.lock: + self.server.handler.timeout = 300 + self.server.handler.dtp_handler.timeout = 300 + self.server.handler.passive_dtp.timeout = 30 + self.server.handler.active_dtp.timeout = 30 + self.server.stop() + + def test_idle_timeout(self): + # Test control channel timeout. The client which does not send + # any command within the time specified in FTPHandler.timeout is + # supposed to be kicked off. + self._setUp(idle_timeout=0.1) + # fail if no msg is received within 1 second + self.client.sock.settimeout(1) + data = self.client.sock.recv(BUFSIZE) + self.assertEqual(data, b"421 Control connection timed out.\r\n") + # ensure client has been kicked off + self.assertRaises((socket.error, EOFError), self.client.sendcmd, + 'noop') + + def test_data_timeout(self): + # Test data channel timeout. The client which does not send + # or receive any data within the time specified in + # DTPHandler.timeout is supposed to be kicked off. + self._setUp(data_timeout=0.1) + addr = self.client.makepasv() + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.connect(addr) + # fail if no msg is received within 1 second + self.client.sock.settimeout(1) + data = self.client.sock.recv(BUFSIZE) + self.assertEqual(data, b"421 Data connection timed out.\r\n") + # ensure client has been kicked off + self.assertRaises((socket.error, EOFError), self.client.sendcmd, + 'noop') + + def test_data_timeout_not_reached(self): + # Impose a timeout for the data channel, then keep sending data for a + # time which is longer than that to make sure that the code checking + # whether the transfer stalled for with no progress is executed. + self._setUp(data_timeout=0.1) + with contextlib.closing( + self.client.transfercmd('stor ' + TESTFN)) as sock: + if hasattr(self.client_class, 'ssl_version'): + sock = ssl.wrap_socket(sock) + try: + stop_at = time.time() + 0.2 + while time.time() < stop_at: + sock.send(b'x' * 1024) + sock.close() + self.client.voidresp() + finally: + if os.path.exists(TESTFN): + self.client.delete(TESTFN) + + def test_idle_data_timeout1(self): + # Tests that the control connection timeout is suspended while + # the data channel is opened + self._setUp(idle_timeout=0.1, data_timeout=0.2) + addr = self.client.makepasv() + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.connect(addr) + # fail if no msg is received within 1 second + self.client.sock.settimeout(1) + data = self.client.sock.recv(BUFSIZE) + self.assertEqual(data, b"421 Data connection timed out.\r\n") + # ensure client has been kicked off + self.assertRaises((socket.error, EOFError), self.client.sendcmd, + 'noop') + + def test_idle_data_timeout2(self): + # Tests that the control connection timeout is restarted after + # data channel has been closed + self._setUp(idle_timeout=0.1, data_timeout=0.2) + addr = self.client.makepasv() + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.connect(addr) + # close data channel + self.client.sendcmd('abor') + self.client.sock.settimeout(1) + data = self.client.sock.recv(BUFSIZE) + self.assertEqual(data, b"421 Control connection timed out.\r\n") + # ensure client has been kicked off + self.assertRaises((socket.error, EOFError), self.client.sendcmd, + 'noop') + + def test_pasv_timeout(self): + # Test pasv data channel timeout. The client which does not + # connect to the listening data socket within the time specified + # in PassiveDTP.timeout is supposed to receive a 421 response. + self._setUp(pasv_timeout=0.1) + self.client.makepasv() + # fail if no msg is received within 1 second + self.client.sock.settimeout(1) + data = self.client.sock.recv(BUFSIZE) + self.assertEqual(data, b"421 Passive data channel timed out.\r\n") + # client is not expected to be kicked off + self.client.sendcmd('noop') + + def test_disabled_idle_timeout(self): + self._setUp(idle_timeout=0) + self.client.sendcmd('noop') + + def test_disabled_data_timeout(self): + self._setUp(data_timeout=0) + addr = self.client.makepasv() + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.connect(addr) + + def test_disabled_pasv_timeout(self): + self._setUp(pasv_timeout=0) + self.client.makepasv() + # reset passive socket + addr = self.client.makepasv() + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.connect(addr) + + def test_disabled_port_timeout(self): + self._setUp(port_timeout=0) + with contextlib.closing(self.client.makeport()): + with contextlib.closing(self.client.makeport()): + pass + + +class TestConfigurableOptions(unittest.TestCase): + """Test those daemon options which are commonly modified by user.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + touch(TESTFN) + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + os.remove(TESTFN) + # set back options to their original value + with self.server.lock: + self.server.server.max_cons = 0 + self.server.server.max_cons_per_ip = 0 + self.server.handler.banner = "pyftpdlib ready." + self.server.handler.max_login_attempts = 3 + self.server.handler.auth_failed_timeout = 5 + self.server.handler.masquerade_address = None + self.server.handler.masquerade_address_map = {} + self.server.handler.permit_privileged_ports = False + self.server.handler.permit_foreign_addresses = False + self.server.handler.passive_ports = None + self.server.handler.use_gmt_times = True + self.server.handler.tcp_no_delay = hasattr(socket, 'TCP_NODELAY') + self.server.stop() + self.client.close() + + @disable_log_warning + def test_max_connections(self): + # Test FTPServer.max_cons attribute + with self.server.lock: + self.server.server.max_cons = 3 + self.client.quit() + c1 = self.client_class() + c2 = self.client_class() + c3 = self.client_class() + try: + c1.connect(self.server.host, self.server.port) + c2.connect(self.server.host, self.server.port) + self.assertRaises(ftplib.error_temp, c3.connect, self.server.host, + self.server.port) + # with passive data channel established + c2.quit() + c1.login(USER, PASSWD) + c1.makepasv() + self.assertRaises(ftplib.error_temp, c2.connect, self.server.host, + self.server.port) + # with passive data socket waiting for connection + c1.login(USER, PASSWD) + c1.sendcmd('pasv') + self.assertRaises(ftplib.error_temp, c2.connect, self.server.host, + self.server.port) + # with active data channel established + c1.login(USER, PASSWD) + with contextlib.closing(c1.makeport()): + self.assertRaises( + ftplib.error_temp, c2.connect, self.server.host, + self.server.port) + finally: + for c in (c1, c2, c3): + try: + c.quit() + except (socket.error, EOFError): # already disconnected + c.close() + + @disable_log_warning + def test_max_connections_per_ip(self): + # Test FTPServer.max_cons_per_ip attribute + with self.server.lock: + self.server.server.max_cons_per_ip = 3 + self.client.quit() + c1 = self.client_class() + c2 = self.client_class() + c3 = self.client_class() + c4 = self.client_class() + try: + c1.connect(self.server.host, self.server.port) + c2.connect(self.server.host, self.server.port) + c3.connect(self.server.host, self.server.port) + self.assertRaises(ftplib.error_temp, c4.connect, self.server.host, + self.server.port) + # Make sure client has been disconnected. + # socket.error (Windows) or EOFError (Linux) exception is + # supposed to be raised in such a case. + self.assertRaises((socket.error, EOFError), c4.sendcmd, 'noop') + finally: + for c in (c1, c2, c3, c4): + try: + c.quit() + except (socket.error, EOFError): # already disconnected + c.close() + + def test_banner(self): + # Test FTPHandler.banner attribute + with self.server.lock: + self.server.handler.banner = 'hello there' + self.client.close() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.assertEqual(self.client.getwelcome()[4:], 'hello there') + + def test_max_login_attempts(self): + # Test FTPHandler.max_login_attempts attribute. + with self.server.lock: + self.server.handler.max_login_attempts = 1 + self.server.handler.auth_failed_timeout = 0 + self.assertRaises(ftplib.error_perm, self.client.login, 'wrong', + 'wrong') + # socket.error (Windows) or EOFError (Linux) exceptions are + # supposed to be raised when attempting to send/recv some data + # using a disconnected socket + self.assertRaises((socket.error, EOFError), self.client.sendcmd, + 'noop') + + def test_masquerade_address(self): + # Test FTPHandler.masquerade_address attribute + host, port = self.client.makepasv() + self.assertEqual(host, self.server.host) + with self.server.lock: + self.server.handler.masquerade_address = "256.256.256.256" + host, port = self.client.makepasv() + self.assertEqual(host, "256.256.256.256") + + def test_masquerade_address_map(self): + # Test FTPHandler.masquerade_address_map attribute + host, port = self.client.makepasv() + self.assertEqual(host, self.server.host) + with self.server.lock: + self.server.handler.masquerade_address_map = {self.server.host: + "128.128.128.128"} + host, port = self.client.makepasv() + self.assertEqual(host, "128.128.128.128") + + def test_passive_ports(self): + # Test FTPHandler.passive_ports attribute + _range = list(range(40000, 60000, 200)) + with self.server.lock: + self.server.handler.passive_ports = _range + self.assertTrue(self.client.makepasv()[1] in _range) + self.assertTrue(self.client.makepasv()[1] in _range) + self.assertTrue(self.client.makepasv()[1] in _range) + self.assertTrue(self.client.makepasv()[1] in _range) + + @disable_log_warning + def test_passive_ports_busy(self): + # If the ports in the configured range are busy it is expected + # that a kernel-assigned port gets chosen + with contextlib.closing(socket.socket()) as s: + s.settimeout(TIMEOUT) + s.bind((HOST, 0)) + port = s.getsockname()[1] + with self.server.lock: + self.server.handler.passive_ports = [port] + resulting_port = self.client.makepasv()[1] + self.assertTrue(port != resulting_port) + + @disable_log_warning + def test_permit_privileged_ports(self): + # Test FTPHandler.permit_privileged_ports_active attribute + + # try to bind a socket on a privileged port + sock = None + for port in reversed(range(1, 1024)): + try: + socket.getservbyport(port) + except socket.error: + # not registered port; go on + try: + sock = socket.socket(self.client.af, socket.SOCK_STREAM) + self.addCleanup(sock.close) + sock.settimeout(TIMEOUT) + sock.bind((HOST, port)) + break + except socket.error as err: + if err.errno == errno.EACCES: + # root privileges needed + if sock is not None: + sock.close() + sock = None + break + sock.close() + continue + else: + # registered port found; skip to the next one + continue + else: + # no usable privileged port was found + sock = None + + with self.server.lock: + self.server.handler.permit_privileged_ports = False + self.assertRaises(ftplib.error_perm, self.client.sendport, HOST, + port) + if sock: + port = sock.getsockname()[1] + with self.server.lock: + self.server.handler.permit_privileged_ports = True + sock.listen(5) + sock.settimeout(TIMEOUT) + self.client.sendport(HOST, port) + s, addr = sock.accept() + s.close() + + def test_use_gmt_times(self): + # use GMT time + with self.server.lock: + self.server.handler.use_gmt_times = True + gmt1 = self.client.sendcmd('mdtm ' + TESTFN) + gmt2 = self.client.sendcmd('mlst ' + TESTFN) + gmt3 = self.client.sendcmd('stat ' + TESTFN) + + # use local time + with self.server.lock: + self.server.handler.use_gmt_times = False + + self.client.quit() + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + loc1 = self.client.sendcmd('mdtm ' + TESTFN) + loc2 = self.client.sendcmd('mlst ' + TESTFN) + loc3 = self.client.sendcmd('stat ' + TESTFN) + + # if we're not in a GMT time zone times are supposed to be + # different + if time.timezone != 0: + self.assertNotEqual(gmt1, loc1) + self.assertNotEqual(gmt2, loc2) + self.assertNotEqual(gmt3, loc3) + # ...otherwise they should be the same + else: + self.assertEqual(gmt1, loc1) + self.assertEqual(gmt2, loc2) + self.assertEqual(gmt3, loc3) + + @unittest.skipUnless(hasattr(socket, 'TCP_NODELAY'), + 'TCP_NODELAY not available') + def test_tcp_no_delay(self): + s = get_server_handler().socket + self.assertTrue(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) + self.client.quit() + with self.server.lock: + self.server.handler.tcp_no_delay = False + self.client.connect(self.server.host, self.server.port) + self.client.sendcmd('noop') + s = get_server_handler().socket + self.assertFalse(s.getsockopt(socket.SOL_TCP, socket.TCP_NODELAY)) + + def test_permit_foreign_address_false(self): + handler = get_server_handler() + handler.permit_foreign_addresses = False + handler.remote_ip = '9.9.9.9' + with self.assertRaises(ftplib.error_perm) as cm: + self.client.makeport() + self.assertIn('foreign address', str(cm.exception)) + + def test_permit_foreign_address_true(self): + handler = get_server_handler() + handler.permit_foreign_addresses = True + handler.remote_ip = '9.9.9.9' + s = self.client.makeport() + s.close() + + +class TestCallbacks(unittest.TestCase): + """Test FTPHandler class callback methods.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.client = None + self.server = None + self.file = None + self.dummyfile = None + + def _setUp(self, handler, connect=True, login=True): + self.tearDown() + ThreadedTestFTPd.handler = handler + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + if connect: + self.client.connect(self.server.host, self.server.port) + if login: + self.client.login(USER, PASSWD) + self.file = open(TESTFN, 'w+b') + self.dummyfile = BytesIO() + self._tearDown = False + + def tearDown(self): + if self.client is not None: + self.client.close() + if self.server is not None: + self.server.stop() + if self.file is not None: + self.file.close() + if self.dummyfile is not None: + self.dummyfile.close() + safe_remove(TESTFN) + + def test_on_file_sent(self): + _file = [] + + class TestHandler(FTPHandler): + + def on_file_sent(self, file): + _file.append(file) + + self._setUp(TestHandler) + data = b'abcde12345' * 100000 + self.file.write(data) + self.file.close() + self.client.retrbinary("retr " + TESTFN, lambda x: x) + self.client.quit() # prevent race conditions + call_until(lambda: _file, "ret == [os.path.abspath(TESTFN)]") + + def test_on_file_received(self): + _file = [] + + class TestHandler(FTPHandler): + + def on_file_received(self, file): + _file.append(file) + + self._setUp(TestHandler) + data = b'abcde12345' * 100000 + self.dummyfile.write(data) + self.dummyfile.seek(0) + self.client.storbinary('stor ' + TESTFN, self.dummyfile) + self.client.quit() # prevent race conditions + call_until(lambda: _file, "ret == [os.path.abspath(TESTFN)]") + + @retry_on_failure() + def test_on_incomplete_file_sent(self): + _file = [] + + class TestHandler(FTPHandler): + + def on_incomplete_file_sent(self, file): + _file.append(file) + + self._setUp(TestHandler) + data = b'abcde12345' * 100000 + self.file.write(data) + self.file.close() + + bytes_recv = 0 + with contextlib.closing( + self.client.transfercmd("retr " + TESTFN, None)) as conn: + while True: + chunk = conn.recv(BUFSIZE) + bytes_recv += len(chunk) + if bytes_recv >= INTERRUPTED_TRANSF_SIZE or not chunk: + break + self.assertEqual(self.client.getline()[:3], "426") + self.client.quit() # prevent race conditions + call_until(lambda: _file, "ret == [os.path.abspath(TESTFN)]") + + @unittest.skipIf(TRAVIS, "failing on Travis") + @retry_on_failure() + def test_on_incomplete_file_received(self): + _file = [] + + class TestHandler(FTPHandler): + + def on_incomplete_file_received(self, file): + _file.append(file) + + self._setUp(TestHandler) + data = b'abcde12345' * 100000 + self.dummyfile.write(data) + self.dummyfile.seek(0) + + with contextlib.closing( + self.client.transfercmd('stor ' + TESTFN)) as conn: + bytes_sent = 0 + while True: + chunk = self.dummyfile.read(BUFSIZE) + conn.sendall(chunk) + bytes_sent += len(chunk) + # stop transfer while it isn't finished yet + if bytes_sent >= INTERRUPTED_TRANSF_SIZE or not chunk: + self.client.putcmd('abor') + break + self.assertRaises(ftplib.error_temp, self.client.getresp) # 426 + self.client.quit() # prevent race conditions + call_until(lambda: _file, "ret == [os.path.abspath(TESTFN)]") + + def test_on_connect(self): + flag = [] + + class TestHandler(FTPHandler): + + def on_connect(self): + flag.append(None) + + self._setUp(TestHandler, connect=False) + self.client.connect(self.server.host, self.server.port) + self.client.sendcmd('noop') + self.client.quit() # prevent race conditions + call_until(lambda: flag, "ret == [None]") + + def test_on_disconnect(self): + flag = [] + + class TestHandler(FTPHandler): + + def on_disconnect(self): + flag.append(None) + + self._setUp(TestHandler) + self.assertFalse(flag) + self.client.sendcmd('quit') + call_until(lambda: flag, "ret == [None]") + + def test_on_login(self): + user = [] + + class TestHandler(FTPHandler): + auth_failed_timeout = 0 + + def on_login(self, username): + user.append(username) + + self._setUp(TestHandler) + self.client.quit() # prevent race conditions + call_until(lambda: user, "ret == [USER]") + + def test_on_login_failed(self): + pair = [] + + class TestHandler(FTPHandler): + auth_failed_timeout = 0 + + def on_login_failed(self, username, password): + pair.append((username, password)) + + self._setUp(TestHandler, login=False) + self.assertRaises(ftplib.error_perm, self.client.login, 'foo', 'bar') + self.client.quit() # prevent race conditions + call_until(lambda: pair, "ret == [('foo', 'bar')]") + + def test_on_logout_quit(self): + user = [] + + class TestHandler(FTPHandler): + + def on_logout(self, username): + user.append(username) + + self._setUp(TestHandler) + self.client.quit() # prevent race conditions + call_until(lambda: user, "ret == [USER]") + + def test_on_logout_rein(self): + user = [] + + class TestHandler(FTPHandler): + + def on_logout(self, username): + user.append(username) + + self._setUp(TestHandler) + self.client.sendcmd('rein') + self.client.quit() # prevent race conditions + call_until(lambda: user, "ret == [USER]") + + def test_on_logout_user_issued_twice(self): + users = [] + + class TestHandler(FTPHandler): + + def on_logout(self, username): + users.append(username) + + self._setUp(TestHandler) + # At this point user "user" is logged in. Re-login as anonymous, + # then quit and expect queue == ["user", "anonymous"] + self.client.login("anonymous") + self.client.quit() # prevent race conditions + call_until(lambda: users, "ret == [USER, 'anonymous']") + + def test_on_logout_no_pass(self): + # make sure on_logout() is not called if USER was provided + # but not PASS + users = [] + + class TestHandler(FTPHandler): + + def on_logout(self, username): + users.append(username) + + self._setUp(TestHandler, login=False) + self.client.sendcmd("user foo") + self.client.quit() # prevent race conditions + call_until(lambda: users, "ret == []") + + +class _TestNetworkProtocols(object): + """Test PASV, EPSV, PORT and EPRT commands. + + Do not use this class directly, let TestIPv4Environment and + TestIPv6Environment classes use it instead. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + HOST = HOST + + def setUp(self): + self.server = self.server_class((self.HOST, 0)) + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + if self.client.af == socket.AF_INET: + self.proto = "1" + self.other_proto = "2" + else: + self.proto = "2" + self.other_proto = "1" + + def tearDown(self): + self.client.close() + self.server.stop() + + def cmdresp(self, cmd): + """Send a command and return response, also if the command failed.""" + try: + return self.client.sendcmd(cmd) + except ftplib.Error as err: + return str(err) + + @disable_log_warning + def test_eprt(self): + if not SUPPORTS_HYBRID_IPV6: + # test wrong proto + try: + self.client.sendcmd('eprt |%s|%s|%s|' % ( + self.other_proto, self.server.host, self.server.port)) + except ftplib.error_perm as err: + self.assertEqual(str(err)[0:3], "522") + else: + self.fail("Exception not raised") + + # test bad args + msg = "501 Invalid EPRT format." + # len('|') > 3 + self.assertEqual(self.cmdresp('eprt ||||'), msg) + # len('|') < 3 + self.assertEqual(self.cmdresp('eprt ||'), msg) + # port > 65535 + self.assertEqual(self.cmdresp('eprt |%s|%s|65536|' % (self.proto, + self.HOST)), msg) + # port < 0 + self.assertEqual(self.cmdresp('eprt |%s|%s|-1|' % (self.proto, + self.HOST)), msg) + # port < 1024 + resp = self.cmdresp('eprt |%s|%s|222|' % (self.proto, self.HOST)) + self.assertEqual(resp[:3], '501') + self.assertIn('privileged port', resp) + # proto > 2 + _cmd = 'eprt |3|%s|%s|' % (self.server.host, self.server.port) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, _cmd) + + if self.proto == '1': + # len(ip.octs) > 4 + self.assertEqual(self.cmdresp('eprt |1|1.2.3.4.5|2048|'), msg) + # ip.oct > 255 + self.assertEqual(self.cmdresp('eprt |1|1.2.3.256|2048|'), msg) + # bad proto + resp = self.cmdresp('eprt |2|1.2.3.256|2048|') + self.assertTrue("Network protocol not supported" in resp) + + # test connection + with contextlib.closing(socket.socket(self.client.af)) as sock: + sock.bind((self.client.sock.getsockname()[0], 0)) + sock.listen(5) + sock.settimeout(TIMEOUT) + ip, port = sock.getsockname()[:2] + self.client.sendcmd('eprt |%s|%s|%s|' % (self.proto, ip, port)) + try: + s = sock.accept() + s[0].close() + except socket.timeout: + self.fail("Server didn't connect to passive socket") + + def test_epsv(self): + # test wrong proto + try: + self.client.sendcmd('epsv ' + self.other_proto) + except ftplib.error_perm as err: + self.assertEqual(str(err)[0:3], "522") + else: + self.fail("Exception not raised") + + # proto > 2 + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'epsv 3') + + # test connection + for cmd in ('EPSV', 'EPSV ' + self.proto): + host, port = ftplib.parse229(self.client.sendcmd(cmd), + self.client.sock.getpeername()) + with contextlib.closing( + socket.socket(self.client.af, socket.SOCK_STREAM)) as s: + s.settimeout(TIMEOUT) + s.connect((host, port)) + self.client.sendcmd('abor') + + def test_epsv_all(self): + self.client.sendcmd('epsv all') + self.assertRaises(ftplib.error_perm, self.client.sendcmd, 'pasv') + self.assertRaises(ftplib.error_perm, self.client.sendport, self.HOST, + 2000) + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'eprt |%s|%s|%s|' % (self.proto, self.HOST, 2000)) + + +@unittest.skipUnless(SUPPORTS_IPV4, "IPv4 not supported") +class TestIPv4Environment(_TestNetworkProtocols, unittest.TestCase): + """Test PASV, EPSV, PORT and EPRT commands. + + Runs tests contained in _TestNetworkProtocols class by using IPv4 + plus some additional specific tests. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + HOST = '127.0.0.1' + + @disable_log_warning + def test_port_v4(self): + # test connection + with contextlib.closing(self.client.makeport()): + self.client.sendcmd('abor') + # test bad arguments + ae = self.assertEqual + msg = "501 Invalid PORT format." + ae(self.cmdresp('port 127,0,0,1,1.1'), msg) # sep != ',' + ae(self.cmdresp('port X,0,0,1,1,1'), msg) # value != int + ae(self.cmdresp('port 127,0,0,1,1,1,1'), msg) # len(args) > 6 + ae(self.cmdresp('port 127,0,0,1'), msg) # len(args) < 6 + ae(self.cmdresp('port 256,0,0,1,1,1'), msg) # oct > 255 + ae(self.cmdresp('port 127,0,0,1,256,1'), msg) # port > 65535 + ae(self.cmdresp('port 127,0,0,1,-1,0'), msg) # port < 0 + # port < 1024 + resp = self.cmdresp('port %s,1,1' % self.HOST.replace('.', ',')) + self.assertEqual(resp[:3], '501') + self.assertIn('privileged port', resp) + if "1.2.3.4" != self.HOST: + resp = self.cmdresp('port 1,2,3,4,4,4') + assert 'foreign address' in resp, resp + + @disable_log_warning + def test_eprt_v4(self): + resp = self.cmdresp('eprt |1|0.10.10.10|2222|') + self.assertEqual(resp[:3], '501') + self.assertIn('foreign address', resp) + + def test_pasv_v4(self): + host, port = ftplib.parse227(self.client.sendcmd('pasv')) + with contextlib.closing( + socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.settimeout(TIMEOUT) + s.connect((host, port)) + + +@unittest.skipUnless(SUPPORTS_IPV6, "IPv6 not supported") +class TestIPv6Environment(_TestNetworkProtocols, unittest.TestCase): + """Test PASV, EPSV, PORT and EPRT commands. + + Runs tests contained in _TestNetworkProtocols class by using IPv6 + plus some additional specific tests. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + HOST = '::1' + + def test_port_v6(self): + # PORT is not supposed to work + self.assertRaises(ftplib.error_perm, self.client.sendport, + self.server.host, self.server.port) + + def test_pasv_v6(self): + # PASV is still supposed to work to support clients using + # IPv4 connecting to a server supporting both IPv4 and IPv6 + self.client.makepasv() + + @disable_log_warning + def test_eprt_v6(self): + resp = self.cmdresp('eprt |2|::foo|2222|') + self.assertEqual(resp[:3], '501') + self.assertIn('foreign address', resp) + + +@unittest.skipUnless(SUPPORTS_HYBRID_IPV6, "IPv4/6 dual stack not supported") +class TestIPv6MixedEnvironment(unittest.TestCase): + """By running the server by specifying "::" as IP address the + server is supposed to listen on all interfaces, supporting both + IPv4 and IPv6 by using a single socket. + + What we are going to do here is starting the server in this + manner and try to connect by using an IPv4 client. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + HOST = "::" + + def setUp(self): + self.server = self.server_class((self.HOST, 0)) + self.server.start() + self.client = None + + def tearDown(self): + if self.client is not None: + self.client.close() + self.server.stop() + + def test_port_v4(self): + def noop(x): + return x + + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect('127.0.0.1', self.server.port) + self.client.set_pasv(False) + self.client.login(USER, PASSWD) + self.client.retrlines('list', noop) + + def test_pasv_v4(self): + def noop(x): + return x + + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect('127.0.0.1', self.server.port) + self.client.set_pasv(True) + self.client.login(USER, PASSWD) + self.client.retrlines('list', noop) + # make sure pasv response doesn't return an IPv4-mapped address + ip = self.client.makepasv()[0] + self.assertFalse(ip.startswith("::ffff:")) + + def test_eprt_v4(self): + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect('127.0.0.1', self.server.port) + self.client.login(USER, PASSWD) + # test connection + with contextlib.closing( + socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: + sock.bind((self.client.sock.getsockname()[0], 0)) + sock.listen(5) + sock.settimeout(2) + ip, port = sock.getsockname()[:2] + self.client.sendcmd('eprt |1|%s|%s|' % (ip, port)) + try: + sock2, addr = sock.accept() + sock2.close() + except socket.timeout: + self.fail("Server didn't connect to passive socket") + + def test_epsv_v4(self): + def mlstline(cmd): + return self.client.voidcmd(cmd).split('\n')[1] + + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect('127.0.0.1', self.server.port) + self.client.login(USER, PASSWD) + host, port = ftplib.parse229(self.client.sendcmd('EPSV'), + self.client.sock.getpeername()) + self.assertEqual('127.0.0.1', host) + with contextlib.closing( + socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: + s.settimeout(TIMEOUT) + s.connect((host, port)) + self.assertTrue(mlstline('mlst /').endswith('/')) + + +class TestCornerCases(unittest.TestCase): + """Tests for any kind of strange situation for the server to be in, + mainly referring to bugs signaled on the bug tracker. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + + def tearDown(self): + self.client.close() + if self.server.is_alive(): + self.server.stop() + + def test_port_race_condition(self): + # Refers to bug #120, first sends PORT, then disconnects the + # control channel before accept()ing the incoming data connection. + # The original server behavior was to reply with "200 Active + # data connection established" *after* the client had already + # disconnected the control connection. + with contextlib.closing(socket.socket(self.client.af)) as sock: + sock.bind((self.client.sock.getsockname()[0], 0)) + sock.listen(5) + sock.settimeout(TIMEOUT) + host, port = sock.getsockname()[:2] + + hbytes = host.split('.') + pbytes = [repr(port // 256), repr(port % 256)] + bytes = hbytes + pbytes + cmd = 'PORT ' + ','.join(bytes) + '\r\n' + self.client.sock.sendall(b(cmd)) + self.client.getresp() + s, addr = sock.accept() + s.close() + + def test_stou_max_tries(self): + # Emulates case where the max number of tries to find out a + # unique file name when processing STOU command gets hit. + + class TestFS(AbstractedFS): + + def mkstemp(self, *args, **kwargs): + raise IOError(errno.EEXIST, + "No usable temporary file name found") + + with self.server.lock: + self.server.handler.abstracted_fs = TestFS + try: + self.client.quit() + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + self.assertRaises(ftplib.error_temp, self.client.sendcmd, 'stou') + finally: + with self.server.lock: + self.server.handler.abstracted_fs = AbstractedFS + + def test_quick_connect(self): + # Clients that connected and disconnected quickly could cause + # the server to crash, due to a failure to catch errors in the + # initial part of the connection process. + # Tracked in issues #91, #104 and #105. + # See also https://bugs.launchpad.net/zodb/+bug/135108 + import struct + + def connect(addr): + with contextlib.closing(socket.socket()) as s: + # Set SO_LINGER to 1,0 causes a connection reset (RST) to + # be sent when close() is called, instead of the standard + # FIN shutdown sequence. + s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, + struct.pack('ii', 1, 0)) + s.settimeout(TIMEOUT) + try: + s.connect(addr) + except socket.error: + pass + + for x in range(10): + connect((self.server.host, self.server.port)) + for x in range(10): + addr = self.client.makepasv() + connect(addr) + + def test_error_on_callback(self): + # test that the server do not crash in case an error occurs + # while firing a scheduled function + self.tearDown() + server = FTPServer((HOST, 0), FTPHandler) + self.addCleanup(server.close) + logger = logging.getLogger('pyftpdlib') + logger.disabled = True + try: + len1 = len(IOLoop.instance().socket_map) + IOLoop.instance().call_later(0, lambda: 1 // 0) + server.serve_forever(timeout=0.001, blocking=False) + len2 = len(IOLoop.instance().socket_map) + self.assertEqual(len1, len2) + finally: + logger.disabled = False + + def test_active_conn_error(self): + # we open a socket() but avoid to invoke accept() to + # reproduce this error condition: + # http://code.google.com/p/pyftpdlib/source/detail?r=905 + with contextlib.closing(socket.socket()) as sock: + sock.bind((HOST, 0)) + port = sock.getsockname()[1] + self.client.sock.settimeout(.1) + try: + resp = self.client.sendport(HOST, port) + except ftplib.error_temp as err: + self.assertEqual(str(err)[:3], '425') + except (socket.timeout, getattr(ssl, "SSLError", object())): + pass + else: + self.assertNotEqual(str(resp)[:3], '200') + + def test_repr(self): + # make sure the FTP/DTP handler classes have a sane repr() + with contextlib.closing(self.client.makeport()): + for inst in IOLoop.instance().socket_map.values(): + repr(inst) + str(inst) + + if hasattr(os, 'sendfile'): + def test_sendfile(self): + # make sure that on python >= 3.3 we're using os.sendfile + # rather than third party pysendfile module + from pyftpdlib.handlers import sendfile + self.assertIs(sendfile, os.sendfile) + + if SUPPORTS_SENDFILE: + def test_sendfile_enabled(self): + self.assertEqual(FTPHandler.use_sendfile, True) + + if hasattr(select, 'epoll') or hasattr(select, 'kqueue'): + def test_ioloop_fileno(self): + with self.server.lock: + fd = self.server.server.ioloop.fileno() + self.assertTrue(isinstance(fd, int), fd) + + +# TODO: disabled as on certain platforms (OSX and Windows) +# produces failures with python3. Will have to get back to +# this and fix it. +@unittest.skipIf(OSX or WINDOWS, "fails on OSX or Windows") +class TestUnicodePathNames(unittest.TestCase): + """Test FTP commands and responses by using path names with non + ASCII characters. + """ + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = self.server_class() + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.encoding = 'utf8' # PY3 only + self.client.connect(self.server.host, self.server.port) + self.client.login(USER, PASSWD) + if PY3: + safe_mkdir(bytes(TESTFN_UNICODE, 'utf8')) + touch(bytes(TESTFN_UNICODE_2, 'utf8')) + self.utf8fs = TESTFN_UNICODE in os.listdir('.') + else: + warnings.filterwarnings("ignore") + safe_mkdir(TESTFN_UNICODE) + touch(TESTFN_UNICODE_2) + self.utf8fs = unicode(TESTFN_UNICODE, 'utf8') in os.listdir(u('.')) + warnings.resetwarnings() + + def tearDown(self): + self.client.close() + self.server.stop() + remove_test_files() + + # --- fs operations + + def test_cwd(self): + if self.utf8fs: + resp = self.client.cwd(TESTFN_UNICODE) + self.assertTrue(TESTFN_UNICODE in resp) + else: + self.assertRaises(ftplib.error_perm, self.client.cwd, + TESTFN_UNICODE) + + def test_mkd(self): + if self.utf8fs: + os.rmdir(TESTFN_UNICODE) + dirname = self.client.mkd(TESTFN_UNICODE) + self.assertEqual(dirname, '/' + TESTFN_UNICODE) + self.assertTrue(os.path.isdir(TESTFN_UNICODE)) + else: + self.assertRaises(ftplib.error_perm, self.client.mkd, + TESTFN_UNICODE) + + def test_rmdir(self): + if self.utf8fs: + self.client.rmd(TESTFN_UNICODE) + else: + self.assertRaises(ftplib.error_perm, self.client.rmd, + TESTFN_UNICODE) + + def test_rnfr_rnto(self): + if self.utf8fs: + self.client.rename(TESTFN_UNICODE, TESTFN) + else: + self.assertRaises(ftplib.error_perm, self.client.rename, + TESTFN_UNICODE, TESTFN) + + def test_size(self): + self.client.sendcmd('type i') + if self.utf8fs: + self.client.sendcmd('size ' + TESTFN_UNICODE_2) + else: + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'size ' + TESTFN_UNICODE_2) + + def test_mdtm(self): + if self.utf8fs: + self.client.sendcmd('mdtm ' + TESTFN_UNICODE_2) + else: + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'mdtm ' + TESTFN_UNICODE_2) + + def test_stou(self): + if self.utf8fs: + resp = self.client.sendcmd('stou ' + TESTFN_UNICODE) + self.assertTrue(TESTFN_UNICODE in resp) + else: + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'stou ' + TESTFN_UNICODE) + + if hasattr(os, 'chmod'): + def test_site_chmod(self): + if self.utf8fs: + self.client.sendcmd('site chmod 777 ' + TESTFN_UNICODE) + else: + self.assertRaises(ftplib.error_perm, self.client.sendcmd, + 'site chmod 777 ' + TESTFN_UNICODE) + + # --- listing cmds + + def _test_listing_cmds(self, cmd): + ls = [] + self.client.retrlines(cmd, ls.append) + ls = '\n'.join(ls) + if self.utf8fs: + self.assertTrue(TESTFN_UNICODE in ls) + else: + # Part of the filename which are not encodable are supposed + # to have been replaced. The file should be something like + # 'tmp-pyftpdlib-unicode-????'. In any case it is not + # referenceable (e.g. DELE 'tmp-pyftpdlib-unicode-????' + # won't work). + self.assertTrue('tmp-pyftpdlib-unicode' in ls) + + def test_list(self): + self._test_listing_cmds('list') + + def test_nlst(self): + self._test_listing_cmds('nlst') + + def test_mlsd(self): + self._test_listing_cmds('mlsd') + + def test_mlst(self): + # utility function for extracting the line of interest + def mlstline(cmd): + return self.client.voidcmd(cmd).split('\n')[1] + + if self.utf8fs: + self.assertTrue('type=dir' in + mlstline('mlst ' + TESTFN_UNICODE)) + self.assertTrue('/' + TESTFN_UNICODE in + mlstline('mlst ' + TESTFN_UNICODE)) + self.assertTrue('type=file' in + mlstline('mlst ' + TESTFN_UNICODE_2)) + self.assertTrue('/' + TESTFN_UNICODE_2 in + mlstline('mlst ' + TESTFN_UNICODE_2)) + else: + self.assertRaises(ftplib.error_perm, + mlstline, 'mlst ' + TESTFN_UNICODE) + + # --- file transfer + + def test_stor(self): + if self.utf8fs: + data = b'abcde12345' * 500 + os.remove(TESTFN_UNICODE_2) + dummy = BytesIO() + dummy.write(data) + dummy.seek(0) + self.client.storbinary('stor ' + TESTFN_UNICODE_2, dummy) + dummy_recv = BytesIO() + self.client.retrbinary('retr ' + TESTFN_UNICODE_2, + dummy_recv.write) + dummy_recv.seek(0) + self.assertEqual(dummy_recv.read(), data) + else: + dummy = BytesIO() + self.assertRaises(ftplib.error_perm, self.client.storbinary, + 'stor ' + TESTFN_UNICODE_2, dummy) + + def test_retr(self): + if self.utf8fs: + data = b'abcd1234' * 500 + with open(TESTFN_UNICODE_2, 'wb') as f: + f.write(data) + dummy = BytesIO() + self.client.retrbinary('retr ' + TESTFN_UNICODE_2, dummy.write) + dummy.seek(0) + self.assertEqual(dummy.read(), data) + else: + dummy = BytesIO() + self.assertRaises(ftplib.error_perm, self.client.retrbinary, + 'retr ' + TESTFN_UNICODE_2, dummy.write) + + +class TestCommandLineParser(unittest.TestCase): + """Test command line parser.""" + SYSARGV = sys.argv + STDERR = sys.stderr + + def setUp(self): + class DummyFTPServer(FTPServer): + """An overridden version of FTPServer class which forces + serve_forever() to return immediately. + """ + + def serve_forever(self, *args, **kwargs): + return + + if PY3: + import io + self.devnull = io.StringIO() + else: + self.devnull = BytesIO() + sys.argv = self.SYSARGV[:] + sys.stderr = self.STDERR + self.original_ftpserver_class = FTPServer + pyftpdlib.__main__.FTPServer = DummyFTPServer + + def tearDown(self): + self.devnull.close() + sys.argv = self.SYSARGV[:] + sys.stderr = self.STDERR + pyftpdlib.servers.FTPServer = self.original_ftpserver_class + safe_rmdir(TESTFN) + + def test_a_option(self): + sys.argv += ["-i", "localhost", "-p", "0"] + pyftpdlib.__main__.main() + sys.argv = self.SYSARGV[:] + + # no argument + sys.argv += ["-a"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + def test_p_option(self): + sys.argv += ["-p", "0"] + pyftpdlib.__main__.main() + + # no argument + sys.argv = self.SYSARGV[:] + sys.argv += ["-p"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + # invalid argument + sys.argv += ["-p foo"] + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + def test_w_option(self): + sys.argv += ["-w", "-p", "0"] + with warnings.catch_warnings(): + warnings.filterwarnings("error") + self.assertRaises(RuntimeWarning, pyftpdlib.__main__.main) + + # unexpected argument + sys.argv = self.SYSARGV[:] + sys.argv += ["-w foo"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + def test_d_option(self): + sys.argv += ["-d", TESTFN, "-p", "0"] + safe_mkdir(TESTFN) + pyftpdlib.__main__.main() + + # without argument + sys.argv = self.SYSARGV[:] + sys.argv += ["-d"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + # no such directory + sys.argv = self.SYSARGV[:] + sys.argv += ["-d %s" % TESTFN] + safe_rmdir(TESTFN) + self.assertRaises(ValueError, pyftpdlib.__main__.main) + + def test_r_option(self): + sys.argv += ["-r 60000-61000", "-p", "0"] + pyftpdlib.__main__.main() + + # without arg + sys.argv = self.SYSARGV[:] + sys.argv += ["-r"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + # wrong arg + sys.argv = self.SYSARGV[:] + sys.argv += ["-r yyy-zzz"] + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + def test_v_option(self): + sys.argv += ["-v"] + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + # unexpected argument + sys.argv = self.SYSARGV[:] + sys.argv += ["-v foo"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + def test_D_option(self): + with mock.patch('pyftpdlib.__main__.config_logging') as fun: + sys.argv += ["-D", "-p 0"] + pyftpdlib.__main__.main() + fun.assert_called_once_with(level=logging.DEBUG) + + # unexpected argument + sys.argv = self.SYSARGV[:] + sys.argv += ["-V foo"] + sys.stderr = self.devnull + self.assertRaises(SystemExit, pyftpdlib.__main__.main) + + +configure_logging() +remove_test_files() + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_functional_ssl.py b/ftp_server/pyftpdlib/test/test_functional_ssl.py new file mode 100644 index 00000000..f0fd5944 --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_functional_ssl.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import contextlib +import ftplib +import os +import socket +import sys +import ssl + +import OpenSSL # requires "pip install pyopenssl" + +from pyftpdlib.handlers import TLS_FTPHandler +from pyftpdlib.test import configure_logging +from pyftpdlib.test import PASSWD +from pyftpdlib.test import remove_test_files +from pyftpdlib.test import ThreadedTestFTPd +from pyftpdlib.test import TIMEOUT +from pyftpdlib.test import TRAVIS +from pyftpdlib.test import unittest +from pyftpdlib.test import USER +from pyftpdlib.test import VERBOSITY +from pyftpdlib.test.test_functional import TestCallbacks +from pyftpdlib.test.test_functional import TestConfigurableOptions +from pyftpdlib.test.test_functional import TestCornerCases +from pyftpdlib.test.test_functional import TestFtpAbort +from pyftpdlib.test.test_functional import TestFtpAuthentication +from pyftpdlib.test.test_functional import TestFtpCmdsSemantic +from pyftpdlib.test.test_functional import TestFtpDummyCmds +from pyftpdlib.test.test_functional import TestFtpFsOperations +from pyftpdlib.test.test_functional import TestFtpListingCmds +from pyftpdlib.test.test_functional import TestFtpRetrieveData +from pyftpdlib.test.test_functional import TestFtpStoreData +from pyftpdlib.test.test_functional import TestIPv4Environment +from pyftpdlib.test.test_functional import TestIPv6Environment +from pyftpdlib.test.test_functional import TestSendfile +from pyftpdlib.test.test_functional import TestTimeouts + + +FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') +if sys.version_info < (2, 7): + FTPS_UNSUPPORT_REASON = "requires python 2.7+" +else: + FTPS_UNSUPPORT_REASON = "FTPS test skipped" + +CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'keycert.pem')) + +del OpenSSL + +# ===================================================================== +# --- FTPS mixin tests +# ===================================================================== + +# What we're going to do here is repeat the original functional tests +# defined in test_functinal.py but by using FTPS. +# we secure both control and data connections before running any test. +# This is useful as we reuse the existent functional tests which are +# supposed to work no matter if the underlying protocol is FTP or FTPS. + + +if FTPS_SUPPORT: + class FTPSClient(ftplib.FTP_TLS): + """A modified version of ftplib.FTP_TLS class which implicitly + secure the data connection after login(). + """ + + def login(self, *args, **kwargs): + ftplib.FTP_TLS.login(self, *args, **kwargs) + self.prot_p() + + class FTPSServer(ThreadedTestFTPd): + """A threaded FTPS server used for functional testing.""" + handler = TLS_FTPHandler + handler.certfile = CERTFILE + + class TLSTestMixin: + server_class = FTPSServer + client_class = FTPSClient +else: + @unittest.skipIf(True, FTPS_UNSUPPORT_REASON) + class TLSTestMixin: + pass + + +class TestFtpAuthenticationTLSMixin(TLSTestMixin, TestFtpAuthentication): + pass + + +class TestTFtpDummyCmdsTLSMixin(TLSTestMixin, TestFtpDummyCmds): + pass + + +class TestFtpCmdsSemanticTLSMixin(TLSTestMixin, TestFtpCmdsSemantic): + pass + + +class TestFtpFsOperationsTLSMixin(TLSTestMixin, TestFtpFsOperations): + pass + + +class TestFtpStoreDataTLSMixin(TLSTestMixin, TestFtpStoreData): + + @unittest.skipIf(1, "fails with SSL") + def test_stou(self): + pass + + +class TestSendFileTLSMixin(TLSTestMixin, TestSendfile): + + def test_fallback(self): + self.client.prot_c() + super(TestSendFileTLSMixin, self).test_fallback() + + +class TestFtpRetrieveDataTLSMixin(TLSTestMixin, TestFtpRetrieveData): + + @unittest.skipIf(os.name == 'nt', "may fail on windows") + def test_restore_on_retr(self): + super(TestFtpRetrieveDataTLSMixin, self).test_restore_on_retr() + + +class TestFtpListingCmdsTLSMixin(TLSTestMixin, TestFtpListingCmds): + + # TODO: see https://travis-ci.org/giampaolo/pyftpdlib/jobs/87318445 + # Fails with: + # File "/opt/python/2.7.9/lib/python2.7/ftplib.py", line 735, in retrlines + # conn.unwrap() + # File "/opt/python/2.7.9/lib/python2.7/ssl.py", line 771, in unwrap + # s = self._sslobj.shutdown() + # error: [Errno 0] Error + @unittest.skipIf(TRAVIS or os.name == 'nt', "may fail on travis/windows") + def test_nlst(self): + super(TestFtpListingCmdsTLSMixin, self).test_nlst() + + +class TestFtpAbortTLSMixin(TLSTestMixin, TestFtpAbort): + + @unittest.skipIf(1, "fails with SSL") + def test_oob_abor(self): + pass + + +class TestTimeoutsTLSMixin(TLSTestMixin, TestTimeouts): + + @unittest.skipIf(1, "fails with SSL") + def test_data_timeout_not_reached(self): + pass + + +class TestConfigurableOptionsTLSMixin(TLSTestMixin, TestConfigurableOptions): + pass + + +class TestCallbacksTLSMixin(TLSTestMixin, TestCallbacks): + + def test_on_file_received(self): + pass + + def test_on_file_sent(self): + pass + + def test_on_incomplete_file_received(self): + pass + + def test_on_incomplete_file_sent(self): + pass + + def test_on_connect(self): + pass + + def test_on_disconnect(self): + pass + + def test_on_login(self): + pass + + def test_on_login_failed(self): + pass + + def test_on_logout_quit(self): + pass + + def test_on_logout_rein(self): + pass + + def test_on_logout_user_issued_twice(self): + pass + + +class TestIPv4EnvironmentTLSMixin(TLSTestMixin, TestIPv4Environment): + pass + + +class TestIPv6EnvironmentTLSMixin(TLSTestMixin, TestIPv6Environment): + pass + + +class TestCornerCasesTLSMixin(TLSTestMixin, TestCornerCases): + pass + + +# ===================================================================== +# dedicated FTPS tests +# ===================================================================== + + +@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) +class TestFTPS(unittest.TestCase): + """Specific tests fot TSL_FTPHandler class.""" + + def setUp(self): + self.server = FTPSServer() + self.server.start() + self.client = ftplib.FTP_TLS(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + + def tearDown(self): + self.client.ssl_version = ssl.PROTOCOL_SSLv23 + with self.server.lock: + self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 + self.server.handler.tls_control_required = False + self.server.handler.tls_data_required = False + self.client.close() + self.server.stop() + + def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): + try: + callableObj(*args, **kwargs) + except excClass as err: + if str(err) == msg: + return + raise self.failureException("%s != %s" % (str(err), msg)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException("%s not raised" % excName) + + def test_auth(self): + # unsecured + self.client.login(secure=False) + self.assertFalse(isinstance(self.client.sock, ssl.SSLSocket)) + # secured + self.client.login() + self.assertTrue(isinstance(self.client.sock, ssl.SSLSocket)) + # AUTH issued twice + msg = '503 Already using TLS.' + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.sendcmd, 'auth tls') + + def test_pbsz(self): + # unsecured + self.client.login(secure=False) + msg = "503 PBSZ not allowed on insecure control connection." + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.sendcmd, 'pbsz 0') + # secured + self.client.login(secure=True) + resp = self.client.sendcmd('pbsz 0') + self.assertEqual(resp, "200 PBSZ=0 successful.") + + def test_prot(self): + self.client.login(secure=False) + msg = "503 PROT not allowed on insecure control connection." + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.sendcmd, 'prot p') + self.client.login(secure=True) + # secured + self.client.prot_p() + sock = self.client.transfercmd('list') + with contextlib.closing(sock): + while True: + if not sock.recv(1024): + self.client.voidresp() + break + self.assertTrue(isinstance(sock, ssl.SSLSocket)) + # unsecured + self.client.prot_c() + sock = self.client.transfercmd('list') + with contextlib.closing(sock): + while True: + if not sock.recv(1024): + self.client.voidresp() + break + self.assertFalse(isinstance(sock, ssl.SSLSocket)) + + def test_feat(self): + feat = self.client.sendcmd('feat') + cmds = ['AUTH TLS', 'AUTH SSL', 'PBSZ', 'PROT'] + for cmd in cmds: + self.assertTrue(cmd in feat) + + def test_unforseen_ssl_shutdown(self): + self.client.login() + try: + sock = self.client.sock.unwrap() + except socket.error as err: + if err.errno == 0: + return + raise + sock.settimeout(TIMEOUT) + sock.sendall(b'noop') + try: + chunk = sock.recv(1024) + except socket.error: + pass + else: + self.assertEqual(chunk, b"") + + def test_tls_control_required(self): + with self.server.lock: + self.server.handler.tls_control_required = True + msg = "550 SSL/TLS required on the control channel." + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.sendcmd, "user " + USER) + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.sendcmd, "pass " + PASSWD) + self.client.login(secure=True) + + def test_tls_data_required(self): + with self.server.lock: + self.server.handler.tls_data_required = True + self.client.login(secure=True) + msg = "550 SSL/TLS required on the data channel." + self.assertRaisesWithMsg(ftplib.error_perm, msg, + self.client.retrlines, 'list', lambda x: x) + self.client.prot_p() + self.client.retrlines('list', lambda x: x) + + def try_protocol_combo(self, server_protocol, client_protocol): + with self.server.lock: + self.server.handler.ssl_version = server_protocol + self.client.ssl_version = client_protocol + self.client.close() + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except (ssl.SSLError, socket.error): + self.client.close() + else: + self.client.quit() + + # def test_ssl_version(self): + # protos = [ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv23, + # ssl.PROTOCOL_TLSv1] + # if hasattr(ssl, "PROTOCOL_SSLv2"): + # protos.append(ssl.PROTOCOL_SSLv2) + # for proto in protos: + # self.try_protocol_combo(ssl.PROTOCOL_SSLv2, proto) + # for proto in protos: + # self.try_protocol_combo(ssl.PROTOCOL_SSLv3, proto) + # for proto in protos: + # self.try_protocol_combo(ssl.PROTOCOL_SSLv23, proto) + # for proto in protos: + # self.try_protocol_combo(ssl.PROTOCOL_TLSv1, proto) + + def test_ssl_options(self): + from OpenSSL import SSL + from OpenSSL._util import lib + from pyftpdlib.handlers import TLS_FTPHandler + try: + TLS_FTPHandler.ssl_context = None + ctx = TLS_FTPHandler.get_ssl_context() + # Verify default opts. + with contextlib.closing(socket.socket()) as s: + s = SSL.Connection(ctx, s) + opts = lib.SSL_CTX_get_options(ctx._context) + self.assertTrue(opts & SSL.OP_NO_SSLv2) + self.assertTrue(opts & SSL.OP_NO_SSLv3) + self.assertTrue(opts & SSL.OP_NO_COMPRESSION) + TLS_FTPHandler.ssl_context = None # reset + # Make sure that if ssl_options is None no options are set + # (except OP_NO_SSLv2 whch is enabled by default unless + # ssl_proto is set to SSL.SSLv23_METHOD). + TLS_FTPHandler.ssl_context = None + TLS_FTPHandler.ssl_options = None + ctx = TLS_FTPHandler.get_ssl_context() + with contextlib.closing(socket.socket()) as s: + s = SSL.Connection(ctx, s) + opts = lib.SSL_CTX_get_options(ctx._context) + self.assertTrue(opts & SSL.OP_NO_SSLv2) + # self.assertFalse(opts & SSL.OP_NO_SSLv3) + self.assertFalse(opts & SSL.OP_NO_COMPRESSION) + finally: + TLS_FTPHandler.ssl_context = None + + if hasattr(ssl, "PROTOCOL_SSLv2"): + def test_sslv2(self): + self.client.ssl_version = ssl.PROTOCOL_SSLv2 + self.client.close() + with self.server.lock: + self.client.connect(self.server.host, self.server.port) + self.assertRaises(socket.error, self.client.login) + self.client.ssl_version = ssl.PROTOCOL_SSLv2 + + +configure_logging() +remove_test_files() + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_ioloop.py b/ftp_server/pyftpdlib/test/test_ioloop.py new file mode 100644 index 00000000..7c86fb9a --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_ioloop.py @@ -0,0 +1,551 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import contextlib +import errno +import select +import socket +import sys +import time + +from pyftpdlib._compat import PY3 +from pyftpdlib.ioloop import Acceptor +from pyftpdlib.ioloop import AsyncChat +from pyftpdlib.ioloop import IOLoop +from pyftpdlib.ioloop import RetryError +from pyftpdlib.test import mock +from pyftpdlib.test import POSIX +from pyftpdlib.test import unittest +from pyftpdlib.test import VERBOSITY +import pyftpdlib.ioloop + + +if hasattr(socket, 'socketpair'): + socketpair = socket.socketpair +else: + def socketpair(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0): + with contextlib.closing(socket.socket(family, type, proto)) as l: + l.bind(("localhost", 0)) + l.listen(5) + c = socket.socket(family, type, proto) + try: + c.connect(l.getsockname()) + caddr = c.getsockname() + while True: + a, addr = l.accept() + # check that we've got the correct client + if addr == caddr: + return c, a + a.close() + except OSError: + c.close() + raise + + +# TODO: write more tests. +class BaseIOLoopTestCase(object): + + ioloop_class = None + + def make_socketpair(self): + rd, wr = socketpair() + self.addCleanup(rd.close) + self.addCleanup(wr.close) + return rd, wr + + def test_register(self): + s = self.ioloop_class() + self.addCleanup(s.close) + rd, wr = self.make_socketpair() + handler = AsyncChat(rd) + s.register(rd, handler, s.READ) + s.register(wr, handler, s.WRITE) + self.assertIn(rd, s.socket_map) + self.assertIn(wr, s.socket_map) + return (s, rd, wr) + + def test_unregister(self): + s, rd, wr = self.test_register() + s.unregister(rd) + s.unregister(wr) + self.assertNotIn(rd, s.socket_map) + self.assertNotIn(wr, s.socket_map) + + def test_unregister_twice(self): + s, rd, wr = self.test_register() + s.unregister(rd) + s.unregister(rd) + s.unregister(wr) + s.unregister(wr) + + def test_modify(self): + s, rd, wr = self.test_register() + s.modify(rd, s.WRITE) + s.modify(wr, s.READ) + + def test_loop(self): + # no timeout + s, rd, wr = self.test_register() + s.call_later(0, s.close) + s.loop() + # with timeout + s, rd, wr = self.test_register() + s.call_later(0, s.close) + s.loop(timeout=0.001) + + def test_close(self): + s, rd, wr = self.test_register() + s.close() + self.assertEqual(s.socket_map, {}) + + def test_close_w_handler_exc(self): + # Simulate an exception when close()ing a socket handler. + # Exception should be logged and ignored. + class Handler(AsyncChat): + + def close(self): + 1 / 0 + + s = self.ioloop_class() + self.addCleanup(s.close) + rd, wr = self.make_socketpair() + handler = Handler(rd) + s.register(rd, handler, s.READ) + with mock.patch("pyftpdlib.ioloop.logger.error") as m: + s.close() + assert m.called + self.assertIn('ZeroDivisionError', m.call_args[0][0]) + + def test_close_w_handler_ebadf_exc(self): + # Simulate an exception when close()ing a socket handler. + # Exception should be ignored (and not logged). + class Handler(AsyncChat): + + def close(self): + raise OSError(errno.EBADF, "") + + s = self.ioloop_class() + self.addCleanup(s.close) + rd, wr = self.make_socketpair() + handler = Handler(rd) + s.register(rd, handler, s.READ) + with mock.patch("pyftpdlib.ioloop.logger.error") as m: + s.close() + assert not m.called + + def test_close_w_callback_exc(self): + # Simulate an exception when close()ing the IO loop and a + # scheduled callback raises an exception on cancel(). + with mock.patch("pyftpdlib.ioloop.logger.error") as logerr: + with mock.patch("pyftpdlib.ioloop._CallLater.cancel", + side_effect=lambda: 1 / 0) as cancel: + s = self.ioloop_class() + self.addCleanup(s.close) + s.call_later(1, lambda: 0) + s.close() + assert cancel.called + assert logerr.called + self.assertIn('ZeroDivisionError', logerr.call_args[0][0]) + + +class DefaultIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): + ioloop_class = pyftpdlib.ioloop.IOLoop + + +# =================================================================== +# select() +# =================================================================== + +class SelectIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): + ioloop_class = pyftpdlib.ioloop.Select + + def test_select_eintr(self): + # EINTR is supposed to be ignored + with mock.patch('pyftpdlib.ioloop.select.select', + side_effect=select.error()) as m: + m.side_effect.errno = errno.EINTR + s, rd, wr = self.test_register() + s.poll(0) + # ...but just that + with mock.patch('pyftpdlib.ioloop.select.select', + side_effect=select.error()) as m: + m.side_effect.errno = errno.EBADF + s, rd, wr = self.test_register() + self.assertRaises(select.error, s.poll, 0) + + +# =================================================================== +# poll() +# =================================================================== + +@unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Poll'), + "poll() not available on this platform") +class PollIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): + ioloop_class = getattr(pyftpdlib.ioloop, "Poll", None) + poller_mock = "pyftpdlib.ioloop.Poll._poller" + + @unittest.skipIf(sys.version_info[:2] == (3, 2), "") + def test_eintr_on_poll(self): + # EINTR is supposed to be ignored + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + if not PY3: + m.return_value.poll.side_effect = select.error + m.return_value.poll.side_effect.errno = errno.EINTR + else: + m.return_value.poll.side_effect = OSError(errno.EINTR, "") + s, rd, wr = self.test_register() + s.poll(0) + assert m.called + # ...but just that + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + if not PY3: + m.return_value.poll.side_effect = select.error + m.return_value.poll.side_effect.errno = errno.EBADF + else: + m.return_value.poll.side_effect = OSError(errno.EBADF, "") + s, rd, wr = self.test_register() + self.assertRaises(select.error, s.poll, 0) + assert m.called + + def test_eexist_on_register(self): + # EEXIST is supposed to be ignored + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + m.return_value.register.side_effect = \ + EnvironmentError(errno.EEXIST, "") + s, rd, wr = self.test_register() + # ...but just that + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + m.return_value.register.side_effect = \ + EnvironmentError(errno.EBADF, "") + self.assertRaises(EnvironmentError, self.test_register) + + def test_enoent_ebadf_on_unregister(self): + # ENOENT and EBADF are supposed to be ignored + for errnum in (errno.EBADF, errno.ENOENT): + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + m.return_value.unregister.side_effect = \ + EnvironmentError(errnum, "") + s, rd, wr = self.test_register() + s.unregister(rd) + # ...but just those + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + m.return_value.unregister.side_effect = \ + EnvironmentError(errno.EEXIST, "") + s, rd, wr = self.test_register() + self.assertRaises(EnvironmentError, s.unregister, rd) + + def test_enoent_on_modify(self): + # ENOENT is supposed to be ignored + with mock.patch(self.poller_mock, return_vaue=mock.Mock()) as m: + m.return_value.modify.side_effect = \ + OSError(errno.ENOENT, "") + s, rd, wr = self.test_register() + s.modify(rd, s.READ) + + +# =================================================================== +# epoll() +# =================================================================== + +@unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Epoll'), + "epoll() not available on this platform (Linux only)") +class EpollIOLoopTestCase(PollIOLoopTestCase): + ioloop_class = getattr(pyftpdlib.ioloop, "Epoll", None) + poller_mock = "pyftpdlib.ioloop.Epoll._poller" + + +# =================================================================== +# /dev/poll +# =================================================================== + +@unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'DevPoll'), + "/dev/poll not available on this platform (Solaris only)") +class DevPollIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): + ioloop_class = getattr(pyftpdlib.ioloop, "DevPoll", None) + + +# =================================================================== +# kqueue +# =================================================================== + +@unittest.skipUnless(hasattr(pyftpdlib.ioloop, 'Kqueue'), + "/dev/poll not available on this platform (BSD only)") +class KqueueIOLoopTestCase(unittest.TestCase, BaseIOLoopTestCase): + ioloop_class = getattr(pyftpdlib.ioloop, "Kqueue", None) + + +class TestCallLater(unittest.TestCase): + """Tests for CallLater class.""" + + def setUp(self): + self.ioloop = IOLoop.instance() + for task in self.ioloop.sched._tasks: + if not task.cancelled: + task.cancel() + del self.ioloop.sched._tasks[:] + + def scheduler(self, timeout=0.01, count=100): + while self.ioloop.sched._tasks and count > 0: + self.ioloop.sched.poll() + count -= 1 + time.sleep(timeout) + + def test_interface(self): + def fun(): + return 0 + + self.assertRaises(AssertionError, self.ioloop.call_later, -1, fun) + x = self.ioloop.call_later(3, fun) + self.assertEqual(x.cancelled, False) + x.cancel() + self.assertEqual(x.cancelled, True) + self.assertRaises(AssertionError, x.call) + self.assertRaises(AssertionError, x.reset) + x.cancel() + + def test_order(self): + def fun(x): + l.append(x) + + l = [] + for x in [0.05, 0.04, 0.03, 0.02, 0.01]: + self.ioloop.call_later(x, fun, x) + self.scheduler() + self.assertEqual(l, [0.01, 0.02, 0.03, 0.04, 0.05]) + + # The test is reliable only on those systems where time.time() + # provides time with a better precision than 1 second. + if not str(time.time()).endswith('.0'): + def test_reset(self): + def fun(x): + l.append(x) + + l = [] + self.ioloop.call_later(0.01, fun, 0.01) + self.ioloop.call_later(0.02, fun, 0.02) + self.ioloop.call_later(0.03, fun, 0.03) + x = self.ioloop.call_later(0.04, fun, 0.04) + self.ioloop.call_later(0.05, fun, 0.05) + time.sleep(0.1) + x.reset() + self.scheduler() + self.assertEqual(l, [0.01, 0.02, 0.03, 0.05, 0.04]) + + def test_cancel(self): + def fun(x): + l.append(x) + + l = [] + self.ioloop.call_later(0.01, fun, 0.01).cancel() + self.ioloop.call_later(0.02, fun, 0.02) + self.ioloop.call_later(0.03, fun, 0.03) + self.ioloop.call_later(0.04, fun, 0.04) + self.ioloop.call_later(0.05, fun, 0.05).cancel() + self.scheduler() + self.assertEqual(l, [0.02, 0.03, 0.04]) + + def test_errback(self): + l = [] + self.ioloop.call_later( + 0.0, lambda: 1 // 0, _errback=lambda: l.append(True)) + self.scheduler() + self.assertEqual(l, [True]) + + def test__repr__(self): + repr(self.ioloop.call_later(0.01, lambda: 0, 0.01)) + + def test__lt__(self): + a = self.ioloop.call_later(0.01, lambda: 0, 0.01) + b = self.ioloop.call_later(0.02, lambda: 0, 0.02) + self.assertTrue(a < b) + + def test__le__(self): + a = self.ioloop.call_later(0.01, lambda: 0, 0.01) + b = self.ioloop.call_later(0.02, lambda: 0, 0.02) + self.assertTrue(a <= b) + + +class TestCallEvery(unittest.TestCase): + """Tests for CallEvery class.""" + + def setUp(self): + self.ioloop = IOLoop.instance() + for task in self.ioloop.sched._tasks: + if not task.cancelled: + task.cancel() + del self.ioloop.sched._tasks[:] + + def scheduler(self, timeout=0.003): + stop_at = time.time() + timeout + while time.time() < stop_at: + self.ioloop.sched.poll() + + def test_interface(self): + def fun(): + return 0 + + self.assertRaises(AssertionError, self.ioloop.call_every, -1, fun) + x = self.ioloop.call_every(3, fun) + self.assertEqual(x.cancelled, False) + x.cancel() + self.assertEqual(x.cancelled, True) + self.assertRaises(AssertionError, x.call) + self.assertRaises(AssertionError, x.reset) + x.cancel() + + def test_only_once(self): + # make sure that callback is called only once per-loop + def fun(): + l1.append(None) + + l1 = [] + self.ioloop.call_every(0, fun) + self.ioloop.sched.poll() + self.assertEqual(l1, [None]) + + def test_multi_0_timeout(self): + # make sure a 0 timeout callback is called as many times + # as the number of loops + def fun(): + l.append(None) + + l = [] + self.ioloop.call_every(0, fun) + for x in range(100): + self.ioloop.sched.poll() + self.assertEqual(len(l), 100) + + # run it on systems where time.time() has a higher precision + if POSIX: + def test_low_and_high_timeouts(self): + # make sure a callback with a lower timeout is called more + # frequently than another with a greater timeout + def fun(): + l1.append(None) + + l1 = [] + self.ioloop.call_every(0.001, fun) + self.scheduler() + + def fun(): + l2.append(None) + + l2 = [] + self.ioloop.call_every(0.005, fun) + self.scheduler(timeout=0.01) + + self.assertTrue(len(l1) > len(l2)) + + def test_cancel(self): + # make sure a cancelled callback doesn't get called anymore + def fun(): + l.append(None) + + l = [] + call = self.ioloop.call_every(0.001, fun) + self.scheduler() + len_l = len(l) + call.cancel() + self.scheduler() + self.assertEqual(len_l, len(l)) + + def test_errback(self): + l = [] + self.ioloop.call_every( + 0.0, lambda: 1 // 0, _errback=lambda: l.append(True)) + self.scheduler() + self.assertTrue(l) + + +class TestAsyncChat(unittest.TestCase): + + def get_connected_handler(self): + s = socket.socket() + self.addCleanup(s.close) + ac = AsyncChat(sock=s) + self.addCleanup(ac.close) + return ac + + def test_send_retry(self): + ac = self.get_connected_handler() + for errnum in pyftpdlib.ioloop._ERRNOS_RETRY: + with mock.patch("pyftpdlib.ioloop.socket.socket.send", + side_effect=socket.error(errnum, "")) as m: + self.assertEqual(ac.send(b"x"), 0) + assert m.called + + def test_send_disconnect(self): + ac = self.get_connected_handler() + for errnum in pyftpdlib.ioloop._ERRNOS_DISCONNECTED: + with mock.patch("pyftpdlib.ioloop.socket.socket.send", + side_effect=socket.error(errnum, "")) as send: + with mock.patch.object(ac, "handle_close") as handle_close: + self.assertEqual(ac.send(b"x"), 0) + assert send.called + assert handle_close.called + + def test_recv_retry(self): + ac = self.get_connected_handler() + for errnum in pyftpdlib.ioloop._ERRNOS_RETRY: + with mock.patch("pyftpdlib.ioloop.socket.socket.recv", + side_effect=socket.error(errnum, "")) as m: + self.assertRaises(RetryError, ac.recv, 1024) + assert m.called + + def test_recv_disconnect(self): + ac = self.get_connected_handler() + for errnum in pyftpdlib.ioloop._ERRNOS_DISCONNECTED: + with mock.patch("pyftpdlib.ioloop.socket.socket.recv", + side_effect=socket.error(errnum, "")) as send: + with mock.patch.object(ac, "handle_close") as handle_close: + self.assertEqual(ac.recv(b"x"), b'') + assert send.called + assert handle_close.called + + def test_connect_af_unspecified_err(self): + ac = AsyncChat() + with mock.patch.object( + ac, "connect", + side_effect=socket.error(errno.EBADF, "")) as m: + self.assertRaises(socket.error, + ac.connect_af_unspecified, ("localhost", 0)) + assert m.called + self.assertIsNone(ac.socket) + + +class TestAcceptor(unittest.TestCase): + + def test_bind_af_unspecified_err(self): + ac = Acceptor() + with mock.patch.object( + ac, "bind", + side_effect=socket.error(errno.EBADF, "")) as m: + self.assertRaises(socket.error, + ac.bind_af_unspecified, ("localhost", 0)) + assert m.called + self.assertIsNone(ac.socket) + + def test_handle_accept_econnacorted(self): + # https://github.com/giampaolo/pyftpdlib/issues/105 + ac = Acceptor() + with mock.patch.object( + ac, "accept", + side_effect=socket.error(errno.ECONNABORTED, "")) as m: + ac.handle_accept() + assert m.called + self.assertIsNone(ac.socket) + + def test_handle_accept_typeerror(self): + # https://github.com/giampaolo/pyftpdlib/issues/91 + ac = Acceptor() + with mock.patch.object(ac, "accept", side_effect=TypeError) as m: + ac.handle_accept() + assert m.called + self.assertIsNone(ac.socket) + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_misc.py b/ftp_server/pyftpdlib/test/test_misc.py new file mode 100644 index 00000000..054db722 --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_misc.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +from pyftpdlib.test import unittest +from pyftpdlib.test import VERBOSITY +from pyftpdlib.test import ThreadWorker + + +class TestThreadWorker(unittest.TestCase): + + def test_callback_methods(self): + class Worker(ThreadWorker): + + def poll(self): + if 'poll' not in flags: + flags.append('poll') + + def before_start(self): + flags.append('before_start') + + def before_stop(self): + flags.append('before_stop') + + def after_stop(self): + flags.append('after_stop') + + # Stress test it a little to make sure there are no race conditions + # between locks: the order is always supposed to be the same, no + # matter what. + for x in range(100): + flags = [] + tw = Worker(0.001) + tw.start() + tw.stop() + self.assertEqual( + flags, ['before_start', 'poll', 'before_stop', 'after_stop']) + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/pyftpdlib/test/test_servers.py b/ftp_server/pyftpdlib/test/test_servers.py new file mode 100644 index 00000000..cf491fa1 --- /dev/null +++ b/ftp_server/pyftpdlib/test/test_servers.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python + +# Copyright (C) 2007 Giampaolo Rodola' . +# Use of this source code is governed by MIT license that can be +# found in the LICENSE file. + +import contextlib +import ftplib +import socket + +from pyftpdlib import servers +from pyftpdlib.test import configure_logging +from pyftpdlib.test import HOST +from pyftpdlib.test import PASSWD +from pyftpdlib.test import remove_test_files +from pyftpdlib.test import ThreadedTestFTPd +from pyftpdlib.test import TIMEOUT +from pyftpdlib.test import unittest +from pyftpdlib.test import USER +from pyftpdlib.test import VERBOSITY +from pyftpdlib.test.test_functional import TestCallbacks +from pyftpdlib.test.test_functional import TestCornerCases +from pyftpdlib.test.test_functional import TestFtpAbort +from pyftpdlib.test.test_functional import TestFtpAuthentication +from pyftpdlib.test.test_functional import TestFtpCmdsSemantic +from pyftpdlib.test.test_functional import TestFtpDummyCmds +from pyftpdlib.test.test_functional import TestFtpFsOperations +from pyftpdlib.test.test_functional import TestFtpListingCmds +from pyftpdlib.test.test_functional import TestFtpRetrieveData +from pyftpdlib.test.test_functional import TestFtpStoreData +from pyftpdlib.test.test_functional import TestIPv4Environment +from pyftpdlib.test.test_functional import TestIPv6Environment + + +MPROCESS_SUPPORT = hasattr(servers, 'MultiprocessFTPServer') + + +class TestFTPServer(unittest.TestCase): + """Tests for *FTPServer classes.""" + server_class = ThreadedTestFTPd + client_class = ftplib.FTP + + def setUp(self): + self.server = None + self.client = None + + def tearDown(self): + if self.client is not None: + self.client.close() + if self.server is not None: + self.server.stop() + + def test_sock_instead_of_addr(self): + # pass a socket object instead of an address tuple to FTPServer + # constructor + with contextlib.closing(socket.socket()) as sock: + sock.bind((HOST, 0)) + sock.listen(5) + ip, port = sock.getsockname()[:2] + self.server = self.server_class(sock) + self.server.start() + self.client = self.client_class(timeout=TIMEOUT) + self.client.connect(ip, port) + self.client.login(USER, PASSWD) + + +# ===================================================================== +# --- threaded FTP server mixin tests +# ===================================================================== + +# What we're going to do here is repeat the original functional tests +# defined in test_functinal.py but by using different concurrency +# modules (multi thread and multi process instead of async. +# This is useful as we reuse the existent functional tests which are +# supposed to work no matter what the concurrency model is. + + +class _TFTPd(ThreadedTestFTPd): + server_class = servers.ThreadedFTPServer + + +class ThreadFTPTestMixin: + server_class = _TFTPd + + +class TestFtpAuthenticationThreadMixin(ThreadFTPTestMixin, + TestFtpAuthentication): + pass + + +class TestTFtpDummyCmdsThreadMixin(ThreadFTPTestMixin, TestFtpDummyCmds): + pass + + +class TestFtpCmdsSemanticThreadMixin(ThreadFTPTestMixin, TestFtpCmdsSemantic): + pass + + +class TestFtpFsOperationsThreadMixin(ThreadFTPTestMixin, TestFtpFsOperations): + pass + + +class TestFtpStoreDataThreadMixin(ThreadFTPTestMixin, TestFtpStoreData): + pass + + +class TestFtpRetrieveDataThreadMixin(ThreadFTPTestMixin, TestFtpRetrieveData): + pass + + +class TestFtpListingCmdsThreadMixin(ThreadFTPTestMixin, TestFtpListingCmds): + pass + + +class TestFtpAbortThreadMixin(ThreadFTPTestMixin, TestFtpAbort): + pass + + +# class TestTimeoutsThreadMixin(ThreadFTPTestMixin, TestTimeouts): +# def test_data_timeout_not_reached(self): pass +# class TestConfOptsThreadMixin(ThreadFTPTestMixin, TestConfigurableOptions): +# pass + + +class TestCallbacksThreadMixin(ThreadFTPTestMixin, TestCallbacks): + pass + + +class TestIPv4EnvironmentThreadMixin(ThreadFTPTestMixin, TestIPv4Environment): + pass + + +class TestIPv6EnvironmentThreadMixin(ThreadFTPTestMixin, TestIPv6Environment): + pass + + +class TestCornerCasesThreadMixin(ThreadFTPTestMixin, TestCornerCases): + pass + + +# class TestFTPServerThreadMixin(ThreadFTPTestMixin, TestFTPServer): +# pass + + +# ===================================================================== +# --- multiprocess FTP server mixin tests +# ===================================================================== + +if MPROCESS_SUPPORT: + class MultiProcFTPd(ThreadedTestFTPd): + server_class = servers.MultiprocessFTPServer + + class MProcFTPTestMixin: + server_class = MultiProcFTPd +else: + @unittest.skipIf(True, "multiprocessing module not installed") + class MProcFTPTestMixin: + pass + + +class TestFtpAuthenticationMProcMixin(MProcFTPTestMixin, + TestFtpAuthentication): + pass + + +class TestTFtpDummyCmdsMProcMixin(MProcFTPTestMixin, TestFtpDummyCmds): + pass + + +class TestFtpCmdsSemanticMProcMixin(MProcFTPTestMixin, TestFtpCmdsSemantic): + pass + + +class TestFtpFsOperationsMProcMixin(MProcFTPTestMixin, TestFtpFsOperations): + + def test_unforeseen_mdtm_event(self): + pass + + +class TestFtpStoreDataMProcMixin(MProcFTPTestMixin, TestFtpStoreData): + pass + + +class TestFtpRetrieveDataMProcMixin(MProcFTPTestMixin, TestFtpRetrieveData): + pass + + +class TestFtpListingCmdsMProcMixin(MProcFTPTestMixin, TestFtpListingCmds): + pass + + +class TestFtpAbortMProcMixin(MProcFTPTestMixin, TestFtpAbort): + pass + + +# class TestTimeoutsMProcMixin(MProcFTPTestMixin, TestTimeouts): +# def test_data_timeout_not_reached(self): pass +# class TestConfiOptsMProcMixin(MProcFTPTestMixin, TestConfigurableOptions): +# pass +# class TestCallbacksMProcMixin(MProcFTPTestMixin, TestCallbacks): pass + + +class TestIPv4EnvironmentMProcMixin(MProcFTPTestMixin, TestIPv4Environment): + pass + + +class TestIPv6EnvironmentMProcMixin(MProcFTPTestMixin, TestIPv6Environment): + pass + + +class TestCornerCasesMProcMixin(MProcFTPTestMixin, TestCornerCases): + pass + + +# class TestFTPServerMProcMixin(MProcFTPTestMixin, TestFTPServer): +# pass + + +configure_logging() +remove_test_files() + + +if __name__ == '__main__': + unittest.main(verbosity=VERBOSITY) diff --git a/ftp_server/start.sh b/ftp_server/start.sh new file mode 100644 index 00000000..d87a03a9 --- /dev/null +++ b/ftp_server/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ftp_server.py start diff --git a/ftp_server/stop.sh b/ftp_server/stop.sh new file mode 100644 index 00000000..e3e41d0d --- /dev/null +++ b/ftp_server/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ftp_server.py stop diff --git a/gnu_apps/extensible_ui_ref_app/README.md b/gnu_apps/extensible_ui_ref_app/README.md deleted file mode 100644 index 882d0e2c..00000000 --- a/gnu_apps/extensible_ui_ref_app/README.md +++ /dev/null @@ -1,154 +0,0 @@ -#Router SDK Dynamic UI reference application and development tools.# - ----------- - -### IMPORTANT: ### -This folder contains an SDK app that uses GNU make to build and install. The SDK python tools cannot be used this application. - -Prior to using 'make' execute the following based on you OS: - -- Linux - - sudo apt-get install libffi-dev - - sudo apt-get install libssl-dev - - sudo apt-get install curl - - sudo apt-get install python3-pip - - sudo python3 -m pip install -U cryptography -- OS X - - install python 3.5.1 from python.org - - pip3 install pyopenssl - - ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" - - brew install curl - - brew install openssl - - export COPYFILE_DISABLE=1 - - excludes that "*._" files from the tar.gz. package in OS X. - ----------- - -###Available GNU make targets### - -**default:** - Build and test the router SDK reference app and create the archive - file suitable for deployment to a router DEV mode or for uploading - to ECM. - - **clean:** - Clean all project artifacts. Entails execution of all "-clean" - make targets. - - **build:** - PEP8 and PEP257 validation of all python source. - - **package:** - Create the application archive tar.gz file. - - **status:** - Fetch and print current SDK app status from the locally connected - router. - - **install:** - Secure copy the application archive to a locally connected router. - The router must already be in SDK DEV mode via registration and - licensing in ECM. - - **start:** - Start the application on the locally connected router. - - **stop:** - Stop the application on the locally connected router. - - **uninstall:** - Uninstall the application from the locally connected router. - - **purge:** - Purge all applications from the locally connected router. - - ----------- - -# HOW-TO Steps for running the reference application on your router. # - -*The Dynamic UI is supported in Firmware Version: 6.3.0 and above* - -1. Register your router with ECM. - -2. Put your router into SDK DEV mode via ECM. - -3. Export the following variables in your environment: - - DEV_CLIENT_MAC - The mac address of your router - DEV_CLIENT_IP - The lan ip address of your router - - Example: - - $ export DEV_CLIENT_MAC=44224267 - $ export DEV_CLIENT_IP=192.168.20.1 - -4. Build the SDK environment. - - $ make - -5. Test connectivity with your router via the 'status' target. - - $ make status - curl -s --digest --insecure -u admin:441dbbec \ - -H "Accept: application/json" \ - -X GET http://192.168.0.1/api/status/system/sdk | \ - /usr/bin/env python3 -m json.tool - { - "data": {}, - "success": true - } - -6. Build, test and install the reference application on your router. - - $ make install - scp /home/sfisher/dev/sdk/hspt.tar.gz admin@192.168.0.1:/app_upload - admin@192.168.0.1's password: - hspt.tar.gz 100% 1439 1.4KB/s 00:00 - Received disconnect from 192.168.0.1: 11: Bye Bye - lost connection - -7. Get application execution status from your router. - - $ make status - curl -s --digest --insecure -u admin:441dbbec \ - -H "Accept: application/json" \ - -X GET http://192.168.0.1/api/status/system/sdk | \ - /usr/bin/env python3 -m json.tool - { - "data": { - "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c": { - "app": { - "date": "2015-12-04T09:30:39.656151", - "name": "hspt", - "restart": true, - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "vendor": "Cradlebox", - "version_major": 1, - "version_minor": 1 - }, - "base_directory": "/var/mnt/sdk/apps/7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "directory": hspt", - "filename": "dist/tmp_znv2t", - "state": "started", - "summary": "Package started successfully", - "type": "development", - "url": "file:///var/tmp/tmpg1385l", - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c" - } - }, - "success": true - } - -8. Uninstall the reference application from your router. - - $ make uninstall - curl -s --digest --insecure -u admin:441dbbec \ - -H "Accept: application/json" \ - -X PUT http://192.168.0.1/api/control/system/sdk/action \ - -d data='"uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"' | \ - /usr/bin/env python3 -m json.tool - { - "data": "uninstall 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "success": true - } diff --git a/gnu_apps/extensible_ui_ref_app/hspt/cs.py b/gnu_apps/extensible_ui_ref_app/hspt/cs.py deleted file mode 100644 index d8e28436..00000000 --- a/gnu_apps/extensible_ui_ref_app/hspt/cs.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -Copyright (c) 2016 CradlePoint, Inc. . All rights -reserved. - -This file contains confidential information of CradlePoint, Inc. and your use -of this file is subject to the CradlePoint Software License Agreement -distributed with this file. Unauthorized reproduction or distribution of this -file is subject to civil and criminal penalties. -""" - -import re -import json -import socket -import subprocess - -def log(msg): - subprocess.run(['logger', msg]) - -class CSClient(object): - """Wrapper for the TCP interface to the router config store.""" - - def get(self, base, query='', tree=0): - """Send a get request.""" - cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) - return self._dispatch(cmd) - - def put(self, base, value='', query='', tree=0): - """Send a put request.""" - value = json.dumps(value).replace(' ', '') - cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) - return self._dispatch(cmd) - - def append(self, base, value='', query=''): - """Send an append request.""" - value = json.dumps(value).replace(' ', '') - cmd = "post\n{}\n{}\n{}\n".format(base, query, value) - return self._dispatch(cmd) - - def delete(self, base, query=''): - """Send a delete request.""" - cmd = "delete\n{}\n{}\n".format(base, query) - return self._dispatch(cmd) - - def alert(self, name='', value=''): - """Send a request to create an alert.""" - cmd = "alert\n{}\n{}\n".format(name, value) - return self._dispatch(cmd) - - def log(self, name='', value=''): - """Send a request to create a log entry.""" - cmd = "log\n{}\n{}\n".format(name, value) - return self._dispatch(cmd) - - def _dispatch(self, cmd): - """Send the command and return the response.""" - resl = '' - with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: - sock.connect('/var/tmp/cs.sock') - sock.sendall(bytes(cmd, 'ascii')) - - if str(sock.recv(1024), 'ascii').strip() == 'status: ok': - recv_mesg = str(sock.recv(1024), 'ascii').strip().split(' ')[1] - try: - mlen = int(recv_mesg) - except ValueError: - m = re.search("([0-9]*)", recv_mesg) - mlen = int(m.group(0)) - if str(sock.recv(1024), 'ascii') == '\r\n\r\n': - while mlen > 0: - resl += str(sock.recv(1024), 'ascii') - mlen -= 1024 - return resl diff --git a/gnu_apps/extensible_ui_ref_app/hspt/installer.py b/gnu_apps/extensible_ui_ref_app/hspt/installer.py deleted file mode 100644 index b0a26a41..00000000 --- a/gnu_apps/extensible_ui_ref_app/hspt/installer.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Copyright (c) 2016 CradlePoint, Inc. . All rights -reserved. - -This file contains confidential information of CradlePoint, Inc. and your use -of this file is subject to the CradlePoint Software License Agreement -distributed with this file. Unauthorized reproduction or distribution of this -file is subject to civil and criminal penalties. -""" -import os -import sys -import argparse -import subprocess -sys.path.append('.') - -import cs - -path = '/control/system/httpserver' - -route_map = [] - -def log(msg): - subprocess.run(['logger', msg]) - - -def mkroutes(route, directory): - location = "%s/%s" % (os.getcwd(), directory) - route_map.append((route, location)) - - -def action(command): - client = cs.CSClient() - - value = { - 'action': command, - 'routes': route_map, - 'server': 'hotspotServer' - } - client.put(path, value) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('opt') - args = parser.parse_args() - - # Build route maps for / and /resources - mkroutes('/(.*)', '') - mkroutes('/resources/(.*)', 'resources/') - - if args.opt not in ['start', 'stop']: - log('failed to run command') - exit() - - action(args.opt) diff --git a/gpio/power/__init__.py b/gpio/power/__init__.py deleted file mode 100644 index 19caa001..00000000 --- a/gpio/power/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from gpio.power.power import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/gpio/power/power.py b/gpio/power/power.py deleted file mode 100644 index 1d30521b..00000000 --- a/gpio/power/power.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Query the 2x2 power connector I/O -""" -import sys -import time - -from cp_lib.app_base import CradlepointAppBase - -DEF_INPUT_NAME = "status/gpio/CGPIO_CONNECTOR_INPUT" -DEF_OUTPUT_NAME = "status/gpio/CGPIO_CONNECTOR_OUTPUT" -DEF_LOOP_DELAY = 15 - - -def run_router_app(app_base, loop=True): - """ - - :param CradlepointAppBase app_base: the prepared resources: logger, - cs_client, settings, etc - :param bool loop: T to loop, else exit - :return: - """ - - # confirm we are running on 1100/1150, result should be like "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info("Product Model is good:{}".format(result)) - else: - app_base.logger.error("Inappropriate Product:{} - aborting.".format( - result)) - return -1 - - input_name = DEF_INPUT_NAME - output_name = DEF_OUTPUT_NAME - loop_delay = DEF_LOOP_DELAY - if "gpio_names" in app_base.settings: - if "input_name" in app_base.settings["gpio_names"]: - input_name = app_base.settings["gpio_names"]["input_name"] - if "output_name" in app_base.settings["gpio_names"]: - output_name = app_base.settings["gpio_names"]["output_name"] - loop_delay = float(app_base.settings["gpio_names"].get("loop_delay", - 15)) - - if not loop: - # then we'll jump from loop - likely running as test on a PC computer - loop_delay = None - - app_base.logger.info("GPIO 2x2 input name:{}".format(input_name)) - app_base.logger.info("GPIO 2x2 output name:{}".format(output_name)) - - while True: - - # self.state = self.client.get('/status/gpio/%s' % self.name) - result_in = app_base.cs_client.get(input_name) - result_out = app_base.cs_client.get(output_name) - app_base.logger.info("In={} Out={}".format(result_in, result_out)) - - if loop_delay is None: - # do an invert of output - if result_out in (1, "1"): - value = 0 - else: - value = 1 - - # self.client.put('/control/gpio', {self.name: self.state}) - result = app_base.cs_client.put("control/gpio", - {"CGPIO_CONNECTOR_OUTPUT": value}) - - if value != int(result): - app_base.logger.error("BAD - change failed {}".format(result)) - - # else app_base.logger.info("Change was GOOD {}".format(result)) - - break - else: - app_base.logger.debug("Looping - delay {} seconds".format( - loop_delay)) - time.sleep(loop_delay) - - return 0 - - -if __name__ == "__main__": - - my_app = CradlepointAppBase("gpio/power") - - _result = run_router_app(my_app, loop=False) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/gpio/power/settings.ini b/gpio/power/settings.ini deleted file mode 100644 index 558e35e2..00000000 --- a/gpio/power/settings.ini +++ /dev/null @@ -1,11 +0,0 @@ -[application] -name=power -description=Read the status of the 2x2 power connector -path=gpio/power -uuid=11c5bf3b-b0b0-408e-ba76-dbe436ec308d -version=1.0 - -[gpio_names] -input_name=status/gpio/CGPIO_CONNECTOR_INPUT -output_name=status/gpio/CGPIO_CONNECTOR_OUTPUT -loop_delay=10 diff --git a/gpio/serial_gpio/__init__.py b/gpio/serial_gpio/__init__.py deleted file mode 100644 index 4f079997..00000000 --- a/gpio/serial_gpio/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from gpio.serial_gpio.serial_gpio import read_gpio - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to read_gpio()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = read_gpio(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/gpio/serial_gpio/serial_gpio.py b/gpio/serial_gpio/serial_gpio.py deleted file mode 100644 index 757bb417..00000000 --- a/gpio/serial_gpio/serial_gpio.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Query the 2x2 power connector I/O -""" -import sys -import time - -from cp_lib.app_base import CradlepointAppBase - -DEF_SERIAL_NAME_1 = "status/gpio/CGPIO_SERIAL_INPUT_1" -DEF_SERIAL_NAME_2 = "status/gpio/CGPIO_SERIAL_INPUT_2" -DEF_SERIAL_NAME_3 = "status/gpio/CGPIO_SERIAL_INPUT_3" - -DEF_LOOP_DELAY = 15 - - -def read_gpio(app_base, loop=True): - """ - - :param CradlepointAppBase app_base: the prepared resources: logger, cs_client, settings, etc - :param bool loop: T to loop, else exit - :return: - """ - - # confirm we are running on an 1100/1150, result should be like "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info("Product Model is good:{}".format(result)) - else: - app_base.logger.error("Inappropriate Product:{} - aborting.".format(result)) - return -1 - - loop_delay = DEF_LOOP_DELAY - if "gpio_names" in app_base.settings: - loop_delay = float(app_base.settings["gpio_names"].get("loop_delay", 15)) - - if not loop: - # then we'll jump from loop - likely we are running as test on a PC computer - loop_delay = None - - # confirm we have the 1100 serial GPIO enabled, so return is True or False - result = app_base.cs_client.get_bool("status/system/gpio_actions/") - app_base.logger.debug("GET: status/system/gpio_actions/ = [{}]".format(result)) - if result is None: - # then the serial GPIO function is NOT enabled - app_base.logger.error("The Serial GPIO is NOT enabled!") - app_base.logger.info("Router Application is exiting") - return -1 - - while loop_delay: - # loop as long as not None or zero - result1 = app_base.cs_client.get(DEF_SERIAL_NAME_1) - result2 = app_base.cs_client.get(DEF_SERIAL_NAME_2) - result3 = app_base.cs_client.get(DEF_SERIAL_NAME_3) - app_base.logger.info("Inp = ({}, {}, {})".format(result1, result2, result3)) - - app_base.logger.debug("Looping - delay {} seconds".format(loop_delay)) - time.sleep(loop_delay) - - app_base.logger.info("Router Application is exiting") - return 0 - - -if __name__ == "__main__": - my_app = CradlepointAppBase("gpio/serial_gpio") - - _result = read_gpio(my_app, loop=False) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - - sys.exit(_result) diff --git a/gpio/serial_gpio/settings.ini b/gpio/serial_gpio/settings.ini deleted file mode 100644 index b7247a14..00000000 --- a/gpio/serial_gpio/settings.ini +++ /dev/null @@ -1,6 +0,0 @@ -[application] -name=serial_gpio -description=Read the status of the 1100 serial inputs -path=gpio/serial_gpio -uuid=0bbe8bcf-060e-4c9c-a141-48f5daf2a231 -version=1.0 diff --git a/gps/README.md b/gps/README.md deleted file mode 100644 index 8bee935e..00000000 --- a/gps/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Router App/SDK sample applications. - -## Directory gps_localhost - -Assuming the Cradlepoint router is configured to forward NMEA sentences -to a localhost port, open the port as a server and receive the streaming -GSP data. This code can be run on either a PC or Router. - -## Directory probe_gps - -Walk through the router API 'status' and 'config' trees, returning -a list of text strings showing if any GPS source exists, if there is -existing last-seen data, and so on. diff --git a/gps/gps_localhost/README.md b/gps/gps_localhost/README.md deleted file mode 100644 index 6566e912..00000000 --- a/gps/gps_localhost/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# directory: ./gps/gps_localhost -## Router App/SDK sample applications - -Received GPS, assuming the router's GPS function sends new data (sentences) -to a localhost port - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: gps_localhost.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [gps]: - -* host_port=9999, define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. diff --git a/gps/gps_localhost/__init__.py b/gps/gps_localhost/__init__.py deleted file mode 100644 index 4b655fa8..00000000 --- a/gps/gps_localhost/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from gps.gps_localhost.gps_localhost import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/gps/gps_localhost/gps_localhost.py b/gps/gps_localhost/gps_localhost.py deleted file mode 100644 index a72d557a..00000000 --- a/gps/gps_localhost/gps_localhost.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Received GPS, assuming the router's GPS function sends new data (sentences) -to a localhost port -""" - -import socket -import time -import gc - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.gps_nmea import NmeaStatus -from cp_lib.parse_data import parse_boolean - - -DEF_HOST_IP = 'localhost' -DEF_HOST_PORT = 9999 -DEF_BUFFER_SIZE = 1024 - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - host_ip = DEF_HOST_IP - host_port = DEF_HOST_PORT - buffer_size = DEF_BUFFER_SIZE - - gps = NmeaStatus() - - section = "gps" - if section in app_base.settings: - # then load dynamic values - host_ip = app_base.settings[section].get("host_ip", DEF_HOST_IP) - host_port = int(app_base.settings[section].get("host_port", - DEF_HOST_PORT)) - buffer_size = int(app_base.settings[section].get("buffer_size", - DEF_BUFFER_SIZE)) - - gps.date_time = parse_boolean( - app_base.settings[section].get("date_time", gps.DEF_DATE_TIME)) - gps.speed = parse_boolean( - app_base.settings[section].get("speed", gps.DEF_SPEED)) - gps.altitude = parse_boolean( - app_base.settings[section].get("altitude", gps.DEF_ALTITUDE)) - gps.coor_ddmm = parse_boolean( - app_base.settings[section].get("coor_ddmm", gps.DEF_COOR_DDMM)) - gps.coor_dec = parse_boolean( - app_base.settings[section].get("coor_dec", gps.DEF_COOR_DEC)) - - while True: - # define the socket resource, including the type (stream == "TCP") - address = (host_ip, host_port) - app_base.logger.info("Preparing GPS Listening on {}".format(address)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # attempt to actually lock resource, which may fail if unavailable - # (see BIND ERROR note) - try: - sock.bind(address) - except OSError as msg: - app_base.logger.error("socket.bind() failed - {}".format(msg)) - - # technically, Python will close when 'sock' goes out of scope, - # but be disciplined and close it yourself. Python may warning - # you of unclosed resource, during runtime. - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors - # are likely permanent. - return -1 - - # only allow 1 client at a time - sock.listen(3) - - while True: - # loop forever - app_base.logger.info("Waiting on TCP socket %d" % host_port) - client, address = sock.accept() - app_base.logger.info("Accepted connection from {}".format(address)) - - # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # set non-blocking so we can do a manual timeout (use of select() - # is better ... but that's another sample) - # client.setblocking(0) - - while True: - app_base.logger.debug("Waiting to receive data") - data = client.recv(buffer_size) - # data is type() bytes, to echo we don't need to convert - # to str to format or return. - if data: - data = data.decode().split() - - gps.start() - for line in data: - result = gps.parse_sentence(line) - if not result: - break - gps.publish() - - app_base.logger.debug( - "See({})".format(gps.get_attributes())) - # client.send(data) - else: - break - - time.sleep(1.0) - - app_base.logger.info("Client disconnected") - client.close() - - # since this server is expected to run on a small embedded system, - # free up memory ASAP (see MEMORY note) - del client - gc.collect() - - return 0 - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("gps/gps_localhost") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/gps/probe_gps/__init__.py b/gps/probe_gps/__init__.py deleted file mode 100644 index 8a4fc5e8..00000000 --- a/gps/probe_gps/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -import time -from cp_lib.app_base import CradlepointAppBase -from gps.probe_gps.probe_gps import probe_gps - -SLEEP_DELAY = 60.0 - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to probe_gps()") - - # if user included a different timeout in settings - try: - delay = float(self.settings['probe_gps']['delay']) - - except (KeyError, ValueError): - delay = SLEEP_DELAY - - # we do this wrap to dump any Python exception traceback out to Syslog - result = -1 - while True: - try: - result = probe_gps(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - self.logger.debug("sleep to delay for {} seconds".format(delay)) - time.sleep(delay) - - return result diff --git a/gps/probe_gps/settings.ini b/gps/probe_gps/settings.ini deleted file mode 100644 index 5a3e85f8..00000000 --- a/gps/probe_gps/settings.ini +++ /dev/null @@ -1,10 +0,0 @@ -[application] -name=probe_gps -description=Query router status tree - discover if GPS is available and active -path=gps/probe_gps -uuid=8f828277-5e8f-4c90-9b99-aeebf3eb61f3 -version=1.2 - -[probe_gps] -filename=.gps.txt -delay=60.0 diff --git a/gps_localhost/cs.py b/gps_localhost/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/gps_localhost/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/gps_localhost/gps_localhost.py b/gps_localhost/gps_localhost.py new file mode 100644 index 00000000..a46d8dd4 --- /dev/null +++ b/gps_localhost/gps_localhost.py @@ -0,0 +1,141 @@ +""" +Assuming the Cradlepoint router is configured to forward NMEA sentences +to a localhost port, open the port as a server and receive the streaming +GSP data. + +""" + +import argparse +import socket +import time +import gc +import cs +import gps_nmea + +APP_NAME = 'gps_localhost' + + +def run_router_app(): + """ + """ + # cs.CSClient().log(APP_NAME, "Settings({})".format(sets)) + + host_ip = 'localhost' + host_port = 9999 + buffer_size = 1024 + + gps = gps_nmea.NmeaStatus() + + # set True to parse GPS time (as ['gps_utc']), else omit + gps.date_time = False + # set True to parse speed over ground (as ['knots'] & ['kmh']), else omit + gps.speed = True + # set True to parse altitude (as ['alt'], else omit + gps.altitude = False + # set True to include DDMM.MM string (as ['lat_ddmm' & 'long_ddmm'], else omit + gps.coor_ddmm = False + # set True to parse lat/log as decimal (as ['lat', 'long]), else omit + gps.coor_dec = True + + while True: + # define the socket resource, including the type (stream == "TCP") + address = (host_ip, host_port) + cs.CSClient().log(APP_NAME, "Preparing GPS Listening on {}".format(address)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # attempt to actually lock resource, which may fail if unavailable + # (see BIND ERROR note) + try: + sock.bind(address) + except OSError as msg: + cs.CSClient().log(APP_NAME, "socket.bind() failed - {}".format(msg)) + + # technically, Python will close when 'sock' goes out of scope, + # but be disciplined and close it yourself. Python may warning + # you of unclosed resource, during runtime. + try: + sock.close() + except OSError: + pass + + # we exit, because if we cannot secure the resource, the errors + # are likely permanent. + return -1 + + # only allow 1 client at a time + sock.listen(3) + + while True: + # loop forever + cs.CSClient().log(APP_NAME, "Waiting on TCP socket %d" % host_port) + client, address = sock.accept() + cs.CSClient().log(APP_NAME, "Accepted connection from {}".format(address)) + + # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + # set non-blocking so we can do a manual timeout (use of select() + # is better ... but that's another sample) + # client.setblocking(0) + + while True: + cs.CSClient().log(APP_NAME, "Waiting to receive data") + data = client.recv(buffer_size) + # data is type() bytes, to echo we don't need to convert + # to str to format or return. + if data: + data = data.decode().split() + + gps.start() + for line in data: + result = gps.parse_sentence(line) + if not result: + break + gps.publish() + + cs.CSClient().log(APP_NAME, + "See({})".format(gps.get_attributes())) + # client.send(data) + else: + break + + time.sleep(1.0) + + cs.CSClient().log(APP_NAME, "Client disconnected") + client.close() + + # since this server is expected to run on a small embedded system, + # free up memory ASAP (see MEMORY note) + del client + gc.collect() + + return 0 + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + run_router_app() + + elif command == 'stop': + # Nothing on stop + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/cp_lib/gps_nmea.py b/gps_localhost/gps_nmea.py similarity index 100% rename from cp_lib/gps_nmea.py rename to gps_localhost/gps_nmea.py diff --git a/gps_localhost/install.sh b/gps_localhost/install.sh new file mode 100644 index 00000000..8a6f4696 --- /dev/null +++ b/gps_localhost/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION gps_localhost on:" >> install.log +date >> install.log diff --git a/gps_localhost/package.ini b/gps_localhost/package.ini new file mode 100644 index 00000000..d665b0c3 --- /dev/null +++ b/gps_localhost/package.ini @@ -0,0 +1,11 @@ +[gps_localhost] +uuid=d2f51bb3-9f5f-414d-bf7e-82e2210d5e7b +vendor=Cradlepoint +notes=Receive GPS sentences on localhost port. +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/gps/gps_localhost/settings.ini b/gps_localhost/settings.ini similarity index 100% rename from gps/gps_localhost/settings.ini rename to gps_localhost/settings.ini diff --git a/gps_localhost/start.sh b/gps_localhost/start.sh new file mode 100644 index 00000000..ae239b25 --- /dev/null +++ b/gps_localhost/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython gps_localhost.py start diff --git a/gps_localhost/stop.sh b/gps_localhost/stop.sh new file mode 100644 index 00000000..a32d1a0c --- /dev/null +++ b/gps_localhost/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython gps_localhost.py stop diff --git a/gps_probe/cs.py b/gps_probe/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/gps_probe/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/gps/probe_gps/probe_gps.py b/gps_probe/gps_probe.py similarity index 59% rename from gps/probe_gps/probe_gps.py rename to gps_probe/gps_probe.py index 464e3061..f4854dde 100644 --- a/gps/probe_gps/probe_gps.py +++ b/gps_probe/gps_probe.py @@ -1,42 +1,40 @@ """ -Probe the GPS hardware, return a report as list of text, and actual ASCII file +Probe the GPS hardware and log the results. + +Walk through the router API 'status' and 'config' trees, returning +a list of text strings showing if any GPS source exists, if there is +existing last-seen data, and so on. + """ +import argparse import json import sys import time +import cs -from cp_lib.app_base import CradlepointAppBase -# from cp_lib.hw_status import am_running_on_router +APP_NAME = 'gps_probe' -def probe_gps(app_base, save_file=None): +def probe_gps(): """ - The main GPS task. - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param str save_file: If a name, then save as text file - :return int: + Probe for GPS data and log the results. """ message = "Probing GPS HW - {}".format(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) report_lines = [message] - if "probe_gps" in app_base.settings: - if "filename" in app_base.settings["probe_gps"]: - save_file = app_base.settings["probe_gps"]["filename"] - # check the type of Router we are running on try: - result = app_base.settings["product_info"]["product_name"] + result = json.loads(cs.CSClient().get("status/product_info/product_name")) message = "Router Model:{}".format(result) report_lines.append(message) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) except KeyError: - app_base.logger.error("App Base is missing 'product_info'") + cs.CSClient().log(APP_NAME, "App Base is missing 'product_info'") - result = app_base.cs_client.get("config/system/gps") + result = json.loads(cs.CSClient().get("config/system/gps")) if not isinstance(result, dict): # some error? gps_enabled = False @@ -53,30 +51,18 @@ def probe_gps(app_base, save_file=None): else: message = "GPS Function is NOT Enabled" report_lines.append(message) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) if result.get("enable_gps_keepalive", False): message = "GPS Keepalive is Enabled" else: message = "GPS Keepalive is NOT Enabled" report_lines.append(message) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) if gps_enabled: # only do this if enabled! - app_base.cs_client.show_rsp = False - result = app_base.cs_client.get("status/wan/devices") - app_base.cs_client.show_rsp = True - # logger.debug("Type(result):{}".format(type(result))) - # logger.debug("Result:{}".format(result)) - - # if not am_running_on_router(): - if sys.platform == "win32": - file_name = ".dump.json" - app_base.logger.debug("Save file:{}".format(file_name)) - file_han = open(file_name, "w") - file_han.write(json.dumps(result, ensure_ascii=True, indent=4)) - file_han.close() + result = json.loads(cs.CSClient().get("status/wan/devices")) gps_sources = [] for key in result: @@ -91,10 +77,10 @@ def probe_gps(app_base, save_file=None): gps_sources.append(key) else: # for example, inactive w/o GPS will be only {} - message = "Modem named \"{0}\" lacks GPS data".format( - key) + message = "Modem named \"{0}\" lacks GPS data".format(key) + report_lines.append(message) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) except KeyError: # for example, the WAN device 'ethernet-wan' will LACK # the key ["info"]["supports_gps"] @@ -106,10 +92,10 @@ def probe_gps(app_base, save_file=None): elif len(gps_sources) == 1: message = "Router has 1 modem claiming to have GPS data" else: - message = "Router has {0} modems claiming GPS data".format( - len(gps_sources)) + message = "Router has {0} modems claiming GPS data".format(len(gps_sources)) + report_lines.append(message) - app_base.logger.info(message) + cs.CSClient().log(APP_NAME, message) # {'fix': { # 'age': 57.38595999999962, @@ -132,41 +118,44 @@ def probe_gps(app_base, save_file=None): if len(gps_sources) > 0: value = result[gps_sources[0]]["status"]["gps"] message = "GPS data follows" - app_base.logger.debug(message) + cs.CSClient().log(APP_NAME, message) report_lines.append(message) json_value = json.dumps(value, ensure_ascii=True, indent=4) json_lines = json_value.split(sep='\n') for line in json_lines: - app_base.logger.debug(line) + cs.CSClient().log(APP_NAME, line) + # Save all the gps data in report_lines. This can + # be saved to a file if needed. report_lines.append(line) - if save_file is not None: - # if am_running_on_router(): - if sys.platform != "win32": - pass - # app_base.logger.error( - # Skip save to file - am running on router.") - else: - app_base.logger.debug("Save file:{}".format(save_file)) - file_han = open(save_file, "w") - for line in report_lines: - file_han.write(line + '\n') - file_han.close() - return 0 -if __name__ == "__main__": - import logging +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) - # get this started, else we don't see anything for a while - logging.basicConfig(level=logging.DEBUG) + if command == 'start': + probe_gps() - my_app = CradlepointAppBase("gps/probe_gps") + elif command == 'stop': + # Nothing on stop + pass - _result = probe_gps(my_app) + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() - my_app.logger.info("Exiting, status code is {}".format(_result)) + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() - sys.exit(_result) + action(args.opt) diff --git a/gps_probe/install.sh b/gps_probe/install.sh new file mode 100644 index 00000000..f97d79ea --- /dev/null +++ b/gps_probe/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION gps_probe on:" >> install.log +date >> install.log diff --git a/gps_probe/package.ini b/gps_probe/package.ini new file mode 100644 index 00000000..8f8b2b72 --- /dev/null +++ b/gps_probe/package.ini @@ -0,0 +1,11 @@ +[gps_probe] +uuid=887552c5-ba26-49fb-80df-6e2ad1265eae +vendor=Cradlepoint +notes=GPS Reference Application: Probe for GPS data if it's enabled. +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/gps_probe/start.sh b/gps_probe/start.sh new file mode 100644 index 00000000..ed58b162 --- /dev/null +++ b/gps_probe/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython gps_probe.py start diff --git a/gps_probe/stop.sh b/gps_probe/stop.sh new file mode 100644 index 00000000..fdf24d9d --- /dev/null +++ b/gps_probe/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython gps_probe.py stop diff --git a/hello_world/cs.py b/hello_world/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/hello_world/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/hello_world/hello_world.py b/hello_world/hello_world.py new file mode 100644 index 00000000..2d80c2c3 --- /dev/null +++ b/hello_world/hello_world.py @@ -0,0 +1,34 @@ +''' +Outputs a 'Hello World!' log every 10 seconds. +''' + +import argparse +import time +import cs +import subprocess + +APP_NAME = 'hello_world' + + +def action(command): + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + while True: + cs.CSClient().log(APP_NAME, 'Hello World!') + time.sleep(10) + + elif command == 'stop': + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/hello_world/install.sh b/hello_world/install.sh new file mode 100644 index 00000000..66afc23b --- /dev/null +++ b/hello_world/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION hello_world on:" >> install.log +date >> install.log diff --git a/hello_world/package.ini b/hello_world/package.ini new file mode 100644 index 00000000..34331686 --- /dev/null +++ b/hello_world/package.ini @@ -0,0 +1,11 @@ +[hello_world] +uuid=616acd0c-0475-479e-a33b-f7054843c971 +vendor=Cradlepoint +notes=Router SDK Hello World Application +firmware_major=7 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/hello_world/start.sh b/hello_world/start.sh new file mode 100644 index 00000000..25646ed8 --- /dev/null +++ b/hello_world/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython hello_world.py start diff --git a/hello_world/stop.sh b/hello_world/stop.sh new file mode 100644 index 00000000..7204ba45 --- /dev/null +++ b/hello_world/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython hello_world.py stop diff --git a/hspt/cs.py b/hspt/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/hspt/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/gnu_apps/extensible_ui_ref_app/dynui.txt b/hspt/dynui.txt similarity index 100% rename from gnu_apps/extensible_ui_ref_app/dynui.txt rename to hspt/dynui.txt diff --git a/gnu_apps/extensible_ui_ref_app/hspt/index.tpl.html b/hspt/index.tpl.html similarity index 59% rename from gnu_apps/extensible_ui_ref_app/hspt/index.tpl.html rename to hspt/index.tpl.html index 236825c6..f15c3448 100644 --- a/gnu_apps/extensible_ui_ref_app/hspt/index.tpl.html +++ b/hspt/index.tpl.html @@ -16,6 +16,14 @@

Dynamic UI example

This is a static webpage to demonstrate the use of Dynamic UI SDK technology. This page can be used to display a Terms of Service for Cradlepoint's Hotspot functionality.

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut id tempor risus, id efficitur + est. Fusce eu velit tincidunt, condimentum tellus sed, interdum arcu. Curabitur id dolor non ex + commodo faucibus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur + ridiculus mus. Proin ac sem ligula. Suspendisse et fermentum ipsum, id fringilla nulla. Vivamus + sit amet pulvinar magna, vitae posuere eros. Praesent eget mauris eu risus viverra gravida. + Integer efficitur nec lectus scelerisque lacinia. Cras massa arcu, efficitur vitae eros eu, + feugiat aliquet ante.

diff --git a/gnu_apps/extensible_ui_ref_app/hspt/install.sh b/hspt/install.sh similarity index 100% rename from gnu_apps/extensible_ui_ref_app/hspt/install.sh rename to hspt/install.sh diff --git a/hspt/installer.py b/hspt/installer.py new file mode 100644 index 00000000..d65cb869 --- /dev/null +++ b/hspt/installer.py @@ -0,0 +1,56 @@ +""" +Copyright (c) 2016 CradlePoint, Inc. . All rights +reserved. + +This file contains confidential information of CradlePoint, Inc. and your use +of this file is subject to the CradlePoint Software License Agreement +distributed with this file. Unauthorized reproduction or distribution of this +file is subject to civil and criminal penalties. +""" +import os +import sys +import argparse +import subprocess +sys.path.append('.') + +import cs + +path = '/control/system/httpserver' + +route_map = [] + + +def log(msg): + subprocess.run(['logger', msg]) + + +def mkroutes(route, directory): + location = "%s/%s" % (os.getcwd(), directory) + route_map.append((route, location)) + + +def action(command): + client = cs.CSClient() + + value = { + 'action': command, + 'routes': route_map, + 'server': 'hotspotServer' + } + client.put(path, value) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + # Build route maps for / and /resources + mkroutes('/(.*)', '') + mkroutes('/resources/(.*)', 'resources/') + + if args.opt not in ['start', 'stop']: + log('failed to run command') + exit() + + action(args.opt) diff --git a/gnu_apps/extensible_ui_ref_app/hspt/package.ini b/hspt/package.ini similarity index 90% rename from gnu_apps/extensible_ui_ref_app/hspt/package.ini rename to hspt/package.ini index cdee118a..628ba466 100644 --- a/gnu_apps/extensible_ui_ref_app/hspt/package.ini +++ b/hspt/package.ini @@ -1,6 +1,6 @@ [hspt] uuid=7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c -vendor=Cradlebox +vendor=Cradlepoint notes=Router SDK Demo Application firmware_major=6 firmware_minor=1 diff --git a/gnu_apps/extensible_ui_ref_app/hspt/resources/generic.css b/hspt/resources/generic.css similarity index 100% rename from gnu_apps/extensible_ui_ref_app/hspt/resources/generic.css rename to hspt/resources/generic.css diff --git a/gnu_apps/extensible_ui_ref_app/hspt/resources/netcloud-mobileimg.jpg b/hspt/resources/netcloud-mobileimg.jpg similarity index 100% rename from gnu_apps/extensible_ui_ref_app/hspt/resources/netcloud-mobileimg.jpg rename to hspt/resources/netcloud-mobileimg.jpg diff --git a/gnu_apps/extensible_ui_ref_app/hspt/start.sh b/hspt/start.sh similarity index 100% rename from gnu_apps/extensible_ui_ref_app/hspt/start.sh rename to hspt/start.sh diff --git a/gnu_apps/extensible_ui_ref_app/hspt/stop.sh b/hspt/stop.sh similarity index 100% rename from gnu_apps/extensible_ui_ref_app/hspt/stop.sh rename to hspt/stop.sh diff --git a/list_serial_ports/cs.py b/list_serial_ports/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/list_serial_ports/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/list_serial_ports/install.sh b/list_serial_ports/install.sh new file mode 100644 index 00000000..6f631ff6 --- /dev/null +++ b/list_serial_ports/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION list_serial_ports on:" >> install.log +date >> install.log diff --git a/list_serial_ports/list_serial_ports.py b/list_serial_ports/list_serial_ports.py new file mode 100644 index 00000000..ec2bca2a --- /dev/null +++ b/list_serial_ports/list_serial_ports.py @@ -0,0 +1,179 @@ +import argparse +import serial +import json +import cs + +''' +This sample code runs through one check, then exits. +Assuming you have 'restart' true in your settings.ini file, then +the code restarts forever. + +Based on the model of router you have, it checks to see if the ports below +exist, can be opened, and the name of the port written to it. + +Real Physical ports: + +* /dev/ttyS1 (only on models such as IBR1100) +* /dev/ttyS2 (normally will fail/not exist) + +USB serial ports: + +* /dev/ttyUSB0 +* /dev/ttyUSB1 +* /dev/ttyUSB2 +* /dev/ttyUSB3 +* /dev/ttyUSB4 + +Most USB-serial devices with an FTDI-chipset can be used. +''' + +# this exists on most, but is for internal use. +IGNORE_TTYS0 = True + +PORT_LIST_PHYSICAL = (1, 2) +PORT_LIST_USB = (0, 1, 2, 3, 4) + +APP_NAME = 'list_serial_ports' + + +def run_router_app(): + """ + Do the probe/check of + """ + + # as of Mat-2016/FW=6.1, PySerial is version 2.6 (2.6-pre1) + cs.CSClient().log(APP_NAME, "serial.VERSION = {}.".format(serial.VERSION)) + + # probe_physical = True, set False to NOT probe real physical serial ports. + # On models without physical ports, this setting is ignored. + probe_physical = True + + # probe_usb = True, set False to NOT probe for USB serial ports. + probe_usb = True + + # write_name = True, set False to NOT send out the port name, which is + # sent to help you identify between multiple ports. + write_name = False + + # probe_directory(app_base, "/dev") + port_list = [] + + # confirm we are running on an 1100/1150 or 900/950, result should be "IBR1100LPE" + result = json.loads(cs.CSClient().get("status/product_info/product_name")) + if "IBR1100" in result or "IBR1150" in result or "IBR900" in result or "IBR950" in result: + name = "/dev/ttyS1" + cs.CSClient().log(APP_NAME, "Product Model {} has 1 builtin port:{}".format(result, name)) + port_list.append(name) + + elif "IBR300" in result or "IBR350" in result: + cs.CSClient().log(APP_NAME, "Product Model {} has no serial support".format(result)) + + else: + cs.CSClient().log(APP_NAME, "Inappropriate Product:{} - aborting.".format(result)) + return -1 + + if probe_physical: + # fixed ports - 1100 only? + if not IGNORE_TTYS0: + # only check S0 if 'requested' to, else ignore + name = "/dev/ttyS0" + if name not in port_list: + port_list.append(name) + + for port in PORT_LIST_PHYSICAL: + name = "/dev/ttyS%d" % port + if name not in port_list: + port_list.append(name) + + if probe_usb: + # try first 5 USB + for port in PORT_LIST_USB: + name = "/dev/ttyUSB%d" % port + if name not in port_list: + port_list.append(name) + + # cycle through and probe the desired ports + for name in port_list: + probe_serial(name, write_name) + + return 0 + + +def probe_serial(port_name, write_name=False): + """ + dump a directory in router FW + """ + try: + ser = serial.Serial(port_name, dsrdtr=False, rtscts=False) + if write_name: + port_name += '\r\n' + ser.write(port_name.encode()) + + cs.CSClient().log(APP_NAME, "Port({}) exists.".format(port_name)) + + # as of Mat-2016/FW=6.1, PySerial is version 2.6 + # therefore .getDSR() works and .dsr does not! + try: + cs.CSClient().log(APP_NAME, " serial.dsr = {}.".format(ser.dsr)) + except AttributeError: + cs.CSClient().log(APP_NAME, " serial.dsr is not supported!") + + try: + cs.CSClient().log(APP_NAME, " serial.getDSR() = {}.".format(ser.getDSR())) + except AttributeError: + cs.CSClient().log(APP_NAME, " serial.getDSR() is not supported!") + + ser.close() + return True + + except (serial.SerialException, FileNotFoundError): + cs.CSClient().log(APP_NAME, "Port({}) didn't exist.".format(port_name)) + return False + + +def probe_directory(base_dir): + """ + dump a directory in router FW + """ + import os + + cs.CSClient().log(APP_NAME, "Dump Directory:{}".format(base_dir)) + result = os.access(base_dir, os.R_OK) + if result: + cs.CSClient().log(APP_NAME, "GOOD name:{}".format(base_dir)) + else: + cs.CSClient().log(APP_NAME, "BAD name:{}".format(base_dir)) + + if result: + result = os.listdir(base_dir) + for name in result: + cs.CSClient().log(APP_NAME, " file:{}".format(name)) + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + run_router_app() + + elif command == 'stop': + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/list_serial_ports/package.ini b/list_serial_ports/package.ini new file mode 100644 index 00000000..4f2c01aa --- /dev/null +++ b/list_serial_ports/package.ini @@ -0,0 +1,11 @@ +[list_serial_ports] +uuid=0268197b-1713-42d4-955f-25f04708ac21 +vendor=Cradlepoint +notes=Router Serial Reference Application. List all the serial ports. +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/list_serial_ports/start.sh b/list_serial_ports/start.sh new file mode 100644 index 00000000..87c7db6a --- /dev/null +++ b/list_serial_ports/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython list_serial_ports.py start diff --git a/list_serial_ports/stop.sh b/list_serial_ports/stop.sh new file mode 100644 index 00000000..5ab36705 --- /dev/null +++ b/list_serial_ports/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython list_serial_ports.py stop diff --git a/loglevel/cs.py b/loglevel/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/loglevel/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/loglevel/install.sh b/loglevel/install.sh new file mode 100644 index 00000000..96e94ff6 --- /dev/null +++ b/loglevel/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION loglevel on:" >> install.log +date >> install.log diff --git a/loglevel/loglevel.py b/loglevel/loglevel.py new file mode 100644 index 00000000..822201a8 --- /dev/null +++ b/loglevel/loglevel.py @@ -0,0 +1,44 @@ +''' +This reference application will change the router logging level to 'info' when +the app is started and then to 'debug' when it is stopped (i.e. unloaded or purged). +''' +import argparse +import cs + +APP_NAME = 'loglevel' + + +def action(command): + try: + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + # Read the logging level + ret_value = cs.CSClient().get('/config/system/logging/level') + + # Output a syslog for the current logging level + cs.CSClient().log(APP_NAME, 'Current Logging level = {}'.format(ret_value)) + ret_value = '' + + if command == 'start': + # Set the logging level to info when the app is started. + ret_value = cs.CSClient().put('/config/system/logging', {'level': 'info'}) + elif command == 'stop': + # Set the logging level to debug when the app is stopped. + ret_value = cs.CSClient().put('/config/system/logging', {'level': 'debug'}) + + # Output a syslog for the new current logging level + cs.CSClient().log(APP_NAME, 'New Logging level = {}'.format(ret_value)) + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/loglevel/package.ini b/loglevel/package.ini new file mode 100644 index 00000000..651ecb6a --- /dev/null +++ b/loglevel/package.ini @@ -0,0 +1,11 @@ +[loglevel] +uuid=616acd0c-0475-479e-a33b-f7054843c971 +vendor=Cradlepoint +notes=Router SDK Reference Application +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/loglevel/start.sh b/loglevel/start.sh new file mode 100644 index 00000000..7a0b2138 --- /dev/null +++ b/loglevel/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython loglevel.py start diff --git a/loglevel/stop.sh b/loglevel/stop.sh new file mode 100644 index 00000000..dd728348 --- /dev/null +++ b/loglevel/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython loglevel.py stop diff --git a/make.py b/make.py index 34bbda6b..ee946f66 100644 --- a/make.py +++ b/make.py @@ -1,1359 +1,350 @@ +''' +This is the Router SDK tool used to created applications +for Cradlepoint routers. It will work on Linux, OS X, and +Windows once the computer environment is setup. +''' -import logging import os -import os.path +import sys +import uuid +import json import shutil +import requests import subprocess -import sys -import time - -# these must be added, pulled in by pip -# import requests -# import requests.exceptions -# from requests.auth import HTTPDigestAuth - -from tools.copy_file_nl import copy_file_nl -import cp_lib.app_name_parse as app_name_parse -from cp_lib.app_base import CradlepointAppBase, CradlepointSdkDisabled -from cp_lib.cs_ping import cs_ping - -SDIR_CONFIG = "config" -SDIR_BUILD = "build" - -# used for make options which don't need name (that assume 'any app' on -# router or in ./build -DEF_DUMMY_NAME = "make" - -FILE_NAME_INSTALL = "install.sh" -FILE_NAME_START = "start.sh" -FILE_NAME_MAIN = "main.py" -FILE_NAME_UUID = "_last_uuid.txt" - -SCP_USER_DEFAULT = "admin" -WIN_SCP_NAME = "./tools/pscp.exe" - -# def is for Linux -DEF_SCP_NAME = "scp" - -# set False means SCP will ask for password always; True means SSHPASS is used -# to feed password from ./config/settings.ini -USE_SSH_PASS = True - -# codes returned by Router API access which fails -EXIT_CODE_NO_DATA = -20099 -EXIT_CODE_NO_RESPONSE = -20098 -EXIT_CODE_BAD_FORM = -20097 -EXIT_CODE_NO_APPS = -20096 -EXIT_CODE_MISSING_DEP = -20095 -EXIT_CODE_BAD_ACTION = -20094 - -# allow a quicker timeout since we are talking LOCALLY only, so default is -# unnecessarily long -API_REQUESTS_TIMEOUT = 2.0 - -# any file in app directory starting with this is skipped, set to None to -# NOT skip any -SKIP_PREFACE_CHARACTER = '.' - - -class TheMaker(CradlepointAppBase): - - SDIR_SAVE_EXT = ".save" - - ACTION_DEFAULT = "status" - ACTION_NAMES = ("build", "package", "status", "install", "start", - "stop", "uninstall", - "purge", "uuid", "reboot", "clean", "ping") - ACTION_HELP = { - "build": "Alias for 'package'.", - "package": "Create the application archive tar.gz file.", - "status": "Print current SDK app status from locally connected router", - "install": "Secure copy the application archive to a locally connect" + - "ed router. The router must already be in SDK DEV mode " + - "via registration and licensing in ECM.", - "start": "Start the application on the locally connected router.", - "stop": "Stop the application on the locally connected router.", - "uninstall": "Uninstall the application from locally connected router", - "purge": "Purge all applications from the locally connected router.", - "uuid": "Issue status to router, display any UUID installed", - "reboot": "Reboot your router", - "clean": "delete temp build files", - "ping": "ask router to ping a local IP" - } - - ACTION_CALL_TO_ROUTER = ("status", "install", "start", "stop", - "uninstall", "purge", "uuid", "reboot", "ping") - ACTION_NEED_UUID = ("start", "stop", "uninstall") - - SETS_DEFAULT_USER_NAME = "admin" - - # could also set to be os.sep, if you like. But Windows accepts Linux style - DEF_SEP = '/' - - def __init__(self): - """Basic Init""" - from cp_lib.load_settings_ini import copy_config_ini_to_json - - # make sure we have at least a basic ./config/settings.json - # ALWAYS copy existing ./config/settings.ini over, which makes - # CradlepointAppBase.__init__() happy - copy_config_ini_to_json() - - # we don't contact router for model/fw - will do in sanity_check, IF - # the command means contact router - CradlepointAppBase.__init__(self, call_router=False, log_name="make") - - # 'attrib' are our internal pre-processed settings for MAKE use - self.attrib = {} - - self.command = "make" - self.action = self.ACTION_DEFAULT - - # these are in CradlepointAppBase - # self.run_name = None # like "network/tcp_echo/__init__.py" - # self.app_path = None # like "network/tcp_echo/", used to find files - # self.app_name = None # like "tcp_echo" - # self.mod_name = None # like "network.tcp_echo", used for importlib - # self.settings = load_settings_json(self.app_path) - # self.logger = get_recommended_logger(self.settings) - # self.cs_client = init_cs_client_on_my_platform(self.logger, - # self.settings) - - # should MAKE edit/change the [app][version] in setting.ini? - self.increment_version = False - - # are we building for running on PC? Then don't include PIP add-ins - self.ignore_pip = False - - # define LOGGING between DEBUG or INFO - self.verbose = False - - self.last_url = None - self.last_reply = None - self.last_status = None - self._last_uuid = None - - self._exclude = [] - - return - - def run(self): - """Dummy to satisfy CradlepointAppBase""" - return - - def main(self): - """ - - :return int: code for sys.exit() - """ - from cp_lib.load_settings_ini import load_sdk_ini_as_dict - - self.logger.debug("Make:, Action:{0}, Name:{1}".format(self.action, - self.app_path)) - - # handle diverse app_path - if self.app_path is not None: - assert isinstance(self.app_path, str) - # make sure is Linux-style PATH, not Windows nor 'dot' module name - self.app_path = app_name_parse.get_app_path(self.app_path, - self.DEF_SEP) - self.app_name = app_name_parse.get_app_name(self.app_path) - self.mod_name = app_name_parse.get_module_name(self.app_path) - - # load the base config from raw INI. These are use of all actions, - # but not BUILD/PACKAGE - self.settings = load_sdk_ini_as_dict(self.app_path) - - # confirm we have the APP PATH set okay - if self.app_path is None: - # without a path, might be just checking router status, etc - self.attrib["path"] = DEF_DUMMY_NAME - self.attrib["name"] = DEF_DUMMY_NAME - self.app_path = DEF_DUMMY_NAME - - else: # assume was passed in commandline - self.attrib["name"] = self.app_name - self.attrib["path"] = self.app_path - - # throws exception if environment is not sane & cannot be made sane - self.sanity_check_environment() - - self.action = self.action.lower() - if self.action in ("build", "package"): - # go to our router & check status - return self.action_package() - - elif self.action == "status": - # go to our router & check status - try: - return self.action_status(verbose=True) - - except CradlepointSdkDisabled: - self.logger.error("Router Lacks SDK function") - return -1 - - elif self.action == "install": - # try to send our TAR.GZIP to our router - return self.action_install(self.app_path) - - elif self.action == "start": - # go to our router & start (if installed?) - return self.action_start() - - elif self.action == "stop": - # go to our router & stop (if running?) - return self.action_stop() - - elif self.action == "uninstall": - # go to our router & do an install - # TODO - fix the uninstall after FW: 6.2.2 - # now, router leaves misc old app info around, so we'll - # need to do a better scan, finding apps to install - # INFO:make:SDK APP[0] UUID:db946fe9-fb7e-42b1-8589-123ff698b67e - # INFO:make:SDK APP[0] State:uninstalled - # INFO:make:SDK APP[0] Summary:Uninstalled Application - # INFO:make:SDK APP[1] UUID:114c8c0f-9887-4e68-a4e7-de8d73fba69f - # INFO:make:SDK APP[1] State:started - # INFO:make:SDK APP[1] Summary:Started Application - # INFO:make:SDK APP[1] Name:hello_world - # INFO:make:SDK APP[1] Date:2016-10-20T13:12:51Z - # INFO:make:SDK APP[1] Version:1.12 - # return self.action_uninstall() - self.logger.info("Doing PURGE instead of UNINSTALL") - return self.action_purge() - - elif self.action == "purge": - # go to our router & force a purge - return self.action_purge() - - elif self.action == "uuid": - # go to our router & check UUID in status - return self.action_get_uuid_from_router() - - elif self.action == "reboot": - # go to our router & force a reboot - return self.action_reboot() - - elif self.action == "clean": - # delete temp build files - return self.action_clean() - - elif self.action == "ping": - # delete temp build files - return self.action_ping(self.app_path) - - else: - raise ValueError("Unsupported Command:" + self.action) - - def get_app_name(self): - """Return the path for the app files""" - return self.settings["application"]["name"] - - def get_app_path(self): - """Return the path for the app files""" - if "path" in self.attrib: - return self.attrib["path"] - raise KeyError("App Path attrib is missing") - - def get_build_path(self): - """Return the path for the app files - force Linux format""" - if "build" not in self.attrib: - # self.attrib["build"] = os.path.join(SDIR_BUILD, - # self.get_app_name()) - self.attrib["build"] = SDIR_BUILD + '/' + self.get_app_name() + '/' - return self.attrib["build"] - - def get_main_file_name(self): - """Return the MAIN file name to run""" - if "main_file_name" in self.attrib: - return self.attrib["main_file_name"] - else: - return FILE_NAME_MAIN - - def get_router_ip(self): - return self.settings["router_api"]["local_ip"] - - def get_router_password(self): - return self.settings["router_api"]["password"] - - def get_router_user_name(self): - try: - return self.settings["router_api"]["user_name"] - except KeyError: - return "admin" - - def sanity_check_environment(self): - """ - Confirm the basic directories do exist - such as ./config - - :return None: Throws an exception if it fails - """ - from cp_lib.load_product_info import load_product_info - from cp_lib.load_firmware_info import load_firmware_info - - # confirm ./config exists and is not a file - self._confirm_dir_exists(SDIR_CONFIG, "CONFIG dir") - - if "path" not in self.attrib: - raise KeyError("SDK App Path missing in attributes") - - if "name" not in self.attrib: - raise KeyError("SDK App Name missing in attributes") - - # fussy neatness - force Linux to propagate to - ["path"] - self.attrib["path"] = app_name_parse.normalize_path_separator( - self.attrib["path"], self.DEF_SEP) - - if self.action in self.ACTION_CALL_TO_ROUTER: - self.logger.info("sets:{}".format(self.settings)) - # then check for Model & firmware - save_value = self.cs_client.show_rsp - self.cs_client.show_rsp = False - self.settings = load_product_info(self.settings, self.cs_client) - self.settings = load_firmware_info(self.settings, self.cs_client) - # print(json.dumps(self.settings, ensure_ascii=True, indent=4)) - self.cs_client.show_rsp = save_value - - self.logger.info("Cradlepoint router is model:{}".format( - self.settings["product_info"]["product_name"])) - self.logger.info("Cradlepoint router FW version:{}".format( - self.settings["fw_info"]["version"])) - - if self.action in self.ACTION_NEED_UUID: - # these need the UUID - if "uuid" not in self.attrib: - # then try to read the ./config/last_uuid.txt file - data = self._read_uid_file() - if data is not None: - self.logger.debug("last_uuid=({})".format(data)) - self.attrib["uuid"] = data - - # else: - # raise KeyError("SDK UUID missing in attributes") - - return - - def _delete_uid(self): - """Delete any saved UUID file - ./config/_last_uuid.txt""" - file_name = os.path.join(SDIR_CONFIG, FILE_NAME_UUID) - if os.path.exists(file_name): - self.logger.debug("Delete {}".format(file_name)) - os.remove(file_name) - return - - def _read_uid_file(self): - """Read / load a saved UUID from file - ./config/_last_uuid.txt""" - file_name = os.path.join(SDIR_CONFIG, FILE_NAME_UUID) - if os.path.exists(file_name): - file_han = open(file_name, "r") - data = file_han.read().strip() - file_han.close() - self.logger.debug("Read {} saw {}.".format(file_name, data)) - return data - else: - self.logger.debug("Read {} failed - does not exist.".format( - file_name)) - - return None - - def _write_uid(self, data: str): - """Write / dump a saved UUID to file - ./config/_last_uuid.txt""" - assert isinstance(data, str) - - file_name = os.path.join(SDIR_CONFIG, FILE_NAME_UUID) - self.logger.debug("Write {} with {}".format(file_name, data)) - file_han = open(file_name, "w") - file_han.write(data) - file_han.close() - return - - def action_package(self): - """ - Build the actual package, copying to build - - :return int: value intended for sys.exit() - """ - import tools.make_load_settings - import cp_lib.load_settings_ini - from tools.module_dependency import BuildDependencyList - from tools.make_package_ini import make_package_ini - from tools.package_application import package_application - - if self.attrib["name"] == DEF_DUMMY_NAME: - self.logger.error("Cannot build - no app_path given") - sys.exit(-1) - - self.logger.info("Building Package({0}) in dir({1}))".format( - self.attrib["name"], self.attrib["path"])) - - # confirm PATH exists! - if not os.path.isdir(self.attrib["path"]): - raise FileNotFoundError( - "Cannot build - app_path({}) is invalid".format( - self.attrib["path"])) - - # does nothing - just create so we can add files to it - dep_list = BuildDependencyList() - dep_list.ignore_pip = self.ignore_pip - dep_list.logger = self.logger - - # confirm ./build exists and is not a file - dst_file_name = SDIR_BUILD - self.logger.debug("Confirm ./{} exists and is empty".format( - dst_file_name)) - if os.path.isdir(dst_file_name): - try: - shutil.rmtree(dst_file_name, ignore_errors=True) - except OSError: - self.logger.error( - "Could not delete OLD ./" + - "{} - have you open files there?".format(dst_file_name)) - raise - - # confirm .build/{app_name} exists - dst_file_name = self.get_build_path() - self.logger.debug("Confirm ./{} exists".format(dst_file_name)) - self._confirm_dir_exists(dst_file_name, "BUILD dir") - - # start with the SETTINGS: - # make sure has [application] section, plus "uuid" and "version" - self.logger.info("Confirm App INI has required UUID and Version") - tools.make_load_settings.validate_project_settings( - self.get_app_path(), self.increment_version) - - # ./config/settings.ini then {app_path}/settings.ini - # (again, as UUID might have changed) - self.settings = tools.make_load_settings.load_settings( - self.get_app_path()) - - # save as ./build/{project}/settings.json - dst_file_name = os.path.join( - self.get_build_path(), - cp_lib.load_settings_ini.DEF_SETTINGS_FILE_NAME + - cp_lib.load_settings_ini.DEF_JSON_EXT) - cp_lib.load_settings_ini.save_root_settings_json(self.settings, - dst_file_name) - - # exclude the APP ini and json - save_root_settings_json() made - # a combined copy - app_file_name = os.path.join( - self.app_path, - cp_lib.load_settings_ini.DEF_SETTINGS_FILE_NAME + - cp_lib.load_settings_ini.DEF_INI_EXT) - self._exclude.append(app_file_name) - app_file_name = os.path.join( - self.app_path, - cp_lib.load_settings_ini.DEF_SETTINGS_FILE_NAME + - cp_lib.load_settings_ini.DEF_JSON_EXT) - self._exclude.append(app_file_name) - - # do/copy over the SH files to BUILD, these have no dependencies - self.create_install_sh() - self.create_start_sh() - - # this will create a simple Windows batch file - self.create_go_bat() - - # handle the main.py - # GLOBAL = ./config/main.py - glob_file_name = os.path.join(SDIR_CONFIG, self.get_main_file_name()) - # APP = ./{app_project}/main.py - app_file_name = os.path.join(self.get_app_path(), - self.get_main_file_name()) - # DST = ./build/{app_name}/main.py - dst_file_name = os.path.join(self.get_build_path(), - self.get_main_file_name()) - - if os.path.exists(app_file_name): - # if app developer supplies one, use it (TDB - do pre-processing) - # for example, copy "network/tcp_echo/main.py" to "build/main.py" - self.logger.info("Copy existing APP MAIN from [{}]".format( - app_file_name)) - copy_file_nl(app_file_name, dst_file_name) - # sh util.copyfile(app_file_name, dst_file_name) - # make sure we add any required files from cp_lib - dep_list.add_file_dependency(app_file_name) - - # we exclude it as "app_dir/main.py", because it will be - # in archive as ./main.py! - self.logger.debug("Add file [{}] to exclude list".format( - app_file_name)) - self._exclude.append(app_file_name) - - elif os.path.exists(glob_file_name): - # if root supplies one, use it (TDB - do pre-processing) - self.logger.info("Copy existing ROOT MAIN from [{}]".format( - glob_file_name)) - copy_file_nl(glob_file_name, dst_file_name) - # sh util.copyfile(glob_file_name, dst_file_name) - - # make sure we add any required files from cp_lib - dep_list.add_file_dependency(glob_file_name) - - else: - raise ValueError("\'main\' script is missing!") - - # sys.exit(-99) - - # all will use this? - # file_name = os.path.join("cp_lib", "app_base.py") - # dep_list.add_file_dependency(file_name) - # - # dep_list.add_if_new("cp_lib.__init__") - - # make a list of files in app_dir, copy them to BUILD, - # plus collect dependency list - file_list = os.listdir(self.get_app_path()) - dst_path_name = os.path.join(self.get_build_path(), - self.get_app_path()) - self._confirm_dir_exists(dst_path_name, "Project Directory") - - for file_name in file_list: - - if file_name[0] == SKIP_PREFACE_CHARACTER: - # if file starts with '.', skip - - # set SKIP_PREFACE_CHARACTER == None to skip this - self.logger.debug("skip due to SKIP preface:{}".format( - file_name)) - continue - - path_name = os.path.join(self.get_app_path(), file_name) - - if os.path.isdir(path_name): - # handle any sub-directories (TBD) - - if file_name in ("__pycache__", "test"): - self.logger.debug("skip sdir:[{}]".format(path_name)) - pass - - else: # TODO - recurse into it - self.logger.debug("see app sdir:[{}]".format(path_name)) - - elif os.path.isfile(path_name): - # handle any files - # self.logger.debug("see app file:[{}]".format(path_name)) - if path_name in self._exclude: - self.logger.debug("skip file:[{}]".format(path_name)) +import configparser - else: - # make sure we add any required files from cp_lib - dep_list.add_file_dependency(path_name) - dst_file_name = os.path.join( - self.get_build_path(), - self.get_app_path(), file_name) +# These will be set in init() by using the sdk_settings.ini file. +# They are used be various functions in the file. +g_app_name = '' +g_app_uuid = '' +g_dev_client_ip = '' +g_dev_client_username = '' +g_dev_client_password = '' +g_python_cmd = 'python3' # Default for Linux and OS X - # self.logger.debug("Make Dir [{0}]".format(dst_path_name)) - self._confirm_dir_exists(dst_path_name, "File to Build") - self.logger.debug("Copy file [{0}] to {1}".format( - path_name, dst_file_name)) - # note: copyfile requires 2 file names - 2nd cannot - # be a directory destination, we'll skip EMPTY files - copy_file_nl(path_name, dst_file_name, discard_empty=True) - # sh util.copyfile(path_name, dst_file_name) +# Returns an HTTPDigestAuth for the global username and password. +def get_digest(): + return requests.auth.HTTPDigestAuth(g_dev_client_username, g_dev_client_password) - # copy everything from app_dir to build - for source in dep_list.dep_list: - self.logger.info("Copy Over Dependency {0}".format(source)) - self.copy_dep_name_to_build(source) - # handle the package.ini - file_name = os.path.join(self.get_build_path(), "package.ini") - self.logger.debug("Make {}".format(file_name)) - make_package_ini(self.settings, file_name) +# Returns the app package name based on the global app name. +def get_app_pack(): + package_name = g_app_name + ".tar.gz" + return package_name - # propagate to MANIFEST.json - package_application(self.get_build_path(), pkey=None) - return 0 - - def copy_dep_name_to_build(self, source_file_name, fix_form=True): - """ - Given a file name from an import (like "cp_lib.cp_logging"), which - we assume is a .PY/python file, copy it to the build directory - - such as "build/cp_lib/cp_logging.py" - - :param str source_file_name: - :param bool fix_form: True if source is like "cp_lib.cp_logging", - False to treat as final - :return: - """ - assert isinstance(source_file_name, str) - if fix_form: - # convert "cp_lib.cp_logging" to "cp_lib/cp_logging.py" - if source_file_name.endswith(".py"): - # assume is already okay - pass - # TODO - fix this! How? - elif source_file_name.endswith(".ico"): - # assume is already okay - pass - elif source_file_name.endswith(".jpg"): - # assume is already okay - pass - elif source_file_name.endswith(".md"): - # assume is already okay - return - elif source_file_name.endswith(".ini"): - # assume is already okay - return - else: - source_file_name = source_file_name.replace( - '.', self.DEF_SEP) + ".py" - - # make up the destination as "build/cp_lib/cp_logging.py" - build_file_name = os.path.join(self.get_build_path(), source_file_name) - - # breaks into ["build/cp_lib", "cp_logging.py"], make sure it exists - path_name = os.path.split(build_file_name) - self._confirm_dir_exists(path_name[0], "Dep 2 Build") - - self.logger.debug("Copy file [{0}] to {1}".format(source_file_name, - build_file_name)) - # copyfile requires 2 file names - 2nd cannot be a directory dest - copy_file_nl(source_file_name, build_file_name, discard_empty=True) - # sh util.copyfile(source_file_name, build_file_name) - - return - - def action_get_uuid_from_router(self): - """ - Issue the STATUS, to check the UUID installed - - When at least one APP is installed, return is like this: - {'summary': 'Service started', 'service': 'started', - 'mode': 'devmode', 'apps': [ - {'_id_': '8f828277-5e8f-4c90-9b99-a3eb61f3', - 'app': { - 'uuid': '8f828277-5e8f-4c90-9b99-a3eb61f3', - 'vendor': 'customer', - 'version_minor': 2, 'restart': True, 'version_major': 1, - 'name': 'probe_gps', - 'date': '2016-03-21T22:46:39Z'}, - 'summary': 'Started application', 'state': 'started'}]} - - When no APP is installed, return is like this: - {'summary': 'Service started', 'service': 'started', - 'mode': 'devmode', 'apps': []} - - :return int: value intended for sys.exit() - """ - self._last_uuid = None - self.logger.info("Checking SDK status on router({})".format( - self.get_router_ip())) - - # quietly get the STATUS into, will be saved as self.last_status - self.action_status(verbose=False) - - if 'apps' not in self.last_status: - self.logger.error( - "SDK get UUID failed - missing \'apps\' key in response.") - return EXIT_CODE_BAD_FORM - - if len(self.last_status['apps']) == 0: - self.logger.error( - "SDK get UUID failed - [\'apps\'] data is empty.") - return EXIT_CODE_NO_APPS - - reply = self.last_status['apps'] - """ :type reply: list """ - assert isinstance(reply, list) - - # for now, we only allow 1 SDK app, so take index[0] - if '_id_' in reply[0]: - self._last_uuid = reply[0]['_id_'] - self.logger.info("Router has UUID:{} installed".format( - self._last_uuid)) - - if self._last_uuid is None: - self.logger.error("SDK failed to get UUID from router.") - return EXIT_CODE_BAD_ACTION +# Gets data from the router config store +def get(config_tree): + router_api = 'http://{}/api/{}'.format(g_dev_client_ip, config_tree) - return 0 + try: + response = requests.get(router_api, auth=get_digest()) - def action_status(self, verbose=True): - """ - Go to our router and check SDK status + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(g_dev_client_ip)) + return None - :param bool verbose: T to see response via logging, - F to merely 'test status' - :return int: value intended for sys.exit() - """ - from cp_lib.status_tree_data import string_list_status_apps + return json.dumps(json.loads(response.text), indent=4) - self.logger.info("Checking SDK status on router({})".format( - self.get_router_ip())) - # we save as 'last_status', as a few clients use as as proxy - self.last_status = self.cs_client.get("status/system/sdk") - if self.last_status is None: - raise CradlepointSdkDisabled("Router lacks SDK Function") +# Puts an SDK action in the router config store +def put(value): + try: + response = requests.put("http://{}/api/control/system/sdk/action".format(g_dev_client_ip), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=get_digest(), + data={"data": '"{} {}"'.format(value, get_app_uuid())}) - self.logger.info("SDK status check successful") - # self.logger.debug("RSP:{}".format(reply['data'])) - # {'service': 'started', 'apps': [], 'mode': 'devmode', - # 'summary': 'Service started'} + print('status_code: {}'.format(response.status_code)) - if self.last_status is None: - self.logger.info("SDK is not enabled in the Router.") + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(g_dev_client_ip)) + return None + return json.dumps(json.loads(response.text), indent=4) + + +# Cleans the SDK directory for a given app by removing files created during packaging. +def clean(): + print("Cleaning {}".format(g_app_name)) + try: + app_pack_name = get_app_pack() + if os.path.isfile(app_pack_name): + os.remove(app_pack_name) + except OSError: + print('Clean Error 1 for file {}: {}'.format(app_pack_name, OSError.strerror())) + + try: + meta_dir = '{}/{}/METADATA'.format(os.getcwd(), g_app_name) + if os.path.isdir(meta_dir): + shutil.rmtree(meta_dir) + except OSError: + print('Clean Error 2 for directory {}: {}'.format(meta_dir, OSError.strerror())) + + try: + build_file = os.path.join(os.getcwd(), '.build') + if os.path.isfile(build_file): + os.remove(build_file) + except OSError: + print('Clean Error 3 for file {}: {}'.format(build_file, OSError.strerror())) + + +# Build, just validates, the application code and package. +def build(): + print("Building {}".format(g_app_name)) + success = True + validate_script_path = os.path.join('tools', 'bin', 'validate_application.py') + app_path = os.path.join(g_app_name) + try: + if sys.platform == 'win32': + subprocess.check_output('{} {} {}'.format(g_python_cmd, validate_script_path, app_path)) else: - self.logger.info("SDK status check successful") - - if verbose: - # then put out all of the logging info, else don't - result = self._string_list_status_basic(self.last_status) - for line in result: - self.logger.info(line) - - if 'apps' in self.last_status and \ - (len(self.last_status['apps']) > 0): - _index = 0 - for one_app in self.last_status['apps']: - result = string_list_status_apps(_index, one_app) - for line in result: - self.logger.info(line) - _index += 1 - else: - self.logger.info("SDK App Count:0") + subprocess.check_output('{} {} {}'.format(g_python_cmd, validate_script_path, app_path), shell=True) + + except subprocess.CalledProcessError as err: + print('Error building {}: {}'.format(g_app_name, err)) + success = False + finally: + return success + + +# Package the app files into a tar.gz archive. +def package(): + success = True + print("Packaging {}".format(g_app_name)) + package_dir = os.path.join('tools', 'bin') + package_script_path = os.path.join('tools', 'bin', 'package_application.py') + app_path = os.path.join(g_app_name) + + try: + subprocess.check_output('{} {} {}'.format(g_python_cmd, package_script_path, app_path), shell=True) + except subprocess.CalledProcessError as err: + print('Error packaging {}: {}'.format(g_app_name, err)) + success = False + finally: + return success + + +# Get the SDK status from the router +def status(): + status_tree = '/status/system/sdk' + print('Get {} status for router at {}'.format(status_tree, g_dev_client_ip)) + response = get(status_tree) + print(response) + + +# Transfer the app app tar.gz package to the router +def install(): + app_archive = get_app_pack() + + # Use scp for Linux or OS X + cmd = 'scp {0} {1}@{2}:/app_upload'.format(app_archive, g_dev_client_username, g_dev_client_ip) + + # For Windows, use pscp.exe in the tools directory + if sys.platform == 'win32': + cmd = "./tools/bin/pscp.exe -pw {0} -v {1} {2}@{3}:/app_upload".format( + g_dev_client_password, app_archive, + g_dev_client_username, g_dev_client_ip) + + print('Installing {} in router {}.'.format(app_archive, g_dev_client_ip)) + try: + if sys.platform == 'win32': + subprocess.check_output(cmd) + else: + subprocess.check_output(cmd, shell=True) + except subprocess.CalledProcessError as err: + # There is always an error because the router will drop the connection. + # print('Error installing: {}'.format(err)) return 0 - @staticmethod - def _string_list_status_basic(status): - """ - Given STATUS return from Router, Make a list of strings to show - of basic things like: {'service': 'started', 'apps': [], - 'mode': 'devmode', 'summary': 'Service started'} - - This does NOT enumerate through the APPS list - - :param dict status: - :return list: - """ - result = [] - - if status is not None: - if 'service' in status: - result.append("SDK Service Status:{}".format( - status['service'])) - - if 'summary' in status: - result.append("SDK Summary:{}".format(status['summary'])) - - if 'mode' in status: - if status['mode'].lower() == "devmode": - result.append("SDK Router is in DEV MODE") - elif status['mode'].lower() == "standard": - result.append( - "SDK Router is NOT in DEV MODE - is in STANDARD mode.") - else: - result.append("SDK Router Dev Mode Unknown:{}".format( - status['mode'])) - - if 'apps' in status: - if len(status['apps']) == 0: - result.append("SDK - No Apps Installed") - else: - result.append("SDK App Count:{}".format( - len(status['apps']))) - - return result - - def action_install(self, file_name): - """ - SCP (copy/upload) the bundle to the router, which then installs - - :param str file_name: base file name to upload, without the ".tar.gz" - :return int: value intended for sys.exit() - """ - from cp_lib.app_name_parse import get_app_name - - self.logger.debug("file_name: {}".format(file_name)) - if file_name.startswith(DEF_DUMMY_NAME): - file_name = None - - # confirm we have BUILD directory; if not, assume no package was built - if not os.path.isdir(SDIR_BUILD): - self.logger.error( - "MAKE cannot install - no {} directory!".format(SDIR_BUILD)) - sys.exit(-11) - - result = self.action_status(verbose=False) - if result == EXIT_CODE_NO_DATA: - # then status failed - self.logger.error( - "MAKE cannot install - SDK appears to be disabled.") - return result - - elif result != 0: - # then status failed - self.logger.error( - "MAKE cannot install - SDK status check failed," + - "code={}".format(result)) - return result - - # try to guess package from ./build - # - find the tar.gz (such as tcp_echo.tar.gz) and deduce the name - if file_name is None: - self.logger.debug("No file given - search for one") - assert os.path.isdir(SDIR_BUILD) - result = os.listdir(SDIR_BUILD) - file_name = None - for name in result: - if name.endswith(".tar.gz"): - file_name = name[:-7] - # self.logger.debug("Split ({})".format(file_name)) - break - - if file_name is None: - sys.exit(-12) - - # we want to reduce to just the 'app name' - might be anything - # from path to file name: example, if network/tcp_echo/tcp_echo or - # network.tcp_echo, just want tcp_echo - file_name = get_app_name(file_name) - - # for now, just purge to remove any old files - # self.action_purge() - - # minor fix-up - if file_name ends with .py, strip that off - if file_name.endswith(".py"): - file_name = file_name[:-3] - - # file_name should be like "tcp-echo" - self.logger.debug("Install:({})".format(file_name)) - - # confirm we have BUILD/{name} directory - file_name = os.path.join(SDIR_BUILD, file_name + ".tar.gz") - if not os.path.isfile(file_name): - self.logger.error("MAKE cannot install - no {} archive!".format( - file_name)) - sys.exit(-13) - - if sys.platform == "win32": - self.logger.info( - "Upload & Install SDK on router({}) (win32)".format( - self.get_router_ip())) - - # Windows is happy with a single string ... but - cmd = "{0} -pw {1} -v {2} {3}@{4}:/app_upload".format( - WIN_SCP_NAME, self.get_router_password(), file_name, - self.get_router_user_name(), - self.get_router_ip()) - else: - self.logger.info( - "Upload & Install SDK on router({}) (else)".format( - self.get_router_ip())) - - # ... but Linux requires the list - if USE_SSH_PASS: - # we allow user to select to use or not - cmd = ["sshpass", "-p", self.get_router_password(), - DEF_SCP_NAME, file_name, "{0}@{1}:/app_upload".format( - self.get_router_user_name(), self.get_router_ip())] +# Start the app from the router +def start(): + print('Start application {} for router at {}'.format(g_app_name, g_dev_client_ip)) + print('Application UUID is {}.'.format(g_app_uuid)) + response = put('start') + print(response) + + +# Stop the app from the router +def stop(): + print('Stop application {} for router at {}'.format(g_app_name, g_dev_client_ip)) + print('Application UUID is {}.'.format(g_app_uuid)) + response = put('stop') + print(response) + + +# Uninstall the app from the router +def uninstall(): + print('Uninstall application {} for router at {}'.format(g_app_name, g_dev_client_ip)) + print('Application UUID is {}.'.format(g_app_uuid)) + response = put('uninstall') + print(response) + + +# Purge the app from the router +def purge(): + print('Purge application {} for router at {}'.format(g_app_name, g_dev_client_ip)) + print('Application UUID is {}.'.format(g_app_uuid)) + response = put('purge') + print(response) + + +# Prints the help information +def output_help(): + print('Command format is: {} make.py '.format(g_python_cmd)) + print('clean: Clean all project artifacts.\n') + print('build or package: Create the app archive tar.gz file.\n') + print('status: Fetch and print current app status from the locally connected router.\n') + print('install: Secure copy the app archive to a locally connected router.') + print(' The router must already be in SDK DEV mode via registration ') + print(' and licensing in ECM.\n') + print('start: Start the app on the locally connected router.\n') + print('stop: Stop the app on the locally connected router.\n') + print('uninstall: Uninstall the app from the locally connected router.\n') + print('purge: Purge all apps from the locally connected router.\n') + print('help: Print this help information.\n') + pass + + +# Get the uuid from application package.ini if not already set +def get_app_uuid(): + global g_app_uuid + + if g_app_uuid == '': + uuid_key = 'uuid' + app_config_file = os.path.join(g_app_name, 'package.ini') + config = configparser.ConfigParser() + config.read(app_config_file) + if g_app_name in config: + if uuid_key in config[g_app_name]: + g_app_uuid = config[g_app_name][uuid_key] + + if g_app_uuid == '': + # Create a UUID if it does not exist + _uuid = str(uuid.uuid4()) + config.set(g_app_name, uuid_key, _uuid) + with open(app_config_file, 'w') as configfile: + config.write(configfile) + print('INFO: The uuid did not exist in {}'.format(app_config_file)) + print('INFO: Created and saved uuid {} in {}'.format(_uuid, app_config_file)) else: - cmd = [DEF_SCP_NAME, file_name, "{0}@{1}:/app_upload".format( - self.get_router_user_name(), self.get_router_ip())] + print('ERROR: The uuid key does not exist in {}'.format(app_config_file)) + else: + print('ERROR: The APP_NAME section does not exist in {}'.format(app_config_file)) - try: - self.logger.debug("cmd:({})".format(cmd)) - result = subprocess.check_output(cmd) + return g_app_uuid - except subprocess.CalledProcessError as err: - # return subprocess.CalledProcessError.returncode - # <131>ERROR:make:res:(['probe_gps', 'probe_gps.tar', - # 'probe_gps.tar.gz']) - # TODO - handle the PSCP error more gracefully? - # self.logger.error("err:({})".format(err)) - return 0 +# Setup all the globals based on the OS and the sdk_settings.ini file. +def init(): + global g_python_cmd + global g_app_name + global g_dev_client_ip + global g_dev_client_username + global g_dev_client_password - self.logger.debug("res:({})".format(result)) + success = True - # save _last_uuid, so for start/stop/uninstall don't have to re-enter - if "application" in self.settings: - # then we have - if "uuid" in self.settings["application"]: - self._write_uid(self.settings["application"]["uuid"]) + # Keys in sdk_settings.ini + sdk_key = 'sdk' + app_key = 'app_name' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' - return 0 + if sys.platform == 'win32': + g_python_cmd = 'python' - def action_start(self, uuid=None): - """ - Go to our router and start the SDK - - :param str uuid: optional UUID as string, else use self.attrib - :return int: value intended for sys.exit() - """ - if uuid is None: - # if no UUID, then try to read the router - result = self.action_get_uuid_from_router() - if result not in (0, EXIT_CODE_NO_APPS): - # then the action failed - self.logger.error("SDK start failed - no SDK?") - return result - - # last_uuid was either set to a found value, or None - uuid = self._last_uuid - - if uuid is None: - self.logger.error( - "Start failed - UUID unknown, or nothing installed") - return EXIT_CODE_NO_APPS - - self.logger.info( - "Starting SDK on router({})".format(self.get_router_ip())) - put_data = '\"start {0}\"'.format(uuid) - result = self.cs_client.put("control/system/sdk/action", put_data) - if result.startswith("start"): - # then assume is okay - # - need to delay and do new status to really see if started - return 0 - - self.logger.info("Start Result:{}".format(result)) - return EXIT_CODE_BAD_ACTION - - def action_stop(self, uuid=None): - """ - Go to our router and stop the SDK - - :param str uuid: optional UUID as string, else use self.attrib - :return int: value intended for sys.exit() - """ - if uuid is None: - # if no UUID, then try to read the router - result = self.action_get_uuid_from_router() - if result not in (0, EXIT_CODE_NO_APPS): - # then the action failed - self.logger.error("SDK stop failed - no SDK?") - return result - - # last_uuid was either set to a found value, or None - uuid = self._last_uuid - - if uuid is None: - self.logger.error( - "Stop failed - UUID unknown, or nothing installed") - return EXIT_CODE_NO_APPS - - self.logger.info( - "Stopping SDK on router({})".format(self.get_router_ip())) - put_data = '\"stop {0}\"'.format(uuid) - result = self.cs_client.put("control/system/sdk/action", put_data) - if result.startswith("stop"): - # then assume is okay - # - need to delay and do new status to really see if started - return 0 - - self.logger.info("Stop Result:{}".format(result)) - return EXIT_CODE_BAD_ACTION - - def action_uninstall(self, uuid=None): - """ - Go to our router and uninstall ONE SDK instance - - :param str uuid: optional UUID as string, else use self.attrib - :return int: value intended for sys.exit() - """ - if uuid is None: - # if no UUID, then try to read the router - result = self.action_get_uuid_from_router() - if result not in (0, EXIT_CODE_NO_APPS): - # then the action failed - self.logger.error("SDK uninstall failed - no SDK?") - return result - - # last_uuid was either set to a found value, or None - uuid = self._last_uuid - - if uuid is None: - self.logger.error( - "Uninstalled failed - UUID unknown, or nothing installed") - return EXIT_CODE_NO_APPS - - self.logger.info( - "Uninstall SDK on router({})".format(self.get_router_ip())) - put_data = '\"uninstall {0}\"'.format(uuid) - result = self.cs_client.put("control/system/sdk/action", put_data) - if result.startswith("uninstall"): - # then assume is okay - # - need to delay and do new status to really see if started - - # make sure none remaining - self._delete_uid() - return 0 - - self.logger.info("Uninstall Result:{}".format(result)) - return EXIT_CODE_BAD_ACTION - - def action_purge(self): - """ - Go to our router and purge ALL SDK instances - - :return int: value intended for sys.exit() - """ - self.logger.info( - "Purging SDK on router({})".format(self.get_router_ip())) - put_data = '\"purge\"' - result = self.cs_client.put("control/system/sdk/action", put_data) - if result.startswith("purge"): - # then assume is okay - # - need to delay and do new status to really see if started - return 0 - - self.logger.info("Purge Result:{}".format(result)) - return EXIT_CODE_BAD_ACTION - - def action_reboot(self): - """ - Go to our router, and reboot it - - :return int: value intended for sys.exit() - """ - self.logger.info( - "Reboot our development router({})".format(self.get_router_ip())) - # put_data = '\"true\"' - put_data = 'true' - result = self.cs_client.put("control/system/reboot", put_data) - # result will be boolean? - if False: - # then assume is okay - # - need to delay and do new status to really see if started - self.logger.info("SDK reboot successful") - return 0 - - self.logger.info("Reboot Result:{}".format(result)) - return EXIT_CODE_BAD_ACTION - - def action_clean(self): - """ - Delete temp build files - - :return int: value intended for sys.exit() - """ - self.logger.info("Clean temporary build files") - - # clean out the BUILD files - path_name = SDIR_BUILD - if os.path.isdir(path_name): - self.logger.info("Delete BUILD directory") - shutil.rmtree(path_name, ignore_errors=True) - - path_name = os.getcwd() - # self.logger.info("Cleaning from directory {}".format(path_name)) - - # clean out the Python cache directories - del_list = [] - for path, dir_name, file_name in os.walk(path_name): - - # self.logger.debug("path:{0} dir:{1} fil:{2}".format( - # path, dir_name, file_name)) - - if path.startswith(path_name + os.sep + '.'): - # we'll skip everything like "./.idea" or any starting with '.' - # self.logger.debug("skip:{}".format(path)) - continue - - if path.endswith("__pycache__"): - # we'll want to delete this - # self.logger.debug("add to list:{}".format(path)) - del_list.append(path) - continue - - pass # try next one - - for path_name in del_list: - self.logger.info("Delete {} directory".format(path_name)) - shutil.rmtree(path_name, ignore_errors=True) - - for path_name in ("config/settings.json", "syslog1.txt"): - # delete a few known temporary files - if os.path.isfile(path_name): - self.logger.info("Delete FILE {}".format(path_name)) - os.remove(path_name) - - # sometimes, we end up with .pyc in /tools? - for path_name in ("tools", "test"): - del_list = os.listdir(path_name) - for file_name in del_list: - if file_name.endswith(".pyc"): - self.logger.info("Delete FILE {}".format(file_name)) - os.remove(file_name) - if file_name.endswith(".pyo"): - self.logger.info("Delete FILE {}".format(file_name)) - os.remove(file_name) + elif sys.platform == 'Darwin': + # This will exclude the '._' files in the + # tar.gz package for OS X. + os.environ["COPYFILE_DISABLE"] = "1" - return 0 + settings_file = os.path.join(os.getcwd(), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) - def action_ping(self, ip_address): - """ - Go to our router, and try to ping remotely - - :param str ip_address: the IP to pin - :return int: value intended for sys.exit() - """ - # TODO - if ip_address IS our router, do ping from PC? - - self.logger.info("Send a PING to {} from router({})".format( - ip_address, self.get_router_ip())) - - result = cs_ping(self, ip_address) - if result['status'] == "success": - return 0 - return EXIT_CODE_BAD_ACTION - - def _copy_a_file(self, file_name): - """ - Copy the file over, using logic: - 1) if {app_path}/{file_name} exists, use that - 2) else is {config}/{file_name} exists, use that - 3) else return False - - :param str file_name: the file to handle - :rtype: None - """ - - app_file_name = os.path.join(self.get_app_path(), file_name) - cfg_file_name = os.path.join(SDIR_CONFIG, file_name) - dst_file_name = os.path.join(self.get_build_path(), file_name) - - if os.path.exists(app_file_name): - # if app developer supplies one, use it (TDB - do pre-processing) - self.logger.debug("Copy existing app script from [{}]".format( - app_file_name)) - copy_file_nl(app_file_name, dst_file_name) - # sh util.copyfile(app_file_name, dst_file_name) - self.logger.debug("Add file [{}] to exclude list".format( - app_file_name)) - self._exclude.append(app_file_name) - - elif os.path.exists(cfg_file_name): - # if root supplies one, use it (TDB - do pre-processing) - self.logger.debug("Copy existing root script from [{}]".format( - cfg_file_name)) - copy_file_nl(cfg_file_name, dst_file_name) - # sh util.copyfile(cfg_file_name, dst_file_name) + # Initialize the globals based on the sdk_settings.ini contents. + if sdk_key in config: + if app_key in config[sdk_key]: + g_app_name = config[sdk_key][app_key] + else: + success = False + print('ERROR 1: The {} key does not exist in {}'.format(app_key, settings_file)) + if ip_key in config[sdk_key]: + g_dev_client_ip = config[sdk_key][ip_key] else: - return False - - return True - - def create_install_sh(self, file_name=None): - """ - Create the install.sh in BUILD, using logic: - 1) if {app_path}/install.sh exists, use that - 2) else is {config}/install.sh exists, use that - 3) else make a basic default - - Final name will be in build/install.sh - - :param str file_name: an alternative name for safe regression testing - :rtype: None - """ - self.logger.info("Create INSTALL.SH script") - - if file_name is None: - file_name = FILE_NAME_INSTALL - - if not self._copy_a_file(file_name): - file_name = os.path.join(self.get_build_path(), file_name) - # self.logger.debug("create new {}".format(file_name)) - data = [ - '#!/bin/bash\n', - 'echo "INSTALLATION for {0}:" >> install.log\n'.format( - self.mod_name), - 'date >> install.log\n' - ] - file_han = open(file_name, 'wb') - for line in data: - # we don't want a Windows file, treat as binary ("wb" not "w") - file_han.write(line.encode()) - file_han.close() - - return - - def create_start_sh(self, file_name=None): - """ - Create the start.sh, or copy from config. - - Final name will be in router_app/start.sh - - :param str file_name: alternative name for safe regression testing - :rtype: None - """ - self.logger.info("Create START.SH script") - - if file_name is None: - file_name = FILE_NAME_START - - if not self._copy_a_file(file_name): - file_name = os.path.join(self.get_build_path(), file_name) - - # self.logger.debug("create new {}".format(file_name)) - data = [ - '#!/bin/bash\n', - 'cppython main.py %s\n' % self.mod_name - ] - file_han = open(file_name, 'wb') - for line in data: - # we don't want a Windows file, treat as binary ("wb" not "w") - file_han.write(line.encode()) - file_han.close() - - return - - def create_go_bat(self): - """ - Create a simple go.bat on Windows - """ - if sys.platform == "win32": - file_name = os.path.join(self.get_build_path(), "go.bat") - self.logger.info("Create {} script".format(file_name)) - data = 'python main.py %s' % self.mod_name - file_han = open(file_name, 'w') - file_han.write(data) - file_han.close() - - return - - def _confirm_dir_exists(self, dir_name, dir_msg=None): - """ - Confirm the directory exists, create if required. If the name - is blocked by a file of - the same name, then rename as {name}.save - - :param str dir_name: the relative directory name - :param str dir_msg: a message for errors, like "CONFIG dir" - :return: - """ - # confirm ./config exists and is not a file - if os.path.isfile(dir_name): - # rename as .save, then allow creation below - shutil.copyfile(dir_name, dir_name + self.SDIR_SAVE_EXT) - os.remove(dir_name) - - if not os.path.isdir(dir_name): - # if not there, make it! - self.logger.debug("{}({}) being created".format(dir_msg, dir_name)) - - # NOT working under windows! Add delay, seems to be race condition - # where adding immediately after delete fails as PermissionError! - try: - os.makedirs(dir_name) - except PermissionError: - time.sleep(1.0) - os.makedirs(dir_name) - - file_name = os.path.join(dir_name, "__init__.py") - self.logger.info("Create __init__.py") - file_han = open(file_name, 'w') - file_han.write(" ") - file_han.close() - - # else: - # self.logger.debug("{}({}) exists as dir".format( - # dir_msg, dir_name)) - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) + success = False + print('ERROR 2: The {} key does not exist in {}'.format(ip_key, settings_file)) + if username_key in config[sdk_key]: + g_dev_client_username = config[sdk_key][username_key] else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - def dump_help(self, args): - """ - - :param list args: the command name - :return: - """ - print("Syntax:") - print(' {} -m '.format(args[0])) - print() - print(" Default action = {}".format(self.ACTION_DEFAULT)) - for command in self.ACTION_NAMES: - print() - print("- action={0}".format(command)) - print(" {0}".format(self.ACTION_HELP[command])) - - return + success = False + print('ERROR 3: The {} key does not exist in {}'.format(username_key, settings_file)) -if __name__ == "__main__": + if password_key in config[sdk_key]: + g_dev_client_password = config[sdk_key][password_key] + else: + success = False + print('ERROR 4: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + success = False + print('ERROR 5: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + # This will also create a UUID if needed. + get_app_uuid() - maker = TheMaker() + return success - # Use below for debug testing - # sys.argv = ['-v', '-m', 'build', 'simple.hello_world'] - # sys.argv = ['-v', '-m', 'status'] +if __name__ == "__main__": + + # Default is no arguments given. if len(sys.argv) < 2: - maker.dump_help(sys.argv) - sys.exit(-1) + output_help() + sys.exit(0) - # if cmdline is only "make", then run as "make build" but we'll - # expect ["name"] in global sets + utility_name = str(sys.argv[1]).lower() - # save this, just in case we care later - utility_name = sys.argv[0] + if not init(): + sys.exit(0) - index = 1 - while index < len(sys.argv): - # loop through an process the parameters + if utility_name == 'clean': + clean() - if sys.argv[index] in ('-m', '-M'): - # then what follows is the mode/action - action = sys.argv[index + 1].lower() - if action in maker.ACTION_NAMES: - # then it is indeed an action - maker.action = action - index += 1 # need an extra ++ as -m includes 2 params + elif utility_name == 'build': + # build() + package() - elif sys.argv[index] in ('-i', '-I', '+i', '+I'): - # then we'll want the [app][version] incremented in the system.ini - maker.increment_version = True + elif utility_name == 'package': + package() - elif sys.argv[index] in ('-p', '-P'): - # then we'll ignore some PIP dependencies, as we're running - # on PC only - maker.ignore_pip = True + elif utility_name == 'status': + status() - elif sys.argv[index] in ('-v', '-V', '+v', '+V'): - # switch the logger to DEBUG from INFO - maker.verbose = True + elif utility_name == 'install': + install() - else: - # assume this is the app path - maker.app_path = sys.argv[index] + elif utility_name == 'start': + start() - index += 1 # get to next setting + elif utility_name == 'stop': + stop() - if maker.verbose: - logging.basicConfig(level=logging.DEBUG) - maker.logger.setLevel(level=logging.DEBUG) + elif utility_name == 'uninstall': + uninstall() - else: - logging.basicConfig(level=logging.INFO) - maker.logger.setLevel(level=logging.INFO) - # quiet INFO messages from requests module, - # "requests.packages.urllib3.connection pool" - logging.getLogger('requests').setLevel(logging.WARNING) + elif utility_name == 'purge': + purge() - _result = maker.main() - if _result != 0: - logging.error("return is {}".format(_result)) + else: + output_help() - sys.exit(_result) + sys.exit(0) diff --git a/serial_port/modbus_poll/crc16.py b/modbus_poll/crc16.py similarity index 100% rename from serial_port/modbus_poll/crc16.py rename to modbus_poll/crc16.py diff --git a/modbus_poll/cs.py b/modbus_poll/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/modbus_poll/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/modbus_poll/install.sh b/modbus_poll/install.sh new file mode 100644 index 00000000..315f4592 --- /dev/null +++ b/modbus_poll/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION modbus_poll on:" >> install.log +date >> install.log diff --git a/modbus_poll/modbus_poll.py b/modbus_poll/modbus_poll.py new file mode 100644 index 00000000..123518d6 --- /dev/null +++ b/modbus_poll/modbus_poll.py @@ -0,0 +1,166 @@ +""" +Poll a single range of Modbus registers from an attached serial +Modbus/RTU PLC or slave device. The poll is repeated, in a loop. +The only output you'll see if via Syslog. + +If you need such a device, do an internet search for "modbus simulator', as +there are many available for free or low (shareware) cost. These run on a +computer, serving up data by direct or USB serial port. + +* port_name=???, define the serial port to use. Commonly this will be +/dev/ttyS1 or /dev/ttyUSB0 + +* baud_rate=9600, allows you to define a different baud rate. This sample +assumes the other settings are fixed at: bytesize=8, parity='N', stopbits=1, +and all flow control (XonXOff and HW) is off/disabled. +Edit the code if you need to change this. + +* register_start=0, the raw Modbus offset, so '0' and NOT 40001. +Permitted range is 0 to 65535 + +* register_count=4, the number of Holding Register to read. +The request function is fixed to 3, so read multiple holding registers. +The permitted count is 1 to 125 registers (16-bit words) + +* slave_address=1, the Modbus slave address, which must be in the range +from 1 to 255. Since Modbus/RTU is a multi-drop line, the slave +address is used to select 1 of many slaves. +For example, if a device is assigned the address 7, it will ignore all +requests with slave addresses other than 7. + +* poll_delay=15 sec, how often to repoll the device. A lone number (like 60) + is interpreted as seconds. However, it uses the CP library module + "parse_duration", so time tags such as 'sec', 'min, 'hr' can be used. +""" +import json +import serial +import time +import argparse +import parse_duration +import crc16 +import cs + + +APP_NAME = 'modbus_poll' + + +def run_router_app(): + # confirm we are running on an 900/950 or 1100/1150, result should be like "IBR1100LPE" + result = json.loads(cs.CSClient().get("status/product_info/product_name")) + if "IBR900" in result or "IBR950" in result or \ + "IBR1100" in result or "IBR1150" in result: + cs.CSClient().log(APP_NAME, "Product Model is good:{}".format(result)) + else: + cs.CSClient().log(APP_NAME, + "ERROR: Inappropriate Product:{} - aborting.".format(result)) + return -1 + + period = parse_duration.TimeDuration(5) + + port_name = "/dev/ttyS1" + baud_rate = 9600 + register_start = 0 + register_count = 1 + slave_address = 1 + poll_delay = period.get_seconds() + + # see if port is a digit? + if port_name[0].isdecimal(): + port_name = int(port_name) + + # a few validation tests + if not 0 <= register_start <= 0xFFFF: + raise ValueError("Modbus start address must be between 0 & 0xFFFF") + if not 1 <= register_count <= 125: + raise ValueError("Modbus count must be between 1 & 125") + if not 1 <= slave_address <= 255: + raise ValueError("Modbus address must be between 1 & 125") + if poll_delay < 1: + raise ValueError("Poll delay most be 1 second or longer") + + poll_delay = float(poll_delay) + + # make a fixed Modbus 4x register read/poll + poll = bytes([slave_address, 0x03, + (register_start & 0xFF00) >> 8, register_start & 0xFF, + (register_count & 0xFF00) >> 8, register_count & 0xFF]) + crc = crc16.calc_string(poll) + cs.CSClient().log(APP_NAME, "CRC = %04X" % crc) + poll += bytes([crc & 0xFF, (crc & 0xFF00) >> 8]) + + cs.CSClient().log(APP_NAME, + "Starting Modbus/RTU poll {0}, baud={1}".format(port_name, baud_rate)) + cs.CSClient().log(APP_NAME, + "Modbus/RTU request is {0}".format(poll)) + + try: + ser = serial.Serial(port_name, baudrate=baud_rate, bytesize=8, + parity='N', stopbits=1, timeout=0.25, + xonxoff=0, rtscts=0) + + except serial.SerialException: + cs.CSClient().log(APP_NAME, "ERROR: Open failed!") + raise + + try: + while True: + + cs.CSClient().log(APP_NAME, "Send poll") + ser.write(poll) + time.sleep(0.1) + + try: + response = ser.read(size=255) + except KeyboardInterrupt: + cs.CSClient().log(APP_NAME, + "WARNING: Keyboard Interrupt - asked to quit") + break + + if len(response): + cs.CSClient().log(APP_NAME, + "Modbus/RTU response is {0}".format(response)) + + else: + cs.CSClient().log(APP_NAME, + "ERROR: no Modbus/RTU response") + + try: + time.sleep(poll_delay) + except KeyboardInterrupt: + cs.CSClient().log(APP_NAME, + "WARNING: Keyboard Interrupt - asked to quit") + break + + finally: + ser.close() + + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + run_router_app() + + elif command == 'stop': + # Nothing on stop + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/modbus_poll/package.ini b/modbus_poll/package.ini new file mode 100644 index 00000000..f4fe73e2 --- /dev/null +++ b/modbus_poll/package.ini @@ -0,0 +1,11 @@ +[modbus_poll] +uuid=ca0a62bd-648e-4bbc-a915-5f67a477c82d +vendor=Cradlepoint +notes=Poll a Modbus/RTU register from a PLC/slave +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/cp_lib/parse_duration.py b/modbus_poll/parse_duration.py similarity index 100% rename from cp_lib/parse_duration.py rename to modbus_poll/parse_duration.py diff --git a/modbus_poll/start.sh b/modbus_poll/start.sh new file mode 100644 index 00000000..3907ce62 --- /dev/null +++ b/modbus_poll/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython modbus_poll.py start diff --git a/modbus_poll/stop.sh b/modbus_poll/stop.sh new file mode 100644 index 00000000..2fedb6cf --- /dev/null +++ b/modbus_poll/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython modbus_poll.py stop diff --git a/modbus_simple_bridge/__init__.py b/modbus_simple_bridge/__init__.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/modbus_simple_bridge/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modbus_simple_bridge/cp_lib/__init__.py b/modbus_simple_bridge/cp_lib/__init__.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/modbus_simple_bridge/cp_lib/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cp_lib/app_base.py b/modbus_simple_bridge/cp_lib/app_base.py similarity index 100% rename from cp_lib/app_base.py rename to modbus_simple_bridge/cp_lib/app_base.py diff --git a/cp_lib/app_name_parse.py b/modbus_simple_bridge/cp_lib/app_name_parse.py similarity index 100% rename from cp_lib/app_name_parse.py rename to modbus_simple_bridge/cp_lib/app_name_parse.py diff --git a/cp_lib/buffer_dump.py b/modbus_simple_bridge/cp_lib/buffer_dump.py similarity index 100% rename from cp_lib/buffer_dump.py rename to modbus_simple_bridge/cp_lib/buffer_dump.py diff --git a/cp_lib/cp_logging.py b/modbus_simple_bridge/cp_lib/cp_logging.py similarity index 100% rename from cp_lib/cp_logging.py rename to modbus_simple_bridge/cp_lib/cp_logging.py diff --git a/cp_lib/cs_client.py b/modbus_simple_bridge/cp_lib/cs_client.py similarity index 100% rename from cp_lib/cs_client.py rename to modbus_simple_bridge/cp_lib/cs_client.py diff --git a/cp_lib/cs_client_remote.py b/modbus_simple_bridge/cp_lib/cs_client_remote.py similarity index 100% rename from cp_lib/cs_client_remote.py rename to modbus_simple_bridge/cp_lib/cs_client_remote.py diff --git a/modbus_simple_bridge/cp_lib/data/__init__.py b/modbus_simple_bridge/cp_lib/data/__init__.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/modbus_simple_bridge/cp_lib/data/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cp_lib/data/data_tree.py b/modbus_simple_bridge/cp_lib/data/data_tree.py similarity index 100% rename from cp_lib/data/data_tree.py rename to modbus_simple_bridge/cp_lib/data/data_tree.py diff --git a/cp_lib/hw_status.py b/modbus_simple_bridge/cp_lib/hw_status.py similarity index 100% rename from cp_lib/hw_status.py rename to modbus_simple_bridge/cp_lib/hw_status.py diff --git a/cp_lib/load_firmware_info.py b/modbus_simple_bridge/cp_lib/load_firmware_info.py similarity index 100% rename from cp_lib/load_firmware_info.py rename to modbus_simple_bridge/cp_lib/load_firmware_info.py diff --git a/cp_lib/load_product_info.py b/modbus_simple_bridge/cp_lib/load_product_info.py similarity index 100% rename from cp_lib/load_product_info.py rename to modbus_simple_bridge/cp_lib/load_product_info.py diff --git a/cp_lib/load_settings_json.py b/modbus_simple_bridge/cp_lib/load_settings_json.py similarity index 100% rename from cp_lib/load_settings_json.py rename to modbus_simple_bridge/cp_lib/load_settings_json.py diff --git a/modbus_simple_bridge/cp_lib/modbus/__init__.py b/modbus_simple_bridge/cp_lib/modbus/__init__.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/modbus_simple_bridge/cp_lib/modbus/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/cp_lib/modbus/modbus_asc.py b/modbus_simple_bridge/cp_lib/modbus/modbus_asc.py similarity index 100% rename from cp_lib/modbus/modbus_asc.py rename to modbus_simple_bridge/cp_lib/modbus/modbus_asc.py diff --git a/cp_lib/modbus/modbus_rtu.py b/modbus_simple_bridge/cp_lib/modbus/modbus_rtu.py similarity index 100% rename from cp_lib/modbus/modbus_rtu.py rename to modbus_simple_bridge/cp_lib/modbus/modbus_rtu.py diff --git a/cp_lib/modbus/modbus_tcp.py b/modbus_simple_bridge/cp_lib/modbus/modbus_tcp.py similarity index 100% rename from cp_lib/modbus/modbus_tcp.py rename to modbus_simple_bridge/cp_lib/modbus/modbus_tcp.py diff --git a/cp_lib/modbus/transaction.py b/modbus_simple_bridge/cp_lib/modbus/transaction.py similarity index 100% rename from cp_lib/modbus/transaction.py rename to modbus_simple_bridge/cp_lib/modbus/transaction.py diff --git a/cp_lib/modbus/transaction_modbus.py b/modbus_simple_bridge/cp_lib/modbus/transaction_modbus.py similarity index 100% rename from cp_lib/modbus/transaction_modbus.py rename to modbus_simple_bridge/cp_lib/modbus/transaction_modbus.py diff --git a/cp_lib/parse_data.py b/modbus_simple_bridge/cp_lib/parse_data.py similarity index 100% rename from cp_lib/parse_data.py rename to modbus_simple_bridge/cp_lib/parse_data.py diff --git a/modbus_simple_bridge/cp_lib/parse_duration.py b/modbus_simple_bridge/cp_lib/parse_duration.py new file mode 100644 index 00000000..c6eb0a71 --- /dev/null +++ b/modbus_simple_bridge/cp_lib/parse_duration.py @@ -0,0 +1,285 @@ +""" +Helper function to parse a time durations; use '3 hr' or '15 min' instead +of mind-bending settings such as 10800 and 900 seconds +""" + +from cp_lib.parse_data import parse_float, parse_integer + +__version__ = "1.0.0" + +# History: +# +# 1.0.0: 2015-Apr Lynn +# * initial rewrite +# + + +class TimeDuration(object): + """ + Helper class to process user settings such as "3 hr" or "5 min", rather + than demanding a setting like 900 sec + + It handles basically 2 classes of data: + 1) lone token must be a numeric in seconds, so 5 or " 5" means 5 seconds. + 2) time tag in the set ('sec', 'ms', 'min', 'hr', 'day', 'mon', 'yr') + decorates so "5 min" becomes 900 sec + + In addition, one can add a + or -, so "-5 min" means -900 seconds. This + can be used to trigger an even before the hour, such once an hour plus + "-5 min" means at minute = 55 of the hour. + + One can also decorate with UTC, Z, or GM. This rarely affects the seconds, + but can be used by callers to as example trigger something every 12 hours + based on UTC (or local). + + """ + + DURATION_SECOND = 0 + DURATION_MSEC = 1 + DURATION_MINUTE = 2 + DURATION_HOUR = 3 + DURATION_DAY = 4 + DURATION_MONTH = 5 + DURATION_YEAR = 6 + DURATION_NAME_LIST = ('sec', 'ms', 'min', 'hr', 'day', 'mon', 'yr') + DURATION_FROM_SECS = (1.0, 1000.0, 1/60.0, 1/3600.0, 1/86400.0, None, None) + DURATION_TO_SECS = (1.0, 0.001, 60.0, 3600.0, 86400.0, None, None) + + DURATION_TAG_TO_MSEC = { + DURATION_MSEC: 1, + DURATION_SECOND: 1000, + DURATION_MINUTE: 60000, + DURATION_HOUR: 3600000, + DURATION_DAY: 86400000, + # no Month or Year due to variable days-per-month and leap-years + # DURATION_MONTH: 0, + # DURATION_YEAR: 0 + } + + FORMAT_FLOAT = "%0.2f %s" + FORMAT_INT = "%d %s" + + def __init__(self, source=None): + self.period_code = self.DURATION_SECOND + self.period_value = 0 + self.seconds = None + self.utc = None + self.format = self.FORMAT_INT + + if source is not None: + self.parse_time_duration_to_seconds(source) + return + + def reset(self): + self.period_code = self.DURATION_SECOND + self.period_value = 0 + self.seconds = None + self.utc = None + self.format = self.FORMAT_INT + return + + def get_period_as_string(self): + """ + :return: return the formatted STRING of the period, such as "5 min" + :rtype: str + """ + # TODO - add the UTC support, and what about sign? + # print "period_value:%s type=%s" % (self.period_value, + # type(self.period_value)) + # print " period_code:%s type=%s" % (self.period_code, + # type(self.period_code)) + return self.format % (self.period_value, + self.DURATION_NAME_LIST[self.period_code]) + + def get_tag_as_string(self, tag=None): + """ + :return: return the simple STRING tag of the period, so + if DURATION_MINUTE, return "min" + :rtype: str + """ + if tag is None: + tag = self.period_code + + elif not isinstance(tag, int): + raise TypeError("Duration enum must be between 0 and 6") + + elif not (self.DURATION_SECOND <= tag <= self.DURATION_YEAR): + raise ValueError("Duration enum must be between 0 and 6") + + return self.DURATION_NAME_LIST[tag] + + def get_seconds(self): + """ + :return: return the seconds in thi period, or ValueError if no + seconds (such as for month or year) + :rtype: int or float + """ + if self.seconds is None: + raise ValueError("get_seconds() impossible for period:%s" % + self.get_tag_as_string()) + return self.seconds + + def parse_time_duration_to_seconds(self, source, delimiter=None): + """ + Parse a given time duration into a specified unit of measure. + + Supports integer, float, and string input. String input will be split + into a list and parsed if necessary. + + The in/out type must be in ['ms', 'sec', 'min', 'hr', 'day'] + 'mon' and 'year' NOT supported + + :param source: the source duration to be parsed, which might be 10, + or '10 min' or '10 min utc' + :type source: int, str, bytes + :param delimiter: allows using ',' or other tag delimiter. If None, + use space (' ') + :type delimiter: str or None + :return: seconds as float + :rtype: float + """ + + if isinstance(source, int) or isinstance(source, float): + # _tracer.info('is number, so just return SAME type') + self.period_code = self.DURATION_SECOND + self.period_value = source + self.seconds = source + return self.seconds + + if isinstance(source, bytes): + # make bytes into string + source = source.decode() + + # get rid of any leading/trailing spaces + source = source.strip().lower() + + # this should be one of three forms + # 1) "60" which is already seconds + # 2) "60 min", which is a number plus the time tag + # 3) "60 min utz", which adds a time-zone tag + if delimiter is None: + delimiter = ' ' + elements = source.split(delimiter) + + # see if LAST token is UTC decoration - this examines the LAST + # space-delimited token + self.utc = self._decode_utc_element(elements[-1]) + if self.utc: + # if True, pop off the last item in elements + elements.pop() + + self._decode_a_pair(elements) + + return self.seconds + + def _decode_a_pair(self, pair): + """ + Given a two-element list such as ['5', 'sec'] or (10, 'min'), with + the second element in the set: + ['ms', 'sec', 'min', 'hr', 'day', 'mon', 'yr']. + + :param pair: two-element list such as ['5', 'sec'] or (10, 'min') + :type pair: list + :return: adjusts 'self' if okay, else throws exception + :rtype: None + """ + if len(pair) < 1: + raise ValueError( + "_decode_a_pair() requires at least 1 element in list") + + if pair[0].find('.') >= 0: + # then is a float + self.period_value = parse_float(pair[0]) + self.format = self.FORMAT_FLOAT + else: + self.period_value = parse_integer(pair[0]) + self.format = self.FORMAT_INT + + if len(pair) > 1: + # obtain period code, like DURATION_SECOND or DURATION_HOUR + self.period_code = self.decode_time_tag(pair[1]) + else: + self.period_code = self.DURATION_SECOND + + # obtain the number, convert to seconds + if self.DURATION_TO_SECS[self.period_code] is None: + # for Month/Year, there are no 'seconds' + self.seconds = None + else: + # else calc seconds from 2 'period values' (10 minutes = 600 sec) + self.seconds = self.period_value * \ + self.DURATION_TO_SECS[self.period_code] + + # print "period_code:%s type=%s" % (self.period_code, + # type(self.period_code)) + return True + + @staticmethod + def _decode_utc_element(utc): + """ + Given a source string, such as '5 day' (etc), check for a + UTC/Z/Zulu/uct/gm tag (not case sensitive). + We assume the tag is the LAST space-delimited token in the string + + So these strings all return True: + * 'utc', 'min zulu', '2 hr utc', 'silly funny 5 crap utc' + + So these strings all return False: + * '', 'min', '2 hr', 'silly funny 5 crap' + + :param utc: string like "hour" or "utc" + :type utc: str + :return: True if UTC was indicated, else is False + :rtype: bool + """ + if isinstance(utc, str): + utc = utc.lower() + if utc.endswith('z'): + return True + if utc.endswith('utc'): + return True + if utc.endswith('gm'): + return True + if utc.endswith('uct'): + return True + if utc.endswith('zulu'): + return True + return False + + def decode_time_tag(self, source): + """ + :param source: source string + :type source: str + :return: int code for the period + :rtype: int + """ + if not isinstance(source, str): + raise TypeError("decode_time_tag(%s) requires STRING, not %s" % + (str(source), type(source))) + + # only check if we seem to have something + source = source.lower() + ' ' + source = source[:3] # first 3 char only + + if source == 'sec': # then sec, secs, second, seconds + return self.DURATION_SECOND + + elif source == 'min': # then min, mins, minute, minutes + return self.DURATION_MINUTE + + elif source in ('hr ', 'hrs', 'hou'): # then hr, hrs, hour, hours + return self.DURATION_HOUR + + elif source in ('dy ', 'dys', 'day'): # then d, dy, dys, day, days + return self.DURATION_DAY + + elif source in ('mse', 'ms ', 'mil'): # then ms, msec, millis... + return self.DURATION_MSEC + + elif source in ('mn ', 'mon'): # then special for Month + return self.DURATION_MONTH + + elif source in ('yr ', 'yea'): # then special for YEAR + return self.DURATION_YEAR + + raise ValueError('decode_time_tag(%s) unknown string source' % source) diff --git a/cp_lib/probe_serial.py b/modbus_simple_bridge/cp_lib/probe_serial.py similarity index 100% rename from cp_lib/probe_serial.py rename to modbus_simple_bridge/cp_lib/probe_serial.py diff --git a/cp_lib/split_version.py b/modbus_simple_bridge/cp_lib/split_version.py similarity index 100% rename from cp_lib/split_version.py rename to modbus_simple_bridge/cp_lib/split_version.py diff --git a/cp_lib/unquote_string.py b/modbus_simple_bridge/cp_lib/unquote_string.py similarity index 100% rename from cp_lib/unquote_string.py rename to modbus_simple_bridge/cp_lib/unquote_string.py diff --git a/modbus_simple_bridge/go.bat b/modbus_simple_bridge/go.bat new file mode 100644 index 00000000..3bbd39eb --- /dev/null +++ b/modbus_simple_bridge/go.bat @@ -0,0 +1 @@ +python main.py serial_port.modbus_simple_bridge \ No newline at end of file diff --git a/modbus_simple_bridge/install.sh b/modbus_simple_bridge/install.sh new file mode 100644 index 00000000..65a95cd5 --- /dev/null +++ b/modbus_simple_bridge/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION for serial_port.modbus_simple_bridge:" >> install.log +date >> install.log diff --git a/config/main.py b/modbus_simple_bridge/main.py similarity index 100% rename from config/main.py rename to modbus_simple_bridge/main.py diff --git a/modbus_simple_bridge/package.ini b/modbus_simple_bridge/package.ini new file mode 100644 index 00000000..0c7a6bd6 --- /dev/null +++ b/modbus_simple_bridge/package.ini @@ -0,0 +1,12 @@ +[modbus_simple_bridge] +uuid=07c01e6b-b080-43e8-9576-ca43e1b8e036 +vendor=Sample Code, Inc. +notes=Simple 1-thread Modbus/TCP to RTU bridge +restart=true +reboot=true +auto_start=true +firmware_major=6 +firmware_minor=1 +version_major=1 +version_minor=27 +version_patch=0 diff --git a/serial_port/modbus_simple_bridge/README.md b/modbus_simple_bridge/serial_port/modbus_simple_bridge/README.md similarity index 100% rename from serial_port/modbus_simple_bridge/README.md rename to modbus_simple_bridge/serial_port/modbus_simple_bridge/README.md diff --git a/serial_port/modbus_simple_bridge/__init__.py b/modbus_simple_bridge/serial_port/modbus_simple_bridge/__init__.py similarity index 100% rename from serial_port/modbus_simple_bridge/__init__.py rename to modbus_simple_bridge/serial_port/modbus_simple_bridge/__init__.py diff --git a/serial_port/modbus_simple_bridge/modbus_tcp_bridge.py b/modbus_simple_bridge/serial_port/modbus_simple_bridge/modbus_tcp_bridge.py similarity index 100% rename from serial_port/modbus_simple_bridge/modbus_tcp_bridge.py rename to modbus_simple_bridge/serial_port/modbus_simple_bridge/modbus_tcp_bridge.py diff --git a/modbus_simple_bridge/settings.json b/modbus_simple_bridge/settings.json new file mode 100644 index 00000000..ad5def62 --- /dev/null +++ b/modbus_simple_bridge/settings.json @@ -0,0 +1,42 @@ +{ + "application": { + "_comment": "Settings for the application being built.", + "auto_start": "true", + "description": "Simple 1-thread Modbus/TCP to RTU bridge", + "firmware": "6.1", + "name": "modbus_simple_bridge", + "path": "serial_port/modbus_simple_bridge", + "reboot": "true", + "restart": "true", + "uuid": "07c01e6b-b080-43e8-9576-ca43e1b8e036", + "vendor": "Sample Code, Inc.", + "version": "1.27" + }, + "logging": { + "_comment": "Settings for the application debug/syslog/logging function.", + "level": "debug", + "pc_syslog": "false", + "syslog_ip": "/dev/log" + }, + "modbus_ip": { + "host_port": "8502", + "idle_timeout": "30 sec" + }, + "modbus_serial": { + "baud_rate": "9600", + "parity": "N", + "port_name": "/dev/ttyS1" + }, + "router_api": { + "_comment": "Settings to allow accessing router API in development mode.", + "local_ip": "192.168.0.1", + "password": "44224267", + "user_name": "admin" + }, + "startup": { + "boot_delay_for_time": "True", + "boot_delay_for_wan": "True", + "boot_delay_max": "5 min", + "exit_delay": "15 sec" + } +} \ No newline at end of file diff --git a/modbus_simple_bridge/start.sh b/modbus_simple_bridge/start.sh new file mode 100644 index 00000000..66efee34 --- /dev/null +++ b/modbus_simple_bridge/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython main.py serial_port.modbus_simple_bridge diff --git a/network/README.md b/network/README.md deleted file mode 100644 index dc358ef4..00000000 --- a/network/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Router App/SDK sample applications. - -Remember - for any network 'server' application, you will need to go into -the router's Zone Firewall configuration and enable the appropriate traffic -to reach your router. By default, all incoming client packets will be -discarded. - -## Directory simple\_web - -A very basic web server - a "Hello World" by web, -using the standard Python 3 "http.server" module. - -_Note: requires Router Zone Firewall Changes_ - -## Directory digit\_web - -A more complex basic web server, using the standard Python 3 "http.server" -module. It returns a slightly dynamic page of 5 'digit' images, representing -a 'counter'. In the sample, the digits are fixed at "00310", but a richer -design could allow new numbers to be used. - -_Note: requires Router Zone Firewall Changes_ - -## Directory send\_ping - -Uses the router API 'control' tree to issue a raw Ethernet ping. - -## Directory send\_email - -Sends a fixed email to gmail.com, using TTL/SSL. If you don't have a gmail -account, you can set up a free one. It can also send to other SMTP servers, -but you may need to change the way the SSL works, TCP ports used, etc. - -## Directory tcp\_echo - -binds on a TCP port, returning (echoing) any data received. - -_Note: requires Router Zone Firewall Changes_ diff --git a/network/digit_web/README.md b/network/digit_web/README.md deleted file mode 100644 index 58a3c520..00000000 --- a/network/digit_web/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# directory: ./network/digit_web -## Router App/SDK sample applications - -A more complex basic web server, using the standard Python 3 "http.server" -module. It returns a slightly dynamic page of 5 'digit' images, representing -a 'counter'. In the sample, the digits are fixed at "00310", but a richer -design could allow new numbers to be used. - -More importantly, the fact we have images on the text page means any -one client request results in up to 6 requests - the first returns the -text page with the 5 images, and then up to 5 more are submitted to -fetch the images. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: web_server.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [web_server]: - -* host_port=9001, define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. -Also, avoid 8001 or 8080, as router may be using already. - -## Web Page is K.I.S.S. (Keep-It-Simple-Stupid) - -Obviously could make the web page was complex as desired - using Javascript -to auto-refresh, and so on. - - - - - - The Count Is - - - - - - - - - - -
- - diff --git a/network/digit_web/__init__.py b/network/digit_web/__init__.py deleted file mode 100644 index 948f8526..00000000 --- a/network/digit_web/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from network.digit_web.web_server import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/network/digit_web/favicon.ico b/network/digit_web/favicon.ico deleted file mode 100644 index 871f8555..00000000 Binary files a/network/digit_web/favicon.ico and /dev/null differ diff --git a/network/digit_web/sdigit_0.jpg b/network/digit_web/sdigit_0.jpg deleted file mode 100644 index 8ce2365f..00000000 Binary files a/network/digit_web/sdigit_0.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_1.jpg b/network/digit_web/sdigit_1.jpg deleted file mode 100644 index 84fb432b..00000000 Binary files a/network/digit_web/sdigit_1.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_2.jpg b/network/digit_web/sdigit_2.jpg deleted file mode 100644 index 2f5f8498..00000000 Binary files a/network/digit_web/sdigit_2.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_3.jpg b/network/digit_web/sdigit_3.jpg deleted file mode 100644 index c916f5d8..00000000 Binary files a/network/digit_web/sdigit_3.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_4.jpg b/network/digit_web/sdigit_4.jpg deleted file mode 100644 index cd343024..00000000 Binary files a/network/digit_web/sdigit_4.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_5.jpg b/network/digit_web/sdigit_5.jpg deleted file mode 100644 index 02fe6d55..00000000 Binary files a/network/digit_web/sdigit_5.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_6.jpg b/network/digit_web/sdigit_6.jpg deleted file mode 100644 index 89922b0f..00000000 Binary files a/network/digit_web/sdigit_6.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_7.jpg b/network/digit_web/sdigit_7.jpg deleted file mode 100644 index ccf6ddfa..00000000 Binary files a/network/digit_web/sdigit_7.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_8.jpg b/network/digit_web/sdigit_8.jpg deleted file mode 100644 index 51c46263..00000000 Binary files a/network/digit_web/sdigit_8.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_9.jpg b/network/digit_web/sdigit_9.jpg deleted file mode 100644 index 26045bd8..00000000 Binary files a/network/digit_web/sdigit_9.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_blank.jpg b/network/digit_web/sdigit_blank.jpg deleted file mode 100644 index b0b6b61a..00000000 Binary files a/network/digit_web/sdigit_blank.jpg and /dev/null differ diff --git a/network/digit_web/sdigit_dot.jpg b/network/digit_web/sdigit_dot.jpg deleted file mode 100644 index 8516e45c..00000000 Binary files a/network/digit_web/sdigit_dot.jpg and /dev/null differ diff --git a/network/digit_web/settings.ini b/network/digit_web/settings.ini deleted file mode 100644 index 00ffa1fd..00000000 --- a/network/digit_web/settings.ini +++ /dev/null @@ -1,13 +0,0 @@ -; one app settings - -[application] -name=digit_web -description=Web server, shows 5 digits as images -path = network/digit_web -version = 1.7 -uuid=8a3e1bb6-71d5-4ee7-8023-4559c578c080 - -[web_server] -; port cannot be less than 1024 on CP router, and avoid 8001 & 8080 -host_port=9001 -start_count=10934 diff --git a/network/digit_web/web_server.py b/network/digit_web/web_server.py deleted file mode 100644 index 678cdd80..00000000 --- a/network/digit_web/web_server.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -A basic but complete echo server -""" -import threading -import time -from http.server import BaseHTTPRequestHandler, HTTPServer - -from cp_lib.app_base import CradlepointAppBase - -# avoid 8080, as the router may have service on it. -DEF_HOST_PORT = 9001 -DEF_HOST_IP = "" -DEF_START_COUNT = 1099 - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - my_server = WebServerThread('Digit_Web', app_base) - my_server.start() - - # we need to block the main thread here, because this sample is running - # a SECOND thread for the actual server. This makes no sense in a pure - # sample-code scenario, but doing it this way does allow you to - # import & run the class WebServerThread() from another demo app - # which requires multiple threads - such as my Counter demo which - # requires both a web server AND a JSON RPC server as 2 threads. - try: - while True: - time.sleep(15.0) - - except KeyboardInterrupt: - app_base.logger.info("Stopping Server, Key Board interrupt") - - return 0 - - -class WebServerThread(threading.Thread): - - def __init__(self, name, app_base): - """ - prep our thread, but do not start yet - - :param str name: name for the thread - :param CradlepointAppBase app_base: prepared resources: logger, etc - """ - threading.Thread.__init__(self, name=name) - - self.app_base = app_base - self.app_base.logger.info("started INIT") - - return - - def run(self): - """ - Now thread is being asked to start running - """ - if "web_server" in self.app_base.settings: - host_port = int(self.app_base.settings["web_server"].get( - "host_port", DEF_HOST_PORT)) - - host_ip = self.app_base.settings["web_server"].get( - "host_ip", DEF_HOST_IP) - - start_count = int(self.app_base.settings["web_server"].get( - "start_count", DEF_START_COUNT)) - else: - # we create, so WebServerRequestHandler can obtain - self.app_base.settings["web_server"] = dict() - - host_port = DEF_HOST_PORT - self.app_base.settings["web_server"]["host_port"] = host_port - - host_ip = DEF_HOST_IP - self.app_base.settings["web_server"]["host_ip"] = host_ip - - start_count = DEF_START_COUNT - - # push in the start count - self.app_base.put_user_data('counter', start_count) - - # we want on all interfaces - server_address = (host_ip, host_port) - - self.app_base.logger.info("Starting Server:{}".format(server_address)) - - self.app_base.logger.info("Running") - - httpd = HTTPServer(server_address, WebServerRequestHandler) - # set by singleton - pushes in any/all instances - WebServerRequestHandler.APP_BASE = self.app_base - - httpd.serve_forever() - - def please_stop(self): - """ - Now thread is being asked to start running - """ - raise NotImplementedError - - -class WebServerRequestHandler(BaseHTTPRequestHandler): - """ - - """ - - # a singleton to support pass-in of our settings and logger - APP_BASE = None - - START_LINES = '' +\ - 'The Count Is' +\ - '' - - IMAGE_LINES = '' - - END_LINES = '
' - - # images should be 190x380 pixels - IMAGES = { - '0': "sdigit_0.jpg", - '1': "sdigit_1.jpg", - '2': "sdigit_2.jpg", - '3': "sdigit_3.jpg", - '4': "sdigit_4.jpg", - '5': "sdigit_5.jpg", - '6': "sdigit_6.jpg", - '7': "sdigit_7.jpg", - '8': "sdigit_8.jpg", - '9': "sdigit_9.jpg", - '.': "sdigit_dot.jpg", - ' ': "sdigit_blank.jpg", - } - - PATH = "network/digit_web/" - - # def __init__(self): - # BaseHTTPRequestHandler.__init__(self) - # self.path = None - # return - - def do_GET(self): - - if self.path == "/": - self.path = "/counter.html" - - if self.APP_BASE is not None: - self.APP_BASE.logger.debug("Path={}".format(self.path)) - - try: - - mime_type = 'text/html' - send_reply = False - if self.path.endswith(".html"): - - # fetch the current value, which might have changed - count = "%5d" % int(self.APP_BASE.get_user_data('counter')) - - web_message = self.START_LINES - for ch in count: - web_message += self.IMAGE_LINES % self.IMAGES[ch] - web_message += self.END_LINES - web_message = bytes(web_message, "utf-8") - send_reply = True - - elif self.path.endswith(".jpg"): - mime_type = 'image/jpg' - f = open(self.PATH + self.path, 'rb') - web_message = f.read() - send_reply = True - - elif self.path.endswith(".ico"): - mime_type = 'image/x-icon' - f = open(self.PATH + self.path, 'rb') - web_message = f.read() - send_reply = True - - else: - raise IOError - - except IOError: - self.send_error(404, 'File Not Found: %s' % self.path) - return - - if send_reply: - # Send response status code - self.send_response(200) - - # Send headers - self.send_header('Content-type', mime_type) - self.end_headers() - - # Send message back to client - # Write content as utf-8 data - self.wfile.write(web_message) - - return - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/simple_web") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/network/send_email/__init__.py b/network/send_email/__init__.py deleted file mode 100644 index f5783fdd..00000000 --- a/network/send_email/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from network.send_email.send_email import send_one_email - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to send_one_email()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = send_one_email(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise diff --git a/network/send_email/send_email.py b/network/send_email/send_email.py deleted file mode 100644 index 2b306693..00000000 --- a/network/send_email/send_email.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Send a single email -""" -from cp_lib.app_base import CradlepointAppBase -from cp_lib.cp_email import cp_send_email - - -def send_one_email(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - if "send_email" not in app_base.settings: - raise ValueError("settings.ini requires [send_email] section") - - local_settings = dict() - # we default to GMAIL, assume this is for testing - local_settings['smtp_url'] = app_base.settings["send_email"].get( - "smtp_url", 'smtp.gmail.com') - local_settings['smtp_port'] = int(app_base.settings["send_email"].get( - "smtp_port", 587)) - - for value in ('username', 'password', 'email_to', 'email_from'): - if value not in app_base.settings["send_email"]: - raise ValueError( - "settings [send_email] section requires {} data".format(value)) - # assume all are 'strings' - no need for INT - local_settings[value] = app_base.settings["send_email"][value] - - local_settings['subject'] = app_base.settings["send_email"].get( - "subject", "test-Subject") - local_settings['body'] = app_base.settings["send_email"].get( - "body", "test-body") - - app_base.logger.debug("Send Email To:({})".format( - local_settings['email_to'])) - result = cp_send_email(local_settings) - - app_base.logger.debug("result({})".format(result)) - - return result - -# Required keys -# ['smtp_tls] = T/F to use TLS, defaults to True -# ['smtp_url'] = URL, such as 'smtp.gmail.com' -# ['smtp_port'] = TCP port like 587 - be careful, as some servers have more -# than one, with the number defining the security demanded. -# ['username'] = your smtp user name (often your email acct address) -# ['password'] = your smtp acct password -# ['email_to'] = the target email address, as str or list -# ['subject'] = the email subject - -# Optional keys -# ['email_from'] = the from address - any smtp server will ignore, and force -# this to be your user/acct email address; def = ['username'] -# ['body'] = the email body; def = ['subject'] - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/send_email") - - _result = send_one_email(my_app) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - - sys.exit(_result) diff --git a/network/send_email/settings.ini b/network/send_email/settings.ini deleted file mode 100644 index 02ca9a90..00000000 --- a/network/send_email/settings.ini +++ /dev/null @@ -1,16 +0,0 @@ -[application] -name=send_email -description=send an email to GMAIL -path = network/send_email -version=1.8 -uuid=d204d898-c5cf-4f50-aecb-96500a71a7b6 - -[send_email] -username={your name}@gmail.com -password={put your password here} -smtp_url=smtp.gmail.com -smtp_port=587 -email_to={your name}@gmail.com -email_from={your name}@gmail.com -subject=This is a test! -body=Well, it looked good on paper! diff --git a/network/send_ping/__init__.py b/network/send_ping/__init__.py deleted file mode 100644 index 1248e738..00000000 --- a/network/send_ping/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from network.send_ping.send_ping import run_router_app - - -# this name RouterApp is CRITICAL - importlib will seek it! -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise diff --git a/network/send_ping/send_ping.py b/network/send_ping/send_ping.py deleted file mode 100644 index 0c7e83ad..00000000 --- a/network/send_ping/send_ping.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Issue a ping, via Router API control/ping -""" -from cp_lib.app_base import CradlepointAppBase -from cp_lib.cs_ping import cs_ping - - -# this name "run_router_app" is not important, reserved, or demanded -# - but must match below in __main__ and also in __init__.py -def run_router_app(app_base, ping_ip=None): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :param str ping_ip: the IP to ping - :return: - """ - if ping_ip is None: - # then try settings.ini - if "ping" in app_base.settings: - ping_ip = app_base.settings["ping"].get("ping_ip", '') - # else, just assume passed in value is best - - result = cs_ping(app_base, ping_ip) - return result - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/send_ping") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/network/send_ping/settings.ini b/network/send_ping/settings.ini deleted file mode 100644 index 8406d917..00000000 --- a/network/send_ping/settings.ini +++ /dev/null @@ -1,13 +0,0 @@ -[application] -name=send_ping -description=send a ping to an IP -path = network/send_ping -version=1.11 -uuid=db946fe9-fb7e-42b1-8589-123ff698b67e - -[ping] -; this can be IP or DNS name -; ping_ip=www.google.com -; ping_ip=192.168.115.6 -ping_ip=192.168.115.6 -ping_count=4 diff --git a/network/simple_web/README.md b/network/simple_web/README.md deleted file mode 100644 index 43a63230..00000000 --- a/network/simple_web/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# directory: ./network/simple_web -## Router App/SDK sample applications - -A most basic web server, using the standard Python 3 "http.server" module. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: web_server.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [web_server]: - -* host_port=9001, define the listening port, which on Cradlepoint Router -SDK must be greater than 1024 due to permissions. -Also, avoid 8001 or 8080, as router may be using already. - -* host_ip=192.168.0.1, limits the interface used. -Normally, you can omit and server will work on ALL interfaces - -host_ip will == "". -However, you might want to limit to local LAN, which means the interface -your router offers DHCP to local clients. - -* message=Hello from Cradlepoint Router!, put any UTF8 message here diff --git a/network/simple_web/__init__.py b/network/simple_web/__init__.py deleted file mode 100644 index fe957482..00000000 --- a/network/simple_web/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from network.simple_web.web_server import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/network/simple_web/settings.ini b/network/simple_web/settings.ini deleted file mode 100644 index e03a4857..00000000 --- a/network/simple_web/settings.ini +++ /dev/null @@ -1,14 +0,0 @@ -; one app settings - -[application] -name=simple_web -description=Basic Web server, with single fixed page -path = network/simple_web -version = 1.0 -uuid=fbf3522a-83b8-4901-97c9-067772d9993c - -[web_server] -; port cannot be less than 1024 on CP router -host_port=9001 -; host_ip=192.168.1.1 -message=Hello from Cradlepoint Router! diff --git a/network/simple_web/web_server.py b/network/simple_web/web_server.py deleted file mode 100644 index 3c17f067..00000000 --- a/network/simple_web/web_server.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -A basic but complete echo server -""" - -from http.server import BaseHTTPRequestHandler, HTTPServer - -from cp_lib.app_base import CradlepointAppBase - -# avoid 8080, as the router may have servcie on it. -DEF_HOST_PORT = 9001 -DEF_HOST_IP = "" -DEF_WEB_MESSAGE = "Hello World" - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - if "web_server" in app_base.settings: - host_port = int(app_base.settings["web_server"].get( - "host_port", DEF_HOST_PORT)) - - host_ip = app_base.settings["web_server"].get( - "host_ip", DEF_HOST_IP) - - web_message = app_base.settings["web_server"].get( - "message", DEF_WEB_MESSAGE) - else: - # we create, so WebServerRequestHandler can obtain - app_base.settings["web_server"] = dict() - - host_port = DEF_HOST_PORT - app_base.settings["web_server"]["host_port"] = host_port - - host_ip = DEF_HOST_IP - app_base.settings["web_server"]["host_ip"] = host_ip - - web_message = DEF_WEB_MESSAGE - app_base.settings["web_server"]["message"] = web_message - - # we want on all interfaces - server_address = (host_ip, host_port) - - app_base.logger.info("Starting Server:{}".format(server_address)) - app_base.logger.info("Web Message is:{}".format(web_message)) - - httpd = HTTPServer(server_address, WebServerRequestHandler) - # set by singleton - pushes in any/all instances - WebServerRequestHandler.APP_BASE = app_base - - try: - httpd.serve_forever() - - except KeyboardInterrupt: - app_base.logger.info("Stopping Server, Key Board interrupt") - - return 0 - - -class WebServerRequestHandler(BaseHTTPRequestHandler): - """ - - """ - - # a singleton to support pass-in of our settings and logger - APP_BASE = None - - def do_GET(self): - - if self.APP_BASE is not None: - self.APP_BASE.logger.info("Request from :{}".format( - self.address_string())) - web_message = self.APP_BASE.settings["web_server"]["message"] - - else: - web_message = DEF_WEB_MESSAGE - - # Send response status code - self.send_response(200) - - # Send headers - self.send_header('Content-type', 'text/html') - self.end_headers() - - # Send message back to client - # Write content as utf-8 data - self.wfile.write(bytes(web_message, "utf8")) - return - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/simple_web") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/network/tcp_echo/__init__.py b/network/tcp_echo/__init__.py deleted file mode 100644 index 603cea04..00000000 --- a/network/tcp_echo/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from network.tcp_echo.tcp_echo import tcp_echo_server - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - - self.logger.debug("__init__ chaining to tcp_echo_server()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = tcp_echo_server(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/network/tcp_echo/settings.ini b/network/tcp_echo/settings.ini deleted file mode 100644 index d6a1872f..00000000 --- a/network/tcp_echo/settings.ini +++ /dev/null @@ -1,11 +0,0 @@ -; one app settings - -[application] -name=tcp_echo -description=Basic TCP echo server and client -path = network/tcp_echo -uuid = 7042c8fd-fe7a-4846-aed1-e3f8d6a1c92a -version = 1.10 - -[tcp_echo] -host_port = 9999 diff --git a/network/tcp_echo/tcp_echo.py b/network/tcp_echo/tcp_echo.py deleted file mode 100644 index 587ca75f..00000000 --- a/network/tcp_echo/tcp_echo.py +++ /dev/null @@ -1,279 +0,0 @@ -""" -A basic but complete echo server -""" - -import socket -import time -import gc - -from cp_lib.app_base import CradlepointAppBase - -""" -Notes for tcp_echo_server and tcp_echo_client - -Anytime your code access an external resource or even internet-based server, -be prepared for the unexpected! - -Common socket error reasons are shown here: - https://www.python.org/dev/peps/pep-3151/#common-errnos-with-socket-error - -BIND ERROR note: - Your code is attempting to obtain and lock an exclusive resource. That - attempt might fail for various reasons - the most common one is that - another program/task ALREADY has locked that resource. In fact, it might - even be a second copy of your own program running! While this sample - merely exits, realistic server code could delay (use time.sleep()) for a - few minutes, then try again. Still, feed back to users is critical, - because if nothing changes ... nothing CHANGES. - - A second common error will be that Linux (such as used by Cradlepoint - routers) does not allow non-privileged programs to open server sockets - on ports lower than 1024. This script simulates this on Windows by - manually raising an exception. - TODO - what does Linux do? - - Example trace on a Win32 computer, when bind() fails due to resource - ALREADY being used/locked. - tcp_echo - INFO - Preparing to wait on TCP socket 9999 - tcp_echo - ERROR - socket.bind() failed - [WinError 10048] Only one - usage of each socket address (protocol/network - address/port) is normally permitted - tcp_echo - ERROR - Exiting, status code is -1 - -BYTES, STR, UTF8 note: - When looking on the internet for answers or code samples, be careful - to seek Python 3 examples! A big change in Python 3, is that all strings - now support UNICODE, which defaults to UTF8. UTF8 allows strings to - include foreign language symbols, such as accent marks in European - languages, or even direct Chinese (like å, δ, or 語). - - See https://docs.python.org/3/howto/unicode.html - - But the key importance for Cradlepoint router development, is that both - the SOCKET and the PYSERIAL modules recv/send bytes() objects, not str() - objects! So for example, the line "sock.send('Hello, world')" will throw - an exception because the SOCKET module works with "bytes" objects, not - "str" objects. The trivial solution for string constants - is to define them as b'Hello, world', not 'Hello, world'! - - To convert types of existing variable data: - new_bytes_thing = string_thing.encode() # def x.encode(encoding='utf-8') - new_string_thing = bytes_thing.decode() # def x.decode(encoding='utf-8') - -CLOSE ERROR note: - It is best-practice to always CLOSE your socket when you know it is - "going away". In this sample, this step is not technically required, - because when Python exits the routine and variable 'sock' goes out of - scope, Python will close the socket. However, by explicitly closing it - yourself, you are reminding viewers that the socket is being discarded. - An empty try/except OSError/pass wrapper ignore error conditions where - the socket.close() would fail due to other error situations - -CONNECT ERROR note: - Your code is attempting to link to a remote resource, which may not be - accessible, or not accepting connections. While this sample merely exits, - realistic client code could delay (use time.sleep()) for a few minutes, - then try again. Still, feed back to users - is critical, because if nothing changes ... nothing CHANGES. - - Example trace on a Win32 computer, when connect() fails due to resource - ALREADY being used. - tcp_echo - INFO - Preparing to connect on TCP socket 9999 - tcp_echo - ERROR - socket.connect() failed - [WinError 10061] No - connection could be made because the target - machine actively refused it - tcp_echo - ERROR - Exiting, status code is -1 - -KEEP ALIVE note: - For historical reasons, TCP sockets default to not use keepalive, which - is a means to detect when an idle peer "has gone away". On cellular, this - is pretty much guaranteed to happen at least once a day - if not after - 5 minutes of being idle! Without TCP keepalive, a server socket can be - idle, and the memory resources used held FOREVER (as in years and years, - or until the Cradlepoint router reboots!) - - Use this line: sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - While many online samples show ways to define the timing behavior, - however this is very OS dependant, plus on many smaller OS product, the - TCP KeepAlive settings are global, so changing in one applications - changes the timing for ALL applications. A better solution is to confirm - that the default/common TCP Keepalive settings (probe count and time - delays) are set to reasonable values for cellular. - -MEMORY note: - As Python variables are created and deleted, Python manages a list of - "garbage", which is unused data that has not yet been freed. Python does - this, because proving data is unused takes time - for example, if I - delete a string variable of the value "Hello World", that actual data - MIGHT be shared with multiple variables. Therefore Python caches the - object as "possibly free", and using various estimates, Python - periodically batch processes the "possibly free" collection list. - - While in general you should allow Python to do its job, when you have - an object which is known to be large AND no longer used, manually - running memory cleanup is safest on a small embedded system. Situations - to consider manual garbage collection are: - 1) when a client socket closes, which is especially critical if the - client might repeatedly reconnect. For example, each socket (with - buffers) could contain up to 1/4 MB of memory - 2) when a child thread exists, as the thread also could consume a large - collection of dead memory objects - 3) after a large imported object, as as when an XML file has been parsed - into memory. For example, an XML text file of 10K may consume over - 1MB of RAM after being parsed into memory. - -""" - - -def tcp_echo_server(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - # logger.debug("Settings({})".format(sets)) - - host_ip = 'localhost' - host_port = 9999 - buffer_size = 1024 - - if "tcp_echo" in app_base.settings: - host_ip = app_base.settings["tcp_echo"].get("host_ip", '') - host_port = int(app_base.settings["tcp_echo"].get("host_port", 9999)) - buffer_size = int(app_base.settings["tcp_echo"].get("buffer_size", - 1024)) - - while True: - # define the socket resource, including the type (stream == "TCP") - app_base.logger.info("Preparing TCP socket %d" % host_port) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # attempt to actually lock resource, which may fail if unavailable - # (see BIND ERROR note) - try: - sock.bind((host_ip, host_port)) - except OSError as msg: - app_base.logger.error("socket.bind() failed - {}".format(msg)) - - # technically, Python will close when 'sock' goes out of scope, - # but be disciplined and close it yourself. Python may warning - # you of unclosed resource, during runtime. - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors - # are likely permanent. - return -1 - - # only allow 1 client at a time - sock.listen(1) - - while True: - # loop forever - app_base.logger.info("Waiting on TCP socket %d" % host_port) - client, address = sock.accept() - app_base.logger.info("Accepted connection from {}".format(address)) - - # for cellular, ALWAYS enable TCP Keep Alive (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # set non-blocking so we can do a manual timeout (use of select() - # is better ... but that's another sample) - # client.setblocking(0) - - while True: - app_base.logger.debug("Waiting to receive data") - data = client.recv(buffer_size) - # data is type() bytes, to echo we don't need to convert - # to str to format or return. - app_base.logger.debug("See data({})".format(data)) - if data: - client.send(data) - else: - break - - time.sleep(1.0) - - app_base.logger.info("Client disconnected") - client.close() - - # since this server is expected to run on a small embedded system, - # free up memory ASAP (see MEMORY note) - del client - gc.collect() - - return 0 - - -def tcp_echo_client(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - - host_ip = 'localhost' - host_port = 9999 - buffer_size = 1024 - - if "tcp_echo" in app_base.settings: - host_ip = app_base.settings["tcp_echo"].get("host_ip", 'localhost') - host_port = int(app_base.settings["tcp_echo"].get("host_port", 9999)) - buffer_size = int(app_base.settings["tcp_echo"].get("buffer_size", - 1024)) - - # allocate the socket resource, including the type (stream == "TCP") - app_base.logger.info("Preparing to connect on TCP socket %d" % host_port) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - # for cellular, ALWAYS enable TCP Keep Alive - (see KEEP ALIVE note) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # attempt to actually lock the resource, which may fail if unavailable - # (see CONNECT ERROR note) - try: - sock.connect((host_ip, host_port)) - except OSError as msg: - app_base.logger.error("socket.connect() failed - {}".format(msg)) - - # Python will close when variable 'sock' goes out of scope - # (see CLOSE ERROR note) - try: - sock.close() - except OSError: - pass - - # we exit, because if we cannot secure the resource, the errors are - # likely permanent. - return -1 - - # note: sock.send('Hello, world') will fail, because Python 3 socket() - # handles BYTES (see BYTES and STR note) - data = b'Hello, world' - app_base.logger.debug("Request({})".format(data)) - sock.send(data) - data = sock.recv(buffer_size) - app_base.logger.debug("Response({})".format(data)) - sock.close() - - time.sleep(1.0) - - return 0 - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("network/tcp_echo") - - if len(sys.argv) == 1: - # if no cmdline args, we assume SERVER - _result = tcp_echo_server(my_app) - else: - _result = tcp_echo_client(my_app) - - my_app.logger.info("Exiting, status code is {}".format(_result)) - - sys.exit(_result) diff --git a/ping/cs.py b/ping/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/ping/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/ping/install.sh b/ping/install.sh new file mode 100644 index 00000000..f18f009b --- /dev/null +++ b/ping/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION ping on:" >> install.log +date >> install.log diff --git a/ping/package.ini b/ping/package.ini new file mode 100644 index 00000000..a0f5b1bb --- /dev/null +++ b/ping/package.ini @@ -0,0 +1,11 @@ +[ping] +uuid=dd91c8ea-cd95-4d9d-b08b-cf62de19684f +vendor=Cradlepoint +notes=Router Ping Reference Application +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/ping/ping.py b/ping/ping.py new file mode 100644 index 00000000..ccfb7a6e --- /dev/null +++ b/ping/ping.py @@ -0,0 +1,65 @@ +''' +This application will ping an address and log the results +''' +import sys +import argparse +import time +import json +import cs + +APP_NAME = 'ping' + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + ping_data = { + 'host': 'www.google.com', # Can also be an IP address + 'size': 64 + } + + result = cs.CSClient().put('/control/ping/start', ping_data) + cs.CSClient().log(APP_NAME, 'Start ping: {}'.format(result)) + + done = False + ping_results = [] + while not done: + time.sleep(1) + ping_data = json.loads(cs.CSClient().get('/control/ping')) + # Need to collect the results as it is cleared when read. + result = ping_data['result'] + + if result != '': + lines = result.split('\n') + ping_results.extend(lines) + + status = ping_data['status'] + + if status == 'done' or status == 'error': + done = True + + # Now that the ping is done, log the results + for line in ping_results: + cs.CSClient().log(APP_NAME, 'Ping Results: {}'.format(line)) + + elif command == 'stop': + # Nothing on stop + pass + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/ping/start.sh b/ping/start.sh new file mode 100644 index 00000000..b23a9676 --- /dev/null +++ b/ping/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ping.py start diff --git a/ping/stop.sh b/ping/stop.sh new file mode 100644 index 00000000..17061e02 --- /dev/null +++ b/ping/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython ping.py stop diff --git a/power_gpio/cs.py b/power_gpio/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/power_gpio/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/power_gpio/install.sh b/power_gpio/install.sh new file mode 100644 index 00000000..891acbf8 --- /dev/null +++ b/power_gpio/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION power_gpio on:" >> install.log +date >> install.log diff --git a/power_gpio/package.ini b/power_gpio/package.ini new file mode 100644 index 00000000..c3ea9490 --- /dev/null +++ b/power_gpio/package.ini @@ -0,0 +1,11 @@ +[power_gpio] +uuid=79be164a-aa55-40aa-9ab3-13ffa889ca01 +vendor=Cradlebox +notes=Power Connector GPIO Reference Application +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/power_gpio/power_gpio.py b/power_gpio/power_gpio.py new file mode 100644 index 00000000..8bdd2655 --- /dev/null +++ b/power_gpio/power_gpio.py @@ -0,0 +1,66 @@ +""" +Query the 2x2 power connector GPIO +""" +import json +import argparse +import cs + +APP_NAME = "power_gpio" + + +def run_router_app(): + """ + Read the Power Connector GPIO input and output + """ + + # confirm we are running on 900/950 or 1100/1150 + result = json.loads(cs.CSClient().get("status/product_info/product_name")) + if "IBR900" in result or "IBR950" in result: + input_name = "status/gpio/CONNECTOR_INPUT" + output_name = "status/gpio/CONNECTOR_OUTPUT" + + elif "IBR1100" in result or "IBR1150" in result: + input_name = "status/gpio/CGPIO_CONNECTOR_INPUT" + output_name = "status/gpio/CGPIO_CONNECTOR_OUTPUT" + + else: + cs.CSClient().log(APP_NAME, "Inappropriate Product:{} - aborting.".format(result)) + return + + result_in = json.loads(cs.CSClient().get(input_name)) + result_out = json.loads(cs.CSClient().get(output_name)) + + cs.CSClient().log(APP_NAME, "Product Model is: {}".format(result)) + cs.CSClient().log(APP_NAME, "GPIO 2x2: {} = {}".format(input_name, result_in)) + cs.CSClient().log(APP_NAME, "GPIO 2x2: {} = {}".format(output_name, result_out)) + + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + run_router_app() + + elif command == 'stop': + # Nothing on stop + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/power_gpio/start.sh b/power_gpio/start.sh new file mode 100644 index 00000000..592d5264 --- /dev/null +++ b/power_gpio/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython power_gpio.py start diff --git a/power_gpio/stop.sh b/power_gpio/stop.sh new file mode 100644 index 00000000..f6c692d5 --- /dev/null +++ b/power_gpio/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython power_gpio.py stop diff --git a/py33.bat b/py33.bat deleted file mode 100644 index 8e134637..00000000 --- a/py33.bat +++ /dev/null @@ -1,5 +0,0 @@ -set PATH=%PATH%;C:\Python33 - -d: -cd \root -set PYTHONPATH=D:\root; diff --git a/sdk_settings.ini b/sdk_settings.ini new file mode 100644 index 00000000..872f4be5 --- /dev/null +++ b/sdk_settings.ini @@ -0,0 +1,5 @@ +[sdk] +app_name=hello_world +dev_client_ip=192.168.0.1 +dev_client_username=admin +dev_client_password=44224267 diff --git a/send_alert/cs.py b/send_alert/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/send_alert/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/send_alert/install.sh b/send_alert/install.sh new file mode 100644 index 00000000..0a1296e0 --- /dev/null +++ b/send_alert/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION send_alert on:" >> install.log +date >> install.log diff --git a/send_alert/package.ini b/send_alert/package.ini new file mode 100644 index 00000000..fc13b213 --- /dev/null +++ b/send_alert/package.ini @@ -0,0 +1,11 @@ +[send_alert] +uuid=9126bf4f-b85f-459f-bbae-439c8ff1e838 +vendor=Cradlebox +notes=Router Send ECM Alert Reference Application +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/send_alert/send_alert.py b/send_alert/send_alert.py new file mode 100644 index 00000000..f45d1754 --- /dev/null +++ b/send_alert/send_alert.py @@ -0,0 +1,46 @@ +''' +This application will send an alert to the ECM when the +application is started and stopped. The alert must be +setup in the ECM. This is a Router Apps, Custom Alert. + +The CSClient function to send an alert is: + + cs.CSClient().alert(name, value) + :param str name - The 'Alert:' field for the alert in ECM. + :param str value - The 'Message:' field for the alert in ECM. +''' + +import sys +import argparse +import cs + +APP_NAME = 'send_alert' + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + cs.CSClient().log(APP_NAME, 'Sent ECM alert that app was started.') + cs.CSClient().alert(APP_NAME, 'Application has been started') + + elif command == 'stop': + cs.CSClient().log(APP_NAME, 'Sent ECM alert that app was stopped.') + cs.CSClient().alert(APP_NAME, 'Application has been stopped') + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exception: {}'.format(APP_NAME, command, e)) + raise + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/send_alert/start.sh b/send_alert/start.sh new file mode 100644 index 00000000..6b234d85 --- /dev/null +++ b/send_alert/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython send_alert.py start diff --git a/send_alert/stop.sh b/send_alert/stop.sh new file mode 100644 index 00000000..0b57469a --- /dev/null +++ b/send_alert/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython send_alert.py stop diff --git a/send_to_server/cs.py b/send_to_server/cs.py new file mode 100644 index 00000000..b83a98d0 --- /dev/null +++ b/send_to_server/cs.py @@ -0,0 +1,257 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text)['data'] + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + if sys.platform == 'linux2': + value = json.dumps(value).replace(' ', '') + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + print('Append is only available when running the app in the router.') + raise NotImplementedError + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/send_to_server/install.sh b/send_to_server/install.sh new file mode 100644 index 00000000..9015d86d --- /dev/null +++ b/send_to_server/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION send_to_server on:" >> install.log +date >> install.log diff --git a/send_to_server/package.ini b/send_to_server/package.ini new file mode 100644 index 00000000..275957f9 --- /dev/null +++ b/send_to_server/package.ini @@ -0,0 +1,11 @@ +[send_to_server] +uuid=82583a58-108e-4ac7-80e2-b0bba753330e +vendor=Cradlebox +notes=Sends data to a server via http post. +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/send_to_server/send_to_server.py b/send_to_server/send_to_server.py new file mode 100644 index 00000000..3ab4081d --- /dev/null +++ b/send_to_server/send_to_server.py @@ -0,0 +1,89 @@ +''' +Gets the '/status' from the reouter config store and send it +to a test server. +''' + +import sys +import argparse +import datetime +import urllib.request +import urllib.parse +import json +import time +import cs + +APP_NAME = 'send_to_server' + + +def post_to_server(): + try: + # The tree item to get from the router config store + tree_item = '/status' + cs.CSClient().log(APP_NAME, "get {}".format(tree_item)) + start_time = datetime.datetime.now() + + # Get the item from the router config store + tree_data = cs.CSClient().get(tree_item) + cs.CSClient().log(APP_NAME, "{}: {}".format(tree_item, tree_data)) + + time_to_get = datetime.datetime.now() - start_time + encode_start_time = datetime.datetime.now() + + # Convert the tree_data string to a json object (i.e. dictionary) + tree_data = json.loads(tree_data) + + # URL encode the tree_data + params = urllib.parse.urlencode(tree_data) + + # UTF-8 encode the URL encoded data + params = params.encode('utf-8') + + time_to_encode = datetime.datetime.now() - encode_start_time + send_to_server_start_time = datetime.datetime.now() + + # Send a post request to a test server. It will respond with the data sent + # in the request + response = urllib.request.urlopen("http://httpbin.org/post", params) + end_time = datetime.datetime.now() + + # Log the response code and the processing timing information. + cs.CSClient().log(APP_NAME, "data sent, http response code: {}".format(response.code)) + cs.CSClient().log(APP_NAME, 'Time to get data from router config store: {}'.format(time_to_get)) + cs.CSClient().log(APP_NAME, 'Time to urlencode data: {}'.format(time_to_encode)) + cs.CSClient().log(APP_NAME, 'Time to get reply from server: {}'.format(end_time - send_to_server_start_time)) + cs.CSClient().log(APP_NAME, 'Time to get and send data in post request: {}'.format(end_time - start_time)) + + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Something went wrong! exceptions: {}'.format(e)) + raise + + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + post_to_server() + + elif command == 'stop': + # Do nothing + pass + except: + e = sys.exc_info()[0] + cs.CSClient().log(APP_NAME, 'Problem with {} on {}! exceptions: {}'.format(APP_NAME, command, e)) + raise + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/send_to_server/start.sh b/send_to_server/start.sh new file mode 100644 index 00000000..0b6ac4ea --- /dev/null +++ b/send_to_server/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython send_to_server.py start diff --git a/send_to_server/stop.sh b/send_to_server/stop.sh new file mode 100644 index 00000000..17e4bed0 --- /dev/null +++ b/send_to_server/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython send_to_server.py stop diff --git a/serial_echo/cs.py b/serial_echo/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/serial_echo/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/serial_echo/install.sh b/serial_echo/install.sh new file mode 100644 index 00000000..40f3f9c6 --- /dev/null +++ b/serial_echo/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION serial_echo on:" >> install.log +date >> install.log diff --git a/serial_echo/package.ini b/serial_echo/package.ini new file mode 100644 index 00000000..3f405239 --- /dev/null +++ b/serial_echo/package.ini @@ -0,0 +1,11 @@ +[serial_echo] +uuid=98793e88-a2a4-40d6-9c1c-6845d79d3f1b +vendor=Cradlepoint +notes=Simple echo server, which echos any lines received. +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/serial_echo/serial_echo.py b/serial_echo/serial_echo.py new file mode 100644 index 00000000..b70baeb5 --- /dev/null +++ b/serial_echo/serial_echo.py @@ -0,0 +1,138 @@ +""" +Serial echo + +Wait for data to enter the serial port, then echo back out. +Data will be processed byte by byte, although you could edit the +sample code to wait for an end-of-line character. + +""" + +import json +import serial +import argparse +import cs + +APP_NAME = 'serial_echo' + +# set to None to disable, or set to bytes to send +ASK_ARE_YOU_THERE = b"u there?" + + +def run_router_app(): + # confirm we are running on an 1100/1150, 900/950, or 600/650. result should be like "IBR1100LPE" + result = json.loads(cs.CSClient().get("status/product_info/product_name")) + if "IBR1100" in result or "IBR1150" in result or \ + "IBR900" in result or "IBR950" in result or \ + "IBR600" in result or "IBR650" in result: + cs.CSClient().log(APP_NAME, "Product Model is good:{}".format(result)) + else: + cs.CSClient().log(APP_NAME, + "Inappropriate Product:{} - aborting.".format(result)) + return -1 + + port_name = 9600 + baud_rate = '/dev/ttyS1' + + # see if port is a digit? + if port_name[0].isdecimal(): + port_name = int(port_name) + + old_pyserial = serial.VERSION.startswith("2.6") + + message = "Starting serial echo on {0}, baud={1}".format(port_name, + baud_rate) + cs.CSClient().log(APP_NAME, message) + + try: + ser = serial.Serial(port_name, baudrate=baud_rate, bytesize=8, + parity='N', stopbits=1, timeout=1, + xonxoff=0, rtscts=0) + + except serial.SerialException: + cs.CSClient().log(APP_NAME, "ERROR: Open failed!") + raise + + # start as None to force at least 1 change + dsr_was = None + cts_was = None + + try: + while True: + try: + data = ser.read(size=1) + except KeyboardInterrupt: + cs.CSClient().log(APP_NAME, + "WARNING: Keyboard Interrupt - asked to quit") + break + + if len(data): + cs.CSClient().log(APP_NAME, str(data)) + ser.write(data) + elif ASK_ARE_YOU_THERE is not None: + ser.write(ASK_ARE_YOU_THERE) + # else: + # app_base.logger.debug(b".") + + if old_pyserial: + # as of May-2016/FW 6.1, this is PySerial v2.6, so it uses + # the older style control signal access + if dsr_was != ser.getDSR(): + # do this 'get' twice to handle first pass as None + dsr_was = ser.getDSR() + cs.CSClient().log(APP_NAME, + "DSR changed to {}, setting DTR".format(dsr_was)) + ser.setDTR(dsr_was) + + if cts_was != ser.getCTS(): + cts_was = ser.getCTS() + cs.CSClient().log(APP_NAME, + "CTS changed to {}, setting RTS".format(cts_was)) + ser.setRTS(cts_was) + else: + if dsr_was != ser.dsr: + dsr_was = ser.dsr + cs.CSClient().log(APP_NAME, + "DSR changed to {}, setting DTR".format(dsr_was)) + ser.dtr = dsr_was + + if cts_was != ser.cts: + cts_was = ser.cts + cs.CSClient().log(APP_NAME, + "CTS changed to {}, setting RTS".format(cts_was)) + ser.rts = cts_was + + # if you lose the serial port - like disconnected, then + # ser.getDSR() will throw OSError #5 Input/Output error + + finally: + ser.close() + + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + run_router_app() + + elif command == 'stop': + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/serial_echo/start.sh b/serial_echo/start.sh new file mode 100644 index 00000000..634bc7cb --- /dev/null +++ b/serial_echo/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython serial_echo.py start diff --git a/serial_echo/stop.sh b/serial_echo/stop.sh new file mode 100644 index 00000000..97162200 --- /dev/null +++ b/serial_echo/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython serial_echo.py stop diff --git a/serial_port/README.md b/serial_port/README.md deleted file mode 100644 index bfd70044..00000000 --- a/serial_port/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Router App/SDK sample applications - SERIAL PORT samples - -## Directory list_ports - -Shows how to use your own MAIN.PY, which is run as the main app of the SDK. -The code loops, sending a "Hello World" message to a Syslog server. - -Because you control the full application, you must manage the Syslog output -and settings yourself. - -## Directory modbus_poll - -Expands the function in "hello_world" sample, using the stock -CradlepointAppBase class, which adds the following services: -* self.logger = a Syslog logging module -* self.settings = finds/loads your settings.json (made from settings.ini) -* self.cs_client = read/write the Cradlepoint status/config trees - -## Directory modbus_simple_bridge - -Expands the function in "hello_world_app" sample, adding a sub-task -to do the "Hello World" Syslog message. -Shows how to spawn, as well as stop (kill/join) the sub-task. - -## Directory serial_echo - -Shows basic serial port read/write. -Opens a serial port, as defined in settings.ini -(can be built-in port on IBR11X0 or USB-Serial) -then echos any bytes seen. - diff --git a/serial_port/list_ports/README.md b/serial_port/list_ports/README.md deleted file mode 100644 index ed0e2a99..00000000 --- a/serial_port/list_ports/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# directory: ./serial_port/list_ports -## Router App/SDK sample applications - -This sample code runs through one check, then exits. -Assuming you have 'restart' true in your settings.ini file, then -the code restarts forever. - -Based on the model of router you have, it checks to see if the ports below -exist, can be opened, and the name of the port written to it. - -Real Physical ports: - -* /dev/ttyS1 (only on models such as IBR1100) -* /dev/ttyS2 (normally will fail/not exist) - -USB serial ports: - -* /dev/ttyUSB0 -* /dev/ttyUSB1 -* /dev/ttyUSB2 -* /dev/ttyUSB3 -* /dev/ttyUSB4 - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: list_ports.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [list_ports]: - -* probe_physical = True, set False to NOT probe real physical serial ports. -On models without physical ports, this setting is ignored. - -* probe_usb = True, set False to NOT probe for USB serial ports. - -* write_name = True, set False to NOT send out the port name, which is -sent to help you identify between multiple ports. - -## Testing USB-serial devices - -Most USB-serial devices with an FTDI-chipset can be used. Some specific -products known to work are shown here: - -* -* -* diff --git a/serial_port/list_ports/__init__.py b/serial_port/list_ports/__init__.py deleted file mode 100644 index a8a8df3a..00000000 --- a/serial_port/list_ports/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from serial_port.list_ports.list_ports import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/serial_port/list_ports/list_ports.py b/serial_port/list_ports/list_ports.py deleted file mode 100644 index 3a9bba03..00000000 --- a/serial_port/list_ports/list_ports.py +++ /dev/null @@ -1,177 +0,0 @@ -import serial -import sys - -import cp_lib.hw_status -from cp_lib.app_base import CradlepointAppBase -from cp_lib.parse_data import parse_boolean - -DEF_PROBE_PHYSICAL = True -DEF_PROBE_USB = True -DEF_WRITE_NAME = False -# this exists on most, but is for internal use. -IGNORE_TTYS0 = True - -PORT_LIST_PHYSICAL = (1, 2) -PORT_LIST_USB = (0, 1, 2, 3, 4) - - -def run_router_app(app_base): - """ - Do the probe/check of - - :param CradlepointAppBase app_base: the prepared resources: logger, - cs_client, settings, etc - :return: - """ - - # as of Mat-2016/FW=6.1, PySerial is version 2.6 (2.6-pre1) - app_base.logger.info("serial.VERSION = {}.".format(serial.VERSION)) - - probe_physical = DEF_PROBE_PHYSICAL - probe_usb = DEF_PROBE_USB - write_name = DEF_WRITE_NAME - - app_key = "list_ports" - if app_key in app_base.settings: - if "probe_physical" in app_base.settings[app_key]: - probe_physical = parse_boolean( - app_base.settings[app_key]["probe_physical"]) - if "probe_usb" in app_base.settings[app_key]: - probe_usb = parse_boolean( - app_base.settings[app_key]["probe_usb"]) - if "write_name" in app_base.settings[app_key]: - write_name = parse_boolean( - app_base.settings[app_key]["write_name"]) - - if cp_lib.hw_status.am_running_on_router(): - # then we are running on Cradlepoint Router: - - # probe_directory(app_base, "/dev") - port_list = [] - - # confirm we are running on an 1100/1150, result should be "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - name = "/dev/ttyS1" - app_base.logger.info( - "Product Model {} has 1 builtin port:{}".format( - result, name)) - port_list.append(name) - - elif result in ("IBR300", "IBR350"): - app_base.logger.warning( - "Product Model {} has no serial support".format(result)) - - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - if probe_physical: - # fixed ports - 1100 only? - if not IGNORE_TTYS0: - # only check S0 if 'requested' to, else ignore - name = "/dev/ttyS0" - if name not in port_list: - port_list.append(name) - - for port in PORT_LIST_PHYSICAL: - name = "/dev/ttyS%d" % port - if name not in port_list: - port_list.append(name) - - if probe_usb: - # try first 5 USB - for port in PORT_LIST_USB: - name = "/dev/ttyUSB%d" % port - if name not in port_list: - port_list.append(name) - - # cycle through and probe the desired ports - for name in port_list: - probe_serial(app_base, name, write_name) - - elif sys.platform == "win32": - # then handle Windows - - for index in range(1, 17): - name = "COM%d" % index - try: - ser = serial.Serial(name) - ser.close() - app_base.logger.info("Port({}) exists.".format(name)) - - except serial.SerialException: - app_base.logger.info("Port({}) didn't exist.".format(name)) - - else: - raise NotImplementedError( - "This sample only runs on CP Router or Windows") - - return 0 - - -def probe_serial(app_base, port_name, write_name=False): - """ - dump a directory in router FW - - :param CradlepointAppBase app_base: resources: logger, settings, etc - :param str port_name: the port name - :param bool write_name: if T, write out the name - """ - try: - ser = serial.Serial(port_name, dsrdtr=False, rtscts=False) - if write_name: - port_name += '\r\n' - ser.write(port_name.encode()) - app_base.logger.info("Port({}) exists.".format(port_name)) - - # as of Mat-2016/FW=6.1, PySerial is version 2.6 - # therefore .getDSR() works and .dsr does not! - try: - app_base.logger.info(" serial.dsr = {}.".format(ser.dsr)) - except AttributeError: - app_base.logger.info(" serial.dsr is not supported!") - - try: - app_base.logger.info(" serial.getDSR() = {}.".format(ser.getDSR())) - except AttributeError: - app_base.logger.info(" serial.getDSR() is not supported!") - - ser.close() - return True - - except (serial.SerialException, FileNotFoundError): - app_base.logger.info("Port({}) didn't exist.".format(port_name)) - return False - - -def probe_directory(app_base, base_dir): - """ - dump a directory in router FW - - :param CradlepointAppBase app_base: resources: logger, settings, etc - :param str base_dir: the directory to dump - """ - import os - - app_base.logger.debug("Dump Directory:{}".format(base_dir)) - result = os.access(base_dir, os.R_OK) - if result: - app_base.logger.debug("GOOD name:{}".format(base_dir)) - else: - app_base.logger.debug("BAD name:{}".format(base_dir)) - - if result: - result = os.listdir(base_dir) - for name in result: - app_base.logger.debug(" file:{}".format(name)) - return - -if __name__ == "__main__": - - my_app = CradlepointAppBase("serial_port/list_ports") - - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/serial_port/list_ports/settings.ini b/serial_port/list_ports/settings.ini deleted file mode 100644 index 49105509..00000000 --- a/serial_port/list_ports/settings.ini +++ /dev/null @@ -1,15 +0,0 @@ -; one app settings - -[application] -name=list_ports -description=Dump the Serial ports -path=serial_port.list_ports -restart=true -reboot=false -uuid=0268197b-1713-42d4-955f-25f04708ac21 -version=1.1 - -[list_ports] -probe_physical = True -probe_usb = True -write_name = False diff --git a/serial_port/modbus_poll/README.md b/serial_port/modbus_poll/README.md deleted file mode 100644 index 33f2fd6f..00000000 --- a/serial_port/modbus_poll/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# directory: ./serial_port/modbus_poll -## Router App/SDK sample applications - -Poll a single range of Modbus registers from an attached serial -Modbus/RTU PLC or slave device. The poll is repeated, in a loop. -The only output you'll see if via Syslog. - -If you need such a device, do an internet search for "modbus simulator', as -there are many available for free or low (shareware) cost. These run on a -computer, serving up data by direct or USB serial port. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: modbus_poll.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [modbus]: - -* port_name=???, define the serial port to use. Commonly this will be -/dev/ttyS1 or /dev/ttyUSB0 - -* baud_rate=9600, allows you to define a different baud rate. This sample -assumes the other settings are fixed at: bytesize=8, parity='N', stopbits=1, -and all flow control (XonXOff and HW) is off/disabled. -Edit the code if you need to change this. - -* register_start=0, the raw Modbus offset, so '0' and NOT 40001. -Permitted range is 0 to 65535 - -* register_count=4, the number of Holding Register to read. -The request function is fixed to 3, so read multiple holding registers. -The permitted count is 1 to 125 registers (16-bit words) - -* slave_address=1, the Modbus slave address, which must be in the range -from 1 to 255. Since Modbus/RTU is a multi-drop line, the slave -address is used to select 1 of many slaves. -For example, if a device is assigned the address 7, it will ignore all -requests with slave addresses other than 7. - -* poll_delay=15 sec, how often to repoll the device. A lone number (like 60) - is interpreted as seconds. However, it uses the CP library module - "parse_duration", so time tags such as 'sec', 'min, 'hr' can be used. diff --git a/serial_port/modbus_poll/__init__.py b/serial_port/modbus_poll/__init__.py deleted file mode 100644 index 6194f17f..00000000 --- a/serial_port/modbus_poll/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from serial_port.modbus_poll.modbus_poll import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "serial.serial_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to run_main()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/serial_port/modbus_poll/modbus_poll.py b/serial_port/modbus_poll/modbus_poll.py deleted file mode 100644 index 4b05c997..00000000 --- a/serial_port/modbus_poll/modbus_poll.py +++ /dev/null @@ -1,137 +0,0 @@ -""" -Serial echo -""" -import sys -import serial -import time - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.parse_duration import TimeDuration -from serial_port.modbus_poll.crc16 import calc_string - -DEF_BAUD_RATE = 9600 -DEF_PORT_NAME = "/dev/ttyS1" -DEF_REG_START = 0 -DEF_REG_COUNT = 1 -DEF_SLAVE_ADDRESS = 1 -DEF_POLL_DELAY = 5 - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return list: - """ - # confirm we are running on an 1100/1150, result should be "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150"): - app_base.logger.info("Product Model is good:{}".format(result)) - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - period = TimeDuration(DEF_POLL_DELAY) - - port_name = DEF_PORT_NAME - baud_rate = DEF_BAUD_RATE - register_start = DEF_REG_START - register_count = DEF_REG_COUNT - slave_address = DEF_SLAVE_ADDRESS - poll_delay = period.get_seconds() - - app_key = "modbus" - if app_key in app_base.settings: - if "port_name" in app_base.settings[app_key]: - port_name = app_base.settings[app_key]["port_name"] - if "baud_rate" in app_base.settings[app_key]: - baud_rate = int(app_base.settings[app_key]["baud_rate"]) - if "register_start" in app_base.settings[app_key]: - register_start = int(app_base.settings[app_key]["register_start"]) - if "register_count" in app_base.settings[app_key]: - register_count = int(app_base.settings[app_key]["register_count"]) - if "slave_address" in app_base.settings[app_key]: - slave_address = int(app_base.settings[app_key]["slave_address"]) - if "poll_delay" in app_base.settings[app_key]: - poll_delay = app_base.settings[app_key]["poll_delay"] - poll_delay = period.parse_time_duration_to_seconds(poll_delay) - - # see if port is a digit? - if port_name[0].isdecimal(): - port_name = int(port_name) - - # a few validation tests - if not 0 <= register_start <= 0xFFFF: - raise ValueError("Modbus start address must be between 0 & 0xFFFF") - if not 1 <= register_count <= 125: - raise ValueError("Modbus count must be between 1 & 125") - if not 1 <= slave_address <= 255: - raise ValueError("Modbus address must be between 1 & 125") - if poll_delay < 1: - raise ValueError("Poll delay most be 1 second or longer") - - poll_delay = float(poll_delay) - - # make a fixed Modbus 4x register read/poll - poll = bytes([slave_address, 0x03, - (register_start & 0xFF00) >> 8, register_start & 0xFF, - (register_count & 0xFF00) >> 8, register_count & 0xFF]) - crc = calc_string(poll) - app_base.logger.debug("CRC = %04X" % crc) - poll += bytes([crc & 0xFF, (crc & 0xFF00) >> 8]) - - app_base.logger.info( - "Starting Modbus/RTU poll {0}, baud={1}".format(port_name, baud_rate)) - app_base.logger.info( - "Modbus/RTU request is {0}".format(poll)) - - try: - ser = serial.Serial(port_name, baudrate=baud_rate, bytesize=8, - parity='N', stopbits=1, timeout=0.25, - xonxoff=0, rtscts=0) - - except serial.SerialException: - app_base.logger.error("Open failed!") - raise - - try: - while True: - - app_base.logger.info("Send poll") - ser.write(poll) - time.sleep(0.1) - - try: - response = ser.read(size=255) - except KeyboardInterrupt: - app_base.logger.warning( - "Keyboard Interrupt - asked to quit") - break - - if len(response): - app_base.logger.info( - "Modbus/RTU response is {0}".format(response)) - - else: - app_base.logger.error( - "no Modbus/RTU response") - - try: - time.sleep(poll_delay) - except KeyboardInterrupt: - app_base.logger.warning( - "Keyboard Interrupt - asked to quit") - break - - finally: - ser.close() - - return - - -if __name__ == "__main__": - my_app = CradlepointAppBase("serial/serial_echo") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/serial_port/modbus_poll/settings.ini b/serial_port/modbus_poll/settings.ini deleted file mode 100644 index 092bff09..00000000 --- a/serial_port/modbus_poll/settings.ini +++ /dev/null @@ -1,17 +0,0 @@ -[application] -name=modbus_poll -description=Poll a Modbus/RTU register from a PLC/slave -path=serial_port/modbus_poll -uuid=ca0a62bd-648e-4bbc-a915-5f67a477c82d -version=1.0 - -[modbus] -; port_name=/dev/ttyS1 -port_name=/dev/ttyUSB0 -; port_name=COM5 -baud_rate=9600 -; enter the raw Modbus offset, so '0' and NOT 40001 -register_start=0 -register_count=4 -slave_address=1 -poll_delay=15 sec diff --git a/serial_port/modbus_simple_bridge/settings.ini b/serial_port/modbus_simple_bridge/settings.ini deleted file mode 100644 index 7866dbd5..00000000 --- a/serial_port/modbus_simple_bridge/settings.ini +++ /dev/null @@ -1,28 +0,0 @@ -[application] -name=modbus_simple_bridge -description=Simple 1-thread Modbus/TCP to RTU bridge -path=serial_port/modbus_simple_bridge -version=1.27 -uuid=efd6b062-1800-4aac-a066-38e2266bbfb4 - -[startup] -exit_delay=15 sec - -[modbus_serial] -port_name=/dev/ttyS1 -; port_name=/dev/ttyUSB0 -; port_name=COM1 -baud_rate=9600 -; for now, parity should match PySerial expectations -parity=N -; protocls are in (mbrtu, mbasc), default = mbrtu -; protocol=mbasc - -[modbus_ip] -; only set host_ip to limit/narrow interface, else will remain '' for all -; host_ip=127.0.0.1 -; note that host_port must be > -host_port=8502 -idle_timeout=30 sec -; protocls are in (mbtcp, mbrtu, mbasc), default = mbtcp -; protocol=mbtcp diff --git a/serial_port/notes.txt b/serial_port/notes.txt deleted file mode 100644 index 5dba0a1e..00000000 --- a/serial_port/notes.txt +++ /dev/null @@ -1,28 +0,0 @@ -config/system/gpio_actions/ - "serial_gpio_enable": true, - - "system": { - "connector_gpio": { - "output": 0, - "timeout": 300, - "input": 1 - }, - - -# this only works when {"Content-Type": "application/x-www-form-urlencoded"} -result = client.put("control/gpio/CGPIO_CONNECTOR_OUTPUT", "data=%d" % result) -# PUT http://192.168.1.1/api/control/gpio/CGPIO_CONNECTOR_OUTPUT data=0 -# GET RSP 0 - -# result = client.put("control/gpio", {"data": "CGPIO_CONNECTOR_OUTPUT %d" % result}) -# PUT http://192.168.1.1/api/control/gpio {"data": "CGPIO_CONNECTOR_OUTPUT 0"} -# RSP {'exception': 'key', 'key': 'data'} - -# result = client.put("control/gpio", {"CGPIO_CONNECTOR_OUTPUT": result}) -# PUT http://192.168.1.1/api/control/gpio {"CGPIO_CONNECTOR_OUTPUT": 0} -# RSP {'key': 'data', 'exception': 'key'} - -# result = client.put("control/gpio/CGPIO_CONNECTOR_OUTPUT", {"data": result}) -# PUT http://192.168.1.1/api/control/gpio/CGPIO_CONNECTOR_OUTPUT {"data": 0} -# RSP {'key': 'data', 'exception': 'key'} - diff --git a/serial_port/serial_echo/README.md b/serial_port/serial_echo/README.md deleted file mode 100644 index 76457ad4..00000000 --- a/serial_port/serial_echo/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# directory: ./serial_port/serial_echo -## Router App/SDK sample applications - -Wait for data to enter the serial port, then echo back out. -Data will be processed byte by byte, although you could edit the -sample code to wait for an end-of-line character. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -which will be run by main.py - -## File: serial_echo.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code: - -In section [serial_echo]: - -* port_name=???, define the serial port to use. Commonly this will be -/dev/ttyS1 or /dev/ttyUSB0 - -* baud_rate=9600, allows you to define a different baud rate. This sample -assumes the other settings are fixed at: bytesize=8, parity='N', stopbits=1, -and all flow control (XonXOff and HW) is off/disabled. -Edit the code if you need to change this. diff --git a/serial_port/serial_echo/__init__.py b/serial_port/serial_echo/__init__.py deleted file mode 100644 index e9e32431..00000000 --- a/serial_port/serial_echo/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from serial_port.serial_echo.serial_echo import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "serial.serial_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to run_main()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/serial_port/serial_echo/serial_echo.py b/serial_port/serial_echo/serial_echo.py deleted file mode 100644 index bb1091d6..00000000 --- a/serial_port/serial_echo/serial_echo.py +++ /dev/null @@ -1,120 +0,0 @@ -""" -Serial echo -""" -import sys -import serial - -from cp_lib.app_base import CradlepointAppBase - -DEF_BAUD_RATE = 9600 -DEF_PORT_NAME = "/dev/ttyS1" - -# set to None to disable, or set to bytes to send -ASK_ARE_YOU_THERE = b"u there?" - - -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return list: - """ - # confirm we are running on an 1100/1150, result should be "IBR1100LPE" - result = app_base.get_product_name() - if result in ("IBR1100", "IBR1150", "IBR600B", "IBR650B"): - app_base.logger.info("Product Model is good:{}".format(result)) - else: - app_base.logger.error( - "Inappropriate Product:{} - aborting.".format(result)) - return -1 - - port_name = DEF_PORT_NAME - baud_rate = DEF_BAUD_RATE - if "serial_echo" in app_base.settings: - if "port_name" in app_base.settings["serial_echo"]: - port_name = app_base.settings["serial_echo"]["port_name"] - if "baud_rate" in app_base.settings["serial_echo"]: - baud_rate = int(app_base.settings["serial_echo"]["baud_rate"]) - - # see if port is a digit? - if port_name[0].isdecimal(): - port_name = int(port_name) - - old_pyserial = serial.VERSION.startswith("2.6") - - message = "Starting serial echo on {0}, baud={1}".format(port_name, - baud_rate) - app_base.logger.info(message) - - try: - ser = serial.Serial(port_name, baudrate=baud_rate, bytesize=8, - parity='N', stopbits=1, timeout=1, - xonxoff=0, rtscts=0) - - except serial.SerialException: - app_base.logger.error("Open failed!") - raise - - # start as None to force at least 1 change - dsr_was = None - cts_was = None - - try: - while True: - try: - data = ser.read(size=1) - except KeyboardInterrupt: - app_base.logger.warning( - "Keyboard Interrupt - asked to quit") - break - - if len(data): - app_base.logger.debug(str(data)) - ser.write(data) - elif ASK_ARE_YOU_THERE is not None: - ser.write(ASK_ARE_YOU_THERE) - # else: - # app_base.logger.debug(b".") - - if old_pyserial: - # as of May-2016/FW 6.1, this is PySerial v2.6, so it uses - # the older style control signal access - if dsr_was != ser.getDSR(): - # do this 'get' twice to handle first pass as None - dsr_was = ser.getDSR() - app_base.logger.info( - "DSR changed to {}, setting DTR".format(dsr_was)) - ser.setDTR(dsr_was) - - if cts_was != ser.getCTS(): - cts_was = ser.getCTS() - app_base.logger.info( - "CTS changed to {}, setting RTS".format(cts_was)) - ser.setRTS(cts_was) - else: - if dsr_was != ser.dsr: - dsr_was = ser.dsr - app_base.logger.info( - "DSR changed to {}, setting DTR".format(dsr_was)) - ser.dtr = dsr_was - - if cts_was != ser.cts: - cts_was = ser.cts - app_base.logger.info( - "CTS changed to {}, setting RTS".format(cts_was)) - ser.rts = cts_was - - # if you lose the serial port - like disconnected, then - # ser.getDSR() will throw OSError #5 Input/Output error - - finally: - ser.close() - - return - - -if __name__ == "__main__": - my_app = CradlepointAppBase("serial/serial_echo") - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/serial_port/serial_echo/settings.ini b/serial_port/serial_echo/settings.ini deleted file mode 100644 index ed6bcdc6..00000000 --- a/serial_port/serial_echo/settings.ini +++ /dev/null @@ -1,13 +0,0 @@ -[application] -name=serial_echo -description=Simple echo server, which echos any lines received -path=serial_port/serial_echo -version=1.6 -uuid=98793e88-a2a4-40d6-9c1c-6845d79d3f1b - -[serial_echo] -port_name=/dev/ttyS1 -; port_name=/dev/ttyUSB3 -; port_name=0 - this will be aliased to /dev/ttyS0 -; port_name=COM11 -baud_rate=9600 diff --git a/simple/README.md b/simple/README.md deleted file mode 100644 index 827d88cb..00000000 --- a/simple/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Router App/SDK sample applications - SIMPLE single function samples - -## Directory hello_world - -Shows how to use your own MAIN.PY, which is run as the main app of the SDK. -The code loops, sending a "Hello World" message to a Syslog server. - -Because you control the full application, you must manage the Syslog output -and settings yourself. - -## Directory hello_world_app - -Expands the function in "hello_world" sample, using the stock -CradlepointAppBase class, which adds the following services: -* self.logger = a Syslog logging module -* self.settings = finds/loads your settings.json (made from settings.ini) -* self.cs_client = read/write the Cradlepoint status/config trees - -## Directory hello_world_1task - -Expands the function in "hello_world_app" sample, adding a sub-task -to do the "Hello World" Syslog message. -Shows how to spawn, as well as stop (kill/join) the sub-task. - -## Directory hello_world_3task - -Expands the function in "hello_world_1task" sample, -adding three (3) sub-tasks, including one which exits and must be restarted - -## Directory send_alert - -Sends a message to router, which is treated as an ECM alert. diff --git a/simple/hello_world/README.md b/simple/hello_world/README.md deleted file mode 100644 index 76e363f7..00000000 --- a/simple/hello_world/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# directory: ./simple/hello_world -## Router App/SDK sample applications - -The sample application shows how to create your own MAIN function. - -The stock MAKE.PY will detect the existence of this 'main.py' file, -using it instead of the stock cp_lib/make.py. -The downside is that your application must handle setup of syslog, -finding app settings, and so on. -(see ./simple/hello_world_app for a version using the stock main and settings design) - -The actual application sends a "Hello SDK World" message to Syslog at an -'INFO' priority. - -## File: __init__.py - -{ an EMPTY file - used to define Python modules } - -## File: main.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, required by MAKE.PY - diff --git a/simple/hello_world/__init__.py b/simple/hello_world/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/simple/hello_world/main.py b/simple/hello_world/main.py deleted file mode 100644 index da3e3712..00000000 --- a/simple/hello_world/main.py +++ /dev/null @@ -1,52 +0,0 @@ -import time - -# NOTES: -# - since this MAIN.PY exists, the tool building the TAR.GZIP archive will -# ignore (not use) the standard ./config/main.py -# -# - it is critical that the file "simple/hello_world/__init__.py" is empty -# -# - although this file does NOT use the "simple/hello_world/settings.ini" -# will be required by the tool building the TAR.GZIP archive! - - -def do_something(): - logger.info("Hello SDK World!") - time.sleep(10) - return - - -if __name__ == "__main__": - import sys - import logging - import logging.handlers - - if sys.platform == "win32": - # technically, will run on Linux, but designed for CP router - raise NotImplementedError("This code only runs on router") - - logger = logging.getLogger("routerSDK") - logger.setLevel(logging.DEBUG) - - formatter = logging.Formatter("%(levelname)s:%(name)s:%(message)s") - - # create the Syslog handler, which blends output on router's - # Syslog output - handler = logging.handlers.SysLogHandler( - address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_LOCAL6) - handler.setLevel(logging.DEBUG) - handler.setFormatter(formatter) - logger.addHandler(handler) - - logger.info("Starting ...") - time.sleep(2.0) - - while True: - # we do this wrap to dump any Python exception traceback out to - # Syslog. Of course, this simple code won't likely fail, but it - # demos the requirement! Without this, you'd see no error! - try: - do_something() - except: - logger.exception("simple main.py failed!") - raise diff --git a/simple/hello_world/settings.ini b/simple/hello_world/settings.ini deleted file mode 100644 index e47c4568..00000000 --- a/simple/hello_world/settings.ini +++ /dev/null @@ -1,8 +0,0 @@ -; Although the hello_world MAIN.PY does NOT use this, the MAKE.PY -; tool which builds the TAR.GZIP requires some of this data -[application] -name=hello_world -description=Simple Hello-World sample -path=simple.hello_world -uuid=114c8c0f-9887-4e68-a4e7-de8d73fba69f -version=1.12 diff --git a/simple/hello_world_1task/README.md b/simple/hello_world_1task/README.md deleted file mode 100644 index 3db1ccb4..00000000 --- a/simple/hello_world_1task/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# directory: ./simple/hello_world_1task -## Router App/SDK sample applications - -The sample application creates 1 sub-tasks (so two total). -The main application starts 1 sub-task, which loops, sleeping a fixed time, then printing out a Syslog INFO message. -It shows the proper way to deal with sub-tasks, including standard clean-up when exiting. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -with will be run by the stock main.py - -## File: hello_world.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code - diff --git a/simple/hello_world_1task/__init__.py b/simple/hello_world_1task/__init__.py deleted file mode 100644 index 2085f4c2..00000000 --- a/simple/hello_world_1task/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from simple.hello_world_1task.hello_world import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/simple/hello_world_1task/hello_world.py b/simple/hello_world_1task/hello_world.py deleted file mode 100644 index 1e906593..00000000 --- a/simple/hello_world_1task/hello_world.py +++ /dev/null @@ -1,90 +0,0 @@ -import threading -import time -from cp_lib.app_base import CradlepointAppBase - - -def run_router_app(app_base): - """ - Say hello every 10 seconds using a task. Note this sample is rather silly - as the CP router spawns THIS function (run_router_app()) as a unique - thread. so no point making 1 thread into two! - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - my_task = HelloOneTask("task1", app_base) - my_task.start() - - try: - while True: - time.sleep(300) - - except KeyboardInterrupt: - # this is only true on a test PC - won't see on router - # must trap here to prevent try/except in __init__.py from avoiding - # the graceful shutdown below. - pass - - # now we need to try & kill off our kids - if we are here - app_base.logger.info("Okay, exiting") - - # signal sub task to halt - my_task.please_stop() - - # what until it does - remember, it is sleeping, so will take some time - my_task.join() - return 0 - - -class HelloOneTask(threading.Thread): - - def __init__(self, name, app_base): - """ - prep our thread, but do not start yet - - :param str name: name for the thread - :param CradlepointAppBase app_base: prepared resources: logger, etc - """ - threading.Thread.__init__(self, name=name) - - self.app_base = app_base - self.app_base.logger.info("started INIT") - - if "hello_world" in app_base.settings and \ - "message" in app_base.settings["hello_world"]: - # see if we have 'message' data in settings.ini - self.say_what = app_base.settings["hello_world"]["message"] - else: - self.say_what = "Hello SDK World!" - - # create an event to manage our stopping - # (Note: on CP router, this isn't strictly true, as when the parent is - # stopped/halted, the child dies as well. However, you may want - # your sub task to clean up before it exists - self.keep_running = threading.Event() - self.keep_running.set() - - return - - def run(self): - """ - Now thread is being asked to start running - """ - - self.app_base.logger.info("started RUN") - - message = "task:{0} says:{1}".format(self.name, self.say_what) - while self.keep_running.is_set(): - self.app_base.logger.info(message) - time.sleep(10) - - message = "task:{0} was asked to stop!".format(self.name) - self.app_base.logger.info(message) - return 0 - - def please_stop(self): - """ - Now thread is being asked to start running - """ - self.keep_running.clear() - return diff --git a/simple/hello_world_1task/settings.ini b/simple/hello_world_1task/settings.ini deleted file mode 100644 index 9f65753a..00000000 --- a/simple/hello_world_1task/settings.ini +++ /dev/null @@ -1,10 +0,0 @@ -[application] -name=hello_world -description=Simple Hello World sample, using 1 subtask -path=simple.hello_world_1task -version=1.0 -uuid=4d9afb98-dab9-4a54-9738-9de9c7e69539 - -; this section and item is unique to this sample -[hello_world] -message=Hello Universe! diff --git a/simple/hello_world_3tasks/README.md b/simple/hello_world_3tasks/README.md deleted file mode 100644 index e52ca032..00000000 --- a/simple/hello_world_3tasks/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# directory: ./simple/hello_world_3tasks -## Router App/SDK sample applications - -The sample application creates 3 sub-tasks (so four total). -The main application starts 3 sub-tasks, which loop, sleeping with a random -delay before printing out a Syslog INFO message. It shows the proper way to deal with sub-tasks, including standard clean-up when exiting. - -The first 2 sub-tasks will run 'forever' - or on a PC, when you do a ^C or -keyboard interrupt, the main task will abort, using an event() to stop -all three sub-tasks. - -The third task will exit after each loop, and the main task will notice, then re-run it. - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -with will be run by the stock main.py - -## File: hello_world.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code - diff --git a/simple/hello_world_3tasks/__init__.py b/simple/hello_world_3tasks/__init__.py deleted file mode 100644 index 452bc5b0..00000000 --- a/simple/hello_world_3tasks/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from simple.hello_world_3tasks.hello_world import run_router_app - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/simple/hello_world_3tasks/hello_world.py b/simple/hello_world_3tasks/hello_world.py deleted file mode 100644 index 0b98fda0..00000000 --- a/simple/hello_world_3tasks/hello_world.py +++ /dev/null @@ -1,134 +0,0 @@ -import gc -import random -import threading -import time -from cp_lib.app_base import CradlepointAppBase - -# these are required to match in settings.ini -NAME1 = "message1" -NAME2 = "message2" -NAME3 = "message3" - -# to make a little more interesting, each sub-task selects a random loop delay -MIN_DELAY = 5 -MAX_DELAY = 15 - - -def run_router_app(app_base): - """ - Say hello every 10 seconds using a task. Note this sample is rather silly - as the CP router spawns THIS function (run_router_app()) as a unique - thread. so no point making 1 thread into two! - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - - # we want a more random pattern; not repeated - random.seed() - - # task #1 runs "forever" - my_task1 = HelloOneTask(NAME1, app_base) - my_task1.start() - - # task #2 runs "forever" - my_task2 = HelloOneTask(NAME2, app_base) - my_task2.start() - - # task #3 exits after each delay loop - my_task3 = HelloOneTask(NAME3, app_base) - my_task3.start() - - try: - while True: - if not my_task3.is_alive(): - # we keep restarting task #3; it will exit always - - # do memory collection, clean-up after old run on task #3 - # else we risk collecting a lot of large chunks - gc.collect() - - app_base.logger.info("Re-Running task #3") - my_task3.run() - - time.sleep(30) - - except KeyboardInterrupt: - # this is only true on a test PC - won't see on router - # must trap here to prevent try/except in __init__.py from avoiding - # the graceful shutdown below. - pass - - # now we need to try & kill off our kids - if we are here - app_base.logger.info("Okay, exiting") - - # signal sub task to halt - my_task1.please_stop() - my_task2.please_stop() - my_task3.please_stop() - - # what until it does - remember, it is sleeping, so will take some time - my_task1.join() - my_task2.join() - my_task3.join() - - return 0 - - -class HelloOneTask(threading.Thread): - - def __init__(self, name, app_base): - """ - prep our thread, but do not start yet - - :param str name: name for the thread. We will assume - :param CradlepointAppBase app_base: prepared resources: logger, etc - """ - threading.Thread.__init__(self, name=name) - - self.app_base = app_base - self.app_base.logger.info("started INIT") - - if "hello_world" in app_base.settings and \ - name in app_base.settings["hello_world"]: - # see if we have 'message' data in settings.ini - self.say_what = app_base.settings["hello_world"][name] - else: - self.say_what = "no message for [%s]!" % name - - # create an event to manage our stopping - # (Note: on CP router, this isn't strictly true, as when the parent is - # stopped/halted, the child dies as well. However, you may want - # your sub task to clean up before it exists - self.keep_running = threading.Event() - self.keep_running.set() - - return - - def run(self): - """ - Now thread is being asked to start running - """ - delay = random.randrange(MIN_DELAY, MAX_DELAY) - message = "task:{0} Running, delay={1}".format(self.name, delay) - self.app_base.logger.info(message) - - message = "task:{0} says:{1}".format(self.name, self.say_what) - self.keep_running.set() - while self.keep_running.is_set(): - self.app_base.logger.info(message) - time.sleep(delay) - if self.name == NAME3: - # then we exit - self.keep_running.clear() - - message = "task:{0} was asked to stop!".format(self.name) - self.app_base.logger.info(message) - return 0 - - def please_stop(self): - """ - Now thread is being asked to start running - """ - self.keep_running.clear() - return diff --git a/simple/hello_world_3tasks/settings.ini b/simple/hello_world_3tasks/settings.ini deleted file mode 100644 index 8775ca64..00000000 --- a/simple/hello_world_3tasks/settings.ini +++ /dev/null @@ -1,12 +0,0 @@ -[application] -name=hello_world -description=Hello World sample, using 3 subtasks -path=simple.hello_world_3tasks -version=1.11 -uuid=c69cfe79-5f11-4cae-986a-9d568bf96629 - -; this section and item is unique to this sample -[hello_world] -message1=Hello Universe! -message2=Where am I? -message3=What? Again? diff --git a/simple/hello_world_app/README.md b/simple/hello_world_app/README.md deleted file mode 100644 index 87095a96..00000000 --- a/simple/hello_world_app/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# directory: ./simple/hello_world_app -## Router App/SDK sample applications - -The sample application shows how to use the CradlepointAppBase to make -an ultra-simple application. -The CradlepointAppBase adds the following services: -* self.logger = a Syslog logging module -* self.settings = finds/loads your settings.json (made from settings.ini) -* self.cs_client = read/write the Cradlepoint status/config trees - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -with will be run by the stock main.py - -## File: hello_world.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code - diff --git a/simple/hello_world_app/__init__.py b/simple/hello_world_app/__init__.py deleted file mode 100644 index 987bd013..00000000 --- a/simple/hello_world_app/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from simple.hello_world_app.hello_world import do_hello_world - - -class RouterApp(CradlepointAppBase): - - def __init__(self, app_name): - """ - :param str app_name: the file name, such as "simple.hello_world_app" - :return: - """ - CradlepointAppBase.__init__(self, app_name) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = do_hello_world(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/simple/hello_world_app/hello_world.py b/simple/hello_world_app/hello_world.py deleted file mode 100644 index abd05a87..00000000 --- a/simple/hello_world_app/hello_world.py +++ /dev/null @@ -1,21 +0,0 @@ -import time -from cp_lib.app_base import CradlepointAppBase - - -def do_hello_world(app_base): - """ - Say hello every 10 seconds - - :param CradlepointAppBase app_base: prepared resources: logger, cs_client - :return: - """ - if "hello_world" in app_base.settings and \ - "message" in app_base.settings["hello_world"]: - # see if we have 'message' data in settings.ini - say_what = app_base.settings["hello_world"]["message"] - else: - say_what = "Hello SDK World!" - - while True: - app_base.logger.info(say_what) - time.sleep(10) diff --git a/simple/hello_world_app/settings.ini b/simple/hello_world_app/settings.ini deleted file mode 100644 index b42d29ef..00000000 --- a/simple/hello_world_app/settings.ini +++ /dev/null @@ -1,10 +0,0 @@ -[application] -name=hello_world -description=Simple Hello World sample, using CradlepointAppBase -path=simple.hello_world -version=1.0 -uuid=15265202-c336-44cc-8ddc-4032a54c9244 - -; this section and item is unique to this sample -[hello_world] -message=Hello Universe! diff --git a/simple/send_alert/README.md b/simple/send_alert/README.md deleted file mode 100644 index 208ba567..00000000 --- a/simple/send_alert/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# directory: ./simple/send_alert -## Router App/SDK sample applications - -The sample application uses the local CS_Client function to push an ECM -alert. - -(How do they show up in ECM? That's TBD) - -## File: __init__.py - -The Python script with the class RouterApp(CradlepointAppBase) instance, -with will be run by the stock main.py - -## File: hello_world.py - -The main files of the application. - -## File: settings.ini - -The Router App settings, including a few required by this code - diff --git a/simple/send_alert/__init__.py b/simple/send_alert/__init__.py deleted file mode 100644 index 88ccc3cd..00000000 --- a/simple/send_alert/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from cp_lib.app_base import CradlepointAppBase -from simple.send_alert.send_alert import run_router_app - - -# this name RouterApp is CRITICAL - importlib will seek it! -class RouterApp(CradlepointAppBase): - - def __init__(self, app_main): - """ - :param str app_main: the file name, such as "network.tcp_echo" - :return: - """ - CradlepointAppBase.__init__(self, app_main) - return - - def run(self): - self.logger.debug("__init__ chaining to run_router_app()") - - # we do this wrap to dump any Python exception traceback out to Syslog - try: - result = run_router_app(self) - except: - self.logger.exception("CradlepointAppBase failed") - raise - - return result diff --git a/simple/send_alert/send_alert.py b/simple/send_alert/send_alert.py deleted file mode 100644 index 3993e52c..00000000 --- a/simple/send_alert/send_alert.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -Send a Single message to ECM as alert. -""" -from cp_lib.app_base import CradlepointAppBase - - -# this name "run_router_app" is not important, reserved, or demanded -# - but must match below in __main__ and also in __init__.py -def run_router_app(app_base): - """ - - :param CradlepointAppBase app_base: resources: logger, settings, etc - """ - - app_base.logger.info("Send ALERT to ECM via CSClient") - - # user may have passed in another path - alert = "Default Message - All is well!" - if "send_alert" in app_base.settings: - if "alert" in app_base.settings["send_alert"]: - alert = app_base.settings["send_alert"]["alert"] - - # here we send to Syslog using our OWN facility/channel - app_base.logger.info('LOG: Sending alert to ECM=[{}].'.format(alert)) - - # this line will be sent via Router's Syslog facility/channel - app_base.cs_client.log( - 'RouterSDKDemo', 'CS: Sending alert to ECM=[{}].'.format(alert)) - app_base.cs_client.alert('RouterSDKDemo', alert) - - return 0 - - -if __name__ == "__main__": - import sys - - my_app = CradlepointAppBase("simple/send_alert") - - _result = run_router_app(my_app) - my_app.logger.info("Exiting, status code is {}".format(_result)) - sys.exit(_result) diff --git a/simple/send_alert/settings.ini b/simple/send_alert/settings.ini deleted file mode 100644 index 5b4af9a3..00000000 --- a/simple/send_alert/settings.ini +++ /dev/null @@ -1,11 +0,0 @@ -[application] -name=send_alert -description=Send an alert to ECM -path=simple/send_alert -; restart=false -uuid=547c5c9f-1d59-40db-9691-a157fcb8d3ae -version=1.0 - -[send_alert] -; place your status path here - like status/product_info/product_name -alert=Hello from the field! diff --git a/simple_web_server/cs.py b/simple_web_server/cs.py new file mode 100644 index 00000000..9359ed03 --- /dev/null +++ b/simple_web_server/cs.py @@ -0,0 +1,271 @@ +""" +sdk_config_store.py - Communication module for sdk apps + +Copyright (c) 2017 Cradlepoint, Inc. . All rights reserved. + +This file contains confidential information of CradlePoint, Inc. and your use of +this file is subject to the CradlePoint Software License Agreement distributed with +this file. Unauthorized reproduction or distribution of this file is subject to civil and +criminal penalties. + +""" + +import json +import re +import socket +import sys + + +class SdkCSException(Exception): + pass + + +CSCLIENT_NAME = 'SDK CSClient' + + +class CSClient(object): + END_OF_HEADER = b"\r\n\r\n" + STATUS_HEADER_RE = re.compile(b"status: \w*") + CONTENT_LENGTH_HEADER_RE = re.compile(b"content-length: \w*") + MAX_PACKET_SIZE = 8192 + RECV_TIMEOUT = 2.0 + + _instances = {} + + @classmethod + def is_initialized(cls): + return (cls in cls._instances) + + def __new__(cls, *na, **kwna): + """ Singleton factory (with subclassing support) """ + if not cls.is_initialized(): + cls._instances[cls] = super().__new__(cls) + return cls._instances[cls] + + def __init__(self, init=False): + if not init: + return + + @staticmethod + def _get_router_access_info(): + """Should only be called when running in a computer. It will return the + dev_client_ip, dev_client_username, and dev_client_password as defined in + the sdk section of the sdk_settings.ini file.""" + router_ip = '' + router_username = '' + router_password = '' + + if sys.platform != 'linux2': + import os + import configparser + + settings_file = os.path.join(os.path.dirname(os.getcwd()), 'sdk_settings.ini') + config = configparser.ConfigParser() + config.read(settings_file) + + # Keys in sdk_settings.ini + sdk_key = 'sdk' + ip_key = 'dev_client_ip' + username_key = 'dev_client_username' + password_key = 'dev_client_password' + + if sdk_key in config: + if ip_key in config[sdk_key]: + router_ip = config[sdk_key][ip_key] + else: + print('ERROR 1: The {} key does not exist in {}'.format(ip_key, settings_file)) + + if username_key in config[sdk_key]: + router_username = config[sdk_key][username_key] + else: + print('ERROR 2: The {} key does not exist in {}'.format(username_key, settings_file)) + + if password_key in config[sdk_key]: + router_password = config[sdk_key][password_key] + else: + print('ERROR 3: The {} key does not exist in {}'.format(password_key, settings_file)) + else: + print('ERROR 4: The {} section does not exist in {}'.format(sdk_key, settings_file)) + + return router_ip, router_username, router_password + + def get(self, base, query='', tree=0): + """Send a get request.""" + if sys.platform == 'linux2': + cmd = "get\n{}\n{}\n{}\n".format(base, query, tree) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the get to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.get(router_api, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return json.loads(response.text) + + def put(self, base, value='', query='', tree=0): + """Send a put request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "put\n{}\n{}\n{}\n{}\n".format(base, query, tree, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the put to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.put(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def append(self, base, value='', query=''): + """Send an append request.""" + value = json.dumps(value).replace(' ', '') + if sys.platform == 'linux2': + cmd = "post\n{}\n{}\n{}\n".format(base, query, value) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the post to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.post(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password), + data={"data": '{}'.format(value)}) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def delete(self, base, query=''): + """Send a delete request.""" + if sys.platform == 'linux2': + cmd = "delete\n{}\n{}\n".format(base, query) + return self._dispatch(cmd) + else: + # Running in a computer so use http to send the delete to the router. + import requests + router_ip, router_username, router_password = self._get_router_access_info() + router_api = 'http://{}/api/{}/{}'.format(router_ip, base, query) + + try: + response = requests.delete(router_api, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + auth=requests.auth.HTTPDigestAuth(router_username, router_password)) + except (requests.exceptions.Timeout, + requests.exceptions.ConnectionError): + print("Timeout: router at {} did not respond.".format(router_ip)) + return None + + return response.text + + def alert(self, app_name='', value=''): + """Send a request to create an alert.""" + if sys.platform == 'linux2': + cmd = "alert\n{}\n{}\n".format(app_name, value) + return self._dispatch(cmd) + else: + print('Alert is only available when running the app in the router.') + raise NotImplementedError + + def log(self, name='', value='', level='DEBUG'): + """Send a request to create a log entry.""" + if sys.platform == 'linux2': + cmd = "log\n{}\n{}\n".format(name, value) + return self._dispatch(cmd) + else: + # Running in a computer so just use print for the log. + print('[{}]: {}'.format(name, value)) + + def _safe_dispatch(self, cmd): + """Send the command and return the response.""" + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect('/var/tmp/cs.sock') + sock.sendall(bytes(cmd, 'ascii')) + return self._receive(sock) + + def _dispatch(self, cmd): + errmsg = None + result = "" + try: + result = self._safe_dispatch(cmd) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "dispatch failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + pass + return result + + def _safe_receive(self, sock): + sock.settimeout(self.RECV_TIMEOUT) + data = b"" + eoh = -1 + while eoh < 0: + # In the event that the config store times out in returning data, lib returns + # an empty result. Then again, if the config store hangs for 2+ seconds, + # the app's behavior is the least of our worries. + try: + buf = sock.recv(self.MAX_PACKET_SIZE) + except socket.timeout: + return {"status": "timeout", "data": None} + if len(buf) == 0: + break + data += buf + eoh = data.find(self.END_OF_HEADER) + + status_hdr = self.STATUS_HEADER_RE.search(data).group(0)[8:] + content_len = self.CONTENT_LENGTH_HEADER_RE.search(data).group(0)[16:] + remaining = int(content_len) - (len(data) - eoh - len(self.END_OF_HEADER)) + + # body sent from csevent_xxx.sock will have id, action, path, & cfg + while remaining > 0: + buf = sock.recv(self.MAX_PACKET_SIZE) # TODO: This will hang things as well. + if len(buf) == 0: + break + data += buf + remaining -= len(buf) + body = data[eoh:].decode() + try: + result = json.loads(body) + except json.JSONDecodeError as e: + # config store receiver doesn't give back + # proper json for 'put' ops, body + # contains verbose error message + # so putting the error msg in result + result = body.strip() + return {"status": status_hdr.decode(), "data": result} + + def _receive(self, sock): + errmsg = None + result = "" + try: + result = self._safe_receive(sock) + except Exception as err: + # ignore the command error, continue on to next command + errmsg = "_receive failed with exception={} err={}".format(type(err), str(err)) + if errmsg is not None: + self.log(CSCLIENT_NAME, errmsg) + return result diff --git a/simple_web_server/install.sh b/simple_web_server/install.sh new file mode 100644 index 00000000..1cceb73b --- /dev/null +++ b/simple_web_server/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash +echo "INSTALLATION simple_web_server on:" >> install.log +date >> install.log diff --git a/simple_web_server/package.ini b/simple_web_server/package.ini new file mode 100644 index 00000000..ef7083e6 --- /dev/null +++ b/simple_web_server/package.ini @@ -0,0 +1,11 @@ +[simple_web_server] +uuid=fbf3522a-83b8-4901-97c9-067772d9993c +vendor=Cradlepoint +notes=Simple Web Server Reference Application with single fixed page +firmware_major=6 +firmware_minor=2 +restart=false +reboot=true +version_major=1 +version_minor=0 +auto_start=true diff --git a/simple_web_server/simple_web_server.py b/simple_web_server/simple_web_server.py new file mode 100644 index 00000000..50dd2343 --- /dev/null +++ b/simple_web_server/simple_web_server.py @@ -0,0 +1,78 @@ +""" +A Simple Web server +""" + +import cs +import argparse + +from http.server import BaseHTTPRequestHandler, HTTPServer + +APP_NAME = 'simple_web_server' +WEB_MESSAGE = "Hello World from Cradlepoint router!" + + +def start_server(): + # avoid 8080, as the router may have service on it. + # Firewall rules will need to be changed in the router + # to allow access on this port. + server_address = ('', 9001) + + cs.CSClient().log(APP_NAME, "Starting Server: {}".format(server_address)) + cs.CSClient().log(APP_NAME, "Web Message is: {}".format(WEB_MESSAGE)) + + httpd = HTTPServer(server_address, WebServerRequestHandler) + + try: + httpd.serve_forever() + + except KeyboardInterrupt: + cs.CSClient().log(APP_NAME, "Stopping Server, Key Board interrupt") + + return 0 + + +class WebServerRequestHandler(BaseHTTPRequestHandler): + + def do_GET(self): + # Log the Get request + cs.CSClient().log(APP_NAME, 'Received Get request: {}'.format(self.path)) + + # Send response status code + self.send_response(200) + + # Send headers + self.send_header('Content-type', 'text/html') + self.end_headers() + + # Send message back to client + # Write content as utf-8 data + self.wfile.write(bytes(WEB_MESSAGE, "utf8")) + return + + +def action(command): + try: + # Log the action for the app. + cs.CSClient().log(APP_NAME, 'action({})'.format(command)) + + if command == 'start': + start_server() + + elif command == 'stop': + pass + + except: + cs.CSClient().log(APP_NAME, 'Problem with {} on {}!'.format(APP_NAME, command)) + raise + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('opt') + args = parser.parse_args() + + if args.opt not in ['start', 'stop']: + cs.CSClient().log(APP_NAME, 'Failed to run command: {}'.format(args.opt)) + exit() + + action(args.opt) diff --git a/simple_web_server/start.sh b/simple_web_server/start.sh new file mode 100644 index 00000000..f0dbea22 --- /dev/null +++ b/simple_web_server/start.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython simple_web_server.py start diff --git a/simple_web_server/stop.sh b/simple_web_server/stop.sh new file mode 100644 index 00000000..26df61b7 --- /dev/null +++ b/simple_web_server/stop.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cppython simple_web_server.py stop diff --git a/syslog.bat b/syslog.bat deleted file mode 100644 index fe58c60c..00000000 --- a/syslog.bat +++ /dev/null @@ -1 +0,0 @@ -python tools\syslog_server.py diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/sdk_status_examples.txt b/test/sdk_status_examples.txt deleted file mode 100644 index 98a2d1f8..00000000 --- a/test/sdk_status_examples.txt +++ /dev/null @@ -1,397 +0,0 @@ - -App was installed okay, and started: -{ - '6903f7bb-cee2-41eb-af-9b71ab8d3ad5': { - 'type': 'development', - 'app': { - 'date': '2016-03-03T17: 04: 06.838652', - 'uuid': '6903f7bb-cee2-41eb-afba-9b71ab8d3ad5', - 'version_major': 1, - 'name': 'hello_world', - 'restart': True, - 'version_minor': 10, - 'vendor': 'customer' - }, - 'summary': 'Startedapplication', - 'state': 'started' - } -} - -{ - '114c8c0f-9887-4e68-a4e7-de8d73fba69f': { - 'state': 'started', - 'summary': 'Startedapplication', - 'app': { - 'version_major': 1, - 'uuid': '114c8c0f-9887-4e68-a4e7-de8d73fba69f', - 'restart': True, - 'date': '2016-03-04T15: 30: 01.455373', - 'vendor': 'customer', - 'version_minor': 8, - 'name': 'hello_world' - }, - 'type': 'development' - } -} - -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK:GOOD file:/usr/lib -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcares.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcares.so.2.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcompat.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcrypto.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcrypto.so.1.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcsclient.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcsclient.so.1.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip4tc.so.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip4tc.so.0.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip6tc.so.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip6tc.so.0.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libipsec.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libipsec.so.0.8.1 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libiptc.so.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libiptc.so.0.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libjansson.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libjansson.so.2.6 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libm.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libm.so.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libmnl.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libmnl.so.1.0.3 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_acct.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_acct.so.1.0.2 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_conntrack.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_conntrack.so.1.0.4 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnfnetlink.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnfnetlink.so.1.0.1 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-genl.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-genl.so.3.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-nf.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-nf.so.3.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-route.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-route.so.3.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl.so.3.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam.so.1.1.6 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam_misc.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam_misc.so.1.1.6 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpamc.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpamc.so.1.1.6 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpcap.so.1 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpcap.so.1.6.2 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libquagga.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssh.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssh.so.0.7.1 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssl.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssl.so.1.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libusb.so -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libusb.so.1.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libxtables.so.7 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libxtables.so.7.0.0 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libz.so.1 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libz.so.1.2.8 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pppd -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:python3.3 -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:rp-pppoe -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:security -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ulogd -92.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:xtables - -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>httpserver: Accepted API login from local address 192.168.1.6, user: admin -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:Hello SDK World! -192.168.1.1 : <14>httpserver: Accepted API login from local address 192.168.1.6, user: admin -192.168.1.1 : <14>httpserver: Accepted API login from local address 192.168.1.6, user: admin -192.168.1.1 : <15>routersdk: _onControlAction ['control', 'system', 'sdk', 'action'] uninstall 114c8c0f-9887-4e68-a4e7-de8d73fba69f -192.168.1.1 : <15>psmon:sh: Stopping process 4028: /bin/sh -192.168.1.1 : <15>psmon:sh: Process (/bin/sh[4028]) exited with code: -15 -192.168.1.1 : <15>psmon:sh: Stopped process 4028: /bin/sh -192.168.1.1 : <15>routersdk: Application successfully stopped -192.168.1.1 : <14>filemgr: Rebuilding flash sector: id: 1, allocated: 234, remaining: 2847, total: 62689, erase_count: 107, inodes: 6, files: 1 -192.168.1.1 : <15>svcmgr: Erasing sector: 1 -192.168.1.1 : <15>routersdk: Application successfully uninstalled -192.168.1.1 : <14>httpserver: Accepted API login from local address 192.168.1.6, user: admin -192.168.1.1 : <15>ecm: Remote code exec triggered -192.168.1.1 : <14>ssh: Client SSH session started from 192.168.1.6, user: admin -192.168.1.1 : <15>routersdk: Developer application upload successful -192.168.1.1 : <14>ssh: Client session ended from 192.168.1.6, user: admin -192.168.1.1 : <15>routersdk: Successfully created paths /var/mnt/sdk/apps/114c8c0f-9887-4e68-a4e7-de8d73fba69f & /var/mnt/sdk/apps/114c8c0f-9887-4e68 --a4e7-de8d73fba69f/dist -192.168.1.1 : <15>routersdk: Successfully downloaded package dist/tmpd0eek0 -192.168.1.1 : <15>routersdk: Successfully extracted application directory (hello_world) -192.168.1.1 : <15>routersdk: Successfully extracted application -192.168.1.1 : <15>routersdk: Application Manifest validation successful -192.168.1.1 : <14>filemgr: Rebuilding flash sector: id: 0, allocated: 887, remaining: 1659, total: 63877, erase_count: 106, inodes: 8, files: 3 -192.168.1.1 : <15>svcmgr: Erasing sector: 0 -192.168.1.1 : <15>routersdk: Application successfully installed -192.168.1.1 : <15>psmon:sh: Started process 4118: ['/bin/sh', 'start.sh'] -192.168.1.1 : <15>routersdk: Application successfully started -192.168.1.1 : <14>UNKNOWN: INFO:routerSDK:CWD:/var/mnt/sdk/apps/114c8c0f-9887-4e68-a4e7-de8d73fba69f/hello_world -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK:GOOD file:/usr/lib -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcares.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcares.so.2.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcompat.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcrypto.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcrypto.so.1.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcsclient.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libcsclient.so.1.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip4tc.so.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip4tc.so.0.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip6tc.so.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libip6tc.so.0.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libipsec.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libipsec.so.0.8.1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libiptc.so.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libiptc.so.0.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libjansson.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libjansson.so.2.6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libm.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libm.so.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libmnl.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libmnl.so.1.0.3 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_acct.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_acct.so.1.0.2 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_conntrack.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnetfilter_conntrack.so.1.0.4 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnfnetlink.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnfnetlink.so.1.0.1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-genl.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-genl.so.3.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-nf.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-nf.so.3.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-route.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl-route.so.3.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libnl.so.3.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam.so.1.1.6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam_misc.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpam_misc.so.1.1.6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpamc.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpamc.so.1.1.6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpcap.so.1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libpcap.so.1.6.2 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libquagga.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssh.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssh.so.0.7.1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssl.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libssl.so.1.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libusb.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libusb.so.1.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libxtables.so.7 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libxtables.so.7.0.0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libz.so.1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:libz.so.1.2.8 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pppd -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:python3.3 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:rp-pppoe -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:security -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ulogd -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:xtables - -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK:GOOD file:/usr/lib/python3.3 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:OpenSSL -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:__future__.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:_compat_pickle.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:_pyio.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:_ssh.cpython-33m.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:_strptime.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:_weakrefset.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:abc.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:argparse.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:base64.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:bdb.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:bisect.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cProfile.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:calendar.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cgi.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:chunk.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cmd.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:code.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:codecs.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:codeop.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:collections -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:compileall.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:configparser.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:contextlib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:copy.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:copyreg.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cp -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cpsite.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cradlepoint.cpython-33m.so -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ctypes -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:datetime.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:dateutil -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:difflib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:dis.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:dnslib -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:dnsproxy.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:dummy_threading.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:email -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:encodings -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:fnmatch.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:functools.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:genericpath.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:getopt.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:gettext.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:glob.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:gzip.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:hashlib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:heapq.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:hmac.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:html -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:http -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:imp.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:importlib -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:inspect.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:io.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ipaddress.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:json -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:keyword.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:lib-dynload -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:linecache.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:locale.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:logging -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:lzma.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mailbox.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mimetypes.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:numbers.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:opcode.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:os.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pdb.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pickle.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pkgutil.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:platform.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:posixpath.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pprint.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:py_compile.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pyrad -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:queue.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:quopri.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:random.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:re.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:reprlib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:runpy.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:serial -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:shlex.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:shutil.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:smtplib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:socket.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:socketserver.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:sre_compile.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:sre_constants.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:sre_parse.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ssh.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ssl.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:stat.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:string.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:stringprep.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:struct.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:subprocess.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tarfile.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:telnetlib.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tempfile.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:textwrap.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:threading.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:token.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tokenize.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tornado -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:traceback.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tty.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:types.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:urllib -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:uu.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:uuid.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:warnings.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:weakref.pyo -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:xml - -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK:GOOD file:/dev -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:log -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cp_lkm_pm -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cp_lkm_usb -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:switch0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ubi1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ubi1_0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:watchdog0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:watchdog -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:pts -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:bus -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock8 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock7 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock5 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:root -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock4 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock3 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock2 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtdblock0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:network_throughput -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ralink_gpio -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ubi_ctrl -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:network_latency -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:cpu_dma_latency -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:net -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ttyS1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ttyS0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:console -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ptmx -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:tty -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:ppp -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd8ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd7ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd6ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd5ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd4ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd3ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd2ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd1ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd0ro -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd8 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd7 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd6 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd5 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd4 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd3 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd2 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd1 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mtd0 -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:urandom -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:random -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:zero -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:port -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:null -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:kmsg -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:full -192.168.1.1 : <15>UNKNOWN: DEBUG:routerSDK: file:mem diff --git a/test/test_app_base.py b/test/test_app_base.py deleted file mode 100644 index a6c0f6ff..00000000 --- a/test/test_app_base.py +++ /dev/null @@ -1,487 +0,0 @@ -# Test the cp_lib.app_base module - -import logging -import os -import unittest - - -class TestAppBase(unittest.TestCase): - - def test_import_full_file_name(self): - """ - :return: - """ - from cp_lib.app_base import CradlepointAppBase - - print() # move past the '.' - - if True: - return - - name = "network.tcp_echo" - obj = CradlepointAppBase(name) - - # here, INIT is used, it exists and is larger than trivial (5 bytes) - # we want the tcp-echo.py to exist, but won't use - expect = os.path.join("network", "tcp_echo", "tcp_echo.py") - self.assertTrue(os.path.exists(expect)) - - expect = os.path.join("network", "tcp_echo", "__init__.py") - self.assertTrue(os.path.exists(expect)) - logging.info("TEST names when {} can be run_name".format(expect)) - - self.assertEqual(obj.run_name, expect) - expect = os.path.join("network", "tcp_echo") + os.sep - self.assertEqual(obj.app_path, expect) - self.assertEqual(obj.app_name, "tcp_echo") - self.assertEqual(obj.mod_name, "network.tcp_echo") - - name = "RouterSDKDemo" - obj = CradlepointAppBase(name) - - # here, the app name is used (the INIT is empty / zero bytes) - expect = os.path.join("RouterSDKDemo", "__init__.py") - self.assertTrue(os.path.exists(expect)) - logging.info( - "TEST names when {} is too small to be run_name".format(expect)) - - expect = os.path.join(name, name) + ".py" - self.assertTrue(os.path.exists(expect)) - logging.info("TEST names when {} can be run_name".format(expect)) - - self.assertEqual(obj.run_name, expect) - expect = name + os.sep - self.assertEqual(obj.app_path, expect) - self.assertEqual(obj.app_name, "RouterSDKDemo") - self.assertEqual(obj.mod_name, "RouterSDKDemo") - - return - - def test_normalize_app_name(self): - """ - :return: - """ - from cp_lib.app_name_parse import normalize_app_name, \ - get_module_name, get_app_name, get_app_path - # TODO - test get_run_name()! - import os - - print() # move past the '.' - - logging.info("TEST normalize_app_name()") - tests = [ - ("network\\tcp_echo\\file.py", ["network", "tcp_echo", "file.py"]), - ("network\\tcp_echo\\file", ["network", "tcp_echo", "file", ""]), - ("network\\tcp_echo\\", ["network", "tcp_echo", ""]), - ("network\\tcp_echo", ["network", "tcp_echo", ""]), - - ("network/tcp_echo/file.py", ["network", "tcp_echo", "file.py"]), - ("network/tcp_echo/file", ["network", "tcp_echo", "file", ""]), - ("network/tcp_echo/", ["network", "tcp_echo", ""]), - ("network/tcp_echo", ["network", "tcp_echo", ""]), - - ("network.tcp_echo.file.py", ["network", "tcp_echo", "file.py"]), - ("network.tcp_echo.file", ["network", "tcp_echo", "file", ""]), - ("network.tcp_echo.", ["network", "tcp_echo", ""]), - ("network.tcp_echo", ["network", "tcp_echo", ""]), - - ("network", ["network", ""]), - ("network.py", ["", "network.py"]), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = normalize_app_name(source) - # logging.debug( - # "normalize_app_name({0}) = {1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.info("TEST get_module_name()") - tests = [ - ("network\\tcp_echo\\file.py", "network.tcp_echo"), - ("network\\tcp_echo\\file", "network.tcp_echo.file"), - ("network\\tcp_echo", "network.tcp_echo"), - - ("network/tcp_echo/file.py", "network.tcp_echo"), - ("network/tcp_echo/file", "network.tcp_echo.file"), - ("network/tcp_echo", "network.tcp_echo"), - - ("network.tcp_echo.file.py", "network.tcp_echo"), - ("network.tcp_echo.file", "network.tcp_echo.file"), - ("network.tcp_echo", "network.tcp_echo"), - - ("network", "network"), - ("network.py", ""), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = get_module_name(source) - # logging.debug( - # "get_module_name({0}) = {1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.info("TEST get_app_name()") - tests = [ - ("network\\tcp_echo\\file.py", "tcp_echo"), - ("network\\tcp_echo\\file", "file"), - ("network\\tcp_echo", "tcp_echo"), - - ("network/tcp_echo/file.py", "tcp_echo"), - ("network/tcp_echo/file", "file"), - ("network/tcp_echo", "tcp_echo"), - - ("network.tcp_echo.file.py", "tcp_echo"), - ("network.tcp_echo.file", "file"), - ("network.tcp_echo", "tcp_echo"), - - ("network", "network"), - ("network.py", ""), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = get_app_name(source) - # logging.debug("get_app_name({0}) = {1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.info( - "TEST get_app_path(), with native os.sep, = \'{}\'".format(os.sep)) - tests = [ - ("network\\tcp_echo\\file.py", - os.path.join("network", "tcp_echo") + os.sep), - ("network\\tcp_echo\\file", - os.path.join("network", "tcp_echo", "file") + os.sep), - ("network\\tcp_echo", - os.path.join("network", "tcp_echo") + os.sep), - - ("network/tcp_echo/file.py", - os.path.join("network", "tcp_echo") + os.sep), - ("network/tcp_echo/file", - os.path.join("network", "tcp_echo", "file") + os.sep), - ("network/tcp_echo", - os.path.join("network", "tcp_echo") + os.sep), - - ("network.tcp_echo.file.py", - os.path.join("network", "tcp_echo") + os.sep), - ("network.tcp_echo.file", - os.path.join("network", "tcp_echo", "file") + os.sep), - ("network.tcp_echo", - os.path.join("network", "tcp_echo") + os.sep), - - ("network", "network" + os.sep), - ("network.py", ""), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = get_app_path(source) - # logging.debug("get_module_name({0})={1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.info("TEST get_app_path(), forced LINUX separator of \'/\'") - tests = [ - ("network\\tcp_echo\\file.py", "network/tcp_echo/"), - ("network\\tcp_echo\\file", "network/tcp_echo/file/"), - ("network\\tcp_echo", "network/tcp_echo/"), - - ("network/tcp_echo/file.py", "network/tcp_echo/"), - ("network/tcp_echo/file", "network/tcp_echo/file/"), - ("network/tcp_echo", "network/tcp_echo/"), - - ("network.tcp_echo.file.py", "network/tcp_echo/"), - ("network.tcp_echo.file", "network/tcp_echo/file/"), - ("network.tcp_echo", "network/tcp_echo/"), - - ("network", "network/"), - ("network.py", ""), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = get_app_path(source, separator='/') - # logging.debug("get_module_name({0})={1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.info("TEST get_app_path(), forced WINDOWS separator of \'\\\'") - tests = [ - ("network\\tcp_echo\\file.py", "network\\tcp_echo\\"), - ("network\\tcp_echo\\file", "network\\tcp_echo\\file\\"), - ("network\\tcp_echo", "network\\tcp_echo\\"), - - ("network/tcp_echo/file.py", "network\\tcp_echo\\"), - ("network/tcp_echo/file", "network\\tcp_echo\\file\\"), - ("network/tcp_echo", "network\\tcp_echo\\"), - - ("network.tcp_echo.file.py", "network\\tcp_echo\\"), - ("network.tcp_echo.file", "network\\tcp_echo\\file\\"), - ("network.tcp_echo", "network\\tcp_echo\\"), - - ("network", "network\\"), - ("network.py", ""), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = get_app_path(source, separator='\\') - # logging.debug("get_module_name({0})={1}".format(source, result)) - - self.assertEqual(result, expect) - - logging.warning("TODO - we don't TEST get_run_name()") - - return - - def test_normalize_path_separator(self): - """ - :return: - """ - from cp_lib.app_name_parse import normalize_path_separator - import os - - print() # move past the '.' - - logging.info("TEST normalize_path_separator() to Windows Style") - tests = [ - ("network\\tcp_echo\\file.py", "network\\tcp_echo\\file.py"), - ("network\\tcp_echo\\file", "network\\tcp_echo\\file"), - ("network\\tcp_echo", "network\\tcp_echo"), - - ("network\\tcp_echo/file.py", "network\\tcp_echo\\file.py"), - - ("network/tcp_echo/file.py", "network\\tcp_echo\\file.py"), - ("network/tcp_echo/file", "network\\tcp_echo\\file"), - ("network/tcp_echo", "network\\tcp_echo"), - - ("network.tcp_echo.file.py", "network.tcp_echo.file.py"), - - ("network", "network"), - ("network.py", "network.py"), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = normalize_path_separator(source, separator="\\") - # logging.debug( - # "normalize_path_separator({0}) = {1}".format(source, result)) - self.assertEqual(result, expect) - - logging.info("TEST normalize_path_separator() to Linux Style") - tests = [ - ("network\\tcp_echo\\file.py", "network/tcp_echo/file.py"), - ("network\\tcp_echo\\file", "network/tcp_echo/file"), - ("network\\tcp_echo", "network/tcp_echo"), - - ("network\\tcp_echo/file", "network/tcp_echo/file"), - - ("network/tcp_echo/file.py", "network/tcp_echo/file.py"), - ("network/tcp_echo/file", "network/tcp_echo/file"), - ("network/tcp_echo", "network/tcp_echo"), - - ("network.tcp_echo.file.py", "network.tcp_echo.file.py"), - - ("network", "network"), - ("network.py", "network.py"), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = normalize_path_separator(source, separator="/") - # logging.debug( - # "normalize_path_separator({0}) = {1}".format(source, result)) - self.assertEqual(result, expect) - - logging.info( - "TEST normalize_path_separator(), native os.sep = \'{}\'".format( - os.sep)) - tests = [ - ("network\\tcp_echo\\file.py", - os.path.join("network", "tcp_echo", "file.py")), - ("network\\tcp_echo\\file", - os.path.join("network", "tcp_echo", "file")), - ("network\\tcp_echo", - os.path.join("network", "tcp_echo")), - - ("network\\tcp_echo/file.py", - os.path.join("network", "tcp_echo", "file.py")), - - ("network/tcp_echo/file.py", - os.path.join("network", "tcp_echo", "file.py")), - ("network/tcp_echo/file", - os.path.join("network", "tcp_echo", "file")), - ("network/tcp_echo", - os.path.join("network", "tcp_echo")), - - ("network.tcp_echo.file.py", "network.tcp_echo.file.py"), - - ("network", "network"), - ("network.py", "network.py"), - ] - - for test in tests: - source = test[0] - expect = test[1] - - result = normalize_path_separator(source) - # logging.debug( - # "normalize_path_separator({0}) = {1}".format(source, result)) - self.assertEqual(result, expect) - - return - - def test_get_settings(self): - """ - :return: - """ - from cp_lib.app_base import CradlepointAppBase - - print() # move past the '.' - - # we'll just use this as example, assuming .config/setting.ini - # and - name = "network.tcp_echo" - obj = CradlepointAppBase(name, call_router=False) - - # just slam in a known 'data tree' - obj.settings = { - 'application': { - 'firmware': '6.1', - 'name': 'make', - 'restart': 'true', - 'reboot': True, - 'sleeping': 'On', - 'explosion': 'enabled', - }, - 'router_api': { - 'user_name': 'admin', - 'interface': 'ENet USB-1', - 'password': '441b1702', - 'local_ip': '192.168.1.1' - }, - 'logging': { - 'syslog_ip': '192.168.1.6', - 'pc_syslog': 'false', - 'level': 'debug' - }, - 'startup': { - 'boot_delay_for_wan': 'True', - 'exit_delay': '30 sec', - 'boot_delay_max': '5 min', - 'bomb_delay': 17, - 'rain_delay': '19', - 'boot_delay_for_time': 'True' - }, - 'glob_dir': 'config', - 'base_name': 'settings', - 'app_dir': 'network\\tcp_echo\\', - } - - logging.info("TEST simple get_setting(), without force_type") - self.assertEqual("make", - obj.get_setting("application.name")) - self.assertEqual("6.1", - obj.get_setting("application.firmware")) - self.assertEqual(True, - obj.get_setting("application.reboot")) - - logging.info("TEST get_setting(), with force_type=bool") - self.assertEqual("true", - obj.get_setting("application.restart")) - self.assertEqual(True, - obj.get_setting("application.restart", - force_type=bool)) - self.assertEqual(True, - obj.get_setting("application.reboot")) - self.assertEqual(True, - obj.get_setting("application.reboot", - force_type=bool)) - self.assertEqual(1, - obj.get_setting("application.reboot", - force_type=int)) - self.assertEqual("On", - obj.get_setting("application.sleeping")) - self.assertEqual(True, - obj.get_setting("application.sleeping", - force_type=bool)) - self.assertEqual("enabled", - obj.get_setting("application.explosion")) - self.assertEqual(True, - obj.get_setting("application.explosion", - force_type=bool)) - # doesn't exist, but force to String means "None" - self.assertEqual(None, - obj.get_setting("application.not_exists", - force_type=bool)) - - with self.assertRaises(ValueError): - # string 'true' can't be forced to int, bool True can - obj.get_setting("application.name", force_type=bool) - - logging.info("TEST get_setting(), with force_type=str") - # [restart] is already string, but [reboot] is bool(True) - self.assertEqual("true", - obj.get_setting("application.restart", - force_type=str)) - self.assertEqual("True", - obj.get_setting("application.reboot", - force_type=str)) - # doesn't exist, & force to String does not means "None" - self.assertEqual(None, - obj.get_setting("application.not_exists")) - self.assertEqual(None, - obj.get_setting("application.not_exists", - force_type=str)) - - logging.info("TEST get_setting_time_secs()") - self.assertEqual( - "30 sec", obj.get_setting("startup.exit_delay")) - self.assertEqual( - 30.0, obj.get_setting_time_secs("startup.exit_delay")) - - self.assertEqual( - "5 min", obj.get_setting("startup.boot_delay_max")) - self.assertEqual( - 300.0, obj.get_setting_time_secs("startup.boot_delay_max")) - - self.assertEqual( - 17, obj.get_setting("startup.bomb_delay")) - self.assertEqual( - 17.0, obj.get_setting_time_secs("startup.bomb_delay")) - - self.assertEqual( - "19", obj.get_setting("startup.rain_delay")) - self.assertEqual( - 19.0, obj.get_setting_time_secs("startup.rain_delay")) - - with self.assertRaises(ValueError): - # string 'true' can't be forced to int, bool True can - obj.get_setting("application.restart", force_type=int) - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_buffer_dump.py b/test/test_buffer_dump.py deleted file mode 100644 index fdc21a7c..00000000 --- a/test/test_buffer_dump.py +++ /dev/null @@ -1,188 +0,0 @@ -# Test the cp_lib.split.version module - -import logging -import unittest - - -class TestBufferDump(unittest.TestCase): - - def test_buffer_dump(self): - """ - Test buffer to lines function - :return: - """ - from cp_lib.buffer_dump import buffer_dump - - tests = [ - {"msg": None, "dat": None, "asc": False, - "exp": ['dump:None, data=None']}, - {"msg": None, "dat": None, "asc": True, - "exp": ['dump:None, data=None']}, - {"msg": "null", "dat": None, "asc": False, - "exp": ['dump:null, data=None']}, - {"msg": "null", "dat": None, "asc": True, - "exp": ['dump:null, data=None']}, - - {"msg": "fruit", "dat": "Apple", "asc": False, - "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65']}, - {"msg": "fruit", "dat": "Apple", "asc": True, - "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65 \'Apple\'']}, - {"msg": "fruit", "dat": b"Apple", "asc": False, - "exp": ['dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65']}, - {"msg": "fruit", "dat": b"Apple", "asc": True, - "exp": ['dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65 b\'Apple\'']}, - - {"msg": "fruit", "dat": "Apple\n", "asc": False, - "exp": ['dump:fruit, len=6', '[000] 41 70 70 6C 65 0A']}, - {"msg": "fruit", "dat": "Apple\n", "asc": True, - "exp": ['dump:fruit, len=6', '[000] 41 70 70 6C 65 0A \'Apple\\n\'']}, - {"msg": "fruit", "dat": b"Apple\n", "asc": False, - "exp": ['dump:fruit, len=6 bytes()', '[000] 41 70 70 6C 65 0A']}, - {"msg": "fruit", "dat": b"Apple\n", "asc": True, - "exp": ['dump:fruit, len=6 bytes()', '[000] 41 70 70 6C 65 0A b\'Apple\\n\'']}, - - {"msg": "fruit nl", "dat": "Apple\r\n\0", "asc": False, - "exp": ['dump:fruit nl, len=8', '[000] 41 70 70 6C 65 0D 0A 00']}, - {"msg": "fruit nl", "dat": "Apple\r\n\0", "asc": True, - "exp": ['dump:fruit nl, len=8', '[000] 41 70 70 6C 65 0D 0A 00 \'Apple\\r\\n\\x00\'']}, - {"msg": "fruit nl", "dat": b"Apple\r\n\0", "asc": False, - "exp": ['dump:fruit nl, len=8 bytes()', - '[000] 41 70 70 6C 65 0D 0A 00']}, - {"msg": "fruit nl", "dat": b"Apple\r\n\0", "asc": True, - "exp": ['dump:fruit nl, len=8 bytes()', - '[000] 41 70 70 6C 65 0D 0A 00 b\'Apple\\r\\n\\x00\'']}, - - {"msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": False, - "exp": ['dump:longer, len=42', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', - '[032] 20 6D 79 20 62 69 72 74 68 0A']}, - {"msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": True, - "exp": ['dump:longer, len=42', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + - ' \'Apple\\nIs found i\'', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + - ' \'n the country of\'', - '[032] 20 6D 79 20 62 69 72 74 68 0A \' my birth\\n\'']}, - {"msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": False, - "exp": ['dump:longer, len=42 bytes()', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', - '[032] 20 6D 79 20 62 69 72 74 68 0A']}, - {"msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": True, - "exp": ['dump:longer, len=42 bytes()', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + - ' b\'Apple\\nIs found i\'', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + - ' b\'n the country of\'', - '[032] 20 6D 79 20 62 69 72 74 68 0A b\' my birth\\n\'']}, - - {"msg": "Modbus", "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, - "exp": ['dump:Modbus, len=11', - '[000] 01 1F 00 01 02 03 04 05 06 08 09']}, - {"msg": "Modbus", "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, - "exp": ['dump:Modbus, len=11 bytes()', - '[000] 01 1F 00 01 02 03 04 05 06 08 09']}, - {"msg": "Modbus+", "asc": False, - "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", - "exp": ['dump:Modbus+, len=19', - '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', - '[016] 05 06 08']}, - {"msg": "Modbus+", "asc": False, - "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", - "exp": ['dump:Modbus+, len=19 bytes()', - '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', - '[016] 05 06 08']}, - - ] - - for test in tests: - # logging.debug("Test:{}".format(test)) - - result = buffer_dump(test['msg'], test['dat'], test['asc']) - for line in result: - logging.debug(" {}".format(line)) - self.assertEqual(result, test['exp']) - - logging.debug("") - - return - - def test_logger_buffer_dump(self): - """ - Test buffer to lines function - :return: - """ - from cp_lib.buffer_dump import logger_buffer_dump - - tests = [ - {"msg": "fruit", "dat": "Apple", "asc": False, - "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65']}, - {"msg": "fruit", "dat": "Apple", "asc": True, - "exp": ['dump:fruit, len=5', '[000] 41 70 70 6C 65 \'Apple\'']}, - {"msg": "fruit", "dat": b"Apple", "asc": False, - "exp": ['dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65']}, - {"msg": "fruit", "dat": b"Apple", "asc": True, - "exp": ['dump:fruit, len=5 bytes()', '[000] 41 70 70 6C 65 b\'Apple\'']}, - - {"msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": False, - "exp": ['dump:longer, len=42', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', - '[032] 20 6D 79 20 62 69 72 74 68 0A']}, - {"msg": "longer", "dat": "Apple\nIs found in the country of my birth\n", "asc": True, - "exp": ['dump:longer, len=42', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + - ' \'Apple\\nIs found i\'', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + - ' \'n the country of\'', - '[032] 20 6D 79 20 62 69 72 74 68 0A \' my birth\\n\'']}, - {"msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": False, - "exp": ['dump:longer, len=42 bytes()', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66', - '[032] 20 6D 79 20 62 69 72 74 68 0A']}, - {"msg": "longer", "dat": b"Apple\nIs found in the country of my birth\n", "asc": True, - "exp": ['dump:longer, len=42 bytes()', - '[000] 41 70 70 6C 65 0A 49 73 20 66 6F 75 6E 64 20 69' + - ' b\'Apple\\nIs found i\'', - '[016] 6E 20 74 68 65 20 63 6F 75 6E 74 72 79 20 6F 66' + - ' b\'n the country of\'', - '[032] 20 6D 79 20 62 69 72 74 68 0A b\' my birth\\n\'']}, - - {"msg": "Modbus", "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, - "exp": ['dump:Modbus, len=11', - '[000] 01 1F 00 01 02 03 04 05 06 08 09']}, - {"msg": "Modbus", "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09", "asc": False, - "exp": ['dump:Modbus, len=11 bytes()', - '[000] 01 1F 00 01 02 03 04 05 06 08 09']}, - {"msg": "Modbus+", "asc": False, - "dat": "\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", - "exp": ['dump:Modbus+, len=19', - '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', - '[016] 05 06 08']}, - {"msg": "Modbus+", "asc": False, - "dat": b"\x01\x1F\x00\x01\x02\x03\x04\x05\x06\x08\x09\x00\x01\x02\x03\x04\x05\x06\x08", - "exp": ['dump:Modbus+, len=19 bytes()', - '[000] 01 1F 00 01 02 03 04 05 06 08 09 00 01 02 03 04', - '[016] 05 06 08']}, - - ] - - logging.info("") - - logger = logging.getLogger('unitest') - logger.setLevel(logging.DEBUG) - - for test in tests: - # logging.debug("Test:{}".format(test)) - - logger_buffer_dump(logger, test['msg'], test['dat'], test['asc']) - logging.info("") - - return - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_clean_ini.py b/test/test_clean_ini.py deleted file mode 100644 index 68837b44..00000000 --- a/test/test_clean_ini.py +++ /dev/null @@ -1,85 +0,0 @@ -# Test the cp_lib.clean_ini module - -import logging -import os.path -import shutil -import unittest - -from cp_lib.clean_ini import clean_ini_file, DEF_BACKUP_EXT - - -class TestCleanIni(unittest.TestCase): - - TEST_FILE_NAME_INI = "test/test.ini" - - def test_settings(self): - """ - Test the raw/simple handling of 1 INI to JSON in any directory - :return: - """ - - data_list = [ - "", - "# Global settings", - "", - "[Logging]", - "level = debug", - "name = silly_toes", - "log_file = trace.txt", - "#server = (\"192.168.0.10\", 514)", - "", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password =441b1702", - "", - "", - ] - - # make the original bad-ish file - _han = open(self.TEST_FILE_NAME_INI, 'w') - for line in data_list: - _han.write(line + "\n") - _han.close() - - test_file_name_bak = self.TEST_FILE_NAME_INI + DEF_BACKUP_EXT - self._remove_name_no_error(test_file_name_bak) - - self.assertTrue(os.path.exists(self.TEST_FILE_NAME_INI)) - self.assertFalse(os.path.exists(test_file_name_bak)) - - clean_ini_file(self.TEST_FILE_NAME_INI, backup=True) - - self.assertTrue(os.path.exists(self.TEST_FILE_NAME_INI)) - self.assertTrue(os.path.exists(test_file_name_bak)) - - # self.assertEqual(settings[cp_logging.SETS_NAME], cp_logging.DEF_NAME) - - # clean up the temp file - # self._remove_name_no_error(self.TEST_FILE_NAME_INI) - - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_cp_logging.py b/test/test_cp_logging.py deleted file mode 100644 index 1b478d74..00000000 --- a/test/test_cp_logging.py +++ /dev/null @@ -1,207 +0,0 @@ -# Test the cp_lib.cp_logging module - -import json -import logging -import os.path -import shutil -import unittest - -import cp_lib.cp_logging as cp_logging - - -class TestCpLogging(unittest.TestCase): - - TEST_FILE_NAME_INI = "test/test.ini" - TEST_FILE_NAME_JSON = "test/test.json" - - def test_settings(self): - """ - Test the raw/simple handling of 1 INI to JSON in any directory - :return: - """ - - # start with NO changes - logging.info("test #0 - all defaults") - settings = cp_logging._process_settings() - # logging.debug("settings={}".format(settings)) - - self.assertIsNone(settings[cp_logging.SETS_FILE]) - self.assertIsNone(settings[cp_logging.SETS_SYSLOG_IP]) - - self.assertEqual(settings[cp_logging.SETS_NAME], cp_logging.DEF_NAME) - self.assertEqual(settings[cp_logging.SETS_LEVEL], logging.INFO) - - logging.info("test #1 - confirm the LEVEL setting") - tests = [ - ("10", logging.DEBUG), - (10, logging.DEBUG), - ("debug", logging.DEBUG), - ("Debug", logging.DEBUG), - ("DEBUG", logging.DEBUG), - - (-10, ValueError), - (10.0, ValueError), - ("Junk", ValueError), - ("", ValueError), - (None, ValueError), - ] - - for test in tests: - value = test[0] - expect = test[1] - - # logging.info("") - # logging.debug("Level={0}, type={1}".format(value, type(value))) - ini_data = [ - "[application]", - "name = tcp_echo", - "", - "[logging]", - "level = {}".format(value), - ] - settings = self._make_ini_file(ini_data) - - if expect == ValueError: - with self.assertRaises(ValueError): - cp_logging._process_settings(settings) - else: - settings = cp_logging._process_settings(settings) - self.assertEqual(settings[cp_logging.SETS_LEVEL], expect) - - logging.info("test #2 - confirm the NAME setting") - - expect = "tcp_echo" - ini_data = [ - "[application]", - "name = tcp_echo", - ] - settings = self._make_ini_file(ini_data) - settings = cp_logging._process_settings(settings) - self.assertEqual(settings[cp_logging.SETS_NAME], expect) - - expect = "runny" - ini_data = [ - "[application]", - "name = tcp_echo", - "", - "[logging]", - "name = {}".format(expect), - ] - settings = self._make_ini_file(ini_data) - settings = cp_logging._process_settings(settings) - self.assertEqual(settings[cp_logging.SETS_NAME], expect) - - # expect = "" (empty string - is ValueError) - ini_data = [ - "[application]", - "name = tcp_echo", - "", - "[logging]", - "name = ", - ] - settings = self._make_ini_file(ini_data) - with self.assertRaises(ValueError): - cp_logging._process_settings(settings) - - logging.info("test #3 - confirm the LOG FILE NAME setting") - tests = [ - ("log.txt", "log.txt"), - ("test/log.txt", "test/log.txt"), - ("", None), - ] - - for test in tests: - value = test[0] - expect = test[1] - - ini_data = [ - "[application]", - "name = tcp_echo", - "", - "[logging]", - "log_file = {}".format(expect), - ] - settings = self._make_ini_file(ini_data) - settings = cp_logging._process_settings(settings) - self.assertEqual(settings[cp_logging.SETS_FILE], expect) - - logging.info("test #4 - confirm the SYSLOG SERVER setting") - tests = [ - ("192.168.0.10", "192.168.0.10", 514), - (' ("192.168.0.10", 514)', "192.168.0.10", 514), - ('["192.168.0.10", 514]', "192.168.0.10", 514), - ('("10.4.23.10", 8514)', "10.4.23.10", 8514), - ("", None, 514), - ('("", 8514)', ValueError, 0), - ('("10.4.23.10", -1)', ValueError, 0), - ('("10.4.23.10", 0x10000)', ValueError, 0), - ] - - for test in tests: - value = test[0] - expect_ip = test[1] - expect_port = test[2] - - ini_data = [ - "[application]", - "name = tcp_echo", - "", - "[logging]", - "syslog = {}".format(value), - ] - settings = self._make_ini_file(ini_data) - - if expect_ip == ValueError: - with self.assertRaises(ValueError): - cp_logging._process_settings(settings) - else: - settings = cp_logging._process_settings(settings) - self.assertEqual(settings[cp_logging.SETS_SYSLOG_IP], expect_ip) - if expect_ip is not None: - self.assertEqual(settings[cp_logging.SETS_SYSLOG_PORT], expect_port) - - # clean up the temp file - self._remove_name_no_error(self.TEST_FILE_NAME_INI) - self._remove_name_no_error(self.TEST_FILE_NAME_JSON) - self._remove_name_no_error(self.TEST_FILE_NAME_JSON + ".save") - - return - - def _make_ini_file(self, data_list: list): - """Bounce settings through INI and JSON""" - from cp_lib.load_settings import propagate_ini_to_json - - _han = open(self.TEST_FILE_NAME_INI, 'w') - for line in data_list: - _han.write(line + "\n") - _han.close() - - propagate_ini_to_json(self.TEST_FILE_NAME_INI, self.TEST_FILE_NAME_JSON) - file_han = open(self.TEST_FILE_NAME_JSON, "r") - settings = json.load(file_han) - file_han.close() - - return settings - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_cplib_data_tree.py b/test/test_cplib_data_tree.py deleted file mode 100644 index b227d9c0..00000000 --- a/test/test_cplib_data_tree.py +++ /dev/null @@ -1,436 +0,0 @@ -# Test the PARSE DATA module -import sys -import unittest - -from cp_lib.data.data_tree import DataTreeItemNotFound, DataTreeItemBadValue, \ - get_item, get_item_bool, get_item_int, data_tree_clean, \ - get_item_float, get_item_time_duration_to_seconds, put_item - -# mimic a settings.ini config -data_tree = { - "logging": { - "level": "debug", - "syslog_ip": "192.168.35.6", - "pc_syslog": False, - }, - "router_api": { - "user_name": None, - "interface": "null", - "local_ip": "192.168.35.1", - "password": "441b537e", - # we nest one more level to check depth - "application": { - "name": "make", - "firmware": "6.1", - "restart": "true", - "reboot": True, - "other": { - "able": True, - "babble": False, - "cable": None, - } - }, - }, - "startup": { - "boot_delay_max": "5 min", - "boot_delay_for_time": True, - "boot_delay_for_wan": True, - "exit_delay": "30 sec", - } -} - - -class TestParseDataTree(unittest.TestCase): - - def test_get_item(self): - - # this one's not there, so expect None - self.assertEqual(None, get_item(data_tree, "")) - self.assertEqual(None, get_item(data_tree, "apple")) - self.assertEqual(None, get_item(data_tree, - "router_api.apple")) - self.assertEqual(None, get_item(data_tree, - "router_api.apple.core")) - - # here want the exception thrown, not to return None - with self.assertRaises(DataTreeItemNotFound): - get_item(data_tree, "", throw_exception=True) - get_item(data_tree, "apple", throw_exception=True) - get_item(data_tree, "router_api.apple", throw_exception=True) - get_item(data_tree, "router_api.apple.core", throw_exception=True) - - # however, confirm None which is found is returned as None - self.assertEqual(None, get_item( - data_tree, "router_api.user_name", throw_exception=True)) - self.assertEqual(None, get_item( - data_tree, "router_api.application.other.cable", - throw_exception=True)) - - self.assertEqual("debug", get_item(data_tree, "logging.level")) - self.assertEqual("192.168.35.6", get_item(data_tree, - "logging.syslog_ip")) - self.assertEqual(False, get_item(data_tree, "logging.pc_syslog")) - - self.assertEqual(None, get_item(data_tree, "router_api.user_name")) - self.assertEqual("null", get_item(data_tree, "router_api.interface")) - self.assertEqual("192.168.35.1", get_item(data_tree, - "router_api.local_ip")) - self.assertEqual("441b537e", get_item(data_tree, - "router_api.password")) - - self.assertEqual("make", get_item(data_tree, - "router_api.application.name")) - self.assertEqual("6.1", get_item(data_tree, - "router_api.application.firmware")) - self.assertEqual("true", get_item(data_tree, - "router_api.application.restart")) - self.assertEqual(True, get_item(data_tree, - "router_api.application.reboot")) - - self.assertEqual(True, get_item(data_tree, - "router_api.application.other.able")) - self.assertEqual(False, get_item( - data_tree, "router_api.application.other.babble")) - self.assertEqual(None, get_item( - data_tree, "router_api.application.other.cable")) - - self.assertEqual("5 min", get_item(data_tree, - "startup.boot_delay_max")) - self.assertEqual(True, get_item(data_tree, - "startup.boot_delay_for_time")) - self.assertEqual(True, get_item(data_tree, - "startup.boot_delay_for_wan")) - self.assertEqual("30 sec", get_item(data_tree, "startup.exit_delay")) - - with self.assertRaises(TypeError): - # first param must be dict() - get_item(None, "startup.exit_delay") - get_item(True, "startup.exit_delay") - get_item(10, "startup.exit_delay") - get_item("Hello", "startup.exit_delay") - - with self.assertRaises(TypeError): - # second param must be str() - get_item(data_tree, None) - get_item(data_tree, True) - get_item(data_tree, 10) - - self.assertEqual(None, get_item(data_tree, "")) - - # check a few sub-tree pulls - self.assertEqual(data_tree["logging"], - get_item(data_tree, "logging")) - self.assertEqual(data_tree["router_api"], - get_item(data_tree, "router_api")) - self.assertEqual(data_tree["router_api"]["application"], - get_item(data_tree, "router_api.application")) - self.assertEqual(data_tree["router_api"]["application"]["other"], - get_item(data_tree, "router_api.application.other")) - - return - - def test_data_tree_clean(self): - # mimic a settings.ini config - - # make new tree, since we change the tree items - _data_tree = { - "logging": { - "level": "debug", - "syslog_ip": "false", - "pc_syslog": False, - }, - "router_api": { - "user_name": None, - "interface": "null", - "local_ip": "192.168.35.1", - "password": "none", - "application": { - "name": "make", - "firmware": "6.1", - "restart": "tRUe", - "reboot": True, - }, - }, - } - - # check everything is as expected - self.assertEqual("debug", get_item(_data_tree, "logging.level")) - self.assertEqual("false", get_item(_data_tree, "logging.syslog_ip")) - self.assertEqual(False, get_item(_data_tree, "logging.pc_syslog")) - - self.assertEqual(None, get_item(_data_tree, "router_api.user_name")) - self.assertEqual("null", get_item(_data_tree, "router_api.interface")) - self.assertEqual("192.168.35.1", get_item(_data_tree, - "router_api.local_ip")) - self.assertEqual("none", get_item(_data_tree, "router_api.password")) - - self.assertEqual("make", get_item(_data_tree, - "router_api.application.name")) - self.assertEqual("6.1", get_item(_data_tree, - "router_api.application.firmware")) - self.assertEqual("tRUe", get_item(_data_tree, - "router_api.application.restart")) - self.assertEqual(True, get_item(_data_tree, - "router_api.application.reboot")) - - # now we go through & clean - data_tree_clean(_data_tree) - - self.assertEqual("debug", get_item(_data_tree, "logging.level")) - self.assertEqual(False, get_item(_data_tree, "logging.syslog_ip")) - self.assertEqual(False, get_item(_data_tree, "logging.pc_syslog")) - - self.assertEqual(None, get_item(_data_tree, "router_api.user_name")) - self.assertEqual(None, get_item(_data_tree, "router_api.interface")) - self.assertEqual("192.168.35.1", get_item(_data_tree, - "router_api.local_ip")) - self.assertEqual(None, get_item(_data_tree, "router_api.password")) - - self.assertEqual("make", get_item(_data_tree, - "router_api.application.name")) - self.assertEqual("6.1", get_item(_data_tree, - "router_api.application.firmware")) - self.assertEqual(True, get_item(_data_tree, - "router_api.application.restart")) - self.assertEqual(True, get_item(_data_tree, - "router_api.application.reboot")) - - return - - def test_get_item_bool(self): - - mini_tree = { - "are_true": { - "a": True, - "b": "t", - "c": "T", - "d": "true", - "e": "tRUe", - "f": "TRUE", - "g": "on", - "h": "On", - "i": "ON", - "j": "enable", - "k": "enabled", - "l": 1, - "m": '1', - "are_false": { - "a": False, - "b": "f", - "c": "F", - "d": "false", - "e": "faLSe", - "f": "FALSE", - "g": "off", - "h": "Off", - "i": "OFF", - "j": "disable", - "k": "disabled", - "l": 0, - "m": '0', - }, - }, - "junk": { - "a": None, - "b": 10, - "c": 'Hello', - "d": {'g': True}, - "e": (True, 10), - } - } - - self.assertTrue(get_item_bool(mini_tree, "are_true.a")) - self.assertTrue(get_item_bool(mini_tree, "are_true.b")) - self.assertTrue(get_item_bool(mini_tree, "are_true.c")) - self.assertTrue(get_item_bool(mini_tree, "are_true.d")) - self.assertTrue(get_item_bool(mini_tree, "are_true.e")) - self.assertTrue(get_item_bool(mini_tree, "are_true.f")) - self.assertTrue(get_item_bool(mini_tree, "are_true.g")) - self.assertTrue(get_item_bool(mini_tree, "are_true.h")) - self.assertTrue(get_item_bool(mini_tree, "are_true.i")) - self.assertTrue(get_item_bool(mini_tree, "are_true.j")) - self.assertTrue(get_item_bool(mini_tree, "are_true.k")) - self.assertTrue(get_item_bool(mini_tree, "are_true.l")) - self.assertTrue(get_item_bool(mini_tree, "are_true.m")) - - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.a")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.b")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.c")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.d")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.e")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.f")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.g")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.h")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.i")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.j")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.k")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.l")) - self.assertFalse(get_item_bool(mini_tree, "are_true.are_false.m")) - - # just check the bool items - self.assertFalse(get_item_bool(data_tree, "logging.pc_syslog")) - self.assertTrue(get_item_bool(data_tree, - "router_api.application.restart")) - self.assertTrue(get_item_bool(data_tree, - "router_api.application.reboot")) - self.assertTrue(get_item_bool(data_tree, - "router_api.application.other.able")) - self.assertFalse(get_item_bool(data_tree, - "router_api.application.other.babble")) - self.assertTrue(get_item_bool(data_tree, - "startup.boot_delay_for_time")) - self.assertTrue(get_item_bool(data_tree, - "startup.boot_delay_for_wan")) - - with self.assertRaises(DataTreeItemBadValue): - # none of these are Boolean. - get_item_bool(mini_tree, "junk.a") - get_item_bool(mini_tree, "junk.b") - get_item_bool(mini_tree, "junk.c") - get_item_bool(mini_tree, "junk.d") - get_item_bool(mini_tree, "junk.e") - - return - - def test_get_item_integer(self): - - mini_tree = { - "okay": { - "a": 10, - "b": "13", - "c": " 99", - "d": "0x13", - }, - "junk": { - "a": None, - "b": True, - "c": 'Hello', - "d": {'g': True}, - "e": (True, 10), - } - } - - self.assertEqual(10, get_item_int(mini_tree, "okay.a")) - self.assertEqual(13, get_item_int(mini_tree, "okay.b")) - self.assertEqual(99, get_item_int(mini_tree, "okay.c")) - self.assertEqual(19, get_item_int(mini_tree, "okay.d")) - - with self.assertRaises(DataTreeItemBadValue): - # none of these are integers. - get_item_int(mini_tree, "junk.a") - get_item_int(mini_tree, "junk.b") - get_item_int(mini_tree, "junk.c") - get_item_int(mini_tree, "junk.d") - get_item_int(mini_tree, "junk.e") - - return - - def test_get_item_float(self): - - mini_tree = { - "okay": { - "a": 10, - "b": "13", - "c": " 99", - "d": 10.1, - "e": "13.2 ", - "f": " 99.3", - }, - "junk": { - "a": None, - "b": True, - "c": 'Hello', - "d": {'g': True}, - "e": (True, 10), - } - } - - self.assertEqual(10.0, get_item_float(mini_tree, "okay.a")) - self.assertEqual(13.0, get_item_float(mini_tree, "okay.b")) - self.assertEqual(99.0, get_item_float(mini_tree, "okay.c")) - - self.assertEqual(10.1, get_item_float(mini_tree, "okay.d")) - self.assertEqual(13.2, get_item_float(mini_tree, "okay.e")) - self.assertEqual(99.3, get_item_float(mini_tree, "okay.f")) - - with self.assertRaises(DataTreeItemBadValue): - # none of these are floats. - get_item_float(mini_tree, "junk.a") - get_item_float(mini_tree, "junk.b") - get_item_float(mini_tree, "junk.c") - get_item_float(mini_tree, "junk.d") - get_item_float(mini_tree, "junk.e") - - return - - def test_get_item_time_duration(self): - - mini_tree = { - "okay": { - "a": 10, - "b": "13", - "c": " 99", - - "d": 10.1, - "e": "13.2 ", - "f": " 99.3", - - "g": "10 sec", - "h": "10 min", - "i": "10 hour", - }, - "junk": { - "a": None, - "b": True, - "c": 'Hello', - "d": {'g': True}, - "e": (True, 10), - } - } - - self.assertEqual(10.0, get_item_time_duration_to_seconds(mini_tree, - "okay.a")) - self.assertEqual(13.0, get_item_time_duration_to_seconds(mini_tree, - "okay.b")) - self.assertEqual(99.0, get_item_time_duration_to_seconds(mini_tree, - "okay.c")) - - self.assertEqual(10.1, get_item_time_duration_to_seconds(mini_tree, - "okay.d")) - self.assertEqual(13.2, get_item_time_duration_to_seconds(mini_tree, - "okay.e")) - self.assertEqual(99.3, get_item_time_duration_to_seconds(mini_tree, - "okay.f")) - - self.assertEqual(10.0, get_item_time_duration_to_seconds(mini_tree, - "okay.g")) - self.assertEqual(600.0, get_item_time_duration_to_seconds(mini_tree, - "okay.h")) - self.assertEqual(36000.0, get_item_time_duration_to_seconds(mini_tree, - "okay.i")) - - with self.assertRaises(DataTreeItemBadValue): - # none of these are time durations. - get_item_time_duration_to_seconds(mini_tree, "junk.a") - get_item_time_duration_to_seconds(mini_tree, "junk.b") - get_item_time_duration_to_seconds(mini_tree, "junk.c") - get_item_time_duration_to_seconds(mini_tree, "junk.d") - get_item_time_duration_to_seconds(mini_tree, "junk.e") - - return - - def test_put_item(self): - - test_tree = dict() - - with self.assertRaises(TypeError): - put_item(None, "usa", "USA") - put_item(test_tree, 10, "USA") - - put_item(test_tree, "minnesota", "MN") - print("tree:{}".format(test_tree)) - - return - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_gps_nmea.py b/test/test_cplib_gps_nmea.py deleted file mode 100644 index 93c78209..00000000 --- a/test/test_cplib_gps_nmea.py +++ /dev/null @@ -1,254 +0,0 @@ -# Test the CP_LIB gps NMEA parser -import sys -import time -import unittest - -import cp_lib.gps_nmea as nmea - - -class TestGpsNmea(unittest.TestCase): - - def test_checksum(self): - - tests = [ - ("$GPGGA,222227.0,4500.819061,N,09320.092805,W,1,10,0.8,281." + - "3,M,-33.0,M,,*6F\r\n", - "GPGGA,222227.0,4500.819061,N,09320.092805,W,1,10,0.8,281." + - "3,M,-33.0,M,,", 0x6F), - ("$GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0,353.2," + - "020516,0.0,E,A*1D\r\n", - "GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0,353.2," + - "020516,0.0,E,A", 0x1D), - ("$GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A*23\r\n", - "GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A", - 0x23), - ("$PCPTI,IBR1100-702,222227,222227*33\r\n", - "PCPTI,IBR1100-702,222227,222227", - 0x33), - ("$GPGGA,222237.0,4500.819061,N,09320.092805,W,1,09,0.8,281.3" + - ",M,-33.0,M,,*66\r\n", - "GPGGA,222237.0,4500.819061,N,09320.092805,W,1,09,0.8,281.3" + - ",M,-33.0,M,,", 0x66), - ("$GPRMC,222237.0,A,4500.819061,N,09320.092805,W,0.0,353.2" + - ",020516,0.0,E,A*1C\r\n", - "GPRMC,222237.0,A,4500.819061,N,09320.092805,W,0.0,353.2" + - ",020516,0.0,E,A", 0x1C), - - # some bad tests - (None, TypeError, 0), - ("", IndexError, 0), - ] - - for test in tests: - full = test[0] - base = test[1] - csum = test[2] - - if base == TypeError: - with self.assertRaises(TypeError): - nmea.calc_checksum(full) - - elif base == IndexError: - with self.assertRaises(IndexError): - nmea.calc_checksum(full) - - else: - self.assertEqual(nmea.calc_checksum(full), csum) - self.assertTrue(nmea.validate_checksum(full)) - self.assertEqual(nmea.calc_checksum(base), csum) - self.assertEqual(nmea.wrap_sentence(base), full) - - return - - def test_rmc(self): - - tests = [ - ("$GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0,353.2," + - "020516,0.0,E,A*1D\r\n", - {'course': 353.2, 'long': -93.33488008333333, 'knots': 0.0, - 'lat': 45.013651016666664, 'valid': True, - 'gps_utc': 1462227747.0, 'time': 1462302255.968968}), - ("$GPRMC,222237.0,A,4500.819061,N,09320.092805,W,0.0,353.2" + - ",020516,0.0,E,A*1C\r\n", - {'course': 353.2, 'long': -93.33488008333333, 'knots': 0.0, - 'lat': 45.013651016666664, 'valid': True, - 'gps_utc': 1462227757.0, 'time': 1462302255.968968}), - ] - - obj = nmea.NmeaStatus() - obj.date_time = True - obj.speed = True - obj.altitude = True - obj.coor_ddmm = False - obj.coor_dec = True - - for test in tests: - sentence = test[0] - expect = test[1] - now = 1462302255.968968 - - # print("source[%s]" % sentence) - obj.start(now) - result = obj.parse_sentence(sentence) - self.assertTrue(result) - obj.publish() - - result = obj.get_attributes() - # print("RMC result:{}".format(result)) - # print("RMC expect:{}".format(expect)) - self.assertEqual(result, expect) - - return - - def test_vtg(self): - - tests = [ - ("$GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A*23\r\n", - {'course': 353.2, 'kmh': 0.0, 'knots': 0.0, - 'time': 1462302255.968968}), - ] - - obj = nmea.NmeaStatus() - obj.date_time = True - obj.speed = True - obj.altitude = True - obj.coor_ddmm = False - obj.coor_dec = True - - for test in tests: - sentence = test[0] - expect = test[1] - now = 1462302255.968968 - - # print("inp [%s]" % sentence) - obj.start(now) - result = obj.parse_sentence(sentence) - self.assertTrue(result) - obj.publish() - - result = obj.get_attributes() - # print("out[{}]".format(result)) - self.assertEqual(result, expect) - - return - - def test_gga(self): - - tests = [ - ("$GPGGA,222227.0,4500.819061,N,09320.092805,W,1,10,0.8,281." + - "3,M,-33.0,M,,*6F\r\n", - {'long': -93.33488008333333, 'lat': 45.013651016666664, - 'num_sat': 10, 'alt': 281.3, 'time': 1462302255.968968}), - ("$GPGGA,222237.0,4500.819061,N,09320.092805,W,1,09,0.8,281.3" + - ",M,-33.0,M,,*66\r\n", - {'long': -93.33488008333333, 'lat': 45.013651016666664, - 'num_sat': 9, 'alt': 281.3, 'time': 1462302255.968968}), - ] - - obj = nmea.NmeaStatus() - obj.date_time = True - obj.speed = True - obj.altitude = True - obj.coor_ddmm = False - obj.coor_dec = True - - for test in tests: - sentence = test[0] - expect = test[1] - now = 1462302255.968968 - - # print("inp [%s]" % sentence) - obj.start(now) - result = obj.parse_sentence(sentence) - self.assertTrue(result) - obj.publish() - - result = obj.get_attributes() - # print("out[{}]".format(result)) - self.assertEqual(result, expect) - - return - - def test_batch(self): - - obj = nmea.NmeaStatus() - obj.date_time = True - obj.speed = True - obj.altitude = True - obj.coor_ddmm = False - obj.coor_dec = True - - now = 1462302255.968968 - - # print("[%s]" % sentence) - obj.start(now) - - sentence = "$GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0," + \ - "353.2,020516,0.0,E,A*1D\r\n" - result = obj.parse_sentence(sentence) - self.assertTrue(result) - - sentence = "$GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A*23\r\n" - result = obj.parse_sentence(sentence) - self.assertTrue(result) - - sentence = "$GPGGA,222227.0,4500.819061,N,09320.092805,W,1,10,0.8" + \ - ",281.3,M,-33.0,M,,*6F\r\n" - result = obj.parse_sentence(sentence) - self.assertTrue(result) - - obj.publish() - expect = {'course': 353.2, 'long': -93.33488008333333, 'knots': 0.0, - 'lat': 45.013651016666664, 'valid': True, - 'gps_utc': 1462227747.0, 'num_sat': 10, 'alt': 281.3, - 'kmh': 0.0, 'time': 1462302255.968968} - - result = obj.get_attributes() - # print("out[{}]".format(result)) - self.assertEqual(result, expect) - - return - - def test_fix_time_sentence(self): - tests = [ - ("$GPGGA,222227.0,4500.819061,N,09320.092805,W,1,10,0.8,281." + - "3,M,-33.0,M,,*6F\r\n", - "$GPGGA,171614.0,4500.819061,N,09320.092805,W,1,10,0.8,281." + - "3,M,-33.0,M,,*6E\r\n"), - ("$GPRMC,222227.0,A,4500.819061,N,09320.092805,W,0.0,353.2," + - "020516,0.0,E,A*1D\r\n", - "$GPRMC,171614.0,A,4500.819061,N,09320.092805,W,0.0,353.2," + - "010516,0.0,E,A*1F\r\n"), - ("$GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A*23\r\n", - "$GPVTG,353.2,T,353.2,M,0.0,N,0.0,K,A*23\r\n"), - ("$PCPTI,IBR1100-702,222227,222227*33\r\n", - "$PCPTI,IBR1100-702,222227,222227*33\r\n"), - - # some bad tests - (None, TypeError, 0), - ("", IndexError, 0), - ] - - use_time = time.strptime("2016-05-01 17:16:14", "%Y-%m-%d %H:%M:%S") - - for test in tests: - source = test[0] - expect = test[1] - - if expect == TypeError: - with self.assertRaises(TypeError): - nmea.fix_time_sentence(source, use_time) - - elif expect == IndexError: - with self.assertRaises(IndexError): - nmea.fix_time_sentence(source, use_time) - - else: - result = nmea.fix_time_sentence(source, use_time) - self.assertEqual(result, expect) - - return - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_modbus_asc.py b/test/test_cplib_modbus_asc.py deleted file mode 100644 index ed6abe8e..00000000 --- a/test/test_cplib_modbus_asc.py +++ /dev/null @@ -1,229 +0,0 @@ -# Test the MODBUS ASCII module - -import unittest - -import cp_lib.modbus.transaction as ia_trans -import cp_lib.modbus.modbus_asc as mbus_asc -from cp_lib.modbus.transaction_modbus import ModbusTransaction, \ - ModbusBadForm, ModbusBadChecksum - - -class TestModbusAsc(unittest.TestCase): - - def test_code_ascii(self): - - tests = [ - {'src': b'\x01\x03', 'exp': 0xFC}, - {'src': b'\x01\x03\x00', 'exp': 0xFC}, - {'src': b'\x01\x03\x00\x00', 'exp': 0xFC}, - {'src': b'\x01\x03\x00\x00\x00', 'exp': 0xFC}, - {'src': b'\x01\x03\x00\x00\x00\x0A', 'exp': 0xF2}, - - {'src': b'', 'exp': ModbusBadForm}, - {'src': b'\x01', 'exp': ModbusBadForm}, - {'src': '\x01\x03\x00\x00\x00\x0A', 'exp': TypeError}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_asc.calc_checksum(test['src']) - - elif test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_asc.calc_checksum(test['src']) - - else: - result = mbus_asc.calc_checksum(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_encode_to_wire(self): - - tests = [ - {'src': b'\x01\x03\x00\x00\x00\x0A', - 'exp': b':01030000000AF2\r\n'}, - {'src': b'\x01\x03\x00\x00\x00', 'exp': b':0103000000FC\r\n'}, - {'src': b'\x01\x03\x00\x00', 'exp': b':01030000FC\r\n'}, - {'src': b'\x01\x03\x00', 'exp': b':010300FC\r\n'}, - {'src': b'\x01\x03', 'exp': b':0103FC\r\n'}, - - {'src': b'\x01', 'exp': ModbusBadForm}, - {'src': b'', 'exp': ModbusBadForm}, - - {'src': '\x01\x03\x00\x00\x00\x0A', 'exp': TypeError}, - {'src': None, 'exp': TypeError}, - {'src': [1, 3, 0, 0, 0, 0x0A], 'exp': TypeError}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_asc.encode_to_wire(test['src']) - - elif test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_asc.encode_to_wire(test['src']) - - else: - result = mbus_asc.encode_to_wire(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_decode_from_wire(self): - - # _estimate_length_request - - tests = [ - {'src': b':01030000000AF2\r\n', - 'exp': b'\x01\x03\x00\x00\x00\x0A'}, - {'src': b':0103000000FC\r\n', 'exp': b'\x01\x03\x00\x00\x00'}, - {'src': b':01030000FC\r\n', 'exp': b'\x01\x03\x00\x00'}, - {'src': b':010300FC\r\n', 'exp': b'\x01\x03\x00'}, - {'src': b':0103FC\r\n', 'exp': b'\x01\x03'}, - - # fiddle with the EOL - '\r\n' is SPEC, but allow basic combos - {'src': b':01030000000AF2\r', - 'exp': b'\x01\x03\x00\x00\x00\x0A'}, - {'src': b':01030000000AF2\n', - 'exp': b'\x01\x03\x00\x00\x00\x0A'}, - {'src': b':01030000000AF2', - 'exp': b'\x01\x03\x00\x00\x00\x0A'}, - - # bad CRC - {'src': b':01030000000AFF\r\n', - 'exp': ModbusBadChecksum}, - - # bad start - {'src': b'01030000000AF2\r\n', - 'exp': ModbusBadForm}, - - # an odd number of bytes - {'src': b':0103000000AF2\r\n', - 'exp': ModbusBadForm}, - - # non-hex bytes - {'src': b':0103JAN0000AF2\r\n', - 'exp': ModbusBadForm}, - - # bad types - {'src': ':01030000000AF2\r\n', 'exp': TypeError}, - {'src': 10, 'exp': TypeError}, - {'src': None, 'exp': TypeError}, - - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_asc.decode_from_wire(test['src']) - - elif test['exp'] == ModbusBadChecksum: - with self.assertRaises(ModbusBadChecksum): - mbus_asc.decode_from_wire(test['src']) - - elif test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_asc.decode_from_wire(test['src']) - - else: - result = mbus_asc.decode_from_wire(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_request(self): - - tests = [ - {'src': b':01030000000AF2\r\n', - 'raw': b'\x01\x03\x00\x00\x00\x0A', - 'exp': b':01030000000AF2\r\n'}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - msg = test['raw'][1:] - - result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBASC) - # print("obj pst:{}".format(obj.attrib)) - self.assertTrue(result) - self.assertEqual(obj[obj.KEY_REQ_RAW], test['src']) - self.assertEqual(obj[obj.KEY_REQ_PROTOCOL], - ia_trans.IA_PROTOCOL_MBASC) - self.assertNotIn(obj.KEY_SRC_ID, obj.attrib) - self.assertEqual(obj[obj.KEY_REQ], msg) - - result = obj.get_request() - self.assertEqual(result, test['exp']) - - return - - def test_end_of_message(self): - - tests = [ - {'src': b':01030000000AF2\r\n', - 'result': [b':01030000000AF2\r\n'], 'extra': None}, - - {'src': b'', - 'result': None, 'extra': None}, - {'src': b':', - 'result': [], 'extra': b':'}, - {'src': b':0', - 'result': [], 'extra': b':0'}, - {'src': b':01', - 'result': [], 'extra': b':01'}, - {'src': b':010300', - 'result': [], 'extra': b':010300'}, - {'src': b':01030000000', - 'result': [], 'extra': b':01030000000'}, - {'src': b':01030000000AF', - 'result': [], 'extra': b':01030000000AF'}, - {'src': b':01030000000AF2', - 'result': [], 'extra': b':01030000000AF2'}, - {'src': b':01030000000AF2\r', - 'result': [], 'extra': b':01030000000AF2\r'}, - - {'src': b':01030000000AF2\r\n:01030000000AF2', - 'result': [b':01030000000AF2\r\n'], - 'extra': b':01030000000AF2'}, - {'src': b':01030000000AF2\r\n:01030000000AF2\r', - 'result': [b':01030000000AF2\r\n'], - 'extra': b':01030000000AF2\r'}, - {'src': b':01030000000AF2\r\n:01030000000AF2\r\n', - 'result': [b':01030000000AF2\r\n', b':01030000000AF2\r\n'], - 'extra': None}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - result, extra = mbus_asc.test_end_of_message(test['src']) - self.assertEqual(result, test['result']) - self.assertEqual(extra, test['extra']) - - return - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_modbus_rtu.py b/test/test_cplib_modbus_rtu.py deleted file mode 100644 index eeee2128..00000000 --- a/test/test_cplib_modbus_rtu.py +++ /dev/null @@ -1,254 +0,0 @@ -# Test the MODBUS RTU module - -import unittest - -import cp_lib.modbus.transaction as ia_trans -import cp_lib.modbus.modbus_rtu as mbus_rtu -from cp_lib.modbus.transaction_modbus import ModbusTransaction, \ - ModbusBadForm, ModbusBadChecksum - - -class TestModbusRtu(unittest.TestCase): - - def test_checksum(self): - - tests = [ - {'src': b"\xEA\x03\x00\x00\x00\x64", 'exp': 0x3A53}, - {'src': b"\x4b\x03\x00\x2c\x00\x37", 'exp': 0xbfcb}, - {'src': b"\x0d\x01\x00\x62\x00\x33", 'exp': 0x0ddd}, - - {'src': b'', 'exp': ModbusBadForm}, - {'src': b'\x01', 'exp': ModbusBadForm}, - {'src': '\x01\x03\x00\x00\x00\x0A', 'exp': TypeError}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_rtu.calc_checksum(test['src']) - - elif test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_rtu.calc_checksum(test['src']) - - else: - result = mbus_rtu.calc_checksum(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_encode_to_wire(self): - - tests = [ - {'src': b'\x01\x03\x00\x00\x00\x0A', - 'exp': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD'}, - {'src': b'\x01\x03\x00\x00\x00', - 'exp': b'\x01\x03\x00\x00\x00\x19\x84'}, - - {'src': b'\x01', 'exp': ModbusBadForm}, - {'src': b'', 'exp': ModbusBadForm}, - - {'src': '\x01\x03\x00\x00\x00\x0A', 'exp': TypeError}, - {'src': None, 'exp': TypeError}, - {'src': [1, 3, 0, 0, 0, 0x0A], 'exp': TypeError}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_rtu.encode_to_wire(test['src']) - - elif test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_rtu.encode_to_wire(test['src']) - - else: - result = mbus_rtu.encode_to_wire(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_decode_from_wire(self): - - tests = [ - {'exp': b'\x01\x03\x00\x00\x00\x0A', - 'src': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD'}, - {'exp': b'\x01\x03\x00\x00\x00', - 'src': b'\x01\x03\x00\x00\x00\x19\x84'}, - - # bad CRC - {'src': b'\x01\x03\x00\x00\x00\x0A\xC5\x00', - 'exp': ModbusBadChecksum}, - - # bad types - {'src': ':01030000000AF2\r\n', 'exp': TypeError}, - {'src': 10, 'exp': TypeError}, - {'src': None, 'exp': TypeError}, - - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - if test['exp'] == ModbusBadForm: - with self.assertRaises(ModbusBadForm): - mbus_rtu.decode_from_wire(test['src']) - - elif test['exp'] == ModbusBadChecksum: - with self.assertRaises(ModbusBadChecksum): - mbus_rtu.decode_from_wire(test['src']) - - elif test['exp'] == TypeError: - with self.assertRaises(TypeError): - mbus_rtu.decode_from_wire(test['src']) - - else: - result = mbus_rtu.decode_from_wire(test['src']) - self.assertEqual(result, test['exp']) - - return - - def test_request(self): - - tests = [ - {'src': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD', - 'raw': b'\x01\x03\x00\x00\x00\x0A', - 'exp': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD'}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - # this won't have the UID in first place - moved to [KEY_SRC_ID] - msg = test['raw'][1:] - - result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBRTU) - # print("obj pst:{}".format(obj.attrib)) - self.assertTrue(result) - self.assertEqual(obj[obj.KEY_REQ_RAW], test['src']) - self.assertEqual(obj[obj.KEY_REQ_PROTOCOL], - ia_trans.IA_PROTOCOL_MBRTU) - self.assertNotIn(obj.KEY_SRC_ID, obj.attrib) - self.assertEqual(obj[obj.KEY_REQ], msg) - - result = obj.get_request() - self.assertEqual(result, test['exp']) - - return - - def test_end_of_message_request(self): - - tests = [ - # the common functions - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x02\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x02\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x03\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x03\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x04\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x04\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x05\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x05\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x06\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x06\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x0F\x00\x00\x00\x0A\x02\xAA\xBB\xCC\xDD', - 'result': [b'\x01\x0F\x00\x00\x00\x0A\x02\xAA\xBB\xCC\xDD'], - 'extra': None}, - {'src': b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB\xCC\xDD', - 'result': [b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB\xCC\xDD'], - 'extra': None}, - - # build up - {'src': b'\x01', - 'result': [], 'extra': b'\x01'}, - {'src': b'\x01\x01', - 'result': [], 'extra': b'\x01\x01'}, - {'src': b'\x01\x01\x00', - 'result': [], 'extra': b'\x01\x01\x00'}, - {'src': b'\x01\x01\x00\x00', - 'result': [], 'extra': b'\x01\x01\x00\x00'}, - {'src': b'\x01\x01\x00\x00\x00', - 'result': [], 'extra': b'\x01\x01\x00\x00\x00'}, - {'src': b'\x01\x01\x00\x00\x00\x0A', - 'result': [], 'extra': b'\x01\x01\x00\x00\x00\x0A'}, - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA', - 'result': [], 'extra': b'\x01\x01\x00\x00\x00\x0A\xAA'}, - - {'src': b'\x01', 'result': [], 'extra': b'\x01'}, - {'src': b'\x01\x10', 'result': [], 'extra': b'\x01\x10'}, - {'src': b'\x01\x10\x00', 'result': [], 'extra': b'\x01\x10\x00'}, - {'src': b'\x01\x10\x00\x00', - 'result': [], 'extra': b'\x01\x10\x00\x00'}, - {'src': b'\x01\x10\x00\x00\x00', - 'result': [], 'extra': b'\x01\x10\x00\x00\x00'}, - {'src': b'\x01\x10\x00\x00\x00\x0A', - 'result': [], 'extra': b'\x01\x10\x00\x00\x00\x0A'}, - {'src': b'\x01\x10\x00\x00\x00\x0A\x02', - 'result': [], 'extra': b'\x01\x10\x00\x00\x00\x0A\x02'}, - {'src': b'\x01\x10\x00\x00\x00\x0A\x02\xAA', - 'result': [], 'extra': b'\x01\x10\x00\x00\x00\x0A\x02\xAA'}, - {'src': b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB', - 'result': [], 'extra': b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB'}, - {'src': b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB\xCC', - 'result': [], - 'extra': b'\x01\x10\x00\x00\x00\x0A\x02\xAA\xBB\xCC'}, - - # multiple lines - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB' + - b'\x01\x01\x00\x00\x00\x0A\xAA\xBB', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB', - b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], 'extra': None}, - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB' + - b'\x01\x01\x00\x00\x00\x0A\xAA\xBB\x05', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB', - b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], - 'extra': b'\x05'}, - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB' + - b'\x01\x01\x00\x00\x00\x0A\xAA', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], - 'extra': b'\x01\x01\x00\x00\x00\x0A\xAA'}, - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB' + - b'\x01\x01', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], - 'extra': b'\x01\x01'}, - {'src': b'\x01\x01\x00\x00\x00\x0A\xAA\xBB' + - b'\x01', - 'result': [b'\x01\x01\x00\x00\x00\x0A\xAA\xBB'], - 'extra': b'\x01'}, - - # unknown functions - {'src': b'\x01\x64\x00\xAA\xBB', - 'result': [b'\x01\x64\x00\xAA\xBB'], 'extra': None}, - - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - result, extra = mbus_rtu.test_end_of_message(test['src'], - is_request=True) - self.assertEqual(result, test['result']) - self.assertEqual(extra, test['extra']) - - return - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_modbus_tcp.py b/test/test_cplib_modbus_tcp.py deleted file mode 100644 index 84438f0b..00000000 --- a/test/test_cplib_modbus_tcp.py +++ /dev/null @@ -1,225 +0,0 @@ -# Test the MODBUS TCP module - -import unittest - -import cp_lib.modbus.transaction as ia_trans -import cp_lib.modbus.modbus_tcp as mbus_tcp -from cp_lib.modbus.transaction_modbus import ModbusTransaction - - -class TestModbusTcp(unittest.TestCase): - - def test_request(self): - - tests = [ - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'seq': b'\xAB\xCD', - 'raw': b'\x01\x03\x00\x00\x00\x0A'}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - # this won't have the UID in first place - moved to [KEY_SRC_ID] - msg = test['raw'][1:] - - result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBTCP) - # print("obj pst:{}".format(obj.attrib)) - self.assertTrue(result) - self.assertEqual(obj[obj.KEY_REQ_RAW], test['src']) - self.assertEqual(obj[obj.KEY_REQ_PROTOCOL], - ia_trans.IA_PROTOCOL_MBTCP) - self.assertNotIn(obj.KEY_SRC_ID, obj.attrib) - self.assertEqual(obj[obj.KEY_REQ], msg) - self.assertEqual(obj[obj.KEY_SRC_SEQ], test['seq']) - - result = obj.get_request() - self.assertEqual(result, test['src']) - - return - - def test_sequence_number(self): - - tests = [ - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'seq': b'\xAB\xCD', - 'raw': b'\x01\x03\x00\x00\x00\x0A'}, - ] - - obj = ModbusTransaction() - - source = b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - expect = source - obj.set_request(source, ia_trans.IA_PROTOCOL_MBTCP) - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new 2-byte sequence - obj[obj.KEY_SRC_SEQ] = b'\xFF\xEE' - expect = b'\xFF\xEE\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new 1-byte sequence - expect padding with 0x01 - obj[obj.KEY_SRC_SEQ] = b'\x44' - expect = b'\x44\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new n byte sequence - expect padding with 0x01 - obj[obj.KEY_SRC_SEQ] = b'' - expect = b'\x01\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new 3 byte sequence - expect truncate to 2 bytes - obj[obj.KEY_SRC_SEQ] = b'\x05\x06\x07' - expect = b'\x05\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new int sequence - obj[obj.KEY_SRC_SEQ] = 0 - expect = b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - obj[obj.KEY_SRC_SEQ] = 0x01 - expect = b'\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new int sequence - saved as big-endian - obj[obj.KEY_SRC_SEQ] = 0x0306 - expect = b'\x03\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - # swap in a new int sequence - excess is truncated - obj[obj.KEY_SRC_SEQ] = 0xFF0306 - expect = b'\x03\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' - result = obj.get_request() - self.assertEqual(result, expect) - - return - - def test_all_request(self): - - tests = [ - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'raw': b'\x01\x03\x00\x00\x00\x0A', - 'asc': b':01030000000AF2\r\n', - 'rtu': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD', - 'tcp': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBTCP) - self.assertEqual(obj[obj.KEY_REQ_PROTOCOL], - ia_trans.IA_PROTOCOL_MBTCP) - - # default will be TCP - result = obj.get_request() - self.assertEqual(result, test['tcp']) - - # confirm we can pull back as any of the three - result = obj.get_request(ia_trans.IA_PROTOCOL_MBASC) - self.assertEqual(result, test['asc']) - - result = obj.get_request(ia_trans.IA_PROTOCOL_MBRTU) - self.assertEqual(result, test['rtu']) - - result = obj.get_request(ia_trans.IA_PROTOCOL_MBTCP) - self.assertEqual(result, test['tcp']) - - return - - def test_end_of_message(self): - - tests = [ - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'result': [b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'], - 'extra': None}, - - {'src': b'', - 'result': [], 'extra': b''}, - {'src': b'\xAB', - 'result': [], 'extra': b'\xAB'}, - {'src': b'\xAB\xCD', - 'result': [], 'extra': b'\xAB\xCD'}, - {'src': b'\xAB\xCD\x00', - 'result': [], 'extra': b'\xAB\xCD\x00'}, - {'src': b'\xAB\xCD\x00\x00', - 'result': [], 'extra': b'\xAB\xCD\x00\x00'}, - {'src': b'\xAB\xCD\x00\x00\x00', - 'result': [], 'extra': b'\xAB\xCD\x00\x00\x00'}, - {'src': b'\xAB\xCD\x00\x00\x00\x06', - 'result': [], 'extra': b'\xAB\xCD\x00\x00\x00\x06'}, - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03', - 'result': [], - 'extra': b'\xAB\xCD\x00\x00\x00\x06\x01\x03'}, - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00', - 'result': [], - 'extra': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00'}, - - # here we have bad header values (protocol vers or length) - {'src': b'\xAB\xCD\x01\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'result': None, 'extra': None}, - {'src': b'\xAB\xCD\x00\x02\x00\x06\x01\x03\x00\x00\x00\x0A', - 'result': None, 'extra': None}, - {'src': b'\xAB\xCD\x00\x00\x03\x06\x01\x03\x00\x00\x00\x0A', - 'result': None, 'extra': None}, - {'src': b'\xAB\xCD\x00\x00\x00\x00', - 'result': None, 'extra': None}, - {'src': b'\xAB\xCD\x00\x00\x00\x01\x01', - 'result': None, 'extra': None}, - - # this isn't a valid PDU, but follows the rules! - {'src': b'\xAB\xCD\x00\x00\x00\x02\x01\x03', - 'result': [b'\xAB\xCD\x00\x00\x00\x02\x01\x03'], 'extra': None}, - - # try the multiple messages - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' + - b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - 'result': [b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A', - b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'], - 'extra': None}, - - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' + - b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00', - 'result': [b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'], - 'extra': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00'}, - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' + - b'\xAB\xCD\x00', - 'result': [b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'], - 'extra': b'\xAB\xCD\x00'}, - {'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A' + - b'\xAB', - 'result': [b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'], - 'extra': b'\xAB'}, - ] - - for test in tests: - # loop through all tests - - # print("test:{}".format(test)) - - result, extra = mbus_tcp.test_end_of_message(test['src']) - self.assertEqual(result, test['result']) - self.assertEqual(extra, test['extra']) - - return - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_modbus_trans_modbus.py b/test/test_cplib_modbus_trans_modbus.py deleted file mode 100644 index c94008d4..00000000 --- a/test/test_cplib_modbus_trans_modbus.py +++ /dev/null @@ -1,113 +0,0 @@ -# Test the MODBUS transaction module - -import unittest - -from cp_lib.modbus.transaction_modbus import ModbusTransaction - - -class TestModbusTrans(unittest.TestCase): - - def test_estimate_length_request(self): - - # _estimate_length_request - - tests = [ - {'src': b'\x01\x01\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x01\x00\x01\x00\x0A\x44\x2A', 'exp': 6}, - {'src': b'\x01\x02\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x03\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x04\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x05\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x06\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x0F\x02\x00\x01', 'exp': 5}, - {'src': b'\x01\x10\x02\x00\x01', 'exp': 5}, - - {'src': '\x01\x01\x00\x01\x00\x0A', 'exp': 6}, - - {'src': b'\x01\x00\x02\x00\x01', 'exp': ValueError}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - if test['exp'] == ValueError: - with self.assertRaises(ValueError): - obj._estimate_length_request(test['src']) - - pass - else: - result = obj._estimate_length_request(test['src']) - self.assertEqual(result, test['exp']) - - return - -# - - def test_parse_rtu(self): - - # _estimate_length_request - - tests = [ - {'src': b'\x01\x01\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x01\x00\x01\x00\x0A\x44\x2A', 'exp': 6}, - {'src': b'\x01\x02\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x03\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x04\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x05\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x06\x00\x01\x00\x0A', 'exp': 6}, - {'src': b'\x01\x0F\x02\x00\x01', 'exp': 5}, - {'src': b'\x01\x10\x02\x00\x01', 'exp': 5}, - - {'src': '\x01\x01\x00\x01\x00\x0A', 'exp': 6}, - - {'src': b'\x01\x00\x02\x00\x01', 'exp': ValueError}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - if test['exp'] == ValueError: - with self.assertRaises(ValueError): - obj._parse_rtu(test['src']) - - pass - else: - obj._parse_rtu(test['src']) - # self.assertEqual(result, test['exp']) - self.assertEqual(obj['cooked_protocol'], - obj.IA_PROTOCOL_MBRTU) - - return - - def test_code_ascii(self): - - # _estimate_length_request - - tests = [ - {'src': b':01010001000A66\r\n', 'exp': 6}, - ] - - obj = ModbusTransaction() - - for test in tests: - # loop through all tests - - if test['exp'] == ValueError: - with self.assertRaises(ValueError): - obj._parse_rtu(test['src']) - - pass - else: - obj._parse_rtu(test['src']) - self.assertEqual(result, test['exp']) - self.assertEqual(obj['cooked_protocol'], - obj.IA_PROTOCOL_MBRTU) - - return - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_parse_data.py b/test/test_cplib_parse_data.py deleted file mode 100644 index 9272d103..00000000 --- a/test/test_cplib_parse_data.py +++ /dev/null @@ -1,243 +0,0 @@ -# Test the PARSE DATA module - -import unittest - -from cp_lib.parse_data import isolate_numeric_from_string, parse_float, \ - parse_float_string, parse_integer, \ - parse_integer_string, parse_boolean, parse_none - - -class TestParseData(unittest.TestCase): - - def test_isolate_numeric(self): - - self.assertEqual(isolate_numeric_from_string(" 0"), "0") - self.assertEqual(isolate_numeric_from_string("hello0"), "0") - self.assertEqual(isolate_numeric_from_string(" 99"), "99") - self.assertEqual(isolate_numeric_from_string(" -99rate"), "-99") - self.assertEqual(isolate_numeric_from_string(" 0.0 "), "0.0") - self.assertEqual(isolate_numeric_from_string(" 99.9% "), "99.9") - self.assertEqual(isolate_numeric_from_string(" -99.9 \n"), "-99.9") - - self.assertEqual(isolate_numeric_from_string("' 0.0"), "0.0") - self.assertEqual(isolate_numeric_from_string("\"99.9"), "99.9") - self.assertEqual(isolate_numeric_from_string("[-99.9"), "-99.9") - self.assertEqual(isolate_numeric_from_string("(123,"), "123") - self.assertEqual(isolate_numeric_from_string("' 0.0'"), "0.0") - self.assertEqual(isolate_numeric_from_string("\"99.9\""), "99.9") - self.assertEqual(isolate_numeric_from_string("[-99.9]"), "-99.9") - self.assertEqual(isolate_numeric_from_string("(123)"), "123") - - # only the first numeric sequence returned! - self.assertEqual(isolate_numeric_from_string("1, 2, 3"), "1") - self.assertEqual(isolate_numeric_from_string("rate = 123 seconds"), - "123") - self.assertEqual(isolate_numeric_from_string("rate = 123 and 99"), - "123") - - with self.assertRaises(TypeError): - # value must be str - isolate_numeric_from_string(None) - isolate_numeric_from_string(99) - isolate_numeric_from_string(99.9) - - with self.assertRaises(ValueError): - # value must have number str - isolate_numeric_from_string("") - isolate_numeric_from_string(" \n") - isolate_numeric_from_string("hello") - - return - - def test_parse_integer(self): - - self.assertEqual(parse_integer(None, none_is_zero=False), None) - self.assertEqual(parse_integer(0, none_is_zero=False), 0) - self.assertEqual(parse_integer(99, none_is_zero=False), 99) - self.assertEqual(parse_integer(-99, none_is_zero=False), -99) - self.assertEqual(parse_integer(0.0, none_is_zero=False), 0.0) - self.assertEqual(parse_integer(99.0, none_is_zero=False), 99.0) - self.assertEqual(parse_integer(-99.0, none_is_zero=False), -99.0) - self.assertEqual(parse_integer("0", none_is_zero=False), 0) - self.assertEqual(parse_integer("99", none_is_zero=False), 99) - self.assertEqual(parse_integer("-99", none_is_zero=False), -99) - - self.assertEqual(parse_integer(None, none_is_zero=True), 0) - - self.assertEqual(parse_integer(" 0", none_is_zero=False), 0) - self.assertEqual(parse_integer(" 99", none_is_zero=False), 99) - self.assertEqual(parse_integer(" -99", none_is_zero=False), -99) - self.assertEqual(parse_integer(" 0 ", none_is_zero=False), 0) - self.assertEqual(parse_integer(" 99 ", none_is_zero=False), 99) - self.assertEqual(parse_integer(" -99 ", none_is_zero=False), -99) - self.assertEqual(parse_integer(" 0\n", none_is_zero=False), 0) - self.assertEqual(parse_integer(" 99\n", none_is_zero=False), 99) - self.assertEqual(parse_integer(" -99\n", none_is_zero=False), -99) - - with self.assertRaises(ValueError): - parse_integer("\"99\"") - parse_integer("[99]") - - return - - def test_parse_integer_string(self): - - self.assertEqual(parse_integer_string("0"), 0) - self.assertEqual(parse_integer_string("99"), 99) - self.assertEqual(parse_integer_string("-99"), -99) - self.assertEqual(parse_integer_string("0.0"), 0) - self.assertEqual(parse_integer_string("99.2"), 99) - self.assertEqual(parse_integer_string("-99.8"), -100) - self.assertEqual(parse_integer_string(" 0.0"), 0) - self.assertEqual(parse_integer_string(" 99.0\n"), 99) - self.assertEqual(parse_integer_string("a -99.0"), -99) - self.assertEqual(parse_integer_string("\"0.0\""), 0) - self.assertEqual(parse_integer_string("[99.0]"), 99) - self.assertEqual(parse_integer_string("(-99.0)"), -99) - - with self.assertRaises(TypeError): - parse_integer_string(None) - parse_integer_string(99) - - return - - def test_parse_float(self): - - self.assertEqual(parse_float(None, none_is_zero=True), 0.0) - self.assertEqual(parse_float(None, none_is_zero=False), None) - - self.assertEqual(parse_float(99), 99.0) - self.assertEqual(parse_float(-99), -99.0) - self.assertEqual(parse_float(0.0), 0.0) - self.assertEqual(parse_float(99.0), 99) - self.assertEqual(parse_float(-99.0), -99) - self.assertEqual(parse_float("0"), 0.0) - self.assertEqual(parse_float("99"), 99.0) - self.assertEqual(parse_float("-99"), -99.0) - self.assertEqual(parse_float("0.0"), 0.0) - self.assertEqual(parse_float("99.0"), 99.0) - self.assertEqual(parse_float("-99.0"), -99.0) - - return - - def test_parse_float_string(self): - - self.assertEqual(parse_float_string("0"), 0.0) - self.assertEqual(parse_float_string("99"), 99.0) - self.assertEqual(parse_float_string("-99"), -99.0) - self.assertEqual(parse_float_string("0.0"), 0.0) - self.assertEqual(parse_float_string("99.0"), 99.0) - self.assertEqual(parse_float_string("-99.0"), -99.0) - self.assertEqual(parse_float_string(" 0.0"), 0.0) - self.assertEqual(parse_float_string(" 99.0"), 99.0) - self.assertEqual(parse_float_string("a -99.0"), -99.0) - self.assertEqual(parse_float_string("\"0.0\""), 0.0) - self.assertEqual(parse_float_string("[99.0]"), 99.0) - self.assertEqual(parse_float_string("(-99.0)"), -99.0) - - with self.assertRaises(TypeError): - parse_float_string(None) - parse_float_string(99) - parse_float_string(99.00) - - def test_parse_boolean(self): - - self.assertEqual(parse_boolean(None, none_is_false=True), False) - - # typical bool(value) - self.assertEqual(parse_boolean(-99), True) - self.assertEqual(parse_boolean(-1), True) - self.assertEqual(parse_boolean(0), False) - self.assertEqual(parse_boolean(1), True) - self.assertEqual(parse_boolean(99), True) - - # handle str - self.assertEqual(parse_boolean("0"), False) - self.assertEqual(parse_boolean(" 0 "), False) - self.assertEqual(parse_boolean("1"), True) - self.assertEqual(parse_boolean(" 1 "), True) - - self.assertEqual(parse_boolean(" F "), False) - self.assertEqual(parse_boolean(" f "), False) - self.assertEqual(parse_boolean("t"), True) - self.assertEqual(parse_boolean(" T"), True) - - self.assertEqual(parse_boolean("FALSE"), False) - self.assertEqual(parse_boolean("False"), False) - self.assertEqual(parse_boolean("false"), False) - self.assertEqual(parse_boolean("TRUE"), True) - self.assertEqual(parse_boolean("True"), True) - self.assertEqual(parse_boolean("true"), True) - - self.assertEqual(parse_boolean("OFF"), False) - self.assertEqual(parse_boolean("Off"), False) - self.assertEqual(parse_boolean("off"), False) - self.assertEqual(parse_boolean("ON"), True) - self.assertEqual(parse_boolean("On"), True) - self.assertEqual(parse_boolean("on"), True) - - self.assertEqual(parse_boolean("disable"), False) - self.assertEqual(parse_boolean("enable"), True) - - # handle bytes - self.assertEqual(parse_boolean(b"0"), False) - self.assertEqual(parse_boolean(b" 0 "), False) - self.assertEqual(parse_boolean(b"1"), True) - self.assertEqual(parse_boolean(b" 1 "), True) - - self.assertEqual(parse_boolean(b" F "), False) - self.assertEqual(parse_boolean(b" f "), False) - self.assertEqual(parse_boolean(b"t"), True) - self.assertEqual(parse_boolean(b" T"), True) - - self.assertEqual(parse_boolean(b"FALSE"), False) - self.assertEqual(parse_boolean(b"False"), False) - self.assertEqual(parse_boolean(b"false"), False) - self.assertEqual(parse_boolean(b"TRUE"), True) - self.assertEqual(parse_boolean(b"True"), True) - self.assertEqual(parse_boolean(b"true"), True) - - self.assertEqual(parse_boolean(b"OFF"), False) - self.assertEqual(parse_boolean(b"Off"), False) - self.assertEqual(parse_boolean(b"off"), False) - self.assertEqual(parse_boolean(b"ON"), True) - self.assertEqual(parse_boolean(b"On"), True) - self.assertEqual(parse_boolean(b"on"), True) - - self.assertEqual(parse_boolean(b"disable"), False) - self.assertEqual(parse_boolean(b"enable"), True) - - with self.assertRaises(ValueError): - parse_boolean(None, none_is_false=False) - parse_boolean("happy") - parse_boolean([1, 2, 3]) - - return - - def test_parse_none(self): - - self.assertEqual(parse_none(None), None) - - self.assertEqual(parse_none(""), None) - self.assertEqual(parse_none("NONE"), None) - self.assertEqual(parse_none("None"), None) - self.assertEqual(parse_none("none"), None) - self.assertEqual(parse_none("NULL"), None) - self.assertEqual(parse_none("Null"), None) - self.assertEqual(parse_none("null"), None) - self.assertEqual(parse_none(" none"), None) - self.assertEqual(parse_none("\'none\'"), None) - - self.assertEqual(parse_none(b""), None) - self.assertEqual(parse_none(b"None"), None) - self.assertEqual(parse_none(b"Null"), None) - - with self.assertRaises(ValueError): - parse_none(10) - parse_none("happy") - parse_none([1, 2, 3]) - - return - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_parse_duration.py b/test/test_cplib_parse_duration.py deleted file mode 100644 index 21211d31..00000000 --- a/test/test_cplib_parse_duration.py +++ /dev/null @@ -1,307 +0,0 @@ -# Test the PARSE DURATION module - -import unittest -import random - -from cp_lib.parse_duration import TimeDuration - - -class TestParseDuration(unittest.TestCase): - - def test_parse_time_duration(self): - - obj = TimeDuration() - - self.assertEqual(obj.parse_time_duration_to_seconds(1), 1.0) - self.assertEqual(obj.get_period_as_string(), "1 sec") - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1'), 1.0) - self.assertEqual(obj.get_period_as_string(), "1 sec") - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('01'), 1.0) - self.assertEqual(obj.get_period_as_string(), "1 sec") - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('0x01'), 1.0) - self.assertEqual(obj.get_period_as_string(), "1 sec") - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 ms'), 0.001) - self.assertEqual(obj.get_period_as_string(), '1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 msec'), 0.001) - self.assertEqual(obj.get_period_as_string(), '1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 sec'), 1.0) - self.assertEqual(obj.get_period_as_string(), '1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(b'1 sec'), 1.0) - self.assertEqual(obj.get_period_as_string(), '1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 second'), 1.0) - self.assertEqual(obj.get_period_as_string(), '1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 min'), 60.0) - self.assertEqual(obj.get_period_as_string(), '1 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 minute'), 60.0) - self.assertEqual(obj.get_period_as_string(), '1 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 minutes'), 60.0) - self.assertEqual(obj.get_period_as_string(), '1 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 hr'), 3600.0) - self.assertEqual(obj.get_period_as_string(), '1 hr') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 HR'), 3600.0) - self.assertEqual(obj.get_period_as_string(), '1 hr') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 Hours'), 3600.0) - self.assertEqual(obj.get_period_as_string(), '1 hr') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 day'), 86400.0) - self.assertEqual(obj.get_period_as_string(), '1 day') - - # note: these handled, but have NO 'seconds' result - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 month'), None) - self.assertEqual(obj.get_period_as_string(), '1 mon') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('1 year'), None) - self.assertEqual(obj.get_period_as_string(), '1 yr') - - # repeat with more than 1 - a random value - seed = random.randint(101, 999) - source = "{0} ms".format(seed) - expect_sec = seed * 0.001 - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - seed = random.randint(2, 59) - source = "{0} sec".format(seed) - expect_sec = seed * 1.0 - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - seed = random.randint(2, 59) - source = "{0} min".format(seed) - expect_sec = seed * 60.0 - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - seed = random.randint(2, 23) - source = "{0} hr".format(seed) - expect_sec = seed * 3600.0 - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - seed = random.randint(2, 9) - source = "{0} day".format(seed) - expect_sec = seed * 86400.0 - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - # note: these handled, but have NO 'seconds' result - seed = random.randint(2, 9) - source = "{0} mon".format(seed) - expect_sec = None - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - seed = random.randint(2, 9) - source = "{0} yr".format(seed) - expect_sec = None - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(source), - expect_sec) - self.assertEqual(obj.get_period_as_string(), source) - - return - - def test_parse_time_duration_plus_minus(self): - - obj = TimeDuration() - # check the signs - +/- to allow things like "do 5 minutes before X" - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 ms'), 0.001) - self.assertEqual(obj.get_period_as_string(), '1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-1 ms'), -0.001) - self.assertEqual(obj.get_period_as_string(), '-1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 sec'), 1.0) - self.assertEqual(obj.get_period_as_string(), '1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(b'-1 sec'), -1.0) - self.assertEqual(obj.get_period_as_string(), '-1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 min'), 60.0) - self.assertEqual(obj.get_period_as_string(), '1 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-5 min'), -300.0) - self.assertEqual(obj.get_period_as_string(), '-5 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+2 hr'), 7200.0) - self.assertEqual(obj.get_period_as_string(), '2 hr') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-3 hr'), -10800.0) - self.assertEqual(obj.get_period_as_string(), '-3 hr') - - # confirm the UTC 'decoration' is ignored, - # including ('z', 'zulu', 'gm', 'utc', 'uct') - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 ms utc'), - 0.001) - self.assertEqual(obj.get_period_as_string(), '1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-1 ms UTC'), - -0.001) - self.assertEqual(obj.get_period_as_string(), '-1 ms') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 sec z'), - 1.0) - self.assertEqual(obj.get_period_as_string(), '1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds(b'-1 sec Z'), -1.0) - self.assertEqual(obj.get_period_as_string(), '-1 sec') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+1 min gm'), 60.0) - self.assertEqual(obj.get_period_as_string(), '1 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-5 min GM'), - -300.0) - self.assertEqual(obj.get_period_as_string(), '-5 min') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('+2 hr uct'), - 7200.0) - self.assertEqual(obj.get_period_as_string(), '2 hr') - - obj.reset() - self.assertEqual(obj.parse_time_duration_to_seconds('-3 hr zulu'), - -10800.0) - self.assertEqual(obj.get_period_as_string(), '-3 hr') - - return - - def test_utc_decoration(self): - - obj = TimeDuration() - - self.assertFalse(obj._decode_utc_element('1 ms')) - self.assertTrue(obj._decode_utc_element('1 ms utc')) - - self.assertFalse(obj._decode_utc_element('1 sec')) - self.assertTrue(obj._decode_utc_element('1 sec ZulU')) - - self.assertFalse(obj._decode_utc_element('1 min')) - self.assertTrue(obj._decode_utc_element('1 min GM')) - - self.assertFalse(obj._decode_utc_element('1 hr')) - self.assertTrue(obj._decode_utc_element('1 hr Z')) - - self.assertFalse(obj._decode_utc_element('1 day')) - self.assertTrue(obj._decode_utc_element('1 day UCT')) - - self.assertFalse(obj._decode_utc_element('1 mon')) - self.assertTrue(obj._decode_utc_element('1 mon UTC')) - - return - - def test_tag_decode(self): - - obj = TimeDuration() - - self.assertEqual(obj.decode_time_tag('ms'), obj.DURATION_MSEC) - self.assertEqual(obj.decode_time_tag('msec'), obj.DURATION_MSEC) - self.assertEqual(obj.decode_time_tag('millisecond'), obj.DURATION_MSEC) - self.assertEqual(obj.decode_time_tag('milliseconds'), - obj.DURATION_MSEC) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_MSEC), 'ms') - - self.assertEqual(obj.decode_time_tag('sec'), obj.DURATION_SECOND) - self.assertEqual(obj.decode_time_tag('second'), obj.DURATION_SECOND) - self.assertEqual(obj.decode_time_tag('seconds'), obj.DURATION_SECOND) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_SECOND), 'sec') - self.assertEqual(obj.get_tag_as_string(0), 'sec') - - self.assertEqual(obj.decode_time_tag('min'), obj.DURATION_MINUTE) - self.assertEqual(obj.decode_time_tag('minute'), obj.DURATION_MINUTE) - self.assertEqual(obj.decode_time_tag('minutes'), obj.DURATION_MINUTE) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_MINUTE), 'min') - - self.assertEqual(obj.decode_time_tag('hr'), obj.DURATION_HOUR) - self.assertEqual(obj.decode_time_tag('hour'), obj.DURATION_HOUR) - self.assertEqual(obj.decode_time_tag('hours'), obj.DURATION_HOUR) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_HOUR), 'hr') - - self.assertEqual(obj.decode_time_tag('dy'), obj.DURATION_DAY) - self.assertEqual(obj.decode_time_tag('day'), obj.DURATION_DAY) - self.assertEqual(obj.decode_time_tag('days'), obj.DURATION_DAY) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_DAY), 'day') - - self.assertEqual(obj.decode_time_tag('mon'), obj.DURATION_MONTH) - self.assertEqual(obj.decode_time_tag('month'), obj.DURATION_MONTH) - self.assertEqual(obj.decode_time_tag('months'), obj.DURATION_MONTH) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_MONTH), 'mon') - - self.assertEqual(obj.decode_time_tag('yr'), obj.DURATION_YEAR) - self.assertEqual(obj.decode_time_tag('year'), obj.DURATION_YEAR) - self.assertEqual(obj.decode_time_tag('years'), obj.DURATION_YEAR) - self.assertEqual(obj.get_tag_as_string(obj.DURATION_YEAR), 'yr') - - obj.reset() - with self.assertRaises(ValueError): - obj.get_tag_as_string(-1) - obj.get_tag_as_string(7) - obj.decode_time_tag('homey') - - with self.assertRaises(TypeError): - obj.get_tag_as_string('hello') - obj.decode_time_tag(None) - obj.decode_time_tag(3) - - return - - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cplib_time_period.py b/test/test_cplib_time_period.py deleted file mode 100644 index 695af8aa..00000000 --- a/test/test_cplib_time_period.py +++ /dev/null @@ -1,859 +0,0 @@ -# Test the PARSE DURATION module - -import unittest - -import cp_lib.time_period as time_period - - -class TestTimePeriod(unittest.TestCase): - - def test_valid_clean_period_seconds(self): - - # zero is special, as it is not relevant to this function - seconds = 0 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - # self.assertFalse((60 % seconds) == 0) - - # one is typical TRUE situation - see '7' for typical FALSE situation - seconds += 1 # == 1 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 2 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 3 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 4 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 5 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 6 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - # seven is typical FALSE situation - see any 1 to 6 for typical - # FALSE situation - seconds += 1 # == 7 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 8 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 9 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 10 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 11 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 12 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 13 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 14 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 15 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 16 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 17 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 18 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 19 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 20 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 21 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 22 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 23 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 24 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 25 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 26 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 27 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 28 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 29 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 30 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 31 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 32 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 33 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 34 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 35 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 36 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 37 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 38 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 39 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 40 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 41 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 42 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 43 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 44 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 45 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 46 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 47 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 48 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 49 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 50 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 51 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 52 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 53 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 54 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 55 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 56 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 57 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 58 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 59 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - seconds += 1 # == 60 - self.assertTrue(time_period.is_valid_clean_period_seconds(seconds)) - self.assertTrue((60 % seconds) == 0) - - seconds += 1 # == 61 - self.assertFalse(time_period.is_valid_clean_period_seconds(seconds)) - self.assertFalse((60 % seconds) == 0) - - with self.assertRaises(TypeError): - time_period.is_valid_clean_period_seconds(None) - - # because we use 'int()' on value, these work as shown despite not - # being a goal - self.assertTrue(time_period.is_valid_clean_period_seconds(2.0)) - self.assertTrue(time_period.is_valid_clean_period_seconds('2')) - # int() rounds 2.1 to be 2 - again, not as desired, but result is - # largely as expected - self.assertTrue(time_period.is_valid_clean_period_seconds(2.1)) - self.assertFalse(time_period.is_valid_clean_period_seconds(7.1)) - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_seconds('2.1') - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_seconds('7.1') - - return - - def test_valid_clean_period_minutes(self): - - # zero is special, as it is not relevant to this function - minutes = 0 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - # self.assertFalse((60 % minutes) == 0) - - # one is typical TRUE situation - see '7' for typical FALSE situation - minutes += 1 # == 1 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 2 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 3 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 4 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 5 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 6 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - # seven is typical FALSE situation - see any 1 to 6 for typical - # FALSE situation - minutes += 1 # == 7 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 8 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 9 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 10 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 11 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 12 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 13 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 14 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 15 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 16 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 17 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 18 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 19 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 20 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 21 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 22 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 23 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 24 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 25 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 26 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 27 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 28 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 29 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 30 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 31 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 32 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 33 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 34 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 35 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 36 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 37 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 38 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 39 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 40 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 41 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 42 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 43 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 44 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 45 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 46 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 47 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 48 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 49 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 50 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 51 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 52 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 53 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 54 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 55 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 56 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 57 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 58 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 59 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - minutes += 1 # == 60 - self.assertTrue(time_period.is_valid_clean_period_minutes(minutes)) - self.assertTrue((60 % minutes) == 0) - - minutes += 1 # == 61 - self.assertFalse(time_period.is_valid_clean_period_minutes(minutes)) - self.assertFalse((60 % minutes) == 0) - - with self.assertRaises(TypeError): - time_period.is_valid_clean_period_minutes(None) - - # because we use 'int()' on value, these work as shown despite - # not being a goal - self.assertTrue(time_period.is_valid_clean_period_minutes(2.0)) - self.assertTrue(time_period.is_valid_clean_period_minutes('2')) - # int() rounds 2.1 to be 2 - again, not as desired, but result - # is largely as expected - self.assertTrue(time_period.is_valid_clean_period_minutes(2.1)) - self.assertFalse(time_period.is_valid_clean_period_minutes(7.1)) - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_minutes('2.1') - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_minutes('7.1') - - return - - def test_valid_clean_period_hours(self): - - # zero is special, as it is not relevant to this function - hours = 0 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - # self.assertFalse((24 % hours) == 0) - - # one is typical TRUE situation - see '7' for typical FALSE situation - hours += 1 # == 1 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 2 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 3 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 4 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 5 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 6 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - # seven is typical FALSE situation - see any 1 to 6 for - # typical FALSE situation - hours += 1 # == 7 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 8 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 9 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 10 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 11 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 12 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 13 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 14 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 15 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 16 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 17 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 18 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 19 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 20 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 21 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 22 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 23 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - hours += 1 # == 24 - self.assertTrue(time_period.is_valid_clean_period_hours(hours)) - self.assertTrue((24 % hours) == 0) - - hours += 1 # == 25 - self.assertFalse(time_period.is_valid_clean_period_hours(hours)) - self.assertFalse((24 % hours) == 0) - - with self.assertRaises(TypeError): - time_period.is_valid_clean_period_hours(None) - - # because we use 'int()' on value, these work as shown despite - # not being a goal - self.assertTrue(time_period.is_valid_clean_period_hours(2.0)) - self.assertTrue(time_period.is_valid_clean_period_hours('2')) - # int() truncates 2.1 to 2 (& 6.9 to 6) - again, not as desired, - # but result is largely as expected - self.assertTrue(time_period.is_valid_clean_period_hours(1.9)) - self.assertTrue(time_period.is_valid_clean_period_hours(2.1)) - self.assertTrue(time_period.is_valid_clean_period_hours(6.9)) - self.assertFalse(time_period.is_valid_clean_period_hours(7.1)) - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_hours('2.1') - - with self.assertRaises(ValueError): - time_period.is_valid_clean_period_hours('7.1') - - return - - def test_next_sec_or_min(self): - - # since next_minutes_period() just calls next_seconds_period(), - # there is no need to test both - - # we'll start with a basic test of a '15 sec' period - period = 15 - self.assertEqual(time_period.next_seconds_period(0, period), 15) - self.assertEqual(time_period.next_seconds_period(1, period), 15) - self.assertEqual(time_period.next_seconds_period(5, period), 15) - self.assertEqual(time_period.next_seconds_period(13, period), 15) - self.assertEqual(time_period.next_seconds_period(15, period), 30) - self.assertEqual(time_period.next_seconds_period(16, period), 30) - self.assertEqual(time_period.next_seconds_period(29, period), 30) - self.assertEqual(time_period.next_seconds_period(30, period), 45) - self.assertEqual(time_period.next_seconds_period(31, period), 45) - self.assertEqual(time_period.next_seconds_period(44, period), 45) - self.assertEqual(time_period.next_seconds_period(45, period), 60) - self.assertEqual(time_period.next_seconds_period(46, period), 60) - self.assertEqual(time_period.next_seconds_period(59, period), 60) - self.assertEqual(time_period.next_seconds_period(60, period), 75) - - # handle larger values - self.assertEqual(time_period.next_seconds_period(61, period), 75) - self.assertEqual(time_period.next_seconds_period(292, period), 300) - - return - - def test_delay_to_next_seconds(self): - - # we'll start with a basic test of a '15 sec' period - period = 15 - self.assertEqual( - time_period.delay_to_next_seconds_period(0, period), 15) - self.assertEqual( - time_period.delay_to_next_seconds_period(1, period), 14) - self.assertEqual( - time_period.delay_to_next_seconds_period(5, period), 10) - self.assertEqual( - time_period.delay_to_next_seconds_period(13, period), 2) - self.assertEqual( - time_period.delay_to_next_seconds_period(15, period), 15) - self.assertEqual( - time_period.delay_to_next_seconds_period(16, period), 14) - self.assertEqual( - time_period.delay_to_next_seconds_period(29, period), 1) - self.assertEqual( - time_period.delay_to_next_seconds_period(30, period), 15) - self.assertEqual( - time_period.delay_to_next_seconds_period(31, period), 14) - self.assertEqual( - time_period.delay_to_next_seconds_period(44, period), 1) - self.assertEqual( - time_period.delay_to_next_seconds_period(45, period), 15) - self.assertEqual( - time_period.delay_to_next_seconds_period(46, period), 14) - self.assertEqual( - time_period.delay_to_next_seconds_period(59, period), 1) - self.assertEqual( - time_period.delay_to_next_seconds_period(60, period), 15) - - # handle larger values - self.assertEqual( - time_period.delay_to_next_seconds_period(61, period), 14) - self.assertEqual( - time_period.delay_to_next_seconds_period(292, period), 8) - - # toss a few delay_to_next_minutes_period() tests - self.assertEqual( - time_period.delay_to_next_minutes_period(0, period), 15) - self.assertEqual( - time_period.delay_to_next_minutes_period(1, period), 14) - self.assertEqual( - time_period.delay_to_next_minutes_period(45, period), 15) - self.assertEqual( - time_period.delay_to_next_minutes_period(46, period), 14) - self.assertEqual( - time_period.delay_to_next_minutes_period(59, period), 1) - self.assertEqual( - time_period.delay_to_next_minutes_period(60, period), 15) - self.assertEqual( - time_period.delay_to_next_minutes_period(61, period), 14) - self.assertEqual( - time_period.delay_to_next_minutes_period(292, period), 8) - - return - - def test_add_remove_cb(self): - - obj = time_period.TimePeriods() - - # print(obj.per_minute) return like "Period:min cb:0 skewed:0" - # print(obj.per_hour) - # print(obj.per_day) - # print(obj.per_month) - # print(obj.per_year) - - self.assertEqual(obj.per_minute.get_name(), 'min') - self.assertEqual(obj.per_hour.get_name(), 'hr') - self.assertEqual(obj.per_day.get_name(), 'day') - self.assertEqual(obj.per_month.get_name(), 'mon') - self.assertEqual(obj.per_year.get_name(), 'yr') - - def cb_simple(x): - print("CB:{0}".format(x)) - - def cb_simple_2(x): - print("CB:{0}".format(x)) - - # some simple 1 cb in main list - self.assertEqual(len(obj.per_minute.cb_list), 0) - obj.per_minute.add_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 1) - obj.per_minute.remove_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 0) - - # some simple 2 cb in main list - obj.per_minute.add_callback(cb_simple_2) - self.assertEqual(len(obj.per_minute.cb_list), 1) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - obj.per_minute.remove_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 1) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - obj.per_minute.add_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 2) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - obj.per_minute.remove_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 1) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - obj.per_minute.remove_callback(cb_simple_2) - self.assertEqual(len(obj.per_minute.cb_list), 0) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - - # add to both lists - obj.per_minute.add_callback(cb_simple_2, skewed=True) - self.assertEqual(len(obj.per_minute.cb_list), 0) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 1) - obj.per_minute.remove_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 0) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 1) - obj.per_minute.add_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 1) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 1) - obj.per_minute.remove_callback(cb_simple) - self.assertEqual(len(obj.per_minute.cb_list), 0) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 1) - obj.per_minute.remove_callback(cb_simple_2) - self.assertEqual(len(obj.per_minute.cb_list), 0) - self.assertEqual(len(obj.per_minute.cb_list_skewed), 0) - - return - - -def simple_check(): - """A sanity-check routine; not a test""" - - for n in range(1, 61): - print("%02d = %f" % (n, 60.0/n)) - - for n in range(1, 25): - print("%02d = %f" % (n, 24.0/n)) - - return - - -if __name__ == '__main__': - - # simple_check() - - unittest.main() diff --git a/test/test_cplib_unquote_string.py b/test/test_cplib_unquote_string.py deleted file mode 100644 index 31f66322..00000000 --- a/test/test_cplib_unquote_string.py +++ /dev/null @@ -1,51 +0,0 @@ -# Test the PARSE DATA module - -import unittest - -from cp_lib.unquote_string import unquote_string - - -class TestUnquoteString(unittest.TestCase): - - def test_isolate_numeric(self): - - tests = [ - {'src': "Hello", 'exp': "Hello"}, - {'src': " Hello", 'exp': " Hello"}, - {'src': " Hello ", 'exp': " Hello "}, - {'src': "'Hello", 'exp': "'Hello"}, - {'src': "'Hello'", 'exp': "Hello"}, - {'src': '"Hello"', 'exp': 'Hello'}, - {'src': '"Hello', 'exp': '"Hello'}, - - {'src': " 'Hello'", 'exp': "Hello"}, - {'src': "'Hello' ", 'exp': "Hello"}, - {'src': " 'Hello' ", 'exp': "Hello"}, - {'src': ' "Hello"', 'exp': 'Hello'}, - {'src': '"Hello" ', 'exp': 'Hello'}, - {'src': ' "Hello" ', 'exp': 'Hello'}, - - {'src': "", 'exp': ""}, - {'src': " ", 'exp': " "}, - {'src': "'", 'exp': "'"}, - {'src': " '", 'exp': " '"}, - {'src': " '' ", 'exp': ""}, - {'src': None, 'exp': None}, - {'src': 10, 'exp': 10}, - {'src': 10.0, 'exp': 10.0}, - ] - - for test in tests: - result = unquote_string(test['src']) - self.assertEqual(result, test['exp']) - - # with self.assertRaises(ValueError): - # # value must have number str - # isolate_numeric_from_string("") - # isolate_numeric_from_string(" \n") - # isolate_numeric_from_string("hello") - - return - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_cs_client.py b/test/test_cs_client.py deleted file mode 100644 index 16519809..00000000 --- a/test/test_cs_client.py +++ /dev/null @@ -1,53 +0,0 @@ -# Test the cp_lib.cs_client module - -import logging -import unittest - - -class TestCsClient(unittest.TestCase): - - def test_content_length(self): - """ - Test buffer to lines function - :return: - """ - from cp_lib.cs_client import _fetch_content_length - - tests = [ - {"dat": 'content-length: 12\n\r', "len": 12, - "exp": ''}, - {"dat": 'content-length: 12\n\r\n\r\n"IBR1150LPE"', "len": 0, - "exp": '"IBR1150LPE"'}, - {"dat": 'content-length: 189', "len": 189, - "exp": ''}, - {"dat": 'content-length: 189\n\r\n\r\n{"connections": [], "taip_' - 'vehicle_id": "0000", "enable_gps_keepalive": false, "de' - 'bug": {"log_nmea_to_fs": false, "flags": 0}, "enable_gp' - 's_led": false, "pwd_enabled": false, "enabled": true}', - "len": 0, - "exp": '{"connections": [], "taip_vehicle_id": "0000", "enable_' - 'gps_keepalive": false, "debug": {"log_nmea_to_fs": false' - ', "flags": 0}, "enable_gps_led": false, "pwd_enabled":' - ' false, "enabled": true}'}, - ] - - logger = logging.getLogger('unittest') - logger.setLevel(logging.DEBUG) - - for test in tests: - # logging.debug("Test:{}".format(test)) - - # _fetch_content_length(data, logger=None): - data_length, all_data = _fetch_content_length(test['dat'], logger) - self.assertEqual(data_length, test['len']) - self.assertEqual(all_data, test['exp']) - - logging.debug("") - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_load_active_wan.py b/test/test_load_active_wan.py deleted file mode 100644 index 33666b81..00000000 --- a/test/test_load_active_wan.py +++ /dev/null @@ -1,38 +0,0 @@ -# Test the cp_lib.load_active_wan module - -import json -import unittest - -from cp_lib.load_active_wan import fetch_active_wan - - -class TestFetchActiveWan(unittest.TestCase): - - def test_fetch(self): - global base_app - - print("") # skip paste '.' on line - - result = fetch_active_wan(base_app) - pretty = json.dumps(result, indent=4, sort_keys=True) - - # if SHOW_SETTINGS_AS_JSON: - if False: - file_name = "dump.json" - print("Write to file:{}".format(file_name)) - file_han = open(file_name, "w") - file_han.write(pretty) - file_han.close() - - if False: - print("Output:{}".format(pretty)) - - return - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - - base_app = CradlepointAppBase(call_router=False) - - unittest.main() diff --git a/test/test_load_firmware_info.py b/test/test_load_firmware_info.py deleted file mode 100644 index 7b32c96c..00000000 --- a/test/test_load_firmware_info.py +++ /dev/null @@ -1,142 +0,0 @@ -# Test the cp_lib.load_firmware_info module - -import copy -import json -import logging -import os.path -import shutil -import unittest - -from cp_lib.load_firmware_info import load_firmware_info, SECTION_FW_INFO - -# for internal test of tests - allow temp files to be left on disk -REMOVE_TEMP_FILE = True - - -class TestLoadFirmwareInfo(unittest.TestCase): - - def test_method_1_ini(self): - - print("") # skip paste '.' on line - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - - # since we do not test values, all need is a fake ["fw_info"] section - # but module will try to create ["version"]: "6.1" - _my_settings[SECTION_FW_INFO] = {"major_version": 6, - "minor_version": 1} - - if False: - print("Settings after Method #1 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_firmware_info(_my_settings, _client) - self.assertEqual(result, _my_settings) - - return - - def test_method_2_file(self): - - print("") # skip paste '.' on line - - test_file_name = "test/test_fw_info.json" - - # make a raw JSON string - data = '{"major_version":6, "fw_update_available":false, "upgrade' +\ - '_patch_version":0, "upgrade_minor_version":0, "build_version' +\ - '":"0310fce", "build_date":"WedJan1300: 23: 15MST2016", "minor' +\ - '_version":1, "upgrade_major_version":0, "manufacturing_up' +\ - 'grade":false, "build_type":"FIELD[build]", "custom_defaults":' +\ - 'false, "patch_version":0}' - - _logger.debug("Make temp file:{}".format(test_file_name)) - _han = open(test_file_name, 'w') - _han.write(data) - _han.close() - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - self.assertFalse("fw_info" in _my_settings) - - if False: - print("Settings before Method #2 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_firmware_info(_my_settings, _client, - file_name=test_file_name) - self.assertTrue("fw_info" in result) - - if False: - print("Settings after Method #2 test") - print(json.dumps(result, indent=4, sort_keys=True)) - - if REMOVE_TEMP_FILE: - _logger.debug("Delete temp file:{}".format(test_file_name)) - self._remove_name_no_error(test_file_name) - - return - - def test_method_3_url(self): - - print("") # skip paste '.' on line - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - self.assertFalse("fw_info" in _my_settings) - - if False: - print("Settings before Method #3 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_firmware_info(_my_settings, _client) - self.assertTrue("fw_info" in result) - - # if SHOW_SETTINGS_AS_JSON: - if False: - print("Settings after Method #3 test") - print(json.dumps(result, indent=4, sort_keys=True)) - - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - from cp_lib.cs_client import init_cs_client_on_my_platform - from cp_lib.load_settings_ini import load_sdk_ini_as_dict - - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - - _logger = logging.getLogger('unittest') - _logger.setLevel(logging.DEBUG) - - # we pass in no APP Dir, so just read the .config data - _settings = load_sdk_ini_as_dict() - # _logger.debug("sets:{}".format(_settings)) - - # handle the Router API client, which is different between PC - # testing and router HW - try: - _client = init_cs_client_on_my_platform(_logger, _settings) - except: - _logger.exception("CSClient init failed") - raise - - unittest.main() diff --git a/test/test_load_gps_config.py b/test/test_load_gps_config.py deleted file mode 100644 index 8f584a00..00000000 --- a/test/test_load_gps_config.py +++ /dev/null @@ -1,51 +0,0 @@ -# Test the cp_lib.load_active_wan module - -import json -import unittest - -from cp_lib.load_gps_config import fetch_gps_config, GpsConfig - - -class TestFetchGpsConfig(unittest.TestCase): - - def test_fetch(self): - global base_app - - print("") # skip paste '.' on line - - result = fetch_gps_config(base_app) - pretty = json.dumps(result, indent=4, sort_keys=True) - - # if SHOW_SETTINGS_AS_JSON: - if False: - file_name = "dump.json" - print("Write to file:{}".format(file_name)) - file_han = open(file_name, "w") - file_han.write(pretty) - file_han.close() - - if False: - print("Output:{}".format(pretty)) - - return - - def test_object(self): - global base_app - - obj = GpsConfig(base_app) - self.assertIsNotNone(obj) - self.assertIsNotNone(obj.data) - - base_app.logger.debug("GPS Enabled:{}".format(obj.is_enabled())) - base_app.logger.debug("Keepalive Enabled:{}".format( - obj.keepalive_is_enabled())) - base_app.logger.debug("GPS to Server:{}".format(obj.get_client_info())) - - return - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - - base_app = CradlepointAppBase(call_router=False) - unittest.main() diff --git a/test/test_load_product_info.py b/test/test_load_product_info.py deleted file mode 100644 index ed024292..00000000 --- a/test/test_load_product_info.py +++ /dev/null @@ -1,206 +0,0 @@ -# Test the cp_lib.load_product_info module - -import copy -import json -import logging -import os.path -import shutil -import unittest - -from cp_lib.load_product_info import load_product_info, \ - SECTION_PRODUCT_INFO, split_product_name - -# for internal test of tests - allow temp files to be left on disk -REMOVE_TEMP_FILE = True - - -class TestLoadProductInfo(unittest.TestCase): - - def test_method_1_ini(self): - - print("") # skip paste '.' on line - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - - # since do not test values, only need a fake ["product_info"] section - _my_settings[SECTION_PRODUCT_INFO] = True - - if False: - print("Settings after Method #1 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_product_info(_my_settings, _client) - self.assertEqual(result, _my_settings) - - return - - def test_method_2_file(self): - - print("") # skip paste '.' on line - - test_file_name = "test/test_product_info.json" - - # make a raw JSON string - data = '{"company_name":"Cradlepoint, Inc.","company_url": ' + \ - '"http://cradlepoint.com","copyright":"Cradlepoint, Inc. ' + \ - '2016","mac0": "00:30:44:1a:81:9c","manufacturing":{"board' + \ - '_ID":"050200","mftr_date":"20141204","serial_num":"MM1404' + \ - '59400193"},"product_name": "IBR1150LPE"}' - - _logger.debug("Make temp file:{}".format(test_file_name)) - _han = open(test_file_name, 'w') - _han.write(data) - _han.close() - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - self.assertFalse("product_info" in _my_settings) - - if False: - print("Settings before Method #2 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_product_info(_my_settings, _client, - file_name=test_file_name) - self.assertTrue("product_info" in result) - - if False: - print("Settings after Method #2 test") - print(json.dumps(result, indent=4, sort_keys=True)) - - if REMOVE_TEMP_FILE: - _logger.debug("Delete temp file:{}".format(test_file_name)) - self._remove_name_no_error(test_file_name) - - return - - def test_method_3_url(self): - - print("") # skip paste '.' on line - - # make a DEEP copy, to make sure we do not 'pollute' any other tests - _my_settings = copy.deepcopy(_settings) - self.assertFalse("product_info" in _my_settings) - - if False: - print("Settings before Method #3 test") - print(json.dumps(_my_settings, indent=4, sort_keys=True)) - - result = load_product_info(_my_settings, _client) - self.assertTrue("product_info" in result) - - # if SHOW_SETTINGS_AS_JSON: - if False: - print("Settings after Method #3 test") - print(json.dumps(result, indent=4, sort_keys=True)) - - return - - def test_split_product_name(self): - - print("") # skip paste '.' on line - - _logger.debug("test split_product_name") - - tests = [ - {'src': "IBR350", 'exp': ("IBR350", "", False)}, - {'src': "IBR300AT", 'exp': ("IBR300", "AT", True)}, - - {'src': "IBR600", 'exp': ("IBR600", "", True)}, - {'src': "IBR600AT", 'exp': ("IBR600", "AT", True)}, - {'src': "IBR650", 'exp': ("IBR650", "", False)}, - {'src': "IBR650AT", 'exp': ("IBR650", "AT", False)}, - - {'src': "IBR600B", 'exp': ("IBR600B", "", True)}, - {'src': "IBR600BAT", 'exp': ("IBR600B", "AT", True)}, - {'src': "IBR650B", 'exp': ("IBR650B", "", False)}, - {'src': "IBR650BAT", 'exp': ("IBR650B", "AT", False)}, - - {'src': "CBA850", 'exp': ("CBA850", "", False)}, - {'src': "CBA800AT", 'exp': ("CBA800", "AT", True)}, - - {'src': "IBR1100", 'exp': ("IBR1100", "", True)}, - {'src': "IBR1100AT", 'exp': ("IBR1100", "AT", True)}, - {'src': "ibr1100LPE", 'exp': ("IBR1100", "LPE", True)}, - {'src': "IBR1150", 'exp': ("IBR1150", "", False)}, - {'src': "IBR1150AT", 'exp': ("IBR1150", "AT", False)}, - {'src': "ibr1150LPE", 'exp': ("IBR1150", "LPE", False)}, - - {'src': "AER1600", 'exp': ("AER1600", "", True)}, - {'src': "AER1600AT", 'exp': ("AER1600", "AT", True)}, - {'src': "AER1600LPE", 'exp': ("AER1600", "LPE", True)}, - {'src': "AER1650", 'exp': ("AER1650", "", False)}, - {'src': "AER1650AT", 'exp': ("AER1650", "AT", False)}, - {'src': "AER1650LPE", 'exp': ("AER1650", "LPE", False)}, - - # a bit fake, as we'll always see "2100" - {'src': "AER2100", 'exp': ("AER2100", "", True)}, - {'src': "AER2100AT", 'exp': ("AER2100", "AT", True)}, - {'src': "AER2100LPE", 'exp': ("AER2100", "LPE", True)}, - {'src': "AER2150", 'exp': ("AER2150", "", False)}, - {'src': "AER2150AT", 'exp': ("AER2150", "AT", False)}, - {'src': "AER2150LPE", 'exp': ("AER2150", "LPE", False)}, - - {'src': "2100", 'exp': ("AER2100", "", True)}, - {'src': "2100AT", 'exp': ("AER2100", "AT", True)}, - {'src': "2100LPE", 'exp': ("AER2100", "LPE", True)}, - {'src': "2150", 'exp': ("AER2150", "", False)}, - {'src': "2150AT", 'exp': ("AER2150", "AT", False)}, - {'src': "2150LPE", 'exp': ("AER2150", "LPE", False)}, - - {'src': "AER3100", 'exp': ("AER3100", "", True)}, - {'src': "AER3100AT", 'exp': ("AER3100", "AT", True)}, - {'src': "AER3100LPE", 'exp': ("AER3100", "LPE", True)}, - {'src': "AER3150", 'exp': ("AER3150", "", False)}, - {'src': "AER3150AT", 'exp': ("AER3150", "AT", False)}, - {'src': "AER3150LPE", 'exp': ("AER3150", "LPE", False)}, - ] - - for test in tests: - result = split_product_name(test['src']) - self.assertEqual(result, test['exp']) - - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - from cp_lib.cs_client import init_cs_client_on_my_platform - from cp_lib.load_settings_ini import load_sdk_ini_as_dict - - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - - _logger = logging.getLogger('unittest') - _logger.setLevel(logging.DEBUG) - - # we pass in no APP Dir, so just read the .config data - _settings = load_sdk_ini_as_dict() - # _logger.debug("sets:{}".format(_settings)) - - # handle the Router API client, which is different between PC - # testing and router HW - try: - _client = init_cs_client_on_my_platform(_logger, _settings) - except: - _logger.exception("CSClient init failed") - raise - - unittest.main() diff --git a/test/test_load_settings.py b/test/test_load_settings.py deleted file mode 100644 index f6010306..00000000 --- a/test/test_load_settings.py +++ /dev/null @@ -1,341 +0,0 @@ -# Test the cp_lib.load_settings module - -import logging -import os.path -# noinspection PyUnresolvedReferences -import shutil -import unittest - - -class TestLoadSettings(unittest.TestCase): - - def test_load_settings(self): - import copy - import tools.make_load_settings as load_settings - from cp_lib.load_settings_json import load_settings_json, DEF_GLOBAL_DIRECTORY, DEF_JSON_EXT - from cp_lib.load_settings_ini import save_root_settings_json - - print("") - - glob_data = [ - "[logging]", - "level = debug", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - app_data = [ - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "uuid = 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - ] - - all_data = copy.deepcopy(glob_data) - all_data.extend(app_data) - - def _make_ini_file(file_name: str, data_list: list): - """Given file name (path), make file containing the name""" - _han = open(file_name, 'w') - for line in data_list: - _han.write(line + "\n") - _han.close() - # logging.debug("Write Global INI") - return - - test_file_name = "test_load" - - global_dir = DEF_GLOBAL_DIRECTORY - global_path = os.path.join(global_dir, test_file_name + load_settings.DEF_INI_EXT) - # logging.debug("GLB:[{}]".format(global_path)) - - app_dir = "network\\tcp_echo" - app_path = os.path.join(app_dir, test_file_name + load_settings.DEF_INI_EXT) - # logging.debug("APP:[{}]".format(app_path)) - - save_path = test_file_name + DEF_JSON_EXT - - logging.info("TEST: only global exists, but is incomplete:[{}]".format(global_path)) - self._remove_name_no_error(global_path) - self._remove_name_no_error(app_path) - self._remove_name_no_error(save_path) - - _make_ini_file(global_path, glob_data) - self.assertTrue(os.path.isfile(global_path)) - self.assertFalse(os.path.isfile(app_path)) - self.assertFalse(os.path.isfile(save_path)) - - with self.assertRaises(KeyError): - # we lack an apps section - result = load_settings.load_settings(app_dir, test_file_name) - - self._remove_name_no_error(global_path) - - _make_ini_file(global_path, all_data) - self.assertTrue(os.path.isfile(global_path)) - self.assertFalse(os.path.isfile(app_path)) - - expect = { - "application": { - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "path": "network/tcp_echo", - "name": "tcp_echo", - "_comment": "Settings for the application being built.", - "description": "Run a basic TCP socket echo server and client" - }, - "router_api": { - "password": "441b1702", - "_comment": "Settings to allow accessing router API in development mode.", - "local_ip": "192.168.1.1", - "user_name": "admin" - }, - "logging": { - "level": "debug", - "_comment": "Settings for the application debug/syslog/logging function." - } - } - logging.info("TEST: only global exists, is now complete:[{}]".format(global_path)) - result = load_settings.load_settings(app_dir, test_file_name) - self.assertEqual(result, expect) - - logging.info("TEST: only APP exists:[{}]".format(global_path)) - self._remove_name_no_error(global_path) - self._remove_name_no_error(app_path) - - _make_ini_file(app_path, app_data) - self.assertFalse(os.path.isfile(global_path)) - self.assertTrue(os.path.isfile(app_path)) - - # we lack an apps section - result = load_settings.load_settings(app_dir, test_file_name) - - expect = { - "application": { - "path": "network/tcp_echo", - "description": "Run a basic TCP socket echo server and client", - "name": "tcp_echo", - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "_comment": "Settings for the application being built." - } - } - self.assertEqual(result, expect) - - self._remove_name_no_error(global_path) - self._remove_name_no_error(app_path) - - logging.info("TEST: both exists, is complete:[{}]".format(global_path)) - _make_ini_file(global_path, glob_data) - _make_ini_file(app_path, app_data) - - self.assertTrue(os.path.isfile(global_path)) - self.assertTrue(os.path.isfile(app_path)) - - expect = { - "application": { - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - "path": "network/tcp_echo", - "name": "tcp_echo", - "_comment": "Settings for the application being built.", - "description": "Run a basic TCP socket echo server and client" - }, - "router_api": { - "password": "441b1702", - "_comment": "Settings to allow accessing router API in development mode.", - "local_ip": "192.168.1.1", - "user_name": "admin" - }, - "logging": { - "level": "debug", - "_comment": "Settings for the application debug/syslog/logging function." - } - } - result = load_settings.load_settings(app_dir, test_file_name) - self.assertEqual(result, expect) - - # result is now our 'full' settings - logging.info("TEST: save to root, is complete:[{}]".format(global_path)) - self.assertFalse(os.path.isfile(save_path)) - save_root_settings_json(result, save_path) - self.assertTrue(os.path.isfile(save_path)) - - self._remove_name_no_error(global_path) - self._remove_name_no_error(app_path) - - result = load_settings_json(file_name=test_file_name) - # logging.debug("") - # logging.debug("Load Result:[{}]".format(json.dumps(result))) - # logging.debug("") - # logging.debug("Load Expect:[{}]".format(json.dumps(expect))) - # logging.debug("") - - self.assertEqual(result, expect) - - self._remove_name_no_error(save_path) - - return - - def test_fix_uuid(self): - """ - add or replace the "uuid" data in the app's INI file - - TODO - also handle JSON, but for now we ignore this. - - :return: - """ - - import tools.make_load_settings as load_settings - import uuid - - print("") - - sets = {"app_dir": "test", "base_name": "test_uuid"} - - ini_name = os.path.join(sets["app_dir"], sets["base_name"] + load_settings.DEF_INI_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "uuid = 7042c8fd-fe7a-4846-aed1-original", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - value = str(uuid.uuid4()) - logging.info("Creating random UUID={}".format(value)) - - load_settings.fix_up_uuid(ini_name, value, backup=False) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - value = str(uuid.uuid4()) - logging.info("Creating random UUID={}".format(value)) - - load_settings.fix_up_uuid(ini_name, value, backup=False) - - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - return - - def test_incr_version(self): - """ - add or replace the "uuid" data in the app's INI file - - TODO - also handle JSON, but for now we ignore this. - - :return: - """ - import tools.make_load_settings as load_settings - - print("") - - sets = {"app_dir": "test", "base_name": "test_version"} - - ini_name = os.path.join(sets["app_dir"], sets["base_name"] + load_settings.DEF_INI_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "version = 3.45", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - load_settings.increment_app_version(ini_name) - - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - load_settings.increment_app_version(ini_name) - - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_make.py b/test/test_make.py deleted file mode 100644 index 0d423c75..00000000 --- a/test/test_make.py +++ /dev/null @@ -1,292 +0,0 @@ -# Test the Monnit Protocol code - -import unittest -import logging -import os.path -# noinspection PyUnresolvedReferences -import shutil - -import make - - -class TestMake(unittest.TestCase): - - def test_confirm_dir_exists(self): - - maker = make.TheMaker() - - # just do the ./build, as it can be empty by default - test_name = "build" - - # make sure it doesn't exist as file or dir - maker._remove_name_no_error(test_name) - - # 1st confirm, will create as directory - self.assertFalse(os.path.exists(test_name)) - maker._confirm_dir_exists(test_name, "Test dir") - self.assertTrue(os.path.exists(test_name)) - self.assertTrue(os.path.isdir(test_name)) - - # 2nd confirm, will leave alone - maker._confirm_dir_exists(test_name, "Test dir") - self.assertTrue(os.path.exists(test_name)) - self.assertTrue(os.path.isdir(test_name)) - - # make sure it doesn't exist as file or dir - test_save_name = test_name + maker.SDIR_SAVE_EXT - maker._remove_name_no_error(test_name) - maker._remove_name_no_error(test_save_name) - - self.assertFalse(os.path.exists(test_name)) - self.assertFalse(os.path.exists(test_save_name)) - - # create a dummy file, should cause to be renamed test_save_name - file_handle = open(test_name, "w") - file_handle.write("Hello there!") - file_handle.close() - - # so now, common file 'blocks' our directory; we'll rename to .save & make anyway - self.assertTrue(os.path.exists(test_name)) - self.assertFalse(os.path.exists(test_save_name)) - - maker._confirm_dir_exists(test_name, "Test blocked dir") - - self.assertTrue(os.path.exists(test_name)) - self.assertTrue(os.path.isdir(test_name)) - - self.assertTrue(os.path.exists(test_save_name)) - self.assertTrue(os.path.isfile(test_save_name)) - - # we just leave ./build/ existing - is okay - maker._remove_name_no_error(test_save_name) - - return - - def test_install_sh(self): - - def _make_temp_file(file_name: str): - """Given file name (path), make file containing the name""" - _han = open(file_name, 'w') - _han.write(file_name) - _han.close() - return - - def _validate_contents(file_name: str, expected: str): - """Confirm file contains 'expected' data""" - _han = open(file_name, 'r') - _data = _han.read() - _han.close() - return _data == expected - - # quick test of the _make_temp_file and _validate_contents - data = os.path.join("test", "shiny_shoes.txt") - _make_temp_file(data) - self.assertTrue(_validate_contents(data, data)) - os.remove(data) - - maker = make.TheMaker() - maker.load_settings_json() - - test_install_name = "test_install.sh" - maker.force_settings_dict({'name': 'tcp_echo'}) - maker.force_settings_dict({'path': os.path.join("network", "tcp_echo")}) - - app_file_name = os.path.join(maker.get_app_path(), test_install_name) - cfg_file_name = os.path.join(make.SDIR_CONFIG, test_install_name) - dst_file_name = os.path.join(make.SDIR_BUILD, test_install_name) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # test fresh creation (& re-creation) a default INSTALL file - no source exists - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # now try using the config file (no dev supplies app file) - _make_temp_file(cfg_file_name) - - self.assertFalse(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, cfg_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # now try using the dev supplies file, assume no global config file - _make_temp_file(app_file_name) - - self.assertTrue(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertTrue(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, app_file_name)) - - # repeat, but this time global config exists & is ignored - maker._remove_name_no_error(dst_file_name) - _make_temp_file(cfg_file_name) - - self.assertTrue(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertTrue(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, app_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - return - - def test_start_sh(self): - - maker = make.TheMaker() - maker.load_settings_json() - - def _make_temp_file(file_name: str): - """Given file name (path), make file containing the name""" - _han = open(file_name, 'w') - _han.write(file_name) - _han.close() - return - - def _validate_contents(file_name: str, expected: str): - """Confirm file contains 'expected' data""" - _han = open(file_name, 'r') - _data = _han.read() - _han.close() - return _data == expected - - # quick test of the _make_temp_file and _validate_contents - data = os.path.join("test", "shiny_shoes.txt") - _make_temp_file(data) - self.assertTrue(_validate_contents(data, data)) - os.remove(data) - - maker = make.TheMaker() - maker.load_settings_json() - - test_install_name = "test_start.sh" - maker.force_settings_dict({'name': 'tcp_echo'}) - maker.force_settings_dict({'path': os.path.join("network", "tcp_echo")}) - - app_file_name = os.path.join(maker.get_app_path(), test_install_name) - cfg_file_name = os.path.join(make.SDIR_CONFIG, test_install_name) - dst_file_name = os.path.join(make.SDIR_BUILD, test_install_name) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # test fresh creation (& re-creation) a default INSTALL file - no source exists - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # now try using the config file (no dev supplies app file) - _make_temp_file(cfg_file_name) - - self.assertFalse(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertFalse(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, cfg_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - # now try using the dev supplies file, assume no global config file - _make_temp_file(app_file_name) - - self.assertTrue(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertTrue(os.path.exists(app_file_name)) - self.assertFalse(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, app_file_name)) - - # repeat, but this time global config exists & is ignored - maker._remove_name_no_error(dst_file_name) - _make_temp_file(cfg_file_name) - - self.assertTrue(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertFalse(os.path.exists(dst_file_name)) - - maker.create_install_sh(test_install_name) - self.assertTrue(os.path.exists(app_file_name)) - self.assertTrue(os.path.exists(cfg_file_name)) - self.assertTrue(os.path.exists(dst_file_name)) - self.assertTrue(_validate_contents(dst_file_name, app_file_name)) - - # start, make sure none exist - maker._remove_name_no_error(app_file_name) - maker._remove_name_no_error(cfg_file_name) - maker._remove_name_no_error(dst_file_name) - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_make_load_settings.py b/test/test_make_load_settings.py deleted file mode 100644 index a345ac50..00000000 --- a/test/test_make_load_settings.py +++ /dev/null @@ -1,328 +0,0 @@ -# Test the tools.make_settings module - for some weird reason, I can't run this from ./tests - -import logging -import os.path -import shutil -import unittest - -# for internal test of tests - allow temp files to be left on disk -REMOVE_TEMP_FILE = True - - -class TestLoadSettings(unittest.TestCase): - - def test_line_find_section(self): - from tools.make_load_settings import _line_find_section - - print("") # skip paste '.' on line - - logging.info("TEST _line_find_section") - tests = [ - (None, False), - ("", False), - ("[]", False), - ("[application]", True), - (" [Application]", True), - (" [aPPlication] # comments", True), - ("[APPLICATION]", True), - ("[aplication]", False), - (" name = tcp_echo", False), - ("description = Run a basic TCP socket echo server and client", False), - ("path = network/tcp_echo", False), - ("uuid = 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", False), - (" [build]", False), - (" [RouterApiStuff]", False), - ] - - for test in tests: - source = test[0] - expect = test[1] - self.assertEqual(_line_find_section(source, name='application'), expect) - return - - def test_load_settings(self): - - from tools.make_load_settings import DEF_GLOBAL_DIRECTORY, DEF_INI_EXT, load_settings - - def _make_global_ini_file(file_name: str): - """Given file name (path), make file containing the name""" - data = [ - "[logging]", - "level = debug", - "syslog_ip = 192.168.1.6", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(file_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - # logging.debug("Write Global INI") - return - - def _make_app_ini_file(file_name: str): - """Given file name (path), make file containing the name""" - data = [ - "[logging]", - "level = info", - # this one NOT changed! "syslog_ip = 192.168.1.6", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "uuid = 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c", - ] - - _han = open(file_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - # logging.debug("Write APP INI") - return - - print("") # skip paste '.' on line - - test_file_name = "test_load" - - logging.info("TEST: load proj over global, merge correctly") - global_dir = DEF_GLOBAL_DIRECTORY - global_path = os.path.join(global_dir, test_file_name + DEF_INI_EXT) - # logging.debug("GLB:[{}]".format(global_path)) - _make_global_ini_file(global_path) - - app_dir = "network\\tcp_echo" - app_path = os.path.join(app_dir, test_file_name + DEF_INI_EXT) - # logging.debug("APP:[{}]".format(app_path)) - _make_app_ini_file(app_path) - - result = load_settings(app_dir, global_dir, test_file_name) - # logging.debug("Load Result:[{}]".format(json.dumps(result, sort_keys=True, indent=2))) - - # confirm worked! APP changed logging level, but not syslog_ip - self.assertNotEqual("debug", result["logging"]["level"]) - self.assertEqual("info", result["logging"]["level"]) - self.assertEqual("192.168.1.6", result["logging"]["syslog_ip"]) - - # clean up the temp file - if REMOVE_TEMP_FILE: - self._remove_name_no_error(global_path) - self._remove_name_no_error(app_path) - - return - - def test_fix_uuid(self): - """ - add or replace the "uuid" data in the app's INI file - - TODO - also handle JSON, but for now we ignore this. - - :return: - """ - import uuid - import tools.make_load_settings as load_settings - import configparser - - print("") # skip paste '.' on line - - sets = {"app_dir": "test", "base_name": "test_uuid"} - - ini_name = os.path.join(sets["app_dir"], sets["base_name"] + load_settings.DEF_INI_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "uuid = 7042c8fd-fe7a-4846-aed1-original", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - logging.info("TEST - over-write old UUID") - expect = str(uuid.uuid4()) - load_settings.fix_up_uuid(ini_name, expect, backup=REMOVE_TEMP_FILE) - - # confirm it worked! - config = configparser.ConfigParser() - file_han = open(ini_name, "r") - config.read_file(file_han) - file_han.close() - # logging.debug(" uuid:{}".format(config["application"]["uuid"])) - self.assertEqual(expect, config["application"]["uuid"]) - - data = ["[logging]", - "level=debug", - "", - "[application]", - "name=tcp_echo", - "description=Run a basic TCP socket echo server and client", - "path=network/tcp_echo", - "", - "[router_api]", - "local_ip=192.168.1.1", - "user_name=admin", - "password=441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - logging.info("TEST - add missing UUID") - expect = str(uuid.uuid4()) - load_settings.fix_up_uuid(ini_name, expect, backup=REMOVE_TEMP_FILE) - - # confirm it worked! - config = configparser.ConfigParser() - file_han = open(ini_name, "r") - config.read_file(file_han) - file_han.close() - # logging.debug(" uuid:{}".format(config["application"]["uuid"])) - self.assertEqual(expect, config["application"]["uuid"]) - - if REMOVE_TEMP_FILE: - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - return - - def test_incr_version(self): - """ - add or replace the "uuid" data in the app's INI file - - TODO - also handle JSON, but for now we ignore this. - - :return: - """ - import tools.make_load_settings as load_settings - import configparser - - print("") # skip paste '.' on line - - sets = {"app_dir": "test", "base_name": "test_version"} - - ini_name = os.path.join(sets["app_dir"], sets["base_name"] + load_settings.DEF_INI_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "version = 3.45", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - logging.info("TEST - increment existing version") - expect = "3.46" - load_settings.increment_app_version(ini_name, incr_version=True) - - # confirm it worked! - config = configparser.ConfigParser() - file_han = open(ini_name, "r") - config.read_file(file_han) - file_han.close() - # logging.debug(" version:{}".format(config["application"]["version"])) - self.assertEqual(expect, config["application"]["version"]) - - logging.info("TEST - don't increment existing version") - expect = "3.46" - load_settings.increment_app_version(ini_name, incr_version=False) - - # confirm it worked! - config = configparser.ConfigParser() - file_han = open(ini_name, "r") - config.read_file(file_han) - file_han.close() - # logging.debug(" version:{}".format(config["application"]["version"])) - self.assertEqual(expect, config["application"]["version"]) - - if REMOVE_TEMP_FILE: - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - data = ["[logging]", - "level = debug", - "", - "[application]", - "name = tcp_echo", - "description = Run a basic TCP socket echo server and client", - "path = network/tcp_echo", - "", - "[router_api]", - "local_ip = 192.168.1.1", - "user_name = admin", - "password = 441b1702" - ] - - _han = open(ini_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - logging.info("TEST - missing version") - expect = "1.0" - load_settings.increment_app_version(ini_name) - - # confirm it worked! - config = configparser.ConfigParser() - file_han = open(ini_name, "r") - config.read_file(file_han) - file_han.close() - # logging.debug(" version:{}".format(config["application"]["version"])) - self.assertEqual(expect, config["application"]["version"]) - - if REMOVE_TEMP_FILE: - self._remove_name_no_error(ini_name) - self._remove_name_no_error(ini_name + load_settings.DEF_SAVE_EXT) - - return - - @staticmethod - def _remove_name_no_error(file_name): - """ - Just remove if exists - :param str file_name: the file - :return: - """ - if os.path.isdir(file_name): - shutil.rmtree(file_name) - - else: - try: # second, try if common file - os.remove(file_name) - except FileNotFoundError: - pass - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_module_dependency.py b/test/test_module_dependency.py deleted file mode 100644 index b39d9e4a..00000000 --- a/test/test_module_dependency.py +++ /dev/null @@ -1,98 +0,0 @@ -# Test the tools.split.version module - -import json -import logging -import os.path -import unittest - -from tools.module_dependency import BuildDependencyList - - -class TestModuleDependency(unittest.TestCase): - - def test_get_file_dependency_list(self): - """ - Test the one-file module - :return: - """ - - obj = BuildDependencyList() - path_name = None - logging.info("get_file_dependency_list(\"{}\") bad type".format(path_name)) - with self.assertRaises(TypeError): - obj.add_file_dependency(path_name) - path_name = 23 - with self.assertRaises(TypeError): - obj.add_file_dependency(path_name) - - path_name = "not here" - logging.info("get_file_dependency_list(\"{}\") doesn't exist".format(path_name)) - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertIsNone(result) - - path_name = os.path.join("network", "tcp_echo") - logging.info("get_file_dependency_list(\"{}\") is dir not file".format(path_name)) - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertIsNone(result) - - path_name = os.path.join("network", "tcp_echo", "settings.json.save") - logging.info("get_file_dependency_list(\"{}\") doesn't exist".format(path_name)) - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertIsNone(result) - - path_name = os.path.join("network", "tcp_echo", "tcp_echo.py") - expect = ['cp_lib.cp_logging', 'cp_lib.hw_status', 'cp_lib.load_settings'] - logging.info("get_file_dependency_list(\"{}\") doesn't exist".format(path_name)) - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - # "cp_lib.clean_ini": [], - path_name = os.path.join("cp_lib", "clean_ini.py") - logging.info("get_file_dependency_list(\"{}\") test a built-in".format(path_name)) - expect = [] - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - # "cp_lib.cp_logging": ["cp_lib.hw_status", "cp_lib.load_settings"], - path_name = os.path.join("cp_lib", "cp_logging.py") - logging.info("get_file_dependency_list(\"{}\") test a built-in".format(path_name)) - expect = ["cp_lib.hw_status", "cp_lib.load_settings", "cp_lib.clean_ini", "cp_lib.split_version"] - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - # "cp_lib.hw_status": [], - path_name = os.path.join("cp_lib", "hw_status.py") - logging.info("get_file_dependency_list(\"{}\") test a built-in".format(path_name)) - expect = [] - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - # "cp_lib.load_settings": ["cp_lib.clean_ini", "cp_lib.split_version"], - path_name = os.path.join("cp_lib", "load_settings.py") - logging.info("get_file_dependency_list(\"{}\") test a built-in".format(path_name)) - expect = ["cp_lib.clean_ini", "cp_lib.split_version"] - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - # "cp_lib.split_version": [] - path_name = os.path.join("cp_lib", "split_version.py") - logging.info("get_file_dependency_list(\"{}\") test a built-in".format(path_name)) - expect = [] - obj = BuildDependencyList() - result = obj.add_file_dependency(path_name) - self.assertEqual(result, expect) - - return - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_probe_serial.py b/test/test_probe_serial.py deleted file mode 100644 index e3ce71d1..00000000 --- a/test/test_probe_serial.py +++ /dev/null @@ -1,112 +0,0 @@ -# Test the cp_lib.probe_serial module - -import json -import unittest - -import cp_lib.probe_serial as probe_serial - -PPRINT_RESULT = True - - -class TestSerialRedirectorConfig(unittest.TestCase): - - def test_serial_redirect(self): - global base_app - - print("") # skip paste '.' on line - - obj = probe_serial.SerialRedirectorConfig(base_app) - base_app.logger.info("Fetch {}".format(obj.topic)) - result = obj.refresh() - - self.assertIsNotNone(result) - - if PPRINT_RESULT: - print(json.dumps(result, indent=4, sort_keys=True)) - - if True: - # these would only be true in some situations - expect_port = "ttyS1" - self.assertFalse(obj.enabled()) - self.assertFalse(obj.enabled(expect_port)) - self.assertFalse(obj.enabled("fifth_port")) - self.assertEqual(obj.port_name(), expect_port) - - return - - def test_serial_gpio(self): - global base_app - - print("") # skip paste '.' on line - - obj = probe_serial.SerialGPIOConfig(base_app) - base_app.logger.info("Fetch {}".format(obj.topic)) - result = obj.refresh() - - self.assertIsNotNone(result) - - if PPRINT_RESULT: - print(json.dumps(result, indent=4, sort_keys=True)) - - if True: - # these would only be true in some situations - self.assertFalse(obj.enabled()) - - return - - def test_serial_gps(self): - global base_app - - print("") # skip paste '.' on line - - obj = probe_serial.SerialGpsConfig(base_app) - base_app.logger.info("Fetch {}".format(obj.topic)) - result = obj.refresh() - - self.assertIsNotNone(result) - - if PPRINT_RESULT: - print(json.dumps(result, indent=4, sort_keys=True)) - - if True: - # these would only be true in some situations - expect_port = "ttyS1" - if False: - # for when is true in actual config of 1100 - self.assertTrue(obj.enabled()) - self.assertTrue(obj.enabled(expect_port)) - else: - self.assertFalse(obj.enabled()) - self.assertFalse(obj.enabled(expect_port)) - - # this one will always be False - self.assertFalse(obj.enabled("fifth_port")) - - return - - def test_probe_if_serial_available(self): - global base_app - - print("") # skip paste '.' on line - - result = probe_serial.probe_if_serial_available(base_app) - - if PPRINT_RESULT: - print(json.dumps(result, indent=4, sort_keys=True)) - - # these would only be true in some situations - expect_port = "ttyS1" - result = probe_serial.probe_if_serial_available(base_app,expect_port) - - if PPRINT_RESULT: - print(json.dumps(result, indent=4, sort_keys=True)) - - return - - -if __name__ == '__main__': - from cp_lib.app_base import CradlepointAppBase - - base_app = CradlepointAppBase() - - unittest.main() diff --git a/test/test_split_version.py b/test/test_split_version.py deleted file mode 100644 index 257857f0..00000000 --- a/test/test_split_version.py +++ /dev/null @@ -1,214 +0,0 @@ -# Test the cp_lib.split.version module - -import logging -import unittest - - -class TestSplitVersion(unittest.TestCase): - - def test_split_version(self): - """ - Test the raw/simple handling of 1 INI to JSON in any directory - :return: - """ - from cp_lib.split_version import split_version_string - - tests = [ - (None, None, None, None, None), - (None, "3", 3, 0, 0), - (None, "3.4", 3, 4, 0), - (None, "3.4.7", 3, 4, 7), - ("9.65", None, 9, 65, 0), - ("9.65", "3", 9, 65, 0), - ("9.65", "3.4", 9, 65, 0), - ("9.65", "3.4.7", 9, 65, 0), - ("9.65.beta", None, 9, 65, "beta"), - ("9.65.B", "3", 9, 65, "B"), - ("9.65.beta", "3.4", 9, 65, "beta"), - - # bad types - (9.65, None, TypeError, 65, 0), - (9, None, TypeError, 65, 0), - ] - - for test in tests: - # logging.debug("Test:{}".format(test)) - source = test[0] - source_default = test[1] - expect_major = test[2] - expect_minor = test[3] - expect_patch = test[4] - - if expect_major == TypeError: - with self.assertRaises(TypeError): - split_version_string(source) - - else: - if source_default is None: - major, minor, patch = split_version_string(source) - else: - major, minor, patch = split_version_string(source, source_default) - - self.assertEqual(major, expect_major) - self.assertEqual(minor, expect_minor) - self.assertEqual(patch, expect_patch) - - return - - def test_split_version_dict(self): - """ - :return: - """ - from cp_lib.split_version import split_version_save_to_dict, SETS_NAME_MAJOR, SETS_NAME_MINOR, SETS_NAME_PATCH - - tests = [ - (None, None, None, None, None), - (None, "3", 3, 0, 0), - (None, "3.4", 3, 4, 0), - (None, "3.4.7", 3, 4, 7), - ("9.65", None, 9, 65, 0), - ("9.65", "3", 9, 65, 0), - ("9.65", "3.4", 9, 65, 0), - ("9.65", "3.4.7", 9, 65, 0), - ("9.65.beta", None, 9, 65, "beta"), - ("9.65.B", "3", 9, 65, "B"), - ("9.65.beta", "3.4", 9, 65, "beta"), - ] - - base = dict() - - for test in tests: - # logging.debug("Test:{}".format(test)) - source = test[0] - source_default = test[1] - expect_major = test[2] - expect_minor = test[3] - expect_patch = test[4] - - if SETS_NAME_MAJOR in base: - base.pop(SETS_NAME_MAJOR) - - if SETS_NAME_MINOR in base: - base.pop(SETS_NAME_MINOR) - - if SETS_NAME_PATCH in base: - base.pop(SETS_NAME_PATCH) - - self.assertFalse(SETS_NAME_MAJOR in base) - self.assertFalse(SETS_NAME_MINOR in base) - self.assertFalse(SETS_NAME_PATCH in base) - - if expect_major == TypeError: - with self.assertRaises(TypeError): - split_version_save_to_dict(source, base) - - else: - if source_default is None: - split_version_save_to_dict(source, base) - else: - split_version_save_to_dict(source, base, source_default) - - self.assertEqual(base[SETS_NAME_MAJOR], expect_major) - self.assertEqual(base[SETS_NAME_MINOR], expect_minor) - self.assertEqual(base[SETS_NAME_PATCH], expect_patch) - - base = dict() - base["fw_info"] = dict() - - for test in tests: - # logging.debug("Test:{}".format(test)) - source = test[0] - source_default = test[1] - expect_major = test[2] - expect_minor = test[3] - expect_patch = test[4] - - if SETS_NAME_MAJOR in base["fw_info"]: - base["fw_info"].pop(SETS_NAME_MAJOR) - - if SETS_NAME_MINOR in base["fw_info"]: - base["fw_info"].pop(SETS_NAME_MINOR) - - if SETS_NAME_PATCH in base["fw_info"]: - base["fw_info"].pop(SETS_NAME_PATCH) - - self.assertFalse(SETS_NAME_MAJOR in base["fw_info"]) - self.assertFalse(SETS_NAME_MINOR in base["fw_info"]) - self.assertFalse(SETS_NAME_PATCH in base["fw_info"]) - - if expect_major == TypeError: - with self.assertRaises(TypeError): - split_version_save_to_dict(source, base, section="fw_info") - - else: - if source_default is None: - split_version_save_to_dict(source, base, section="fw_info") - else: - split_version_save_to_dict(source, base, source_default, section="fw_info") - - self.assertEqual(base["fw_info"][SETS_NAME_MAJOR], expect_major) - self.assertEqual(base["fw_info"][SETS_NAME_MINOR], expect_minor) - self.assertEqual(base["fw_info"][SETS_NAME_PATCH], expect_patch) - - return - - def test_sets_version_str(self): - """ - :return: - """ - from cp_lib.split_version import split_version_save_to_dict, sets_version_to_str - - tests = [ - (None, None, None), - (None, "3", "3.0.0"), - (None, "3.4", "3.4.0"), - (None, "3.4.7", "3.4.7"), - ("9.65", None, "9.65.0"), - ("9.65", "3", "9.65.0"), - ("9.65", "3.4", "9.65.0"), - ("9.65", "3.4.7", "9.65.0"), - ("9.65.beta", None, "9.65.beta"), - ("9.65.B", "3", "9.65.B"), - ("9.65.beta", "3.4", "9.65.beta"), - ] - - base = dict() - - for test in tests: - # logging.debug("Test:{}".format(test)) - source = test[0] - source_default = test[1] - expect = test[2] - - if source_default is None: - split_version_save_to_dict(source, base) - else: - split_version_save_to_dict(source, base, source_default) - - result = sets_version_to_str(base) - self.assertEqual(result, expect) - - base = dict() - base["fw_info"] = dict() - - for test in tests: - # logging.debug("Test:{}".format(test)) - source = test[0] - source_default = test[1] - expect = test[2] - - if source_default is None: - split_version_save_to_dict(source, base, section="fw_info") - else: - split_version_save_to_dict(source, base, source_default, section="fw_info") - - result = sets_version_to_str(base, section="fw_info") - self.assertEqual(result, expect) - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_status_tree_data.py b/test/test_status_tree_data.py deleted file mode 100644 index 873a1f9f..00000000 --- a/test/test_status_tree_data.py +++ /dev/null @@ -1,61 +0,0 @@ -# Test the cp_lib.status_tree_data module - -import logging -import unittest -import uuid - -from cp_lib.app_base import CradlepointAppBase -from cp_lib.status_tree_data import StatusTreeData - -# set to None to get random one -USE_UUID = "ae151650-4ce9-4337-ab6b-a16f886be569" - - -class TestStatusTreeData(unittest.TestCase): - - def test_startup(self): - """ - :return: - """ - global USE_UUID - - print() # move past the '.' - - app_base = CradlepointAppBase("simple.do_it") - if USE_UUID is None: - USE_UUID = str(uuid.uuid4()) - - app_base.logger.debug("UUID:{}".format(USE_UUID)) - - obj = StatusTreeData(app_base) - try: - obj.set_uuid(USE_UUID) - - except ValueError: - app_base.logger.error("No APPS installed") - raise - - obj.clear_data() - # if True: - # return - - # if this is a FRESH system, then first get will be NULL/NONE - result = obj.get_data() - self.assertEqual(obj.data, dict()) - - result = obj.put_data(force=True) - app_base.logger.debug("data:{}".format(result)) - self.assertEqual(obj.clean, True) - - obj.set_data_value('health', 100.0) - self.assertEqual(obj.clean, False) - - result = obj.put_data(force=True) - app_base.logger.debug("data:{}".format(result)) - - return - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_target.py b/test/test_target.py deleted file mode 100644 index 80a389d9..00000000 --- a/test/test_target.py +++ /dev/null @@ -1,125 +0,0 @@ -# Test the TARGET tool - -import unittest -import logging -# noinspection PyUnresolvedReferences -import shutil - - -class TestTarget(unittest.TestCase): - - def test_load_ini(self): - from tools.target import TheTarget - - print("") - - data = [ - "; think of these as 'flavors' to apply to the settings.ini", "", - "[AER3100]", "; user_name=admin", "interface=ENet USB-1", "local_ip=192.168.30.1", "password=lin805", - "", "[AER2100]", "local_ip=192.168.21.1", "password=4416ec79", - "", "[AER1600]", "local_ip=192.168.16.1", "password=441ec8f9", "" - ] - - expect = { - "AER3100": {"interface": "ENet USB-1", "local_ip": "192.168.30.1", "password": "lin805"}, - "AER2100": {"local_ip": "192.168.21.1", "password": "4416ec79"}, - "AER1600": {"local_ip": "192.168.16.1", "password": "441ec8f9"}, - } - - file_name = "test/tmp_target.ini" - - # write the TARGET.INI to load - _han = open(file_name, 'w') - for line in data: - _han.write(line + "\n") - _han.close() - - target = TheTarget() - result = target.load_target_ini(file_name) - - logging.debug("result:{}".format(result)) - self.assertEqual(expect, result) - - return - - def test_find_interface_ip(self): - import ipaddress - from tools.target import TheTarget - - print("") - - report = [ - "", - "Configuration for interface \"ENet MB\"", - " IP Address: 192.168.0.10", - " Subnet Prefix: 192.168.0.0/24 (mask 255.255.255.0)", - " Default Gateway: 192.168.0.1", - "", - "Configuration for interface \"ENet USB-1\"", - " IP Address: 192.168.30.6", - " Subnet Prefix: 192.168.30.0/24 (mask 255.255.255.0)", - "" - "Configuration for interface \"ENet Virtual\"", - " Subnet Prefix: 192.168.30.0/24 (mask 255.255.255.0)", - "" - ] - - target = TheTarget() - - interface = "ENet MB" - expect = ipaddress.IPv4Address("192.168.0.10") - result, network = target.get_interface_ip_info(interface, report) - self.assertEqual(expect, result) - - interface = "ENet USB-1" - expect = ipaddress.IPv4Address("192.168.30.6") - result, network = target.get_interface_ip_info(interface, report) - self.assertEqual(expect, result) - - interface = "Other Port" - expect = None - result, network = target.get_interface_ip_info(interface, report) - self.assertEqual(expect, result) - - interface = "ENet Virtual" - expect = None - result, network = target.get_interface_ip_info(interface, report) - self.assertEqual(expect, result) - - return - - def test_ip_hacking(self): - from tools.target import TheTarget - - print("") - - tests = [ - {"src": "192.168.30.6", "exp": "192.168.30.6"}, - {"src": "192.168.30.0/24", "exp": "192.168.30.0"}, - {"src": "192.168.30.6:8080", "exp": "192.168.30.6"}, - ] - - target = TheTarget() - - for test in tests: - logging.debug("Test:{}".format(test)) - result = target.trim_ip_to_4(test["src"]) - self.assertEqual(test["exp"], result) - - return - - def test_whoami(self): - from tools.target import TheTarget - - print("") - - result = TheTarget.get_whoami() - logging.debug("WhoAmI:[{}]".format(result)) - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/test/test_time_until.py b/test/test_time_until.py deleted file mode 100644 index fe94dace..00000000 --- a/test/test_time_until.py +++ /dev/null @@ -1,33 +0,0 @@ -# Test the cp_lib.app_base module - -import logging -import time -import unittest - -import cp_lib.time_until as time_until - - -class TestTimeUntil(unittest.TestCase): - - def test_seconds_until_next_minute(self): - """ - :return: - """ - - print() # move past the '.' - - now = time.time() - logging.debug("Now = {} sec".format(time.asctime())) - - result = time_until.seconds_until_next_minute(now) - logging.debug("Result = {} sec".format(result)) - - # self.assertTrue(os.path.exists(expect)) - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main() diff --git a/tools/RouterSDKDemo.py b/tools/RouterSDKDemo.py deleted file mode 100644 index c3cbc3e7..00000000 --- a/tools/RouterSDKDemo.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Router SDK Demo Application.""" - -import json -import subprocess -import time - - -class CSClient(object): - """A wrapper class for the csclient executable.""" - - def get(self, path): - """Formulate a get command for the csclient executable.""" - cmd = "csclient -m get -b {}".format(path) - return self.dispatch(cmd) - - def put(self, path, data): - """Formulate a put command for the csclient executable.""" - data = json.dumps(data).replace(' ', '') - cmd = "csclient -m put -b {} -v {}".format(path, data) - return self.dispatch(cmd) - - def append(self, path, data): - """Formulate an append command for the csclient executable.""" - data = json.dumps(data).replace(' ', '') - cmd = "csclient -m append -b {} -v {}".format(path, data) - return self.dispatch(cmd) - - def delete(self, path, data): - """Formulate a delete command for the csclient executable.""" - data = json.dumps(data).replace(' ', '') - cmd = "csclient -m delete -b {} -v {}".format(path, data) - return self.dispatch(cmd) - - def dispatch(self, cmd): - """Dispatch the csclient executable command via the shell.""" - result, err = subprocess.Popen(cmd.split(' '), - stdout=subprocess.PIPE).communicate() - return result.decode() - - -class GPIO(object): - """A class that represents a GPIO pin.""" - - LOW = 0 - HIGH = 1 - - def __init__(self, client, name, initial_state=LOW): - """GPIO class initialization.""" - self.client = client - self.name = name - self.state = initial_state - self.set(self.state) - - def get(self): - """Request and return the state of the GPIO pin.""" - self.state = self.client.get('/status/gpio/%s' % self.name) - return self.state - - def set(self, state): - """Set the state of the GPIO pin.""" - self.state = state - self.client.put('/control/gpio', {self.name: self.state}) - - def toggle(self): - """Toggle the state of the GPIO pin.""" - self.set(self.LOW if self.state else self.HIGH) - return self.state - - -def run(): - """Application entry point.""" - client = CSClient() - led = GPIO(client, "LED_USB1_G") - - while True: - time.sleep(.5) - led.toggle() - - -if __name__ == '__main__': - run() diff --git a/tools/RouterSDKDemo.tar.gz b/tools/RouterSDKDemo.tar.gz deleted file mode 100644 index d83950c1..00000000 Binary files a/tools/RouterSDKDemo.tar.gz and /dev/null differ diff --git a/tools/__init__.py b/tools/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/gnu_apps/extensible_ui_ref_app/tools/bin/package_application.py b/tools/bin/package_application.py similarity index 78% rename from gnu_apps/extensible_ui_ref_app/tools/bin/package_application.py rename to tools/bin/package_application.py index 2ca76204..8badc71b 100644 --- a/gnu_apps/extensible_ui_ref_app/tools/bin/package_application.py +++ b/tools/bin/package_application.py @@ -10,6 +10,8 @@ import subprocess import sys import uuid +import tarfile +import gzip from OpenSSL import crypto META_DATA_FOLDER = 'METADATA' @@ -35,17 +37,38 @@ def hash_dir(target, hash_func=hashlib.sha256): hashed_files = {} for path, d, f in os.walk(target): for fl in f: - fully_qualified_file = os.path.join(path, fl) - hashed_files[fully_qualified_file[len(target) + 1:]] =\ - file_checksum(hash_func, fully_qualified_file) + if not fl.startswith('.'): + # we need this be LINUX fashion! + if sys.platform == "win32": + # swap the network\\tcp_echo to be network/tcp_echo + fully_qualified_file = path.replace('\\', '/') + '/' + fl + else: # else allow normal method + fully_qualified_file = os.path.join(path, fl) + hashed_files[fully_qualified_file[len(target) + 1:]] =\ + file_checksum(hash_func, fully_qualified_file) + else: + print("Did not include {} in the App package.".format(fl)) return hashed_files def pack_package(app_root, app_name): - tar_cmd = "tar czf {}.tar.gz -C {} {}" \ - .format(app_name, os.path.dirname(app_root), app_name, app_name) - return subprocess.check_call(tar_cmd.split(' ')) + print('app_root: {}'.format(app_root)) + print('app_name: {}'.format(app_name)) + print("pack TAR:%s.tar" % app_name) + tar_name = "{}.tar".format(app_name) + tar = tarfile.open(tar_name, 'w') + tar.add(app_root, arcname=os.path.basename(app_root)) + tar.close() + + print("gzip archive:%s.tar.gz" % app_name) + gzip_name = "{}.tar.gz".format(app_name) + with open(tar_name, 'rb') as f_in: + with gzip.open(gzip_name, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + + if os.path.isfile(tar_name): + os.remove(tar_name) def create_signature(meta_data_folder, pkey): @@ -86,7 +109,7 @@ def package_application(app_root, pkey): config = configparser.ConfigParser() config.read(app_config_file) if not os.path.exists(app_metadata_folder): - os.makedirs(app_metadata_folder) + os.makedirs(app_metadata_folder) for section in config.sections(): app_name = section diff --git a/tools/bin/pscp.exe b/tools/bin/pscp.exe new file mode 100644 index 00000000..f1dc0950 Binary files /dev/null and b/tools/bin/pscp.exe differ diff --git a/tools/bin/pscp_license.txt b/tools/bin/pscp_license.txt new file mode 100644 index 00000000..64e0dcba --- /dev/null +++ b/tools/bin/pscp_license.txt @@ -0,0 +1,7 @@ +From http://www.chiark.greenend.org.uk/~sgtatham/putty/licence.html + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/tools/syslog_server.py b/tools/bin/syslog_server.py similarity index 100% rename from tools/syslog_server.py rename to tools/bin/syslog_server.py diff --git a/gnu_apps/extensible_ui_ref_app/tools/bin/validate_application.py b/tools/bin/validate_application.py similarity index 100% rename from gnu_apps/extensible_ui_ref_app/tools/bin/validate_application.py rename to tools/bin/validate_application.py diff --git a/tools/convert_eol.py b/tools/convert_eol.py deleted file mode 100644 index 696315f2..00000000 --- a/tools/convert_eol.py +++ /dev/null @@ -1,50 +0,0 @@ -import os - - -def convert_eol_linux(base_directory): - """ - Given a sub-directory, walk it and convert EOL in all "text" files - - :param str base_directory: - :return: - """ - if not os.path.isdir(base_directory): - raise FileNotFoundError("dir {} doesn't exist".format(base_directory)) - - for path_name, dir_name, file_name in os.walk(base_directory): - for name in file_name: - # see about converting the file's EOL - if name.endswith(".py"): - pass - elif name.endswith(".ini"): - pass - elif name.endswith(".json"): - pass - elif name.endswith(".sh"): - pass - else: - print("Pass name:[{}]".format(name)) - continue - - # read in all the old lines as binary - full_path_file = os.path.join(path_name, name) - print("read in file:[{}]".format(full_path_file)) - - lines = [] - file_han = open(full_path_file, "rb") - for line in file_han: - lines.append(line.rstrip()) - file_han.close() - - print("write out file:[{}]".format(full_path_file)) - file_han = open(full_path_file, "wb") - for line in lines: - file_han.write(line + b'\n') - file_han.close() - - return True - -if __name__ == '__main__': - import sys - - convert_eol_linux(sys.argv[1]) diff --git a/tools/copy_file_nl.py b/tools/copy_file_nl.py deleted file mode 100644 index 89f119f1..00000000 --- a/tools/copy_file_nl.py +++ /dev/null @@ -1,51 +0,0 @@ -import logging -import os - - -def copy_file_nl(source_name, destination_name, discard_empty=False): - """ - Mimic "shutil.copyfile(src_name, dst_name)", however if the file ends in .py, .ini, .json, or .sh, then - make sure EOL is LINUX style "\n" - - :param str source_name: the source file name - :param str destination_name: the destination FILE name (cannot be the sub-directory!) - :param bool discard_empty: if T, we skip copying an empty file. - :return: - """ - # logging.debug("copy file {0} to {1}".format(source_name, destination_name)) - if not os.path.isfile(source_name): - # the source file is missing - raise FileNotFoundError - - # first, see if we should SKIP this file - if discard_empty: - stat_data = os.stat(source_name) - if stat_data.st_size <= 0: - logging.debug("Skip Source({}) because is empty".format(source_name)) - return - - # logging.debug("read in file:[{}]".format(source_name)) - lines = [] - file_han = open(source_name, "rb") - for line in file_han: - lines.append(line) - file_han.close() - - if source_name.endswith(".py") or source_name.endswith(".ini") or source_name.endswith(".json") or \ - source_name.endswith(".sh"): - # then we make sure line ends in - clear_eol = True - else: - clear_eol = False - - # logging.debug("write out file:[{}]".format(destination_name)) - file_han = open(destination_name, "wb") - for line in lines: - if clear_eol: - # this is text file, force to be Linux style '\n' - file_han.write(line.rstrip() + b'\n') - else: # else write as is, with 'line' being an unknown quantity - file_han.write(line) - file_han.close() - - return diff --git a/tools/curl.exe b/tools/curl.exe deleted file mode 100644 index 7ec89f0c..00000000 Binary files a/tools/curl.exe and /dev/null differ diff --git a/tools/curl_task.bat b/tools/curl_task.bat deleted file mode 100644 index 91658dd0..00000000 --- a/tools/curl_task.bat +++ /dev/null @@ -1 +0,0 @@ -curl -v --digest -u admin:franklin805 -X PUT http://192.168.30.1/api/config/system/logging/level -d data="debug" diff --git a/tools/get-pip.py b/tools/get-pip.py deleted file mode 100644 index bdd757ab..00000000 --- a/tools/get-pip.py +++ /dev/null @@ -1,19154 +0,0 @@ -#!/usr/bin/env python -# -# Hi There! -# You may be wondering what this giant blob of binary data here is, you might -# even be worried that we're up to something nefarious (good for you for being -# paranoid!). This is a base85 encoding of a zip file, this zip file contains -# an entire copy of pip. -# -# Pip is a thing that installs packages, pip itself is a package that someone -# might want to install, especially if they're looking to run this get-pip.py -# script. Pip has a lot of code to deal with the security of installing -# packages, various edge cases on various platforms, and other such sort of -# "tribal knowledge" that has been encoded in its code base. Because of this -# we basically include an entire copy of pip inside this blob. We do this -# because the alternatives are attempt to implement a "minipip" that probably -# doesn't do things correctly and has weird edge cases, or compress pip itself -# down into a single file. -# -# If you're wondering how this is created, it is using an invoke task located -# in tasks/generate.py called "installer". It can be invoked by using -# ``invoke generate.installer``. - -import os.path -import pkgutil -import shutil -import sys -import struct -import tempfile - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -if PY3: - iterbytes = iter -else: - def iterbytes(buf): - return (ord(byte) for byte in buf) - -try: - from base64 import b85decode -except ImportError: - _b85alphabet = (b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - b"abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~") - - def b85decode(b): - _b85dec = [None] * 256 - for i, c in enumerate(iterbytes(_b85alphabet)): - _b85dec[c] = i - - padding = (-len(b)) % 5 - b = b + b'~' * padding - out = [] - packI = struct.Struct('!I').pack - for i in range(0, len(b), 5): - chunk = b[i:i + 5] - acc = 0 - try: - for c in iterbytes(chunk): - acc = acc * 85 + _b85dec[c] - except TypeError: - for j, c in enumerate(iterbytes(chunk)): - if _b85dec[c] is None: - raise ValueError( - 'bad base85 character at position %d' % (i + j) - ) - raise - try: - out.append(packI(acc)) - except struct.error: - raise ValueError('base85 overflow in hunk starting at byte %d' - % i) - - result = b''.join(out) - if padding: - result = result[:-padding] - return result - - -def bootstrap(tmpdir=None): - # Import pip so we can use it to install pip and maybe setuptools too - import pip - from pip.commands.install import InstallCommand - from pip.req import InstallRequirement - - # Wrapper to provide default certificate with the lowest priority - class CertInstallCommand(InstallCommand): - def parse_args(self, args): - # If cert isn't specified in config or environment, we provide our - # own certificate through defaults. - # This allows user to specify custom cert anywhere one likes: - # config, environment variable or argv. - if not self.parser.get_default_values().cert: - self.parser.defaults["cert"] = cert_path # calculated below - return super(CertInstallCommand, self).parse_args(args) - - pip.commands_dict["install"] = CertInstallCommand - - implicit_pip = True - implicit_setuptools = True - implicit_wheel = True - - # Check if the user has requested us not to install setuptools - if "--no-setuptools" in sys.argv or os.environ.get("PIP_NO_SETUPTOOLS"): - args = [x for x in sys.argv[1:] if x != "--no-setuptools"] - implicit_setuptools = False - else: - args = sys.argv[1:] - - # Check if the user has requested us not to install wheel - if "--no-wheel" in args or os.environ.get("PIP_NO_WHEEL"): - args = [x for x in args if x != "--no-wheel"] - implicit_wheel = False - - # We only want to implicitly install setuptools and wheel if they don't - # already exist on the target platform. - if implicit_setuptools: - try: - import setuptools # noqa - implicit_setuptools = False - except ImportError: - pass - if implicit_wheel: - try: - import wheel # noqa - implicit_wheel = False - except ImportError: - pass - - # We want to support people passing things like 'pip<8' to get-pip.py which - # will let them install a specific version. However because of the dreaded - # DoubleRequirement error if any of the args look like they might be a - # specific for one of our packages, then we'll turn off the implicit - # install of them. - for arg in args: - try: - req = InstallRequirement.from_line(arg) - except: - continue - - if implicit_pip and req.name == "pip": - implicit_pip = False - elif implicit_setuptools and req.name == "setuptools": - implicit_setuptools = False - elif implicit_wheel and req.name == "wheel": - implicit_wheel = False - - # Add any implicit installations to the end of our args - if implicit_pip: - args += ["pip"] - if implicit_setuptools: - args += ["setuptools"] - if implicit_wheel: - args += ["wheel"] - - delete_tmpdir = False - try: - # Create a temporary directory to act as a working directory if we were - # not given one. - if tmpdir is None: - tmpdir = tempfile.mkdtemp() - delete_tmpdir = True - - # We need to extract the SSL certificates from requests so that they - # can be passed to --cert - cert_path = os.path.join(tmpdir, "cacert.pem") - with open(cert_path, "wb") as cert: - cert.write(pkgutil.get_data("pip._vendor.requests", "cacert.pem")) - - # Execute the included pip and use it to install the latest pip and - # setuptools from PyPI - sys.exit(pip.main(["install", "--upgrade"] + args)) - finally: - # Remove our temporary directory - if delete_tmpdir and tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -def main(): - tmpdir = None - try: - # Create a temporary working directory - tmpdir = tempfile.mkdtemp() - - # Unpack the zipfile into the temporary directory - pip_zip = os.path.join(tmpdir, "pip.zip") - with open(pip_zip, "wb") as fp: - fp.write(b85decode(DATA.replace(b"\n", b""))) - - # Add the zipfile to sys.path so that we can import it - sys.path.insert(0, pip_zip) - - # Run the bootstrap - bootstrap(tmpdir=tmpdir) - finally: - # Clean up our temporary working directory - if tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -DATA = b""" -P)h>@6aWAK2mly|Wk_|XSV~_F001E<000jF003}la4%n9X>MtBUtcb8d7WDSZ`-yK|J{ED>nxD8%G$E -w;SJgIu%S({0^J&P#wbpYJnS!bH_kszWzd@` -;vPi#p*CD_%FEDwUP}x>QY86dAs}lCqL9dr|FnS(%%sYf;mkW70OF=;$}RNrmRY^E4N#kz(1Bh?oXzS -#3_x=De1CMWSPdq^VFT&qb3h3+`z4D<@4fG23yuYNg1En5lNT=TZ@aW%en{Bx7f+tbWTJ`7%?StR*d% -7gf0yo3~_$YZ{5!hdKvE%FRo@&f20;Kk}k}Rmok}G~8SJO53KleAU7`DY#j%5<5|3GPP4r6WeDn+p2N -X(mgNpN+y#3)^gRTq|R1%*@mm6l|>t~4M8E|L!oMjMhXH1l$0sjbvGn -*~PWwBkAw*`;c)Go9$!!ED%YNiZ?OI(5ydt?OGVW1fYCxKTRRQA4-cMO86=CU||B;cPxz>B7(KuT=fl -fgUA6L-tz$78f?&P*iXN?OHp&6_jDo3>8s11W5mLw!q5hPrqmEWmN`^rR7Yl(Vr4xT;Y3?1MWo*slPl?aLow_g6 -r4U=?@>?zfE7f{ptG6)%&Y}0uWcO5`c6et5VwyF9|UsOL|;EfaVxvZr*KxOwwQ8-n@JlED}{0q6wqhi -_3+h00L1q;ehA`QUwcb)J7hhOz^J6T%H?|x0=t4U1+u%Bp|MG{y+#mMj8a#4ZfS^>=u%P+zr17vWRb# -YdyFNBQ40~)$PJ~z_A-J=9!K5zj&dU`Exzx>OE4SD|z(WIAXLKVyQY?Ti77Q!chtX4JL#A -C-k#*bByk)*;9#Nzt2q>8``c2#<@LD$NUNqY$~M+cR2CsEG_ -A_wFn=#wex!j}6p|!&{NSrwtRh;C9kWSoQV3FV$f~!f1h=Cy4=sbf{%0r`fJ@R&h+fwY=*VNXMi(q$N -5{=<0J42ct{ve}E#Fp;Ov9^|h%God8jSnFHIhf@FuzQ>JqneD=Rpeu&%yB_Jm8*}!f%SMD>q>jpjG59}%6o?Y3OP#s?d>o!G -_5lFWSATP@&=vTmDEfp?uiygL$i=ckb26*-B>yik2`-&Xg!;xYS&)eFH#7sVxCaiw5~^bz$0RAR|8Iy8V$8 -%x5Edos0Poi=uCBdfMniva_RR>KU6#)O5H7SI?!>Ybw^glRl@z{{@~o|B%(+3u7@R-@_a8B9DqX|>}5 -79_t`q6}{zT!Q&pVV;ZzZlo9ALp~~t$0~zMuXsrB?!ijGcZvk9g1#((P5)zgj*Tw+kCFZzyETEb2x2! -s%kvbMnd(8He3e1Kc{A3w=Y+9Gds0ACM};EXVqdA4Ja5KsFTY+Gy#ax8-QjI`$t9FlKFGs6i|?*=Om@&ir+yNK-k@Ql2V%35-l{Mjd -el}*bqJFx(vtF}ix+lRR`<%TL`SmSx~V#(ZW3z-uA3-ZZP?wstLs71QJqioJUYasAXu>H -@N+)<>fZ{CPqr5^`_<$<+|iDA%ze>|r5;bnB9qeIdt(XRVHrA;uJnM`3aO&F?1%JdnQzSVbJal2Y^Gw -(tk@%A9b1b_t{@L}b1IZIH5PP2;hsgiKFu^IAhcg$V^@vE6?=sDb%5;F@h0Gg -l-YsO3SiUeJPJ7%*+>^M|2^M3^~3v3Awx3d{`!Fc!j=hyG6s!)M#*Q{cx-Mf2?fYZVklhPguQ=Y>7h; -!^X;5m$r0{YRm)}^75Hk+9wwN2O&Gn>O+{|AY!njExF?xkpW*hi1(^hAK6LX=$bx<83Fh)yGfo-2_k`tO*XEIv3tqb62nR2`Qox$%SjxD5bYA6R2mw~-_^xY+eN84-;cncv -Lw?Ecl-aaU=va6-I_3;553f$xsSQCzTOv4hY}xc&O%X3Fi7Q;t$ta{>3jrTn$ppLXu;lDewQR>TC}}M -(BJ*U+On~kEvuvmsX0~Pkj*)|XNMN)aB`cN+*bMYd*;$e1Xe-{@^BeOzk$hL}`LQ7ka@n_WH{TW=TfV -wWg{lgGtK5e&`wrw$nb5gwI-A+CDY}c2-`UWS;f~SM!j2i)2#zFij^`v+?ApMVqSF({*sLAH4sQ2!fM -ram>xUM=tZJoyu-|=~FbB;F+mL1po84y1^dYG-+e=527+^^aZK~(lFz+8;PDW0~2Bx-H<2DQbB*}i_z ->{BiHGmT?r9p}5)y>V@n@ia4rVw&4cYr8v?xToeo9$Cwc9$8U_1OFeqji!<*$6zKtvhmuJLODY>H_bDqgH(rX2)*UCit!pP1A -KYBdid4+D`PG4RzvS~mbJ;641?e{Gqw#QqoY>GX#y;4!77#xx58DS^jEMLa{;i9m#YKxYUQ2sY=I|Vrd+a>a|tCN9s&+ZX* -#2_1d)qEZ{^+r-PrdwxHTWO5H+4$48$%2f6PIj@XkJC<+JTcuh46It>6_>8n?TsxeP3DvSU7;vIrO!?AUV*>s#8o)IDwdra7t -opp4*KaEk@0rBuqO&f_0!Ts0ZHXIos{NGu0@S63ZQc9dNBHC7MbeT%@7E7=pFRhG*wf=HkKi}zhu{ -fRq&0DkDB0v`^F|eE{p2^iV9F+8U<+|hZhzv?UN(I^Agkgu=aT#WY+Lsp2K*SNig#io)!&t#DERJscj -2G6rq7Sw%##x@WiDMP~ey;vRd~tdC7fc^1B^+yn-QZk(f3D -8>W-~V@SSNMyQtx|XYDg;jO>XzgsPO$d`ltd7dfzIbPOQ(cpkZj -MvLLaGB&D0J%s)VImU{Ift;^KQCWvymY2D)`$=?M6DC54}%GX2n$Hd!2)7q-h*HowW;X58w;$H2{4jj -NfP699jC5Mz}kM#ADO2*DOSM<_X$_2uurn!r9PY2~<=6T@Rh_r2617HJ|dk#*Z2^sKx&eP)h>@6aWAK -2mly|Wk^d&q-8S!002k=000jF003}la4%n9ZDDC{Utcb8d0mlDPs1<_#qa$TmUd`|M#mXx2aa43_b9Z -<8j&U;c2)N6v9pv7a>}3gp6y?3jx#7_7h1AW3f!DxCeR-k{X#}v&rMVBBW^pqVAJg2)xv5(X9IZ&!G$ -weNz|}T#`pv1aRi#o^#~Nj*C$OAd*3IApuLYP7R`eCY#?1|iUE>Kj}QGi`6KVZ1teD9ZX0L0?WBiK_H -f^)6SVDf1H5)JkMm_3{F`%BYSu0&)u&6A1=dP)h>@6aWAK2mly|Wk^UaCXK26eCo$!2jj`+p}EFh@OXvddO2BK&w*Hm?x~3q87 ->Z?a9g6_4SOLU0q#V0WYf`Ru@07sQvlV^6KXH?fbLUPjq#0L(ea6SEojnmse*$EiZ1b>ATzI`%`+lys -|i>X+1AkbXHB6utG3nA~UjOiWY)zXC%j!X6b(z1)=U*xS^G9z3>E91_M3vF$myAiS{Rp0bl5W3A8C1? -^yg{G#XI~!UMNjkh_W1{p23TaWYWV!s#>wF5m01fYLnLgDZj^jI{GQqY3!>R~D=3TuTLk0Dr}Erhk_b -9K|2lr3tJ6M$V&5njx5sN+npdmoOfT*`u1jdIhcvYD-`_;~8m=F(Jml2jV7d192kFQ$=YYS+<#x6k-G -lV4-KQeBomZk$!l9S$=Q$!$h+=nG>WC>K{#qnV_oR`{acrFXV*0Ac0e0Fr67RT>7+ywsBel+$XI(Xk$ -i6LU#Z`O(+6i9f#ne!WQpQL?NVuflPx5IqC$m1JwUBxp$5#1S`F38!#FBZrTr+FHu;5kj#5P9a!DXGB)&t+f -w!nxbH2tB-U7>N$G~%QCZf!-UqQgQo2Jen;XeAntPZIr95f{FL0Y;7s;M8J!|KDdPb(qXO=CL8mhUiY -jO@~vN3~vtVGJ>0$p}SRJv#2gZh`}OBNT_TBoem$|8t1i=dNtn&B?sGQ|JkuRP5ICkl-V2?g{ -q-YHd#r_&CdbVEAegn^6Hv8Iw#Q<`pp_CHdpUM29SGzCM;O<@6=gdoeZ7z>mgmr5=itP-UJ2Qs6+zC2 -_49-JX#BnC)`-f{GDUE+WwI+~-Im7vWE!Cx#BYTmLAE&dRqe7ZRrE3ZI%CcVCX5A|0Hd4wQDx=A4^vE -I2f6dj%tzxWf@@ICWdQ<8l%*>8jk2i8BKgIkcUWw~!awt5}Xrv+%5NuPprhyxub1cLn|m_Q%*`03csu -*K@69DIyz&zoerm6`^VPs4rmc|T>4QT9+vBaaWTh$u(y_l2q~i$G=~Q3*exnlLp9${vK(cJUCQLW7Ql -$U;lhv66F0zD4P&8$6{Hxx=o5jw#O~v6VL9;Hwu=z95?@RvaabnnV5+gSu^TC2JXH`zu*2$m@AWj~P3#4CWgD{Pt?KTzxlwmdKkYi0ERoNdhhW35=7g<<0 -Ws?fV*uW!|h1KGwWVph7-9dw2Vtk2wgrT%BKxYtePvgP`dNV_Thqfxm7V^*Bjow9Xi1kEulCqozPbU+ -AfZ#+x)#=4+3yK^v8o38Sv>>xI&g!4$rYJ0*DdWAykQ7kRFy8TYqTV=V_D;IOQ+yU-t79^77xD-8$Ml -j=_3nN@V#g#oV3Y`E?h!5&-}D&VQ?S>iKH?{w50Vi|%d6qjt6=Sp_?e}GfneDo{mglAkpWszL#Zo-7{ -K}DICV{GeUuL-&P>V8N{m4V%L?P(Cni5{7@T6I2K)(wbz-Xm%ikxu*MGbeFkPYJf5Q4rkox@ -9zhlVR6<4}J1OH~v2mlO$VT0f$WuS!_bo?q|hgW%-Wka|L%c3J7-iVU@k-}>GqTswgJUr)Sz87Z@cmr -ehfoTZt2Bd3!dr0_AQyze-RPAR&6l_)3cM7%VOXBVbKHZ%H_w+lMnaUbXKgkNLx3+0Npj}fgq&Y-VT`l&_T$T=+ClC9Cj7=UJ7AzW?%~O@!0kV%rq@SpV-_%m3L6URXb*gMsTCPRQ!Iq##uUG+)|T@n0zn%>zj%j=tK`u5~zd9gA?$)vfb(xBGg+St8wH2A~36{?mS5)u4>W@Z5s -qBoS0M5=U`2}MAb9U;1c!XbRM8yxH$hYm%8X;15-!&?}O6r0qnKlDsa^o}CZ-axK|s7r8>2fYbi(YcL@ClQZ95XqKG7Yl_MY%myR>Db%Q8M4(F7KJ+Sv!S=gZZpsdr@U4p3brnH%JPVRTL7J%d -`rWKg|*%8L@U>K|}}v{cagZIX<4oU_rl6rMi+*N-p$&NOk7!UZM!GLZ+z8epv#!nQ6o8Q+B5_H$eA184&h>Se_}y -`qy8TwMnWlT>V+XaP)@-()%GXhy3&q*XM%XSU=L=BvYuwQ8o~*DLPBzZia#A_fp^Y{$w|O#w$A>_&K8 -L@yb4#V0if7+}8o?ChFx&`~BrElF&v%gD)Xm>iCIn?UR&RDl=%;v&^I7+2}RHOL6+7(;^W%#)^17#z*6^GNnvhBQsb=c+a<10>Mtoy1cP`7 -USNq>$0t!y2wsR3Kf3Ee{$g$C++StrDwLKYLvy^VVyhdyC;#xaT%Z`BHNxNy4*>KQ{#e}yljSz&Lf_g -$K*&v|rJBsF3Dgs`G~ZCr6u7%Co;ThT<3bl_CJgdPQ!|kgsjMNVXO%b77&B+(1aBXk3`~QYq>)J?YmS1sbV_%FHlPZ1QY-O00;mWhh<3GFvLBk3; -+P(C;$Kv0001RX>c!MVRL10VRCb2axQRromy>g+_(|`?q5Ny5lBkc>LrJMG3rCpyxbWmF2SMc7snO25 -_gxiBnl+8>%F4?y)%4~NJ(ouX9Ui!G#t(h=jEB9SF6?EsbWNQnv0fnR4OKkV!H2EtJTF;iiQ-$w%5I6 -ML~Ge38@L)C{g#C754kZg?p|}%6|lghS~6acq4~rnmy{Hq@CTS;!5|L*DA-~ek-M=78@bz3rp4UE-wz -Ot%S@Ke+>tGn(b(LD(sqK%WJlF42liduB8n;kbT ->t1btgESsb|3J*`=LSQ{3KgRuez>LMGrHA)Yr?YfFfwI(~J3(aK1TUr;2|IRKhE(&1CrD%&zvMqlsBq -KLq&%6{(2V7IR`uCUHFU4w&4{tMAQPxx`@|nhR`Qs>$8yCkkGaw4bv2MQu)Qe<{FnZj` -*4o$W=AN<*C}c)UzhFXm|K2TN9S=u1T6)ZBewVE2|vnJK^UPy7pT-vpZ?vVA+Azy^)tHPfcEV@&)lgK -@iWWDdeI}kPN)(?tavddqd=fQ3%gLjTZH*a!3`B-qJZO_+hrh4Q=v5W&`Pz7vwY~QW^P)yjkGGTS%!o -edGNhThdF@2c`-%Cqzxm#2f#{)A|QCzu*&ycMZAv2uUnEtH|tp{E9#z7b;1T|3`4A)-3Awj8<^|&imc -BKxoj)JtQ|bB>8M)R7tGWu+wyJzw}Qex&GEBv)y||cg*c=KA#D#kFA0VP4u3@Z?1@F>l6H9q-DpN!~Y -wmkBqgJiHR(rC0i&p+6xh?G%#uf3G!>cxOt;McanT-zasN%@=Ad!V0n{+Tw+BbRO%}}oFP6+Pb~T{$`P--BuX@lRH7WR7 -bBm>K*wOfu>S+0gHkWqu>BiPg*65BcxP|qlCs;o3aiP0R{cg@Ld8HsMy+5bsd}5bd3iBmEGC%M!Crq8 -30hkj$wWjrOtA -Xau56ATIF8`(_C$-`o2_VB5OR#m+BGKe2c9MY=16Jf?%AaG#HsA3+?!HaoppZ=|rq67N -VU>(N|y{G{1GnR>M@H0boJ^lt7w?<(!mv!&lSJ-fH_}obsT1A=|4^=>(ch_SmV^ncuGS_pOaTK_VMi9 -qCW^>(va`c^9q^5Isnniv*_Bif&xgSEBLnMzOeIn(al@BhO4IA6f_tMM^h-vFh5p+#Dc9BpY0eW(}fp -B>J|t*z*jRn`X15 -3oTFM;>W5OH1bYLMF1yatXLJ9SO(vaNroZgF^F;J4)FvZnS42PXgxBfo9aw`XE>cTtj0ufIed{a7qC| -v|-k#DibxbLg&Kig?Amx%%X404m}*1G4wF3=8{*CNRc~QTskt09}P}MWfd?A51eeN+K0#^Pzbv!2rlc -NctB}_^P=~kfBkg(bMfi+A0Lz1!NOTZ91M$!9a?}Ni%=nKWiK(EqRntnMvlw^N7|muMC|IwD;9^*m@b -?vp>0^m1t`R+3$eF5&j9Pk9%%F -Y{|G{nvukH$`lqbG5VzySwi#PqZa>^jMX}R$eBN_!F=c$*nC-T+~Ord@A+>3LaCi8k -li!#&femWKR^Q=7b)<^ja+l|A+InzNLL;2R@wUOy6FsfkuaM*jeOViPyqVIf$^-YV2;qTZ#t71urZIo -{kg&Obg%+|=1|OX!4u|DCwOh<%yZ42R6U!<)g+Flk~|koGx`}2up{*$jhjlnh&9*bft}V+lj~qpEl-P -y%#E8YH#@8vR*xOiW63%_Ej<51>W@+a=AjB{;tjyjp%q=dgZ|6Vh_dVf{#T^x&8#tU0i36-p!)GSS2 -Ze!K(K`JO|j9z`x-!=VF5Q6Fe&DJl|>j6vki#26DB#=LOd8=xVH%lqC_uC!*622Ni77||L^|-wr}!Cv -k0;(Xv#1SLD)3e)nY?ysF4HmnE0*(F7^x-Ldq<|rl{t>vjV)ybyorKa2uxX|13KbTbJZ*ehzAWN7;)d -eo{bx`Gn*6G|NI!-hl%*J+jByepj8%7D>_pj#dxSre*hSbnx@Q4!gsCdp(yGnD25RXs@h`!i!!{bpdE*-`*pt#A=6;4*=~*TRjXUAn8Iq!ViFB@e41}rzXrwH` -UCi=5YFfX!9w?912hOXXwf7qp!PAmxv}52h)(fqDf(6~Oe+TUxwq9OImZxwy -BXEIq+5zR+q*=?CoVggalZp<2TSf>B2@{;u%YF{f?PvX;26T++~DQNTtIk<*LdTtigP@7w|7=0UjjvY -z%h$0f^OZU5c;1>^2wA7`oIdS$BYEKu7J7IurQCoJ`2*=PHkb0djYG0j2>tm5)X$FDqwT@LQ@!@%@S( -&`RJKLM9o-WYEJO91g~yDzvMg=eT0Jr)pro0FuJLP>9N}p`EJX#|d8i2!+fCn$`~y -f9_bz%)PsHKrCl7xa5$6ZF7kE+nGSEsapiIlSRA$W|;qlPOcc0<=)iyxd)s$t1FT&E$cVwYGotX-(82 -<#M{G#tj*0!7WwRkNoI5@NCS!4&N$sHbHw~~i-RWK;)F?a>JB~v`SHNC6H4(7w)(TSBO{gY_ae}fLKY -D6A)h$SfK59o0Jy7(GE+46xvtiDR#(_$JpgR~3)!$eJ#fJN`I3u0122aqg9ThVU%mqI-@s|R698ZntG -$e|rN=-1FL3aP!1Wx;o4Jff$v1-xW-NEMn1j){vL^&7$gNpNfBv@MW#3=^&()}fo_MIHZq50tA$JOs1 -2DH)*ao{))TrHb(rx->|F;m(^(tM#ib-KJt?=75S>bgpHq6NA;{Q-f0|XQR000O87>8v@)ylsiyb%BZ -1wH@(5dZ)HaA|NaV{K$_aCB*JZgVbhdCfchZ`;U~zw56UFbF8M64UKr?@*&YoNIfN;C44g>|HFH2E>R -QNmElK%OUOPivI8SK4!=nN|u~tThzdAr1^OBzURF+Pf-+2%tkgU7ggQrsx(5DxmY(YCAsC8{Wi?u3MBa0qh`=~R(xolNa;`wtT& -upr$$ji8$)Y2gK3^);Y6(A)t*r`pc$`+Ovg@+t`Sye?*Jp$Iih+G-HE|bY50yeIiYAe!o)wNxt(p2be -T{W$cOH&nHt5W_x;dQI7%-X6dj9Uiy1G@^Ig#(EmH~Eebc#T4ti`$=WZr)$t-llJ^zrUH=xrtr*D4P< -PYV0w9`5y6kk3r|fMUUUR#~em(!OD%GF`SK7!WI5z@r5c^s+3j>U(lX?e!$8!MSCk4!a!Q< -vL5R2Tp$uoh@lBLGd9h^`lx;jS(jlny#)34Y~&u%dTi~$w;&&3A9PC!UNE6BjKJDg6 -UosOdE1`ty4oX%if!o3LhL&QR{tD?DxSU&!Ulb;*<=!lTzZn~9V!;H+F! -Omu1r%hIIdsT8vocm1h!?Yj9znB#TZ!A;^&__|NoYv%>LZQJ`=Oz{lqI%)@8;8JN!2;$&zXFFW8{AkD -&+%r;?hEyxiu2#2&c7J`CNFx8c@d0HHmayo;E26dm=e}acHZ$~1F+-80$0X!D|6GH$KHUuhrJ>Ecpl$ -w;0DBxNen1y@C=~bd5SLo*r5>+XxB#Gkx>933eJv9TfuGGHl6CXkOiE7CkvxIqy}k4@4J#>0!@D1K^F -)eENWd=4N6g!54x$!EnM^BK{gt_07pL^8gDef12tLcb=T0HmGH!DJ^|7j^|wv~egFyYOaOU|{ucr4a$skCoSi|G0hB(dW?7k|;*A#;F}pUvD -I9a?WK~zTBTRnhYuao5pg?focdM?gyEd^WqC10IOt&!4F&zepOENzYV2#%JAEMRZE0TD^_#51J6#t)l -{Bi9Ve{bwbc6#>VPDJF?C*6{*fM8U69Era%60|Zv!5{fN27+Q6$3VoQsMaU!6jq>HrVopiE|mKQ_aRS -u_B(vv!o2{tbRN~R-9#Q9(G3isP^560#L+!kBJ2mN0=&R8>k^PWZbVlg28gT8Jq16s!I?@~E$Dl|HwX -3CGKIRSKJAXna6xMVEc}snS1UxI8J!&aS`^*ZD$}bSu#8UxcZL@X-frRp`lvI^ULN!R`AG8#j6C2V1j -b23&Z%uQUBSmNwW~%B#2;g9^`4%6>m&Pv{G_+tR+Qa#iJ=chjC{P3*#KuKexvViPx8I!##Dmf4K^1lStlYk4`u9_;`jiE1q6FYe2!+04Z_^(I<=P$UZ)1 -WtRd$-O4o(pMVw8M+lG$_O8B502{^IvCl<6lAm<4@zJ@ePF~EBp0LG4EYC}&O2{Tbc@bSW3P!Jhng#y -bNMWbZCV=(jB_t2ez=u{IKx=?-LLD)IdM>`Vx8DnfED*9@=?svEct{wyS0Smzo146a -)Fs#2RN!#yMn53A_i9IR3eJ0w@XEQl{y}g;MXaD|Qrb1eaYkr%z_fY+tNClat2KY~Skprsi|MEL-;a2 -MRiwn|ZCxnb;CtaeNeC%m2i`n4yLJQmcmt{=*Gjyl0@$8EWG2)7pR-BaJiWPEzxhKK_*q2JFmvpEhY{G9>V5MkAaCg_v{gh(UZ -VR&7IohHvanl9Ql*1~|e#wSqlWic#H;Eva6rwXo*oC0ZUcI7|u+Sbu+9YzgA2X28Za#)ZXCl|>NC(+P -p8Y;R#C)f{(1g&4V_0fp=Q3x*D6F|fQh@K~a=;q?`e=lBNMw`jVtGe|@=vL$v=pU9qHxd%r2U+NR&jL_Y%bj?0cXxA}M1vq-i1%t+!8W-9zKj1#eu -the)U6Yu1($bFRXh*Ydg&atXNQu@X0ox&xlDM}O15(jt$^k -;#6p6h{P(r8t`YN-0kEQU50S7Rv_^r<*ia$S_rAJHBR4&|tSMhHZRt1*AL$`%4{8bY0TpNK5a)1RL*T -2sefk_+~=8>z`yQxSAUXIos10amE+%ux0Ne_Mm7@a(+XNUw4~hKJA3gyAB~gygI#o^UqZzZekKim^7s*NS^X09exP -8`pM$=>`Vs==;>@ro?WXk`%fOu~Fo#VT(s?uaBtqilXVaTy?c(nJ!`>Q@^bg!CNi5_@m>BJzjt(*eIe -$@+881In;OaIXy@oj^6^WU%F-00v&jA8kaP1uJu!<)g!H<&a74R(9X&ia>MemO(e1}IC4MjH0sfWjZ( -*6AOiPcR+UEofc9$bR>8Q>8l?IvOIJMA&~gKb_ot3EFfk9>;qcUsV*seFrHOl^IY=;5KBm`YeO$M@G? -i@V+jQrM?Q!bYd?N>>EiEadi#MxxBc$zCz+x@&R<3A7X(re(xpAaqcEvt2PZ^il-1>c344WuQl`tyjB -pSig2;*5lP&G`{-pn1eG8w`;%>-&F~J*i)e8Q_sm?-?or1ImC}$g=lvl(pBy|3w;XNk8CojX -uK|=HqsBOouo1??#D-+9CQJC4(eRhR?k+LEOwSSEv>+?4m4e9a+lk-V1Ww(D|MY%();4p;>bvh7KCGr -f{cBXLCXS%=U`1Y-?EiVaTqs0yc8V*KprW5?;mpjyg`R==lccv5sV1mqRZ?;&>u1vP{+B#Fd8GMw|hi -XNQUvx$U;WyK{uzT%48Ipn}`*WoXB*Dyp61^;coHVY3Hs>z9tW^>eype+QMST -VTeSzvAwq3FZKJb$zO$-rjb&71&Q|WgwdexJ?UllAYrl{(1V7qv;8R&V`l5+%I42bk7yqiN7ud$HuY4 -R+{M#zu2)%#4^4&P?!X+31jiFg{03<>8mYv^ndPw6&8a9TuuW=dhQeh-_BGIIE7z+lOWxsorF^)PngB -URymfII?3>o;50l|q}wi0jaKVl%(w*c_m$&CJacs`xOTaC3k(IaIOS-=Crs#A~WfCjGOD4M@;Ry=-Dr ->-NF9+>~^f_cRK4^A1J{`sIu;M{e0Lf-LW$;i`c@8ggh^j{C;2K^aNX9owefUZQOd)uSPpUj`Y!B-B) -M?HZjL=m>mjb4}nKqJNnDPd1QxYia!=)pxc>yA@ie!A`*n)Ob7>!_2E|KLWh0VtiKi!AiL+H3kd|4TF -snXJ*^+0I{erg=qq;VT~+xQxF -;%IrFGd?nTNq#M3NkYS<2XLe4?gWFY1c%vVa;=8Ya(j)^)DF~v4&7`HpQaT? -)M=X_I6KUX=*1@t~VUNO6~o!e3)&alD^EiI;?gS1rdExva*ax}v*i(d<>uemw5r+S&urkrr^rT% -QVorTf%&eu;}wF}2Z$DkJ6Qh6tH-jaWDYjGWz{8v9o}+@buYw~S3xJCk`NV5ix>e0urT9aT=cSvNz*D_a^0Y9uDhgmlB#U)*mTS0!S`41aMc9{49OHh)!gc -M`75? -`?uCTW$AqQI7?WIrYtDbSwii&f5jzM^IwQJxSkK9$55>hhp(YTz`_-0CaXp*H)-($aL2N-<#C&7Fcei4|ou^(jR2c^Lt#ZtekQ6m2k>$y=N35*Ai)+lL -C9@2Z+>4@~!7;*@?Idb3pA}JT>B#eQAZ=RS0eLa)Wrm8fK|0zs2G2P2if~--QOhHi7Vse{AB2z6HbM^jJp -FkriV_lhRiQacPj(U>|c2zos|=w3I5SIJS0|83|j^ou<<3l3Z=1QY-O00;mWhh<1P_M~kwBLD#EdjJ3r0001RX>c!OZ+C8NZ((FEaCz-L{d -3zkmcRS2z$%jw)yj;M?e5IEe|aWmeU(%5UR!buCrg)!y}+DsX+$CQ@FUBiLAr&hHmfO-Z{Raa=U43}$#TFaRaewFk+->O)Ks+P2 -3{w)ggXH|ef#Xi=?~A7=RduB{_5=X^{ca~n8Uo=HJ~tu87HcpZR%<+m6`-IiDfyjGGIT;-fdGQnS_Tj -Y>G$#YXM7{#`Be&YlN$tQr2M)&Dy4UUS_gv5_lRmRI+I|O;r^tnYDS5nI31)-@SkH?)B@JXUW-%)9=G ->ZEhNV3*#46n!Ty3WexNEV+zy^AD(UU(!;W-=BdXbH#ICLOuvxu&pNHIWKFd1x?Im~WBA%*-+)_T1%a -RYOm1GsX_jspSzDNYOqG1f&wxAMy?dj7>;us@E7Sccd`A%67ry0le6**x&+E@#zXBe5mpps%{?(t8v( -tZnJ{51J+5m|LwyFi9n!25%nCt)n60qN7sq!Yjmfz;{W>0_Q@pRes<_-Saevf&IELW9HT@~F>@a!2s+ -et0ABmc02eFy7HF#5fKKdx~3P1Bd~n>4F4psA907_Pugklo8E?9(t5rEQWIe6%c29gG%51(D(K({7Bd*Yz{cIL?&VO|Rnu%Np8N= -ZUBFy%-^&`Nss%s{6Mjk0q9iHPHK@}GG|S{dv9rW0}KWY@kg -_g)KyT|*z1_3O$uXwut<+`mi)3F9g7bmL;${wVEIN(QOb2LHXTWh#HfR2duysG{y69oQrdYd?8Lw#Q518f;v97PgMI50s&A+5EO;HmIZZOqUb -c^P?QodeHCw-dAm#4YH({@@*^pGMk$3s?1lcvfMDMv6ayG*$lx%Y9l`Wslqbhww5xfL6}5g56y=>Lzb -i(FkN+dpnvqY!8-HjfPs5l5ZkCSw~jD*E}0tOgltr*o4N)XDx -4FX9rIv_A6+%`QL{PKyZ=q(U{GVTDt}Z%3%|KXE22b?wrVvU)(O-Am~KP$Zx0Uat7(7_va6Z~J -d5wPV6{x8nj^+rYXRwxOsXxdb@I*CS8*?=7^aPw9zIEdv|$n{KS8Ni#@5?#7HEjszG+n?w$WBwi2(zHkRS(}MvLqFT^b|pB-0(3Ee{DEN25jAVcQ)^NQy1CcdC)qg+(2+Z((a50>{hr_+@>c9=(}4U1|7BHGSIEUHm>u|%UmZOL`0P{rtX -s<^XgctRutzj9jYd&oY1|K1YJWb3)E&RzWCzmCasrh;u>i`!hFgCwo?jgkXS)5xs$7+qAI2Tn52sXcjcBN-)?Yesh8Y9|y~zC#N|pC$`}-R;fd?oE!K21PrC4yO>3+Jd -4PRCT^Y&k`Gya*ozH;tuMvx)D`d;}ktkc63iITMb3g97-8Gn-3OR59&Xx(;H*MVUWjqEiA&STtyaBf+x-d%|#LmYQ;4%9a^QJ4d4Qy2T(_fq}Mhy4i4ZwdqSO)4CH~eI>&ZO -mnmsQvg!}H6Op~YccKO51SuH7hZdWc1+MrPamFxvuO}(fX4)INM@Lptj@!^cZ%MclrJl=;J!Q4LCnaB -1-tenk>A>QxM))hk;!y@!Z4eBEkTa!UtlQbi>>}Km$iQGPY;COZ7vnP@~p;YrDcy3iH!8Hfr!)GV#3A -I#NQFFtUnP?j*k4-Y_>U&JHR$`Wj+WBt=~!!)X1ROji|F%ug@r2i#a2B5(@SnQePc?)o%hj8D2s6J)x -y_mF0_^G=h`+(;f*p88RZ2VYpUGRIC%lQS$=t28WwHpv -L-kjLjoWvGJ*xOUQm;l7d08h1KE~ISYbaO!V0meiF5_Js|!@hQ40ckLan2Fi8vy$d*U86>qw{qT+UyM -O|ak>zBLop1Z4P{FmP-vv)MZx(=0tR{o!E|V~pk4}WxX4ipmKt{gN#J4!9uJrEV0|P#o7o1m5u9)sxc}F(eIyH)XDo$V1mq{2)0GBW)Jp_ycX8j -g?r9zf-$u7R+&Kw(=r${H3r5#-YcxDqrWAbx_x&nThQ$TgeraBi?c%dp_`Fs&TGJS4Rinq#=H-p!THH -=ww^$)t}>Yt0gd(pY(HTfz>V<`i_xr|6s+M~{6xhhEqsr -jCF$dI95S&)>dFUcG+xd>k}N6Ktof7Sag5$v0<`RB~i1cJu>-oOU^T`s?0?C~#2?4k!TNSMjFWU<8z& -f`@(7u?usy-;kb-blkjSaZTA|RjDR~?Z1S+=WgVm*}0qfCt1Zh@{1+`Md+SljmoZ&4u^KxM?0s8-XXQ -8e3@GvvttB|0pUL2N;Vz)t<7kP(f3EA0Qkvp2Fo|^YYEOP4Wmo4;cTcow!tgy8MSSUXIjY&{XG^ -}Z8=RFm_JNhjP7eNJJSQ;w8FV5#AgVr)FRp$eJ2aYa0J^V(zrubthD7skWvT8`2)h+CJO6RC*je&Nsb -KODV1@MBb(;|QcGOt4eYd>>fCxDcKyat?da-j%>s$dbuuw2VZq{wzC+yZDduW{qkM#oAPRXvvlI7lbw -fY?Q!NEr^R)+w^Yx&l87mj{bn;a~V7n;Y~B7AXkq%*KmFk33bKF&l4c^pa88t=F&)alZ8=54I_Ekqa*Y -0X$p@qcC>bMuI_M5K9%o8=$0~dFT!1gMV}VNDWIw80Ri*D*I^dAVpyk2;DKQ-*fpElj#KT=D0JO+&y> -$l-O7FsOJqb=-yGntjUI}L3{Uksqe*9$4b^=fh>z@;>w{)}Z?q{9eCVFFb8`N#!7!ff9^zh6qH-^Z?X&0SvAc0olx@m1AfLf%a+qIl@8xvV5Z#X{$Ke5d6_|YTr{_RWfh?N8F -M&cGzl%K{{3Y(!eGpXtEHLiJmg@OQf*s`o>`(QN(2?G}x4CgF+*(2DHm9n33q-sb2L?eS9r7~T}lby} -Eb%=?d6DQk>LTxy8&>LzHoNfT%f(ZL$mrTGlK#RFNG>eK>pe6>b4A9)lE>?nR?P|MB$~B2{#=oO2_NG -P3%fLVx$cY8y`*8H}ku7w=K9ijkLhDN-sEx0+Y*tmKjm3Q)##2({;H~kjg|1t&MqWilxq)G!O)Iy|Nw -+uYYyHzB`M0^+;Mog5TO8)R90#Lsk(o*Eot`c>4(qNXYCH#-3VIEMq0W-%xw+Yp(P;E1*=YYkXBknD6 -Z3tI`xS#r -S)S231b_1Ws%erbe(c?)BY=@iD)jpAFOU1#39FtD$R;*{e;HP`-79Ng~_~S|7N;1KEM}zw6Yy6h!gA` -F5-hYV2BMUIJz!Fxts_zagTgO3t*Nsi!?`?Lx8V`)2R2_JL6{*f}(5 -!AZ5f%w~dYkCL$hU4m$$y6Gy~2Q`eH3ODzr#I(b7Wz8H4Y24F??_&*}t=f9H5{1xF(aDzaT)5*aAhlk -xCeG#3fhre7*{`#x!#>#_Fq2GB}}IHAQ`C8 -Vj_0^9=H{guu`v{>Hgq$V&w`wQ7c_g|8rILDB+&bulQl@a39Q=#Ju};rTsSd+;vS#5Cps0Hx -oAB~NF#IG4`PXug<+6f)F#VkaERg>HYkN8koGDxGb{wY$W(m#C#vFJ6M)l+a*^Zr17oS&NcfANXbHpu -4$V1XN25iCvZ8Te-!Xe^# -#{J@U-To6zj|TmjyZg=P6_qYq#wd6{9L8f*mEMKyZwyIf -HX*)%hdGJT|*ro&~;yf;jh5}lOC;)9hmpgik{BD_gK#Y!+&pm^j~Nu7^1phIplzVIxe=!djurOr+LS# -I?=>u*hV4s^uyrhr#wDl35}(+12Qp2!Ftg2bdUNE~#>e{^vN^mx8lVA%LtzQkjb;KF@lkJ+BS`tG&kX -056lOL}`@1I?x|fjvg$-hI4DeIuf;j>H$@@zK%IWH$&a=LiC_t_$T}{aovN7{demid76tApP5oh^S3M -7Vdo20+(Wmn!9FUa`S;W+sJv2mBXnnbT-)uuN}T1h$!xC#Ti#+_FYM01Uvrly$%$P!Y~vY=Eh?`q3H_ -P@zRYqQ-pX29ExdatWmhEk#73knifXX~g2kZAv?4 -EX){^5YSOlcn^$q}Lx^7cz#pp9b1{>KA3g~&DN(Und!}YK@cD99H1vIT3T@5FNdLve=gCihW{K9Dk>JN0*bYXN1T(;Z-hrB-gDs -GM*>c!fmuVj-o4TfrQ{;adlIb|jp_dp|M~;PD`sAqhJWLE7fB$XIT7fgv -^vo;G~yTRImdbx58yK-sj;Xvq#KBv8$M1|9CB10n-f6t!M|Y}PGKF>|QHdOd6fDKr@tBqaa#&eEz)EH -wt&B=lJExNgqCQ-Az+FxW)aSxlGgFc2T_-q?M+5gncW)6PmlwQv0J6UxuWp_|SIg0&5389fWbu^@&l- -|mFFYl$3dmCd2!y4=~U!GoA~q{B -@!-U0hwll8?m?7s{0v0&ZB3Cg_-?z^L*PASG&IS541Cn@f%bvZE|ar%8I9eBWEXMdf6T6k$-n7FfGKY -Xm|?Ju4tA5nw;4ODwSPZTkSXss2*Rm0P{A%zM+^QPbICa=1l1T!dy^R=roM7)sIhk5=GA7)v6(JM)+N -6jR~Z|+9)R`wOlv3XM8P8;N^HQ}$(qJ|ty=7mTmH87*Fp)* -Wcej?poqW)q$3KFq#DzWm2)q(19VoC2t?hfqC>a-1v_G3g-oj@{qzUIKf`CyVmE94hHqft|Aru@U@Nc -Fr}v5{4F2j(z>oNb;pO!j@m;WSKf`!z&6hAxwu|b(p>x%L?xY(VU>nX85*>m^tuVvxViSv&u@na{@v+ -qygn0W0}N>ZmEgJpU`I~u#H>CQuy7M?vE?5np2;7 -y{hW!IJBw9R^h3*5(Q8V=f#+uuK*M=#B>pV`~6^=EeX*6M*43tYmZGbg(GnD6q$4Pb;s>>PQdkW1DXQ -yQFxq3AHfmgC%9?#Kg4B0b+G%b^rm&U0;2JJ%XU&RXV|OQ5(7|m<$)k?5=_TJ(8yOx5gu9mk*)QA&ME7Vkde7W|-8pk>m!9!I4d?`92K)C -q&vf9X2vZ>((O+}a18GHIbij{VbBKuJk1lLKUHKU)j!vHE6lNjUkdUI9~uewrhN@f`jEdbp+AHH3}#ZMOjhjP?4=1b%8ilGwvB7qSoP|*0zVtB6PNQv6zZW{!NKjnN -#A50)|r?bb3JRG&YwTAy;4@te_L$voe1{FuvADa;;u(Dh+^3k9Oh#WV+!CWE4|Ja3^2sdInjD)Y`zIS6B)w?Y -QJL#MZI1sYo5Qyp++KFwbUN^p4U_*rIycmyj*0xgrf{9O1zti)C7X$tA3HZx2Bj`9S=LS)#l#=Uido3*v7?jyD2`XoX8b>|y4PX%hqDOiFut0;sWSXIKpgH!pA;w9#Xhxp(SO@VF`T)6R4HYwh=CEfmECa2efuPxI-Q&*MIig$l1z`f@ -*_+ti9hY|>xo9x8dl@y#;O@UX?GTb>E -|N~rL%_JSw+ZDfj2FuXz|uzI8IfKBZU_2t0M~`#o&mu((>x{57qsRLHF) -_d0}96Yr%HK%m#4PMEt%C>ZbdseAF1q}L;i&hU-ccWN%5cm;=k=!l309m&=|ul)FF@gbj(=wdcYX5Ma{YEjMIjD0@Fk+KQQol0be>>Ynm}%Pwx -x>`%wgIzML*}Q~}-7AD*4jFNcPxnBC!~Fn|4bXRxp!v=zy#m3B4`=5*1iRuf(b!?Z!&#buXdt)qn#;B -vUPKfVvl*J&49(&3B%=b&#eQ#|Hrhavpv)(tC&j`51?wQ(@TQ^8KOmf2If01IYuvqJW__X=yTnQBhS3 -GUVxMsWXll67l|b29S2aV{^c->v<8@QbMfJai9C8ZVyDJhvr|%%__TfxUAh5miwj4*=pSEVsHLW3Z~h -!Aa{J3_{KtATIAqeFj%%6N&-Qtp(U&&39EB;S! -2WX^7SN2SeT7EbW^v-|OxU=V;W9KkwA9gx? -XY?<^J@LicNpfrCtW01GR+lND5C9u*5y;Bl~rbJs^AQB0mXZ6Xxg*ad#c$V7=p=sP3-x`{eq@u8y%c>b>e3hwB&nu)2E2Pnu0w -nMjvF^BppLJ`Ld6&0+t3-j8xzhx1ug8-n5%DAMIMtGw0~iQ+}Ik&Ct{OfXy@so0iMzH{J2uy2)6?G)RM_nngyWs13pBA5@81BB4f`z2a`ROrc%) -R9(IMkpVeX|UC?*7b0wCZ|aQQM8wquHjR_vy#uY74>9EK00K+fTxk}-=p`WR@Q&Z(FNItD-c4({IH^x -tdt-)ZviG4XxP>!8iHJ$?Pn>kUc}2ED -!hsq5c;JyfuF&!LWr8r+_*ZD<+-Kl!p6^r -xq*?PCW*-12f+G(`|7Ap4m^FSSm%}DG>rVaW6ooTA?YxZ1W+;VuuR$jHL@amAXLVq_pj=7jz#mHI$aDTk*2I_5lKqSl3V(RP%d*M_d6m*s -)ow7xPs0b+hxZvr%Tc%46)&7cd~Z}aNZlFXIxjj5BaW&Nc@c7SRObGdElYq+niI52*jd)sR@C2D-WI2 -Vmwe(*<1Eri~dQ5u8?I99q{A)VTFU>S$|Bo@qYnOO9KQH0000802qg5Nai)y_>c+!04yH>01*HH0B~t -=FJ*XRWpH$9Z*FrgaCy~PO>^5g621FZpgfh5R3&qEG9L$R<=`ZC?Apo4I!^X5Go^w^NWz!|H~=YG`|J -001Efewa=f*tRaG2O0ve5n?$@uO^Yino$5fS-w#JLvt4vgDXY1A4)|Dt_`XE>8{QUfE>1-k9^JQJtPR --{+7o~NTkPC0~x>ED-d=|#(O1a9~+&9D0wbW)RRUPSAtU%#__MRKG -?(6sPrAHv&bo2y@?>@<@PdP{rB`H0sOCHJx|w-vd?oW-(&xcMV{32WoifVFd~ZNxwbN!LbZ2tc=oh2^ -7qhiFW%#Z3mD7uwKL22=Yg?Oae6WT65_eM5!EM*d2r078Y>`T1Y$X;-EXj(ftne`5mphDf>aSWmRVY( -+m%rP8?5}mMK1t_Q*xs|9ST(z*LBghK?L5NCdD?kzWiOUkL*&}1r0d0N<*Tm>6Rf$+os!uuF0Qg8D0f -${)=CuCSE4R2DtnR1N{LOdBv426x3F%ffquE{IlT#aQYyY~|dG=PjXD>DKgi@T1h0HS7X+=l3AX5l2r_##0T -|dV*GS}I^)=K3RHbxfesep;&X<3zX5YUXIpJBc(i40UQ`;@uP*kH0}=|=iBPw-FKcwgt0E)k9G@DLOx -RbDttv4IbeL)K72IpQ&_2w-?EvXZGKXi^(p&F5pU@@2G6FM3S){JeN2e*0rTLzX4@kH=5L2_K)9#DQT -*cSYp<;er$zaj9d*JbWKNsNJhv7K=;@H4`eaE>kewjHg|uxsP7?xn0gv;&s!I`M6_oD!P{DbH;u??|9 -UD)$9buk~LCra%6~lta!7@!e+bignd!8bkCRlOuY;f`^!0nl4Zo`cQZ=b5li4f -OSY!uf&@lwN^K0XDsILH5`q!927DnCkb>UViegJYDDm~p=hjB1D1?+rHGK_iGKG?})zvx?mpQz7fB_# -FGo&gkIXG_zi9z)yyYfWkl~^_>p)KxuxU#oJ;Pv%R*!7h5y|IQ-Rz -F>S-_=7`JQcV4Ww<^$_(g#KWjY(SP3I2EVTKufVwFN -2)LhEc;dW+{k`n%Z*&v3BwZU8q=Uu9MSQx5doFCI7~Zfg8b9i7ZBzue1vmcQ7X3I)!aAa$W>II*o -iFw=ulG9OUhhk_A&!COr-WNUIO2?XiVwcg;Hfi0O*+m_S8WzY>-7G*Qbjc|fEik+?9VcY8ItARc0o0# -QfQF3`n#Q1he25_Dk_k!qjsH`5~Hv~%>_H0U%RG>`_!%Tuy=P7AIrwy~HUsArrX6$%P=~fRgWu -y|}OO@&+MGDbA*BlUB?COBJY(}JPDi-O(it}T?;@qCBE;zz|EEYr`T)_k74`?s0=o_E7PP}?W4HQv(X -362%=%BiSSkv_}DER;G6>Z48AbMnuRyC>$wh);kw0iNNSw9EPq<0NxHlcCQ^abPXwuY5oUK$qk2isiuKF-&&szn_DCz^7M!8Vl!;L|+*PPQ&{d2m%x%zx1K3%@~$K|`L&*JSz@!{jWcz=2 -S=0@Nk-22m8ccVGLGO20W>( -WBQ0rJ?@~Rryfq{nI7B&$_dITYIEM{x)TXnY8tJY72m-y*2%gc=+XEL)^Y^JLCOuMx8Mf^ecgS1Wi(` -cX*_uI<646i1*p-Z0Pw_GtsZ*%b$ONq)#*T+&@pk05169<&Qu8{MHIL7|zcG}JDRX3pYYF48q~@n+8yzA6e>07(D^p>5KQ^BE -8xmgQSV6WiV(QY)|0UWO6!|_V@OU^T!sAKM0I^Q#;{9w^dmA^fE1FbkI(~fq??Y0}oqRnsYcHli?2r{ -{c`-0|XQR000O87>8v@`+$Laq9*_VN09&k3;+NCaA|NaX>Md?crI{x#eMsi+ctLa@AFq6a=oI~67897 -+U+!x)ODQ3=kt1fJa*Ea$yA3DA&EN_sgRUslzsf|-+cfeUet^mZ>!zoa7f?+xVX44TwIJsqj&YbD=OK -FMwVG8bJ5k}?cwdKsVJ&k-pO_}8l9|~dMoDh)xO&|ay}Quc2_r@$QEr~?mIc>zfVro`?6lIi)yX^T&@ -d-u&m2cF1wsYw8^!=`a=Tt^|DDbyHE*%{D8GU*sHzKBeN%-@W+$)ekS{FaPuX%Qu&={`lr{ -DlR{~ef!6E?_Yjvzdf0F&8E5BHFBBZTnlQ`7;RrR*m{mYMx)2Z|J^qG3?LsJA{1`Cu`b#r*z7nFB -PxpPQ^hx~2Pc1^L^cOhZ!vgJ*NC{I5!mIA`2hq?NC41hz1%dD!aV#$Ptcnxdno -<(N#^78e|rm35glau)zCJQ*pg}54jhosxRsH?82%kfl9l>7wY;^6>QZ=jL$v|6WW8S~9|q?Pw}SzvX`>Q}u!VE^9y>_9FK2^z5 -J0XZXLTzmI)BKR48D^~!$Y>TV;Ourk*pIo81RV-+6AcCsx%NO}L6RIg+7gNeBlN+4mb@iYxer@{bC(DK|u4;Q -w2hRUPWUunw0SA+z={FL(?f89KS#R0YU40V@@_NqvJ_4LlPJ4RR_L2Z2fne%#?30VB7Ii)@EFTvGrtK --^R}ZK}IC$UU*!b1JJ;bJRoN8G2UVk~9U44U~B3vTQHD%gRLgYxXUw87PL@3zQkemzSV<&Ozss -sF^Jf+|D2nXaM<-s?JOc2!-S~y|gA6hV%k(0prZv$w~VeJ_icRwjQ8!@mx5Xf#BH$c}%h{6(3qQS5yV -3pFkQ&?rd$4Ft7K`QkppuPsO6BvgXjg0Y$AsSna;-P>1!w5z=`k`x(=-W-SDT9i%4gr$_5q0s7Z_uvb -VEN&@7LTne6meFNZtY)(n%D;gLxQ2cG(HK5}fIxVXc -Lgc8dq5WA-dH!eRvuETs$RUg1l?FRKy2lKW>PjExjQrJda>Sg(0LrN6n}z+0Nu2}EC2_2+4m1fwmNhh -L_XmG9|#kf_*~Tr_Rq~eVLldsOm*Cq;P#yr -+ZHfg_#%Q7;M1bQ@16QR&MZ}^N;9kzs+efl+Ur6Bdfczf7wL2afg%XS;Z -v+V(kQ{QRMb0;F*P~(yMi6};wx(o~=!t}9-)m(^bg|_S{p3-YxK!-=R*uV(84gPS8)MjKrI1y&Ajd?96aM4#cW-b8fVb?*3`7z%3?#0!0}B=)rvQ{eQvjsrHcHh}-ih;I&NZ_ -!ezbJC0dh>dg*o-tbA%(1b(O+Wt`sPD#m#NHl-|Z%;2TU*pobl^{=8TWCwMt@C|}l3u(8#?LYwk$nVK_Ax6;oZmEFC`1tc7Q -nb}dQ~sW^MFp3+UL*BBlzEUum1B7FVCUZ(7kq3@5>zN4Fub|tQR1P$$^1iNn&#epri8OKmq;@!VZXCV -GIR+i>eZ0Luo~f(sn18#R`cLJ~=Rgb@wqV_cGR5P$larE)Lb5u7MU{F1NCsllvtpQkk;Ef9((!sZ>YS -x=U4>!xBA3)T?iMhVq(vtiU1%25XpQiS~X1iJ?w%TL5QR#7Jt?u=^89CKK!*7mBN;qfm8@`cOUn%>FA -XC_$EYTJp9^^86d<4qdTiBdIqb+PXnMt6Ct5d76TTwS%F}K|5nL-&NVo^CkQ9^$II0pcisGwIh*a&2j -_URXZUpjgYN?Ji=$T*$k8n=&5LswvaSikr0F_R+vParSGmHrL+lGMcC~-B1-&9+z+4E(5H&^QB-}n)A -+%)y4v2Wgt#J9kuHV*c-#=dU)T;{F63)*W-lUL?~zpTIU#p3I5)NRnp5hqv0m@^8b4CsP>!`2z>3lpn@iFAKf}ZUjOgJLH -LR^5T4g-YXa~x4_CGEx!hp4)sF;TRzT4K~;mF<^RyolAb1`DE~?CEZ>7EU3^^exUIR7Lu|r#t%BQ?_p -MPc -Q~qQ8UtO8`N76u$k5$mTEi(O}>kdr2-Al_CRX4AxpCxc~Gj+GU89Pg~)gNirqhZ%U@E9d&H-9pFx2tIU9ww)@aisCP3^L38Rx ->yfHTd*Mwkk$qYqxQIRZr>QWLrQDdU_4zDSz!@5PSBi``E>pE7IcJk$3k<`iv)`rKqo$Pr^YKmHNm@# -`87b>+!JuIRT)WCG?Jr;O3? --2^R;pH@nIIkB+Vd*&B*KcgAO1G^0K0hHYWpbbH?UP7Sr>!PVB>b^Ng+luz%O7RY`^fLNJrwa>g+0p1 -^l@K1f34BGD~iUD7P<8?2)!eb6W$np;!|=@mZ9GbVE*1&)zYiIV5&vdvCNkM|?QC^Z7`8D}mE&(K(48 -8pIyT1L9tp-i-#kscJd!MS@{p746nQxW`@x9n@F=7Dl*#xB=?e?Ne>WTl?nx0$ZQ>ZWO)Gjw?zzQCg` -6Vtu7;6`RMR!v|-7S?{Y(aaoMuL3t`re^$W?ov}<6C}1&_1!YP8rFOsEV1?1T=$`OKH?mdg5p8^@B6& -r2CW%~_!eRxIca8qK&Vq&wl^JjqC9!&JnUP#5#DOxOHWY`GjGv6B;&gH~J9YhM_Ot@i)EF0w+8x0TULNeq{Sm>WazD*?SOrFR!00T-6R$l -XKCFy?7GyT}}dzw?X6=7cNY+j?jh-UP8gqZzE15^w1X7Ub@Cc*BoN1){heOJC;Uy&7;mGVpQZfR+3V{ -{u*iAx9n<;fZA6DRpfmsFsae(tc~yJEWJyES{UVpK5-rHl@`0XdW>^fHT=`-=vTnwu#Ri*#%d@OFO;I -;Nyh0!)XiXhQ%P@SeS(9FJjw<@$|z1o`)>97QCK5$>ZJp@P{djqTGDu2Lju -@X#(A0v;e&vXURyc%PLv_rCFn#(sY?+^0@Sj$I7~jAfV^!H%m|wa*P(z{s0STJw^aKtXZBnjB5E8qF^ -nS1FP#RZ`m(7A#OFp{BCxjl{w&Q?lR5?CW|eJmv)$y?1pl>IiC{ov;;swQ(qnCcvAFOF-jv}iyqeJ9R{x}njAedk?Gia -|I~%p{qPU*(zON)d>|^NrAkVO6MXaSZV9*_6`S6`sN(#+%-5>7=9xsgEalgX2X+p5K5 -7>8v*oyI{6J%BlrNI4cblNw(olKxX{hahMF$M}pY_RFVFkq^nxA`7@1wh26PmYmo{=mU+Z?N+2uqy_< -W&i`-@jB3-{h2x}ySIFP-q)Q>Y*cCC<9R$E;nqHk@~+tKx9oW6ibVlB{ecO=jsu(a3LPxO&_fD$B3Jc~gt0uF{Ize<5n*jOUS6N-9Z>gOr(AQd{UPEm -u3guzWnB|r|m-Atx{8}#OZT1b$dMct&nltK0Y^OEB5c@N-eEQE!p4*z{3phS%O!pC5FTt#TY-qrA^`n -q39w8M?@2F7x~e+{7lM8w+jrf47|^}q(x_nTspmb|3wU3V)^2&XsL+cN;3L-WeTpqFpfXdfivE{1zC;Fl8Qkhq6J=q8|J**x~KR_WEj|rkAfp2jDkDt4H-$tv3Q9IRqy~6J2Z&BZ~T(vz&a}0_#oQv>W)BvyJm_j5Sea1<>Ng^?bQ?DKS -Tnpjr0ysN9IF0$Wbbz+aBMpn|1%c*UzJzdzHsA@o_gQju9!e$N4RWJh9T0{Tr=VW1#-S9qLei!ldq(I -nR<(0vS`dGWpAC2tPZX8!640PN5E91oDk~4a^bx6i1`Zn`g2AUW*q0?evOoD4+kJ6{p`&~TZ0+b=QOi -npF}7lkC|inYw)u&45a%axAvX;tPCo%-iS*$J>VwgYg9tQImCk78=D>A1jt5 -}BvSl1Jeo4RuTeq_7qKscGj%7x^Gf!d!^)I^d7muo9MHRb{MeQkpKe;sP=K%2oJUw&YFlL2fH{Mf9_@kKF}QlqZy32?#Kf5C6OSW3aUdL{powR=Sh1PPYJoRk=I+)_mU~vq -`{uxE4w?$2GP0Kp85Pv(QStH)jLoiPP`wfHu5XKM6V(V55ev{_WY(*5+R31rL2{&n*?j@rNN%I2w^8p -qa(=sa;zW%O*OJpKC}!lr=u%G=06X8&0euw^?QuT%h1+yG?6r5eY0rSahP@-3*s`gdvf3FgEtaK7V5m -=j3HZV@Z4mHha@4chaH?g0*34*e5Gnu9)y%f?x{uB3P*p5tcJ6n6#yAr!O3n4`^FnW@k?McYVJPW|N -&_GoA>o{K8BmSd(1ZrH@S?@HVS@U#9S3~ig;CDD2V_Kf|zk%tR)R5u8H^8#6pcY@~29tklf7<3C{ozE -8sdyB@~px5kJBPTd)@53_4J<`J%)S#1LA+`NXtIiu_^pK@eX_g?>(F}@cH7{rvNC)TP=8I~I&pW7LwJ -IK^7mJTuF%F?6WB?dl3`$KR8WKv|BWhE84RfT{AkCoYc^C_cI0f_%>CKtjc+vazw7p<&4ke2z0Fn84Q -}?P$ZdFl^PaG4*r9LJ-W}s1`3`0CXS-hp9KSwDGDJtwvoW^3I5^hP8nBuF8u&qgCvD&?L_Z*c&U8!Jv -GB&m@23GINgFhG$JfQZ|q`dSZNBhIKA^{SoPTpe{Su3&S)L~me>DEc|9Ii~IIY57FW4s<)1I6jIA0EkYpTib=R4!U00RmdZzQF) -!_z&@c(ywvSEpycyZ$Nt=IYt(U#`EI{KMD|l;$n6BWGQ0q-&3ZEc=71?C`_$Q0Tdt^4XNN@ycSj!c>? -S*8IWna?H$z$9rU1Y>O%*9+&S)WA9`e?E`>nv6VSKkehjGyPAjTLhT0)r+(1!U~zirr+( -lcwx*OIQ5k~$cJnp_B?eDcsbR;ftHrKS^MqQmraEwNS`%)(n|8h;Ou{mpNWc -rjTfY6V_%2nS%jf;-~BKW(70AT2IVmvAfg@pig>1C~>AO=+7rLZ`*IdQ4j8$)>9{;S%>vRm>PiwE5zH -ERG-9NMD@X3+#A4F@WyTs}?&kY^ftI5|S)_@$^;HO80tCEbE|ohftIe+p?|L!8=Gbk5BrAR3%5_z+vb -M20>MFy6)-SciV)akwD8lhmpu?2aOiSVGSS_aM5`Rk!y2DIl}0$9}YPWsE^D9@^m%XImQeBu$}>dOs9 -(N<#5PpD**X3<3;GY8f^79iGDIa5%j^srrE<#qg&G4`-V#>-O&S*+}zsI@)TFW1Rj{>>>xdkJEyNi-$ -Eni5iY$E_xUOf9{Dg8c6&M8U9q%dhD3mz&YNdV=WktJ~5^sMMga^ay9yJ@12i<(HJ=5ndaD(`=q`OdQ -H?IZkrmjys3nh6b_IejiUno5c)bkwg(>D`xX6HN1p4>a)|14lXxPCHVTfiwJ?Vv$*P0)QQ><)9M5xew;?RE$#rs3&%{H?QZkrP+0}7M6VxA4GHiRhkbahUpI4eamaZdy -FEs?Jt%dMMwze`R-L#Z!{yyVcPrh(sGe-;^~Hsh>0Dyn7OU|`MtBEVJtc!O1GekRfGxvc^keEK?TJX3 -8zTYd6gPeU{|XRj27DM-J6|A*IaF-oPkP?hq?DJAU`TbR`oAqSLi8)VKBPytT_(iEDT`i;DBEY##GoB -aZo={&uDGMV*%eeq-lFUF`Yr>`b``p~!gvUE0U`CRIrd@r;wNt;qsT;;UXZJ*O2SuQ~l@f4sgUZ(35* -hW1&OHb`;)^NBcN8|gsndpc`Sz|)Eu1?uYZKX7XhE?VQihZY(`8t9Vqm#3We9Z81QLOj%zAXOcUBRHIo%G*cH&sTzA~&G|vKxL}FMZ^_GhW3RSWv*rC_{3G-V9l3;l)>*X6-!)(h^_3^UP^4Wqug0!{Ut&cf31|7?qXujRT(*#XS85>@%i1$!N)e;O?}Sg)fd8D1zttub(stFfv -!wl?NTQm77MpZLlBY!V{qixc=wBP+XFS)Mcwp58Qf#mxFD$7Rb4 -#w^)j%f2gF3y?upa`)!_3j!WkDr`JoDH7DV}XyuQpY-hbFq-qIX~iF;pi@6;8Q?KvFR$Fp-xE{$JG!P -1gtHx9L+QwTzHYqU;;J`vOkBW;2cSkX0thDC1uMyDE9kdLmGdaIQk){t8TjClJb(HWSDRuqba0%)+Enhi!i&mF^n$EpN5{1|TOZ5AsrY#>n*%7#%DX)Qs{JV#Up -xy?n8NTzW~5+C)&j2g2k|nz*-d;! -`;fz%df^pCJyk_d(NdQgo!5?!Ld~RDa@tA=-0b?a(`Z45 -s}egCywob^pQEE5J*PvSqjNDbtR<4VmWsZ>3&>_F13K2&W_L4K5EuXS>{+DFUyp`Ez;II19#B=EV*=Z -Kn}jP0qiBuD*0M!pi~6JFy4WF|gTC8iU()vn4+eE;(Kx8_PXJYS6`rvh-4`cZ15{HCgJMTQecR)4YOv&MAp9ObMTN5QsB0I -sGSBmG!ZU@~JeTXpmbQ(EY2&nSJN7g6iVMVc^0OGJHRo2`#&%Lx1JyNlH#yic%M)E7NBes3(-XAnTcL -g%=k8FbURh`>xcq+cY2OJf+)U&Kdj99xmJyzyeLR>QOxq-~>a3LibI4fPjwcN+Rpl#30@dFe?NSbZ&9 -6d3OqeU;!2nClN!db#0QUFEMs|MPU@Ni%Ix5dfbj^2XQ%hNZ+{^%7RAHzfknhyL@7 -Jf?2Ql3YbsE)@W(!axf55jp}k7ji%N2uv+VV_poMgY3Jxe^mE6C`kB(gDODYtd-0^-4?$mOY;Tf&Oi)*lv&kXl6uw_8Ql|Qgu3=QiG7?>9S)dtShQX}tr2_sZx#-o+ISt|tiSe59w_yTjDX_#$g5bkBEmEY!sz?qQKT5{GvtI!0yM!DxOMQ8G%O -KE;ZoKoZ??_co1OjV5-Gk12RHA{t>Izg58vbEE+$Mc_l`pfKE?+x{CI%_cO^>wor!BqSqyP*uFm{vtavbE&%R~}x>kO -kTpMthh)dSK1L41NrTY8n -k_q2I#Qcz+=~Mhp3-)|aA!>2A;{Y;V*L2=-&F7u{Cs{KUiaWyW5-;%)|Dm~R-f;6=<4%#eU&Jm7-PgYL09|jiSEqQum2F -P(>134!*4YmfAyMon7`%?|8n)fyEq50iF{oISCYkVg^Up;VX@ -gt*J94&7$QeOx`L*4~Do)tv1|PDfD>$+v8Ixtv=!}ABbM%peiW#~6)S>AnJ6nh%*3}2Fd=Ea%=28fjh -x|VTdpZb~QfNE`s6yv`EuCFkoZkdD4{SH;&yi_YI;A)FgLHYM5sCpaQPxLLfk{PFHbfKY?F0RFX5An? -`Nj7YR~T^ItM*87(R!yHpB_^`AN}SUid-aD|NWcm-%iHEn6%*dqjv-BycTurOgPAR{ERhx$6|^EVw~z -hg05&h-1m*vE_?s5qmYk}f2cG68;BrQ?vJ~#IZi-RJ^c&(b9VNR_@CeXp8j+8KS;JzHDBl^T9-!-7Tsr=&hkMgsK|4maMAj$q8P;%>~Ip09`#y`DQ83D$Z$aIBx&X<8v7 -6`S`E)O`p7q;QfiY+g!P*K~lDy9hR@Gq@3mC0HF3#z7+-I+_-&ZQA3+2FsG(53?9j7Qz{jlHM}*~v(Q -$RyhD1?ajK;2so32U%qq(rn5xUPtAp~=>hiRyhl>!2h8!POm-?npDCUkLgmp4S<@w(3k#J#!EX#c^6M -bJ-)~sdcYW0=8#fWh%pqJ`Y?Ow6-L=Bm9^;@?}v$jsRv{~oCxQU_Hc&tvrvopz-2>I$9{?W0Xs*HFdP -910D_#DreLvPIO0k)p)#b7OqwDFNZ>Ox=r7vsc5P}-nOvJFV=ogZ=BH! -GskNy6gRQ3PV}_^5fo=*M255N3FgU}03^Ax=a{ZDPJd?S}8Ba;w|OlqC{u}R!I?F#p -)h}6vl2}G<^CjxM{fUfY+A53CfEzM8Ks^C~FHleu+{}pZ@5nQzw;WQTHO-(apzrn_|{-azQ>4!KX(~P -R_HAlln-Zf$@<5$|G&yD-n>|<@8v@bW!e_lL!C+02TlM5C8xGaA|NaY;R*>bZKvHb1rasy;$F -m+qe;a*I&VUA0($leQ@87fa;KaQM -wP!=WaV$y=3kD^+FKVJEa;2f<3E?%76ZmaA$bw`Rsip;-9Q5aXIe@bg^ckQc@R&>ut6M4gh#A;TShV@h4p_BPESNlD$inR4I$cNV%TfO@G@_L#5x -ctY8u`^bwUwK%gmNjX^nO5oJ)w|d4Kd!LH#~L;pBXlo1a);M-AvL^>&e&yLm(t_NUP~=`Ie)tiZ*UQ!F@^4G%#c}-4=w>Gk+sINd{MRM -VMZsLm)<<%WS%p0pBdF_(KL$QS2;;8+1O=V`y-$O1_bRpDbGZTgFnRlnklJ-@N3G -jWhmSGA~;*4T{2hj_{mmUa`h_nUs*Q0lgue3cQ?fQW9YN>oZ1vIV{)8%>g=V~N*l -+sQf*~Uwt5fJ`sj*j=?X3IhFnmu)VcTVoK`vSRsrW7^PCt>WqVanp%~B*Zx!E*qJ))6C^XmQ -6hcI}F_LDVK)}Ae7&5DxS8{f1Y>_zSh1pET2z)Dl)fjfbajUE4HZ{Zz>MM!jCH7JMeh|b5RcL(-H4;>4GjC8xB|_Q`{GgLjbkHKMt8v -Fydv6{s@g(uO)7{&I_Ydp>7;XkVb?szPS_{*#{toYi5;-4H42}`S)v7cf(N*I#yrwS?FF(24wUjFW(!Do^($9*ZQdlP*%MqQe8lWtp%9; -{HpR52)!=h(Y->~JN44AMEMnCCGYrA8FF6l!0wTGy%&7c#%6td#3y8o86-6_pETC3QW*>gs0cr+k+8- -A5At1_mKDLTBQc1AJiin>nz`wXCfP^dki_cdF94^F-r}ts7YusJEi{G)&mN5 -YZnYO)+FA=L9+iFKGPD@k;3Aia9#AJX`^9>>S) -`@nqcg*G*srCwcqA%<(f+U{H@xWxidv8xTcK?@+;hoR4Gz4?oQ`HiUY0$f%YyqCAU!9-!A@q$1VUY+& -ECG&_UQo?{@Jr%+FV@v*=cizmg*Hc#Cl;F$0vT%-lM~5!k-JJVDb(iuUHpC$&1^9x}8_14-chU1+4bO -IBuZSJ_jmZEWTuL^$aS*LBU<4(0sfovQGtm9qYEbP9Dp2|RH_DOyzzkJI>ihsK9j!S*8RycxIZ<4Yku -wO6&*c-C^=(EZF)%IgEd&tIZ8n5|=dIK-pXJab#-dP|6Zo)}jo-TP$0ZYSsGXqX6!^L(?N+eZ_rH{VO?@l -yUvWU6-KN$U(2f-tUPxYrbpPq4+?80RH7mM4qO^Cbek43N6aG0!IQ%X=jBb{m!W`uv=G>o~qo*+U41@&v -y`UTa@LS2C+jU6ZLIf48yRk_e1R+paj7|{KUBPb0(ep4(r3`4p+Y(5Ep~p_x5!^oDSamezE2b}k~cMC~rrXO3=Yw4f{8)4{k3HGsb&TkbFdyU4C`K -^Y^-Et)!xEm9EK2N<*r~$d{^K^O|ueM+~u>j7+2!UCP(0F({?P}up?qdUX#5=(uWa7JL8vrNW_cNn$> -?1FmLc}M-bV8ku`G%S}N7fJp12}FMKirMw`m<_yhv_nJ<8vfjRp|U#jMQlZ^!cFB@?qH27(~>%!ST-M -_vD)4v6%#HCU?{6?aR9{Ag3bVgpYpY_S3_4Z(PSsJ>$ln#<`~FdUy -~CNr)+uN04@Oo&NXRT>vCV$xd2jl87L%*j?<)vkQ5i_f8a|=8a&y$k=09-?K;AEZFt=H9OpY#m>rQ#T -z-5xolSKhR;>(dER7JmrIr;v$kn#kt9qmtFmqwpQ$wvoAtJGzW3XOp#g`0qq3q>@#flEN&xt2z_4$ll5nlrcexM9ogDXz^S$moye6Nx_$($#61 -BO7$h-b-GB9&;HuTA688=F=Ie4(R?Nkybj=RsCCsT4L`MjQ!9Fz;tk7avQ_ -96DHHV|NTU}@wSsN!?Y=(jh}zSP@k`+SzWaw4<7x6H5MjiAZi_}P#V>kRIOgSjSqMi4KAqNRLwR8M2b -=p@&%{^aL4w6*sN`HMMbUtT!eOEY$vWvgXKcY0Z<;y)K_li@t7?^i0VtT7$YNIdVXu;Eg|tA~epUHe_ -8cg{%GjD^tQuZ73cVxnKl2B^dHv_r6YJRtyiIfIZ(1e0U6t7MJ~i)+y7UW2Vfcu`t?O%~Bcp&`O=cl% -GO_~CQCCrBW#ffbu|?VR3&Ai(M@Eu@9(zHYU3@?OT*@L?@+weG9Szv;&L#-M&=uHD*dVgAW-L3Z-<`3 -Y%X`6^7Bs@(NLZ@`lLgB}T9b;vGFh__Y$=OU2Z}Xyt=Z?guv_RT6$ra%no1q-?99Pr+9^8m4j3WdfiS -Tn(S_8`{+l=ZV_yiyY~SwQMcS0cCLAyM=dzAE3ik8FWE3mhU}yDab#?m=2KVm9htTl3qkH;aAAlllLe -d~@Hwy`Yv=XVD$#i6w@nEZPA(dwm=kX+9cRz8eH*Iq(El9I+YC^C?g7BZy|OH-wFoo4OSd1uXj7v -;Gn6fp;s|Qb3N#i1$o1q6Yn;g?zX={r-aC4hro7MJ$BKARd_%L59O?2I97b5)HaYTR_B+IiKczh^gf91a%45eV^NuwuPIlcQ(#mpJOIHv9fqh=v21fhF7%!V1!qRKeU$Qg6?V -E-CG5GbZJ*ZqVHXR{6vxcmpr@%IQlhyKI|2;N5k-KIUJR4Yl7DFnnUPw@83#m$e&^{+pjUS3|Ee -T6}HlfTLgscreRqCURAIJhluWp8%|pUvyKP;&UK%%g2Q?PF_E7JzAR5U~SD -gY37pb!cZ#t{5tbjH&IFq`d$Fg8k#O7mPR^htr5<5nD#AwHX8POlSY}Z2*bVid=8oHFUEZ%Ky?Xsi2Y -we3*urWJr0uP_L*3K&Vs%evFQm_+dY?ZALcaG);kUB|g>`JxUXX5~-59Eij@d2m4dt$?)wYLof*?|3~ -()zjyfh@Xf)|;cF+1S!lN&$I7KVeN%zu={!9Mbv%cA{~e0g(}93A=&uvi0&8Lwf{09g#nj(Es~VO -?k4TI2!(Xdgv!5F&Cb9CGE1{d9VE_2CmlmFvr^Uw=A(f0Nvt{`~Iz2A4c8WV*2F;t^2Iw3Rs|5Iqig! -yyG(LdiJ<)|3}bzG7t#l@Dyn_09mbYG1Lmg)~w$ur^)DLc~rYA$B8{MD`^dUx&S6NLyvMfb7U%O0(G7 -R*dzKod=5T=$$Z6tKB0Te$oW&r)R{I2XBu0BuOUglC7%ruTX_$-o()X*$yO)6iYxrKMBB}WQV6FB^+W;Ut-$DV^vk>i#|++oyu1vxb#NWW^$S -A?bX)N`L|Tr(&byeg8Q3!A7pxOtafFKslL1g7fsnMwa+;ybFws$yotJqryTSVG5sHCF0ZbJvhsp)%z+ -v7)n9WLhq81^X1t93Q1|w=4Lro_IQYvM?x=e*qxGPVamPBn8Hoa)Rc}-m}RY4>HKU-7Nx;4&=HbLjWZ -ESIid16d0-Zas*!J*Z;M)zU0oR&F=D^%e(Fdr1 -w8_<`a?TVm3c-(B7ao|arzvk*3{hF34trguNxI;5Qn}5H1Sn#evw?uguUo8I?L$baRtGF~8^Fhd4v?Y -{3!x-%C`ZV!<@V-MuwZ@M15#vm${XSvf-N3-=<7TASwv{13y6Zkz9uzQA^iMlp&By6X}>RbYE;UN*<#}S8novRPYr>%c -=pM!blkoo}A -c&|R;KhS5z(KVL2%yT6^99X$h88WE3Pd#sIe?`n^_a@PtE -nGIg73W&{Lvh(fOR5Gb{3ogVU{GkIm1(RdERhmA_Twy?Dc;mF)2r2epAwD$sT{DKs9!h*eB0;W%pIXe -6%LMP7?!Z7enbiOlm4mZ;=4Ua!VX#+=a;FM6*kPiCad^q72AMb|K45erD!hDn%Hn5Ag2G77dy+7n4=k -_CX@9yep^UP<2<+RBHJURX%05((2d21>IBP^2%H?}p3u>M -6FO?}&Ir{ZK;#ZNnmaMP(AG7|f9yX0i8W`(6~|799X@u6YoKgjML33WhPsHi-oXK$F&zF~Zx66P@*h) -3p2->r^&l4N5=KLEYd3cwL#e`15izG%Cgdja6DB#12-`2muH6c}`uW -d!P$Rp;ZXoz3JIlpjW8?KwCC1w}Y;cs&f2%HRSahE4pjGvmH7&$7T-W4pW&+s8`;N8p&6dN5hLEBZ)4 -Bs!JREUV}O%**dX9_DQA(!;oB6xTQ82C{s$Yp-p}x{+?~ZX?{>rYMhX&3^Fj+d4vzI>rrgG5BRrOrwF -0Z|-c0sEsRx{a||}VxuyMnlp)!lNQE03J2jDfkyut#dfA&$mE6UdeaW|^_6aYHhxF0LvDjr1(X7T_8% -ls9WI3WfL96G~+6YSg0z3~^heN4`*G=}br7Z@|2OE3{nX!qa4>0)W-= -0$p<-G1aT`_#Er@Wa+-<+p~KHWoGDt@R-Uw5amAY-1K(0FDpaJqJtz0oIiCC#h1i{aFb+Sot-l*SpF=($93rY -nAk0YXQmJi=7J7p;YXeI0pE2vx@2Dtot-+k|&vUz(;mx|5<1boeYEG`pPO>A2$0iEEH~72P?m!&V{D$ -+3p3onD&~fQE1b^Bpo)ObtOIsq7N%u7&Sa~wSs^MrtqEp-2JOg6smr(SBUB|9)p^K?BZumb?O9KQH00 -00802qg5NZYJ;_I&^V0Gt2-022TJ0B~t=FLQKZbaiuIV{c?-b1rasJ&QpK!Y~j;_j8IIL0h*jY7;S7X -C%qQ<)F}lV2Q-!{uadU-hZDrc!hXk}$=E^v9heQS5y$dTxG{R)Iy9|B_vvOSZzIofEv9*>;ptY62nli4HdA|TNu5rF -`Mha^st|GxF=7XZ?p?B0uWY!N_rb#--hbyanBVKSK#ogByU^>_{N9<`t=eptfT6y> -PwNtzX^yz3^R!Xlf2Vud)kTvoRWkqnJ5?^KOmQK#N%I1bs2f%5qVMTWpltJPoZP80tLnJPA9R_kwuYu -I-x+E_Ia>iqm$tgBdf38nbqLdx$g2CYsM1WAK{6z5l>nNvpeu3s@VBj?9u{ib=2h9~D&bK8*Cq2rimx -u}x~gaJ=q@et?9a#$uYf*fx>2*>Q`s;pDSi`K*hRHSJe^XQZ -`xV`9<@rdWU=nb+h%qY^%{t~T(?neRhcx~yewU+b17j*T{JHgLK2mBv5i#p%7rsG$1`zF10agsWQdbg{jR!!B_K$Rh6p4WiAPtrwNR%H$(&3{n|j`t| -@#+&L+**aCZ%vW1s_9K!XiF#;*^I#g^uZ!u?(Vs3pTwT6(RH{qAn%DhaH#E30w0eyIixEq3T_~Dh)vyQ3vK#& -V!1$yf>S`F03YJV5l?HwYu`|t!c}eVuFoairRErd-ivt6tQ~3ABW!$vf1wh%LK`Af`?o|LH?k>-i+Zq -9a7Mb$_(gqru(XtLVB~H$!D}YtjY3ty4kMD}AxI)Q&!C9U77!_Q -*m$+ui_v+)e6ccU#(llBCCFZH2~+Dp7hw9V%4X$Di486HL6(tN&F;^V=MsO0xYo@SCFxkDUYKu&0wX; -fOr9P8I%$l2*ADpUJ}8eKx7Jm0+z(6(ZIxn$12NV1rk0naR?npac)6`{juID=H3ZK{5VX}6x_P -Gx&38`SdlJ|8TIAGSiG;#hpoMxR0Twm%>WaH$xRd*KefI2TCjGpAcJ`gU`R?p{d-MI-&5iCCXz92B$P -`D*B+yDXO?7X{naK)H6y~<^4jE3s(Ru*_68Z}|7xatqltosoD&|mU8DtZ&naqN3G|wIl*7Cs$d~oeO) -3Xw1niYfnWgj8AB}wufDnxTHZy2Yq0*9rcQ~5Gov|p^?_F;cvqg1n`IUj1-p|6NwB_=QF;KfJcE^tCf|*wuY)c&=OAx^d)+f#1TbWWgtk -s(N=XmLtY;q-j$2-K%I!W55dn>jl^yo_dK~;nY6&>|@VvtE@PWkI6H`h;ZB3C5Yx$qh}((!iC??{WHTA}cyXP^uht -Ll8ptKAfqJCK;S4NSfBr@`-kA3S?vReI6pL#zx;uF4g_BtjFur0ALTs8shMf{wH;uyz(J^OD{$BzXOV -ZfUVa&w?O0t>@DSr^3>b5gR2;ku@D}MFFd7Z^%E_2Ce!&ecsz7 -!z*qZ8$xjdIK18Jwsd>FLJY>pjk9`A*)RrfM?f*X4k}`r~_**E8v -ab_rbTe>m^RgzyH(RgUrM3%%?1Mz)tO#ox&Iam#XPmFr5x6PQ8^7$I7Aww3(xo_02bBp?k2*9};%cFe -f@Z1y&I#JjcJ`3Cg2?{F_rOcG^~_{MfZ7Kh_-?R_K%jJ*Ej3>$F^fwV*leIQaSJpMwtsNkz&jWznv~(>1TdZ?6QgsH5sEE1%1aMZyg+|ip -geae9n-@Q0a&s@7q~2H-t;S8)l~**SXfxLB6<1-^C(};Mhr|BAQXV;Te -bcRLnlKDV$wE)Ae4vi!qX9d==6?S8S<9w1gCo%|UpgeVIGTsngF3&pmJm;BpU?wM^+tj-)bR0EaQKWo ->K~qOPz;Fx-IiUak&l#=}B11o-;hc+eNE^_*szVh77JM*r!62sn85F66M#6Xm_cdxwb;G_Dfwxg&y`J`A2t~Tb){+ZWjuhBHZ%#R5@A8pbL)Q}FB*LEfCfZkM^0{=xDZOZvx(%O*8Nb+~t ->o-3#2?c8mvqltYl7Ee8&|ZY^?;TQvTwSXXKBC>&*ZNzib8t14Y4`hPE|Hlp{)L}>T-L7{_ZXMZ;}QK -djJ$prGl*Yjk|tXSXsVkL#Rn!t&uv>KIqW;7-UAO2h}U{F~a385t_@DF%|C2tk)nPEyKm2YA!u-d+iVTKdwZ`EEFJ*g$vAv#3mFdlhraDp@u;bF)G_E~dLJ -V7EY<%L_y5f62H~46ot_3HkIT0&8`c})x51`lAS&*qV#ce`yJQ4d4KV?!b(7twBWBL7}+UCmHeB#wxb -`LU8U(oTdXC{IWHP-@=dysa8a-$YPy&-!18pzACF)O^51$jq}jzCbD73%t!veg%%Ef>vGl7ZwrQZfx(cRcdL+lfQmdUWF2~W?Uo -@lk5Lipu&EayN`|YvW@DIEZaJg$IcyHf)9KWKT@$+}X27v7j?|3!m4AT+m@jiJ%>139gB2Nsr?E;8d+sVRLenfWjX0!R6 -xDpL?09En=7UH?FZB%VV-Uf-9573{;vX*?w?W_|oIJot8*y#p#F^&hqUbU;CBoSqcYIHaSJTZ=^vz^? -u^xY)7Po>{kI@k|7M}3`HTp|*v75LUib!9PQDhzTomkv$0X(pVg&e#G$z|eZTjZ_Jlux7UCpTbtW^5` -?V-)IRX7a$Vr>Q}G7itWvA(H&^Ao@Vh;WQub1sVX;DLt{kzorL-j$X2~FLI>Wy}iW?*3V@SI%w^+IT{ -ZqZMmXZexTAZs^t_x0J>7Vtt*VaV81bKT7g;sss-*KJwMQ}wx=8)$U*D6-54Yr!ir@3@9tVn$L&0+uSduZax~z*B^d`e0f%TE2UoXX)O00o&!KXF#m0DyWdt!4{ -|uLVdbyV?L~agPoWgsC9HwR37pBBW&0{CBVu7(iDB*!+iu9Kq^&nlgvO`J@aEHjKn}ZHD0*~xX6o+Qv-T^i647=Kp0M_X-o)P)e43KhT^J)5DSi$9)4qQa2VCP3I -@-IFMgl;b)O5K_b2JH}A^2D<(r&bTw`fLHiUefKfdopk#4vs3 -I`&h9&$4e~qBq@UaI;|2A+X7wnGDeAy`#f`CI~Gggbgf5bVCS5qtv)u(q=cY4 -0P1Ghi4oPMGg`y;g}y(dIXl^TOR8cAe$~Q;?xclJo-6#_n&)u_jLrr3oMUK-ZT_RdmijxC4?NfBk>Ok -ZjP^#%b!18eQs|;)n&2K$?ihej$Wb%hEN$iLJzgw>qsNBwuGfp|c%?sxhHLU*>Eey6F{H>Bb`xMe}Ur!xv -QFuk0C%7do4$hHX4FO;`Uq^~a2S(?DSKPSC4);P^nL~5Xy6L#YjaOpaKo*ZEo5%FOv5)zWj32IC*bOK -4KV)lKdj>U`Zk!lG(tg0$H&Ym=GinRyfp1kAOUWyxf_2+&o3qo?6>xkvM<34VcDGGWxB2!I1b(NQ)2F -|C^2A~jOOF66-gop`HHxbfM!E~`(mGFZ2ZdNjkeZu7Xx@-Xi%n$$NK2)4EDENZSJ{pfv1SbSdEU;u#j -R@Ns$Mbra)yGtaQf|UPn`}JY}kz#PQkE>qW=C6i1!Wze`fHcaMk;~QGozwtzR{!N2GA3PSo;Ho%pqgK -XVZWYnKkoI|y=6lR>V)D!aJF?We`R -i2Zk`|5CMYPt97E;Bv>PCM{SOAAI}08${{my -s?~~vK9Yeyw4JJcS79q__aqx0!$Y9Una6AN2LzSl^kZ^0FX^Bd8J_ltPDy`d1aV!hKiDp1Vk1UlDAgF -6#1R!)iM)id$NgF}FOdEt32=nPd*x+V_0tkQis!f11yFyLQWIX!2CX56f9wNtvDadpZhu9>ZpckxH4U -Y)+%G(nEbVOy`=NAlU>^*WPRIg9x=*>M2PQEz_IDF(R=-TDU@9@dUF~glcEchto&PV2W)T7YpY6RtSJ -a)v#nrL}Tl1-YINh0eT2C$^{>JB5ED0TzZlCYkJGPIw8N$&sg8Fi-U`M;qf2>v;BA_GJ{02LpFNU^9v -54scOf3;PMJT1h1m2XK(SoOn;{+F;T+=r1}5e8zUB$`@H$9vaWOb6n*YQTcSwwAm-GaG3vLeGK#qnLN -}T;f|)Kk;Y_pSGG9JSLu24YLnmK8ybkhFib4u~f$aJfrr$jlZ?;0=Sz6s$Imvk92-O0R=pGD7NRo6d8 -q-2rRpQWlU`7DWB$J+mt$0>6jIXRB4>OBh(}XS{i&~;x@l=51Lk~AZO^< -0a;2JGusvyniOdFrez{#>sIXaejUcx8vIbtyL;$kGIS!*_i)~!L9#upF7$UR=oMbimVYX -c;S>D(C4R%S1&%j{^)e!qoeB16`u6LFh6%o7JH|=s5|U-)oF8#+;X@l-->R*QA?u#(X`u -1C2>376!!Gj$)NM*&_;zjSHyh1xKUVLFmw77-CiVyxzOSJ1yW%s7Ehr96M2xc -=-k)q%?peflwnCwOF5gJ*Knknj=PB8cN+0&bugL3pQZvuV_aX1rAe%5^I*vEn4}BgD~*;Y3 -5KkK-Vl_Ze6Dhh7alJ(Hij&KWE|R_;VILXBF=@vYrG}n?d#>@Km9pUf)VQk;ANe_|q-YXVj7E|63gEf -)>^tY3+Z^58iA&$QdK8|L?Pi*XvPU@z7wV@rdw64srp6C!aHBC1xZ8X@sMC4IgAJUq>Tn`gf#NNw!_?p~FPDs8Uu -%qj=8P1B!Gk(XKeHb;a;C>rau7h$KlZ8)A_g%~nnoL{#(Wc+X8d)UhD8_Uo6v$CMu^nay*}19wiO7k+ -52G?NnEJ-@l0Ni?lntIoa?;#H@?q^#hwbJXY6L7^Md;zbC;xN)jgKm6ZT>_Y}hcKD6~zDgpu@H2~AAj -(gyR*2u}YjlWyJ+Y$Tww58XxsE^pY4G}STWgDIt%hX0yWDV&QVT$bX7{STADD@I>YgKvxjR|B-B0h%*mz -l0l0$%V0n8Pi%5PjJ5jg>m{zE8qB>i{X&IyHnhUkaroq+l;LWEA`uT`&>M3Zv7W7Bd}QefgqPbAyLNx -a=HjvQzv2vvhM@sT2Hf>!`irC_e2*Nx*R^vfBV-YmhOCW6*$IORGqo8pqpW+L^f%H8px{(Pgo*02i+5 -0;<_74^8P60lm)v}4@iH%AY2rz!w^3~uZ(t;?A;FZ8BT)lB3iau`@$&kh5~l`iS~*RaILD9h(mtYdr} -p*Kqv-S|?jOXnwb~Aq&P}=nsou`?3*nt9~Wv=tAU-w(ks*C0Iq)hJDy{v -bSRBAp7V&o`tK=#lp~TqhMd`)He$&xFBjWQmNk6=t-Vt?nF^Tq%X^+-T37gJOJoMvm$D-I-omrlnOi# -X}#UuMc}T^$pMixO#`++}w;KX&IS6QK@jYAX%N-q`SGIKw|G4mh1!4Suu3cBY3W8&o2%Rx7#s3c*@;9 -tbTZzNuKI-hkvWv%Mz6lZ!uaqbxFg6V^1DpSnkIiGy+=q|1%gP*J7m<%>leLVVNi0?=M2FnF<@&}F>F#Vy}1%0 -!}GeW2v0-O(vQvrEtvG&jC+k+Pg8c8S4T7N8g*nmAdfH6rXateHMG6}MdBr#)MVxGg51Zz3BYdq0qpWfk39=Un5{*P -n*fQ>vyOd(@MX7ue>&{v9DC=VG8qhNHP5a|HYY|&%l|sEJ(*Z?3EpEN>6E?6ZrDLB0j=vFz%z4777~O -{pW|M^(Zp&M!&{AjN##&)IUW#0M1d$TwYUnm?Zl6yulKl{WTsD%(lc?kDy;~oex7U>vgAA-D-{P4rd9!)R@ -l6MdMaed=g@aco+5BvvQ#g2^tER`LdnWPlu!xbFuhzv5q=*$Wz7dS7SlU|$Ik2BtywOyygq||ugYygZ -{ve%dx@_{&7+)gB)lOY@83!Yh$hI~p)pu>U7v}i!UY -L^@43{z+^TEoj!rfd-O3l){Yt*tU{F7lauM5xs_0adzg%P^F6BZ?gzVsAj`b>cgt2?LCMkN$bmY_4-& -a7b;1b@@mPnI?V{j{5)TDa(0e7#Q+@Zl!@fI+gghX!wg$F#JEBa*?hXGVpMH}hi&H{2iH>vbJF884dEvEJYed_BclDNFQF{i$5UI6AoUp3p~=Y!*F1rm6L<7({3b?)SMMtwp#? -q*7Wdh?%Vi{vMklO$HS6kLvYzO)LNYMt@^|dipr3I}U;BiF0K$blZ`R?2yqS|BTp4_N9Zp}-3S&R3_3#m8EYVH&uubD&SJ=q=|QLtCuPBCG}764Ot`C+tKHp_9!;cYek>dY0^@Sszi=di+kS%I9PA^Eds;ud-7P|^;Od -lugO8^iF;yk6iWou0@vz5n?jYIhQ`;AzDBS>u>aeNxCnQPDZss#~q;!%1C -1--jY)P-h9?Y>fw?Z4^*(Bng=BoCT6{B*nCN$)-jvT_OAYj -Fvc!Jc4cm4Z*nhRUukY>bYEXCaC -xm)O^@3)5WVYH5Xzx;>S&V|X@LMaG}sLqAn2hV$RP-{v^18Or9_pa)^UUU_r4(|+gf|mXi*>5mdN?wn ->TN`X__XT>azTn+R|8|oY>$iKB=}6C3W~sZK22svk}-Vyb+xx?D3){DtTLcoy6V3w8EK&e9cSr$&+oR -&XVM{((s~Mr%D=}?IXL|^tu#l5`_sHER{kvP4DGelaO#bY%#6gVPDdBrS#Z@bZ*Hy1v9a+rV)MTyd~L -WNs8lzS9!kaeQznx;j=T=3%Pbi_nz|joFv2hN}Kig+qkTgz8)Xl&LznsacQ<~3?PuB!cVpCy|GSgYgN ->umbV1l)_q%Qq-T0NMR7QW)Xu;g0>B1PIdLm>PkbDJ;B8Nt_>*66mDYGxRYevXfkjy=kK;-mt${Ke!L -E?KBN1SCLaQ4RsZ*cQ!KJ*FN=GnVfIhjrdiB?Dmv65w#Y^!T`4AuP)eh-yWUu{7Xk%_rub5t`EEPECE ->J`Y91xkKjM=u$|Y0JTAtQNi_lhm$X*04fJ_U;F&~<4Et8OIzR?f&zO<20=~q%nn=ekIH?@=OI0!;LwPy5(Y&?davZ{^p3NVLYI4*||VDYTd5GK9Kr5x~nEYD4$HOYME>_5!2VTBN1U}46KcjWhm5`@%6~=T&m-{;G6!0I>>XqU -jA%VH4HPijk^CDpD+DW)xs%R;92Sc3W`pj!U-45r7j@IAoYJ-*`&S`h&r^}eOp6$BYQYBN?<1v!lBHhQ~T1$B$vy<=r0oY -D@dG~}GDSYl$<+o1koNEk%2P@*#h_0(pQx6FY9!JySsO0l308NZ)&#s{?9@#2BaA0`gg(t+PK$r1(=5 -1DnHR(_FY=}J6XPQ#K%MeX7^0(<%pU9Ij59px{OnF(YddomD+S#mTy;lJ-zT#<;KYy)*FuqUL-EFDH8 -k3b%d%&K9^Ok`85wUwdZVBMA=xA^7($D22iGpzi~%(9FFl^u+u>UbK*LCcaoifCcVadb;i`H8`OXxFJ -C5CxKIwt|ze17O4BS;c0F7a`~Qp0$;bhr$}iD$j8{Ej??e%MytGevn5}JQBYQ933b4c;hU_YnRnnnl;QrBthf4HoK6-u5 -_hKR{*`RF_!?s0g?*=+ZmJv;2KW|d^6RXruzI>+3DfdiTI0Ro-i0itbtc%!jscV(zkO$xqr{rR -&1pseTyJo-304m>>dYCkebtbAe!fM9D?;|CM> -Eiid>E&UN^=m^qy{{GVE0 -c!Jc4cm4Z*nhbaA9O*a%FRKE^vA6eS3Esxv}T}`V<)HdPv$7MN%>?E0(g3$4+$4#GX6jB)jW%c$#EW> -X_{2bT{?LX76X;s>1ux&88%KJh{Vr^z(i -KAFa85zkneu^)bV!`A66&RGPt(lSpb>oUvP%ZogYmvLJ5c6N4mcVDu3RFqw|K%&<%kv{+7>gWLe7_nu -PCad)#Dw8aQB9lDIZ&{MEA8yObECmYhFOx$5P_VUVeVWHn86T{&Pw{~w5))+>r87p_7Fw9CSu^evEsUbD$<2@HJ0zfb0D()vIB)`$K_c6!SC -CyechA!CQEh+#m=WLi-DSv*&iiO9Ij=r7|d5N;d(*5S?LCy!w)QfzD|UKH`kohf|aL(^NQt7!VEJ!yU -OhgO$OI>_-Hlyrhh1~os1#f_?=n*eFRmp%1mAqDudtDjJ~tx0 -mxGP;M{^wqzqFIS>7NLUjy2qs067G;!9nm2rs>Iu9<9}YfV8obs(_}AGf8?wib+2PQZRURcpET(iX#sO{bLY4ReE -L^rOSvH3v=|xO3Z=JXBzssm(IHPtuu6zU?seR|5eY|~MGG1704@S-T9t@A~!}x#x=XRV!`L1H&jE#=e -zrJPto4NCmy_0iS^Y=C@&zGwOtqk#uCQQ47mgF)jBA7?qvca&HV=kALP?tTdN)u53#<~o&XBxBe68dq -Sr7iq3|AZ4^*n7sVFXKEWGs)tcq$mTXNYQF1zWGRuwt&kDTNEaNQpULhtf&Q}H_g^*+3wf_ha-(Lr3r -^-)cUn{veAIHVfy4HO)!v08Lv(U_7ubK*qJaF2^n>$K>xbFOkiD4K*ylAZumBd7c-V -x1&exH@m;$034f0ADOIk`#Fb~gD(Je5{>JT4YlSOL+<}6j=@iw+<3$M+aTyJs@IXM#aCiKh`b4KNmLmc<2X$|aCl#9Fxw -y#!A39Qv}jWw&w3+LvXyIypF)tS^e*iu)v4esPc##X27J`$xl$Rtw^*UBrudmv^hvU=HjU_0zbre*E( -qW*iI!{Mr*O@iL~};HAJzvKw;D1=lINh&usNm~uqT6H)= -Rz%_-nLSb4RVM|0+5{`xN??(bv!%ZYOA`fmTXvBzm-O5MSQ$*ZMDUJHL3T}C -*UN7tqrcxVA~F0Q6!Tngaf~EON>iC(ig^zp)b|rNaES=B*}s+0xSZsOh0~nIAqt!VqwskAkS%$fbHf|nusmkJ-{4DLmp9XVk1U$wt5wAf -%&4KsI~_`ag;schF*H)9Ucp0BR8(dTIm9T@G!3))@2SM(MKe;QiTzx61m1)8RKv|7bcE)*b2rN&u$)? -+Q3z<1yV+-7Uy552EQY+Bp6p;a*jHtaBDIgViEm^&n(j@Tm$jro0xJz`2|JGwVUH@4;+XnLNWsX!fV3 -`ERA%ND2Uv^;&Euj60dG6?QAS_T<;`6Ug*Th4WGI+voS{O;+a3We~ee~MNAV~)@HEs95G%)m^QF#a$Raql8 -b#FZtXCNq->btshox&UD24*6WK#!x>w9IRAWg;@+nv(cAN8AHwDl66qF6Yt~8A=fZHpY*JYM5m=qVW8 -Xwyh1^Q8^)n0I1Eo?_!9>ca(&Wz%A*}E&yg++GlYMQ5CyK-)ayE5n6b+oILW^`Aj8H~Q9){O40HNbCk -M(;L~gVC2C%8?q%aRCcqInGunxo9yKF$I4_Wdt^3RKpBF@RdI7S>ckE#_=qkN$DVEc4{6Ms>XUmy4^Y -uvfzgt^mB)uvEgPvZueaZi%pis42Upbc$t(KF@fQGl<%4jlrc8=ofOABNoO*KBEsa#d(gso1EpX|E8? -HmF`grU#uZtP4M9iO(1|I;8>N1!bAht*fa*vEk!ZDDQ{})ErO~-j6Ty3oa8AdcnSl0Q??V~x}!=ZD}G*KA49ErWBB14UXA|#gbnPd9 -X~K2@?9!s{VJPHRytl7ncw);)j7u+3>0M|V7Pw#&p6NU#HDDb>*WMpNc@K$&{tTV>oon21A%q;)+*yt -1|}sS6Lof~Id9U><~7`5o=Zyhv5`DJPsD+j`1dk -N+xT5KbaeG!g3`B4NS5uhL8l|lOR-)qqdfU%?93@+$n+rmBn!iAkj9oDJzjtzOE)RE=;iC12m`>nTW4 -oyG`ZSU%$V`C*5LWD*YCmqTAi*U(d+eC?I%cLa0o;?o#s>++nZ%mSRKj}_T=rmAOHOF`}6<(di=LPox -gwm(+@9Sy;h_?nEQCm^TF^bu+4%$4u;1EhXb=$73B{52ZzH}=VM5{N&F|qNYn&Pg{>hO_pAxb35Wq~S -if>_MR_)uIV4nGjWXHa_R+sa9i>gSJta5Km|?INH8L@jkme!96O>p4yBze&ZAIL^K5-r`VSIJ*#H1eoAu3`qh#l;)y%8}gEF!Ob$8&D -fu(Vr_43}$+eZ`jSyTr1L7-~O)i$Hc)u1VtG)lKd`DyY3jdZeV98H0t6g&jgNKc=m-`2nAXrbD$m*K( -|!>a#V*M6A`>33t}XYgxhaX-s_XbS^@*Mt-f4UCYnYFnM36pVI7_!a{{UV@_1mAsmE)A$6IM8)5}!3=ml9ay9Jhtgk;x*Pe)3jrL*)jxrdsRVsAZfGuZC46M@Mea!>Xd -VK00NOD$70}^0KOMt;jw=UvO-P_{Smsar6N2BjI@`ze6 -V;%80M8lE`Sp=du0aV`tOWAASm1vK>Az -QkPVS~%}-`SY{fd&HoK4&vitS6v-XaxT6|1%7StaakdQ2Bjtm95%ZJBl^zE_n&~vnT&)l%Uix?9Qodv -Ud@q7s^Z8sTi^>WjUm=JvT-553=e<-l%SAVUqZUezYmYQfRDMV|F;8;!=TvKqg-LIjoh;LGCFH7*!o8(zEo8r14Yn*XH-Vc5fAMWmxbgg -HBzY?fzx!nH-CVIeZzY?Xa;Dh5Md%LNS4XN9VRJuj@sW!w=)isgcr-99^Hfr|9VdD{RW&AL(1Cb)q8; -}ZGPMBG5K+ruGr#pIVmdOoTbm_5Mq|D$NTDA`#g`Z{oLm0geA7xS{59C?#A@ibx6W!6mvw}QE8urTZ< -^6Qk6>kCl`(c0Zu{SpSar8_)cU*oxhg+`rT6HBuhPx%CC#_tvGVJ<71hCm-Gb!saPrY|K`Ax0pX}o;>lGK7(JN<4GA!7W~`;CBsv?%j-B+=Q&&n+$f?9LYb%0Zp;P=5_O#e@H=_*&5h?eok -f_5N=M+cWIm5+^TSWbVY=Y46)Kr_@T+2x=y}vEcOu(XWT-EBJ_ht3uiNz~cnmyqUk6mXOqMaG4qeAr7 -z*dt9WY-7(<82v;!?QL>@os9OIA|TNN0ibf(sODKr7eLZ4VbL{3jmuV8t2sFabvoz9eGPD80{T7Is!^n1y+^$`f==p-n%eWvpgEMl5Hd4gZ=46zPp -oAEhRdHX1i{g5>coUG!{|j0YZS()Ns;@ZG`i85oUzZ -?N0xIUHFO=ng6rgW^fLkJWoozKU86n;G`|gOl0h_~fKvL(Tv1xHsv|kTv?(5Skqc)=XK3mB}50%E1!q -;11HVz`cCy5p=Jhy^N=yD8oWZl;53*Jj($#M(s@ -93t*hv-A6OlhTl4JKdjHhQMD;e;*nbZb}b53hwMlhI;j&mNxDK3olc0mGf^lUdo)F$J0l`gV#j^3hNX64$XGFz3G^?6DsY&a)`A&sTBCTCasW2ea973$=X;Oga`zh3fj$ZB~ -tg#awRf>OjoN==T3ucNNTIWHAU*HHa8fwa4uu$-As=?Pm$#pwz*!lr+8Ca>6l~ga&mdn{mD;pr6o=UYr~DuN? -a!sJ*$>9Ht!T9*jJC9&1q&>tFe%Yy1zYJ^1s<=DQlP7E5$$1(G7S+xm-jdMs;OzbcT$Yt}EnSfz*!BW}6}YE;a+Usdd<_w -IC5DL%~*QD%|`=wiX*Wp`0v{i_g=2r&$VX1?|mbl?OK#p442M5gdu2z1U-PPh+vju5p_q<_LRou#Y~# -b-Y;Me~UG=vgRjq*Ov+CGdMVL*Pv2lcvKgsP*Pwj@H+_<42LbLIT4XJ+6Vvjs5k!Z^{ewgzx;mu-T5E -S-~ZIZd+{rD(aUz#P~wNCW`(la^$g2+TQQOxou#Uth-g;j;M{=|A%b=vBRZOk7a@yt|9)>X{hbBboLK -yy0S6|M`gMR8(a4I42_YOAOS3mD%xX?~G{5%Qr8upaW*z1Acl95xXnQEl-Hm%O -IRt_*}5Rx0ow^MQ2VrQQ=k_9!PmtO1~d&yC8b2s_C>H%CrZ9bVSn#l(y~grV+| -~$u8oQR&I4{EoL{R$-tMKn#hkYtc=Gb<@qKUapsn=z}0PfskDl);wHBU6+|F*UWR9FO1k)C`!c?1+x1NX?nzv -jP}`c_JYaaQWl=KYu7>j9x7liFUwHT&tD*siDj2Yqy+m7n|JZ`}aT-l8W1fZim1rA~KX^3L)Sc865e| -04n@AU(xFT?<_eUHOLY7rIVrbRY1?3G;Egm_SND@w}FQM1M1_4A01i--%tFi${m+J$~)JV?952Aa@1V -b;w!5lUGNR{FH;ZXne8ltHhrM+B6T^`qh8_FrM00?E1PuQTVX*vdtr#te|mh+JYy?tZ5S|lKM41T#@8 -9k!x|GV`HtPpf;sU}nUh5Cl;4<@NPhl)G{OAHzm&~&Tvtqpc4*#a;cs7nS*>Dq(KohkDa -ycR+1;kdIJ=zO)#uAj7`JN*F8uKD@;9N*^LxryA0N#}=Z=h({-4-*KrOuK%wMf{?+C93hlu2AES-*-= -0xl63yUAiJUtq^J@71Nwy7dw<-3tw6t?>y{Yb+f-%X=T~peU>fLAbqXMf!Jfw;ySu5*c?X>l>{*iVD% -Sx^rgbO#qUPFyH7JyNp$P8I9gDAKPkc0OL4c5>Wv?I@{cZ`K_sdpYUu~R??q^pfR -C#sWX?CM9U};xC7o}QWt(IlLZ^D{ -ECLPL(Te$ohUC0d8*h02nz2Cr{P8F|?!a21q*)-vYQ-T`F;C{KHzJ -xbTC1Lwx5t$&Lf%Gd -wUm9)go_WM__OB4kD&OddGTW?9=wLy>-CzEIPUlRCnpDoht?SR2@Et_P8#6_dzXO4+Hf0(WszI>xvJI;(n~;PO|+ha=n*KJ$WjhmD_|^UB0Y#T3_*!lw9TkT;N -9@wPh(^R;Gw(0>1hL-#_Ds4*G#93;);fuw4k+zy>2OdZIB*awt^Vo1&u8{DEIQ+%qu8Bg)j}J2;|%r? -WjKZ{A)iZdTf$fSlqHuiT{egL%+mG`Ef;{u#w!^7UTwQml~gPZRlX=OmB --0QT4M1)pc$0Mc+wwvQP{Hp+x@kJ}r_6t|3tzd~0vK_rY(2U_KGvjutvCW8!*!G!6pgEk3?bDyRs8*i -v{NyTHJ%hK!K#9*7yC2oX+bQj|UXMPW&Z-nhaD<#9C%|8kUL?)CC1ps!-!JU<=a<*92Cbvwm?ynpXsL -elZg`#&;V+RO_>h_{ij2R -e6fVQ>sLbLbLAMOPcP^L+4^IHKMowEIP%6N(+r<)r8goISqcM9GoFD(i-2+v2&5^0yp{=4!3O(6(m^W -QweXj=0 -hfdI+f9Ec{IJ?k3FTv%${U6}w2QrPIjYKzGGji5o=eq6ke4`j{4ZYv!5JAeX#ut-JWf~E5 -4qEzQ})#C>3;agBh06Vpy9J(S4pk1Z1OobYZpQ^=={GVXvJk{C^++e4NgZ1$(Opl%S;vSF1AiA2ve5 -Bv)ac-?L=i@Ar=bkxhyiU@}SnH)!E|YdKu`asKnOt)3uuQrJSs!EHa|t|qpe2wX#J<8c(3R!B=@-?nQ -q}rfy+&U_DR7%ves8<)cnR9sw?GPb0W@X*v1aefu^wOrN58#^0~ADD)00n@k3@M+)x?Z_bL-BZWUPjSqCeDjJ -851$>fVec6+A>c@wY`q;p_e>E46NST0gH_MlqRSJ`UvUC%`!Xt`vdra4Ys(Q_EAZqBeB4@r+{$qBZur -0ylWK!eu&R{qs@&1D(~5!YY!c4qgkdkt`InS}M`gzFjU}NI(utx#615QXjH?Z2TP+Nc0&U?eQyQ0dNF -`mB-xTkm6z}YT6z|2qui^#Q6S+7vUrvZ7!FHU#e*Z?TF85zcu&O_9A$R-=qv{vHl$s(C -Ppuo|||H497d4O7CH3D0w3e~syWK(-Lsn9O8<rtDBrwsT9{9c^k*_aQAUy3&*(2tRj4j2rQKn -_M7x*x)WL=y9eh^X`>>wOpl`GNl8pdis#>e!zh(HDE{_0MaVcMAdZKYR+xs{e;``*0{8ww<#Yh|bfVIgl!l)f}F#X -}HC{@7uL1aRUBLXCh@(bH~P{we?+M<*U{}9NwcqUun1&i>rNFK1G*;-p3zI#k>YIfNfmWucPwdpP|!_ -`VQV%vT7bPBTrT3nm_TYnCsA^*#LIYSdU%Baeo-O#aqGg4}{zVMiqAvkZaN_kUhTZYZBvQ9y!q?PDF^~?MhTqTGC&7-csOFjOa}udf_$KZoQ -Ou7kPkw60>-@DsRn{NVNtEY0wjM?N-menv6FTdC3k&t&WOIul`JU#i4USLuTNmF -JV(p1>K2ItNDtwtNikmFk{3{lPSPk-$wl%y)P9JuKLIizJF`t@XE#{9jnST4=<3<_*(z_*;qKJ!Zx+&(FZu-JlL+LL%^z$}M+rfb|u%3o -l>Q*rqM5_scz(4-J^?2H`-Eiv!VahtaFQe6XBZc*mi0_ymKo -$q>9F6F@H(VeyW`hib+GT$T^L$zSR$djRLM@_Cf^Jn0&?&ij+3RJupTox7fD%z(|vN!o7yNQ#M3-pI` -{qAQX1y&CWoh^>GV(ZkGW8#tP_uDx}NZ!Wbbt#AI&>as}(G%M&SyB~YAGZD{qgf5i$AKcYIQbYLE|mN&})PA8lir25D%oi+0U;|N{|vBxaOt|GvudvGU3bWk8fTL`iD>1Vee_>H0SK9Erb^$GF0Hh -$q9(2(nQ#_FiW)MJY>i%uNR`WqHnb*DptuVaup8=T7*Oy72!4a;v(AX0hsTo0TfXpT3DA+nyl6fJUmk -ITfGsCtDgF7;SEnPMdR^x9DRZ=jmIquy{-byH#myENs*VTlue-n9I-@sVXpHZ>>Hl6?9-G;W~O?o5_;0)+Bv)TSVzzzg%9w)CC4WifBaM;(51aV+2DDh^U%lX=Y`Yp -%iw3^p!QM%dM)_^<_P;($58K%N#yQn-fGDH!$6g| -~#c|-NzJ*}i(OfYNPQdF2lCNAy-B`3P0uJ-G8QtgB`7)#Y`_47kI?!~Pw0L(*!X8edfwpr)mz@O=2}u2KMML`;jp*r=RK98S~om3xCmZ-G>}V -{LMsLT;twqZ#e?DjM@$EqTeyMoQ63uvZ~)nbY0?nl%EKbVFUFtc0z5^8r&u%qk-0pf0GKg>D-8oxt6S -Bsp+Mh7atU38Ja6HKqcc+?-PMB0N;$klG~Xf#0k0D*WYC@Lp&PjTBuI1-F#~4dZOZ_nj*B#8apZ!}UR ->_1sSw6_S@-3rT+)*)@}!F{QV8L#Ot^raIg`i;K{kJGCzx2#G=w1nWI@M#`&b&Q(HCw5n&h^93Pmo7l -X;Ae~i56{tFyw$;M{lTj5KymYW9+5fDF}U8g>JF+yBKWef32_>mDlkK -m9fqfc=mkSxkPqC-5feVo#xw9`O(byttba25BT6iDYp#0tpEmIBCFx4hL!h0+3oD7WSK0Yd~xf}J>#IF@ -|OcAufL`wC|Z!gBC|P+)x81keQ-c2VEVSp=21e|G)wmg2rSbTtZsxEGLhc!bppL7jf)MSI_@Shp1nI~ -z*ld_p(bEU`QbXgN=q+c&C8gdkRHg_HDtiOgFN!oQztsZWD11J2=3yOj@Tvrh`EvbJjY)Nt)>Wro}^h -yucEU#Vb1XEa2f^D1N0{kjyoJy#Yz$&(hf1JA?A_oTQ@d1o4u)e#hnXNVb+_ycY7 -%JGSD8PsGy(DB*+U1xl6f!^Ju*&#RyUWuCT-1^tsoWqK{JQgRUB9iVlTFM~XVEA`cbyH_eU}!{tBU!J -mknLbT(4l4Zz+cv7c*Uj%D|E;-Q)z)SR6J+z -r3)QJfIAnP^bo)sY?J-c3b3$cp~hjqHKhxLL+0QUC{XT<8*f0Db)KakEB2FuVn$4wOF( ->=dyojQYRf8w4DTTihE;t4QcJHbn@V51SY6yTizN7e9nvHUVi9!WA{X4Lr^G&8hg+mtG?0XZY$&@L>s -jGD*Xr7jcPO1b#!ztsfq;hVqp?s@Yl8egY3ZT7COv&*a5bsnF5R?MN}POJ -H8&MbeFH_x%VP7;&Tkme!`iktEioIjzNf>Wijwg#@)-F3P&7{r=>Y%@Qq9zm57b;sdU?)~jculwF>*= -mnpXfQ_p-uJ&h-ob_&=SVd;c5*AH-PL}-H#)-Yz`>wjfz}hgioS_dm{tI6!MMkc^j^thM&~-af9S~EH -w{M5VfP_87)i(0JF@kv4WIE1(ZR8*h*D2$OFbR*P)lnY9rh2A{lVdeVn=wAr)Fj=qD~+Q!?D~_zp%=Vbp)c$@Oil0m~9T2#HbofVsr;EiP0ATlNj+YLZ}cU4;A -81um9)!*YC%#-u>n6dnkFVzTJQm@$wI^zI**9noMCm5wXn>I)i9XVoG@M43l*_y)?iXMqdz|;Ws#i|2 -A+6_dz|dfB;6)wEVjP0r0TWNL`uV;_7qNrGG`O$(6sx-U%bR*M6CA4?a((Sy(@<*ENKE=o -X5QO#+xNfZnH0DN?-%7Qz5uzN!#wE9Jv>JDK^ZUOn@HS=w60sUdX(f|aU(2 -w0W-+9DcguM4@j?4F$Ls#~oB8HPJS1|?9(nF#s-IevgeMwXjEyQF|^Zj{fY?ZX>*S=xkCqU8Ra_sQf -X^7msXZ+8l*KHW(osEKkB?Zsrt>D0YRsqM1nRNkQvzx;)AjKSP@7qN19Cy=c=Zd8UE)6+0o^tv?Z+#G -spT^^`52Cj>P9O;>vacTg;W(0R8c~~I?gD|vY|odLs%1|tr$tSuqDUD9B7in_%e~n>;f_*6IWjOgO#g -Wad0xpuHu79JkP`vkX9chu|?L17`?uBpigNnS`CLmzNR|Q)^xsvG-xeP1=IyUpru3WX@=VrTbX;ljyMWwYq=G{5O2%-D4;a)pS1yXl;Vog(PqoNY4UT8uC-4R2 -s_%#s2pba1ei9Cr}RlUNgtMRnZb-eU}$mZI|CmW}vG4aYI*Zvm=CL)>8`@3##q?)HNdGYm{ -qQndl>Fai32VY90C2-LYYN;cVNO1By#Q1&r$VqSYfC0R2*9-uMX7L?Y6g|FB{E*VGaMd=Nmj|;CU&uB -*+~j`1iA`&Q)|09=p?rK^m5an(BiWNcP(u)S@dC -{8sOr~$$?%0a-=rhe2{FX!vB$O~>Ui9j!tApC7vNLGICw>E(0)0*IKeH2OZeYw5aYz2iI=MYP^=A}=o -TITI8Lx}Pvkg$2=5`s0ddI_`i{=fBa|HODXXyLJ|pg%4py6(Z^5C{N@KxQp=rbJRban?5*gIYE`24~Z -U%R-vpuTa&>r0v+$}7M?@a;lx6UYd&o`9(d%2I5!EHh+bAz3z2D*tZMmg%{Ebcrh|5+GobKVy2q~%3p -QZH0Q&@WNa(Ar=zJ~4rE9-v9y(r6c=M%^Kskk}g~fJHb~z4^6cTsWR(Xb(MEK;CBA7{#9`j -*CH%!~WzEKvqCrfjUrEMv1b#pR-L#YZO3r1!<%hr=cOd*`He0|e+E8Hu?&j|D)zM+0yRa#eZKPvSRbs -z>4imwRAZC^+6lSq{eg8zk@d{VK03viCCSH)0%Dup{2S*nZSp~6kauNIBJT@P@>A8rkazKXf`##Mhg= -R7X)z;GT_z(7|FwDV1~sJl+~YAkS}vp{J-U>1C6wLXSy&xde#PeKjift*Jt&>zDhZ`ms~75mkl;XJS>@(VrmVv)4IzY9c -W=ng+zn|A*hEo=uP&t@_j9S=!m->x-9EGttv -N$Vg!sUly>bi?#;@yjSWs~%Q~wO@5j@Nv*y_%NKJrEwI1~khjb3)?6gds;|E2;10xXHo0A(*!=sPnhi -TYIzEkXX+2IgDzCP{*$Wd=R_e{=YSEE}eE!!(GF0XCzfCdcA>zft7=)j+!P32>0O}v$wqwG6rZA`pWb -KvTcPKMxjDX^Npk?C_90D`P}QhZU>gt`yYxDgWv#9!zt>zRr#=S5|Xw0m~eK$#W9sjI&=s#_=jwt3OT -l~owF)PQ(UI?W?iUaocZBoHAkJp~>dt~8{LTfmhbK7Br-H=pX^N^z^HvXaF#o^g269^XmDD5WlIwR~Z -?c%?*4z?H5>zXqsMl`SN43GV|{I{Iu-rN#gTZ~f|J<}=*-eaOp)O5CkszdtycO^#1aT2+?-RpF!I)1& -^g(dcLZi~jKG^WpK+(ewV{^W&rEPY0u?hsVRw(^20&&uwy3Q8gj%DTev0yrvi~ewSN{m3I_3tx3OupW -Xxf6eCw=o!`(;e?vcQgnl}@1N!M#22dFp1cO^+2~{Z`k9_up;VSdb02cd|J@{-wp&E$)p4i;07qvL3U -lo?4d0(vIi_Y61IaUgxNWkpgM+iv36T;h!5;R1BNf%L6iWMT3sYJaQ8Ww&dZ*t3lERUZ(*R$*|psL1; -$?|#iz|}r+G{NbE4zAPW=QRderv@^caWh5Zs)~k>2G0*a!*Cux9q@-`M|T`l6k^js{S|^EZ~MUW=K@F -m#qN@p_qa>?+w0isv(4+++HX?HG`|ka<1NL|02x|}VNeaLcXc#gVitZ#35kE2;laJ#ABkSc2Qq@tJ9qAEmI8WZ2wfEiHlu;jAcJHnF7Q;~;;CEw7)3RLn -9o%z~P$s3AVN#R!xN?tcw)==GD7Td_7VvGX#<=aWkn9jdWNOEJGeuE_cw}d1&M))^Ka+}O=kmS{n9NB0C+muY#l<9A|ZwW#VN^Z -XdMg$DzKbJ_r^9y)x+%9Y6Ky28oHd^4ze1^syyu(~ckhtJ8zKl&Ab>(F-xOzA-&*Cl(KHc>9*x@56B_ -d*`|e1_Z$tw@X~20619b2UK=x%3XiXn4@RsY*0@FUC>#P6a -?y{qi(V-s?3aG8DNTZwYSEfaa&*lTU3X43D6v4{aqHP#X1+qWY;l=e4x`XE<;! -F{Diir+HHC{%5scmNl2E79vdEacD)h5#{?d?2ud6f=lLzl@WpCHPPruoDhg^hl}6Z4z_RV -LK?qij(uI3V?nLzSp&5N61-i}hI1cn3+JwPO7r-*kDKfMCRBE#%Dj(NjvGU=>#BSk<07e>zi;@NA~3M -4KX~pUmFrRiVsGRkhC92%lY`-5eTgBIIjUyH2u&QiaPCg20Wwt6ulbZ%epX#Tk5WCfd9CaT!agLc+D; -0_?C+1bj4SIM)9+71?copAV{^m{9C|fJ`iIIU!!=)Ss`;v_=Ic$|zUf!=^Ud)F0lj%m1)jSD>SdMs6L -zX6?2dKL<>Jn3v#|630Z>Z=1QY-O00;mWhh<3T|Hs8ejsO7rXaWEj0001RX>c!Jc4cm4Z*nhid2nHJb -7^j8E^vA5y?cM##<4K`zdi*@KQ2iNVyxsmH^uaO6j^Tct9&_@a&mfPRS!f$5-Jj408p~(sGt3v+g`EY -MONDMoUn~W0=u)jv$L}^v$Hc#qG>UnPts_1H%rPY%P$AByNxFsPolkIc2{PXS9R1I^`oz!KmTUy>*w4 -5Gm73P^GS3(h~A9;Fwe@A3cpXwX;xKPkw;k-U8QAueivPqNnWSpIJzj)G%7Bl(N$7jrg2mkQIg+9v$U -+BLUCRvSq?1&oFp1Svskvi0zg%9QQszI3dP4!QdPw$O8{atE=Kcdn%7BY=lr^C^VYP$J`H9V3JKUX$1igJ}Lw|uV5V5*BB9)UEtr81~;3ZP -qOMN1}NhU!Jp4-cwFJ*QJP~t7|mBj8CB_If-o}ZJ&oA(kjg+i0Az-!tp!=s_U%PexF{x*;ueMo&F13_$5rjn1RlfN|aB4$NNjr!vqa3{XskB;8GKK^m{!+vyd6utlO?%xky?Z1jTyGQW56GuND9Do1rhvN -uJeAs<^{HN&M>uC4wpQ1k=ynTfj-~W&IANG%qqIVyngE#LF5B6Wh(ZSoj!yjH9y!}J;9n^gL?l?L;cy -n+JV2|HXlLG8uAL_jZAaC|R?0pXpcE39~JUISS9KAj`ev43F1K8c@{qBe3gS{UPcRxh$fB5kJ-O)aD; -S~UWd+_%42WV&i&HmeC7!kA#Pow>Rho8~W_q&IO)ZFe5FoqA*$Gvy&|McPD58oe0-@iM2wGR)!+lSum -es{RfjlKO7uEfLLgEw*XYWL0VANHx`Hf;gThZPGmN?K165KR}1vsvF1z%)2lVSQP9l>gfR8`4Tx6jGT|iybUaBveRY?NMjBv@l#ZoE(Qql -mK@IOa|8sK#6_?wqm*&M|Z`1V_f82SZgV-+)0|0&}I028&!9};Xc8lEg1*)h_Sh8Qjf0iq16bW5qsex -VgveklZ_Fzz|qdjq$CzQ5`5QJNsV2hz9V7)PF>ECaa?Ic05dCrmrU+(fxo|l*~teR8{enQeUb>8z>-) -W8&9Lo_rTtO3H?}la_146X002tmh+WXvjQ6-Z(^V*kj^dcFLaZG9Y@^_v4{?5+EMg* -UW5?G&^XP7ErDA)w-GRb6il~i2riJ}_SduS}tO;Zv20py64dr~>-N1IV6?!dpz$G__^>?<4>RP+W&?` -CC|!-jdqZ#r8qfoD-e147CXYzO&ePeAnhLbrlC*l=cZ++J)>tw!!N7Hz$ZPP+7{8%Nz3|DiYd<#*jvh -Rd@Aq=?j^cS}LLLJUmaPxB1c6s(4Eaz25rhj$2=NGu1N)WsGM*d&1^RFrocL^V;KOw&qsDMR($P3*wW -U3%XAb2lnr&%kQcK!%D~7~{S>pTJ1Vw1#a99?-n*eYTO*zmG<+de33ejz&lcC;`yQs89reJ+_ASfh=k -J29l;FnB*G;Z+f_eQ#YYrE!(i}hR-Rkpu;FcRK+w^7=md5bWCM^;5J*|&9V_}ZgRdayls-Z4*)vuVn2{&(~`JqOWm`+r5-|FZM^| -6-wJ4n$S*dy)i242)jP>gnk6U*|adK~>DlQ3^}@G9Bb;{SsQ0waVSwpb5WClj}0QP|Fy;^E`XHxY#q=CFA%1-YYX$%A!-hqFr?dL58*g-96Sqmn-`(Nk*F_2VCFp-Bj*GowGM -^HtKa|CMhF`4q4*~XmuD>4DNm=s`>CZ;@4enE~v$CQWZwva(U+dkRntYe1@5(ntnevax-Mf6m-)Fqz( -)SXfy~h>y9gKSa(P|L!*T^O-_KzIadm#&ET}h3{pfX4CljlrH1vTvl`eKfdvX(EDQ)9}f1`!S -Yz%*Y0MP=b_a^-V^IWHu59ww42}>PaXR~8s=Uh{e%0Dz(kLEM#I(|PcuG3t9*(-9OB0xGk+35a=tVXt -lvHB|@@v8>-pJ`br)NgcYzsu)Sg3p_u`SyR#GpYi+_}-+b(&OY(e!iQfx%?U()pwJ0j~Na -98wX$j06iG6+d}oU#3&o-e{gG{tZIJD0L=#Lp?>wmT=>Qrm9TxOMYpV$%g#UXeVw>3)5g -=o@b+CoElK`nM(pFG3+W)0fA;YAOl+eZc>b{2~nUJXTXh)VQU92$xts9SIqK@>=W>`gj?)Ft;CuX1Wz -gsfCdTe{h!Xh0p^Z7iEw||QK0)3mg{xD>Gn6WiwH~YkN|l0X7@kN4nULSI@9D+mHjoPSLZMcMgYF7OL -~aIy(-}W{L295@Jb4ZkbFMRCM-&!(1V*6h#g6JNiSeGgAI#>9}&dnB80JkpU8%apt$O8dHJ1G#BHmZP -h(JM@)*Rbk_silqwkhyj}^X`e9cADD1JNX=3Z%H(4I4ET(E~^dYRUOe1-cSjHml)I -zd|~$hks=u-I*&<~_hB38B~h{gaR5&2L)^4qy>k46ipyrsv}%s&}I9$B*+aD6p`x^XYZECtvSu -ZJ+l0@b|PILY4&>cxhgtmM)4Jl<2R+K#ECu%0ha_d{bv2lx= -OA>l%r330cH1tA9_DniR8upi_;ok2C5WOdJ?`UZ73dNxRAAPS9pFz={z_N-=~Ms@8jr29K8f36Y -UA4RFWlE^fjD3tT!M9lIK2|qD`8Nb+DXq3Wb=*%pA4B=B&D#o`Wg{J?;Mf<%`b8ZkOKGh4lsqyt`R#c -E9a1H>R0`CZfwa0@j7XMU>2nN>5AQ%rv$U12nAj*KF3a8;z}Itf|DoK&vz+={8rR@(f#+0+!hjv&Y$< -QZ0MTaRuZIUT$oNwFo~$LEIpzMp`6{{f|2BvO7 -u-!`Mp?st2y_FpIGBbeUKhL+p0h;(C<%5I#US11>AZIA_e|M%CvCiNxMV9wQgv}9RQTnKh;if -P`tH1TqSTp(xJx$%Lr@tAJLvcl@AJt;6icRC{Q_6D*N&slXrd*}=C^~IIeuVoItY0()>_oSzWw%VCXO -p6SCYBsLa;b{wI!$M^6$unwyx{F-ip?WAUnD@z$cYM25n=E~qBzU)tUf#IkyR{?W>6nQnPN124gyPcI -eZJ!H7p)b1?g*K5ggVSKvBRq{A+KvP!X+gSi$2iSOwn>sio$-84GmQ-fiTi01)`M6^7FQzp&L>D1oE0 -gsU@QaRwU+Flzzebbd#6IW(!cecg&!U4k#RjDru6me12t+2#!Opw-O-0ve|WVMHd3bzo4 -Nfo>p2=F`eWFkEq}C#V_1RPg{iQCODZw>?cPu{B55#xXH4VLA3XFg!F5@+dp~7U1xD9muM;i~718o== -WtlELD4_q-l8*tdzRGK6I}FHLuzR#6Wn2ar&rr(=+)df1ztr(-;moxtBd+SooiE#s%BmqYCLoKY`7gk -JhqkI;&~pi3;#;!9I$nv~ZE`D|VrRK@f#EsvD8)_M867aecW?4#XyBnJV8!ifIJZi>${1T3tW?eGZM( -M5=8Q3tim{sx?AB-Jb(;mIs-kztJhTRs!zQQ^vcDF_=n=-Z069X@A`+$9PNzS@g=>eDhJsd58E=&WZ?{5ZpTrJnSQJKxjOs(V%GbmZ1+o8uA)_z!Yn+yM%{(T#&e4;!ZXP`Vccgq3Jst(;Fib}fxS -y}Jr4B5DCYV2TUeUKta3z}Y&uQH=)f|$YwfXTU+x8lVfy?Rl)ER{bsFt`KKvXgdVe2-CRVF4fG~$A;d -p>W!%1@0ZLCkc_y~IaVmBWT&h*ch_xB*3Z<)iqJ^&hLXxoyrF+D)CnBc(ev4UnWN&vm8El~I7Ii92-m -%Aox%Sm^NO_D|6JcaHNA{C|_`Aftv+adv^o|e534wW#+g&QZ!91UJcKZd{yv94Y54OC0C@OpV$eVQSY(98_t9xM-LQ3do^?M)j5I>cH)5tvHPIH({ -BafmRW+`71ItvQ==JSav3!PBpU@t*UJwnpZeQ6E|S&5OjgQGa0mZLO~)LG%=&Pr|LBd%KRRZN?~9sZq -j<_6w=5V+z$4Lz0ZJ$43ys#kK!}eYhYB&Cu77yHl3Bo7hsu}s>R@vRVtHpPKU=b4Hd~QaEw-bR8G7~Y -ef)*Fuu`PD{AY)x`@AD+hdjh(D-#3MTVyOt8*?hu+QV55j`f{>;daoTOtcqGVvg!?9$d|VuE4?%=xx@ -|26Q*FqW~OWGfY<;~!ji8^g)}=nF6SvZizXBLZd0pkau-OT%Qz-0Yj&lCJ?StXFmpUB1FK_wuh0M;@s -L)bGmoO{@HsSJJ)9GnD|`gZCKjq*CzC1-m0(?&c8btzplCLqv=x8_FrOlgp(XC~e -Xt0P5e<9vB~*k)jwT-b>l8r7&BzNIned|uXtTOiIYgKEZ@&pf`eLBTx04+gDq0zWa9zXpDlOn2Rln~w -;+r0V08ZrVX$Y(E4qXP&P7Hr~*2xhJfXy!z8AVA>K!V1HJ3b1r1Win9e+&_vAcRwB@%e&@z%nri^8pM -AIkGu|%qr0n}x@Lc{EZrj^xT#8E%C&Z>ou`r_&?G@3_LGVX -Tc6HwHfBMj4y0UM}-A8X3>_tOw?USwe);kNC4v88o|K|Q@~f`z~wpx;}sA3yI0!G(!?t%jV -#vIpbMyAao-xpXRVz!{Rt1BPtJOOOz-#%9oYqcJHYLqzOFhGYA+2+WU~Ea?VEL!0fEC>j4Qm}(Pn#f` -Hqs~!stTy`l!X{56wX}T0mX}Eu3s^pZW+4&y7HEDB>i#fy%@YZmf6WqH)k@Z`OB9n=2MM|0fCjmP5W=TPa!#*n`+T?crqH<8rxO$hLU34TcL+m`VStk>YcS}+W -67x622Z3`{riYr!a+awB@T6KM7v9&t9)~0uOEI-7<`JTTuyIcrb46Gri2#J~K0f}ZVvO{5$3wF^AuY8 -5}P+EX5qUSApK(smJo;NGvL}IH1dvm+*br5DXVz0O{o6ua}A^lF|8a~B;mJOtAk6P{beTNOIG!VCjp5 -N$2_6d|a0YSzVba=D;=H2ptqcyM2mRz!IA_R&Tz?;evod&B09W#>+)xt*pLaqwj{V=YjAn=kN8D(PB6 -zC2P+90DepHERpr3nF`lBFN%papQuVb+IaE5U`>#Giu|u>S&(18f?;c|by^1 -cf{zz=M)@SISodTH-M1_VSm-*vtD1(ePN(xhPE=6q<_j@w+K8~mz;pe(N-FYPa?u?}9o0ls*cp!XMm-whf&kJ_&6z-=-5tqwa -K&y4=eOt@7Fz3yJ<3o_W}NB+(S%n@QnS-=R>>tozpMECxnVJkehz&^I-Wtzy~<6k23}a#8y4E=qwCLC4mLKTa(EsgBU`&q3z_m8|IGJry_7#5f8FUC -HTxm?PSQb0y$`^uy_ZSvBjZy=$nv>#Mtuq``iP=DO)9Y=kgNTKW>r^)W{F$ivcfDi<*nouVw^lIQMtt -S+o5>Z}Xs_LwfJSbfm6Da96&r8JA}S)EBg)m~?+^rujN(x2u^8dI%3s8^n)iyMriMV3F5LN|x1b1RuI -X~Z%%sb>v(gt1J5;_V*3Az@n&k_EpunKNMiv(6MfQss903w_mnSM|oxgv~D9RV{}P>g*v{&C>`RF=ho -eql$g$Apmv!7d;2yLzL4Mlzeq>h$kc&kIiHhxQlw)v~eZIb1>%(5GmF0H4+phY*WnQHUP);tZfbo;yP -lSc4^O*0ihXFmM`5UJE}UKC%$-2Sc#HNe&{xXeNalg*CEApn-ypmlS(IOb7*CnC@bn7!cV;P7llvw>< -`X2z`dd2;=Eh^)UOD0W=GFy*N=q;T?5sPFp27%r?oOpuj9vT2}0x%FA#UDZa&Dk^N6Tp(PLk?hIKV=JSQ^%#a1ybEp{L5p5Vn -o-r59P;G#PR(a*2FzUE$;%2LpLVUzBo$vq{9x2kkD>~`(0VOT=PQHS88eA8&6v8! -NRBqM+tki_O%e1Z3UzpiuU4b8b@_Kj+)ry@}d+`+A)MO)nwcNP+=nyU*6T4G;iu-9TK){cf+;PB&0p; -%?~K2k+u32MkOR>th-PJxy}#<3HCJdm#V!~(D4Ahj@OEq(EunXENHjN-F&fULu|57X#DzTD{Ar0F4^^ -M -kJ?YIp|ImY<_oC}xyIhP9Opr=P+$*gx)S=3#bL*C;#w#@CDnhtIaDlYT#V}E(sI2VAK-LitePRYMY0( -nuZNvZOyT@M)=tL_C)Z_55SkoB7!HSGtx}fR3uK5Kf{LGP{U~+v=SZJQ<%XXz~>YH4XOD_DHV$KV@x+a$li7kjsl_P<;2R`_ -g>c<{-U5-2m?|gP3O6{_$3-_EBZ9S=orAkn*IG14?R5WCuC~9$;R_!!u%T -PHFa#{30pN-UJb0CD!?DqXI1jkP#mHQfy0%~rCWV{mo0ep35ID=pT)kqy1&8({P&-xlkbZ0o!kbq!?> -_a4D|y?-wqG)DS(*?H%z=_b(#qDYW>Qc7hk#U+q|rW`+e19f2xM&x~!rB07t>Y^X~bKb?s_?ZgDn|Ae -Gzsss`_y7^8To-8#r9(FGIe{icmX*{R7N0nxqg`DI$8DCb}A@`NZ`?2$u#x`c~I9p;L-FXFfEk**oh5 -fjNd){#rys_@JgTzd^25}vnioA7SnOxH?BtrqH>p7xa4PL4l;h5D6U!yY*>w3hXVLphybBrW*=HXWVz -p2u6;zKd(vJBmF!T;JmsyQxbRj?n5&PAG+hWQFqFiFQ}Mp5?}(Lz6tu0uv%U%6hvqzYA9xR~Fqn*q_L^oIJex!q=JUc4t-V0rRx0$bcd>8rguF^43-v -wbRVJXA27*T)?mYHozhHD*7HxsH**ZbDE%FD05`AX6|d?i&eg7Fg4sF+y77YH#x=ylq9P2D%31*A#eg0HT|g$U9SKN^=aJ>dlBqg9t`nA8@P;? -v~FzYt{)J*FT2b$k0JDiyFr0^bGObO1(R{mSy)P%o{%^)>kI#;JMR{o~dXFS|Ks8=Y}BiFPAS^K1@&E -;?cIE4i`|kyoA)ZMG7aM_x8o^be+Z{gdxdTg@zr0M+xtJVl$9YRO9NofnOenJ!oVz6%ubAjhCvj8aRHVvloGKVim6m_Aa+X30WdKq1_v} -2P&khqwP|}msCZk{>5dajSjAY8n7yS${{gLo?(-(+mGbSfL&_kmb4(18a@-ijRuwd0A{Y>25Jg968Xb -N<`0Rw&x6`QHzi55+@}3Vuv6(DqONPXzDRSMG9N^diruII+INts4<-MdfspglLES*~Uh^9$K<)Z}QSp -lJLvh-Fb580HX+)45p1A8B2;RLB4OQGG&l(Gux5@;giLN6VU3=176+}7sy4wvNp*WK! -Z#ZMV!XF}8x0)#zK_KN*r;mS29zb-qUd`@iTk&`d7{MDa5 -AxD55>i>F%83`m}VI79ICGAM4&zl$XjI0}lbHE;NX(m-HAPVpXHxd%e*?pOzsI{~h@~Fp>|P&|UXcu?3P5dK7eldqFt~TSoDbD#_}?Z!X0|gA8jtCmV`z3vCYJxPeb1Iv$|N`p8oxy -!tnTPr%mmBFAMuVV3wQ8(~9I;%gdwGMQ8uyRJJgz -yBjL%8P-0d52;BATeYUxamyw(UKkg1Ams(gE=+UKIFKF8z`y>FKR@elp7nnJ^vUmrTmSEG- -|loyPQA!eZZFXTxP>5FsBr~SggRT^O{q$b3-6+FHYP=Kg!%aBE*d2TA(Tm?wUA}a>2k~IOOT2m7__E7 -mvl5R-ZPZ(dVS|6TB8{##;NhnNJbf&v~q)#qduS2=e_!zz$^qMCzu|@0c@y}K&W-1y9=TMv||WA7a0Y -QZLf}g`t|unXlfJswZ8* -Ig`}UUD*$>^J1P~=f!RQvO~FQDS)?RtkvZ(cUFyO$vRJOdH0ntR!m(snYgV3#y+I8(r=pHS+OjG?{Vn -6D$Jn5gIDA_n;B~K_X8?I6i0Nb{3I7qmRlFCx;D)#FwIt`$sMbUl)h3eCw74!b6{)G7)IAfK6L1rUes -F>oYurAj>JNj&W_Pa;754mgTBigly}VER<+DHOY*_WFdbBUdJ(WPC#*wph=nTXAlyz$rKzh<}HT%$~}dyU`mAtM58lqN2@VCwpZ|X8{E&BXlVrCCbr!I --hXX?H=F6pG-p7+m%4qL(n(b$JG2AfxI#TO$+vaTW6|#;ttX@u8{XC3U*^a$xV^rQO_782Fy;S1xHZC -h;*C4UXd<#T;91Xe4)GXo8%fVMkqgH5_R`nbJG1FiheM+H*~7+advT$mL}4?KlUin$3~PqOs|UpDF4BhA~ijeDkhoD0ZT^GWS)0fR5ae891fAC_S+W-|ffdVYuq8qye%TYn -Y&%i%Ni!4`$!6F~gxdcPA6J&Q#4EGk~u$C2}6|)gyAQM`N~!BIK(RhCANl&Kd06cgOoXJUKdYO -7si~>RDqE_$yJo{=c>15hV0x(XK5ZPfaos2a{aw+6hXH*+?y@m_q>Iu$&uK62(IT+zUeRmJ||=j*d;~ -$RuZ^;R5X1ZbP%b|EZW$e<*=?MSHO?$M2W2LU#M>{@5AyN#=1vV6TbQ>{+LF?={)nv-C9~FJHGPdQLH0ZNXwp>#M>rO2Xi -6&Yf36Q%j!E!Tgi);GFZNzkPj&K$=g2IcMGsPSnOLvd=MkLTtpe}ODr6jy#n0y{H4nNP-UGbR$$Vnh(>J7w^V?=mzl6&Ciy`3l -IB4=lGoG;2gBMy?pZl7;`qrDkrPB|^<-sg+FSK)zHjAsq~dyi|vTk9P^P(LuZBZfGlhxp(YUE*EzZ$+ -5kap>+(q6D9(dZc4?3!%sWm@Riw-1a>voMJGP`b(n>GzTxI2v?=!m!TctOx4m6Sulae=sLM%oFpdika -8MTTc{2CfFUUxEgi`M8E^)fJ2uM-bTa-?${R^^aD_3zDr@hz0b8M}F61nex~wwO=y9-+L1D2Z*{6eNh -gRO9^XYAd*V=$m#zo35EqTg%gNmP?-*NK;$J+InC)Db*-buA{Hoe+uwE8^uskHh$`suX#tawVTJ`GVk -V9}W-vz~P>z=CcMsMU5y3~GcDY79e8b)0t{*+rLx1xUu@b@?TRjInh?AuLfyx+X@9qd*Qct+*`u{)PJ -+#I%JN+x%jb1s^* -Wr4OUr8p-Wot5c`Ofw{5DjAo$eUn30@WWtGrIxd7^r5vPMDO{h%S@szXbNMYB2;e^}5*xikdymFHs9g -Btip^?2(I7#O~F7Hd7mg5ROJ0gsl+*VivhSVy{1(K&&B&~dzXURW=aDhNxYYVbs0D#pMg(P;tB=(K7R -z>O_hIJ9h9zM2(U_v61A%0SMLF}yLELB9KhVa9p5jjz$WeM6~i;Oc22{l0(M$0b*$r^>6?cSmUQrGEp -Mpgq#Cxpc60VBtlp|!nOfM|5#TBWMw=SH&&9$R9Kwl?I2NCqJJ)D#|tDz@inbPNO+WDKa(J#C#(QBAF -tSh2~TodbxAd@;G5p+)}y?QoO`+=TSA6darO5O39MJYn_C@_aOH-n+Bz)NAHZLe^m-Vp(><=CW@+iw& -NQ+aILO@q9YP#hZ#w)ANVgTDyJZrUV_!xZEh*9dg}KKa}eL=Iz7Z#4JUJTsk4jSlWs+UPr9P`IGIBy# -dud?{NX&>L3-y0_!TG2;C%i(4Jy06q+?`{)MT4>sS?MmsKE*+rYIctVc)0i;;<#JebZVL2UXag+K@(D -s^H7U;&}CZl9l+m5qs)+R)@}<9BkE;&0Kvf&_n&*_qF%$8U?h1k|pTIvucs*i9RkvZ1<5!DlJ`=m}pe -S0^wo5m>HhrO#a&?oW=lUMFU=H2WBKXk&P&U!iZ_=rsV#yH{-f}UwK!`oI@=UQ-q`5wmvDS*|Hn$TyWa5Jo?q^Oj*O1t34B4C_w$C -YoGiCE!C&exr44_`n{jJ@pGsZf>OOs7f`nturC{)rOp*4lEENv7t+FNZ~a!!L6meBZswp@Ropi%Lkbw -wyNx<~Ek69DjDdVAA(oDCInF51_*!C|{Jn`wh+>uo*%vvg_})^7haCiH=D!eqd*q?4}^ -`|4b@i#Z47LmOUBXX>;5ZiOGcV>LD`q#d-%IA$o>#e$&1!qeA5;YocSZ9l_L)2}qlh1OjgkFhS+Tob> -D|;QQYycnZn -lo#Oub1BV(}{*Usd%vrL{)2ymnb#=airD9H)J+w_wPx!^!pG*E)A@#I$XN7Hlr8C%j)INXkFWonJBL# -7My+x7>@ZhGhI78QqGO72;HOn=ohZ|%kuJ>H6=N=<5lq%{7)Q&n~K<|_ceH -X`LWQfy(k%c$yEXRXMahZ*ri$v(wzY^0&cq?Ds;D2k3XE`a -;mg1qW0v2-6O&iH9aR9F5APx{@h5Pb=-1%Us@XzK1TihOTX($pyx#;wd3I(X(Q&YkLpI?s@FRfdvI=W_lS$C4e4iF>8a~g}(ikUM3-|yXi#IpBwq=~!vAiIUu)^`}1|ASc -MJ}#l%z<2=bgSVjAhq)p_Yb@s?5=kqms`ALLVaT<#?E9v&9gE!j^)sDR=TcW0M~}*ctP^%xg?pZ$emX -q&v^6pun}NWCc~BIU}krUFo-ABJURRM7%1|-AM$eBb9~K^Felk4t3_U}*jpSJBKo)?Sr<(=H;+L_MLA -9dG0b?%Vg?T+C=myPA%8j7_r}Av--;5OD|WZ@!h71ICsxwrw#0-v_#13pR=7*@%jPV0Og+v_HT@iz4d -LDe+Z}LA`bqpdY4eh!frP+%!seFEu&^=A0)$uF2=T-))hL?`&YDM0*y>fbU!jKi!uk2~-uFTk?=ACs2%Fs6&@adl+ZN;)H>sOPxs;8-72H8s`Zvois0yw`j8~SXQ3^@uKGxUp|uP3&y{R9I4UQD(2 -jM6-C_DVg_%O?3NCF;m`#wnl%7jZhomft62q%u}vnJCZE*JowGN)|8aKk_84|&K+YtY1?7Yzr5h@7%LhhBFB)?4-e*CPn>1Dg26;Er7xGf^kv~sm15U@yjo%0>Ja~V|+#gjRzQ`qD5wAl2?Ph~~ -xFfsez24j{VL(I@wxszlXLvnxz8_65%;}!b!&y)P0^U -$ZetIyoWh2#SD32)!eRz%9Jy*Q0yEUrJOC+wf_*X`sI88~9gC{4r3TP7oCS4{;mc+-8cKWE8VOBzh<* -+TrTr{Rp|v&K3V!agbYk5BlF>>Ft+g=XIrEss$IA%kVRPdRcd~rA>cj0V8v%Y62P?PXEkxmRFw48N7) -A@R{I?ZAv|s@R^9py0uyoO|Fn|(PiE}-w!bUE+zo!jZF0L7A03%h>4x_K03A3QRa+)}HoOA0MJB>JTl -oqzJp~kuQ+3WayY_5GMWzJKRlDbAo#sx`wYAv1N9@plIrDN6+eH^kvpGZJ8C0~*Fz}5(q -zi6Xo{^F?mJpbk*p1x#=h?MvhAV-WwMLD*N&t!Ct?0a4gP=&r=XO(xp$(lldRY7xOY+=R;jBc{;Y+z4 -tkQi_Ux4iO3-?*g9aut(HmUASRd5%UP9H(kl7gwx~)X>6$j`DzIH>a!`Np^Wv10B`3Xmauf%cqK(WHl -&9y6|LF5)USQjIO0hgOb-Gb;s$a^yz5?Np~>SI3s581AWFy-KD8HEF|%l|jx#Cre^XrT3t34yx>`de_ -3vlqoXI9#yrWcPI=#44Cai=kyo%|`PC^QvW4E+pT(Z5}w}(HX_DPsnrhW#^UTrQej^ETtRIkW#Y`jN( -C-wY)dwM-PYa|x;C=y+HX}Egh*^JP{PQg^S^d^Zkx|@&h?>!ya&H>Gt$thT3cr%b(JGMJ5#@0`2N|vov}tMga#TUQeNH4xE)e><)KCQFDIe)?%I{3Ku#2x*w -u+$NfQ`?c(-$64^6#>Lw6rCW&8XF3FhzBj52OasVWxS}_~cpnsd@Uu>G4MEBO@R`YXrZkJ8%mY&+Uxk -|;lMVA}!vgmKq`3p^_vufeT;EbFQj{xVGFe$u_8*{yM9dG@%j(=qn&qLm7tmEl?qULs`W&F%q#vqavw --ssnI<^`V_{NF3<_c+4WHYki1!OUS14smdS(7{)p_lw!o$i$-(+cp$-9?Kn*mgF%kVZp+N||M9v3)b}@;DCW|Xd9P(<1Xqt8+MoPSazRQ-5CGrOSrhI6I&ZOjJzLPKY_9qIo?0057>IJ@xF -U7YoQ~0;gcY@{O{;%+|slw0l_w$rbQ-+_4*G$QKdOS$fwV&zVz2>r!_ZwG+Yr%FzUc^+!(y9+@F^5`T{>D -w6T>qOVDp9>~5-2F3DwV5JkDB7Wi|UWsa<$uW>d1rt$q^8lOS(GCSRuOzdy!{jXxOdB{IE)ca3s&+VV -v7VW$8xB2_U=I>p(Y>|G^O#Ze?e-*RLL;iDPmBNIqIk&%WTczj*{M!uvVl#;7U3%>GCA0b4O8r&LGY| -Rqjg^|^H)*Let}UI|zqKvXw?)01-?*&dEwfLN5yGlx-Fy -Fw>aIHP^>Q4fpCUFisy;;K5>X=GDn9?x<)rME}`5+*r!K^apd*bjAfE0H>rsp8ywOrzJJaG!f3N}5e0 -^1C9ae^cgEB9Dm8?pGAFn;c^4DiDpFy~#jkW=E&=E0fr_RyIY<2fFioz -FSKGb}w!5W!)&Dok2y2C-G4^B39tRQ!T4R;y;%-A2UZw!q4%a%t}s^T; -%Shs%=pyU^qG!HyrHvMP=M_j<=YZsvjgtfm&Y82@kR`-;#enDy(`g@lu3b6F5CJ#a;mIben{ETT -D83o;yUn9Fh&;ZT)Qj4sD~T>|$4IAy0$g((_0qxR07Lao#ZJigb;dkAF5b$Pid=Yv1f -uI8&1@Z8rIGRJ9%ZrNz>_Z@wy*Bi&Yo`^-~}9N+iuttQibAGMgln2h#+XmUI(Uh>$e(G|Api9cpzBiR -Mg}K816$2D2o-oii#24!>9(l8+Dzmi9}vptYoxUB05#T!E*qn>|dHMk#F;ndK< -QBm&X4=P=IJy#ae?H=<5Y#)#7EY< -&QY$)_$^I7DDE)DrIEDwlbqM6I7ZQBOGaLNNDNi3BmPR6VZ^DxN7MkDu`b<+TmG)f>KX`gjVV8|lf5R=JE -f<7Hk$X_qe253;i6a*_8PUL5jpV9r2lr!z-D6UPiPjFq>*C8^Hs-gk8MmE8>={>0I#s`{%jG;jhN-W_&{yMkpLwsxyA$YZsdA`e1UCsP0aEZ -)af>S{O-5G^6HYM*y@qGv1G0&_wAB=ejmFLBqSQ4t#Dl|qZaSq6*oQ9aEAdfv@gwnu@W0ca5HA -<%lhiAuDe#mJYr03H(JCBkEEfgqzd7OAP6Nh{t>d(Y6&jXrc6N6%~^@L(oy4hmZwNsU -NS}8?$8DmC~&AFDOu$fC>Kp4qYb-V`s6cYUT4@ -k;rLYDXcfO6QuZV=V{gHMo%qs1|zcHM!fr@6oeKQ9qNanVhsynpD)DCe}feP_N92&O^=)*Ip?V3FQe2k7d0z%F59^t8s_^2_-;S3%F -SqAl#8H4S^*$MK;z)m}Kg#@RXGS1(FCpe7U1Pwwr!&I1G}rAD*-4td`+$J0cs6tYXt=s~iUY?Du`kxk -;_D>41!__4uXS>Ff|lL)Og3G7k=`s4%rZa=PXRGc}yyL?wNW^%W@wTLWTtj^s{eq -F!Y1YR+j^hJ`K0K_c34ngWtWoAuyn>zQndW!s!L2;I_~aEGT9!*#=P&(P=d>|NNLURI4Bw3R8@KjflO -^*%+$O@#DR$ZLPyt=gcfHC?^XxN>WXn)zT`dS*>U`61*rOX=-+rXTI8hz5B!A@!22u|McU#53kPlzTf?D1mvu`Qf+q5{z -u1k&}rFUxIOtcWI4|WC(fahXxdx_rZ`ot?oRbbOEe4nQZ4MQcAFB3b5EAPlH25OK=y -2tIb-&l!A%Nz2?{uqv?iY-)g?~rP+RdBmD$=h2c4(tqP~Ko&o&(qMYsF*-EpHhJ49i)M^|E5Tg&$l(= -dYtwhqe&08~N_+nB1!W4oLv0RwmEV9GgCftXtb6lM0oE5;qQE8CUV+?*e0Q(Aw|p;v1uXTd=}ltH+Zu -hb0rD`Q(nYF)hGS}ChVYHe%A5`%dX9%=f15vk$tATj&{w0ohPA6r#)9hMdsGEQzq`F+SI0=+9l&o-7D -*bfYwd?mgrq+yHPFD{*=+Y_r~2;_}|dTLWaOmd^f27$1uUySc;MYM;0h2q=pshT=qWTR2+eqSs}R;v` -2c0P>_*QSUmRPZaxlYmDVKpL0z6**0t-Hn3{@Er*NL#p$3)Jrrd1})`Yz&mlKC)K(Ct+Q29p-MqZKHW -#`piAv*{U2=_&tY+=2_Yyibc7eLtGHW; -5ISjFTlWc}8tFd>y!%!~&)=K64@`cm+>XPm1N5lb;Y8JS_SV-zPrJT)uhYFX2nNdqa-?0+Te{rf{0W= -P{!Yza)y|u^Lalof=zIwaF`gT&UJ(7Y+l|#?0&G -jWMHBy?a+?obN_{w-i$vr~2*S$h00%iSMGhzTNh{h_Ts6TkR46|HGvUq0xv^YOg1={((} -XHLy_uf-CVG|aB#JneZ6gq(vGK_dU~N3?>L4G%`ND5xAl&~K8%jZp8Te-}U$RFLYmxXM1z`lLRbH-%d -mq>VgpMrDarq06$E&zc#WTn#NcZL&3A8~VcDa|W&<7SzugiD;JQfAl>haa{!1tfbPT7B}tPs96 -a3b5DN7rg_t#mE%cOqO1Cj(N^TKQB#uBBh*hZQTMI!cJntQN=HA!@U7JQ7xrH1dH?x<9G@IT)Q(J(aP -BjJ>SU{0bug>7*uqfIEOVzKxD-=~UudHh_fP>IfyVa6CH_68bOyCF~Wcx_z?qFCdQ$27^wc*zypS8x3L# -sWrdzxdJ^6blMp@Z0A4j^qyAn)2ff#dCwVgU~g``cXwKUCu^4|{2oyAiyLoHm<>q#vXMNnf<;OEo>lT -Ew#W13pJx2zg=Qtub<}O-W^M9yz}BaD4XU`zv%j)~2-_tn8@0+tj`!&4+PZ5s5W!*mVV?qr3^_uGB~f -%vcDMfXv?HFhAtQsht@7=|%8|ekL+t7u=u?UktT#f5YHw_t^ftps7o$8=%9`y>2;BMV#R!Ed1+&SY2YZ8;dQtp=`UZ>}=Nz$hlEf(p4^;nz|aKO;3&hzO$U&d(`NW -f%u3=hN3zg#P-TQmH6S?UY5#Dk(`ik3RaaFSj2Xwwu4Zl_TuO{RleLTDsB#J?TrOt_Awh*t{T}2ruVZuQ}wZlj+ -ITpq&1D!Y#7!Yuw-hS~LJYxjqHhWB3yi554%U50VC5&cC`iXbmYW4cb+Fk>x|GWfr_D?r_Hy<&=M$7e -Li=b7)O*1$W^wu%KyHvmo&k0Hebx6NAJNA42i~)*+io9HrF=zsQjT{G!gO69{#4OdU1MbE+z|LJK0s< -kJ7900u&K+1PYsD00CjpIP2J+itt*-jaf-yg%jig>n^G$3faf0HIS&g)W0i55AxPzL^s_bP^^T6T_$| -0C_gzxI?g6R4|cO=6)QFx(Sxhg1tsDF_}nzs`RIMN-plY5tM{f?rSN;1MO9FluoPY=-&>z6`-@FQm4P -TGv*H97O0QKBu!8>1#XZKUT&{ay4TBtrE|7w2LI@IFLbBQG@;yC{bpSHSlpKF$3l5Ut1IQeD`3WUgA` -q?fn$sG)}{_jiY_vzLny`#+-jb)qYPi_fMuF0`$1x74rQDQkMuJjtxFfzD&K+ap&sKC>?yK#c^qtEnErRnaCg?c1l>IT -p-VQRqUS)+*tUr%Pkd0oi-i=Q%kW$TS{=kTZ$N0>;TQOw1|F}LG0RI*DQjx4E*!m-~bb4;4dh -yk>Sf)ST5V;PIZJ$Bf)#Ld6b4PUs>!gFN@r& -b;3-Kj^3Hn9tb~-6MQ0s0HYq1Zpngx`)e4tL`g99mDNeS?ZnwWYAdkY=^A!>EG5=Wp*F%ndYUmJo^wK -{wbWpQ7jCzfZb$`oRQ?ZVH>sLW1Vu9o{Ba)hCzWC*SoM$guE{5 -;0Q9yHo+1F-9*A`5>BK1;$M2>`_4w^7&7y%VrUVY#+>hH}`@McaRY<7BszIOxJ? -Lq$;ro?&L0AQoj+9$$SY#wIZS;?@os!`{G1|6rT4LVspDzagbP9SGQ-Qz>rBi7h9Ik6|kcYGQvUlaVO1YAw)tC~3 -lI1$kPb5N-XW?cJrrl~5HlHPI&iwx|#C2YFgss`E2g`Oo+`i;v2xv|h*M8beL`e%n>U=~)3(zR&+9ot -~H4=b%&K6CCsb4%~&%)4b&J?vm)$yrUJ`8CHG$`?!Nz#M*QUMWTs_`~T(|`Unz=D}MCcF5nHt#;xzxx -E+so^GP|lIAr(_@W_Iu9e*pm3{(Mhic&<@W0;%PB*(5J*84DhxoCtrx|)B_k{ha=6aj-20F5leOc1j*sRZD1u%Gd||~UmO2q -KaoGlpxH*YUTg1RA8SOHt)#Nk^vI7cV?__6-@vHbWk?_kFET}+K$C)XHLQeDSlp&}M>Aa>*d6WqkyA2 -W$9DE=V%%N%}254QVzMvQi5p?-sGiVWwop5YGsZzt-!IC{VL(()SVe5^b87*h{8k8^r__}YD1($l>U? -$Zl;`r0Xb-;_s~m)}-HKu`_n$vZ6~N%O`L4r!H0B2*B@ee2!H3FXY_bQ)KJsH~OF!oC>fHR4<>{u{sb -^Pq&|cl1=lRC1uF>jZeS+Y_s`<_5Yfn%rs^jSa;AN|MeO3lWRlyRBJyl)52(t02CD11`iB{B&vX> -SQ*ELtQ9L-NYC8Q%D}QkUGB5Rg=RVOkein>6pJ9UrLdHL%}!optKkYp~s2e~?wOv2a{}7@j5oD+j4>WORg&7K`q@@31E1bKqrkWqyP+vrUJ -7*zF!{~13QyP^t%nx~Y-q;*9~7CfTUd0AreqnVVXz;rcAw -E}+yE33#zm|0&_~k@V@mgBM -ARnD1!|aV`;n5h}mlMQtrq<4W9-A0mEL^+)XbvIuU#?dD7iBMK-CLQtP|UrRMM=8)YDWl@!%J8;BT8( -GQKxe&7?pQci%|AH>sX2U2-EU$Hqnmm`-AAXh|(!2k&<|4@AJp=dy}aZpfzth8tW&X>+O9u=+F_3`}Y -I;#k~q$Sn0@!&Cx5Ml!utP&Uq?#1}aRPhF6bT^b8yrU7(Z5@l?^~Eg>A -&XZ9EU2nh#!lg`*B$1I=VLqPdufHp~ES++TV+9{0Q6x$ITSQEA1_25>-NXqFl{({kaXi -^%%l}Q|cgQS>bM-OmCe_LROWG-e21o(3T)LVE`AMs4%benZ5GgQJwSYXJvF4wAkacyL5s(-NJiI5ANY -gy<53w>t!up>?zf@H~pP_oS#97L -Z!0+~sz6ETFwfcAeg$EQe!$S9Up;4yi@2@iIu)jd9zvZn5zM4TsU!K2PA4F|;?6=chi`=!Lk9OBqp1! -V)@E;e67ZOqIsoR+tM~NKq8q4qWPt)AM`)@}_P7W8Sje#>U)l<`~%epaoy&s*zS -J_b%*^L%63!~sY8FFSMAFn5siZhCY1Pcd(8wqA4%3;BaD-d#X-{$TK}FC4)4m;~6}$wX`>%`9Pj$i9% ->y3h%%3$?-Toy$FR6Ce)k_Xw8z87~{6G&etkAan`w*HEkV^2j)os3OO6_Y{Abewz?ylcQuj(?dQ&h84|8aoIRyaU+c7NLkcbP@b -t?0g9sPaD6E!iiWzn&RfQbj;q7+Hx@L7s7Ah!V~Zn<|qNkScWl7HUjDHXPsC;L3DM*_(_N74yW3Ilq{ -fxHxq|I|`8%xL79NMG*I>;Yy{47e|67TFW}0xB{Ol>Tc+(foVV4-Z^dh7g%PQIC7Uv$OvOK=y*n|4{F -<4?!CZ+WmT~WGiZI$$iuF)oe!Ogmm(~nb&7?CFeq_X!Vz-Shp&+@0`pyB*-nWTTJFitpvPxu@JunX6l -HD>3Ld)IU^jcSv~^f*i85D!y1)i(+q6U0;FTi6KEnKwH@XHORzflRUM1CadRO&=N%ZuJ+ZaF#$&zRtY~Lxx78})TpMY91B9(1duF)k3F(z5j?L%PPHyFkGF*@_Ek^Z2M1`~EoJ09Ntar$K$uDiI -L+e?z8olw_3I}HL_O0feo&#gPUcr7ggZp?V>@%SE`7gAPd`5x26Uj{ymfX{zgom+EI`^H -`Zg{_I$d>_$k3$FIWn|pxP{C?SemDw}`n0@ua6!q9Qb*FmgqnO_W`Z1>K+2M3aZ==$K;>#9q~k}7>jo -R-%F^JUgVLb2_#SqK^R8T3g3@6QniSk3T;+Fb3RkUnZ+@ZpAXDP)hva#?we3CDG2@->N6qWdcAQzd#D -g0)bna{7@bI?b>(z9zw|7%#CxvY$D0Zy2kFumOV4oq4knqmvAqMI#D8^_N*rAau+K1)(sqyPww{LA5w -YyKJYL9k}25p0Bzg!XC&Y#dSQeFnrLV`aC4aF#612Ze{G>3o()m2e~^j|lCNiDV4q2u${u+sbPd8m|f -`=Y8=0J;Yfz;KdG&&NshX(!tHgwep9(n!r`G0}4n8h%|N(mQ9i!N=lY^cc3b -uX-NRjeO<53Rf<>a>uTd=e^WzCdYMB3Q60efz%;C&GNU~$Q2f{*mD5YF{_#B^5dnC_Mg_WhbpMM!?&f -w}dsctQ)?qLEPTeTmw)mg@BXJM?h1O6tuJydkFjFoiAd0w3~}-gIwSMXxtd`ius?{&KVTfY>JcRX9qPboAh4c_S-m|?@Mwlo!Su2*3>8NM`%Bn -U(LVrE;rhIo#|T*59?460El{2d4Z!-EVYPv2Ob>cBk%wXs{%H^q^o~4yuPa)0fy4BL+rZ$IxVKvJ)mVtXNSx;w&D(r*(5E$Ax8oEu|UO -Z3)Iv=Sbdd}=Jv}?aZ*2V@+{o*=JXKz#>G;i&4tPi_{Co4~9`{KHxy1A)(vl*nK&1HDHNGOe#V|1nGY -8q9WVIg@iN^)ZLZ_`h;CDkx$1E*mwZj``avy8zdrjZABft(^NeqzhgH$+0ap-Bo1IPFOo9qZ8DWMP_jXJJBV$yD|bt>ik$)H|@Bf?Bny2uT3hPRAMa-ef0nlCJfgUedS>NVk_`=9r)F8* -G7cv6X|i^KL*Wm9YB}UJ~qC=XB7nbaH(fUaweh(@!VYS{et!IpUz6I~W^N4S-G4{dIgpY3Kmp0B^tqh -wqI5T?c*TfMhJY+^<^^`u%T;kbAL^+uYBuh-IvExlr^XRr>z%?-v^Fqv=;`dx<(@c+dsIMb-pjn#RL~ -mqCg5kz6^@$asz+u3z5vMiTwSGjT;mRKI`+NnuWwE`Nx^g`;=` -=}yszLr=p-`$0fV1&)XqPR}3(qwF6?8{YAw#Tz9U=H5~5s0D&xI|6*#DHI&SnvyZ%W@iJn7nRI7fEqhxZM+*N`-s2b(-$%Lh3awk|SDL0v-0Ut8 -#uM}tf5ribO&X#*Gw!}YqThzPx$|O@4I%$%XqCsWOUO+(H%s-lVtd^aiLoHNF=r+m8!xFUCS#n7Mru -eA8T4l|+gs(54zNVXj^BjRI{Wp#}n?5UjY{jI_q&;cavmZHlk)eQJkqzi?pWXYvvVE;{aX_8vEapd02 -jj}%iuUrVJ3E=hf~cnI0>FmBVjdPVo-JNHTg@v37(JAkh22n{O|8(On4X}Rz02W?oATZThz6V_sxePk -&WYzM3|yM0cI2TR`NC*pxI~Fl1*2O^NMDq|bCNQHlFI!B4d@yz7Ch5_-#k>9TxK7lQSne3mlo7qnJh> -_){^FYu*I-i6h|uL#8&c*L;^%xiSW0dETO2+RJp(L7tZ3Iy1A55V`GESBkjOvIN&p7L-B19Zc*f*XPJ -)YABDsG>pCPO>rr{bv<79P)vD8QpF6XWbr_`WGE2t~KwTE -LLGz`lwnSpD@o?y*^2kS0ps*z@M)!3Lp*wgFuAz-ZXeQV-{VvI=_IcPKZ*c2kXCH1M?$U;%VH*>c5GN -4pQu<>E^)_-Q}A)PQ}rMf~tA!Ir+lL@}VnE}VRPY3^C-AB(P85=yzs=Fn8}6vL?JZr8D~4EGl)jrLwG -fPbC)b^CTb7z|cDo_Va`9cDWb+tPYrJ6yZBBP%vTQW!51)2xv$)Tyq$pB9|>^Y0^PI4%7qXjnc!0YO9 -8ID3Tsf3Y>n>beL?rlqzfeEZbuZ0MgtNjPa^I;Sfz9SOPe7W8M%zEzz6B^{}gR37VjQB1_wxfy -EGoUfe0hMMC_qw8v_S_5ZcFc<>nOnFX$e*H!~P=w!6r`?B5gox$g;X~!TxM42Vwv;Vr2dL1;lnE4lqnw9J6~RGHij|&F(R-y?ATHn31Qq44;v4gnh=uGV#NtsmA~E+$4QdA`vb -e5qzpEN#yNWni8rK9l?L -ywQLb9!N&%PSUcT6c5X&oh!1%ao@-hEVxXE{bNc&+MhagI2o+o-w(^GC?%lSkOQ}n){tJtMU?B8(uK_wXsLQg61FVNXV*4AaSVM3=fC41Z;?Zx@8ijow@aK7I@xm -BON7aGWJ0(vA{q`EeITACgcZ8FTv`1{Kf6IotQxcD5e_ZS-;E?17%!k+EA7zZ}44(oEIw^C>aH3_Q8g -#!>^n(552DhrqI|~odyg=o+*o?YQVhG#>haz)Fq7=`3g|qz#*IAY}B{*9r|v&k4U2Z}t)_e697rbtGc;b+($BX5BDFizsR4Nl -?RZScY$pV}b`Z4_!$m*&WB&^b16y>I;1y<8FD(BIjWp1Z(;bi}uTxm`z$|aEVTMdK3mYtQUU-6<&!i}>RR -%ptU88_kdK5>Jbsa_YIO_h~Mdc84V1XQ5+O8vfC;x_MzL1<##rFKYG7ra?^~7I}ozGvVqm6Q^N3$}NYr-O}ZXxFQ`XHxApf)L -UP(6+nhESM;@C-Ccbh=WFGtF83mwqK$jqE4XPtbabn`YoDC{axqy#FPfgxXo%d2+*6|`KsfXBbC_=2T -~JtneLdQWMlfkLdJW%R%qP(_ofaj>X8;b1Jg~B5J#Mc|k?|8d(_T8N4_;B7fn*q+3A=t1LF -`uqSMn7LvDzP-UN*b-I9s@}mCObRi&Svt83EFF7L4TP*ULuf?SU9fI*1Ts{tcj+szJ$iSe0@u07Z -S{*CDx_}tLtmsW$82SH<+RX?1g$d*N+4iTAj86cU#Mc4>mV_5CF=)RU}^@UL_z=*L~jz=EhdZ^p!;0a -vRe_{p5@%K%81C+P~JfJG7D3!qwN3gncbSBo3U0GtI!xt!$V5Wr2iAsZ@<>>G3)S+K&4i -*?!H438@pPQ1cp8d1y>~dbrt0>@@u6^7{_#1)Xjmk_`p>Rt9iP$s!4&m>8?k3~E0Q1fTyWM*QQagdK8 -iFW}@X9r>uVJTyHUNPf4o4)&ZJ95dm#S-g#vY{bTuENvE^!FkP8iK-q%X0?pP0ar?BNXz&~lf`?U6Az>~o)7a@CtiPr(ZklkbD}-jZXA1%Mm94s;{Nmu< -CCHLLwRR{&?TcQ6|shRzS=9w^dc?M24i34VQ*=^`ja*6P|yZ2#R%w7ga5(Ib)B43%5~^-QNo@xDw7#s -JQ9l*10NNW`7~GC4;js0Zjkrf -Or?A!KfY{Lu;XZ2nPyhcw#$_Or@8}YY-dMc^h6K5C5nkPEAkLbGo6j?~sW%|DxM%(D-W~cDhZ{5KYlJ -q;X7cBA}UkNj&y}*!`xE-z60H6fhShU}@>})nCZSR`enp9zZXG_I;Sb_7>N=BhXu|pNw3J(I5dSZDx+ -Dz(;b_>fHR>`!jY8i_~`a;^|?i~`2<7f)~`mw~IT0A5c1{39sho@9|H7_Ol7~1~H(;7ubG)w@v@RrNt -nx^$tG2Vm1z8wt#kSq=407IXiC1qCM^{fib@?`ij_BL)0U#Rw~coNqw#x(}u(}9W>=Jct@aGHX8GUTS4oO{$)+3lH}frb~ -bAJ)0%FZc;~71Ygf{JUYA>T)>wz$G$~4qd*Z@?_}$n9VgHNvNHks@*B?zXaGxPOv)EBz;p*)4E!kNP1 -AQ3Y)xZ~E`AFmfmPLc`4U8!l-B0cs)rWNov8b?>c-L2G5lBcKjsvkl}eCI)e%)?_lAtW=@b6)Q=4d3A;5A -7RZ~a7t(#E3~mDduNxarHl9S6lj1y?L|5qq_)rCpkO*Oy%%(7eF{w)0pBf$xJ8>tL@L{QI&;jE9o}gH -+fQbRcYDaP5_}^`@bEK3hl@*1IE#0`=YG5H)I=~}sy9x%IOek1mhJ<*N=I*`v*iON$JP%n!w=FJsh+1 -ajF-XPdcZ5AQxTvusBWgFcM3jj_m!?D&z-#!tma2M|BBh`;{4vGiMDG=7wPop$3D)S9v*Nd`uw#bw1hWStfUC$p8VOCDl2S4KEX^9a%HaB ->vs0t#G-O -palkp~@@cf<+%_SVwVec7~dY1U1*2Qo=N>gsC&>PA2psIn{T>Fi>Hl%4kuWvY1h7nG6?fZX@6%?%-o1 -6n<&BBw)d4L|EIIq6Y>Z!>3*bc}-?C5nGvrZ-tJublXk3l_ZVuY?XM8M5wiR)T&oxzo-|7iBS3{UOJ4 -UNvf!VL}7+)1kI4kg;p6yB#aD&I|G|>9y@FtJ4f=zQUc|>89v)_R=1J-A~lvk(Lk-dTAcZNfuQTsIx<{EI(5x)f -NOGYAZUwMEK=+e(fPWmAqCiBv@?v0}Tw{bF9gfTa9L+q-+Odro6Zz+f-{2J^ -`?L+uRZg4K+Flk&}1V-7Af_sz+}S8_qX9ChRbwopFq!SezdGZji$kM7ZDtzTOPpw4LaMNddF4Z$1l^78RIn|3MqGxZxu`-d`*M&E^cNVO^@S3KZdL1vLgL$k&K}NRKR*(5)U?mq_Y6eK!noS!2Z!x{I$T43BQ|d*})8w0 -`7yz!qWY9Sz!rJ+-3B5*VieTtI$O>Z-Jgh>n3yr4dM^W$sixA9C%;dLnct0*XcS!n@%w`J5lbtaSae~ -eUDSN*dLhbrY>yGhd0JR64R%*d2}7XW8QT6>~J}~0aTr4Z*FL?VHO?uB?_Ns7+-5din9gA3qZ;TutM~ -?rzbAa8jdhP4r8@_>E~dX&6D$g5oD--V`PSL`aT_ZH=06T^xzPR6|32c91l;#zQU22!z28SGWJHn7HS -vVz1da@#>oOB@adc0$bUHF*O=mrJSV+v+gdl2uBb7QY|DsK56lS7a?%0(fcMwI`n9!Q_5W6L_i#-k+R -%}KEP+&?H~@d$q0EwK6qmxXd@A{A4)sXZyhutZd3qf!mxg;xn62w&F8!(5F^2!)mJQq6@Ltm;2T=t*P -iSCyVL2gv%>05mf;6$CKuKVA(aL&Tbi44E#A?Z>HH76|hr>rf*!#RV0w;>}TqV-fqo{4Tjo>n9l!m{B -x?93W2sKMPgvlQsx~{5ti4Qn;m%WbY3WZ^rAz(fteB-XV8fE}L*D?g!N)HS#FF+d=a8WIEq+!plhVq*nOX#uqpvdt6Dk!@3ucbO`B!hED(nkZ{Tfz1J0S>M#RGr41UlS%gd?B -#?~>sCM(5UqdiQ7iDD@w;YDJR3txs(p!_!XKZ=eB-#`29YWa|q_inaf3-B_Hqu9F78^QEbLM2VYWFWp>ti=(Z;4E3^T$bJprBEP!xmx9lJbPo9{f9VAda!>XzYwtFE}Czf3W<)cZv^B -y@=0D^ZZ#Lr;A1-wzg}c#ziGekxM4S>eeUXxl>8%mIzo) -#QB0N#86;xPqnKQhoWT5~!xRDgj32x_XS4N|iKigN!HZT~m-Htlp@8}9oy<$408R5WY*dP=nt;Eo+>^ -BS^S%NchswhnimZeO8&(*R6o!vB(`Z5_&fApkx{?k$KQTy*nd$`m7_NWzJ4|S1^PT -oQhcZvwWF5rukJcbZ+-J9{u)7(pu=E2(p?7SnYvE|lS?8m+8nS{J -Wvxwe$?E%W*$ckq3}8s1F@KY2tmy3yngX~_lIYD1`{~HsPr2|=-_DNS}v9=Y~yiJ-JO-7>Q}?UprK7Phei9b-_ --%>@8NcEO3HeLT%)v;2eQtOe@*B!$j^#?zqDJ8F0^S*68$#dGvBjv;K^qYw&qx3`1v(fY>YFaGPxufG -0!Sl2dbe{^k=!HEHW_IC8^yYVE;r@zgL`QrVD^X11?eLp*f-q-j2>^O8+U8yFNJVIfZ-#9PGMQ$csSa -+48wV$XTF<{qpTtoQ; -G^_Sg?)FORykKo!=sG0ngxpl=s|h?gA$`dQvi)BfRCn3M}w8+!9bS#~I6^WspZ$_|a7cUW8vkqb9@|l -qv+)XQWt&p%*zz#b9k3=I5ti?an+I(V|7vW6wEAqY?aW7T`JpLg0Iz)se3lO{Vyw11Gn#ZSpo|WOpHtjZWMPWkd6rJhVg3;c8T*kbj=q|joLFu8z!rr1$HW=HE?dxb%j% -0?8`)hJSSSq2Fi0IR4ISebR6oiN)>j98O{A>^tLVuDJ_i!xCW(9P^&?ijr{F@W3CvkBeX6K|)<;)B1O -3qGiz=(qy@0T(KAw-Oip`bJ#NFiGD)G&;n7eiAu6zNfQBwFo(#3 -|F1sqf9C&o~HEFN-8BSWi73$%(sddPw9ms8(uKMgl=;`fcpYJ8lo46>Po_g9sE10;r0*AZI{f_2Kkt`vG0%nV{W4?e#U_AdWzM@Qc^)@z-D&E0i@9`1D5`0Eq;b&$RX419U3q1x(>0z`#iYMYjHsbvz($$OZ&4&9Gn7_8Xrly&X_~iLbha -5*swaT~jYz}+TM&12n4A^SJTcnsEDp{mW4F_uXBe*}NnE_!N#uv>f|;0zK;lGObaLj~zsN_C;1^h0%S -Ei{d~X`OHps%j$|&)NS~(9e{%Jn{?{xVAL=KsP4Y7fn4(3y1v+R9_|Mr0sM`_|C28+?-;nX4q9fQkp#43c==@s+D>8q!LK|s;tic -70ZiaCf7_8@``8b8WKyhA8W$oMgyx0agn)_Bzm0znA$xi>^$Pep=mZ{EhbpFais^%-c$+&N9n~DzgE( -LF~x-IIQ3wOF7@R$P7)fa_G-1edhgw5M}vVz@De*tnoeHv~Yjg6?m1#MHONzsVc)}Z7hoatILu^6Nok -gHvmwNV!RoI+$B_yzY>xeKf>DFR(xfssZ&lr_D&>OlB_`P@VJiu3`4$Z(0#z3dn=gLz{lfqkeik-ntZ -VZjM1I_y8Vl4qhTBIayEaz2r*!)M!!>ejR8|J=TEG00F%qKYXaV)Q_GZyhWz787!}+sK-t!xgHRHtAb -Hn{LAq#A&&1d#K0EKf?^7Zy{qhfg4mD_ms845+s-TGb0sb-)K?U%2KanMO1>M*o>!}^#@wcncZsAm1Kgo3kP^Juq3*&@6_JAmrr+!0<99qyW&qF;=7J2EY1CvTS>P(P$KIk7ThMYrI}DVoTG0FZ=Z -Go#2H~CR);H_T2r;Ik!V%~j6_zPf_w;hRKY8xAr0!2qlCczv!_PPhkcjLC5ms7!vRi;(TOeCC~?7=xq -Q5r{b;2i@I`8>@O=!SMbImSx{nrFML!I^X77aJ5ehM0t#1eh0)}Iv>VT+zQ9h-;lPxBP>1<+9KpO8Gi -sK%PNp>Y&uNhNRj>q<3Hd4a^t&kFBRemy>LN{qj-<0?pYyi7#u8s}5>M%cx$|Jw0M>o)w$`1K5Bsjr5 -7r^Lotk6rC96*Z?xo_)Y3`@|UncLLt`I8lYjBj8UJ~8y*xb}<50HU;q)R@PC1(DT_fc2NgHPc;>gAI( -@ecbd%@E1tNffR6xEHX=ksWO`0rd4SKgo24c22!AoRICb$+O=$#!bADT#R%9$s3K@CiRl6{%F0el$?v6={E558$dP%q*8DzncR@5!2vCntzAMg-V{S(pi7IeVH-47M+I6w!<$DXUl9*eav!r2EL|6uxX3hto -Iion3L<=qvf1oye&LMra6s5_Yp!Sf6K-cYzaZxt(7~2Dcyu*?UR0WC*DE#ec7w+inbYwEtigoGAY*2T -PTGxCq8*A-SVJueiPzw{?m9*zlAPr>GsotAt`quVDfM3$KpSZF@+Jj4yIj@CT9br)O-+Lmv*-dycOJ2!xZi|R(cOCs6fBFKh>p8aUZuCi2wI=JID*m?wU71sDmR;o}T5d0gk3`96-H0qCMF!DElagS-DF -%N(ZmRQ##9Mt>@KU?eX?XySRA)TfeLaqe-G}kC)$4@`Q*Mk1{o#A@7{pvwjXiis3!y^n|7jPpm -dif))%v>=obol8dVWSd8@5Qv9<_oAXjb2)ie~PmegNF?qyrBkfG;SEY5newyhs@ -f+f2*H%1BU*Tx#RDjf_Kx5sZjUuB5jOOhY-gS1tFv}KTYi{lw-BAKtF7aOI!`c{Y#ZxLN+#$=>jv7!0 -VRIx?vQuchUUb#Hzp99G(NB=jGi~f!(@sZxg#oW1`UP2SnHS{j819)u4?GhSKME@~X3Nnua!Fpfd?f51fytEUOt;ZmDb-3DA9;FUIs5huIf%awP+<+}D -Dt1%a;_l9SB6ckD4D+?8AyyGq7a9{T;D@4erI -;RBjfmo>wyXhWe^2r4w+q -i~d?4za_LR0~3}J_wKDk%a>QQE5*E0UuJ|`p05MLw(?Hl-p%KM#zzS$ceqbD@d(dIk+(C%SbBE1@ji+ -+VTU&pY46S^LqdA_+Q^WKioUs{ch*gK`bd^RSCcRbA4*AJh4*)Xtk=K$Uq6gbo;eFeLjE9Eb3?Qm#uk -lt404^Qtf23w%kX&Rqwui{_kv<8XEq;Bgic^wF*nMAv#t^6edfTfsoM`9WVmS6^yJH#c#3SMbBcL*jn -A)isz-X29uIxFzbO0?5AZA_usIQ(=rw5b;GwM$%4>(v6m*dW`)Ck4aIr$3PLh#$19|o}@0zr{4W|-@Kd -Si{u>hyg1*P%_BvsfIHTcd#H-^WEj2}SJJviW92Q5sE4-HwprHs)YK$U?-F0*pSKBpDEguce64bplY6vQ!MO+mKdrzME*-!HaX~yGU+m>gj{c -UR0kF_aFLOd{2O^#g+fg+sT3jRLvqof!`|%*pF&=(QM)sCD5z7T#&S9&<(Y{TR2^jZ!rv`d)$Boh@cd -<(&WP%Dh1ti!c9+WHH8=U}*{{u1cXOz>iTQ)=Q0O)!xM5=IOL@&fdft3lt)5P?7}QVZbvF9*SKEvkY4(5@C -Uw{`XI@(04C_2rjQU~V^@QKGzj9$y}egKscuOHj60$V3n_$XjP2~EFFc)0`me-H~Az1miur{+2>rl;L=o-9L{x~1_iV0d -(f(#O6Yj6iVYf)lf_*MD?=@~Cb8fBGf-?`!yV2)_>L*Wr;l6;NTZu4l3iZ3aW=`4zPO3R>SwvgHq)zh -Qpebr~pL`o^lHwoPUIZb0FId2$9ZA;)j$lkr|QAI+EFCs5oU;?$#$MkEi<%%x>c@?;uhgJO#2o*30S|&^P3DciMNeLCrTF1C)S7Qxa=ih6><+(s8nnXZH`-Ru!UEPaBrt{?Dtq0H -8Ldd^ovElH?i5s@jwCkS2n(ZGzzOTh3Rcby@v%RH$7nl*UlYnDjpmI4O0J-v_bi=DVSKWY1r$NKn8Bz -sD5{B1SuGnzK6;n>TE|oGxE+6*Z^s^)QxS!Fl&9x`yujt9v{eHiB+5e^4^vKp~R -SEa2S&Y)CB|Pp;QKwje+JlZaA<;id!yLn}Ns&(RJ#FXAaZK|1`bdqqaXVXp%N<03VbI4KVz;s)$d5Hb -gmK7531+cWOBEFVG@0N-o~6T{JU8bS9*iA5V-le|#Kna(Vd_YQAF@Cj42cg&zeWH3=nRZkA7M^ZE|Z1e(+3kBX4}k@{lH-vi6wAuaOwGZ} -^Ie_*g2^(xT}p~gYjQLl1RT3f|WtqV9*p!16GHC`G-AyFbVd@j*#xKAyne>SQXI%2vKRfmL!DV-sCX# -{c?(t{uO1<}Y`{gvSoQP5axp!LP0S|x!tu!IS&3BGXqXJr#_&ec(PQdG^@n^@6aWAK2mly|Wk_Hi2aRR{000F8000;O003}la4%nWWo~3 -|axZdaEp}yYWN&gVaCvo7!EVAZ488X&Ja$Mr(Ms%y#Aob;P!zx|9wDugfI!;E&q>o#X)K4-ao%&DpG% -j%a{)SE&=C2@6!z4QVQaemgg9LTnW8wML)b92?sYiTdhg>`j^`set1l>(YRI5cWmyzQY~bp3i?Uo7z^ -iv;gIs*Y+!`EKH?+sj)%^pu2ZP%F42j*MOn6pTh&CI_jnFPgzyLDmOtj9^L?c+d38#Y{Ff+=JMalUt)3Y*`#}9XBaHYHZtzY}1KZxBqlnuu)3ISSi? -SKqwXUC?y6eUmLVCPENb_R>Io`KA%@6aW -AK2mly|Wk?}=yb8|=008tR000*N003}la4%nWWo~3|axZdabaHuVZf7oVdA(ZyZ{xNV|J{ED;ruW;=T -n?qiw@abG`maDU~MuaX>Y~tAkY#O^G=pDl8R$={onT-!H?QHpugERkkspdIVPbN01pmm7JZ8CM38``zBx=pbMM-1$Q_sxFFHA^Yz-B}9y0Ym8#0D3DWFP{LDc6B4Rwq1Q^4=O-u -I?KY%Z*su@}Cz&CWCqG`jy}Z7=d<_EE3x3WsCM6N<-xU|2>}p472?E7*1!87&OA0~ggJ3YH3KV!NxZ? -T4oX8?oTPhgBBwQ-NSCw+?)rbdL+XP@Snv?0p9l5%jlD}QtUER$Qv#GllK?5w-?v=9 -av@xqf$be|2*WPv4V^>tD&=udd(C2?J{ZANEuV)BwcfXi1i6v)wV~h>#XWBw4bUr#uGL@`s8(F!E45G -LeHuq-0{lC3;kXfC<86e8Ux0dT5|mIGF&;LMS4Dyx2@qQEW)b%P@LmFrX#EX~~}&O1YEGSH(B1{Vr(C -Ry6)RnM{t5DBRE|o-1-f&cMdS#;VlDT@{d5-)Ys+ul -3hpMyMo@G~)>vFjw?RFXJi6SRQB%W@HIA)N5jzWtb9X;=|%&Z8rR%VYh;ok;DjOK?-y2q*e+7AVdlYb -Iwe&v}8$?2FHzCTCdn2kh&6it8*|5QK%q=1=c#3bG@cz(al;jjohtjhKgm94VCkd+Ztifu|2<<(}z#6j|eWpp=2g$$#VKe1# -lg{eo;hUb=`?I?ZPyBLa%V3=)So(Pg&C>z9?Z6EdmLWzc1tQ}djsWQc}ObZ*-p%@oW=WSHvk=Z_>vK* -v@y#Y9miigqPfqhRQ)9uP#umnJOt||_aV}!zHrveIB2~y^gxGc;)v^+9G9puzLbWYnxb -bx33;N$DhTjbkv9U&QV^%EZCsy)^=G+wS=*YzlTe7bCO{ALXgVY|sLb&tsX&AS^y6KE3F(4y;h#AFg# -G3d~EA*4m^Omu=ZB}RfYA3nC@`_mo|7AT^-opx9u?uB`xcd}3q(-ak`k*pomoBMwhUITc*anT;!1_dn -8l6N31t6>_yjIYkD!&=`@?!eB~Zi4-Ae6N|;_j1I!MF28NP1G@O -Zcyxm-MXw40x(=Xy%FCAm-ucre6#>?Or3FC4b5o -x^m4aD+PtS^$sUOy(zGqOtu*s>$V61lJvc*ull1ZNhOrN_?{hH!Ay4R$&;yHVS -n*ODFw-~*Ve$VT+;250FKzA$~ZeMj+l!8IMtYmM0tOGF8mnTpQg;XuYMp`G(>2!LB#av_SYlQ}}iE;A -na%>k4o9hPr)CbKGfi%j#KHsaWR^orADc(U5eQkwLdZ|}w&A_MYKF%J6&C#LNGcQavdpZ5y6?iYEK|v -8hvkW#=C_$`ED4!OKP9$sldqJxkc?>wAo1%g}3GZ;x{41umj6o5j@!Ck~6S05H>Lj)|&X*V2ppVJi?+ -Ng#tk(#5bTN~9$LG08mfN%F4VMi=8zTjmIZ13N-Z4HTKta_=w0P9vdEzg0a_oA{^%5_m?iMz5nblOXC8gwvxEv;+ -Xa7|^`3-k@37yoBD+Hp0#X!uC8ihVf!*hTKJeHn!_3cFad!R2z@9<>J0_ti%4G@tj6YnBakUPii(ED2 -Ua0ktIwa3IA0f*Hgd-dS0BH@F@=?P|;#UoWCFT=hVi;hlT4(=W+I-BG1^&=+-1`U$AL<=*rgn-4jt6$ -biF!T~0R3X>MW>={z1)sQKw)VuNLsXptN?_=LpYTpn!Q#+qg%+ou>9#%h()39p7!kc}*427r^GuB}?XHRVnkGe5#~*)lDVyGBjki -Pli!4%vg7|exY+IL?K%5PY$m)jpDL9bbgr|d|vQgG5RGp^LR$;8lqyOL&uBa1!+~#e_!ci_+nmT4<*z -QTJN)Z#fc8naM26-UX6K$MW%Uq*5qmX?hF?+-N;4W%oCQ!w1n!Z-x*U~6ER3Afph>b_ueq$uN)XgD$r -`Aybv4cw(6ACh1WW(G;8tq3AuGdP8eRB=&Ls9PuF$-Rm5>6@COb_C2$TQ*b|E@TobkmwDsG4322xPi; -|N}c2P&~8G)B(tvfX+;QLvA)o%v(>VB*y3HKJ -??nlpaj~^#gom8T7>3_=16U^>SS(CZ!3mgGqol3s?wz1o&v}F(s}lWelbDJXH^pZ6bh!72c7#rEI!Vy -Xn-EXc|1GNl5{6O6GT>c3Ra3bJT2co!w5C%w04C&C%-LzpA)U^`M>htPJf?%sVLtgUP)h>@6aWAK2mly|Wk{J=O#x8m(=qj)%1fNIbCan%RcxivJ%63#H0|YiI1~w4tSOQuNZWdE? -tj1C1wewIlI3_Z5drKj7K_DV@xkSAIQ*OiVUPt}uvMJ0#}uBUJLbhcXW81#Sei$I#Sy#SW^4F4W^UxO -x5MFZFnGmI<7Ar#ck7H<-iRH)e)IZ!`0oeyfk%J3n*gwQ##0eTgI553&C^XFL;z$0!PY$G%PqT0-6-S -!n5|OIS-fK2+D-5Hm}N0kwPgw6vv`@gL4>Tiz+AEgasZpHfslw-*`u3s;>;By_5v3uGC%h64UaN6!x~ -pX2yL{oHD|+x!WfRI8lSr%&;xd{R2Q}-0*hLZ)Phhz3-cJt}#^9=) -t`Q-BE4|esDO)mdn|Cn8#joI|~>-luCU{`Y>HoLe!pG_fmc6oaK`D}LiEBgSLmsdCJe0DLr0kSt&RHY -&|n=X*t#dLo936hf!v-8=_A7dc(advZwbU$9r*@RtB<~Os`&*zglyZ$`CzFJIyg)<<0IlKHghkB+L)5 -{wh>V-Tu{S6Xq@o92?j+Ftm$!BQ89JyqtSJ!{cXTN^BVV|zf&!&+1VG7($KAcZwWze+K^U3UD%+4kkl -V7KVbp>>=g$O8lWM4i_F%#>Yz`xU*+0`XB=Je|FW)8_Qv~_+{Ab*)Hreijl&lV__kMpYwY$gg8aIT09 -U|voo9Ta-CZvX_-pBK}DBs-f<&Vkwjk!3?QxIGxGQczjPS>;)ta>rr81~maRPQ)P$4@ag41C_tZBQJ~ -NQ0R;REKMaN24!@!IMr#fEsC;j!o^_VIBpJuIEC6y*sx)=*iTE9^-Dt5&yLf(pND|<0Og>g67P-u=C% -DA>>;%0v*4>dBvT47TTpQ~6`Uz}811VSm_1Z<)Ny-o{l_to0M2YpGKg03*Vnh~`I_qx|BPMIvDTaiY`LTSwrdla0zMNjmIzD<(q^92X_{=kp -Z|yO4u6TQJ?Mj27v?l*5>enBui4&eCla7Km8CTcc5t$)7x)Wb6vGa|N@?G)?2Qit-91^gT$ZlDShxO* -u(gKfc?G){k#%w0s`2-(digEL`7rz<03F!i>?2P!a*3XX4l03iMK>EyD4PZV`fFhV38~G7<2riAw|If -IEuA&w1@RRNZ-F1F7S6f*uk9HZ -URu)+sTbgLWlAx|%wQM)Fw@37Xd?cYcz3)+1G+F869W@msyxhapdkqdl5zOJt&u?*EzxxW-vs`-I2c)e7lJ1SJ{Qax%zcOhU!a?-M2%o~;A?{Us!>V#CVnV@e3V&`zyx -F0##5+S2NjT($uSzy7#)*Ld4Mg$BLQqfYSfof21N&6fJf$ENPQ|hb4M>ir-a~hNBr^RGL9;pn!~hd8A -K)PBfFqdZG<_u7oMhJu&1j3w%*&Nby$hpXysIl;*U&2amVTt{!Cd8&pxp)q_Xe)&)moBvasM=1XE142FPZTj(=DPt$; -wxq%#G4U)*~ZFQg(gY}tX4b_0gn|#0=tcOOQ%rBDdgR!Xi;PHqx_l`+NjlKSHiBN6SW$$#62O!<%ZXn -GELA5Y~e@$!9f~l)&HbawEuzMSswpQ&D -PQf;dN&~`xq0p0YI3Rvb9>Ep*Iri#hM77Y#c;_b_Brh#g)DCJ8VgbreawyAcY)VDV&(Er`K;^AG1{u` -9KoU!`OvKSR`k9$S!f~VFQ(;D@@eT6EC)=>>uP7P#D708VK6RQtKkxO^|SJiw1QKT4;q5tJCB0)S#*` -_fh;9sbVxE|NcLtHgqtJZlleiG5Z@V97Ciej0(~0>3lSO1x$;sR_gp(vif&6w1@D&#)ZWr)@U$<&RT^ -b4ceSTpI1{^2li`w9b?j_y5)o6WwyInZB+mUcXf`DaNRTA)!2p$+1>iGtr}Ljb^qYo7Kbi%1HqXbWmI -epX+%h62Y(*aC5khKmFU54iqvpc5aszTp?yvd*4H>9Txb}QX>F^qWO!yHD_*NKp~_`2gkQGE=<`4X5$ -*(f+@e8Y_@T9RsU!>kw>e==YfgZUlyc$2lw-E7t!kLm#8MeKfp8MnyLaz;dO({_{F_S}S(<~|2XcQC! -F}D;v_?@R?Q)&|1;990d#qrA*D>wjf;7P(rYUaUiiCSi8f`&lu|QVl@vZ%i0hb&t?Yke0#jnYrYUJ7# -g?HM~Yvj7&$OYHUP)Y>FO_g4G%;Nbw{fFa?VuDJBZ;o6PobhkDK*03(OlU@J$RFo#{MmB2J -o6LFq;%894eAUT1i$Q4E2VBi!p;7R)&s(o8yswIVxEFstY*rB(eownH(y5p5Z5aa(v&S>0ZP@EVf;1-Ru -eTyGuc5qh>|>$OAf`GT!EO3rtYItz{!VBYEWO)LJn+67tY2_`o*LG@TY?WXrK4!BTy{L|7C7oKK7$8+ -wse%Wfy+xMt6LN44(=h`Orm*8xvRyNjbQ}U^!H4ns;Q@sep(Rem8YL_u9x7=LTJvVjc1)=dV1@2t5g4 -kxl)I`S>7*QyWSh7@q}C&jOD$&V2UbFyyqY*vTq_pM#^TQT6klsFFX8T&DIwfBJ@s@ULLUs89g --(@ny`t#B^e-(xmvkY>`Pt#(){e3&h5gZ1KQ72G9w;6s+~l+xES-HwoKQ?5jwY#n@VgY%ag=Pr_?8=m -nOer(KY19$v3X^f&_-gNWeH4j@qNJmbW_P7CU7)D0*mdkcn1X`g@KPXWYK}T2^~Bm%M ->nVPJ_9N72pwBmGDg^hX@kT4nlZ}S0(hJl0$?DsErW9e!982Uhwomc3U%txm!&2l1<5`vn7!xYT>zs==3m*9yz5P-A8>@7?{1%zptujcK;$JUB!`7#*f95A`o}XK^k?#g -@Co-GFIAiA=4k?iZ&Tg{mrUtcXnUZ?{3-c)b~6M!H&u48c6G9GwmnRcCuZAP#AI!?<2`^t=~&hLuisR --A%HOSZ`Smmub9=v(j0qB^6_4ZX2zqO_(O0;kL*OH9^QkpX;LVMQxh<{o;IneOgX*icmS#-Qs;U-)E0 -OJ4E%#yM>8r -Y5P#)4&HzVZ%ZToIyu?=LKJNpu2C{+M=aEn?-1H5h`k9!P0nkSmLtyIq&4CMLBNp&l!p>@O2pq(!&)r -<{C#_1*fRlQ{AV0Q2cnwr&&3re?gMf2=p{Ah(ZO{Wdq0I$-yh9M!dyHbH;)diR3RQ(Z3+u(+{}JHDoGa> -De3k=+^o>$=VIoSiE;0?3NUIof;MV?e6~^{frw{e>>Z2_5bow6>p|$Iyq}Mq;?1<+Vdsdtt^;RiMm)=%Iyi0G3wNM+0oq -C&cFi#@;W}2($RdrWo?ytRo7biSwS3q4w=NTWk|6%AL0p6j*J4oP>f&UT$c+iWTdko;M>c0aYppG?-L --6l-mZb5M+KPWJ_FNSo(()@3+F5|t8@epW4iad8&iKj&Z=dYPCpYrL13Lm~&ACtPyp)bS4Y^KZj|*{N -+W`A%`w~WIJN<OUIy(8yh)y|U)soTLWsKSn^4m2I}7m -G@>?|U+fbqwemkaKiobXX0o=2F5&p~gKXtt|e<6*}5nWV#sTj#_zRXeqRLUX69}b#<0Llxw8|BF%*x< -d^XWwUZqZ@hK7g4~ubL%X(cqMWde!KtL&13)_gp`=a2`87*_xnt`LY_k?#&it_V -8y#5=Y-~^HUS7d$cllf0q`-sgKrM)JfIRp^t#;g|Xmk@IVeFfbST;1h?ChEz!HiLWivO9Sjovz|kO)c -MOLP2A@A3wDR=~h^n-Aj|c_d91@z)?lDmwLVdeoVH|8#M@Ct&|KOM-5>DUcb)UV?(>fHchlM?LuB337 -+LieIn%BeWvz5>6%*liQ_;o$o$^2A!td%{X%#nnqXcMdErov<(-0v&0v4@E*RtCSOA -BecKv=i1YHjER?iyF}-bnP`&Pt{?(p%dM5_))I7E)W;yFgTsh$PMlS3FVV51XFM(>;B9WfgMAeP_uSb -M&G-gNCZhTP9kdKlndM@c7-9A&r@ry -E{vCMs6bQ#A<8$Es!a$s^w`z{D8k3C2AJu#78HrDCR8#y+F+(}$}aT5^&jxUgoC%PG58L>Yx|T9RQF# -(?-~6shfNj33bLj#2r?VVSfc^{0MR;@$%E&Hp}VBFWc6cD{Tc+{cx_hfMq3Lz9k+p1iPH$ --5%hy>W3OPe`pnQ|Y^35MpPxUKOd9;l4$ai$)5KCn=8BhhXvpt&H9Eq#m13OWHZZ0T^b75&=6}+xZVy -M6-UMQNiVUa|il^rOZH$znqZiTJXy1`)!f34k7Z)yok|CK=rMlTRTMD<`dO3 -9DmNW$E|%5mrx!gHBB@;?uPCfH7l%)&G@eGNzjgtoe43J}0Tou#Rq1W=GhMNkp7=>2g3 -p?BO0vo$iYl(Fi@kgY@vU4dc5TG33yR>zdKtp^PNYB+|;>&DYSHQdq?cB3koHVc48>z;3gg|B`8?f$X -N-T@uoCLn?^^cl-2D9CPn1=dC(po?R>2uP?l{4fyNZhQ%4;y(oZ5u0VoPU;M5I3NwNbZ9`!Ms&l=aub -8N?na~-JL?>NXtHXUYAI0vqzrQArjr_k7sp<#5Q_k-_Fnw&+3o@<{6z*fFMp`3t -(N&u*z!`CHN^Wu(f4o6MO)sAK^YiNZ;cKE{7)G=DoB>*o{Y*CMA7KZ`&wPujePz@K?hAZfI(0R0ni!%pG_`Q8Z-QX2Mkc4U>q(yH_ -_V{g_g>Px>{mZ*#@O|*0P0zIQ1d_>+4(76c;LJE%Viw1s!Pl>T7U2YUpI~axz{P!)Ulx@%E~XFNcp2d -t_>DML`YSEp=6AR&}{YWoa+KP}%2c<6fFr*t+Ey{ZwR@g=E-5azKmnoXYWytV+4saH%yx#hmz?o8AdI -5#p2Tfm%2u3EGgt$f{#k>N1*paAgLKUA4Wep3WM~Nq|3b&oRQ)$vqtX+A~NW0ZjX!a@wr1vP4a3W0oy -jqW8!+c&(AKbcnnxu|^FPSO?+l299N!=qyy>vFT$HY1Y+D^0Oo0;^QTkbV`16jj6h_IJ_$v&+V50p`8n_Mvz@`((Ed -^~#JQXi6YaL3eA^zhk{-r9RTXP0?RS(r*zOUs?u5?L}zl5J)5=NIw0N5$p{*n?tY3=I;PL$Y3jK@dZ^ -T{6NhQ79F;Vf`|{VW27l4r9(o{PBB{Z9^_G%H0g~9E#X)?_Z;v6+*a&Lf5@H!aq*E+ORsdeHnT!>M++%PiTnCVkjuE -ou6W1P*jENUycAY89;AjQI2Z@bPY7J%xq0YV{-;J?KY|WE4+cgWS|L8nN7)pff3}tSiISqzZ8m~8gWI -=l+qFPc#Kk< -L4&Lt1&0Z^3;@*NQz37Tm2`8K5l`k6B*=mlZy-^F(Rw@?FLr`Zyy@r*CM~xa!#*;H(HK>d$t@vzje!r -2rgqKRL7$_$k=!j -sf`+`T%tEroj#6@$S2wD`G%nUG7m5a;G=Fa{={2Vu_Nct6E*XeKK;&_*qlbeX7^5`t1 -uVq`mBq208a+81eFXmmnepWBsHAcS3O1&%rgO6=H8+5B**e}Tn<5;H|Zo$Me856JB_bP;k_G`9?N6uC -c?u16O2;`>#X1|;{HjwZ2vN!TwhP;&SbHe{d$QXd0Esi5#))FpC@WT`h0yhxtT5ux$81XsOJ{9hc!Z~DwYg?lB#bqq?WU^Lc_qpbYy1X6LdHEL*)gA;@S@8tHU*NWn5O -E7QUSd>7UA&kvgz{jLW(tbR+sU!+Ua>mU!i!1wMCCeZabI+BWbrm5uUs1KLhoL78&Z$gNt4gANVup#tHB;CfYLm~sSZ+(B2&ILa -0iPK^5vkUIB~ZzR+{;#h%lCicp9E8)RE&*f#V!ieJz6C*V{tiE*C-9`;{JUtAh*oG^u)Eh%P;1q9s0f -!84l40%Oz^0s$Op864GpsRrC5%*rWWwSXE5Bl}beS6+e|D^ZgUVX`1x(I#yGq>9LoBRR)<)_2Mv-PV0 -oS?G6SXbX)cAOdqJe#%2raLD9CAL6GNRK!~WP65V9E|z>m?6aBo7Qg7BV<@jd0d>%Eyh)VMhH3RXc3L -r^&+kIDuY+wLE1saJ%7h^W`Vt84Oj6MCBnh|r!yrZDlaPh9LnEtQL -a47UwV3hd2WP6!FNSI1MaitvZ(Sm1lGS3fZ(~g;99EDk?t!k6>DS<*?0Z4bUSn`+r>a%gEh6m}KN_3UvJR -w+B_&X1v#zV1VX4(OK#WG!Y>KY=DE)6Pt0K8y8H~ye1DvuBX=wL*t{G;q$v6-Vg>M6H;-75yKSA2+!> -8k3X_s=A7d6_!Dvmux04(sBJ_L6!~%>*M#G3xlz+D>N2~CC&H+N(JF$cmhT9lr41swoXfXNDqfPtfWHy8>IpXjT5B{u5m|E}L~ek*WiIbMma)9mcL{l=RGbyGSm~#P=EW71SNH5gWp>;2*OxiL7U_(ODAb4Bd7%!1oQPsgC52!4c~G~8=yy0k*O`dfM;4oWdZ3@x254ng~_0RQEdUXhpl^2#qD4Dp@wpJ)FyE -{V>UbNYfzoV%cU)X8l8^cdki9sEIvm6h$Lfv4TCI7O^yuTxUmP73T;wk-zqLFj|Tq_P)h>@6aWAK2ml -y|Wk`1hg2n;?001-s001HY003}la4%nWWo~3|axY(PVRCC_a%^d0FJE72ZfSI1UoLQYjZiyo!!Qipa| -!|->;QodP#|N6E}gmyicPmdB~hSgCqdD>SGEbg_;7SGF17phYc4i6 -;nHQHzr^mqC?f~>{;3PaNV}TeKONBvT>S_+4i`!o4vW7pg|wy3b#`jMtJ8D8~DVLZ4%q -TnL;BuR1)JH;6>PE(U{a7RdF0h3Ykj6Hn#hUswS1zJ#|{cvCJN0S7c3JuV3;X;Qv7j>B9lDv0qS20|X -QR000O87>8v@E)(3O#svTXiw^(*ApigXaA|NaUv_0~WN&gWUu|J>Yh`k5X<{#JVRCC_a&ssU13evtqA4;+3F72vZQE=wm_B%j6^wHWKtt(JI2!gzB`hVB}YZSOh8~tyf@xG_waH6& -z?MkLX|=;j$vi1XRqi>G?`3JrRBQO+``-WTX^x^OW<-Xw316MEi9S7d_4`8a0an#YEHRlcF)78K(NA~(wu{_8Y?`7THJAnkFxLMDJTu`M#vXOF_k-t@|g};RN -~P7Zvj(m8v9m|~zTl08QztXmT4`@WRJ#JtVDdLxvx(oK=Ky|l*5D|63AfD9d0?r^Rn4bxv$8z?WXygB{!#~d1lB62b86k%M-G_PG?K8e<`fZGB^FMVShwk1wj;E8 -y)$gmPWWJgb@ND?I}Ix0d{%qTWW2^(Q=`w>1qQor*<0*0L*1ETpk^?yGh0VSvrR)cKqwKhsU?)FiI(B -XZBuIWb=Mqxwh<8bQR;P+ND6g^kSGML$0&TGJrTn;kzIf~E|fd^?D`N7!4!6&=pTa%<|*En$&lLQLx; -OHc;@KdHa#$50OrH~x*IK4*vb=yc0)C41}(zXCH&^wR*LN)F}2*UpMdG!(&wYp(7)?T>wVtShqZT0$w -c6{(@6csfy22L*Df4mizyK`YXXXmQq?=_Q#(#v)5PQm2}Z(;L1F}$wuO2M4E)b@ImcCv1RE`n}-0F*^ -eR0B(H3|n9yI>nQ-vx|@O*C_1*_yu{5Y3v1h`B|y@qVek!i~WoZ61L6>bZ&K~Sm{@nR(n=I0MZ22aP2 -B@2A-Y(N_{*;G?{*WUF)<&LWh@0tG<&l%%i91|Ys5)0wg%qE{Q;7eH({y(kc-W7?I(wAey~O7o+P+wVH^@Oq -7C`h-^EHwOHYID-fE5M8e2{y|H#ylssJ94CChj7c(B$UMvu5>*XQK2TGqRV4bz$#E&qjoh*Q)Rw*E;F -W+aF8CbMlhExxa4@@=03F~nfA%2Rrdjf>7@wc?CiZ>QI_%o?-@Eu`~*>VdD@xcW~*3x_%~Hh*#dKpT@%+_dDPAhB$f}B=`1WqJH;E8I43uf2yM6_`ST6N -iUokU5hva<9)3KB7t7_cwyq6u>p;8Ix?KKi3zpa4Y30l?7=kszhZ5-Vbzq#G^U^Y{d3S9+=}BkMFx)_ -A*qwVLGtsydcU$?w9prtjo<#!7vwX4ilhx99jgPiK(d$hY2S+1>XMq}d8rEbFOL7_wuiXu_E3J%r$cn -QM(O?sN=AJ5%cKXL)0RFNMEW04O9KQH0000802qg5NKODQP_6&~04@Ol03-ka0B~t=FJE?LZe(wAFJo -b2Xk}w>Zgg^QY%gD5X>MtBUtcb8d2NqD4uUWcMfW|$q#JgPZrqt@hzk=v1DVtT8lclmi^SVo1QHi)|9 -gM(M{8Z8ZK-6RgnMv9U`cTIiKIqLMyV%u1TFz#{6OMOsf7oybJwoj$)%RZ1U>r(JPs5?z7UPlTC1wUF -=MGJI74w0XW@Yo*%uknfUaK%WpZ)sg>7e8ni;=IVvZK!V2(#~DwO&SFq2>iB-^V0XI2AW7}CCRUC=77 -fAsrqj@1`XO9KQH0000802qg5NZM3Jr2_&004N0j03ZMW0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gD -9ZDcNRd4*F=Z=5g?z4I$v<$#e^&V3~gX;L+ZbX7H*vt_|7SOwd)O``VKcl^b&P10}y&%Bv=^PZ8j(bk -hm4}(-gRh_J!iIIl!XHdPiL?(cRUkfzX`%0M8vbC$%cWi_JfHPV-$Yh+s22nyx$FU5TS-TtqSFP3`Jx#s1@dX-|#gxY&@u)#u`hACTpw -OY=gJrfml1}1f?S9ZNaYEih`WTm||26M@6aWAK2mly|Wk~UmbX@ra003kV001KZ003}la4%nWWo~3|axY_HV`yb# -Z*FvQZ)`7NWMOc0WpXZXd7W2LYveW%e&1gqE|m3##Uxyx0{hVBa@RsB2i@ZyN-$(z-E80zkb`Vw`~3C>9?o-)B3N?%DGfsM-MY&{f%k{I_vfJC2OP2x%++ymoM9Q{|cDTvIhGYN1 -9c{Ye?fib3(nA5WiDd{9t;78)*hJYla^#=B66z#Hw7`@^<8TJf?GljLC^f?d#wm+y7u|0s&Y>#9?O+n -o#-MaM%5?IXRpP^k58?g~G-tdw&Dl2t%wlf42T0zd(r4tZs+m=Yj@mggVFLjCgQ-Pf*3Qirh9joG^H8 -QywQ-IYU^PHRP~ND{lVPE0ACgxL+wIu;%hP`SFi&1pge7* -;9N3wfS^B&4x?Q3EQx$VtJQ)yVmV;fwNxVKJDahf5!?DkL|rTxUEoY))D=$PNo5=RT92Y2G&2j3ApuH -%pzWD?yd#vgmx%`HO<3y;Bw56T747n4mF;%QayQ#GWL5Lh+FEIFX;ZG_XMYp9#(}zoX9K%Fg@+d-Na& -Y5=XFM}gb<5as?P47&&^8EL3(&_GHuHcnIc=l9_u8)c!ks9bp+x{%{b2w_PHwX)H!I^+d$Ojv`IkXQ7 -$qD~v3y^cdaG*cDyk>tj5+Zw|Cc=zL63^(jMuyczUI`LPa4O?7O4VYT&%o;hh&^$OEn$ -ri2Y}kghza3RBi*i){?3FMJdUHx4Hzx}T$@ypvtRK>V6imjmdJpf7mmSUzp+!h7#Bi=Vx@JV4Sxq`ZSFvdn7&6gixsTBmk!i=M+svU|g}DY+;TexCwd(s8)b|*$f*4v6qYnA6&6xZO+1#DPy)bj!!-gLGf@AI2zX@Bn**@>ySpE-(P-E#)K!B7-koPymKsV& -f12hTA@Fg7ljpp&K?bzDtb$TiSVk-?^1N#*Z~3@Y$-d4KMT-bCDc)4PQ~`P6R}&qkAMfr1!dSi_mjrx -2XF>9mOq^)_lg(tQ6tiyFV>qLz;|y)atLZGBPqr6Zcb+6b4uAkaa&?VY^?i40DUOp#FU|F-K7)DgdSf ->1Fr@(;_{TF0eIa-`aMph*_#QP_2Vns}QhUyJU7DqZ?6JI4ueP!@V&m --XjEN{4`11b$f*W0XF9(Q^=y8v@5~{}VECB!j76SkPApigXaA|NaUv_0 -~WN&gWV_{=xWn*t{baHQOFJob2Xk{*Nd7V+cZi6ro-u)COCL*OgK%@-)St3PIhwjL=kKo4G$Ua&{QQp -00NCPYttp^a?eZS7KOuW?yN-BQ(_YYJAkFPtZd;rt578`VA+;P~51*C0_mFz5mi6DXMfKrPnD+J>n8f -<(xgEoqccQTv{k+Db>d~;h(Y)tI~!QQKr5JFiQBRt87&q*a8e5^J?03TL^BT^pGR%#2%KA7UC!m&d-t)e2MMK_u@@eFA5&J_^k -j<{nz#rhqH)4Nyx11QY-O00;mWhh<245=-#T0001>0RR9Z0001RX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K -%eV{dJ6VRSBVd6iGWN&`U*z0X$!yi}0Acq@1m^dhD7zD#FRjh)@ZBvZD3@3`(#3wm%4VR(7(R;G9(-3e>C=SmP`l!l!E&Np!f-eW=P1;a8W`t-U)9+8?SunFgm<|a^)1GkXS -R2KUg63B9rel0XYrgZ}*Nq7empD(m-DW#u|M4kKdGR1?57MU0hFu6Zutmv~q9YjhbZgg^QY%gPPZgg^QY;0w6E^v9(TI+AzHWL57e+5rQu-eP6?AU>?XTa^HP67l-n<7 -cCAAIhDmbkmB^{Pnu(Zl`kH!~z9Qjd+@76;q{iS;0d!+HK@s2~VtpLc@ol&Z4y`k3)-Th^)C<&srptw -h2$Wz8BX*d07&wfMCWQpso*z|1tqF)S;$+qBr)+gjMGm}jC$cx~@hnv2<{E^{Vwo@P;_(o8b*vf{NA3 -0GpRew*o0Ra!;sTOd)^QMA+?C&GOyB1gmv(97+(H-cKFS~)$_&K;+ydB5bp8 -zuM4=l{F$!8f8nKhb`VDRHvoFGL~OJu)=iy;@NaQ8WAFoc|A3Dr;|v#VB=E!(?5-ADanH7}^Ma)XdsS -5+vGksO%gPP=`1XRG{QaxbXhu}<7fKL8E|!cpYKO{xSi*{Gw`3sU8g4c<-{zuF3)q>@isgjCGN=9nCC<|4WSq2&?uYVD->O}Jjx -2{CJ;jv(X2*w|&N!#)I&L7sT2cw)DAl;cQ(56}cAgSd7^qX|sYwD~h>pYAzxn!MX88Lh -hue3LyQT_PwJ<2hz1$ZQwoa7t9Zo~tH0~|n%TIu~6W`#dc8EA%SN=8o|y^dY7ES^GCdUGF(N~L8H1|N -#-f$?iuW{tuPyaQLMf%hyc??e*ej$!3zGGNbW_nk+W_0G*jgcE8f&L`g-Q -WXuK{lCyu|5=v-?M87V+kva8aQqWJy>hw9lI7Fdg08+hyxMSM1dsTf~7}b3&|Li%C6NLcVq5dBi7MR@6G?rMRWzz6MoFegx^nQo-_oCRlxhXMTU#P!@=Y33|XL7}6v$7;1h -fa#8ztsw+&qSm@Q)pns&6Nz>FFz&q*JQ20@ouIv@SlbyoGjr`MvoZ>wgXl@z9E3){{aUc#fK(znE%7<`?Lxd*4;>)_s9%VF)IY;!IHgLTrCDvClH))aJbbsd67`x>(vmxSvKBvghC_gy3&(HZeJ$@8Ft$Co~g?(W@_xWA)okh={` -3{z>jiVapFn1Hq@ljLaHlYF*t4aW1xR+Ehai2=uSkd{-$BUk^YfNxQjGgS>-&`Q+FXgjz%UrW -b+TH`{RjzASIhL7R#N81-F#=F5)w!f`RX&{){8OWMHU#`13g&a$#mC;^ShH1J3PlcFpj7;iv -II|cxkd+u74E6ow|7stM9`5XFZh#G&~amfNZJun+$*AEp0QIYgq&K0)j-}9W`ulUxkPiY{IFWbjgXqr -b)WeaAnVRpda=ID2^cEBuMO5hSP&n>Exu)AYenNV|S0pFcq=s`@Dm`1QD@ofpk?->)K`f$V)(v3SKl? -*}NJ}G=(nq5MG$835F7QwGG@yN|y>_mLS?sfv*1f3}61*ClN&=%_@j_1hA?xKT&5O;VYt(h(z{s#aH3 -5;&$j#E+#bw)6I>3GnfWCL3h$7+fjNv}yYKFeZ}HnGc0RjeZH$DFDWRm_SXDvC?4ng8&d#!2Tj<9w4hT=ax~?ECKV -VvIGE<3bl#KdV9E2d3IQD;=diAyf}~=imgtgm(g)I+tqxQA|^n6qc~w>wa~EwjhwIqcn&z7gvo$e+P~D=8?-S7##Y#g*`@h0Ssl|mA7!&3N3pGI- -SftI_5VI$!d^JLiGi>EY>`rn+@V$6xpI@QJ%X2Kcn`B%Xa2w1#mvrA_!^EZ7_F)fgWO~fUh$Y>#CGGt*z%T2+K3q6hDG+oj`$ixX}>{*Jqqa*7h{GA$kc6L7;m`mQ7B@Etm&a#_? -s7De(3zLxpoMAmI-bk)_usofYX;Nd%OEYR(_XL}BVRHAd1R77&pKWMVR@sxGe~G-5g{(*)6s;)P0e90 -DT&Xr$Y5CN2%gv|KbWEyK`%gA#f -;D&tw#>IV_rPMW_?tveb_eK_=GwtgQ7WNEhhk;nM}<))AxDKn68k*tttG=+GK7@OWNmz7v3QD$MK5hD -W0{bQA$1$FV9q2#%)DT+;=W3;G20WA$S6SSm(wMwoqayxs+K@c4nW(^1Pe#&9%8kQ{MtykA1Vxqh4Jo -T;Wnlw6BHcE!-(mrDVS1|Ssv9C>`~JrRQ0-b2#tra2PTD58asEZ=czJQ?o75d4v0_*%t|0pNbYr=0tf -_n3Q_2ex22u+?r72_`UMRjoS|YSJz$UV4B#d9=n^p3o@04W+&Bbcc->35Z!oca8`Bflqby`p6x8Muq+&}BFFC7n&BR -MV$0b;`zod%a&#l+AYM>SIJ@U%RCDRezryZ@@JIA{N-PW(4`{L$~v1DJ)HBVaQYuLAXRlK`@jCVqOah -j6^LmMHtmprW7mK8-k&6CODO!fKWYK{W7}sb}-NtNode60zmUCnqwnIFB^BE{z4OhsxRI9v9(oHIor!DoJnYH?akk()BJ(9Hp+;xrO9a_D<=- -S$hD>{of5u*ucWeB5d -~}K}GK2LI5#YORNvORXfcDN>$WI{RLvdB}V%zo|q;L4xJz4AS0buonAl&8g5g?=Eqmv~&!K;&_7gv4T -B9x*aj;!wDr-(txo3UkoU$?le -Z{|j5MoZ&GJq=(geQEc%L4{JxaLRec;L8Q}fQVJ>{fDRO}nD`xH@+Pei_Gq&-w(FLNKqDkJ9Gs8D2Wksqv&oD$8QAK~)5O@s{-Db~C8 -vxVC14A;{X`ac{scF)n?&m6{^!LTGMmbfllm>6~!?nR6Km4G4q}ZoVob5#~!5C2It~g7Sr2tW=aGzGWAmTdJ?`=3LB~W-_U8EEi}rh9qb)A2+BS0A*!;Rz;8Q>{$Gtix*;dP9h -_*3%4;nH^ayWF_7Vt2Rgk(Mtz_lIjMot@AE$=Z9``cqE9;rX#clOsSj%rGfA{}SVOZBS6*ZRI&tCS|cYaa#0fG0)(M!}N9l?O -#z!Rt+vBioKvH^n*Y;Y8YKBvagtLvtk?uO_SUc^XCUHzfF?HM@$+od#m?@BsRX%r0ab3n^IE^HG1cS>- -Pd8n4?Xrcmlg9RQsUCuxKS_m+p52Sh7!iM3B^cqgMEAg`=)hxvE~SoW9iR__Y1P;AQlpqw3Jd#v6V+g -P)a9?IoM4Dy9C00?Z`o*(_OC&TrfV=bTRK`m3OtF=F2~DvBa1w05n{{>J>0|XQR000O87>8v@K>{Q7p#uN_cnAOhCjbBdaA|NaUv_0~WN&gWV_ -{=xWn*t{baHQOFJ@_MWp{F6aByXEE^v9BR>5xLHW0o0D+b9SHjv6ij{$N>w_Tu@roa~1Ls1A6HL|#-N -P(nc`|taPvL#vDOY4K}Si?7O-n<#63BDua$#lIBiGIE$j=#NcHXChKjPyxaTPc0~Y%G5bs_!{$ef5oX -+P!QDuZM?+{Q0FrB%Q_(Qo#?dCPht8V>q2!qgy%|L?ve84-S{q8h4<;swU%-CzYY9p=mfglLEr2Wk;! -1i5f5<6G>Yi8S!bv;xp?ZF;eL(Rs5b>oZ$?ny-Vy;B;SlUBYLsPPv3x`QVOO5Mg(o9KKQdi0?+{lp$r -c8B0MvLv6}1gKif=5gc>5wcidGR;>bWz?T5-$#LzIyyRg~mQa9RZ=K%m){oJ$S0Jo^p5%R-$^iwX5ZJNoLxIht!ls&0SL_{hGWk7mSbE1$nD$8si>0lqs -hf2Hz0k^+?{2M^lpCVcBkAg7tIex+bT|02b3CiRo$}M2tZh^ms;KOKD*s}H<-BW@yO$5X2k(j)In*+* -gFmVeM;~ME5qt^je4mpn~9K{RQnh=$_X%$%#g=IOjRISRHD+UvhXAW9~OXY1!eSF#PYp-L`V-CG5d^q -fX+QXATcbfNnhCyRQUt-aw&OWN-yC1SMO=IBb2{Sw9%HqBW+o$qhHi&S2ntc~I4Z+^QB)2`;CE{RTckJz7U=XSTqJc$TdF={e!d5EOOtj;q=Lo2Lm^3XJ#j7*bERd!##gH~W -DU$!t>sE-%98?ey3IF4YR$WMRd>5;P>Ffe$B%(&33*T=X*yJa6o`MOWRvGq3J2#Tih6W-F@@ruls=L! -tNjss;`v&FWPNM9H9g;SCfcePh2;~2-Z7j1ckBpdF!3ngT=n6cV`Q{!KR^dtd-?tGB@{!-i_eScHrky -{&5{IdB6P)h>@6aWAK2mly|Wk?QG9{j-t001oz001Tc003}la4%nWWo~3|axY_HV`yb#Z*FvQZ)`7UW -p#3Cb98BAb1rasl~-GD+cp$__pjhw7?wR-wqvKojOC$Q+oeF$Zmol2C<1|&D4PvMY9tlME&AVgNRg5y -=hFB=*5%yzoy$kkEEiIv7&F2XEIYJ()-)rXPAWx)NQNm33r(3q?rSP!hIN898KE3YMOwF!{^pd;ld_)6r<0Hrk=vX~65&)68CU}&YxV -5C;AF7uAo!VQFlPURtel2i?1F)nkVF@fmlD4s7l*a)g(EGZEy0QLqMco*RWCpAxP?Y#lYywc(?!lmFc -=KLXw0xu=nt&OIVmJnn#O@xe{M@8Am_a!s!%S)4NXXbv=9h0CU)MHLX2J$G@H#U<*nBF -axwu?rb0?|8Vb3YY<0#aGL2uH%oooTteXlxJ`GR91=?+iBxpwvCnTxNXkoIkh{O%7?_`0 -tqLA>!lJHK9T(0`F*%XaY_v3+uPa3k;?bqNJxvkm~JM~}=F0H7AvkJ=Tp&iv*g!vv;v-2+xpnd;g^?C -^an+VQ7T)uk+c<%5Xx{AlFNC;8_8s&2{ky=#xW>2)9OT|qHBCNn@(S<&G|3Jclf{AW(`F@_RRWnW(`||j -b2m>Fn!)7TLn#_*tD6?!n}iK}PidAq>Aqqdlzr#2Z^5Jd2&pEPZkSi(612s?^v&!3RWoOPU1v?ksAwm -E<*KvUV04tYJARg6JTnACbN6NW{A$p2S;gZQc;!f_#V79bJ<7I)$FbmA3T938^I{Ftu-`5Vr&3kL>(c -H^yU@qWVmO3(J{*Fz{|chCGQ0+y0y(DK417b|IgwN(brDom5Ji+z9Yrqx!{PPrAosSXEX)vo4Y`$i`q -;fEm9>+>(Tr+9TjgWKh0)6}J$l5-1T6xFJxzHPP6ui8fRV6C`Oe0=$7l89JMK9lw2e6{*}V!Ff(=Ql6^Ia~_6ZK}Hau_O%QBohVul9)_5(K{lqCop -Wahy|M%^$Az!@aI!}4)}&~owT9|i>=_y=hQ*(jG)9K7=U;Rwu-Z}owhp?Je{(FN6Xf@2*J -m1)Ic)ezC9%uuhCHGI#U+Pi%Gj43$C6b=0Y_mOx_)o=e`O4e$p+o!knQ5-fHCJ2WQPYko+h>x)V_DGV -%zAVh-;wv-Alyo)8QuZm((>G7=7`x0nYA>{xet#n0ZwR+F>Lff?IJFugw1S(H)f3)z73`$D?YZaYe7# -tHlXnl8jZMWtrW7o3WR$em7%abOWAk!Cc_!y%3^XYWZK;RA&zn>k({pjjzqH_n+W0OZrcR+ -3yHl<~1q4THe@~9ndGW*uE-QYp?I_P8P$7HYbKTt~p1QY-O00;mWhh<3Hji>M82LJ$}7ytkz0001RX> -c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eb7gXAVQgu7WiD`eomg3K+%^z?_pg}BC|2vLYAyt=fqiImv?{!*mc0mNy!ubA!xf4R8${SU6H5``l~Pj3t -StNCAgtJ{j7oSbAlhi&rw*_O++%=k8GHCJJ>K0g8c)g1Sg&WaettQ5YFGj6T|rcxn-^#T;?9jYrl>;U}g<%8H^zAM(6JxCYbjW>Nk+W$BK -SZFO11xhVKu&8mvm@e4#qSbF;!pMA8?uA@Z^W9%Ls4NL;hbE;re61_;;?l=%Enjy0S{cy2c*8F;DLr^ -=Esx -zoC%XzjO3W-iI?%fVajZVN9K!vpI%Y!U5o -KN1Ew*-N27I>Agqxk|?UbrTsvYQFeFZ3z#b{&l<@~~bnF2Wpw+XxQit7$RELi4=}=L?rKMAFp_DcGaf -Ckvtn#1^IWW!<9%VPpnnWT%Afa|cQ+>r|t+#yZiH1<^70_)Erb#!EjBx1WcMIE5Fu(lli#n;H(RZo&C -3DcdqbMeL=5h2?{BlJ|tE*U5h0x(DT8*Pg;kTl+*%`=^t^tK)D -6YsKvX14ZPWygeF+Y8M|O4O?>MU=&&Y0G2%;eVR0UvT`^9o6) -gts(#cHO@T5UqyJc8gnK)V%ofbA`B)#jpZ_`FEglTmCe;oV-XhuNbjSvH$JNDXh;IED9q_aTqp@f9}X -gmVcuIBZDM@%EK&sqsLW8wkH-C_40PoVC`)eqO!gn%8@QVZ7Vn9PEgP5UG3hc~qNQ!|ivNQA2)tPpfXRhNfp5rf?3Q;k5VJ%>%ot{F1bc -)MnpP+2n=q^aOWGHWob09Wy{@ob=UVi)dYJLXs@<1p&2c^9{zdsIfxN)&vb}T>vcG;hG{MY+CVVJj=YA4}#zW7FFKPS|m=m6c^e(?fWQ -0C=5?i7f66ldldit3ijX=`&;-FQ}GmQ7&TF*cfjgwWn0UEN{{SD>-U^VMb5;=LX^@~k_VLa3?f`U8r% -BbOL0+1V=Td8+~yEI>6+iD&7YYbZ`Yf4XuC#)dNsK4nZzrI~nJvpXzvtHtFoU0=mjS%r3<_R4!!o9?_hnc@j`R(Ka}^+3(hys@Jrl -O!|!Go3$!Ag3H2gROQ5?Przs1kM~4;rAA)M#7UAe(bM_YePB@&;<&uvC|Xe# -`+W)}I4_?t9iSaDpI&CSk35z;0U(K#J>dv4^|12FcKPDosm(}}yIddKUAg1nqLbsQg096Gn|@e%80rR -FGSb;3VQJ-Fq<@#-N}-N%Hf -S4f4UKIzs*5A1dHlzG$-|NXQ%-unyAn_pN){{m1;0|XQR000O87>8v@i+jaFyZ`_I@&Nzmoa`m!9c$30C -(aCNPV4rqrXBNpi!Oq0p6Lk;L$iTIsj?O*k;Y;a$AhyMH+y01@h|mVjC0sv*U9~ysIOy%Nxrm$FN&9i -*u-HiFt4RKq6_0#j%2xDyi~1oAipmi+-xO9KQH0000802qg5NS+;5{L26U0C52T04o3h0B~t= -FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`yb_FJE72ZfSI1UoLQYeULG40x=AQ_xy^tU0L`6NS!Jwu@ni -hbgH7nC#>Ejk>jKA@0;W}1Phs*&(BZK?j?>;y6B(S1Rhfqg(puzjZwL749e@PYTffdKhC-nEyB-2J}N -MDag-Fh#l04)Fj0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`yb_FJ@_MWnW`qV`ybA -aCx0qZExE)5dQ98!MPtQXND4VTL%~jup~`@0%;7S*|MT21X?;pj3rVa={O$x<9A0=5@jdbFnzHo-uv_ -1krqv>v}F}HRV`K%;g`xxCZ$#lYek!8Paq4W(^}>0Qq;hL&^4~FaQ(Vu+N=wq_w_!}$6ncMr)kdfsuw -xu1}^ABzpJFxs;)tIB5wPl|2&yY3Mg4-Am3=nR0}fOYGENE*OE6dXJw-bm@Os@pEv9tn3DC5ZMn3}D( -0l=#a6;F(wZ})7+GS%G9iP6QzA>wPFeE@!%nec=`y>&UEW-OnFI&!)5Y!O*&X}t7ngrtT(hmHYgTemG -f|>1aIP4yHSl7`;8_@Joae3g6|8wZ7tN7`?U7S6>5z{eJpNUR~B76qCxb5>ZiGW_C?2c -TacdvLn|9EqD`{i|ie!uf}4yDo#1(%ZA=s>&(w*qQF+Ho1kLFICxI3$G$rVuRc0knEIct@ui8uc{v!O -ZQ!a}KR#$;aiT)=JOW^0Rx10)NBqI(9Z2V;!t6f{v^mOLH#SBT1OGXc@0?j{>KZFxcK65v?!tI9*~I( -G^MePhAsJ7)#HCG0k%fLdil{p(G;GqIK#4y9IPZ#``ijDmgfjZO${=;xbqQDuMU40<rIAV02T!0+uXt@JX*5F#pPs0b_a`Uw-dq2mZYS_73J`rV{FV_er#FhQrPs`by%~fhRv>JM-X!K2GN{H|i%}RZarOtVH{e<)nO<;7qQsSv^*xJLJpi>ULiAcdV^5Lp>HhH<85jqB8 -1TrFT%wp -H5GsN?&st=4WhGITETN69>CZn;NTcq%0NyxE91twS>9bPa3*4Hz5A}~3G=+-^1&Ss)b4m{?9d#xBh3W -7KBSQy|9l2mu*%xjERK&KgsnC!yyY~LwNxW3|F&|)G*%ZX}PwphB& -elFaLG%%eNW(k$v^cZ!b)dmfI?QRKk}bjau}`8H{R$EDp6=!!9Znh0#Su^`zaHr&O=blfUmId&Q1IA{ -q}I-D08K!IJpl_*Spp&Zz2T^`drJrF2xq2%9+SIeVi1{kPyF^gi)5-nINO1Vf92i(4-^n$h%N!g$vKR -4_@GJTAzc&wf7Yj-g=x;iUFxcc*J#r@eOaZm3v?69#XP{&I^a0MG4)*RFX-GS21;7i_ZT@|lkZz>B!h -gP?d#U;6rSM!g5;z=rOg{PeBaoYU --_B!?1xG7O6@MlMVZwdOID?fDR-7hAD`R)Cy@&Af4jEiFP33try!W_!+H&9Ch1QY-O00;mWhh<39h{j -Z=0RRBa0{{Rn0001RX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eV_{=xWpgibWn^h{Ut?ioXk{*Nd394wZ` -&{oz57>ixztWmFYAUJ3iPzgPQ@@_XF;*(TCFSv64N&Sex#%LV_S5vX!5B!K0 -%PB7vcVTcD*&Sj$@Vyg^LtSg23w#&+Np$1@FtaHs)qvjtX8X^C+dQS`+5gW=ntCo-a1Ps4h<07LZ_1^ -icH(U%|huW>&S`71$rP;FOyZp0O=DvO0c(03^_Ig*p?8h*VzrX0yO%RU=8=*`0;hO-B!gcOW6x*pswM -|cD>$J@cC?T$Bok|!7ns6eb-HgW!S%9LtN~jAp=p$dP_=`30=G4*1ezgBFV%3O3mvG6+Lfp^Ax*K<-_Tjy(uBX6{bC -TtE%{tYXd~DR9W72=eH0fs0azu?o9S7o!!<+a6-2o%CA)X=DKZAA-s6$DRzMtBUtcb8c}ZV!r-`3=pQ# -pMrA3fU3;enhB;fFM{q!3^Z!?7>@Z0Ot_P)h>@6aWAK2mly|Wk^01r(cT$007(x000~S003}la4%nWW -o~3|axY_VY;SU5ZDB8AZgXiaaCx;>QFGcj5PtWs&`cf-a~!e3ZKk>50gRi_66gS#T&L}L6nnwC8rdg_ -qx}0@$qvSFzG)un2SEGj`*yAU*5-=%S$+^~lPK~b<5VS-lY1&u3J212+SM6Ji;3Yuq1SeYVHzJiNF#K(A?Sd*ng^J-(wajOorxWAe-XAu8863Ycf0;D}Dnc0v0 -N%m_gF&w;?e-<@2m6$pG7dk>{HSj>N~y$NaLIh#fL8YW7euP$YTYnooT)HuNl4daTwhm2B|zlv*(hM^ -V!&SlNTEoT66|@W8@*3hln@Q^-S!y-cCiw9%rj0Hmx^N3sbaX=gHDV$QpO`VQR|$@nF8n*PUNUG(Tf` -Jlk5e}(JN%nRAP1+2Nq7juKm3OOm);+XM%7P$0A6~#QE*(i>(RIl-bqALWPoAx@Cv<*+zZ+L(?6g%{H -KPT}H2Cxb2I*@?uZt%#lW|-L5xqCuAlrJNWqNMQGrj6n0?@Zd@_6kHqo(Y4#fGsG`1~FP?^r@oU(A#U -2g)kFTMQE9$5DvSj`kxE*r%fBM1D%`oca53{_l&&B&pKCgW7QwjIN(5X3+%1A&R -tI_p_#8~9^QRinFLOKwXHuR49)@?pY&ARu-Wb6DJe-{|-WuTPezFScz)e~1GFa7tca}k}$SMJRw(CJ# -Qzp^_4rZF-#OHY?6vQ1fRC)k%v_8q!#XRD`##F?`j87+b_bYQ&VeqGsf9F&Ho>dVxfLj6Ptd5Lc1r%p -x5Cr*j>&IoUKr8n`%iemYx08EohsSb($>UI(1Jh)gk?l2gE`XODz)_Pacj3TinBJzIYf;%S -f;4K{%Z1KO~N2>1U6Dj}M}@r9F6z8AG-$Q^QNFD5z}}?}IW}z8u=tzfem91QY-O00;mWhh<1o=|^H-3 -IG5}B>(^;0001RX>c!Jc4cm4Z*nhVZ)|UJVQpbAVQzD2bZ>WQZZk42aCxm+ZFAeW5&rI9fu28*N@K$HRdsNaBS8cm!BS_wwJn3xFgDQnY%lW<2H#_QCGr*#|E?Br}n3i*&VCbToj5Fth$n0SzQWwaIh -@IhKwnfDX~wdaQ&kw01dA$X!6Zm-KuQ{5A{E5_MCFXicMvCpYl7njqqY7GEwL&&*1fG)f{dXSr*}cny -0a1QXL!|RG1TTEjW0+wCzYL2^UH~Jv$)qYg*cl!x7jo*bwnr8JYG4Q)R))no6ovVc$h0N -b!Cfj0oimVxEr8GYyzzR7waNpWhcW&shookCBLp9Ra?I$7ZFJ+TN^6=4he+wl(D0dFIXWHpbBBoYOB^59rT -EP5+!gEg4npGWFDKu#w*Cy!QIF74Uld8Snz`6|yoirgFIMJ?{!p`e}(K -#nX%M{xxY_AaGd68?tD0I^TzBFgOQWAymzz9e8^|aUrylo;2zvA*%pgE4#IwYTf -_i}*ra75j-(HerO$#6&GlY=rmy!Vuq~R0h9UY1^|DoQ1ta+)F9ZaCA+FD2(2bw_9b0w-egrZ)S9Ow;R -BtT->rWiUA&*1N74kJ>qaUwQ3$Ws`6Jw84;|9brT_1A|mJU{!o`2Ef3)hK+RN54*w{`EM5_owGjnuah -yWmSlb+}a4pCEhQ}#73t3`E)xQ>gN&r8Dlr+qwwqZcmA8968tDx?>)DO8RHjv3J3ijAY2mwOdJ605>R=+|80(QtC -M4i0aNM~^UQ30PMwg&ErvJDk&!YGSCGx})J$0f|VGBqNh_O>e4iLQC&@GsdQfdJwIPbu$l>snSpodKI -075HsJ+U>tIUpe$uf}AyU4iT5&f1U0Kw)SP+MQ7FCRKqXWsU=5%F;A*VBn40jVW)jV6st3!$AqP7~7_ -r7Eac%(4AQ~9QAX>Q4mivn(;iZEg^}<0D@^I>3W>?t4Y6e!0Qm_c01tXBoh)Iqnr4+p3d$2(WpZojjj -Lh2*q4g0_MepQf6gLt5++i%BDB0>gA5Wst9@l>jc$~$p>x~+9FLr43s#mpGJtMCbwmD*Y<+vC#Y!^;=PpOx-DKOPPm6;%?KE{(-FZ_04yaEdZ%|PoTwE(dsB-P9vQGTgdWg -SzA2Pm~-_Gyi>DAR42`$E0dlOUKQ;ebk+l!mackk~ULus}))n>&u7{qvc`EdZ?`qRzF>D3^BY4T?=aD -z6!xSGyBI0#FI -?1^UBGYDsz?1_hLdLe0_=X+vVVhzICHhUr>(+1(N!Jb%}l`oCMmq9Q*GU{m4lRihkeM_42ZQa!-4`5iEG=`^59>9P$X=`7bx*6Q1O?L3w+GNa!EOu#AA8l -SHU%xhWNwgY>}7SiRcRPrf}7yV}%4#XYflw5f-fyHjTkKYd(F%q(LEyE*=U)Sa_ubPDm~hSAjQAhga)8f -Vjvv>-{j;1Q7|3qmdgXAmF~pNnW)dZpC6Y#V=;WTlc#$bTi&NknTR`9!>Nnah2i`5H_`^=H5}9J;u`; -KFYWH?7-Rg4xFV{`!?A0S-=JQJDZe+)Q8!}^&{=?q%}gZkcr0|{HCy>3cn86J)Lt -DJ6^($6S7c*UY>MdiWU$C<9KwWA;vPNJnZEpW*@e#DUT5;G#M~XM+T|KtnwNUF~V1-i&Y`YT!y1|^E! -O+#y|8N>^fuI#MUK?W7BXYa>Dj10iTn<x1Zh%hcFbJO -RJ(GeB&fh8C;UL*F}k7i{2kV(HW7u@!OZAVePvms2N!G9Nnm;awW -Y=Um1mhY(WofcZ?U7+d3?Cpz4m@R)&HgDZ>Xt>wQsD3ixrvN_`ooKXalHHq-x{ZHK+8+;|$i@0tZPKx -L&{&Tm=MXQ7*RCujW)?cv_UENtqN*h%|fwmW#0GvO<=kI1W62FaVpTRibnTdl=dK_!c2Qm~ -qp5Eg!!e94(k7as<@$WqS!#|}_y^qa$KcFBcI_4msoLZ76_O2v9zz}1p5i7ON9%b#+@)XEM=3YJj?%a -fKPycQjwG>2pgMr9t63;4ef&paYCxB~9Om_pIKg&SNkx2(QLWDAhL1yk??3Y;AAQAC0Zd#7(^cmrt1c -!U0iNBZuM2OG;{Z?0^f=;G&6st`LG%29U~#05?7N^Zxx#(etyKdfu)>PpY#XWHZu6kgxr3gEFv+1nFz -4PLEieC2jIfM2049dQux7VIzPS1-*v?sW0Pn;*K>eFzie5q|#XD_EGO3T8dsDI^&7P|SRq5F8h%PMbc -QlLn`@UWoQr&s6qVo_ZeaR9)n=&d2fTj@iSlL5)?Zz=^ZUvU3Rz*1>-OP)h>@6aWAK2mly|Wk^x8u5? -@i001Wk001HY003}la4%nWWo~3|axY_VY;SU5ZDB8IZfSIBVQgu0WiD`ejZ{sK(=ZUd=U0rxVbdx^u) -QHg0!k4ew3l7M0Z`>_Car~QS7V2ge~;}riJjf08>y;2pKspG_yw%hsUyBW82F+jGY2N&6C)$AgX>qXS -0%i^{Q#?%Yr$HLAovcY#sV}VeEq!M+}v)U;R4HyA5V%5FoS=&8E!iz?cE4|KS*?^v#cR?gpx_kjXHC= -x`v>AXGM4FyAx$urYx|nHnnOERB)ptjv^!)MA@0x>4f#nGLpu|J-%&iFIM)E32lX0jSUcLG+QP_zIc; -?{UR`V1Z7c1mcf@xZgSQd+&CCz%f*a@+% -;8A2Dtl^NooXH$;l^D<9lLaBhw5nbC0O=HQ}&G6y7JwDYu~r2+{c^kG-wN+?4lUeL#Lmk-4N622-z`D>PlHJAZm#7Stg8m0Mn!_gn_ -cjllE(5l9&j1{^Fj|Fp1|#u=t{2+L-*at{@^J+gGm~Mn+;D~EsL`)Sm|2@8o({{GOPX?)G%vEA2MB~e -8KCDbna=F(^d^sbszkE>rxthM>ua4{v9hsG6s%#?v&2W8qZlgK9;+b=N$4Gkx -7Y=~%P1^#X50Xj+oN#Io=YP)h>@6aWAK2mly|Wk?NeqCHFn008wA0012T003}la4%nWWo~3|axY_VY; -SU5ZDB8WX>KzzE^v9xS8Z>jI1v7xUtzV{3+z$Umv7beLz<@7oU%z3vT9eW6(TSc?g)--(=>Ow|9)dkz -$8F7TAj40ipuzz@jS!KczjBRbiHNqOC-o2H1$N}rR2e5B7rTrJioZI$*prouFi&umvJCT>_f`IA{>BB -#>3In8Idqf!0z=JFv$&gmUXUAdc5xwJM)P_8a@p$%w!Q8VeB#xbGC_Q?T?5Z0xe@i -gTb;y9=CSRBdI7K;gqs9Og4SwFhElVi;`DAZG26_cvf2*bd{_%&41{3uG7~Ujj53Rib6oynUf1`V(iC -!u17b*1P7_$bnk5tf-a2*#aM6H(gu`{4x63Vq&?1gtnJ+SivG-j+^3cu4*?77b&3 -n1#a5kIYq3sHW+otrvXdb8>RRb1{iiY1XHy9oZ-?5 -pEbm5^xu{{aOm;W1IvVC;l5wEajJJ9V`zs#wmNGJQgucTY;N0vK%obxu+&9XZYa*y3v8or@>_6&PT&V -gAbp)w|JGUlvazs-^%|22#M=rNL<(8kc3*OLj~JoFRJkLY%UY&PNtkni*F6e3@aOPdI3HRrgxK3b;{cC-cjp0RgGD3 -_|_E;@+O-Wkk&%557r1mJcyn-uihyX8Lj7G$THBU#tFNR3~c{qH4LFS<^4b!fMtBl+bmtcBN};cjC0a -gr2zvrMcrF~oDn^98U$1Gf(JyXKB$Se$^JRk{}mJ>Bc@dWv^Bd1SP@(!3q~p0OWK_WwI#$Jfmw#)0#x -yBkQzpjq*qkW3zj&SKtD=gL5*qfy@CrSX4C<7&3OuAc&il4{d~ntB^xC@o^X2qyu3ml}tTe)#?Q$($UaEb -k?I7?|owdL%ncv{3mAwQC5u~*yCWb7POwwv6kw|w27bscw2)nq^9v^ZOxh8Zd%;2y+Mqkl#&ES)>P -n|h>_k$N<0^j~Ax$jf4%6YevLH`TR1Sd?|ZaQBW1C;0vTD7#F`g*;nYj*!(t&IJp`ZYB|4=zgowx!_=DMvf4FmSCN~c91j_T*c;#p%028|&o^Z&59YK!)(mt@fKv09sL`^W0{`(1WOR4iC|m)CE}*v~e&D6gToNGS_c9a-cTUj~Ce6`#b!J&sBau -RbqUW1AsYe&YPtm0(9e8*`&1{9OssbkfS{%@Fce`?d{7Wsu9 -0FoF003QGV0B~t=FJE?LZe(wAFJo_PZ*pO6VJ~-SZggdGZ7y(m?O5G!8aWVu&tEYTQUfjFAnEn0lc)j+Q!8FAK{9Y -r)qQ}~#$9;aN?dH9(`vhnK?;P2w*y!Si)?LynqANru_j;60h{r*7Hr<(5Foe#|ZnWjhmOEC0HO}{_C> -JM(uk^WiJcm122;YYRqD%W>s_l1OzJ72QyZlEDY7*|jv%8 -FZRt~OT@l&iEajApBS8qe$fi>wuGy5=4a4~Ma`@r4(ORBMDUvXgB=uFML{0+SOY5G6Mz!I|lna8^&7v -Z<@TrgsK1FQdOVyDUjG8JqYjDkgi?3Ng(7x@FmC@$&XfRYdPx19J -d3xRuL)Wrx38%lYy86;}Rmu!w{{LAesS3&7$dOLz_~lFmYp!0W1Q6{?QdyBqG;!GzI>Jh^0uRBn-k8y -v4-*bm3;D<~p${pplR6#w2Z(GD$VWRG}6ay7$i10g)gw6U;e1c;OroJcrIJT&X*U0bgA+jkOIQH%<5w -(2Q*0)xt$@!ZR#xf7_y@ZuZ~2ftQU6t{fw|mU`X_S~g!_p|s)@rsJ|8+G$Fy%bcNVM3?9eINR`jzrs* -lk*UDC=I)lb3+pNs*0B22Hk+W++I<^P)ydZm=>^B5#K4a&+ydri&lbk(p9{Q}>WX!j=h{+ -v77zCHa_N)9{;UAXor^tU~Wo#{8#UYURYvhc2uEiI#B?*R85P7{Qw@I8XijMYJyEgQtTQ?L69&V8|V- -o3$H<%MNm@upmVG%7gkVlgk=;($`j1a%{818n#=$%3beOgOVSFsx3v`Tjmbi4|_e%U&i@F8;8xPMKVg -NYA$)&E1Zd9*zJB&cg&RYHVQblQ2k0?B2xc>MY0S>j(0%z(Fg8j*)I{c;EU2FY@WuTfg(y?G>7Dm(Ze -(mNS-<`v9j%W+irTB08R%(IgXsFiBTt21(exo_um~tT#355OZZP4z^fYC3Vi(6?iPXZ+Ofm@P8Sl{wW -qP&0TbmYEvJ?pR-ij#6L7)wJYCG6T{A&+5#w;jlU$GI8t>Ndny7Kqwvr;G+P@s@+F&)brOld;+UkmYH -beZICFN}yRuk=Hl#D2(HIA25(x8pQ)o12qK0=x5v)=^)t|4MaHm9M9$mP- -C~aOwWGVl(RU@w+pUH~Pn5Y;o6beKr5p2x*>=e7F8C1E1yH^7$vqGSQt09vs5BTOiYEI0SR34O@2nk -_e4@)hiIfs|QWZ;;ADtb37y$H!!_j3O>#E(@$A@Z*ggQ1)f3(daZU+$4gwoBLGLvjb1NLWHYEExaz#@ -H^$wel6If#n(bPUJMk`GPCeYX-AKJZgp4F=@Lsh(NPe>Nb9B0VhJ)74{sY|cLAfowmgwx0mru9^B8XN7JCX75~vsU_lK&vjR+1{vq&i__7e!^zsHN=xc+TH=}W=orWtRXz&%eOY7@ -AAfE>i_0c;gt6%k%=rGu=?9+2OF8v@4%y-oT>$_9MFIc-9{>OVaA|Na -Uv_0~WN&gWWNCABY-wUIUtei%X>?y-E^v8;QNd2bFbuutD=hV}7SU?#fKa7f7)4B)HnalQDr?;?&?c4 -Z!S?sKU59ofhs1t)etu3o2ya5jU14k$!-322p+}UbJXpB)M5fv^_=^B$)4N$H+dX_3t51;XPt!(1TbU -efhXt?$-Xpx8c<(HvuH+w02+!_d3(ZC_36MZbKg(ox2X>NGgxH|vUMod})pC)n( -@6aWAK2mly|Wk|wI*vL;R007jU0012T003}la4%nWWo~3|axY|Qb98KJVlQKFZE#_9E^vA6eOr6mII` -e-eg%#^IiehyiJh6gct^dHcARPNq~n~}>Fzy_M@@^A%{E1Ei=H%PA=cjMZ@JDF(U|tprF`dqvx+#ps>qr;nez7s59IGfr9PF3`c$n -dcs|Gf!@H!c0KaLP&x?yEm*UUj>7fuiVp&Gli|A0~1)vO#9tilOvCjbi{QBfnjjX;?i;BKX>(w%;#6( -<0Rl+}w>2aOBujwH?Yw{F`pR|CvlZ5&r&60C$7*%2ppQ#tMl?|rTt0v9rGzS_$8~LR&n;aGSJiUG$l@ -*e{LfXUkC4Fy)4X0Pp?3Owx`rI^inzb#OGNVrFx%xiFPfgx_{-sEB`cN&iw3d(Qvjr?muN0^NzLp74D -7ga=zcxipGoI2X0FbK>Wp=QP>YF@TBuq-*pK5Uc&Br+X8ieb-D9&zRtmCpQ%CR7HkeH(f1Sd}BJZ?~p -4mD(ujh_#@}CW>VIbSv%b7Z;~?&%%9#bvT`|de*BLnX-F)7#SbmTzB~W^rIc>jvi-##XOV#*r1c7z%`2pg?qSIXVy69O;|_&O`DqO8TpTmUVaX+?P2vfrvz^H+0jWfIPc#S*kvF#LNE -?vDO~|NLPn7Xtn*qIz}%O?9`Be&P9>$k5M+YDv+z@VYFTWw1XM2c0#C=#B+KdGH_s!f(*&X|atqhp$~ -V4nO+foI{h7(;v%dxzu9QRpLIa-O6BRpg4KZ#{19age>7A(YUu~9D>9Aq3K5vH9*lF=VvN%wdOy7fshLG(Tut_E1Mxj}0Cgpz -*$iYFKY?5R=EaftV*lVzuvkGj(G&k7{A2jE)xanhhmRi<6~i>C=V4J^KSnk@F6T4sx+8n;aZh7NV-ht -g$csx;W#>gL;@mbGQa!(DjUjh0sehb@q*8)mXt+^XGOrf}YKbygL@5Ol6Yc{Z`&fS3KfG)KY9c{H)g5 -e6lLP48p=DUwrr+4~wGrPDCshSr$j<(kUw`%FP(*h{8Ux4j6s95yu-!>{2jP&oS^=8#rb7BJ3dcI`K$ -sNe0*sR$SD<~EqZDBvB*ZgOH6;kr3W371jIOe@x=CUO&a%kT*-Au<0>(Kzdx>J4Q7}dERt4swxQECrt -x*cDz!7l(lu1+-IY3Xu?4|(BLz_`i3TCAv!X2$P0Koyem)+56S -*a1V50Itmv*3bfGBxVpdiTlSz3)B^?9yUq&0zt}P`eiT@lZjqH>Nj)bp7jA=n=8;R3$Y&>t2P5FuRG$XomU<-KG*404h;r(N& -U(0$P?VJ=h0@dJmC)6WYkar=ip1H?h%ybq{<=` -uGSWP8L^590LTL0!RQb@Y$Z^sSE3^dN9XkDGXfZw49?X{Re=E3)FwKnEJ^LUUyQ>qGb{&5|2#MSBXo% -QxvWH6dgqr3(fjmvs^mDW08c{p}4=l-~E))!NFK&5rom@@ZBy(BPb7`g{^fM#W4|)RXi$BC0G!Ar`I` -XQekNs-FIHF{>0^_4aDIV2x2D_320>pEr9Q{A*!t5yKoqy2OS`!5&!g`Lo`U?$zNZczBxX6c6QteLZG -n`&LG!DhNZV1jsP`gYwK;({zFox^A)cCTI^!OT@k=+;{@{#F;W14q4&fC!%tT$-5-p@Suwj!>hLyvR_a#$)hh!7+M`OoRnf$;YUs!50CiiE%UsX -{qj-7+f=Cd}1(@VjiVg15_5LIV@)YR)IJm!t*AlweHZo!cox+3QhXC#sewf*bOpwt%DW}Smyyg=-IV6 -jvc;<94r^=$$qnb34l7=j^a$X`HbKFjJ?&=>?dFmwg7>^51A#T}9bmlt$*asYE|L52pbyklx)1IME!U|v -DD_(9z4JW{h#iI`WjKm(O42N06NqMY>{=? -0IL8)Q+MN!*>Yht|!NpENt-r=Nc6I{CO@d1;XC2ajroA`4+6c~l)NTX2g8+JlGKz?uuaZ45TEgGSQgM -^Co@EUv4+9`38Z%~Aj%WT0A&z_H)=t@e&{JJ5^zp>HJ);v%W?p>&SaiUnK!u>rjJa8?*Vh_%*#=)(q<5U{5#z>mcspp_fcS|0T51qJbXl7U{mPsq+Rc|1V -dX-DtYhRGQPK-jlhg0q3v5Hlp;4kZ5AeM9iB`UVBzWgBGu`I)%J{0*$Lnd(4mnbgUm$?6o4;zeDNsgT -i_GSpt6ln0oC7gSHD0lVb8ySMjId0n~t5AZOYPUCb2O~Ymx1E~YqvO_LTAS!xsB=RYQ*}-N;jLA^tp5 -IfM89>|#bTC_hW3T`RWl~h(i|Og##QzcS=_mL!rVQQ$le9Kn|C-(b-6E0}a6Y*InEa4Pync56o%Tefi --r0QVL5>n=1{>Ys?^5hHNiz-=v2^FB>)?vC?XQ5GEChMo<<7`6Ss!H@-ssD8Rr=RdufEu{6g_FB4xK? -@3w-k8W!k|0=JLu(z3|OP7DyQ!H6)Mfw2ZrLHd^|gb+D<3P2rD*vQW$Dh)hwL<3J#pmxL&nmKkLBS{U -Mml`7?&q@X<$Ee9rxc+~OwY)8V9ni$bCEETnM<@Du_nYX+#J^z1_%lcdw&}Qo -_@h@u^xm5Ro~F)8NJ)q)S&l3ev1|YW+8Q+sn&-iNnm;`l_`J;y-9;VUqPWA?1XlDcF_uQ?5?P8L0D=D -K6Lx2!Fb~tb!m#eixb0zJ%@`r6j(<9*MVcV{B1EW=%eW5_1dm~gpY#9^s6QBtJVdic#{v)5v!((vvke -75P2Pj=iZLUx1cpv>EYYaCSsa+ieVSz=!Z;NCSDZ|BLz`%6Icw6I?Jy5FSdygzeUJ=XP_T@lF?vXh(3 -pdR8n;DAz0r1Jp%Nvq=EY4U7g{Gepk`fe!VGbts3atiGUl`v%uxL}BIg0AEx8RJebGXiabySZF0H3s| -IBSK9ufTRI4vM=k|>ucNG17|SQQPB3|&g51%};~$kN+HOB&KA0R}<2sIKhxz4!n%@mT_{ -iBwnf*R>plp-)9?G>lTNSqW;g+UKb5@su$g5Ah -`g)5t-=aenxxSL#}60`6f-h*F>N1^R>t5jx`z2UlrzcZrP;se1QjZ{rLWMyNUS${LpIyu8LsK;qk|!b -BwKFCr`;4O-)sV>_Fmzq>}=!wyg&jwkbA@UMKWGf+b1h>Y1>Ls8%F4)YKN~^uUA7-u5G2JS01>f1p(LCc}G8kI|aK7|i`m75w?JIoBcBiiPj=#O -e7%$BI2f9*)KBFlJhP3K#V6f7+l3OZ&?3<^mK80D1ke60K1d5Vf_DcQ`aZ}Jgucc@4FP0i$4%~i$8h8 -)IJ!bCZo5UatHC7ZjdYLrBNCY1x)PO}5Z(&B1X8M1_raYS@LhA8W^nt5;gPiLeR2*(3Sxh0Sk#s) -KC0AHR%x`JnTKq%MkH9Xb?II`U0#Nr$X=ac2$lY=jwBEjd-pAxVLh9{tUm1?}Yr8r41@h-|zE(EF)WHl*m3pBubg{NIewo-fQ0FCy%S{(Uo$zGLSQ{9%@J -~HWy8HpCJ)SwS#2$ASsIwo(qvI==>y6six{h-Cw-<=NTM$q8*oVO5~46rh -nkz_656F+d^Ettdb&r!p>_&^P4`bxu$jM1IQ$uz30&5yHPkg%Ag7`2o_4w4zE}A33_4(CNIDM7fjI-G -{IT8-DmmDZCH=R(>3YT!|IHqp^n6P0O)1FmwTSaI*C(FFt8qpAZEGbbMeC(3_Q%eS0O*S};W7b}@C_B -?%1605*#!Id;fYa^wO9*UQkC)TW1%GP{43+(dUNm{VZnBAr!M?f|nypv-HhBvTT?WaK6$4x0QnFYen_ -!==g{+oNaT&9bk7r$yvYeE2v}Vxl94wNAR9i1@dS9HsMCiF(Vkh-I8B`8TaJ`znuq6I0}X+cBmk=1>! -HSr%7WvXF4d9L%1tyHX7s -#loN_fe4E=xc^w8c=a_y;&zf?HX)nACu-N#sj=(F9X4J0UT(8J4d9l-!L^|bbWW(7^QpLB>eH`{=>3% -dt-hV}W_k0mz7NPbj|hCAW~mWdSk9adifO8+kYy!Vax_h_;FA0nw|p%cdAp&k`jbMDC(xf@~nb?P+t9 -hyHpz*FUVb&#>7jvad7u#Aa9~H|2|rNM*5)x>wDgG1ZYdW1}zIm9kFuH -IJ{mavjzxphsJqHhdihDPn-$tA+cOtZXeV+85N98hG7`Gx#r=})*k2&Q9#JY7H%=hAZ&DO+h(2^EGfI -FMcx93F=S^vS0PZqi#}9-JypX5c&b!?^VWeZ%W2uN`L`lD)8k;dF+Oi3HsVhD -OSL9q*+5NOfZxc#YV=2!m)DB9zNA^gL8eWeym#R}G}ItcAgu&$U3f$yz8;4t9kP;B5eAwH%JYE%f+z%oz#ZW{oxuot0?rwkj1F*!s;_Rj|IrGu=+m -k@~8wT*WQuN(7)WxcXzk0ICtF8L(M@z -BjOxP`<>hOSLHfi@|fG6T%DG+eDvPm^mQ$!FKo%K6(NJ}06qhJu@Y2kJQ=^y?cXCRlsTyCfTn{SzxeR+vTe5`qU=6e!SEn9NmNk<7b4{nMR3&|Egg4oVdAt#AAh6BQ1y2i2vOeJdXtQ*GHUW+(Kz -pF298E@f@RX+q@?b;iK-h__K#Jq1x!wFmbc@kn+jd9LGQ>eG&TEvf842D6^6#S -;Pgy8p%XZbuGe@>SMB9)5TNqD3+@ooYkUoAo%Km8$Dl5UI(!-S}jbz+zfGD7$H!lF=2G}pun7Bwd+?o~3m5-joSIK>g7@tvNFF_)i -!*lG6ejE}AuS4SXdhH@~>yF_|Huu{#$Gi{W6X -&!jUzQqPm$IgZcCsH)btnW3!P$6M;hSVp+$H8#Y(#2)0b-Dr2Crb@h&L8;=p(6xNjD3C3+1qL-6oc`C -KZa_)cp3;dQ~8+sx{OKhh5^BV&he$l1RJIV%H*xNw*Xt6B4E_o#wKutUOke{(M>m2Co0_2%8@*<_1!=V=LnXA( -b0?|n;x@9c;VA3mrno^Yop1nz?`#&PbfSUd*zEL|qm<3D}*#TQ>ceJq0V>(PT9@vSN>1z*4zI2g&q0{ -QM`LqV$A`#A{zIEH`zg#Ubj|9nZSb-7xu@PgOZ?DcbFcnAiJ&ev2;TC+Y(^I1_YF`t2_LBR6To_0j7W -QZL0WM=Cdt~_B<>2x6FWc9a==~x04H;ct;x*fnCY^Ko_GEnt~C13rJCii2$U_yRA0rg8CH=<=+5ysRv -If5>Wna_FNhSbcYWPw-Elz3ne7Z8=QWB*nlcf>Q?xw}a)9TBq=L@_q0cJTZ-kZ~_Ua(c)hzcSh;-H4p(F4}b=iurjSz=$cB=59vo` -Wk3s~_oGaW)Cy2Q_ZpR#-8)er8aPVd^GC>}LdM31}h=}7^stHq7V8GF9Iwo8uc(LZi>o>z@%phr5n*+vPMLNdng4#gABw*U%+H`ooTB4$x@+z%M#BhBIOWyE6u0;203PRf{@g3?Dkx|XJd+fJ}kHt0C`o=@?u!Xx -tsfLv6vMB6i&=%ENPIugA<^L`&-DdQ+(@JQr%K5GNLhp-1j14aE<5=*&Xtyu>2FCR`5g$Hw9hvD=o}# -h^ZTlEzO!A@{Oepv^+PflK#OBi=9gEdS{CP6@MilWydO2Cy?8k$!oKoeGlU#q!2~_eYT69filkw;>g|9vure_2#1NdY(0`}ku2v -Tf0V16Hfp@gk$VIm<^U)-&1GjZC))IVI=i6UBMm(98K2jAAC88k)y8Veg6_A96DNglQo#twtu_dKim8z?f6SB -j%oB@oDt7AW%N*DJyN&Qy76#pC`y47WNIsX$p@eD?kDM&XdilkJe@ZgZ`}8?uYT?%cM}gr8~dbYyNm-qKokHf~6@8MN-tUyVsMPq1vP>#PBxbs`D56 -5N8e9gW+}RxGs>Shm2>#U3;B8h&9fOkZLMe0h2D;#9dU{) -lx}l&|2J@3qTC&cbov@&sC1pwdu!XYR?{zOj`lR4`->$_6r1er*^7$yZh90OJ); -$*Vl0X+VF47eH(yVC-=38#6<5zNO0Ag#sZGk>p)T4TRe4LHQkahFCI#F#G(WUP?kI${Sa55N%V;w)0 --sGpMUIM^cFA=x^TcV(%Z#{pJR$h+r+zxdxmq)qJv4`Z%a8ex(o(D;m1QFzrABRbMg& -jP$o5cFkusryT;skYzQ;0Nd>Kxui^}d3_zg>pt7fs}Z2>-UJfOlg`zRCFvQ0$LIUUrpVlwgZLiHrP9J -_9xcBhscFZW+(OUeNo`u*tEuuQm_XZtoYE&-BbVsTUD)xlUSW`q)};7V_J$rhLbLK(U$LAubzQJGp3>>?^}NhbIn)L{K?uBy*c{rW<+hJbPg-ig_PB@$m#F}@B6wsSC(Ar?Nz|XJS~eNOsmnn%FWzdm#2I;YBa;`@a-VWdT!0FP7~$ -8`k&eb+0M}FJWguli;&P8qnSSM*IN-+CAIv0%i=p^tI?_DF8q!qe7EBm!a@kp3WUwZV?qqMRhdQCi`Y -zOyWX#(U)4%cGu(FaR#DnmOwDrozPal@I{C8nq?x37u^=AWU%N?dJd!bW4?Il!PCTUZwgZrEy8gU_s< -Fd^Ce}l%--UvR)ZE{kSXmono5MZ?Ek`EVEqxVUIN<4fC@b__DuV!9)atU)Q=dRcAfcgU%kwV&^?G8!%` -YR*h`q)fN!l`4lZV-c{Q3gW9fdsR|jzb)$=F;Pe~pfE;MH+(&)DsAiu=R5_BzokBuPjLCP#c+7~ye;N -DyMVT=DzRp-yWhzhHC$vxub;J`p^=oaV)APPjxp}mi-5Na%-g34lMR*)PfTLDsYiHNo2cn|)=j^Yqf~ -9@zP?|ziY068broc@c1LY8!5!eEVzG?Mw8HF50YqTG_4GNt)`6-8D!eqdc8|Bo{I%U%ZxibReFX$}c_ -o<5PC?f>HWpg{#mJew@NYDk;729}Z_ya)3;lKWY2^=I67zmlhp;Z?^JFdM)!m;`h3lcW%8pUJ2A*4?% -W+bpIOxkxMhuSx_2p;=n66UK)k;q4J*t3ZS5#J<@K!3e#bg|;ZZ6Ci_~xW4alENB_`N@Gqwq0PXjq@H -0TXWS#igwU>cgftS)-6Kl4_qo9qQ#pNtYxkjO?Pcthm0$icY>_P+#8_RSCaox}j_l>Ap0U6;hv28hG; -j_|^Hzi<9Fw?z>|zqP>4iFY(_edtXm4cmE(?y=1;l$r#jnlk)dO{Z6n$@vuZ+#DFVFO2_RPOD@(q(#N&&H7+VN!(}7S`QaC;4Mk-V($4BpUGLZu*2?pEW#r2~Lt%Gq*ZQr#@^zGXfLmX;FhSi-@Ju3$JWO -rck!Me6NgmE6Qu?^ZL0R4=e96E(U9XuLxtTXDd*BOg(nk|dhCwuWw^j^sOZSsvRXf8wUdVzoOS&1$fh -?eyFBFf}^ts?XNbRiM_fO3g!G3Ep5ZcxIl;H*L8*DT+5MIZAo+88T~I!{5PK)J7m0no|rVcJ7;Vh#2y -QE6oU%oTy;n`8+AZqd2b<}?74 -3WJ?xDG>yb;j=E!!I9O`Dab+ExueRcli%@PBLVf9+L^fA-2vMuv9 -uYSPSroMi5et!Jsm2h7A`}g2~zW(#F1i0_N!I<;%(dh8;WBifNYJC&#j{abtYW(r^>MhcrW$4`$r@ -A-`E3we%e3$yU^d~Vj(^<>R}@gr+KB&1ViO6b!14|eto%*Rk1(*LU6(3ZjBY<^ -8m;D1k(k7hFI$3!gB>l?7MQC_R?D{2iFzkP{bSjIVrEWK*|7y5;EC^6ff&NvelOsD+B^8yv?I0{Rb_c -bA$=9swQp6t=r!BPim{a6^lqA0SvL?YnUwwU6Ne{Z<8j-v5}gS2ESdAp6y6Uc-N-FU2GUK>lgsxVwTo -V8u4^_AdvU#OSl1S32g+-ZvCr)CQ5h%T*C;wv6v+Nrwx3B;Sw#=i}h( -JIU=!@d!>&G=D^7>}J>IIeHs?%9Wyw;=Js*k%2Q_{Wriw(AF5PZ#yKR(YF$_?|xy5yBA&Z=lqA25+~{2)VVC?(TAQRlw#-OZ_)|h{<-j6{24GPJ-+ -tD+vGgqIo)5;nT&jyW_uw*0sJ%&rR)s&|b0iT5^oD!rn~|2!R~5=nXA&)Q)#Qj4(9H=m-E^uMmne>8; -V82EKQZUKQf_jOvA`D`Vfn-4j7)&fFK#UE*TBBPUn1u`Mk*V!N}v*W7K@Q`xCPWydG>mkyl*bl -vSbTrte;!_~>*_2WtzzuzBM4zCf}cmCDW?d&Ew`sct&k>5UvE)E=F&^xf)palQ4y>jAbX(4(r>epm(Y -oKDPMe?@dK%BJYkUtOos;+gjB${p*OPeMP!dKH|AgueWaqiX4E`Sz)?m^2%O?mh_`s645MoCG4ZW{1- --VzYc@TJ{%)<6k8Oiw7Q?5WJ$|%WFr~loZi=6Z^JKv$S5dSB%Ry$f&{rwxd`fKx -VWfAztOPL5J6UTF(;v{|it{0|XQR000O87>8v@l_%w*!7cy*2f_dV9{>OVaA|NaUv_0~WN&gWWNCABY --wUIWMOn+VqtS-E^vA6efx9UMw0OF`YW(jxjSzn!Gn|^uxM|^W|cyO3KYu4Mgyt?kQ(X(; -(#b=*?aSZ?YI(vUDvNzlQx~a2w&9c8MT9JLzZ0bePmrXr|n(u_jzI*ZP?A5!oX@B2mMZL(Ly?*un?Tb -Hu|NiycclZ*@9XxvU=*`)i?8|@pIxAP}N~}cPGZ6T4aIkEfRW_R~H~pp+vl*6Y+CJOVC3INKsTiE7*Zi04>{Rvfd -D&rGXZLfl#sN*TR&>p#or_MEoHr`~s>;9XTWI{nYx(h3v>nt^U#~*m*hpGo$8-EG$RRxJL)rOw*6 -X=TNg=nI&KLe+dJe^0WPXYZdrfBOFE?3)+goxOVc@@$g*@NZ{l-_3##64a(ID+Ok>F4|7aTJg)KY-yS -%*}Q-rEoSSsS&O#cPBJc27b`J?37^R^Ob)W}X+^P^iF<_IO|rWdX43q8_U4xq}7|p1pqg^69JR(UL$gE5NHNdhq&fezHxz697J$wE3c}{=*-|x@fy+3=N`*Q=I-o5_*?X$CYP|oi()O`1EPv1hdF*p9=)imm$VFViyKrhHJ0O`X*a -gGtfEU7GepEqO8k)HXC)KTFU0^GWfiJ5@k`9oq$HN5{aRniz%`+U}9yL)lIts`q*YoEwW|PW&-+``BO -iQ0>^fzI08V(-~Q8mjbn(sz7jRCFj&g)?Hgca;y@2)UWuX|8p|^*koDIRhZBI<_lU%@M`V?Kmz6x+42 -Ic@9i5(XgX6H@@h<~iFBS{x_=ILG(fy~3MOHXdioMpuF}*zjzEZ5HgtqcGDUE}wR>mbWVBOoHzrv_J010 -dK_W3$;;NW$GeQUMDBTl_jQY*GZyI=~2PwwxfCDn1sUtF=vBUIlIv7KN7=v{!QPu)(o3<3)G<&h6sqf -ZeUM@?qi0((3#vC#MfVc=6wy<=9K;O3;Xt%#EJA;n9vZ`PwXl{sgARB98jBPEk5N}yN?mWYI?gRjXe? -ia!c7?JHO}Z4Ak`%87fphE9xa2`f*8@& -J*bk3rg_KNKK<$E>+yR2-&zG@Vf)g=eBb4A}w1`x+s)Mc3V^%dnLvDPo)@||49i}qp+isNV8*a>)@|D -H;|54ZVAG13rpAMy}?ewP~$s-nLqvA!qc`C)nevQ`B3Nn9~QTz09Z{=U{Os0{D|HpIfV`8F#ADdwE%< -yr-0tgZ*Yy=iTqYTrv*%I1GJ)L^Q@>)GzCt6Ddq+60a$SsEGqn6S#R!f74>4(Wg{`Yn(~L|;<7C2C*N -Og>V5+=+rqED8zX{2xBxB5hO}K(Wqkvy{JI1I14I&7=?#|5mSrW7r_jWR`nGJF8YS+jg>C5gDe_Ni3X -tRQ+D~qB%d%dOu&T#9}fG$}t%2bGQLAL^=2|VE1gKtrXAd|h8fFkS_w+w1|3L->zSHg_g -lJ+>>2xzEX2|Qqiq{AXK=t%o3d>^7v5ot=D)R1wsVRP|r;iCbl{Y5F$)1RBNW-L#_W8hS2oR3ab@g0FOWE0$N!k;?^)&VC}Dq9ydJz0;7)a&d@ImF -B;JR68V#QwJs)X*fxD|}%RW~8S5XjVQQcTb;DuqW_@QCQ)NR~dzNP!a|;dVrjz=Yuwb_T>=GC7p35u@ -Y|bS%1gv4%2048Tgush9%PZFX^yALkbrAWH(!xrW(_bydtQIwN)ug=bip1v^*~ytdG`1f&IDjnxcFVW -;J1OVq>8lUWBp(H2ZfH~vyq;(D{X1TJGH`9zPHYMh|xW=4%B6Eq3ci=th)))az&xs}5~b9ju*=s{$(8 -(;|yibVA>$gpb|;ASP-a&DuCFaZq$v?`Q?^dk{#>D~QyEoA>YGLOf`Cvh-1M1?1YGR@wt3$(*0y8;i0 -|0vQg8Z=1b#K01k(};sjjCBy?NYG&b!5r%bitbvV2^^86(76IlwOkJ&3m&eCV_t~5(_exZ19UqejQ -u*3G8qLsCtDOg(Wc0fiepI)cQA(N$tm8p750F4DN-lMkg}_NfBy;uiLiE}QQFmZG#qO@M%L0`M0!u0w -!hO9Tu?EOYd1z@vqT&hek6T+fUa8*OODo-L-y0}Q!hyTDz-Ek=32EMQzsU|rS=DUQu$&4@YAtn1SXE`uhYOd2PA3?C)eAlVOj5_<=wBk8FCeEV*Zo3l5^U;g1M;2V3%or8juO&`#}{F-d74$F~jMZ0&zEUJCG4N2d?vky6@^7SZx+-0N3-8j!4 -XCqfva`i_kgZ>34ll(>^tE|A|ao>Sul$SM-Kq=R3V@=!Rb^`y0ruQ_X1RZp2*bX-hB|wjrsud#n<5Vr -~P;yw4=~5OCN4=hLuh!_dLI+_nXO_`AY7$nnleCb_a@@!;zsDycGWk#3NSQVb1g#VQl^tg~8qwGjg3N -=u<%l|{QS;$~&0||pfm(aZUlL6AEi>@mI_xd<*#&9pakG*%DMcx0b<6OeW -UnMf=zm6pv_cvy3?K%X2@&$rrJ$sX4`K`v8hnIs!7wsTWAcjR7lDVSIb26&7BGYjAdZG!{`XPǖce>fm -~Y0Uhhys5uNQnB|ig_o2C+ED|BQZgVrbY)9Xpci&$=+><-Pd04;hqm_uh#Kt;5FP4EElFr@&tS1tO|h -75+NukD8?4TE>_thR{#CiiXp)ngD*zcX6(Z$GAp-X1LJ&}p~oEn)4oJTA6-|)wlil6C=6r8aL -zDT$q1bV1C`wiz9zw(o(sC1)Z5UMTj~kdT8AcB^oiaJH>w7j -NR*E%HR9gssfPm7Uycd1O$`*W7}}*rkhxZ+mCI*u4?;!vfkU-rOgsaxkeOQT&1qZuLP8hmYWKMuc{l9im -IUo@(X-p(Zwo+QYHbi`4pL&s5CI7kbfCjRrO&)dI8IlDQC#HR--EzkMy^bYbrZtvj_I4VjqsCv%MB+x -!sKO#%7uNH{rKaLa!)>9 -H4DL7VomOUqF5wNk&;pHMPq=ZUNn!PDQ -^)ftfaAcFz(-rTav#-_cOqmgAi7(~{-v!n0ZTCw6obvwL_}R{tFQ*Xd%Hl?K`%y(VrAN*S00V>Bu6N~Uxpo!eG3@&*&LDWG8; -`~lo`tKLkF`DH6uN(cUY06%FMPTp1<@i?P#5hLOQqAaiz4%FEN0l)ICj6Ej%*7&NJ}U(guhaaa`u?7& -)Fpd2}LA0HCmTIYhri^qbIr&=;X&g*7g9!pp`Hk!8t2L^G&%=2RH~@1 -nilpWbXQ%P-l;Q}@KDFtQHi$L)=gbo_|Ah|^m5x)( -k)9mcNKv?7+$;Z%^Kf=XD-dz`;=NA`AU|i&jFaCgq~ly(FB6Qiyg^vUota66|0T{$XcqRP@S5MZu3Q*XAVXJiqLI3ybtqnGFv`}Z)|vb=PQZNJooY$n>$pBP;P)fqi_#u% -S?P5#&ciZAQ+}}&v&*mlAZpCUGU5{BX^X_X9$f?~YS51e}!b4EVQO7v7k)O_E -DG%bbN)8f+#17fp1|K3)w~wg^&vx(N&W{S}^hDf;iMN(kWkDKTFK)N3g`m>DSbzfSTQJ`y8Bt)SeX69 -Qh*fQ`Wlx;P_Oobh|D4^XH*L3y;uI~oKx0J@f3OM5HH0PYSxS#^Oj-ap*?~vU%;WCS?lV^F>~D^~9z& -)IrC_L}DMqK~1OzcXFSIQ^)%E+0lFyl#S*cpJb{(0SL#A>_=AVrGLZtDNkM~iS!TGY1Y%I!9T0TT^ZlUu*E`?C`Z)D% -b4N~j!=uD!Rd%Er!)(1$`MXN0Thv!#REc^7z{Zp8%W*vA0aGre3F$V-Ymp#NsRzGT(4!R(rDL}mUfIC -_N1kh|J1Ln?$U`-B_}r6xf?zrkaW(d9s^%!H&Os&f^*96-Eg-!KMzK+k6XwuPMee%E|1|fJ-j#bCVsP -3S_Qglq(9WrQJ8+IHCpwp0p+{E -7oeZ^V`dM~+|v+{R54w}aXEN%|a_w@nz{<-TZAFphQ_zn#*9lUpp*RIpV^S}>L2atZ>7jKFZB!hY?nR -#C6?-*8s{Tc+^Wi6#Zu``FP!MM -~xg=v6J)5z*Na8R*Zx>PAz|BeEtG}R=fFOt(FmM6Gapx08`toNvn4>CJ7Pp7BeWG1IZ#n+e?4I;@X@b -6-4Oac)q({0k6fl8OB+R97I^&R!SRP+wAl$kC`t9fwGbYn@CcR%V7p!}#187o-<`hRTf^bi*pBcpRk` -AzCglzN~lYY}d?ott$a1QG2l&ou2JrR#Uc8mA}ka*RoQABmxeM9y$&P#l{J{ACK|hM6g@ySD|(r%=;Rn%Z|9f}1^#W4Nx}}ZMM -K)-degJYiHn^VqBc?-fB8>eM{AQ)3MXecRSIh!GO>ZAQL@fsVrpt&Fmw1Jk`U6tEr}t_Ir8>WN+Rntz -jk}8NQBPEmuu2VM&qzKnby`OQ6xV?`6wjlfrH!l;{%uUpGvprD&MdT=wpisEtPNpOlVX7r@0$h%$BjC -wQi>>Mc@$g;#DKLP>+an&7n(stps?AsR8y{^@n18jg2T6jZ;3)i7iBw30=raRf|@KhisP5$V|bOM18c -~X+vYDggTu?)#BlH3Rd>UlGX-Q9Fy}v+qYu2DMrlV7 -=-I<}U3I2{@CZll4;5lC#(W`xWz$MNMz@;MGV&x$!BR3*OIey_7*NW`|Qrx -=G5_1KiV}+3OTjN2*sAVYbqc@v|e -6*7_ZY{84POrta8k<>}@(;SIrS4n&v2M#VQ{qh0GB>Oz@&NqD-N-G<2lk}hTR-6Va(L<*(!B#AH;<(R -QDJ)yq(# -l{LSlQW7-4{2sbD~`ac9>1wQ?5sng^^BZQ;_?nhK1qc8AYp`i1&;l|6L8`SAL3aO!8RN3!bb;{Wa$OO|gJ*TQv@Kor#Otl^yI5-IBd`UDODyrjY@f>kJ -7pO)~q^+kCw?*AMXB(v*{ZTAT22Z|F$MNcQPvh9xXn$L?B=OjGr1w|*A|GoVd#v7qG>+Jd_;#%+F&(Q -Y@}3;s3HVB})10mpOEM!Q1(XgH!(o0&Z$~cEAup4X6Q|Pm5E#*i(k;m5;c(yB<|DJOSyVQZgw4M5#lP --_uX}ia+QlHx0nP_~4}|$PV1AEsF>N>5+g{>xG>2eiRG&wlhBTut=e~Kq&}MUU2ESLO9$V<>F)OAbW2v1NBK{+Oo2;%2qhT{e3&lOJqIw0-Ws0LEMy)5b3Qgsy|6v$L%M7+_GB~yX_V#CH>T&yWPH^5v6d4LuhJn-;?(mayCFH9Xk>X9460vRR5 -%Du^rYtTYspIMkglpf9TZhUBu~tUy%iC(B;^&Nf`{x$@XQr@;Udmk!ru7t|wpbkT?otr}$1N(0hohG~y&B*Zj;!U;>ViHCF97EYuVhs7{5- -2naJ0D?odV3~N#V9YY|94{Y|_P7a~0WtfHztov1rN2=2r_<~+KZ>1_S^16pAE%I?a)07@Cb1ADn>auI -;w0ehetlBk)oSwVfBs?bl#|i9%3YR(M|Mad#Pcpi)tEe=?4KFuSh+3i`)n%WUukU$hf*VewkdA+QiKQ -8|3etD&*)NOO6-8s;d-C;e3Mm8=lIf&Xw&&1Q4xcl_R<6@T}c!})lrghc3{k{t;)@gS7vsbOZ)o3U8k -n5wrP)ge=DakS9+d@`y8+`vq1hJ8F|ynKoL|mE5$-6R_pI+-L%gj%$P&ad_e3?#r}3RNxYUQ&+A&FmOq$L3|c9QGwp&(sJ7&{9cowt^8gJ)1Qe8p0dSTrV#>1OSjE@n-Pj_w#m4M -kYd17e1B+=<3kR0~p>xPr3`$_8j>i|S?s#Y-28kLG{&eZMLLDGCzO-&g^f_V>L^745(|LfA(qiu?7H& -Wzf|3PUG-Rwho0q>UOk<#)Xe=jXoFMg#OxR()Tq!(Re& -lELVM?0;D|FDU!_e)s1gp!hjbJe7GogIFAjx|MUk(Y>NnT=V9tE<2UP)@p-?G|=CnJ3U*Ol5oGCP+2# -lBKb>N#D-@}`^oxrUx`Yv5V`D8A!*8+J?!OuqMJy*%0R{`c^+cep7WMTZcijB>Im*R?^`rRyBDc2m0x -Ttpvk4#N=AtgypvsRIRB*^YPH7X`r%v9t8F*?$9P;AvwNaD2#KzgTENCK9l+YPjNwF|pgm9|i?{W94nuI;NV -$(((lG;`W$=scu==wd(zIpNE%QFy`>qVt5`0P~DoxhfAb)15nq_{203dN293>Jjmz&{+PJJo-QHsMhy -!+XCd^-sKw;*zS2AWJP@#ON}v9cqn?h^Jr>Ju3cMJ@GsjowUNazEK~+rMg*fzQ -)W$S0I>6xVZ583-~gQ~A)vMrmP|7Q7>7KB7_lgGS8%uMg_~e?F*j|L5R%UK7PJO^(N#EY2tWFzoPl9&v5{+lf;@7hR8UP$`A2q`s5Mc9?2H~k%2>Azj7)1J5E08WP3?KDkyu -B<T6|5G9%hHP~G;J7RGB>AOA1f3Awp)9xLo2c4mbH&~XOVEM#!Gj -cy2f8AC3^k*jNbUh!pp-U*xTFae-3bey8|@Ww?*BdwS2o -1#!(i&*`l?npOt6syx_FmKhFuF%Yo-zuGk*qKy7SItJ9@ju`Qb9RyC}J`D$rykT4yi;T})>!|PN`?f9 -C*YTSj-zo<}_eQ_$3aefma$eH)K5c=JifW?wIRP)WZtpHG?r|?Mo~3mIe(j$}Z-qc@Q -$ci{blY&q7M~4h(S{R}O=e&$++~M>C&m?g6O>o#_zWdT6T}AsAW3jwC{Oq+Z&3X|xoJ`A1M&z?eOlLc -ip!qFw2fh8umAIbDVDKZm$dU5lp`~gIF`T&FDte+lT=6a54cL7#MP^0$AL6;j2RSs5`VhnjSGusrGlb --GyW<~RWiLSaP~k&+24V%B`M~uf=iz>mZ6<B=U55VBm0^oBu7cM5za8N*c>PKnIH1Z0PI+D_JueIYgmT3_2*GEb)AbL4Ss)7zDD$GeH9q$Q$UaI{n-wkT-2 -f-cEe0HE6-Kb=Zh%Ha#Yz0shFIaHhZ1Q#XFh7RvMzh;Van9V|+7ld9DK3-wM()VCP;(Cz9H9wJR$TQ- -tNpf&DP-~np)5`2|sM~trQ_mskbM(WlARB6n}Wvq~{kRSnk9#o`Tja7XWnfg)X0~V)UBUlQjYQ0Xx>?g%$|-8qE(!0DgpPU8lIBQd8WPEL1&efA$m}zc;A8R}%bL*i+P&q5n8We}P2AYNbdG|4VDB$3j;r!Upc%d` --1fO*Fc#!3XdOu~7rUq>S>U4r03R(%MyDe=|Lv#xksA%bWD85EORT3UKwHHUmSB!!tT#se^)u3Vlr6y ->OKkb*uWDV;5(um({(8suq9<0^vR=6HMQdXxe}qT2$A8M`-*c9{@b3>lVTjJ)CSo&4bCX=Sy{ji}eTe -qvr`qSjrjfC8AQ;6q%>z!2{c@cB$&c33Z7ZJa64l=VcA+aUnoqMw&#elN(&aC&=1tXb?B#-r1=CFYw5 -+pXvT@LbNYXfp`1n|Hu!(;Cr}N2sZSlA@^!}=xV}2J{(nIHmZ$yihRy+wbwKA-w4!Nb`q$*gsh{?P#k -%#k`0YdO~R#?+0u{L15$A}y-L)y~5gf`EcXrn|Y0LzPb3wr`nxjt%+g_Avjzvjh;ZODB8TnKJnwtSJ8 -tJ2fWl9@!`d^@jpC9byhuIy!TgFXh`HHpvJyjgD*$~8B0^EMZu{!9VtlkE0EjF8}*-%w(Ob1PmEFbdl -i?m~VL725Ug*pz?>gb}TpTcJ~fv>P`nqsK^WDiOF~+8xGjj0(G-W9UwjSKh`sl5R|~<(s%UcL5wgjoj -ZC=;lrZC+7F}1K;Xgi6t$1o{3ROMxP#|8OBWO9u=r3^x?+KrU^qj*@-*pND9P6yCnLG^bF%;&GPvOFO -(lM-Qz9$9 -F>AJU@<;91jvt}0YP4f0=w}jtfT1Lsu=1fwvB_4>eUpLcR#>}x5a3-SsOTViExUCQJEQV6I93?D##dz -GWKBV1E+yHZTjdc|}y -p!aep~4auZUw-~Ht-qajOfqdSj$W+MNePuvzHh8$q3o^68QN|x!tL$N->s$nn2<)n{7x?Q-ld~aC}2H=E*@J#K*1rte%8{jBi^NO1yOl9K>&4l1z};0AQcp9D}r=Til@U3H#g(^K@bgPA -TV_yluIHVgLADQ@V#D5f2zhMzGct6+fyl?@4e>Okx^s}=gHoMzl0p)HqhDRLAq^1E&-m0nJLVhpYpc3 -he6wfv@&fWSZMm6g52k}Tna>52La19PDHSnD=AZM|`{0$l$WP)h>@6aWAK2mly|Wk})%&Rhu<008h&0 -00~S003}la4%nWWo~3|axY|Qb98KJVlQcKWMz0RaCz-K{d3zkmcRS2z$KR<)yhnq-gfr1=bOp8?$$GD ->apG3+sS4mii9N86v+cX%i5d$-}k)-fCRr}HObs`JLP0ziUb}W-uHU|_?(@7amtb+75RF`D!n@WJw7> -ke)61M6y;uu^+vPsV!~d(I{S+KS>*Aa-NrwOGJ+OwMZ$B%Q>F{1H=JGX^`^+#ZL!k3SaSAlQRQi@1w4 -ZrE?EpiSt`XKP=j`p(#pS!(%Sbp6R|A|so5r0n@ -lX-&#YLlVG8bNq1>I?R9a;9uWDJ!BH^ljulcrIiHx6Uxu2adxK^^*!fYguktoczyQ2pu{4wFBW>@s^Q -p!SloziN%-OtCmo=}^}J^e-~4HLK|fIuPpIw`hgtX-qF(wcO}QoNRbH2a1(k>iv}cU6k`wK+jt -)AGo}h#pC|u1ZfYvJ$Foq)?B}(XW@LF$6G$1Pfkw0x%~Fck8khhSMR>L{2yo>Y_u-b?B&aHUy7(C$%z -WNrr$x`{O0n_+wZZtd3=&&FkN;XC-?E1Ux83PhQ;zHp6H2T#ItA5=oU2 -q|B&IlL5lJ`WYoR#W8H`MV3pd>I6rQ_628>wQK8OqT!W-%bf6X9729dLn#4(s!u?#%Y<0WIHc2{ -M?K;#d=h!jPvPy%e)e#Ty?fW2USbm&QGm36)`x(qWwPPZE=mCZrDkTb&#*5!Ka`FvBrtcG67GC@^-+; -`uSL6cJ^J1_QoEqQT!T$?&ReYWI=}Aki4(2zZg)&yzUI!uP;p& -?K0@0ul!w&`hO8rOy$O5d$}VL0>K<^_>j%1tCFX&e^MBObJLs^I@gHYY8HK-+OEQ#SgchujpPV6P_&q ->p4ebClgSR?H1&OWP8LI{E2JQU9>W2yA`vYh)p8bT*iq;Z&VvN*3vuGX!A^RUCDe#04fl-(L*j4Q~2eK1N^GNEfz4DY`a)cKm -c@hPBXG0h4^$ -V7^}XV&coelCMw;(T1Y7YZSFDhae{PB7-;USmaYr0-#Z41Nd5Cb>#$h+|OHsEqMxY6$B&7W9;Xavbh3 -Qa3?@5v6;3d!{bpO!2>M1z{Y@K+^2h+-o&0I=Q;9dO7Bf*5I44tyYK*g3xpTLRD^eJ1prQ`yM{U1p4VgjeIU82wHW#Rr(I(CyR(TL9=!yf`kGS2N8GFNKlzhw<-`i)4I#Ne8A>IeDA -a5Cufj2QjYK#Xr$W0p&vAo(Y0bz9S&hgi1gQ_hLfN`|(L-^Y1W2S)Qg;uJBxPZe=!o>s3n0ySGuL|B^ -;IbEHGwI(23P65L+f9^*F`E4?Z15=%rM-+z!8&1ilX;{7da4K&hqd)DN;VSPs21 -4nr{Jv45Y%fS9)4GrSJB|VoOMfy76!9DwzKI{v+t%W5l}y7!&_M{Jm(`dNG-V1Vt30>j=y -OUMNzAKauLV{9d1QNCr7-3WReooQPLuPm -XvJ*e8E}Jqk`)Ruuf-F6y%OuZmD@lz4`i@F;3XBOu#xis2lBpqzNh%$P{riNs1Nc|=oqU#ev*VEku@e -P`aekgLl_+xryn{lswv?+X>7I7UAeA`dmli+ivHsHmTzQ`-sqk_Az$5-;@apq4^Fk!((JUIxKfZVPE -{A!v=WQw&&xVJqYe7PK|q1CvJ2q2BY@~T9^>I1KSHB5JxnY -AN*&4Q2SZgfkCBgQid-YVNMReuj6iOZ7+>u;+s1oHTgl3IiW^DB!GD> -OYOluXRZkn6b_PhA2s_k(64&RZ_Fw^K_|AN= -bc_#;_MKt&$yBqT9m(_=E&sudcV7rR)rW_4x^Vhgv<6KfvJ$lMs+^hgJD^9pz9xX>;i8Fd~NT;4*L-( -tuA*VxewIkJw~M2F*`J}GRBm#WCHdCgFd1u!kNBaW;P*-^mOM}?>}vcO{8y;zg9)&X5Ncl!9OY4CC!^ -Rz4JDwD&-4?hsupE*~`4ho5xm^t8NsUeBxctU~W3UVm7n|71q+f%vOlhnN -#0^E9`gX+?sQMO$3`vJg=Gh=>*d4cLoCgZ)d}z4$whjo7ib{fzA7`b0BqTxoB*&6tV|$ui0l$cHre_i -jT?dTfh*9-*@CgY2!e-Tpgc%g?S`%M# -6BfyH8pKjvT#dFIEbb|SSTo0&LyfXUcxWCjUSNHa1Ee{tdT06BFLv2S5;(&6sz9?WA5Y@287iYiMFKm -!qiDUq6CtX=>va)*`&vHQlugs{S$T&|#5_piHu{ -F;R4#ar8MY$1hrc7?dQuC3to%e%IS0ErE$E`xR)&NIdP(7Q256Ec6s -V51*sG<%SmVFI}{TaZ?r{YTKkWFqIx>?HHoj?&-FQ?WKHEOwJ<1P{Zy*Cg8TN7KaT(k6cWyRZ9Q&GV; -vr%_dsP;}C4w-PjC0YNZ!xl>3AfNeS{XwR3jtqvbFZ#U}XfQ-8_C+ -(rk_Txxywd6c>kiq)50sB$rk;{WLiH}+#wDD?MBo%JHZZ@#^fh)Ylt-gn&$rgxTU@+Ok@6h7-Xs~EWF -&u3ka^&Lymyef?(bUET6m75=dyN8Wg$;{^A^k>gagW#_5&o5Yxqx6&hJ8V#fev_bFS-JMF43PmAdSJw -yK4xx30Rhfioug{M*a~I24Ms}kBTjfD|GVh#Tn*B)77-7`_Wc?V*~B3{hE?kM}Uzke;9}xv(#E}=d^$ -n*v0y+LE{G#!;N}op8*)9FmOJo#@RFQgI>bS!=dZ7K5PjM!k=OWv6^D`hji9aYF{vKViClx`FSYR`Sk -T@gVdoF={|-B>Wj^?$>BBC=BmN>O`%VQ&o!orPMO(K)fw>Dz5w0u`nM(MqnDsNZS5wnBNX?uEyXrg#9 -EMg=+9t7j7)Foy#9lsfjU$w!I6-NzfFD>lAV%XC{fCOgs>|2Pd_^|FDEH8{jFaFF~l{e)Dw -^)jYV~#7Xd%fHHG5kf2u3P>jhwNRSu)xt>lW^z8UQOBAkuji|)yFSSJh3ogk1;i-DJ^z+cV(iI8PE;_ -A(KU$l-@2%;IRj(H$`!O$nv;?G9Bf8;JM(KwwaR#WP{wWO#ox}jplg3u#w7lh1}v6O;?M>eVerBqv=w -olh~%H)}XePln^&&`zAmx%wB)QX$$$A&3oWiC?Xy?cnJ)@QC{1ZEoLaYd4)sv_-e3%d}+C?Ha8t|P>w-o;AsV0dWW^$)e;)egws6%J6 -%5zk%pOV_wVML8cE+Dau0F^--Zo=gVLr#hC_RbO*+M48-j_MA$p!Pq3hm!W?TZ83RjyG=Uv$oAB~>CD -@$Nf@~PRwm@^szBpm5{Q&k0MtotDFr*@S1agtj&~-ZNDgE3z{-(^w4 -Gk6Z=ZWpwrw0vu(M^`bXB&g`kcin_>5J7hwJh^Lv_ycs;gyrErHw5xtqMu>FSHweqDNbZ&RJ0?TFKl{(u17&J@cX~EZMvE~X -dNm9)ueO6PK?k1My2>fAf2}vqP_Zqt!)K2z4kFi~ZTJ4!g@y6f!1&Og8iqiPg&73gybm4H(}pBd8n_R -5>^MYg{+8@i*smdup6*6DqE_SQyN8c%n^^!OzC33@PBYZ-Fd)o!)7ybYwQcOK__ZL}7H&Q10K8fj0Kd -Kn*%6;&lbPhtWtCN}ft42^L~mc;a~q&Jv$8HsjI!Gl8MoUC099jfd{;uk`QISw=Ab3=;=kzOQSo*Uxx -gC280<@qb}i!?f(t|k4;--Wu!efV!6>qM6;~2%qWF -U|Ys<9<$DfS~lM#MDS~DUni%n+1 -Vadan&XGEUP=g&ORip?{m0i|z53eQNghkxk!$ZmO(a|HcijMacW>!SU$?Q`=~`VX{4}YCY5#PpVc`N! -=I+Bus|l_9P?3?|_WRS6T-Cep+h~fx>1n{etfxB6yD)I^9ry+M&IN730|d6GSSt7ft;n4}d!e3zIUi> -K{iM5PcX+I2Fmh(6-;<-L-LaH(cNkr?(iGfc1X -(f#*?_Jm3(e^wYs_yvTOCm`6MIY@CM{nd%VcAXIz&?V=`j3`U91b)t#7ug2extR>v(OMqZT-8v-~|X4 -_Zf|I-lGcEQjqd{`=fl6{-pakaE&9*>e^ru}HA$*6Y6O)~%D^^uyC&UZ# -1moCMouar$R`b98ca5Z^cFOpsGGWkcoiJ{mx`8+G?EDf46Xs)y1`L -4Mxi{NFsXl|1#3%)A1MVd4@yn_!}6(rDJ(Ny_t+m!HaS!LNeD;k9LGRuN*o;`W`-OHzO^R@|+A`PB=| -J|z>&;ItqtM6aD#FtR+=qO)r%Bl&LzvP>vMOChYybR>s-*!z_Kl@%4`A1zA>hr2xE@7bRYgwyLo3hT| -Zj$C&y{NLIrrJ%80(j(Ab6sT#&eKu$cAjmT;2FJtT2*B=34)VgQzgrFG6{+j2KXkc%*yF@z1~eVh#;v -Acv9w!;KrOriWdFW1mpRVdn4tACVvF%ICvCaLO-K5dKk^BE4VNY?O8eL92JW9n -n)`FWddq2$lovcc*_R?m|Se5zs9isiJ~ZL)eC%(nR|otB%d$f_PdU%z_w{6&`LRW@(FhQ(ar^4G!Nk~ -(u<_W*d2L0{@-Ops`*$H5OTzM)SenG8U#WCjq;IV0?X`5If_p2N_`!8*CgrYXFZ`s=Jo5QeJ%t$dfCq -2@}@^gNkgXX$iP!LX}lhqDP&Je^lr3MiZ>E0`CchHaHi>s3-;dxuqTR(UgB+fSUT2ve{(s_ -^agf(jM!8C-ZuBIoAv5lvVnz|6KlHvHo9&`U#wTq^p{V+di=vTucptw`||0JP&vGAnoT{qe}A*vllK=81}0d{(#@eW^}2iDxsZ}U|S_z6E5SchX -C2Ti`t%58K0ar|IJMY&s2BgU>xH_7}aS;8ycHU(k%w0}Gx<2+njn8}%jzabDwH@NW@g(ZOiN -IgYb}*Nj?dz^(2-RJV4abl;FMNbShrPiVu`gV`dNU)B6tKyszq76jOoo;0A+rdZHs0bd|zaFmCr|U@C -@cZ_@-n`3$B6di*PuRDX9a3a6g&Nfw~%KGGEofESo1=WNS@OC%c+D!$cc|q11j>+gJlT ->gG2e@#WR@g;0GA-BCi1Pxpu5;7uhYNF`~A9i&by39jNKm4!B7-B-~dB3Mgg-_ykwm>Il`M&rN*E# -R+{I%%3Jl3~JpRoP~h%(E%Kai7qQy^=Q#K<#}hF@nE=XY3NU2ZE_4v>tXyx`B! -p(u=-q>7inASuwt*)M16GyMhSo4>0vvtL49zZsYa)vg0CTP~?!_BNxxx7RDF04nurn}TNLR@tPBa4fT -WVhFS*fzg4?Bl!&q_3d>&zYa)Sse@g)4Q>;VN+1%}Ni)BWNm>F2OMt@&tT)LDI2?dNdgEfiN0Mcd7j; -8yk=kfAo!~Zau9=r^ivksQ2E-hb*nFqqpoH=jw*@2uY76&qO0a>!C2G+WLx;wbVxA#{q#f-RxFRsk%? -_C7I7ntW{8P=ZM>6j2fC-$@l<+*}s+ws)G|+o$tuu*cdClCD8aLkq3vXaK*7ewcb(_ -)10gnRIHrYB1s)Wb@xKR=OV+*Udt#NF?wy{8LcU0<}TLxz2>V>0`(dx5>X+%XM6r*!cgkVm|jA0Il!V -Ejkv|ecxGyvi91C3H8r8S8O2If{RZVm;GH72Dh{TY$doQmrmvSI8AsPZ%~AXXI`I)heyvgi~@Z4R1h# -cN8`X3&}Plhq5x?G5$`VK&pUJ(M?1|HUT23UVzp_^5r&XGeJQ^6!No(D#4=PmCdtuFl6 -pW;d8Qy+EFU|M~yQy77f?TW9mAHW;Se?y9l(`=Zp(%q_Szef62ff= -+M&wA06zq{O#a3M%xvK=77wPhWa>$_t;dtaiqV~AR8ekt?D^7KDY;ee}f3##;0bmiI1Gs1G&+XRWf9kQ5i!J9e`z) -TxiyrtD%HOpXc%OZILid!8hhbIpL_cG`eycz5MwfKvI4wX_a2y21<7ktUKkMgLnj|z}VO@6-0n_UnA{ -!UEc^w6y(KYqX+ -?p9`xdYopUjz=NLxquxe$k6XqY=_=M8{ELl1N|>SIK8BD=yRD(NGzNs~FA;VKNfXE?UN_o|X6|Cplz3l0qp>dZ5fNgNir%gFk#xj_^i?U@02Mv#`1Eg4Vw&pCBg#!rNSJ1 -UPHqnEQ8g}q&XxF%D2n=sd9^P4&q4`=R8?9^>0q%W(owCeEwoVE&n*;q>stWXnX9()k^50~;IylDR9F -K!zncCx)ryQ%PJEkQ#hFLiNYpZ~c*^y#^^)Cxy4(()%InUT2Qa(EJHKefTwAmA7dB$!qo1r7R#(uC8{#aHz9lrZrgK&T!x87Ofq9xQ)ELyq0NFeoAJ?>kS3^d^E{ -jIl=}J?!pJ9>20h*A{roO|Hy1k*>6lOgojyy2icYtO9dnQ6^#oQ0e^)CO^I%!2$d=1<*JEt+t9$i -CPZOxNq}d}l6kikKUHxublUBKtZLikQUt@EUO|!@@13h2ceM?r$attW31e*>3tm9=>ZZ~ygs=_WBIdy -P$$MzhTRldY|MFkF{5sdcfHPRh^xHx@yN&fu%jziz5d@+GSE`*L6XIIxT59Xs(K~~yq9nt`AYj~n)xq -4j}y6T*>s!))-*hjtU@l$aZfW?Zar?F2w^mJR`^f#nw>7V`B2~UWvgp7Fpw}DjrMeg_5IU!252vVwy*X!ei6n}6kY4ln~Sr_ -o)vPlUk>!+SL+vv2hu@07~;~ee%&S&ZXQHoUc5+* -ox@N+B}FaQ5}Of9mbWY*1wQ5;d5v1gwIJRVJ -#Rahy1BV)p1Ba30#)qK1foCB*P1e4=?J4%O%kPMi9IJI?)J%8mMoe*5dQTU4TY5Wo0G|?olypgx~Bl9 -jEL;#-d_Vp?THkXcI3h=}qESHw+ojzj14j+^!8;s3S)Yl0>LLs+KKXo-Wj79- -Hu(K4dIV4dlILiaJkc=ElQFKL@x7nD|x4PR6+PEqrk%Ag^v;`I&T<#yJGOUMm%UEi%i5Gd1tkjQ$4yH -4B#KvsvQZlNufi3&H^Opaz*S58-ugg_hQx~gsVtMD5t1Kzn-5o7bDf&n(_(FTElnc*v*!l55e -`vk`!afL-bS(~NA_OMcaOsfRMab3a@bUtF_i!w<_0m`(ArRaL7QBhmf3u|Y&Tt=Fzw>5-mzBZ~(4)A{ -8VRC^+9}MMXf%qGG_|d1njhIFla}4Fs`ZsQZV6O&C3OIl4l_pAw~CHD|0;p??%NK_*+pvwV_1%9CMp$ -IgGFhN!|0=K*5Jkx1Y-P%VXWrvr@`AnX(-DedKf<#g-C*%th!D%y>T1#aFs{r0n{1?p9g37=L7uu@hE -h=Va&z^{b*NLXm4I!jd2gIyv5&7rGVb(8*IHPabt+5KaGR$Tj$xGLE(l%L9Suf+h)Z7PH59}a%Jv3Ug -<4IRE}UwD0{B1SjP>k+$zdw@1cjiY>z<8ZPk9osRb^q&y$a=`w --p_<-~>!`*p(Be(SDNXS$Ppvv9}%DwvfVTMg-BldK8Woz%k09ERt`dxDBCKko1nSup4tHw2E!}2j&os -Dph3^JPOVx0XiX(lKTI3tO_hnXl@0=9J4~N+{Z -~Ja15hsg}Gp<_)Vy#5zGaQhdF~=)p5bdEs#ud3|2e1k#u4F8c*<25c(VUkOv2wjg+WMX&PB``C@-DAO -znsTaZHyh6v~yq>nd4qFPeLUFR$fQ=sYbY*{cx`VE7at?qn*;zOUze$&ISda&IvJQ*p++i#r1-EqwTg6VI*`Lf$4Hy!!e-!OcNY>%?c48Th!j#P- -V5$Wr&_CPQh^K8jjSHc+!cj(bxCbSb|UDjID6YviHEAWg8Ynns(ke5OxhhaB~I4%1o@%n3jh3dMBg24 -%GP1+U`bkqv;$DF7lPU0RusBlc)RB1T~fYI{lt|j-L&RU-ti0H2HmbtM%j|rw$&uS95?P?Jb$9%%;m2 -WX#UEG$ik?WI{5M1Z%(0F8w}}0mj}-%gc=m#vf?hxlLC2_3X37Ssww^;Sm#J~HClIIz>@{dugJb4&W) -nf4q5laA(M*z;fFUs&em4LAwkHl=4TzT;_zTvXc>4XYMC3I1kZJlC`X-vO3D)rqksw8&U=H}sBC1f2X -COD5+DQBqqCPBy|`0-lEeP(zyn%(Qvau8`&4u)JBDKaX|X#6khXqKW(yM_VMAfSuPynz+Lg2~O;45V6 -+;)zfp#tQHiBPo5k_T#6>!8TlZq5%iZ3pED%RWe8a+N{J$dJ}B_0gZK#CNYj-c(~=f$FA%BwFPbY*4 -RDbrKnb5WRmiVyX8Se1JG0qRkH0qqfR3m^v5aPp#Ou=wmB0e9$b128)1^M~|%LVN)Bd3hXZ*b2k{^YZ -)e?kWwjURVcM6=tQ$szm}a1!N$+1ftl1(zVVdY<#gT*dK|}Zv>Av_0@?+l#zhlAROt4#CyxUR+<Kzb1(al0ZQ-7i$qL&ZsY7eSKVU4*2+ -Rd}S`GDCb>w1QMIJ&&$`(aw&SbDLaS!|Af_Lj{j%z4UWiLrB%yTse%>_jCDnp!?ZaP6efbcN!)Mf2f2I-QEjKYsVk;zi=*%{Q`ehZN8I2{@dDRaPtkmh4?-Z*ltDlM7QEX<(%(!r70Y>nh997n9@Yi|3 -!in}?4semcJVaCH1Gls9MaCWJTPJ9+Ujym<7weqF%J(d+u7(HBS9f&&Xrdf2e=)Q1HNURbb>B7o-Y)h -C@^ed_k=GZPYY35Liyn}T=v=OzF7kr8}KsV(g6Pf+1=d<}gthI0`fUw#omTgQ(sKN`6}`?D5srmgSURgA!?|L!s<$3lhs7 -J(Rhd;^FdQTDi*+Ui_4t{_^1TFMs-9AC2!%{(An!zr`o_rmtVWJH6D|xCp|(Icd}Mk|6ybVlm2$)+&` -1krdYD6dJSyO$i*B&s4(-`GR+nS-B;*bY8h^Vax~eGO*p(dC?{m0R@as?lz#1flb-!hf06o6}Jmh`aA ---*jCgfVzJtscL%A?`LkVTFV69+92CLr25}H(AU?L2?l+&wz()khP!J`Xs}%0KH~!Jnyzyuc(cuq*~HJ0VDFOPSFY!@Z)gbI=75J7U%SVR(sro4=fyUp}TUzVY$zpO}R|HNSioI -%Xu35*uS}gUDdbFCRNP>Y)3evu;va(ld6~)nFK!hQYXyy2bxGU7823`q2Pb5x6Y6rJ_D+CT7L5mMcwewbzua?-C814#Nv6Q&cd_%-~Scknuw -dB{J~>{4XPM5@}po`maGQB=S{u|Q30f#3JlP~-D*jUR8}{9tPKB4K;JC4(6gkz*4@ETwhA9gy{bLJo@ -U948S9&*%CYVfmUyR63(g)Zyr@>m;!rkLwG~{B34&HIH{pbGosx%YO`1^4Q$kGX3I$|MXA{ct8vi@|^ -A$=R+z|xr_FS^OMk%Q6?C>&R6u^ls7&HFy*Is47+7IJ{S;cle#c6|4oIiZ<-W!YRcXwa0D$B;*TuU>Rk!ABX#%mpn3@Z#2O-$(~)2i=(N&`&W4nZI=wPxhsVJVl<-o3CkF;|X6^O!7}nIc3}MoRy9EKk( -m9v00K%uS*UR@PNT!9XiNU0Y|_x6!2-6G1eFg7~hP_yx@BO=OA(BUjQVpH47SluCLvBY -BbYM)j>a_pg{OpaQZX0}>sN<)+4*I+Iuvkb)#Oh!R}EmmIO%vaQQ*7;IhYrmQ -x20@(RL2Tn=4d4rM{7>H%Fz=?HGn&0fJ_d?Abs=aT9EMr>bR64vZlU`IYpG@=I`sID~@c(M%qrb7FDM -7*=(myaqL+XbqeVNlQw`@{T}LFb5dk`5O2?K!;UBG6ZnM@_?d)D31+(NBBvKbT(t*I2y2g)JW~1L2?C -ATuo9eB`niGLHF>gHK{x=Q3C{=HS8W`nz5Cd23u?qES%=NoXfPoyccz6 -n288n9Q*J~3*vmLZoaME!61egiREGOl{b=0u1P33f13!|+||D2=yyU92^WSf2N7L}KB!%hxRHSiZEo; -umFB4Mg&)LqUwg^u(rL6t~4Xf0uTgiy_i#{ktlE#7RKh!Q4qJMRUu;X|GszG-4vyIjBT9w3J{8N}`xt -p?~MCDH|~lYV@f|8PqIHyV348Qa-F0i!d=b$@Q9(@-IYQ#thGi>2hmp-HDqDiNmQ|)3NoA7roJuqEKH -6u|w9Bb;>I|9+N#`&+&@yFih-QrQ)aEtvVV&Qb?6R^=s+myI^0GuLUxTBXdy>BSaiZ&ok{Bl|qO=pLysq5&P7IDMp9h8!m}N7p^*Xs~5s%J}YtBp+@SND5SwIaBgS+z0647JY{tW9y8g -O91%NOt(qtC8wmt3wHCSh$M@v*v-;In&Pj-ymRdPOsF=WMdTxj42MGU(Mw9+O8bB$CJE2y+f~x>iW9g4|DOV{~qwr>4`Aul+1Ox&JY3Uq>PY}dsJp{0{ECz3$JM?`L|BRw}9_=ozNccLzrd41@mlX)vZ;gU2bB(05^v$ -tUtc7l#68zC~KylK4zyjw$?EJ_R;*$|PGoaBhztrNrK -1=dWm9aB^9Fu?Y@WT)4*Fr5Q_4h4n`Xal$=o$ze!F4s!rGIOmzuTIOc!F@SSmYH={4|nK$!#s!)F54V -P`0j+A9LWMz5|H+EOsSXO^`3OYXQ6l9kv(9}GTiW|)fqf?Mq)*%zqKCr95QRI4BRxZ8GcVY5w@XB3rG -1Q0G7?Wr)5*N@}S#&i~X52dhHZNo|-Uw7qi$%4qn@ZN?2_KN!0n+{l_uL34`Wlfc2d{dH9A8;3)kQ%SQ+=`Z%fp|k>!DW -1|}jIPjNGPL8oImL96y$s-OYHY7MQ(5Us_HR&9+m5$y1uO7$Lp*@?=mKS-kkq4d_2di7Z?!kiIqH6K`MyGa)dqyWCR8WTHv+OUcRae{17pnGUfQ%9&CzW)g6i&l1CHjh -Cc}_(13^t@|Q~RBE7@8&587eoIwOm89u^yfVqZdZhR$u8}s#A{BCU=0Tv*}W{I(N=RTT+QV1-E_i2Rh?|(-!=sKQD1DrwYTS2j{^j4?fwOKf;It{> -ljeHpNeWA_R3oDtGFx$q?SZd3;bUIc?XGq@M@!1jb08k5~d87|vzpIQ5@?AG#?BIKIinwg -W<&BonW)-;!C4f1R|zLYcOaM@9DYu6C -J$j?{dQV$n}U(eBw3mr1gP7t)-4$U;IJR@glg1LkT4Ihwdn4gfFrS!yp9rRB7vU@V#`!dL?r+;VEzbP -s}wbNHKwr&##WuEDZApMeMUXW-_8dalSsq0nE#3}#Ntl^KCu0-4}2GBOCl%@6I+b&zuIVW_Q3}R87Rm01u -m_{~~82^&)^5fk2fk4B&^=-;}`kg{RJlpa3^lo+1VWgCHYw89uOXc)1ZfVkFz!Bh>!@8Ss_oUtYndRt -6kYp-=XGOMvYU!4OYg&5nfM_YC^J6E0wwWPCGO@RrP!8aaGSKsG>XP@l03m#i*GFMKbQ!tMgK|h$4oF -QgkN~_ba(L#f6W;dYez(kYw{n{!R6 -S^oT58-E(2mA4IS!GK)-XWd1+M8kvoBmB66xRo$0QM#5#;IzPbo-ns-4jjD6dz#lqXVYGC`Z*$)DG0` -jbn(bf`Ux8F}`0!3D2`Y#jbev$RMmXd_D0YGH>8!;qE;+Ch||{TbJ+YHtoVlDNK7U_qrVY>Zxz^(*pv -B3-zpOzxL6a5L_3{_xl?uqdgzhqdSWeIxU*sM~7)vVdyC34n{ptDRp~E=R;NUccUEzkX?{&hyDqC0RvHn$P%64rp)tpvek -YgwXX6i-B+wyr*HzDUYc}e>yw|KHc3g1iJ0_#m!~#4zP8w=>B^OT3~&!(zItWduK`E0L@cRDtCsgSBG -nOHo1ETU>%%(_-A7BCPxp?nU*0k|W2QK}=ukBf;9%xPnD55OufD8C1pbgdp5D06F8VOaS8+jQyhIVey-~> -;XnDrFuK7G32diS}7^^2IjZPty_Gc#U5C9RgCo~t9+X?pG@yl5K-Z%WJ>fOT{b}6}OW`KG -S36=ZOx?>mY`~|39~da+tc{jdByH?`%!NGfXEEmi2v)*S6cl#=OPIHOt5!iBt -(j6sVc=ol?2z3XGj5T1t(iV=9ori&LH|b5#JQFDYQ&!H!Pg<#Z_;O!M=n&x23SJ`3*ir%xWlM^11#Ot -8mD^kR;==g{N!^Jh`w!(kYyzT4z6Uq~94)$%^y`*HtH9!c}E3V%IuH-)X@Ik4CTS5BHqSuEN(5CPqBR%!H#a5CE*zql=$LSbg*-?3MV$ -w8NM)HTiRPej~}GOy?mTqOxvV(N+5Fa2YpcHoB7mKISy^<_TuJL&%ZjeYe2$+l4I_s!x=D&srtBM(;g -swq0Y`B}tmv9;Mcu*E0u?)6@y>(8Y0n -`xJ6K(OomOp%X-usfo>UTcbTZu-xhb_LuAa_z#0W3&pN_M*Eq5XJj+p+cQKvjzoUJwmV>Jgh>$ENm4b -=-jNN(K(;DdgB^EAe0xEaT$IztE -cYT^>m4=H6#RCiGB*DXI<#(ARB^|sL{4mTZj(Da2dd(_I-PisoP{^=Hx9s_Nm%fQSk@ -@ApVvCT=jS@JKCdh&u1%!+RHw1s&sjM6r$d4Hk7CG@7(Hvry@@1;3DcDNprf!BLlFma&a0ZTSBhj%gr -z3g}squW}O3O!{e}hX`;Q#k1eVoLz)U6RrP*s(U%0dH$0DtO?^lHxy#b -!U6z9!63IWuX}kBtuM$_23*@d|ca<9Sl!Im{a4ESvm|c|)H+bY2M8Wfm>>UvN_#+rXU2T6^mv3$=b`a -pnnK=Fxj%h}}Nt&gH0z8+W~1!-O%|1RL-^uP&AFS2n`3)Oh_~yn{?)mC~OMozoV7nZgm8BqHvCeVOUP^D}K&0e;6jt)aiasIx07a5cDa)IWwd6_ES82DVYV@~ -&Z^Ks3wkNub{_Ww&Aj+>O3`Ff*slo8|;a68`JXcrHhZ>`7P-Jvzt2V}hWO#K&BZq22jCGd}DA>#k+L_ -~AwXjOE;jiWwr?exAsF@Cg&Hux?Vl`b|a!51o{6$hlxnXNv_A?7bQ|P> -=-87S~(t58^2nFiVi#!I(nea*coB4MIk6v_uAb7*-(2mHk*N}F=@TF%iVOph`H5P -goOUhu680>95vf%)8wlLt?~*h+2pZNr^EIJ{2{=Ie!)w*nn-tS2i*xicA4nyAz -zHqJzysy_?ULe<>XkVgT6N$@;D$%#0+2RKgnjy1!{;YmIPl1asxGpxoZp$oirIN{5Y7*w) -qOoynQ)n>M}6T;%MoirTBIaN&>*iD -fBiZ9)X@4UBonAY$tM!g{K9&AJN8f7Bf6bs+h0yh{uEK3mL(4V#fPR)^a}+~b0&DjE{r&m< -e!np%yWqRhWhWP2kGTK4~9t-1DM1!jPF5o|+LhMjzAc%>)B@&E6X^p_cS83ERDeh -4m?QL7>megO8KflS%0wrd=|tK*vhkTh>(woCd9}0N#wS`aa0^JdY_BjnCb9qu!uYeD)t$mwgjH^J;W> -$o0r|BsGmQQRMZMyC1U7zO$x#SEp^tw3L?=vua{9vznnK@WZ>&F_-~?z5FSXgEv%lg$sNzk5u;y+SHGEhvJ@<6WU6x+nf|)8W-4)FtQ!z7DYaVyonm6gr(UVH@ -B#69TV_O?$r~dA9H8H=|?umj6LIdM>~xuo@J`Td$pbbY0Mri4o}U6%=|kwjqe0($F3kZoUZAKDp1~5g -;kCZ*5lSv=`&QBLo#iBR~jvG$O`rj*b*2Je$Sb9p7b!^@nJkvJtwFU7i~9mOqm3>0VbsqL5|4E -qo~LVq(P@l6G-9azja+(skYfvFJLgTRKscg$7M2GmkoXy_CCekl9WR^F53&Q$r7vZ@+gz<|_vAI?+b{ -Wxy7M=`XR7MDeyN2wMs3XQX!`lJ5>P)h>@6aWAK2mly|Wk{h5BzQm%0015}0018V003}la4%nWWo~3| -axY|Qb98KJVlQoBZfRy^b963ndCeQ!a@#icU0;F3n<42+Oq0IsxN$OG$JzBXwllW7?M&=dAQF;TQv^$ -pmNnCl-*XNC61>Q9;xDU!vqWd32mUVi`L^BjW!QP0$2V`*2k@Boc(h_k8%@ -TKcDhsO(G`~@$SdlKl&r%dbC&A)*o^EjMEiV#TDF!@+1(`z4ST8{wYlHzi0T>`>5g5^zSj^dUnq)~io -%%|obI=^F#Qv$wghAG>x8ILpal!$60r-G+s*-0Bz^xbT<-Fi4g0sgifdc#nN?HH|WU)cKG=+kJ;fNv} -JO+^oY3DwuVu+d&@|nWFzWtraVnqYAP8PTjR3uu*eh>t0P+((V4*;wI{2i`WB5yAQRn#KcXUK*2q@7W -s^aD-uj{cW@sAg#rv4aoCX1HcBavz`)f7?ubhjM`_qXfhb5<20B;)a9Rr%x8APoI!`xByro)2eLHLq4 -EMs79ye=$MANOC|}DtBv(ydVFzy^zSi!d3$`Wzq~*FX|$C)&exCE^{PV3(&ciqz2wmiuEiD2`dsF?94 -ABg-i%jjSx&@Z)(;*7Yttx5Qq|KQutzO-CBFf$&tu`bc$Hqw|;10L=r-K+ -wiY&m|%E~z5zK=pM+DBRQ!98$H2!n|N8D(rHz^A7W-*0PxsMo`G1ym!A2p0uZlxm%Te{wufhh$vYRi) -Z?44nfmC`4BD+#s!P^J@ykZyctnd%4z*Ne^P5YV%Jy41}cS%Q=W9NK63})bLyU(oP77*PN+IRp7*g)s -8jK(8AUJJD#d-aqL`_RgsyEO|I3%DOrFwZf&i9SmvS|!Gp#u?#LBK3D3A~96F^5V==21zGq+{EWqKJ@ -(@s4KjtiEPaJ>Rw-}HGB!Xi$b@WK(X;OC2#t$SVka(FrT30jXdvLJ#+#6WlB2w>i{2S!0Ra%;O*l1^f -b333oHxQ^G*~>RiTnn+{>2s`(tnLJA#2nGor}Y`oM$)nXexXA8^NxEmIRCLvCpx5%I(%$d;x2xjmQ|j -D14BHT-+)lxIcp_s3jx12n;SZU>qdK{RPbUTKRCp3N)RGQZ$U4uriQ&#shyd$rrLqJJ``#Kj7C$f@nD -?Bdg|X>4CjO1M^%Ygi2*rUGLc}8iSjUIhsi@Q+2|xOHMCEk%-|RkxI(Pttr*&ER`s1wjf8eY4Nl-{42 -i~@tEH^c7~K)oR3H+le+B~()0IpDH4{Sz-jrB?$;rX#@w=mo%P`4+6vAt-AM)w5EX#cF`SVytpb}Izh -O$^Zuc19xvMM5hzr$s@N_XH~aJR7UZ5-2tXj=LBH+^3;Lel- -3iv?=m63zV*-wxO$~9JAH~6>2GOQs2841-paX)Wg66_*voAQgp;Ysi;F0aRn0OHF`pw{WX2IFJaC%RVvM$zl~P89M^= -n9DN{=FJ?UW-KtRFdB2Tf>f&u*hlX#ZnYtg;R3xs!+A#GX7JVD_Z1d=Ete-=CeGsgN^L|nAdD-Fjy_$ -x-#+rprqS9y70FNvw$#7KsoU%E`ZKo(Ns;sL)EsDB&@hFOhq~53Uvu5PAgPlugLnrpBDS<5bV#W7>B* -_17o!pYqv|B@D0_Gua!;TpEWraC-Q(i>8!tAY6A?`4fK9*vYr@RzX%;Va%DaW>Y_jbkaQ30t -5jrStDl(rI2B_XIbS{p+KYoARG)8Q288?aJXU(bi>84O$58)9w^aJLjfINm9jj( -yDv$~+E-O>|g6eYY;MB5dZP!(6l3|<6|9;%8a$KABYPG!Unfmf<-)NiGBCBrpPH3QD`ciG_=|oS)-A- -{!WnZzEeLwn8_d(}FG!%C^*oR`T40$hJ*I!@0W?>liyvhKdKDw8=gr4qpQ3n9HqIvvc@A`T;wh-e6ly -gYgKEsk9{zR65)L(xEV`{DGaRqpA9XyVSFYjmK{dU!6)|34|Q-L^B5M#k_zjBP_^;RaeEouv=+5!3@V -G{CXY`0|hL`B-IdtDETFHW%RCTu!b4i)@$(nA^0cK?{DX(1NkZs?lgLSVa>VNsMV6!Yr9{rFOy!@5(U -mfmBAUL{{pT7dYjacO{KqUdgyhArtU+Jqokl8_yz_$!wxPi4vZ&XbGu91Sf(lbp80$OgR^9iec`Az1x -ZI5W?AeOr=~cu|0kdk>BxC9I$BhWX~HzW3FOpU$PEF?_#+9LIs@GhX0eh-lPsTy*BBKw~zOr -ERb(;ehSXp|z7Xsw#)fNW=zvnYsGK=mry$XKEhWeKjmUDN*%6t87wDOaz%YdurXI-#putl#VBOEm*FF -Xruucti~?ZZ5oqBCh^@%f=s5nStUAzS%a!YG;Sb}-dV~~pA9@xkA|rhUut$MFgZ98Qr5cPjINFZh;FO -o2^K6cM#o~0nLBQVhUnc7HfJHLDGQ#dl-nL~Oa+qex#L@(dkEy~y7?A}t! -^(^CT;D1S@$}w9cZih9*dN3fcR8$(nBmI>;)L!Z@al$`+5xcj~;vA!r8vyaSn2yinPG=qHaTDs01&fr -BBZq`q8&@y+8b}!}1Y@*9~bj=Y*_oMgG@@taG}%dsxVFqLnuGz6Cn^dPbH#<%rT7UhU1$@rmqNFo$2AK_pi0E<`i=qk9>6R{2!qA^ZS)vU;e_XM=5=uy2q>i{fd8SPsHG`6lp -q1lN%?$viSgNL+#wr=T|9jnoN*OgTW~Mz}F6*H}|scMsXK*BOZl2HzLPtj)AH+o$xH -h^qA^>qyP}1DCF^Ks(I4Zl%jdsD-w^0_|o~|UyJ|t~)5il9h7bA7&Ic~3`N8Icc4bJ9Pa&Is&W9>Zy# -)G-?w3IN^Ke&Hr3xMY}Cz_Y7K!#8e&xOD31(@drt+thU#|k3A^spbVAOI$qNkSHeZpP|fom;oLw93nk -Q;PLS^g3^X^y%C9p&z(?+?!yb*@7Khx4sv#h9;KG^)s;8oFmP{yllQRte;)genODBBL$lS -d(f^9BRk({Fn^S^*3;DhewY7}!AR7cz$I{t2BQ{U@qRR+9HT2$V8==iiuiEX~3zFctO-GI`fB7I+hnw -4O&Um+bz1mPJU1!7K;NB+lIYxU-8JhgSRF`4x=ONJRq5yIUtT1CKv~ie{s$X6p@3ke(pWpd%;6Fsk_!cHpf< -uxC4^_%DtdF+6r@^}N1Dn&-w37oqzw;2e^Y_GQPQs1Vv;BByWhU!G(I;eH&1shiS+SkP(qEa;!HaNmo -2T*+oy_?SaC5Fa;`B7Ei!^Cv6XC70N3vuK(=x;vbYj)ULc=&%5lw0Bvs#%g(#t3M1tY6-+OgKfGhJpX -tfrvKGUTqhmTqltj@QLWxT+e1XMl&xy{_mN(#-^4_M@u0w*{WuK;rafXS5L040w3W8yTMfm>9Bz|cHL -OlQ#elK|4>T<1QY-O00;mWhh<1T5v~G_2LJ$y7ytks0001RX>c!Jc4cm4Z*nhWX>)XJX<{#JVRCC_a& -sS}Lt8A_z662FShUSq7IjG~i8u7W?;JiPQlfK<;R;AhQ -Rn?8Ps#bKbFvZ@Z#GNP>Gk=4aA$TpJ0|;AST$C!eTPKx_>pkSl8*xfBf&B6})#Oe(VHD=E%v+H5+yVWeVf-hiCEA)l^3k#D -~Jt^hi-8Q-@;0xzZgQYgPthkn`CR0FsDY_^tSPl}b;w^S4V)o5M9TWz#43NMHPJ)2Fb4=qy;uhYEtSi -C78LGszGENNYrCAlPb8E-U`tzB+umXk{lKT$VN{=ei%Wp1V~JUH&YPsPY6;c -7=1s2NHF?7X4Hs~%#y%6fJWa_O8;v@B_%Nu8>IeXO$!bcPpm4@A#7w{2&>Y^6Oe<~!oLRGTuePeEQ9L -+E<01jnaaqS!EpN}Z`R^&d2v5ro{|=_0|GCiOt>+3FP_qSr48-JtBsHIiz?Xo^6XZE$^@eG}icL -W-ihmauuNNjQO{zm00j5u^C$6`lWD`x+9R&HHP5@e{w&ps|NVXtv=XY-cWip}4hVIeDLKV0QP2_YdT_ -zz_Epzvl&*v#vFaVUfF%SF-Fs3?a=j+1Wh=hstXdFsf+ASlYaw>v`L4zI -*INjNJ#AZvydudL1uPs<$OylJO7+*57)Xgut{A|e7K5q~isf3;4VuHLE7ohp^btjWW+uWhAArr;tR3m -6EnjUhd{?wHxk5=R1?0s(ExTu1)~o@!-o6#Ev{MbiPFe7;Mc?ToX#x#d)58trHqY1Jv5ft$BQpeN>5_g%0$nJrhe~F;AJWSDzCoggODX?jHm0rEF -bmtKQdtR|tlbZt+qvtEM5+IULgyx7yeYEWL1F);kCfNgfah6k+**CLC5_4}QI{!$PK|j|K|| -4G*N7*wg+FNOKGVnYeg*e -HcD7DiuIP0HOc7g@uYk*na9j%~;94`%#R6KgR?H6&VWUikue<_h{Y1f!{gx|98N%XB`~vX1rkch-G7!>XhX&3aLHx5~&k-= -UD7b3}dGjT=}4&8+vWYVvK+I=v^qP99P5!lUe2Qu`R740y88+rnuE1Gl9~vkD2%U%ae&~dMZsNT|bQ -46;3lRx7?$JF5QbA&9EOQIK@r)A2A&D@d@Am(}?2ePKMNkH`^(te-YPGEH(Pa>17+ -f(#h(8v@Ap;C{p(6kQE -|&lR9{>OVaA|NaUv_0~WN&gWWNCABY-wUIZDn*}WMOn+E^vA6J%4xH#*P2$Q%tS;k?ZVkE!DldZmVk* -d!6WF%YKraI8{`dT8<=EyX1!CN>&qp_X9A$IYaJBPG5U{r`N_3IfKDqFc<&>fZJ^JWW;7=mKT>3wrVa -$|AkMswzsy~^KyAt<(F3ti=PkJ?$e!Jc5=np+q>qfEZA{*(cGpLXD`cDk)=&u7Gvmi%sG2?@O*lBJRL -V58 -}ORc(q)XRa3KcK4%N7!y<);@y__^knN0j@ISjlLSc9Ol<|*CUgb34d@x1~wl1o2!II=+)vPL>BrIPbT -(&B5APGU5dH@vFh*Cdf`XXRpH$zs_Z%K2v{9{AE%U+ALx&}b1TVBA-mKAU -0>s4ds0zIpA)+Fb5$u++lvP<41^h{2CEHP5;@qInJf~6ed0zG^7H2HRVbbRpUFgcoTZQ-nW#rD+n$I$ -*2J&Tj1NEbl&0T5y~PwQH*{_DK1q3yJ)$|`PCWiZ)d@B<6D4@CTNl~+77Y>?M%A$sEqw*fydi;HTy!O<+ibZyhv~5Ww+CWtB3;XD1+1MUufJ4>F7$k+g~k)rw# -63b{ZMXw1GYm;7QipWm_D9E8L!+4t-Cl9nq21^s3Ls -bLCN0Nt-#)q(r@L -%iE+j-icK#Yc4%>9XuR|}8^ci8F$uV+S|BHn%f2j`Es(F1kT*MJd)$mIGjn5j@UM-PqZq}) -*zkV;Y9;eInBb?VoI@haabH=)n0-$}~XiqOR_|T=V4Zb!=^QF;(`5ayjlX&^4L0^!|%kuUE$vDHO5J -dEF?A#X{*fYV-?WvsptbE4I@}WqipCQfuK4d@HR5RB}me$NmKVDymf#oJGwSk!rD#hX{sOak -b#Nlh8hiQRuCLY^Ms+?3S-lo(`uyMf{#V6Po75lR-H8RgV0E4d4E -KORDw)458?evUhbVNDY?&prS+dZ149nfgIpG(PIqNL=Db~EBbIIC;+Bbe=Y3~LjzR+4bei`WA{K(Vvq -i5#!p>C-d8kmw|1zcSLR8|qUFsw1&n*G1$x2jGx0GZnlqnI5@&RpdXy_Eb`$7AT2H)?kG0D0?wE8$=0CtEW%!ne=`Cd$sM@|qV+Dz;LNov -#`O>g#pQZu91f9HLEG&T9w44z4%I!1SifGw01NfceO44HUuQX!E>jU@B==lG<$b!C9qQhR&c0jy0a5> -r|`*41$gmR`6RzbK2H|y|9J;bJzY8sBQ>OG@6yGc~(RXUFB+yt@JmEFlYQ;+SLNZ7NK%i80RX=ZUblCP9NOS{N -&B60l>f_Xdt{%ZavgY0pjCrd>2M_S_`kj)^j9;U%kzMsO`Y(WX>ZUy`9|GU>1bqouTbbhDQECt67lxo -H)$38NZqqQq+(G?iL#m%?eVA<$ -@!f{j1%w1$EI$s7EiUH*dCJ(=TRry2NBYq2it`})H-?h_SMuTnLRqX8K$qMucwD -6$LbJ>Es2Ql-yI%2fAhiyVcb%#->Jjlg+N{(93P8IlkyygkMZHlgICkz{g*9gLUj7q{n202(chnrem_ -3@!uSmEmXB`h;NaHgFg_S6jd_UX5?y}c<2^Dejir1fPCit8P`Fe%s2X&Y1#N}yqV8_QNqKPLfFlhCOx -K}0LQ|$*jbOIvQd}M_v~iug{cQ!~B`8_vC5%XbQ4vw*0BRc53e?IHRhMFv&X-qdv0Cs7G!N(%jUx-r> -?*~JQoL5h3&4uQ(K!P=geJ!D3KDdvXcUbZ+t0Eb=bPe{6bxg5sh7|TZ{KY5Q(ot?a}F!Roq-AKJ*F3M -T(8b!7-dLrjAf-AB%!8?*b?bDV#qk-XfSZ}l*jexaUHS8%w1}8Vf%_-rn5VxpTvT{%&KF+z?_;GJEcS -`yyA17)?6;OzR=fty}HAvO0|0 -6k!)g}BFEL_IKz+Y@GNkVKE9uMjZSS0`miRkN^a;DnL6sNpM)Uld%k3ow)?GoJMl%zJRR9+0atVw31e -rR8?EnJ7>aJReJ9XJevjsh*l~y#C%K6Eg7jIZr&S-)(4LJ;5;UXEOM+W^)>1y7f-MwWTQAu9pO_Iblp -I;1#1bgVX88SIt`IiJL{_>nm62HAjjuE*oqEiH`A!=Y}AbwV0fsvz#{gLgqbab-8PVSam+FcG5nwpv_ -?nC}@Hec1SG?teECpAu_6Ao1EPwpG7q0n`+@qd}F!lB2DSx)Z}Q@4d&m_11Wd)CBG(&2<1-PuV5(k#Y -y24!*+s5!3hO{mAReG7`BPCpdsa+Zw;egn`EleTpd4-5+h-(ydU3G9V9_3fH7lva+%)$U-8l~6Q>crI -C1bCncT=#I3#sN(gQI>sQ`ChT$jLtzUy>R}IpryjaG^|l}h>lt{2BQ%YL0Ji$qf&M;fHkew1QLSuf8K -3uIL&Qkr@gV31f(HmGPVU$}9-8XTWdeZiE{}m{b_1tS5(Y{N?ZwbWRquRiC~w;ua>nAoFWg;QR-Ux!n -q#}KjfB%Why}VWBm&lrOweth#W4Oz!s9>YSjDq8N}|DdUf%L59<=9bxd1c=se{}gEv0EfEU_MIOHapR -PCa&X&ER}2v8OZu<;J(L5w6u_4w`3zp*Q(QiBg-d^9!*IZJ|Jy1hyN05X2@FDkC(QAMoZ+?hnnGpl+@ -Q69nOI__S%NE)I2gL$6$LSnPl5ZXU5e_8X!7a&MPx%cK0Q4!x3(x3&d_&KU$ou$^}F{ -xb`5NA|5(3osAHx;#(v}j2c2ge0$@w^YyhmL3Dffp73ws?E~0y~xRsa@$`l$Fq4^b6X`b-EAG=0TUIfxii&8eF<~*B7db0 -7S=sGR2~3zsvW;0<$*dg8)}<{XMRiplrp_5KL -ksroP@5&CfhgT74s|9NxAd&PHAW_0JkxW9M{h`h6yP2yY0sGaSImg2!2;$yi;dSc2P^?{%0` -mDfUnVPYR~b59zN4sWXZxwh>QaN*Sj5~uK*T6Zf@F+~@B<`{1hFlU|;%My}5 -yp;G@Z*5QY3j?UmV%~tjJA9Ndla=lYrf8wME%bMh+w?Pss6h?82+pmK|RW?rjBDY+Up{@9ya^9Dr^luM(>LXN)D`eS{1!cO5& -#x4V=577yg1wn-tqDnb^Nu(d1cNPF_+`ZldDYgYms^Cql^pin@6Bdk0oC1Ie!2LktjHkN>8so-C7IB) -ZkLOsa2_yPTl(#i8-f-1V|FTx8xe8AX=4gbJ!o`Cjf{Bgobucf{EX0%1Ld(;kWQ>@7pRc7|2OlBGfFh5vZ!QzbAyBpu*<5(r;vrI!4t4_kT-%%lFLu8&-!e00tpkwLF}7UBQm$%=X=%)^h8jLUG`usHA>)u6t3Bs`!mRO^d(O*VWswFs&g*G`;w*bvR< -X*6{BHl~@Zj*P*6=E1!Qv(5_HYj`ef3~6F=Oja!qdt$fxY+wV>0v_P42pfRRg>p<+E!cf$3sX9A(dT!=LDJHDQk{HD}@T5gIK#9t2Vd>>qoX -KytKiVFfK-9=v}&6-ks8$L6)VjT@4K42eX!)_-bon7v%>D2@$RtAnN>m4Np#gdh7o5r$ -D;IjCpKS~dn+$Cw^-WQR_lFWWWAf|5SRNIN1HVvztwX(>hlZNkC$WM%L0Z?i)M@lrvQM@>86ELX>li( -UziPFGE{O<`~6mhWr5BhLngC(yS%p9`}glzeT5YihjZ8Lr1| -wc0w~sxMkY;tAh!mp-WYq->I?#?G+{vmIU*cR3fAJ5<9pjgrqSA#fY}({yjZ&L5aJINVV{)XkcsvMn+ -Ij7`akp(-z}9*%40B4j5t3MW0Zbt*@(pE-|DAiu2PfNs(m84+ZDK#Vj~NR*QM+oF`q*Q1Yj{iHbxFv7 -0=0v5wDmin`-<0Iv5`wK}MxtkcJT5v|(E6%4$f#6oAmg+v*WD*{aCxyVk8W6$mwhUYriNOOwYdhtfR) -~Uny9Q(6|Rza14Rn_j#kS2pa&gpS!E^sK6F2o4odm{Y9EK{!@tk_!Zy6C&Age@lEBF>~3i=a`*Cu*^G -|5icfZ%TAms)Glw5oF_@5CwT}l&YXg%VYRb_~$_`*QOwhP$og@ff!iT;@)LdWUv~uvf?9(*qNp9qHgZ -yRzx2#3Y7%9U{`lbIKxJZ{3Af2rg1*zW2UoM?8aK^4Z@zrjZQNeotNxTVw`K&#(wV5FpxXcV(nOD5vj -5Z-dhV)CxqI>o~tl)uZ_v%0N7HU&=Pb43=i6H1lksBxA_bfm0<7^2Q@n#Igv-+euuzDk1a)Si$n+vPI7j5YfA#CBsj`D*Ygrz)y;=~^%zYATEn|IhK -8Lm|C%cd8?)%Mg$sj4IE_2SZpdc3Y)Ov|8M3I)bBIegG$*UBO;2GE7lITLqSCB+RZ>4cxJw4E>_%qKw -to`zO!8o*uO_KmYne_C@@|82&qW@@rMH0?F9b{`BeS_xq#2oW=5&WIQ^1q8<+Z^y>)4-S|L4@&^wTi^ -Yb=?_R&&Kl;97)bUR{!`-LP{_~Tc -!r7S=fFK+UxEa(tX(>VVA@MQme^6mbs16t|UA_t0Z#9iBG2#ap7x?&J}%M5J}=-?b5Y`BZXRjn-&>LM -8}N$PnCv`A>d64i=DiK^Nwyit*Ru>ISog36@iP$p+0?dHvMJhx>f>}7WT2mB9taj=Lsj~$2d+B{4zK- -nNgu0pI&A%Uno%^?d#d3ckNt(`_Pmv3D~ye^D130pbfiY;0Q?)BoSrWf}GjfECQk{0>EM^t2hV~n^;K -{>|&s+x7_sbT<`j~fU-kc<{~b6`)IWE$v?_s;TGEPSuJ%?@DO$n4dnq4bR=B4?GY?{GfLNs>aBaZCb;y(oY66WmN -5y-+P2<6&153gHOojxA^ab?-vC$-YHBtz$dk&o(ec!zXJuBg;xo(xU@PUVyp@8YRUHn?YHeyHP4};9*M#cTY({r=DBHp0rOYQLmjYEHj_`;0>NYL9po -l26l5!@~OIX96W$uz|>hM#aToC-S*_BUgoPL3*QL%&~H3A|oKv|VFIx%R~IPWbtz2)(?oz+TzP&O=mbFo}qzKiR4vslV+SJ*~U7$W;)jOSn95yQ8fzLN`4btBIqEcC>Lc -%#Jof%_ZEtl0R*<#3vHEAkhUBm3!$Nid0wi7D;Zs#r216(D~ZBO+aLwnoMm(>vC1ixU7d2w<}MR`qd& -+TQ3fk?zZ}UQVQkVnyJA%kaSH+(OT`qNO&VF1?%eyM&pi7k+nsvBSG-%`rZ~_`z1=H&yi=Y1fO4{@_J -35I1%s3v(i5MZP^k$59in2ssa80e~NH4y0j&2^4bdg$8%OZitiH;rJ>sR(M9t7dXv;eb^rVTq}bS&a(!2k;Z-qIjto%&J=`;R>crAz0iwtbG*c^Nzy^Zru*8TMe@u~LaR& -oQTW--IW1SE|=j&9F2%pCcz(PjP>mZ^|_g>degk}W*R8>*vGD0lH12lC12brQ>+R3RD6U+~68IFQiLc -8%_Tfa&E(Yg&W30T1Tn}A&KNU!VZbbfv22_gJx3!-0Ew^*lH7-^!ejzbuB)PdMogJ_-iz>U(+>Kyw+_ -S2_M&1-{$Jv;Tz@bH8jvTt^HixbdBymfYmkr!xnQns7Ay<-}txaXLg9C~_J_Q;H$bIPKsDV-f!)O#lg -;E&SaWtM32lRfT@6@dmCn`$Nw?i<_L4 -M{{b;r-!Sc6gBZV2soguyH!!$_G4o_!INcLr3-=Zgzb~>3BlJT;F7~RJ3?12C@c;#9nt{yz -!gSEsJ0D$Qc2!CBUTnB9tNVBje?#41{46{uomz+S2-}eZtOY`3QBPUAUY;*eVmEoI?690hgM^=eM(f@ -WIwiqqa?|m^}XI$J_8g_WvRTec;rXeWn$amiiM6A;RKD;{>;SJc`QPhS6$wbRRN5ms$ho?m;%g^3n9gsab}F#8XUi(7S -vnQ=hCEcfyhkZm_i0z1v-(KT6i`uP`*wU!{&kKay`j^|g2Q@}M9!IEz7r)&Bi6pYNsR6N{S&v;;DKga -6>|K}fwvM5D>$dV;4KM(_pD<7WNBD2jLLBUK$thU_Kwx2YT{GUCaTw7I}eH&q4E>a -fx&XFf8+P&Z&wZj+&X8CcTZ8giA#ywT7hs?%eQCW1WU5hJ -xgRf(c^d-K7qLoez1@sgffIiC*_y^?|wj7!2EJ$jw^GqJi|RUh1&0s91K%_XgCk -oInziAuk<&IS)fH>m4@!CV^!ahc$)`66rcrcGIDvrQQ`hB57uzPf1Kx)>EJc{J<$Qa5uyuNDw(I2xcG -6lm8iuVjZ)vbbg~=jA=j5bAvGy%NiKT9CII@H@RW68-vCJQK%@^(}H+nWCz9i2rF6wF)^8y%qXM+|Y~R8Mtz{1o2s1hrde<6D!rK(MCY(l-FlIc^w*_{$zh8PWJia6Tx -w^5GSiAW=D#EdM8WPBr>|s0~`L;^sD{n7>E7#t@yx}pD0NT^dhx#!- -V{A6n?T~SyOgMGU-d3@fzdon+o+j`LFje2W3IDeZj~b!cr0)1GW~5W)#tmaqKrMZmdSVwrM2;-neZQd -aV#MDpoxg(F}T^(FNFaESGR7S2EbcZ21L{vegkxYAg3m3;cKu?oC6=ezj$mvg#{z2u+otw>WdRIza -H`5ehwV6D|_Z2-8ksM7!tvbYExlTvP$BxIolid%y$<_WjAJPqvR|9oNURqKbA9lYBGq?IWo!PrrrUTv -On$it#!e=AaxP5}DQV;ppYs&4IERNbrE@f(LCRqZajri(~coycmvvQkOC!m`EU^KhVn*46+))zYr8-< -g#ocubT@X;qvj#jPg`$4$QZmGgyjLW|5T6V)9@(~p4n{7!vkkG^z>ij4F$putZr;O1e6@LGhv1@ -0J7C$8D~}&80b5ziYIA3%X4DaAqXpU!SC(%8L7D8D5_=8H=M&*Q+kd45ee%95-OvlCsD}7{=s;MaM!# -2K@|kn-&~G8xDLcVi`ka*o%g|x8R0vH)A?+4ULth)IJwY%>fE8=r265Ryo8Ut+Uu}6@6 -aWAK2mly|Wk_?1qiTK&004X`001BW003}la4%nWWo~3|axY|Qb98KJVlQ%Kb8mHWV`XzLaCxm-dym_= -5&z$xg3aP!`RY|ScWqG^U9?G>6bq!uA&&yBVJNgk+sw+6Akw?*9PYb!X7~^($zJbWjUcPX%y2l*A4y% -X%kMAQs>)@#nX$cIU;YK3j4nnO>{V4C8oAkOmb{v|L -)U=o0p$Iz5nnL9|GNI-Bdf4W$V4(HzLcJ+|^a1LB52kh^&xWG`vuwk$th0O02ZIla;$Fs?7#w?rtmPu -Ik&(UdzJWs%><4@O1i63xD%X*5+zt6il67|By--a{2mUC2BN;eT_05$%?PGBG2lkszsv@Q??O0qlej& -D=}p?*V{~2_E|2QDSL&NqY+L5%zEu+lLG5odX{8a$#-C&$!IhpMxc6Em4dN%VgnYSJgXv`{b(SSc6zeHN4X;tICBdIu(_W7pL(TwiIYX6Gds48C5cjc*UwU4e3NOS+;5gyp4eDDuJ)r3v_3zI{1{dPX}jtVFDAx-tj`()CU&M -w!Fz1g!r-$J2ty)?Qe#nky;Cb7dys1V%C2Os1)IJY$YOPrzcGLV!)8HgsGLQK|@#rVosfD#8Az;5o`H -i0t6z~ai(jDWif4oR+j53@pBF{%7_@wDPY(a8U&+!|3(z*7+sVyXz0{b8oD$iVZnh?lbE}S4em%Bye5 -0J^6Znu3=~tt9Lh?2QmIh7WthZz;t%L{{8j*g@>Hkfi3g~(%8lHYXL9b^1X}o_Z>Er(EaDTslj>c4h^ -Q~?g55XJmMBP;2aN@;6G36rLM>lW;`HR)OU&R}ly;U=HojkuCyXn$?vIMUbxI?9y;B;#mwZNCuK~Qo3 -R*3{LjOG8089u^S;viJH}ejxXfEQHHgxb$Z{#FY!Af=c^nw}MMnGsPKo3~`IFy<4vgWH>z7au1R{PZ! -?ZX)qBMy2%<$`2}ERu?(VF;B~v*Sf^umwO%YK_YS+bhr>VFmQLy=D#g93H?6PQ2X;@`h&l1)8daQFpu -qT~y@gG@1#76~ZAdOES_MfGwhpnU^pbKvB5@4R#Q2aw!4a9NHquF)E>t@5mD;3S<;&WDLWXw)N4Pgv` -O2gCZE0n1oD1o-OZWwa4jjjrS&WP*^GMt-!YQKp-0Rpc_iFPEZm8I3pmBs_X*qT -w`?Td&-+6i_4OWEk|SP4IwOy6ol>zB{-UsARLZXy0g6md!o`2md-KJ^}B-=1>LqG*6h}bmPRuI;qP>l0s)@eK!0JUKe!7l!EHKU -ac7BT7+4JIyS6uL%8sJ;_THy;*b35&|d2-%)hhTZ -Bbjt*&RH5bR}SgVc@p!NpLGGP~}+2SX6E%*2Y;(9{L}xwAgHk0M)1Dc`=8>gdXt4WXSAO$oTN8EcID4B-N7+x^C1-)mX<=J37K -~NB}Z1=8GAKJsxtv+Zd8t6C`{$O2^g^R>vbi<|Xh$LvCrn}7>)3NdYMc=CKw*m*{Q7|}R$9ZtMfox~k -+x^JF1QGItc#lhee7YtgNQ{Ti|CMzILC3deAZhPhbb0zs)`YE9yrw-1vC@`nPov(pt7Y_jyn#90`P4M -fHMLJ;;A;1<3V>YK>^b{AgR8_+wgN8z|NV4ec_mai)_~DKIA?@qVPOjh7x9|k+D8qs!XBzUEC!JQ>LU -yLJ2-yJ6+z=bL&V1E0pRO3)U3m%;S2V0bL2SB+5BBt*1W!Y-V>Lo9+@L&(8_&ehvPxf~0ypn -XShgB@o+lyPLF*jP+z851*3OY*G5>M#WElL!p`ViICcCe@>0O(SL5`2oFGyD%> -A2E5E)xS{C|=zl7kpjeA75xXkO=UhTY6oAy&b1o+dcwR?`JESlx4UnZmXbIWM5H--vPyy!WBk%WQ>UUNk;ix@+EC`nDgx@z5xre?^w^LKA6H$d#l3XNHeeji>8qjpL1vVez!YhK -4|D$uJm7Y`w8bx9}Q)eETbchAcr$hI%`iUo|wr9dCd{AJs|KW+4lQ5+Apw&vxA@U{Fcmi+)my8PW1%@ -Z&APkxICEP-&WPFqR-M4bG^fSYSSLG+OOsaZ91Bu1Num%b2iL1N0hbasx7(yaNx}7^pQF17lg>yo!6; -5xayvL*!|*hx4@)rV$oN3Jp8Po6R%OE#u4~GP)h>@6aWAK2mly|Wk_JbF+~Xy004J80015U003}la4% -nWWo~3|axY|Qb98KJVlQ)Ja%pgMb1rastsDJw+qU(0{S}BbAE`#7^Rf0iUzN^H?K|z<#1}hlwj|?0Bq -SrINDcsPtG7RX?*SkQkd*CoWpar~0uS%|3*dyzznPPyNO`_FB9&UtzsDz|$!J1e7v)ay&6OhI>nVBh? -D@+%{PQjOmFILvF6b9tMlkG*CoGpNC8{9mijni3x+-#VQLNQ16^xt~Ri0AC;Tim3f>2m5SAwr9Rlwaw -Ft%m6LRc4!k+bFNlQ$P9k-Afa<|%po_RZzHK24E|(n;~~@7~BFLSF;i6UuFtRDn(Zrn-QMJH!Q!Ijz&m1aClL(AcCQ1`d -JvqIo+~2hEeg)C#T2n&o1L%j^8X#PcANDLJ$O_AAY>sW(3xjAgsmsdGu^dYI+yr_m`*h@5jJ75DLwTo -Gr#XCdWU%8vQ_}WZPA?BQQUgi*Y6LBbi*WEtT^vPedU>8Ro#+BPzGi&GRwY(wwiERKL25y&91Q)G`Hb -aJBQ$!9U{>DMbN{mW4PL$rVSjR06R1@cno25~*B_4I$C05YS!FeH78?bouV$GCo_rIf>t$fKCaf%?XeSSHZt0Poi&14f*!RkI&|Ri#~r7e -k7mN=jfa1kADfKql?$?mgkr8<;gGSXUCT(MkyzI92B5gYcyHF$T;3ou!A@_(iYR&?+mS8KT8?RK|9TE -Shy|H3Tm$!*I0sOo7YF48|c9?4*-!2w|%e4dZ$DeMjeCz9z1e -d1`O)rRH&sS3p$w;Q%Ut^R;Y2Ji=D*1l>PJ^`P1%>Zc#iMNbsaE1;vt#$^^PpE`9)L&AF91_<%K^9Bs{$fl{RRkc@du8h#v-& -Z@KyD$dS+az$ZG4oGw}P1yD+cQD&?>p=?=7Ba%Xsl#SjNPZV -gQS#pNFZ6~6Xj4P^=lvIeiM#N9WNxV7G}oaL@CrkWEy0KApTV>_G$J>fO%6#Nm2APgJ_jgKOPvZb@TEC9O*V$iw|QCMPw;HFUgJx_^U$o^%Qjq3Wy!^Wi -Vgxae(SkK&F$&^L~4cuGr(75S&zXTw^}=<`uZ7?39-dw70|#KYrPp{7;dzcP>&j4=FJYVMum(LUr(Os -q<4Dna$a^-Upl51CCgo*U>dV7`-2@uu1k;-O~V&ct%8#$K}Rq;F$&*aoCtwMMHoRPobWuxOptb2#zH6gzh;+hKVA?UF2kES-kkef4>n)> -97=hYo*8w4a7_2Afg0F`dkf4SB#q733ubfH0@d7cYkkx;uZuwEiC($Gr<_sXvm$Q!w3eyWrew-CUm5z -EusDl~bRb`NC@6Y?IDF9B66!(5QLEP=nj@jNYVcH1JM*SZh;xMljPZ?uIT_p -ps>bG#;b~FY1qwXUDW&zO*r-8+I~&=fcNV`^;~d^7%qm-!@%Zo!E~@~)Lhox8xi|(%+DAibQqj`IB_G -g&U^n4gnKCMU-_ryo1d5O2FPeIH@JuULHv61?qd1&%|ooX7Y)OJsjHn -XD_}Z|aIFldTvJi;*s6070WuN$hs$E?6`y4kx;pg~LD(4_r*07Q<2$x#^yO^Ui?plCDiL&2kNLEgv-AC6A-#6Wvd>|?c&!DDtayvnluQy!uB&S>&@FNO#LY{S?n!#w!dBI;XTP&AYohpryFxEFzbV*srJtjRES)Edr -e$EJ%;)!f2)yrVkN>oFgk&E&%L=DTsJ024aInFA7iH+I3s4!&ZRuv^`ab9Q{% -1W(r>_soSWt9KztXHSGx|X#aG4k$h-sZF`s5fJ((%uX9q?6Fp4j8omZY<0N|H({08r;VBt$c(&h)+@r -n9f*v9o@&kSdDE_g(hI6EHnPk1vS9lB6S@8c4VD`@~HfN9o?R2c$QfxQVR^N}y;{?evm=1j4q{+UP1meWVEoQnr%% -$%M5icOX{fR{dxM}&@7>ipUd%xk@vwUOv{~khRWL*a;(<0jq)RJ8eXx*$IhJosKYI;5CM*!)zd! -&Lu27$cZP7nRpI=8Z$-k}*gBM1ch8NaXl%m2PM+b%FRZ1(3OAlKIgf0f^P8K+cH#96O_S@{imk`6i@` -82dX9TZg!wgG|Zqi*p*o}u;7$G@nQX#jSLF25OUl)Se);VIRl93~`RtlP&!4`k8kw`EX&yy^h?I?yrj -X~}!<+jqf%)NKc_ZTqk{crc!5SvbvFBlCF0b4c9vvzm(g#Ew^YyKi`{39&6KTnBx=S3Z!rt{?QNm6aA -Q2&3N)!fStLOjF5KSAaPIgxHKhXkfS$Pl7(hc%^Gc0QHRfq<|MzqE=Vf9hXXm2FxA}zzf -C;wgv60hc^6t1AdTZ*o)HCuYnH)GWxEvlF`8X}dtHcdFf^3xvn^i`cQNuUDXP^^Pt4D+Zx8<6vW~0>d+i5;VrXEs{D7G4!J -^x|5GL0k=)(|=!-L1V`gPOY$Hq4){@m^f>vP4;HkoW_2%CxulI#Il^&Cm0yud#oQw${9__un4jKc&|@ -)U(2mcDc`;9i2Lfim)S`r)|7wGc(*R^b<3C%*@Ha_B3hTY8VUV){gdd!XPqYF!uJi+M^vJ#`hqRkDq% -n6W{ciW8nq19UXw3>Nt$g`^@(7#W8!+zqSw1W=3^S+Ya`c*)5NnrdjN&K?R1QtsPUm-|k$8Rhhiv~2wYkf$kCIx19 -UeY5$?X-1JxKLtNqe@zbLF*b%Ng!>)#TRB%4fbG+YtmZ3_bdRBwo#q%Lw#-(QsskxzJe9zsyEz^lnTsknCo1N1uO&kj0Z21)%K`|;z#c-Aq!L*8uNg~M)Eu9@)r~j -P74(~T2H$&HXKU>|S>Es{5UYOY%*C+V2?`leXMTQ^svBCaF2=THe{^ZkPEWbo7H#|_WU}o-wu -9UV$lum2l5Dok6ZPS>u*c7GtoViHtK^eJe1;T9+(ZD?G}_%pYI5#Gcu?|nlBwi!fQ)aa)rj|o^*}$)3 -BwoIbpF1K8a6wz(RMDr#OQ8ddJ2e4mZ;C$&SMRohDk{Y0@CLs#iX?t#bSOCIMqMV=eE@Q-_rBma9@r?_@=Sm12Vz5@7KshZ@c=KpcEB0YBb*FWM( -=JzmhEV|x-!LZy@Rcs?NC>$jUxvo{Mag|VfUij%qK1G7X@Gj+u6N_BwY~?ePQrNwPE%(+Mqu{ -@JtU|6+WJOl1|7DFzK{AqUe4G9Z!G9xs}xa08mQ<1QY-O00;mWhh<3O{xsmHssI20Tmb+Z0001RX>c! -Jc4cm4Z*nhWX>)XJX<{#QGcqn^cxCLpe_#~VwKzWeBgrJ$WEMy;{0^Q3_y?UU^Wz@MX;0wQekoqW-MyS`$Wxfm!iFi*L@1(^ ->Sn0Hx*X*$X<GZ1)>W8QbfUvtMjv8NnHJT3xDO2?qKvsH#^59^pm0agkrZ$cqPav;>zo|FnZ4Z>2+A$l)T-XJewp -XHavQBMwHc?6>`y+>ypGha*svT*-QDYOuWD#qCcCWtTFpWn^(C%@GLk<6g!nIo-|*MmZj=Fvw1}FdQr -^>Q~;8qpNDM1hH#|$LoM9gU)`N>Rmk!%8(lSrN(aZ7Sv)pY>#q}gkk18m}MkiRaKwSqYC|GU<9x?c&E -YpG090%`Ug~1JXj!AcS+8;J=$oJ8sk-NfQ=*AI~w>aW+22W=#0RUSF$jQmAi>e#jN@EC_h2&_NCMW8{ -a+qwA{!RMorH6V{$i?83TTRv&U+7lkqUCC1;Nel`>5Cg_^EddPBGQ4dt~2DDM`wuS6{+ -@m4y8t9WGbh*fl813vTboTgCpvD>ieoSivR!wdIJV0y2gEI<+&O)K1@a(-tTd$9CV^zIYS{3xkLt+*H -#Nyk=h5FjPEl|FTbX}x)36{-M>sBh+^|JNlry!x;K7voHut(Q_p_n9RHx~5>v|4s{*Q}yL>z2FKTq_K -@copL_`V3GXkPqupa~((r-GN@gqUO#)fOx84D3{ynv;jq88@b&(p_Bmb4<2as!x*v8 -h2^QY7ZlWn1Tao!YfJ0x?Dgj}QsAZ($UdMQ2qUYs+9H~aFH6yFpvWUg7}BKu59!=MGWjibfR(BP-i%JMq$=Mlo;v% -e)bjr^=$xDzwf=lfokhg0hvPzc~fd!s1iu8|yVn^3WMdV2c$!$u3w~4{E%nCvZ8A+F9i(<|f^JNx-jq!_Xw;gTXOsj^>UVG%s6<*$N)5ifLX{E6Em -mSD|{kVlN52>2$FC1XMMQgM`Sy@C?|N!~0+6cemwuEB;b!C4y*sKQk~OI>lw=Dbpd&#EG+PzJKEX8}_O*`bnCMgBex0LT*~aqK`K04i8n8v$jZl!@_}$XMK -%YTVBM0>i=Ryjf0inn)5N&Sv-URL3`H8H>mmDs=5y;H!{)bF*+O*@(wYw%R7n})k53_PXoETn>mn0?( -uR-}?Vr87VM_DBYN&hTSo!V3L*a8NV%WNj9Gc89%}Xr|&{vI;A-3ha)m#@~cL*_w@tnrdV#w+u|1lYf -*V0^}!2^iXB8Ku^T9&~awCDD4!=w^gVP5lUi@H@+3&YS+TF|}uG`p7fmLYj7SuFBgPU9#&4{hX4-voa -P|G}znsRAlnF5gnX+D^9KT43orfC@f8x-~;QV7TE;zCk1g{st{pI4u%$!$^;OA^bPi^bx+SrjL|yt6= -%#vlBTSkVE)tWdvdWE@#-@d`%q~*kU>-o>6V@;0S)q7!1vPhkR2%Kwo)-!jB2ZtPr$7(5M_*6@pdH+Bt)_VN^5H>%jk~%3Zp`T`w^)EJgH|Db#ItgF -9?0)XTGZKt+9@z<{|8W!4#bzqo3D@!!GO`Qy_6AU2?2Y~0&tHKEk2)J=A0bZWU&An0Z!sI@85-k --9{~@%y0Kqw?2ZKh%3j;kaI4tZ!|;<{241$+cFg|%hIBdH2Z}&gNs}WOh!h@BGWTM#y?vumj@X_38&? -9uiNh)a>UxcfgbEYN)X#9PtT8y_h3!{6`;`JWd9wgOFm}1v*w64&{Zl{8bNx*F~Q+siQyVrIJqYqaXkfE9eWU=mRj=VXd%q*KEWFEYcW=(3s#?QTz&pM8-)`|=qwhj{ -B^}b4nMtE$P=!irE^fa8w-DGhE{k^c5)#1WZ^)P-(=HeA&<;Mu~ -a_T3uf#?eve@Iu2oZ1L9)%qng^-HYrq*RLwWg~09fJwHL)8q&vpkVHY$!sNTWb8Fds(rgA7GTxI}QBEoh+aI40cmUiCVRd|x#05IxDy13g89Tf8V;7Kb##u -qu)CR;cK5gG$?iJSwPwwi1uLuz8RCF}PZ0-AVW}+`4B|p_vZ+JE}5LE+3CNr;9khh9=aH2QE`@VNj%_9b -JbVoszEc1r+I7s{PUKzQ87HcC%V{v!H4RpGAv+#$St#cawK|6Wt|q)!~DU{8f1uiglCmgB=u0x%4@vJ97I1@76Zl3{!tku9g_E!E7IZX=g`tm8TYACa}&xLUF -UW~zAeBt!VnUou~E2kOt#MPC+kYkKz`gO1CcMAjH$EqH;d2(jXGteK>$l}vt@z3^)7OL!8 -kus6zNWU**ER3L>+uIuSOAfo%#MBz-{u_*e8UQ`!-DZt>y4&VK$-#m+m4Al`T>K{lx}#Yo0(8Fr6;?T -9;DDs7%9*WS+=*(Agab{?m8R*2NVHFeR@737gU_JxHI+mQ3nLQxRF?b0KoeJrWs+pShltc2}1E2cqLa -J>-PBAQpR?y3T7d$AX0UYR2`S9yA=~|REYEPdE8DA!0xaB5;`VkB4t=Pe_bcaS#3yCGxEz$>;pN$O -}m(zr2E9D3Uz>8j)o1lR?zx0^1G9-s4>0Ee1zAju;c)w8kRv&ip9$DlSJ62N(Dwjd#R;Pr%<_F3m -6jTL!^Bw)_YcSN#9?Y*?Ph+R1SNA7kCh2)bG{1?;k;3hz@^d?==!X?!W_xnWj0>Ei;$}C)`Jcr7gZx$ -6xD?Oh<35wy`xFd1DP5$mep~?eY{j8_ -?0pvJ8-qFcm~lflm^Q$igD}4k->7%GpCKD8v>mFkwh+a<}Npxat8O<=(lX%)$4$@$l`L7duepJ@~|9- -1}L}t=8(zA*yJp#!keE1*>|b;Nlq^cvT7PBPvt-wCyas$$m3r?L7Lt_ewQ*X35Z&6aD7hb|Vnn8BnL8r6V+3-}jvU~zy7H(oz#)6RaxJXFpaiB>ktK -E%&!0wq#rT#4=?0(=R@*#~x`LFAX`Cq-D7vWeYs#IZKnLzH22bJh(RO@3@h(1hAHnCow{Tx{+oN&%3U -<-g6Nl(4aFD*94$;>iUxnA>4+gp~A_F%EE6q|6cT=OZK4hL!fMD8<{Zg+jEl0n*Rm&PW6e60bHSoBVNet^SQ*}uy6=auA5MOBeU!uq@a3SgdYpOL&yPsTti8_G!QbOmZw4&7|p -tevZvYSoTQwXy$(=DcMKx$qx}Y8qB@WNe4+=086KW5JIq70+GMa3)ysxp;2G#%y2K3K%f+pC2*e$fcC -w$z67h{m{73m0>Z&)<#Hl7G1ql_r^`7Oa$U09c2v1Nlx(hd_S;@}Np}#Cz=4nIL?}7qL1*`x9hfg7S9Y(FvM^Y)3M -Q}o_u6HNdi3=tk8WSc>si|@EYl;WwmDTsXOzL8SyAcBl9!rI@7OGJ$jsBw{-qYVoYlSy@Y~7uI?VMNG -luaGRuieJUtS5T8IdZx#0vxbs^E>l0#?dP?#~I9-EWng-7=^j@xWowC*LDj6;x^ngf=<0E|Q&bxgBa1 --fn7C8NNVH9o~fJVp9m -<#~o*&@N_OCWv8=Sczc;n>Z+Q*ai-jz?VIjl3jAM)+W$>ZwTJs=^&$zz@$c#X-=Dt#MD -#YwXINk>JvCtvgA#x2-fqJ}o>N<ZiCH?e_SX<=(4`@DzRCuNVeeil?z_3)tn -$@R*V89?;8Yf$ey4Sw{iQj|aDhVR3mhyaRWo<#2Kj6Nqq}ufuk1Mc)~_)3Tl;g(xNJJI=Kc?!PNxdYl -2>$WU$zi31%QN4b6M(7kP#=r@QZxUJ3bAT*?POE*tkvOTe -;p-M=5J-(Y{lFaj8dY6PrXVIfPsTBei2s5cyxgbHxY*j%H{}jhhTyhuaix@`xD~wXLAlA8HyJyh_LR?{?ZK!T@)QUZ-qp|tZv{$5 -+p)Qbja%Fv?-jQ}%vu20>wvWVW$>txvZDfmtxoH2aRXj#v@l!%r8#w03$E|8aM4gt$n8 -??k-$mbtIqPH@Nohqi+82kQ&MBQ_@x=b_6rS*$@XhO^iAVGhmU#c_c0rJ0HUNc^|)3ZMkR_Uu018B6g -%7bY%Hhth~#XirOLHOl;204oofIG&5fPw;I)1PsJJqtBf{BfRz8h7J9rAvu}KH$7zF5`0Giy9Pi1B+B -U~M&y~gxK`PTxV&qT(Q)6Gl+=TFlj*m7+OW{D+ufCC9%Kuekj`r*m>X9JM;;7K6S9AHtexd2}t(%?*r@Mw -?b#e?hp_d$}os(wdnPN{xO{>lX-UD+pjAAtg@+~fy|e7q$gS}f|r`IZU^4rtZf4b$D^91wlb?6e@~Ia -K(z&vf*hcN-K?_0g&=sONLxYlxeW`jpJ@1Vm8ZVA@>9;AO(g(g4|9GK2J^&_87R~g+0Rf@s< -{2b%|`jd;t5z?<6W=$;e{VQyo5a;7rfK!0CULcm(b@;k{0wN^b&SkGl -T2?zqwGf;jscg{s@VKnB~_*&n;JN|r2$1J2Dj>OMd8E)!_}JJ8#eHjydvb_ -8lpQdQnp{Z+uR7PfQ5q09<-aN()ZluLrou>Y$uz}83o5Pcv8X_gI3{@=M}~S>XsQEQ1S1x(oOR#E3^Z -NsK7x!f9PIL?6`<~?rC_yC%&n!f{bFl8Hl$|j;yR5lxIL_OLr5Lo@LQ}jtx+QGw23$6L1-Qg*4yo}JbY=l|Mrs_s(;*Js>6pXU$qYrP8@h2@2Qod|u? -13fe*%|sg{3zv^4Aqtv%&}$xk=TLJ`s?vj45tpg`D275y#^?JxMU7J3$iZC%`wDa>Z{`Wuaqmm&8Xai -6jPKBozo)0}KLhlAWi>*)!Tm{DGm;qJbU}P-zh;@3j`7C2{W8WoG2s6N2Wi9T1Y{(D|koiP5~|gtNd> -Qn2|r@Z?o>5wU;*qHudgR;%O*NF46{7zYCh^LmuH#cbfSJnC@|nfX@`6s}^>oUVCbbqa8=QxtNQjJ7f -92q~Cc>hZz!PwQ4DmkM>}>j<2%DDYXlb8u+h>&Bs3gcd|6izlE=F65ClAldjP(kZDeJNtVOIY1D>nY6 -*h6M3^7RetQtBcJ2ZA_$=u1diO$Jt+7qE?)*&!Dthwc%o29ap6heLAb(0Za;-v^%uWPWP-`2WJ>dtG! -Lo3q)R8mx^5hT405%aJeYuvxr18I#CeP`3W!~!QGxZ1AkW}AhdO(>jfdEkU*)l#^rhKCH9;WB+K#ioF -RsmjZm*{;p}T#fA&BcB-C=FC;?}bL0zbxnR>&nw2Eo4kf`Tb`(W86G*=dh&GyyJEVSLbu`zKLn*8*zG -lp_hm1>~+)v(!W?2ZV}Q_Q>WbYGvo9G>}VM@{|#f$5lmU(6tVUIYoBnC>)+ecedO*PvxEniNr`x_;j1 -590uC+>KDoOtkT+38>y0OtyJuT#ls4-*jg-DgpsyT>{$Q{^zvzPBQjbm*_zBuk3h*!;2T=6ZT;9UY?v -<3Sy<)v&i4bY?4y!u9&RlxBIQ`soGC&LnF}Flj^dK@JUZYb8em)5Je5J{F(5jt!WCqso@XjyF;AW{J` -lQhTw5vyvXZl(V_VSrfc)#{v?EkG)m68=ODsvwX_X|bv~Y;rJ-b{J<Vwfur0Kp+OO@&%pE7=xS84WYugXj -8KT}raL;>LbpBO0C*JXNNW1YP-)lTAL;25cWXb~}*(cM^1@Vl8x|!V7(nv+Y^avP&6$q@2}}S;_<|)( -L6kv6T#pMdfCD3dw-P(b)TBI!oeJSdJNDdmZbHLXKFm&Ipwllnj}Lk{xDDY^%c!kAE_OpI4GIp<2-<@ -T@ -F`a9vO)U<sA)9F4Druzi6fzH+@yzz8$!ap6mWWwv>M=t -qmEZO5G2*%FS`D58Ba{ezFoXcF`GkhCCaHg|`+INVD(DFc=s35`|TZ$q~3ecF8s&2?rH?VH9CXGr*cc -yCekEULnAMt{=1^)Z+kgeQupRf#c)HL1Lr^rpN)xraO0jSiI;RAF4cW^G%0LTdVhKX5&lQOMll|Ca|9 -p-}j=H4fyq3K~h=+RrKX@ii9-57510CJR^tu8flFBwXuY$b=v`yEy>WK9P)03sUTXmBALFIUrn_YJI& -oLy3NCvcx}L`3>uM;Lsn>+=Or^M-sbZ-S2iw{YJ2R*Ie#h?0zw!UxCs3#uC?&9TFZ(GjG-g(7UuxBKUCo}*YI -O1glzdh-x1`$;wJ-AwN -kA`iR8DRB}5DIEDf?cGVX>3X}dAFl9Vro&T>*&_B5Q%w`9+dIn&#{rREAb!vD1O#+0UW8GHlX{aLG4T -ALaNn3hbL3obP1oBVBM*(n*kOXgaaWKTt4Ck3A0!pfXO%YmRq4RmUTFqMK{g`i_8XifqYz)Tw0RT(}e -SV@*aw_I|XIx|(x-KndQn@o62Kmv!kmLH=gQ)%UXz6Rsc43#~>vQ%>~)5Q5Zp=nUFk@M%^mdjy2qsg8 -jq%^T+#nhC4L9Xpai=l{A3mmM+-p+|f8G8EAYM48g^Zz$|%+2C0EvAK~faBQ1R0iv*upe5V7c4cPHB+ -V-HbPRHDMJwe|H_Y5rxn`lB-Ch39o}joLo=_25MYM@cI32PXlezY)6}SD@GrNSvgEYZO0{K4YKk(|Uf -T`ID<(V%xd3`UA_fn^d -vR-~|L~^Ng|Yjoyz1ayI;?PF+`M%ZRNj!in0qPH(eyIP(F{*oM{zH`!R?puGjLeQB8&cx^n@Oj%1=w} -hXk{!U2(`D}+#BT$wI77nBgq3<%C#p4-U3f@h2Q4?vMMfP(QQX-D+=&awbgQK&CTef_3fp0OgnKk>3PC -~AlF>y<8_+~%z4(ubf}VB&j`y(QIUtMoaRQyo;z!g=nPS8!hKw-svE7t%dc^R6J-mJ*`b&t#8A2A_+_ -CyHVh`Z53Pj|`+)k@erxIh(8NV!*tfW*@ouHCw^Z~34>52@7ZbY`*GFyt#Ao%cK&9(n0G4_M3;?^f{!L@oUw<-`UB85sdU6wMr@K= -PV@d224eaeC7`X?6$}u?tY&yZB&uVs%avHb4tjH{O82C2Ls@Q2A7jmFUoLU7t-J!IGxbNUVHDd -EPgA#ib|4@+}1xJhKZN<9}oewU!mI{*W-b`8*2j4cTJJ+6mP6AQtd~950^}&zl>r?|WbT*u+~3kG7=-MN`8d0W>pC{{Q -!_w9t9u^O0`W^AI{s|!o3JU|HTV?Y_6&%A=dgpglF7I&eEUmBo)RInTr0||wo6;gQ~h&)P`JYU+vO7o -<;JkV`GKrxP>HujE{`X0`p_ssWDh{jDHE1qDv$=#qpHP>5IRO;&U&^uJDXPFwv(_3nB@Tuaxkf}hu(Y -z^B9?@b{4ctp1#RBX8G8d`%N|$>DQ+jGdO--C!G#M|k=&)$VY96@}NirCGYKS#PC-nw7nAjli~c>BM@#i&7CbwZ2FmDrjQ5gj-IzEnRgw -)%(YR78#IWg5dvL@bcm%0v4-=PuDRL0!U69fn$?FFyH7(St^@B{c3yqhCLiApM_Uup7NF~08d+;T+lJu`IqJ~w)J& -4y;@JmEe%3FbS3gC~esNe#>2d$y$Kz=mPLM^2jMx*7W!urz}^&)x}n`$3&Wfrdxz~;AW^2J -eoIAs5w~YizsrmIpYEd@4B!-Zk;*l?AvixNO4$}Qv>esCsJ|tknU1yVky=KrZL!T*U0RZ+A-i3!FoS3 -}Q>z;>z`e#uL4@XW9z#FXAzk@CWYKDIO9~tyHTrCDL)hyGOysn?DU93sy{{pI{Rzf~+sh0ytQT -0-)!fHV&br46fc_~SZ)1n<(Z5b$!(1^Y;&LPgUbeNLG1toPg4Gh^k(RBV!;dU*Gj+3XDhPR|@e4?516*M8+QXW~_tI?aaC6P5ZE&cM5qhA%+0#8U(OkBaG0;6MDB$!&_A>OoyE4LoNkzjaH_MWIgA<8`uO)6aN -a7k8!nl#gG_>C|%3H$0TV-8%HZOI=BJgTPc8fSz7>&GK*RQf1$Z>eM03rmOVhn+KTKUXMj$?-f%vjry -->`3_=O_+M<5NjFa#&Wfb5NBTbLN8hLCGXl&rv*3p~IfzWzuU;C&NeIE?=P@hu|L6gtR?Sj#E33*Uj7 -%Nh)+^6~Q45JG}qKxk_!CiB3n>X6jVseA_gj4 -l_fyhtY?=eouLEGe)?l@o#Cuhu*D4XoYT -a|zbOeaMLzew4Z!1LSZB0G_6CH8Ne{TTLxg4Dg5eQF3q_Z8^@D)+4coufJ7r));lt;>G?NTD&}Toy); -oyVSwd!QZR!$KLE2nt7H!v6u#)$W57CX4N)NUcuy-ha!mI~F`{@>G?+f9={%F)&~d(tDe5H^v}jq7&TsUPqaMAMG=ed#i9~R -F0E%n5V`bMT^We7y|*d8e}(w|;jtn3X0^VN -&yNnpxBpUne|%H}YfFNWA07Q}_)e|51mA$^zv#}6pjq>S6@LqU&mK+Sx2|d!e(6~RKXE6^k5xCY7wvkd%+b%Z`xnMwMn&(in>gnPpt4S!39XTcWDZd=U=ax*yS9Zx1e5I8zj>il_3h5e6?Pw2}8Sj -HD9lEcSEtD{bZ%t+B9Zax7A_HD}t6a^>VNbD@&k{qH;LCV|ZI3p5*&L>!@Ora$F9uy1Tx{T&kY)?TzQ -$gkPZ2EQY8E#`W1Td@pjm&)m6j^GZnd;@HHentIhG6H_LQ%nM&5F(xTcte70S|l4$I&Cxy;C0)|5QJF -d-p5TD}I&I+QIX4<)|w_AKS@l7|!DIKE0<$uAS%=rOM>EeR&R@fb##SMp%u8#il}YfFBS_-@7Ti6y&@ -`gah&vrAsY?=<+vtw(jk8Xv13tC6;>QPQx3%kZOa^-6l+R2vaXwxP8w;MzJD9W-a5Q=rF&I|O-1gShu -Sw}dw=a+5h@VC;i@s-ZR29}-Uc%!@pv=p?m)s*y_2B@ih7O|66~#pWhykfCW_NlkEtfuG_tR1xDz-1) -%m7=FIH%0r*kvh~HzPDNPeK%Tyq&m8MX2~jHgf-q};JHVM -fWkov<5SCRbXkY$3OlCVwSYTcWtBi_XP-I?g^PYnDby&ik -0=1dXHa<`I&Ht*04=mhlPXbhC0kvG!Hjkhj<4jmTGU|(Pa`D)rm7MielZaTjxaFQ?K_J>>md?k@RS|_ -twQ=xlW=hU>po+??ytAm&z0g|dMW@Hd!n86y$rhc4k-AaT7?xh-rIL_Nnx;@1ckOYAOA1 -F@P&UTDJ+u4>0onz5zXBS$Hq1&o7Yc7nmJ;J9!-{#~ACQ0a*tNU>)y{anM*h4bt9dc+%SAs{_>u_C@RiMqGiLW)T{pdHe5x0?GZy`Ih1 -YyXgLP;RU*w((YwDM7Xr;`+PWBbV$&%O>szSiWzhm+U% -rp#_};y^O3r*!RlRwCPdGWik`pbzbyKHu -bO)V3P`MM^N@#I$cqRdHTL68YUOLua%wx;a6nkyu$GMfK2UffM=q|mr{92u5URy<=PdHX`)G>=#XRzU -!W>SM4%AuLLl7hmV%8%RT}!=q6zMaNi+`cSR}<*vf8f?2)mhy`@B_n<=L2myPVJr5+;+sxQZwlNO1hg -&e+go3y-?G;5WfmBjb0mDah72{=m-ACBX}4FDS*ZfMC0d&A(gtz`{UGvpa7RZ7y0XM1T3GSeZ8Z7&D6 -fa+Sgp|>jmx0p?y8AeU)in|E7IaXkU`{b!!O@6Gm*p2te1O_w~_`w|McM8|cC*O{n6URU9!s%|jON9% -B6F(MBozeJze^Nn6%J-YY#M8(qllCDi05r&)Dn!P_FvX9Orjo;_sXCNjA7aP~#Cx0?=lKys$T_u+eV) -F5mj$F#8O8A@z;?NmCMd6AxBMPO}2zZ&l?0N_zicq{O|gW$lO@XnYbv&=#AB=O>e9=HTA~QI>^)$bYtdrxdiFan|3=AG|l>+dSpjpR+lB{`I_TNdM+AH&+k!M9*4 -9%qyJJ`#!D;H$a_>&tXnnc(2fTn;vI3iP*!Ez|@_o -VpHAzYgyQSJwEwuwuZ9M9HOEcm;!lz5M@3uKrtCA6QzDu`oYeBg^1@lR>isJ$wrs93n -O(5Glt-{DYrXX%&1v>3AKjLBwp8L|#uE4c_@W9u3A -mr02{%$~b5n2scf`XYV54?<~JD%52Dzk^T+W2X(Fc-iuT~UI2G5jqhcmua^P1H$fDYrlXZ)jE)Lx -5JWpoB>Qz?mfsU<9%r;IAeF2NON~_+X-kfrHpXy39qL+ASSv5TNY9X^lF&aXT*%0tRvozAglXr1X~6&My%e94e3ZYNeE^Cvsrf-WgQhs`7<>4Ax -}^YpA6qXrwvQD?$ -DyDR&6^QW4s6EV=l^}8PSC`<`<@}Tqymx_q8MQAnLp577BTQ^_4j7a?5Rdobr5ltAG9b>VnBmFjlb7@ -V0dGK6p&bsXv6Bltq1m%g#&Bi01n0HJ@=GX}xW^vm0{7}}Mp5qmyYT{#xN$?a7&mVy6yqseU?m~|y^mqUj)9HP6*31L^TOJxQW~y#dYmadL2LQ`HQ_$Ioc)YV+#~ -SC~K&?QGybtJt3!uwDb7SY1ue`s(`8Gwf{7U0f*j&tDGA)SRtLOiw#I#z<66vlacMt})+CEI)e_k|Jt^Jp#W -wG`#w0WH{apJ}BWDU7;;WhJz?Z|7%dOJp&TxU9 -ahGFnDiMo_-apFp6B*pWyPp*-uSeYJm5HBHqaAm$eo*j^|;*Mw5W_dG{Tj^%`S} -dYnSwbFaeLtZw!&~zV?R^JgZ>^DZ(Db9&=9-CH4fdsg?n+Y1bT{FTAH!)6{KiivZu#i)qASkE>~Ithx -tzm&p?l|FX_<{=+O~wkKr3V3ez4zyN+SIj_?uMt^(a>l_EeEmv6V=Ni)G6|Cuf`-O!8EL{X_$yD*SMREREy_ywhOrXm@#R2fQ* -av5OtNz$0#Z-IctpL%M?i*<;9ySBjD8lsfUEid&#v*U)Rmv=kRjVIk!QvZEkOCiTHuY)IU=HRR3@?NS -VSahL5_GhSX6qCLlZyL52$%DyxMwqLt#p&Hjz@2~E0P_wkR+zeBjDD=|%A#R{oS5)-d4w05P0cDS%J0M1&Dg`o=fk#!29IQ8g2k;8s2q*w_K?F55&y{GgLm+&KJzsa7}RL2 -HEn0w4}g-*IaCV0aoWLw0kbfUSP?Mj2>8pQqLZ2w%%L)q6=GSjZD9r?=@o^1gpHvA}_ZTFJpxi+OeC#=&J5wml=5MnY(%|CYU23i@mL(>#oBVQzc -tx53f(bWO|#lIt$Ct_9^i81TVJr;g^#Rj_ve7BXjFwK39(TjHqc$fj<`hAS^5-?^D?g0P#>Ax0i3h-9 -BvMc6=z+>c(5;lY&UDT9g$Wn! -6Oj0+lsZy&uYLqd6Ou4Zgug2q9A49H-;uZx>8{VuDvNO@9RM~k%TC~O%B96U@3mWLnQNu=kN8aG)vMR -S`WZ_BNKcU?|f%jSTt0(XR0Pa4@N^20RNDi<>xgEZ1?Ct9-_EKN>_Vq?NVv5)!4V()%Em -ueM>pMmV7lOptFptLGq{s5tg;k>~ICHN~wX|{pR4BS*@gDvvg4wOUw}Upu$!f=E+D50(?8_%F9iVQR- -R>dzDKM->DZacFbrJC@25-;yS-dSsr?HpNof6Lr^t(R$FLT#t${s)+A%99B5Md#T3Lky#H5Ij*I?cPA$7z(Umb@cR+^ou -B;PM869X-#ccEqd%8o^{&=R#JFiVi}4{>5{*(p(^^!Vg|Pg57t$*RJiD=g?YCZ=qa3*!lEjUiRvucAB?k7Nu(2{ -ZA?TGO&^Do2L2%BZ|69}q8q1T24r!W=dxTW`$wpfSo6`Ap$g+#XA6y~N_#2i{h6GjCcfD0qnjdcLs%74%aoHD -Ezj5Dk5(XDIOBnYj?8k*n -R2X8v28L4`K~r*IQd!7XZv8b~YLrTkQbccwau55NhInEdI`Pv74mZhsMZ1V#f;iBz;Y~NW@F!GtUwCf -N{T#bYe)}A5%O;e&<TmnlP0Zs0xnPOchMpZy>ZTnNwgqeB_+;L;;l95}XgurgKI`XzDFXGdFSmZ -SX-NpQ41d)^@nKA}1Kghg!Ku!YN8f+27IRk=#6GaDU<#W`{rAW4H9&YVKbB=#TT4pbsGvq`h^yv5j&7J0&xKI_8~Y~(v1T1`xk}^6#<_sxdOJlD7q}7Yp@katcJf#IvR1exnoe#S20Wg&V-9Tx -wg$LPF|Cig@7zUb7K?6**{!2An3h|(nR2#N#?6U+X~c`Yku>KZX>NzlrhP~=j1<(0-iga9rLDZxw~!J-Fi-(cd!=>#Rjs2ImP$|Hpf#P(0H#CR(Z*ys%3G|C^5y|`6vH(9Mef$J6m}!Nr)gtIW4? -VD7h-i3&On=PZ{xPvF)mU_8G;?%$kw9sz|>^Zja=Z*!y8?J#?wzDKA`Pd%aLTU+B$6fLT=9}(}$+Fwf -2k9{;`JL*nVHu%6chJrqQN0!)`1>d)7-C9}mf5`d&O`k=y@6e4lSI|ETGk+Tbc#CN_0@^>^`lh1)j-5u@5Ob?cJ6foBdQJiznP`eRAKHRB(M4C -?jS%Y0Imu#kHJpl1mr)|7d54!T(lvw;){6Tm41bL;rLLGxK_Q2H`fSp^?Tcs8wNRO_PtUL5vqrf-(HC -Uq4p3cb8=K~grDhlfZW$gJL-s3aVGc8FD8gO-2 -JO7dSA^%LGV;@aV?#C|cU1)K*HanK|0wjmyQkH7wZ(Ic-H8H(J@v$=sWVEP^5FJWahV#HXnvLXaBUYtDN -2Uhxl{_3k=&iL>6Lmpbd+2oSMXhB)i(q0V~M|HN5uXTn);+r?qddR715IO|>b4$gXq6V7_$hdArKpgZ -f4|E06ulQV`m>pgYxyE^OL`weHkxtg=ytncKkHxs*}4Sj6drQUi^pqqDWM8aEdHuctL-cA49Q!jy_@9wGhR{%E+$cy5#r$3f{si&UqwDv6yd+0Iqf -6P^HOFaJxT_x4^xa+c=sZ=apY1nY^?r5nf9k47i~C2edI^Ak?y7f?_J+16hzqywrpN1UdMgud -di#gC=^@-@-1KHqH$4Qnyqn&LA#QqC{O{fL#s!XTy@I;wjp%C^k1_w)Pfy3*|Ikq{@SPp?mX6dN^)$j -rI_e=R{<)*x?SqbbE56~V=eV(6oWmqM^}hPwd+Oml{J-d__p6Klz*CRxyNstEN`BwwsWZwPa^AetV=tN6B_1dl);;DzR=&9FoX-fw^_105gIxOp(o_dd8ywp?ANiWj;Hc -vg;sc&HI`B%T;sfUb+dg@_-Op;K?q;cQ$)Z?@XYwK-25?k|i(pM8S1sX`>0>Ped6|pbz-P`(e)DMF+jqjkp>^dNfC)@u+(^T* -Ok7OGn@^MWI41pGyqHDDG>Tq>QIAVaHa+KQ21m{v^LGISIK@jejTyGXX294H5em(vYjduL-AGOphHYD -dMsk%cML;C+cS%X=tRBQ5Z74Vc?-9he#bo%3SAB;~bf!Y-?^oJVA>ffQ=c_k~AqG>~xc%y}lmeO10lo -9Y@4wmLX5=0>%mP=XjHZvm`hW_~NfWVQvk`2NG?;%(jNnC?%J^L779cNcuFh_(Z9No+!%Q)b7Bh2Fu5LT;xZ4ag&(ab4U(LTg-S-%@#}eV{2wO -xW)SW-~aw)w3rtUS^&84?bk44YmTAC>bP0x3*JA09;?$h7UGr)eXHscO6~ZRVM?th7$E0ys%|i=xh&} -G)SwrIn|_Lf7Jmv3g~J|g=xGgn0ibOGjL3&MRup{VONiiJI>fzXvLD(wx-jf;1s}=6bYK2*^1D5B6Qg -yZ0dxR=NI2nR)D4zkpdatb+hBG<-3aQUzXi}T9e)w33gRIaW}qjT?14;G#C|n0r!pO&e<%QzQYp}A3R -Hl2+vdb-ke_EaElHhVZQYiF#VR9^spn!;K1M-7E!$ENVMYJNhUG``8 -T4mkvgt_hhxy`#F&i_QmSj(`2AAb4S>pLI8!h;mA6(YaVpU9S3{z@d*{{$Z=T~?^ -&)aAjv7O!wx@a8;JX_Z?{1x&>FLmk2Cm;Npx^xilukaq=EOP!o6V4PHy^_#rpGB`1ogL`q$GV63RSal -;6>f4?#LKo3NLu8TDKunr5BG|prqvI~LMK!Z`rY6!Aa+@+U}VrrtsA%FCKQcBN7?~lVl)MV)Y3WE3bZ -q)wx4(EMq|ocap0mbawv!eK$Mf>K))~&f8NGn9!yjYV%{|Ly+R(`Ht1=U|IHm34_TxAolY& -k;y)Q{68hQBQb9kYds_jYICFLBVI-xDk?#%?uO5rRdNxfH69?F)ff$XQQ_MY1rD5ZRNEPLLkwQj= -FuhvMtvWm=nM@m9Pf6K!!JUG#}?ow-OtR@dP{LN;qJ#d)71C{6nPx=AK}Z^Z2w`---hzxOXVI*y+}8~pqRT37fC`#@Hr-vj*U{-T8?G{S934T!`q_(WgV?eUC=4E%sv51Gc2;lH=LFmSy;N!bMgo -boa*cl+gP!&!we-f#N;TnExHhI-(|>;F2Jlzl@?lp3#~z^2MYROy?lMWRD@P>A>&Q|4|{I{P-XG;j}M -z%mjqK2m-42#6sebexgZx=6aqz2&|C=RA}E`1FKQ{ES5&-W`pp(g(_G5R`kJ}4si5Xk?v|Nasi|GW($ -WNzod5aE^E{UeqFHb6`~H6a-}ixYXP#NlnK?6aX3jh_#$U|W#0MFb8;Ki=V;vrL)M+aUZXo$W$W9taf -#HvQbQ?sfRJv&6O>?-Tr4>b?R7(?7RSu>3D!I>+x~gZSYnNit5Za{oL__HR -0{OI`Y2+Dvd{HYlES?r0gvqU>l{qRsL~1ATmM@Ai;~A5lU=BGfHlo~!%C&bXxde&d39Zx4rQ}$INPb_G6s|x{;>XQ%)(dIE;1G|O4{cvI+P;+6 -y_6-#?}=1l$~xoOn)6h~hNfl*+0XQBEu^z>N9BE|E00qdDS_6}htgX+dTSkT(H2rQZbmH299#oD!aR -Fpy#O8R0{qb(0r+O|5tx0Qd-Zg&Rcdy*6fuG#>ddou(OWMx<#@|+3Sgd%UCY}s-S*fMZ`moF4$E(wrX -w-Lx4b=FNO0!aAT-2+V(A$8GH?Pm4qd6Y-3%|w43TSDp|uc;q(W?Eb|xg66B5L=qYx-Cxz_NYxxZtbb -0nNidqkP;NwoOkwO$=#>$Fy!t7z~^G;6fghMq~K0hS49>ey|YX!#l9d|x&KgP{00kOskgu&{a07_pg5 -+ygKG{m!9<)Ka$W#=Zz?0FKHf$*qtE;7*nmaL&z?N8zC_Nu%(&g&2ifSPUza4@`aWA);w_W};;Tm%5w -t_1fSz3Z1nav5n{Jf9z(xr>F%M^^;sm{KNvxj&~{WC(6ox??lV{?~1z|ZX3EJTGo)=_Uz>s=)LajkNO<78G7FV-ZjrQh&)2|k4_&~=g51O0Iiw(IxS7mM~9|_%(=!DvGc~cVdI0yjQ-tVlqU|W-&f8Nl?nvJn?zQvY7w8m=@o8~|xuQXQ{cas%ar4KZIOW{Nh|hZiJZ!D7B3||!LEL8E+s*j(0?r7>Qy(B`%0S0)qVXy?kI_aD=;C5RGH(hno -IFnw#ydBux?n{#5V=jPYU+!w5F&Mw{zhd?YjMviP#R_yxiE+94l@$Y~g*h`WN#F>;Nb8s(EL1D!K19? -}q7cq^dvJN0AGTF{Lm^Dvz8xa_Pv4=KGA-tFM`_Qm8f*BEsFZ`l7>;yp~!DH(8bEqNT6}O$U0AN5`S= -ZM;vBO+~gA)hDY@m`*bD=jLPPZ;;QJ9j?+0|AtPS?>R-q3?0Pz#eKx5tsjAX)3g;vCsRv4dM9r(`cKu -A;fk8lmpH~{3-pxnB*?yq?fH=^dsAOdoflMp!4*V -5n6-ZmU^+0w3blSSZgS^Vx2ziaXP!WsHa(;j!3wYnKLGL3M!S-N$_Eph(bEoc&_NSeenQw?;9TN>&TM -OuAk%ArE-cjA_$_S+(>{e)M -bP$YsZR@qBQw_Ck*ugmGS6zPf*sI(U;UT=^h-Yb7pNrcx#8wQ(d(hWLOO{T%kR5RJ7WWP!wN$9na7P1)I%oZnC>_`6oVVzsL)7l?$NO*MYT_GzXfdmrP2rag{wL@J}oECjCcmVCV=-!<@s8nJSa@R -bkyG}Xu|M5uIT`3Q^AWtNAqOmUgjMOcQr%xW*xh+qq%7foSEnc%QJqL#%hsU4bg{OTMJcTxCy -P^%medoC2Jj~!q4DgineS=HMS(-eXpbdOkPD$cwvnXPTXkFRX+FOd!X6sc^9cT+B`&>#F*kh>yVIeD3 -ekXK@h$QI{_p!Q?+sbVHjLx#Vd8^2YbEDICp*`H`cteCzOI2?kSzEdcnYzrn3Dw~#>-sqyO=~WTQrUz -T5}+5fa>DsE(3d(pVFMDn51NJ!j<+=bUEaZO7Zt??0fAfbJMD1gX+_`oJ>d^s;|svs;_Nc(`fY4naw+ --P5d{l|^u7_L;J4|pO|P{27VdSMTe9~SR*sQvyjd8#OZEXpzJ8rhEy6#(tOO+*Z|N_#%a9;3)unV3T5 -^?hTB-1pLP$hPT~DPSG?8^}LTsrNnn-V{Sgo2!#4$X$#A8Pte^mYgW#XE{qKVw6(xh+5gnP^3pYS9u=Et>GGDMI@wicn}D -!O3Z*4$wY|j@Dt1=+p$-)s6}S>GqT${bp7!lC+LXu$c9oIlRW4P+R$x9VUI$dyF?GNn9z`Xr9#WEmTP -zgL&_an?i5sU-rJFF+8&nOJhiSL}|&|TZ+c8uH4Nv&=?}J&=@+%8pEBG;zD?b*OrznuB&vu2`x!z46j -?+gIq6*Ac&DB<5PAKj_L@}L{x-eL?}sNC~8+KeMw -hsat)5K*@2meA3O;igO)g6b5BC=f}J=yZcHkq?MNK7s;QNlQ(b0KJe#BnxK|Or9qZ?LU9Exh*ec(rJQPg*|zRZ{Ad%oi0~=*~B-79}gt6WLkHuXFCO6#+(+oGMyh)!S4iGOp?-wP=YV -E^1MsOGMl(YYp+jrg_T*86KAjx}(UFt~V?)(^m9GLTw0(#gd8PPPmOQ3o+9yS_MfX-Zx51Bw2JfvC-k -mNxwk`g}ea0AxvkN61(3T6?dgh*E+@QKT*~jc3G`sT~_MYP=bFG+#+yZrDJgf5vygrA+8roDk+~bLee -(n50*{|xGHWSlGJT36&IVBf?INO^tm@LfTiXBPD)(I1?SE&*v{8r_VFyRJo>vn_^cP{PJ#J -hZC)kBs=MOO9Ik|JeOto062bDT;X#-{yZ+DPpK0*3^11@w)-G`IyxQtSDdf;HTrQ7-F!*y9U>YqUWXe -HScDEt=3wv#PD7c^ww5i^NecwijQ?YB)Dbb)lqzJY8tBUA3M*u7l;69Ogr77|TeqR(*Bn2@5wORD|;Z -v!hC^`lMsk=Nk%Zo~{xX*@`P|>u$4^KN$5s6*xv^^qtVmGJgR^wb8YBZ-2){vPOr}(dT)TN?GvHFkJa -`-te$UTl&OBR@;_E1R!3#IE?7L<&{F5NN|eKpiBmq*GQ)vcF6^=M0yTbDc|>7TuT6RwRtiIdGszsqu{2QMfpXuQ{)}m|n!F73FAW9BC?~X= -?!CXhK5Xr^2Z$gj>fu3&W{&^yw-GT=lE3WtN_(y6LBxv0d4i76}sNNYl_UK_bPpTUTW=Pd{Nco}u((= -A#Tmgp0c=7)_=h2W0lwR5d&{DzdW79vKeoqS)ixz&svf~Fn$ -?RM)NnMuWW>ztb5G)z;ujv`fkTk)e)>GbmwFB~$R?ObtIsj=N)og~ZdKcrH1wB4Yrd@*`#8kPf_Tjra -Az5M-q64sB*ajxL83iA<%-P+p)Ds7F+%oR{P-Gr2q*GaiqrFBxG=|>mSMHlVuG08@!$_Lue2)svp9@? -g&(%y8=!E{rV_t>>H-2H{C?-TRv`52z2O2@nwrZWx>dudkdNbcOdjBU`dg=Eg6*M)89M@NuK(Dlf?CU -c$VMcK{mZ#OT>o|2v0-`=#_!L&ytzTcWSbz0W2B9xtJH;#)ys5h*k7D{1EyAcK#yO8f(+cbV8<4c151 -iJ`6CD=%?j$k!th*)sGG~&7QWLn6yka?NR%Vb_9^D3Fw$-GWx4Vg7$){nn-*9r*MWOo9X#;IwKcVr@B9ytZ}m4+(!n1HdX+l -4DbSW;tQ6~tza0S_S5nLvi7v<3P$lU%8rk_RWT@j_X!6s?oCWoo3tDh;PsD-&UAs5<&qd;|a_Op -ZO3Q%^LiE}yYr>!Gn06{##aDt}^(g|`2Odp84=|pASiA*;#-NO!ru$h^aJuBgJ|Z19&qpiXlsvLgs{# -g7_48vN++6OW&#@snFtigq}_c(~-K!QZ^2!s2Lfsd6A>5Wg(ib%61P>70w9lX-{tsftVX232YMvS`N` -V+~cXp~>9#VFQf6j~u?!e4_f4X{W2lG4Jvr=3~Su>f7Oh)?L>p(Dztn ->i&nSZoMfKvs=a1xrM*?~fV7fqU+P9f=d;p_19e{Fe$JwT)|PFPm2X|gP8Q$pCUT=27Ga~!%5t;2 -hyf!K9NogDM11@kr -_z35*jj2=^^vq4%2e}=O9{>|(Oz?zl@Ifl%j}6b9g0W^ctMa-Ia2v-xWT2l&x;l7Hj1b*3=)^khtQJ> -l)`LVAPUo4`fS*7u6@-x^RFIP=c(qlXrh}XJ{Z16QCjV`XBKTi-RHu)rB83`7Q|Jki>;+%L7awy>+^= -SnKjhzVG`PKHP?gnmdLwCy2_`wJ9wAt8eJ=Q6szSEnB^5-f9>5ZArY6t+9g2WT`DeYvt)e(b-6_x*Ht ->~GIS*)a4{}QV(>SZz<2!|2LavWhA<3N(_KBTcKqFCF19KZsi=H~s#08}l`uiY+qxgYH~np6#SXPvTw --krHCnHj^f7{G1fQg1JpsA2wS$7AG_t0IPit1>sJ)iwXx>?6Z{Ce_AjD<$J4J;7HNPdueM*y}VPdQ9A58H0F<460FHvMjGnApe}7fxOO#OH{GVxaB_WuII_= -zM615G&FUL*6<#F_NgpilWf7bD%GyZsbu(EEJvcAS-M&1@W%E==mu=wIGj54H=i|vF2SYzHao$ -{anD)%@2uw$23@MIwEH*bQi}iG$*Q)s3jN;Gj=C-QpQQviD@#pqcmf{r0qN)e#e2O0}E`d01bQT9xj^ -2sF=*G$!357albA*Vv&n`6IsscEdhsHCU0z5FPBZ*!raeR{aq8y45)8#g4fsrcddg+8jp(?s;Sv4D{( -q1+XY~y`O`-QPBrCT;&dz$HH`^k=J(n(NNPkxAAWP%-{=RbmC^-2?x7!-RW3{RLBKyB@^+0m{_T{f(C -2z4pLR72tiTMjlCXY@z`4UY6%8!);CZR$GO6gbuL#7AP4N;TmjOK-A{xZ&i}GK|?9T1~J^Z_Ai&f`es -2uHaF!xldJ&6qoeQ51K5h83EsZizV22%kyKzxBYcr{I|8J^Q^_QbCP@_K)tW~dma(D(c4@8m|~Uu2b) -6H6ArhHPL{>gvR=mvI8f4$?%RorQETyucARQ3G3*m!Z<660^q>1|^k;+1aM;IQ;^f0!_nLQgV^^uK+? -&_TTrpy?nUy-a@9c$^UBap=?^H&chq*~}!DEG;Y2Yw_9v$FjQ0b_OZkYCQ^A3-S0oonY?MQNVD;-@EI -NIIVT%7HX>#8H&&Av~FW2pm2_e5{w=JG;0l%$TINoVjF-T86s?YXMGB~A_)Le7QZ6V7-a9bR2+sypHYnK-8nN4B| -3=i;7$r0%Ps(n?5>0*DUbI5ykvl+Mu=$C;MIxf&ibN2=k7MUKG88kgr*Nr-o{jVSK*G*7S5-YV>8gS; -P**H%n^$nthG@~_q2GNkWu2Ii(cs;Un0mJ6dAA}?JfK>wUno^(pI7Bz2=w4-Kl-QjkGsQ3WGy -|IC4@L(H7BN#<+K_^=-=v2oAYH&dt!3C~$T+j(zKrDa{c8dywXYi}IJ=w>$?$evzS@&;@yrcL|l$
=WKY=A!e9A;~Mfrdda6l(fjID^e_ylqA;c%(c5`^YfDy+-a!df_@k;3{N5|`0k6jp;`suDs)r^bp -YQ9f~d9L@zOaJ1w?8(*NZqzki!=0(|KXV77WWb!U{3Wa!^?JiynKpj54-B -SvCoe55@i`VZ|Y)mgT~F8rH?aieAQ2Dy+Ce#8N1%*nwlo7uL&yeqj}Rq(%$tRYAY7UKjKWYmJ~^SZf9 -S!iv$*(p^|_Q-Y#5PU@aBaU|0)!XqBm2w}zPaF#$}9S&=6VI -2i)S7Ch`RyUD|+nCX|&CW2w+(hdncG%JA{9B?plipG56f3*@Ai1ctPVutK5y?fPb($i(Y!@yCK3d;tE -u{@)@4}^iMlQ3kH`E-}LhCeCjyFe2$yMt#Ty`-^E^V|1o+zu;kK7>y#?Hgi0=Ity8+} -(o1r2*E(g&E*&M80IgH8?9xm+7id||uC+6?Go?AQwXQC+ux4 -N$s4|_oUD~oV3W>o-oB%(;-gM^5$mToA`7LqJ?-LfkMEO==Lkr8}iOP+@58#9)%|U6Ej*vZlfUMO~Ez -mTQr2U{&-R8b!1MEv%ng_ah)OdVRI?%C%7e1VX192@kx5_6j)K(2N?Nw8Tl_6pd+5&T=j;5RVw7$Z0r -nC7-#qZKb^Wy00DyI*zOYerxQEh0OfWwCdTa5ebM1Mms-r-Cz2umPISOQ5 -Vr<^n${ba|}Pr=jlQeIOT -8op7_W_}Y_81dNggdJFKixx-|t(l}-ayM1j&1)jOcH6nEa&*uCnW}x4dW@}nZ650GXpVJPRqqh%gTZF|AUk7kiqt(Q#m?BR*?Ggm -6&BZ^FneQra%>fp;$U<&RXCbFOQWIciv -)X2zklYchic6GXzM%4;ig0nL=(w+W#fZv}ui?NM+0|KbU40 -F+*!cJf4)mW+=;_j3FX3Q7oF=^rla-yyl!daXxlJ93bP~1HY2ei076lKQ<5X#;KxSyFBOBro2r -m1hE8Z1XBp+6RafIMsSkgDnXOq80$h1KoCnXiXf9<7Qu@IpA#G>I7je1!Gkv#b0_d -4h$VQMU>dCtjg6jnKHz^*0nm|L)k06m?EI}s06oPpKuMlh|*iCSP;5@(Xco=}u?XQ3o|6|jCBqP(morYEPYGmED&trV%VT=_>&;SGCjBvbH?nMIU>Oub&(s8Y -^oMY2k?sSmnJ|Y^PJ>ti{n>_*d=n_=fvkTcK2#?ZD~^SU5RLic8A5bum^b~wwHJjWQHuWbgi|h=YJwd -4H4+WLF;3(dOEyrHD%{dUx>+J@I#H8PwiNo!7paUTTBnjrDy5M}zS(R%y=7AfP+lNXs7nj}8Nw%p;$^ -UB35v)*UNdTDL11?AO32}H%H_Ozg+suw8oH9>`gw=f=VgXEON~k?+7`O(v@%rUB -E6+v>&g0%YbK@km?%AP!8j{Uw&j`9Xh~%Y9Kk)~M -D64WT=MG0j1V}*QQQncr&8xieS2B!JDXw{1b<|U(i%^+swszDiaSoOUn%uWG9_x2nv#67Y)Z6YKAwNV -S9jL8-ln|6DV?Emz2sSaq!P{%rH1zO-@z9R>Cjl|P;gSptuYEx8^rgbzC(#`6g4_lF2#G50?GjR-PvB -fnE&6bzsZ8ad$+U3^7H!}`GHGQ|7L!^*8FlsO(ER^3ZF-@j3SN3T8`2d>rzzG^Z6V2CyP||Y>L3VkIY -fh8){Ce#XIr$WP$gr?;OfaFJmmV=N#D|yUgF&qu%pDUB1DrP}b!z59Dl8~$lLoh-SR%MdLtPo -$b^?E@zMY5ISMU+U+SD#|-WPF+d2t{tLGa|A#16#Zzr7<16l_13bLxZ%$BsIjS^`^OIhE}rqJ(r3ZbN!e+U0+Z2IdoQ3q1%kw%H#BCRdrQIU3hz0`sP%{?iGQE=sBB8 -{He#>`BzHo?Cg$>YqS32S2S>KGXr>4e8uL+K!(Iv{?yvx@m^G)eK!Bt)xGWlZ&S`qT7KVdhSuM@1o+G -gGO^P|=^Wiv8!oqrT&>r}ghnzdQd1P|e)yZ&o9J^+U|C`LnyvKil{-ZT{@<^G}JZ{@*VC{{x;o>oom; -ngi_W{_gjw#{qVAfA;tJSH}VW)xSTZAAfD=&-TsM@4iWv>8}5Nzx3A8-OxqctImpN%rwm^nLTH2={)o -NvIWmAT=cK!7r*f0OD`{Z<<+IjUR%E6^_8nuuX*FmwQs%s&boKs`}g|yH+-;h)8;K7e)RDtpKjf@y}V -+_&Rw7F-m`b#=U?nUaPZLKBS(*Y`PK2SPkeLo)ah@(`~J+?b3dHFaPdb=<)y02KmB~=>My@uyZ+ma>Y -AIs->UuNHfMJB4vtPuoK;PmHE;1iOBdHx54LX8_Mvue?K^br^zb8{)sJ@R>fWt;kDiY`?(syg-hDKlU -fw>we*OW0LE7MuzM;C5vEx$pY2(u~CQQuC%FfB1l$UQX7EGQp^_h{QMvqB;`tJVEPMcm>^uJC2|91ZW -0we|P`?Bg!9lq&1g&#D~f5DBEAOv6qouksB7+*q^h -pFSM~Qva$cm#{RsGeWQJyeUptH+KHx8dSavAlO#nAVFT$mR;*I=W)WnI6=9j#5k+ukV*LC5a7V2?E=Oc;abNTs*sYMllI=4zr7A&n#}^%nVqJQAx=Q6dB{z3+Zf-q!ZUFrSkwgiZGGDAvlI$%1J;AW{B>kzaE>q<{ -Cfxf7_dc=yq<`8IAzVz!knR-qKvp%lvYJx*6E`+-%=d1T9j2N3N_t8Q6s5&zO=Yg-OX4R>}wIBVDA4w2%(cpmOCW!oXjY^MCiS@F&Bv&>jpVfPMkr2SlY9QZm*0yu6$|bw;* -2GsT!aE?u7&r0$k4CarmbLlG_`Q=h6f?W^#+5DH_JDq5(%ls+{}z|QV -7LI_~jT4jeC+G6+q*Db$E{>#0`3NuGHJBjrqvBN4NYQHf1wjZ=j;dFsR4nr0UfP0cy`pd;WY-m)kfIHLe(Y1#)`fNkKx~IN?T1`fpTA!6`nEKeg^BJpGD@>OHIRk -8`Fo-CGEPYlE^>1BRH|pWL(74-8tVq^>m!1La??2evy8dUKke!AkL?nE+nH~MQ$?3R%#a!(0UcrZNV4 -@QqGzxM~{-{K%(5a(H7J|T&KPo>XD>qZ0tn??NhUhc(Df#+Q;jz&P!Yfwx>7AQ8)+W!Vd(qsf4`VAPH -I5TLH-!16Y%1>y-E{Z#|L&1{`#Y=e?LVx`z5Pday|=&Cy}ti`8~=ZG)3Gqg`Xy|@3m_v- -uaZQ!53Uh>ED?|`^(w8h!q=*X6&q*QLFk?RJtmb~Gmqmxi5K8g-J`Bu -APK+stK29+T#`Y%L2H(vdP1rgk7=8}ssL_)}86>6ny~VTj7fOQ7L4Q!mFIPa(7Qc@#T5Cp$kUQy*@~% -Z#MqlBE8JSXh33#`tVGg`qihS+ehu0}u`;A;G!_rj0$h#^t0MGxbppn($5-5StVhA1yo%*$>dCI%2IX;gZ-?mo2>n>&`0@I@_&j|+D*Olg=xjnaB{SohhKPq{rzYfPWQXS%DcmKZ8M0w~P6lXapJT3P^ -X}$2#E>~Go5Z?Qc0kY~)h3m$y}V)*mYSL;C~&Le7^yy}M7lX8QSO3+SPrO=e5B0THIx_pP=0KjRGuVq -8go%xN$dz=lc_h{QS=)e6EgKQ4=r~XLUSOII7>t7uqvpt)^~C+@m`7@YCbZ%AR{j)8#1L-gVfYoIVns -#G{?%x?4)QNjRa?(IS#e1U+sb2x~{bsZ`KvB-FOr1c7G!e=Ytur6E+wlKqHf?@rI_7 -!NG=-|NjZM|0#C}t1lkxgI!D89tq~fI_cpfD%G-KRExx6dx>`N%&siNGc<{R``Lo=w1@0x@6yoc#c`12QiT -D76zBnngipU(tgy!2M -C#-B*4Oli$PT_jozZ5ZOIJHk0J^N62|+%I;Z=iQNy-17I&wo{Vb~Ief8<^99?7l_D@1X4ewe0WaER}^)X878pxNaNP-+JsCn19v%ZvKBY{N4Qj^Zfni&-O>yH+UT6M -~SaGtz$K(bS(E<9n1S(C-{xab9+2$r{~+-=J(FWy8D9vnaB06kBt3YpZ_5Q|C^t?Yl|fRyK9^Nck%uw -pa0i+Kz{x&X9e8jV`uNE!fkU;&zw3_Rw=SKzL(6`*GT4!?AT0D>n7dJ?!3s0lg|xDm7=a3yF#pdxT2U<9=m9lK6oAvi;D!Xkw~Oy -*vKa)ONn>j+j5EGAe;FrT29Ad}!}f~JmjWB71-k0%&Fpd$z%&=B-Q+5{?snjdxSJi%dta)NaPs|l78E -GC#wFpFRsfq@{CAeCSg!BB!&f?$H41f2+6SGU0C_6Z13*RE)}TBNKBmja| -cZ?8J_T0gbpL{ihpo$1}x-A#B~}-o^dpyST5ti~EOnao>9v_Y-$098zO9DbRbF-s7hav0H#=VSv1 -2$vMtOJhvtw&jb?@bU?O3`~-Fwdn^6ysnemH~PUF+VbJVW6f>)y{4+KGK^%KLyKJNA@KemH(uO`sE4D -myS6e|q=s&HDH6&ytdoSVl$$Ga8LjMHLs*e=&Rc<(Ju;Z@$Ss`sgE8US7_={`zZn`}S=nH{D_pm|k$4 -4^2d$+h5#*x$K!d`H*-$#IMvAx2T;wdD=7txWf0>9^(6}$$JmItLWtl#ijSjlksjjw15A8@h+wnD*Cy -?Ytw}PC?N@r~BGGwsij?A| -VasqdM{KjMi7AITQ{)QvFfl)hc%XAqp`c#igo!>lM!cDS&vdIdEuomv#F^SOB?S4h!Zr6kV(V5dH^O4 -c%W;d#Jcvgg?ajAH%lS)CzBer}C{_TEm66D1Rhjk-QOJoC|S6@)qd}+$3+Nnkzq&H>>SjUUbHxIFwkR -vkf>9*cCIWqqz&%$>D6{h28~<2zW8GBXA&aCU7C>%)#fbGOzy`pZoauu(-H5mXMIZo_Xe(dOW^x;X?N -I+i$at8#f9bKYsi;yKv!xmCtjr+_Ikf(DhrKQ4`PRKFc?!ZD&Q>uWV17n8S*wiqf`oUbMX`nK)r$(a* -mnZzfC8YZpdxek*PIz6-;K4cnd;>+=qo$rEEH4dQ%LGA}Icv3=yQWLCsiZQnEu>3y^S=@)VSQ+4%vCb -;-n#tJ)k=C012IdhPK-2;>`&N8@2<%BZQiE_I4*IV7Xbz>eL9!#Uru#k`t78Vx9qNAfl*$o~%m?b7Av -JoRju+gJOv%=IUHh%ngHetd9mX(#o^78UT|1o{~boRP2l)dvz2>YN=%S_`V+5Cy&Y;jH)do@qTKA923 --Yt%1ugn$#1yCw6e=xtp`lU7Y!T&ROt5&PE;JEc{E(o;=Cf;O{xhCm42)vyne -?Htq~(V=r?yW5x_tT3X5$En380dg&#$WXTe?bm>yIV#Nx!X3ZK=zw6hpXPY){5`EdHpMJ`Y?Oe}h{=( -U&tDJrI*=KCuzJ2V#fdlNwkt6KOFTZ3bPo8Aoe)}!Ea^VC!_A6&+&z=>0QCV5Zu3x#tez?im)vH%ob# -=Ai6+HHRXncrgtO+rVY9V9Ih;N!xV{b_fSj`_~A>5OV6kkX2pQ88%ioby3zeVwPQ2Zkl{{+SVj^h77@hd6* -6^ehuCVrppjK!09m`l@_g$BmfFQ7TiTQsKaq(*qO72_v78UJ=LU+lEt%j)Mg)L3z-KsTR(P)IR7e(X1vu?jC6yJ~H$5 -Q;U6n_fEUqJC!Q~VDo{x*ughvHM7i%wJg3l#sVHU1Nn!jqIj2Bk2EQdmPN?4lITQVLgFarUbxXV(XFc -4HD})r&a0`95d2j&N)IjugKa#Sf+ULn!_@ieEtS=TiJ76#os1zmekap!kO<{%ML|WfNZ|mq1&J-;v@! -O7Xi>{3j@WUy47R;!mRZ^C|ut6n{I#KWP)c{*x>{woc*U{lfZ%iS>OJ`*_c8?p?Zc?{D=D>lYIp9T5= -|9vT`Jk=(0ij~?CJ`}gmvq!1BJ-t-?$mWYUCkH-+8e}A>?9}^cI852s$M}~)m_6tjX0s*>p>+-1VA04 -L)iwO&l2&eeKtM?Ng9(|MRkc^3oQJ$!dZtaqL_m)y{@A9a+Yk$P2^kZV8Lt~O3Zr$FE{3$?BIY9ShiX -R$=$l)=`9b30$!Kf($!*)TZvAA42;d@2X -1Hk{Wp1oq3lzzC#LMgT6U<8oMPo(dujiVxti;RgAM2H-e@+bV0BRq9LA%@cE5Ea$Bb!!S>^-uO|lC0| -!Nf`_z|3{*t1PYz3{^2f8E&(kykuec*RLW7E1rZNN$?Opt6c*;?;NqkSO^!>Bi;k4Bj8nKPI^3sUcpp -c*W&>pplvQ+ub*3Ag9N5RTX%joURs#q_pcoeuN{m$JALj3?@ows%dN7(i!}}30`}l;1*ZC(W_X``OZR -zafN7nFUq#hknmw$3Nf(+6=VDGGhe|RXdYYeONNBIzg>6*7{IVd?iE>ugr#+YsT(lfxdRbX5gm1DGxH -`0#~-sJA@9j=Y8?=A5Ur5{0z7#-;w)WF*&eJYyhu%HIsET;Yw*T9?e@}e4-;?OfBNRC5J3M6#Fj!r8`|k!<}dh0K31$rRMj4fvX~#B-9&Fonz)Uwo0 -h`s%A}*|KHq_19l#t5>fUvcd-+d?4h(k3arc$O>PTzsX)Dd1F1v3cGggVh0Z%6tcnjbKkRb=gzV7=g+ -gMsw#H*@@4kxFIR+Y@Y`>{u|NL!gDtA&?0tGaa?7Tl?ei!NbpABZQM+6?jt07^G|)Xq1Kk^}72n7_`8 -GC~?`4ztF}8@GX7BS0G=5*TiBAJ!Jk98HX|P@R1jYBG_&SOoL-9va{7Dpl4#i(a@i$QXeH6cLocoWQ@ -*g?n|5iDrgWLyn=+Hqd{kIod#}01Y`t<38%RD-$J9KdC@aW^6U0b$%g!rs|m!93a_2|~8m8;8R572vu -N4xguL1A0DcJAZR$Afk1@_5fa4>#-6iUPEJ;DJXvJ=(2jpXSYaJ|a7))jc2Yqf)i#NpT)~u;l|C+}b_ -fr@2bitf`Yzi^p7CIyg4z@c6^cRn6eg$F*}?XFKOEeegWo)CuW7+rq1w(S48mihL}zJz2- -kHzErO`;+0Wj`qipGDVgpWcuKyFy-&v*yzSu(1wsYssC{zGGk?Y>HX;bQ1#Bse;QsT%N>M6uF45h^Xs -mDQ1x5a&iN4a8n1pBL(FJGQS0nC=%Hgk+AL<$d^Ml>HckfCj%Btmk#&sJxa%89VLB#bT)>TF)Lmdi1r5ii)Cs{rY(`sh+R}ryY~F$N(4AYcm;OSGELU>&? -Rmo0XO2T~kx@J3}{k|M<73@;*du&20>60iB0qfiFh -6qShzN%`BqPB6JjGf5{`>Dw-?C-PbQ0MjO(xTz+}zxV1q&9$lUxnlusz75UAuPPrKP1$d3kxE?A#(EB -6=d-zl#60Yu7l*GK;!)?dnTqxZu*IOT4_i+*;0u4jtm(eDjUKlj=v{4*QuiXZWd8r^GvGQh;P0VG_?> -|K*ooYKZS{QJR+DfB&8T`s=UPzWVB`kAC>!hwmw`n+QjAK2Pngb8#g!&LxTSU0s^{&j{(5{z<~q&lT -SY3ROg&zX3>UG{-}?A`}PR{?tAy{6>a6ji4(#O8qf!T-_TA_N5F0K&z$Q|avpk+bDw>j_xTL>40GPQo -bw@{bN==P&VRdc;~2G-PJadeZQHgja(8$4B^iDJ^_Qp*K?CqbnWJ1m59$KA!;ZTD?z`^YFUc>`bY -0cZiXq9OZh&TrImo_2(D%{HrQ;=b^&uC4|z&-eEB?n1mU3p7wY+(8 -R)0UrU!#^^w~qs&o9;2ZEi`dyl{34{g_{!gBgXz2GP=Y0v+AfmyS_$K#+gnz~{&fPz)zklt|@TW0@EB -!zCpT;8&Xjmr-pQBt+7vL-KhC&D0IQR^Fk9MJ$Xcu@A&T>AGXow;HkNR4oA>=UUfd?cSG}IQJ*uiWde3>(R|!e8TyvS=m4+ZjlWR-@W;4-c<5(rpTzUjMnS{3oJSK4kwnA#pZNVezYx4~H~eqj -yvb>tsRy7z0fmmn>ZCC`Zr!@Yk9@R@ubCFZUzrrjU(C@7B%Zp+`LG{2AAE-MIMDEo+@6VNf|d5XPioK -Tle%w{+H)w_(-8kX=dpy-fNyNtGiZ?8b8qxX)Tef%KIu`Sq4S4Q-%8Jn#_;dbrHhsSX^h7>e@A_w{bD>&>H -_Tn?+OjTpZYys{!%J`XKFBiV{!;zNi-}Y8t|l0|2c|i7*2Q$Jtxtiv}e$uv}g25o_na=cXIw1XrS^}e -?;wnuSn)#WB5~lFZiF@+5*T!Xmbi33N4@k -(gmqKqfd%CRo9+DgVLTsgVLVSC-wY9BvaiGf3!1JkkdDUMh(ZgVEk%KR!?Ym&-ZNVM))b< -lAcX&R^%DR3cZr5%qG9BZM1xIxj*;7Q*fFV33OFE*&q|-vgJ|gbNu&Lez@Oy%`M$osT}Vcm1^m%3!UW -HN2FN@LEpWqFtH@>W!*~ms0nf+9fqZjeAb+1|c$a8cOEi4CB!^$WQh8sUO(oHpMRgLOv}aky=&`MF{L -x=1{2vq))E#x90NN|?25!Jzp#^_8rseC1hKB9=1?rO)Wrgwi6 -Z`SG8R6VCKAab&MsTz7DgNEphXoC`GDdG%#<&~))ZYvKCwvwFZ{P(JI00|82jH#L3Ho?j`55CbXaxW7 -oZFXgB^o{?8a7TR8mKLxJ%fgoG7U>)8Ww_v3H|t-bfO_Gf)}Pn3L5aBPYSY;F?>I3od3~hU<`vC0a&| -sEq~{ocR1uC1#Ii&UhU+MKmOo{UrXT?CV&3v45ESB!UsgdzvcFfKIwI1U%o7_FMl~VlrPTK@#iwbxS4 -2}O*G6T8m1EscpAzWV~#c6{{VmD1;PKs*Rw#&?Af#Vx^?RWJ@-S0LJQ(rj(os(%<|{uM8hXUgJ{o01N -tPTJ+GiXX=z?4e<@eTpC=j?5DoK)h7#hV8G3OQsOXczY-EfNsv7VAfxp84TCKJ_Z~%UD=gzg#(2$OM( -elS1wft{M06yxs~?9Qh>>YT>k -MS}+qj>*o>{v5cXJ)#X178VK`Z29ZY>I5d*RKqz|W6#&S-m}UWXwPMGdv5UDQ~m+|sQUo}2B04JTW`H -3WG~1vrKP33prF7?!@cOZSDoNNpR}>?KH4+U&`3XXx2HYIY2m_!71Vd~9zA;S*ZxlPja3EK_N>SlOXT*vklMnZ;s5EUpS -qB&GgQI9SFc`vX=!QMsi~<|)Q^gKSh;c~A2etXPe@4MWo2dj#TQ@Xix)2z_+Wkyd_e=o0*n(19hk#_2 -K1-kLyU#UW0yICAAJ+}-{7CGE~t|+3M3f=?HPU2D^uct{-4gjf`fxg!=NL -^5avcOySffINqOp@<|~heD>LAf**i8a77&{e5KR}=mYOzo&&iKb%FMPHZ9sS)q`z&eyc%y-oIrz)@RX7^`ql%>#_~@~bTm -FoG6u%ycczE)EzghVr@q+CZ~XS#F{XJ%%Ka;JHv;3d$2b^ -$tp8|Y|A3)Ubo4ukJ8Mx(7@Jp%JcxIsoj-GP=y9wPtdcPOL3y8eU6)OJ_o=H`C??6c3_2A)YtNdnNO6 -&fCY{Bb^c@?=pD7&DZ$9fc0y3;dNeU(5|)qCKFEpglBf?-;Xf=N_0pv%k3hhPDO%CtiAe#*7(XqMgRX -#PDg;ripq04Zy!&zkYo9@ZrK8bpRR^01e8#7jrSl*Wi2L2@^a7+H83XG8Mv8U-B<^cXz*y8#g|OImF+ -=AG`?wFJZkIZoms`&PJnAwBPXXaIVwoc<Ms -QTME86@Kff+ihO^L~w{G1kXh8X3-H_UcpdlqCg~!Im3f@}1dbPkGWsNe29eAT1p#On8WK-}DWNowqv; -njUq>Db7aKBCI%yM&c(^0*z+_h^Lt|*1h$N7HoN4~c5k5aAx%ol+V)h#F40=tSud!YK}#BYKJFh-$_l -{m_JBKlF3Df<5n8#eIw-g{4sf&c#ZzePNhHII#Ax|gJ|p)^91k|#@a^K^acK?8%2hIUI%;+UP76p4ZV!b{C~aN>)~(9|L7wcjTOfu -e|b#;Af=`13W -PP9Wby{=qx+ascoa%-y?pi+&enfc6T02mWXW#E+~0LEHuYw(FnNPgvJ~Km*23$S>&szy}zQFm|GhG3P -|tqaKv?UDN~K(dVPDML$M!3Zd5^n*V9s1^(dOd-3_*_}#WIg1@qEfpP|o;C14o+ax1csID-scKC;J$A -jZY9{O(vt9UInyp;PeM$@!>*oFD&H;Bqe;hkoHabeG -8g=X>P;F!kZ9D9;*OeY&#OL|H$VW&Ya)*Qn1uM*Zf;NEkX(=o_Kuf_?>S;#iCQ>N@9#f4whVZ)fz=Gi -T0BS-g00B5;80g!u!;NA&9$i_lj?mW7-GKiKh%wAASfq2qzR6KmSg?_fHDB7+jzCHrHgvcl?MDE^0(dl&~VQK$^_%rPZv(|H%*CR9dm)a1~~R7 -&XX@mdeek+lHOO@KVZv`&<{b+7)<6@^?9;0#uS>rrjVSQXuA&s^$!^sxM2MGWJ#7-1H*bS*5sA^0b?# -ndQa%v1V5gTbZyY_fFGfwg02&K!VzDHsC$+CF^x -9rlN#`&PS(*t*9RRF_|apl)ITWsL;kVVl{VDB0~b8&W&Ome0%<+=`jtz3#xM1806!)+;Kxu|XX-8UV* -@>%$e-p(DNB|tNwmcSd4tbk;(@*mYpT#MU`=@QOSyllGraUW=Xr!n$Pr1`hBg6xr)~a_E2d7Jnq5>>G -{IK)3LLO*g0jGa^>yf)-j(%z&@*8@6#NJs6LcFUS-&)GoCGh<%n&#Temq>K`)-(jLtSt~9Wd&@Vos>g -cR^Q#HBGFIEBuIcap+f|n-DrKIsfUYQ6jJYHvAZ}zkdBgCMzv1OO1n<_HtRjfHndB2y}(T`UqFWCJUzwkw9YW@})w9^@?odvDrZYOWqcpd*zQY1?wDG(?uC$enoB3x|ZHh -7pBw=>f*CS@%-xfFZq=pj@R*{p^hJ4EgXUD8y){CeS0PU#Kgq2p`oE--4(j&jEoFEZ{9pHzXMK6U!e4 -rs2`NgHOonU@b!uOBU6~z3!w1h%BfL&!wb2*>ilWW?;`*7?c3K|r_)UZ9fgI3Ib+9;-J{WH_{515#X9 -hU1q;O5g);X+nZrc+D|;JJ9u3E2JV=wm9VOnnr~KEeSFd5z?!`P1V?FdT&@GXi#Yc`DDd@i2T64p?QO -0p)ZzUys0_%xD -(~iw9+(tnor#hE4@?JJGQ8FXU~_zu_8|?V1F}qrAL4(KlePsUsV8_29pss4PmEw0`n;AAD=#`ZodX>(BbPyFRs@i$l-Y)%*4q@Mqub+J3uhjmIQtc>c;? -_-Ymi%a?ClxpL(V>Z5*Mvu4e|-gx7Udi~D2b?dHB8aZ#j{kCwYFpjHMt%_c;V#T( -1-+foqr!t12o~cbAw~Y^dT17<#)>&^+8fS?AuA?1dZVY~XkLDl=r=)#Qp(J0wdQK#@MTw8F;AzNkiae -vVDfDH@mC~FE^660$Ykc<9laCq3VC03irp$?KWxrxt+i(~GExgX|zn0E%yyc04EWGdi|e -hUxgFpw8Ncwv%A!}h7VC9UaT9RYJTlppE|W00~3^^@6=Lf(SB2U!oYCS+aAH`XmJ6=`73F@W$u{f!tg -Lg=~ChoKzK?0TJVE)Eg%NyzSy+c9sz+-bVLzsMuud(PKVxxD)7t3n>c9uer(P?zABhO+qKiuFS7pFYk -ibA#ATC_fq(M85@{9QF?=V=12Z3;Xg*XO4;T#2l(5L*#G0(FVMSz8-vvx*RfOh$vI^)2PeucC6$ZX7( -5RKbK8;l7Ig3n~1+jiN9^zHX*Ai`*F~3L*_z#pg*<61$K<(w)~IqO8qN)l8_e0N#u|52=fG_jeeQN{v -zA>=%;}<(m`23R|J|cW@9}GaWT)v*u87lu6l9t6gsa9b0uI7GO&Aq62@5u7r5IoXQo!WiqoEO;+h>!b -QNznZ_S?JoK~D1fYa6-7*2J?8_rsDWEzdsJK>}?Cpn~AoU_(M4yhKWtU0sbl>B^s*4WIc>M2>7+4&(| -jCt8X`Qy^{Stcq28G4P&$qGuz&+1*^*+rd|lAV#J&o?CM^YSxtvO~If_SSUi`#>|bI@pk -B%olg7+-GXuQap;8uODa3%P>rp-^nFUKgmeB=u_kKG72&>_2c#VR!>{+NO5r;Arh-E&}XVM@f*@5B|k -d5AZMaJuZ!B45jGANOoVhvOUcaFcj+74=Pr@!GVgQu+=Khn!!@{%m1ueo?xU1v-}tcju!xB0f&GUMmo -5K!|L5K@2;;+(W|}a~Q<`TqWtxqeZJOhnw>_JAb@ht#8tawqHOFhQ*V|ruyuS1L&CAXEVee4yq280c7 -kDr8e#d){_hIjg-amO)dpr0%=+n;UF&}@QV4r7wUh( -SN$&f)%rc>zrp{I|M&i*0y+e$1HA$l2ks2q7kDDDUC_fp{y|Ye2|>w0IY9+MB|(dVUJKe0^kvYwpz0t -yZ4d2J+6--;_C@Vy+R=Sgq0>T5p=(0dg+h$L!j8XYkfu^I+Vc<3%U)l3$NA*>6#2~edD&;R&)Ytq`t0 -&KsnYm?V@uM1w6y_$GG;2q$7lIn+hNBNBLN%t} -Nn0*%cEcZF)bH?X4pYFclzR&q?^*!(Vi*L2>qkjE~+R1*O`t9^P;n&&!G5?|dnf~+q-}C>@|Em8Z0bv -3C111GL6EHhqYryV+qXEqW+fcm)20j%yE^uDpOGN$Kfh9p}gFXm49@I^nsm<3;*FL9xL%UJ?vG!Z-dF ->zCR>AFp)xqAuNx?4#R|PjGx{ZA=hh7V1LScnls|z%Rn&&mIX#&A{-X-20ePVr%_|EZL| -J{Hs0ZD-`1nvpE5*V+&sIAd{6g(>AnUEDBtglPo$NMsIIRoxy4fXW(+UMQEC%|WfPl(?O0ZRgw1-u@x -Cg81rcLUxJxEOFPpmkucz~sO=fo+3^1dR$Z1T70XL>zWhdqV3G5=8YfEo5uR-jJgqKZN`o@?Pln&^@6 -CLXU=?0KQL4brhkA(WGe>XijP_YJSuFuIcEh_6+kJ;5pRuDbF;|Oi!cdv!1g&7ka+v`I_fys<+LaJ3S -A09`pRxlY6%D3i5i!tC-r>dauo1`@PP4HS?b3{igT(-j&|JcnA6Dd=h;|`i%3*@X7Tl@R=^^?q5DHQS -Ghu+353;&o<)ay*>x2F2C{l&gUl|2j8Z?t$ZKyeZ;q`uLrd}f8QwICw&L|KIQw2Z?W&oz90L3>wC%f2 -Jv|l;&QEDxZgm(8viK)KLp$eXdBogFeorKaDL#rz!|}-f=>rKgmew*6%rAW5i&m{uLsT -5%_-7#Gs0xJweBUnrXXe!?i25k-_@lVIhSfUx!2j|6)lm+F5gj+L -52`;PI=@Xhy~Piep9dw}|y -K)+aOYv1^F@n7V>-oIObXFx$fT31MCbyRE4dH(MCh*uBaiGFYRSNgXN@DET2nW^`Ap4#B6!ON -+x-wggF_+aq&!HyxJAwxnYgv<;1HpHp#BYlJVj_x7ud)9c?dXDwV@|sIL^Z&JX{wz)eV -Hn?oh$&KB5i|!WQlto&Vs~bL6hR_faYBkK -6b?L4;fgCF9J)n{fGMts;G0XOpJAU5Fz@_%-rvJCGi`FsKKsaiu??Q$X`bO(u5*)^1^${hd5gDshaV8 -RKjgmV?%NOT@AkQEJ5}es^Xj%E4rnKi#Agu*lUQ>DKjF(=nOrhpygKXaY=hlmlHFr3D97HicU0{Q`@w -#*Yy2iT)a7^iWAmx`C-=mDX1}y|>^Jr!N4t8=c=1qi$ZVqAA*9U-ZO4jHvdpIH6oR7Z -+kG=3*ftZp@9lNjL3gUDKrjCaXIxR9*#?RT;0~)x3t+^ZMQ=FYvV4YMxO&qnmn3-_l$9P(P*pywoGa5 -*T@^?HhHfJT@kV#!@WLe4<8|4X&PlU(@1it8?(JDrM`Y)w0@Fhf=C*^{hT+)zBJQfe75GJ9Fp6%Y~~U -f+%8GM;r+xiOFg5QkIg*gaaUtJ1C%t5+0z83Vc*i!zQ-y9NXAI9eZe?i59x(p^pKE7~v2{7~`1Sc!E= -W!x>Rzf(rzgVum>`v4AEcGAd(oUB+cXCS^*dWkzPDE=}o3$UG6JAp7!=xO*l88B+skyCoR9)?c!Jc4cm4Z*nhWX>)XJX<{#QHZ(3}cxB|h3wTu36)<`xlSw8o&L9i~5Q#8oG(Muinm9z~z ->J)c8Hp7%RZuh-si4wuMzMkk*RbDEY1oGe+@D)G>72ymAH26p+JaX<@YoD2f -SKII2@80jb-_J*8&e`w1_S);U*V+@jWw~G#1R)*%3_}oB3jCi(`0xJ`@Hc48>OsQ4vo@c*(h}Hw?)14 -oURbc8=Ki~D?zp$0>W=&FyI(1|^R9xL(0v6zzOTSL?Z$$8@1JwmC3$(-F0+ku|M-vR?lRVOCI7l#pV> -7Bz8|^s{;r4V`@Swaec#eGAHL^5c5~NkD!aa`oYLQye9#EjeBFw=7d?5&D|JRin@NbVGG^Cw5`x51@MGAaR*kld}=bnCInF1KiB{g&IQ0@?fPbcs; -g&)A*_7lr_;QuAOm{4De6=A?b;oELGxm@EV3wS}ABg|FEh3~UZDd)MQW?@YgJQGX+Ho_0#yYO`7?wWr -;6ubzLxr48T@Y7SuO*%6q{?GguZV%lXU?2MVN@(7wMy5VYAjP$ueyw5U8=jyhjE2#J3Jq*{gRU^tg=6@*RAn>NXjnJ&8=QC -yCjW%XbIvt2q4`e8?8cDQ6$fVBsrla7h+cKBJlc5WVa+a`w_mC?+WN#(XkZi`>^?(iEie-RY+mL1bOW -Tv=EW!5aSnb5zwuHg1&>c@fo?_$Dh}>1I -?X?Sn#5RQ+AUzl4rEtLWmY9SS -hO1w7k;B#gX{}}@k`ipbf(MWXY2gz)>1!P2#*iTe)ffQyL6j0OPV?B6x{O2Y^Th64d)e`p<{KN&+G>XL87Ks3GPrKBs{)IKGHHM5X(jCd;`idPYUdp@jJjZ#P&}}?WHW|y_$I>nHYm#4wBDnJQL --I3^8(n_bG2uH#INc$3<`maJ63&td^S3I)JISj>`k -7|RcwKxg;SF?RvhMHsJeD6jJUe2RIx>;_34 -@7imkHRm@cl^hQN9O2yaQZH(G6n4HU`>L@)kqKP>vVKy+@JK15>gi!0i+J97y&2Km+XX_C0YCsae(F9 -OjkY?oMscW_IVwENkIUbfzFUbg`|E7vc!2&^NN1BE_^M_E0PCPyE4Rixooj7+$x#kPF3MX1nT#aV8FS ->F;LxQE~F#Z^<&38(uIN!^_bhu4-QAyPaZJFiM$zzDY#9JiT^15 -T`>NF?!Z@11;5aAe7HB{hO~@7U3VB!|3Anvj=Ul3jXV%%A7*OZC?9ludc@tSUhqHLsmt`jrW*s6-uB#dd+Fo3zLr`Oy#%p}kgKN(RGS5tFh -P0AKzuIm9pZ2q3(TcR{Z(fzzak*z(ASz)hy!+{0HrfN$STd#Wq!f7dM`zkI0eQ4@(`aX1tC={K@Q69KR>Ct%1lKM3(s=h~(N3_FJ`jK-W#U7lB4=^N#VC)W7O#aZEkQKe9V -7mTeJ2gXLJvQQZ?Wj8NLQ&2p^W5x*F?^8gv*X57Qj97sICCw#D?B(eBP4oPgi9Bvn%YJ^1`uI@V1_UazoysU5!%MZd>_YywMe7>MX+2ju -3?9%S#&dF(4-Sfbei$|IT16kU)^u5XEUl##M<-Y&P(vVq}-V!8UF3+=wEWpEZJTC$k+ekC0Ov4t=e`t -|Ix`bHictHR~>EWi53|V%a{V+z~6{bUO5%seOs1kmD8H+Pq26s&+RigJm@a+romo;%i6rWR}-)jzvJuDYVc&7_R+hH~|Fs;d-L5*T6gU##>L08R)%-#7&RZoX-IWl}D99g5uPs4<`& -#a5%oxK-h5nMZdKfMwuAug6>QQDb`dQkgP)hr4DiL`ZO0y)|a*-B4930vVDcwtFwa`80-H(Fy>^#>nW!n0XD)t -S~b(SB^x?dw}GLT~un0x#xf!I|AHjgB*)r6AcV@XzR`clD2 -wYzG!wGok|+St+FwGNi)!ZWFI@#-l0o|s4<83%adzdm#Q(fnacnm*QDVPHq=}J!x>M*X?ySjarIB2R? -&t5W9o^YG@y9Nc$6Mv#n46^hiK}b!Md&*|3^TDqWBmRm>MIDCm0LPkfwhVGjkeHt{+>>GRHHmD* -1QRHJJRXYgQcLh75XESyZmaECTMFeA_=wBl$i@=+;nqFKcbx(53s{>B!F_#h9K(=BEkDcj!btg!2U)C -VG}!p;qK7+nl>aw@j^qAXuAjjl9bGEzik$f>bG4)Lc)eJ2;@z*&G7ep7jU;daDlA$8HyvoT1vK~zL0+ -*S5%9VnyUEf?w%?6xg>DtN`{wBbLknlPhUhNLA058VDe=vDt650Fxz%AkUnOO9^ET#djm^ -Mu6v~0RBQDEZx|%a1~KniC+Ol)M@zza?vLMe^4NMzKa*~G>3k`&syUn;jLfJT>u<%QGOsYyMS{@KRX; -`8t}&e`wWp~HX_Tdh!waxp|LX3f$RWl0wuZ#`+k0K3WG}EQNNUA0pQxp2{!{Ed;tGMZE}-#J?f~Owf* -K8KDL79Ln!A#N-4_>^r8Tp1Kf@n9qDNxHvqQ`qPEv0gG{e^5QZH6`8+CM4zfKE@xVOff*Wx+APqv*tQ -(q7^7nl0h$A)D$KVmLGrpS>>lA+vMCZ6>Yj=R6vAlaqzDErIOyE+AR?q0K{zK3dj&w#KaXF$Hk -IN`pkNiN%Da)=WF+fsX6ux;_Gn>^rgLfIlMe<7Xnp#mZYmyb`UWnwh1&`i}ni-&NJM&Xgj+xzo!Y!lc -DUQg?82w9|OzNwKs>1a!C8Zm)#}V3Qqsc5P{PD9ut7Z42t%1AhvI*Mm!gLqL3S|6zOz1T_s9BdlKSPCsVu!KIPXxU -)a|uFZ;O+&NYmP{5d?QjXdL#>w#wGF!^=l|AeNo@E(p5w3&YspoVB+AZpddan@7tu`OGngS9l?6*oH_C)GUe&RrSV^7Lf12<Ta} -kuuxs;Z!Ss>ZgamGOz`INs8QIY~fLzQhv1nAnaN)sRD*nQ9!{y+_^ib;+R!lu -;9wP?q)&pdIWukZx!`Z4~O6x90omcRiFXsb3uPMsCg5M@rqz7G)@W+Lb|)xcZCin#|V8E(8@;`f)KRB@f)X9DG9@VXexnG8ASM_()3QBKItA1^_l&>mRX~E9n#RMLP4#{7x+pQpEU0QH~^IgOn9jFMowxR*T5r?@$+CD9wkG)BVW -+Fw6WbLMqFVeG2#VC(mx!45oSNwMe)kuEojiNUAV(AKr7j$m&#SPAMGHc(lGq8#_CG(*eCSGsvNX#l* -##_&XQf~N@TBt9>^+t8V|I@r<$Qjk2ai4DWS1y`D-9Wq=EeD1KDQP*+fenNeuw)(^{cFSu|`G9f?KpG -(~$YRkRx#)Ye1)%e9Ax5InP2jlzsF2O5BiR*?+84rCMUB`-Y}LTlO!lg+ZEujfHo?MaWBga$qBh-ezv -wMBWPWMsmMeJP?`V*B-A&`ct>-wZ|u;#>q@Ebl(})V}OOn4)CCrz=uAZCDg^BT7~S8{1eo -Hwa4IdIbUEInW|~~)cz%ND`)%x6bClZa?5oG8%RmbJ2w}NYHTGLymIqUjqtiENn@A}f&kjnH2pO};Ce -6gSs^Hr-0F3el41^;os3z&xj_`4UFl>9qd|qHUf4u?~tYWCgSKuZY8 -JgjIUSFW5%;@p-~pPl3APKyd3M)t;4|rUelS(t#*H8UcNuFumI0z2FmB2 -AbZFjWS221-OnB>4HoqT*=GTEKxXk5tHtmkXvAOj6WJe`+7^sV1~H?>eWj?@q0npv_|I(3Ws?&DcbE9 -+i%^tJK8DZg?};I$3u65*@yTsbMldU#^7z>Yerv1IfxIIEd53N2)gHCuBP=buF+ETfB#;(jLZRRKQGl(-gAs23s{lA@DijBiR&93ROj -aNxEi=#r6^CUPf=ntXIpixj6fA0eu|RQ`Y%d%B04QG5>`a2WHP&P@3w%A0Yk>F{bGU~u%-6pJQPA4R0 -<8|9iy3TwYp!nfvN*y`W}mvRTsT8En&sFR>9Tbn>6*!w2`gV@jdw7D6Ks -7Z*jZwY+YTVC=FNARDx{N=8R==rwPy_Z>>wAx#CRVW*JOoJ!4(k-sAa@ebhJCtHIQKO7g6AvoGwTm-SM0mB0j8&6?G_J -K+VJqcHtWV16((P-rEGRpsVS5tmfX1p(D`|#Q5P9prL#nx=P3^sL=|jprQqfkiej9cb&rzlr?aI|^JY -P9qjb|$()ZX;a<36YpenCK8r$gQ9i2n_%ul_nSj@akvZUu1}F-+a+i7%s4o%&)(P76Jt?(b9g9aFcai -L1ZL)E7WQp*x`wDaX*O)cAB|ni?OZOoT2k#J-2Cy%}P7B;ZI-tI!C0?+4J!AiWGUv`~?cwTY{{2kAxl -Hk#hP8l;alOXfmJQ5VJ4`*RoCB=(`64hgw9eRJ;NbZFLxisrj$DM{~#H0*&LZ-Uaf(n`4ho_7BMj?<5AvK;1I}lu0UJN9(SChBaTi?8Y#7E_`t?vGN=58uk%j$598hGiUIaxRr6QSTk&5 -g2FtT}PgP@q>grRuLO2qI%I{yoxI=czP9N4LNkb(zRT62Imumg%1wy+I0G?2qcC_mym+Zzdi<`jaC*2 -9+r{tCIW2jb8I6iF75Ds~$DHtlc4CA*?iEt=a2e5S;}!&-Ow=NEwX5IuA{+V*>M~y$Mu3gJdjwjj(!a$vlw6s0K-O;{ -OSj30ZANSSA(-T05yxK!x|DqjefYXb{nQ4G> -u!Sg2UM|_BeZL#w# -M(B34%<}%iy!~eLObI}|8cmLL)J!e~hO_e(5+f}cWJXsmFr4o~%EXL?BVpNfpqS(wBdtQ$Bs?s5`jvs -60t59mKPq-dkjjk4lle(|N-~6xQWRLPTp{c0z{8x!a!hSFl)7eHq+! -N1hV9i&cCb!XWKK2sr5mIU5*P@DHK3_EmpLuyLtBg}#)~OB3;9Tq+q+zI+PUY|SnBRRecaXpy8x&t%;?A}4ae4Om)*N-4xQ{T#EYX%j4D7Y!vRL1^yc7dq#4_iHRC6MXFy$$hxeU@R-_$D -K0s?1JZmizNg}r6$-PA}`_D(CY+6PByxk6J+)66-C{RjchjE7y -#?4dbie5d@*|`-Cxh9}h;Q@2!9EV=z+RkQG12bKj!er~knZC%GEejiV?GFb&^eC#H{ -##g*jW5xXybNF33KEB$Z9HieCjAwXmixkc6_G=yAqPa%@ZQjQt+rWd94I{F{v2ehTeaq6=xVjqaQ^Z| -L`ycaUn24hkcfz6$E!(?hG$qdOT4lV9dk;w;r%q5Lva==E -kA-rB7YWvB*IsNYd+CPYn+#G6RSk2)QB=4XJf$r64PvWxUB;5n(Y6aQAY`@fnr0a?Y`|Q=8MLn#WAHa -(BU%?2Y*ITu**Q2@tC=)Uh26HJjPc6R|nt+U%kWptGL;1gcCGq!*F9!UQ-X3a27GQhrO;R7xpX0|tZY -dVSLoJwh@ei;dIhj>QuDHyK0@MefcfWwMUhLWr{agqwTn#O7AGE?CZ3<^UcciqlET7SM4=B2*F+A -DDs(I|DR`Zvkr3Dw5`-0pHs)i}{Pz%i9ui5%BMiw7pYL-)>tEv{zz3}qVokj~AQar4Lu+c< -ku+3?8N4^7))_jSQlgHEWZ&yhg+6kv{(|s|?cIa@y8mS+{d5ju=oKOJ;sh2=cTnL47xabz<@JP9c(*uuE18*R=fMX#SMA}8~s8Sinx8@U5_mGs}o&@G8e!4lsl -wupI}jL!7qDgke}tcfFJ&qc92i5`WlGr<4~29D$pg>FUR1Ci#+d8rEI+@1{#|Sqv}&%dEA$4wEB(p=pW@iy13rQ_W9YTGAqB5#y8Pzpi|03cb}478*tle<6$C_UYF70b=&KDI#;6C< -EVBu_C1hv(F+2N?>QWaM%yNcP?Sl|8OF!#7TaFJV0q=hoENspebCafVFKHCA6+zwMgeCjzq%qG;H -z3yB+kBZB!>Fz!f(wi$KW1U=g6!jeOaTx`ryxDfd=f6CMWE=O?ax$LX3ew>`*jEpef|5FOIo(+qv4-+ -I;7cVS=p2^RP3Xw037;f=jYEZDLvf*nZYK1?%0CCH>W%(E19bPgcS}r{#3=_Q_rXxxfRhcj7*&yePwW -5Oe~E!^11yf6(^(@bIojawQ$x70%r+0YePApy$q)VlJxBk<2Qd0ZB=YPi4C0vNJ{D^HFM~Mq7`jQEi* -o>cgacSNCFL@7DLNX!D83YE6k81EG#bSp%~5O?a1`IdD87^2--X`&Z=g8EwqZo4S+v_h>R%qc-HPYg? -$Ndp7GcRpS{TJL`um@Rn>nY}fQ+)BId<51Gmvv%V=$alWAMsK70V^wd2-N3CF4pEcvBw)7Uh<` -V16aDdH6*gh`SpztiRzx?k;(abraCphK-~hMp6Wmw25aFiOp%-uW#)d`9d1YC6$&sjHW@u74eHFBCD^mLAD>?i8|^XRAP1br`ymvqAtTyN1nPDAt3E=k>L@Ksnk98FB7#t%L!n_%B -n?(^1kaR2FIu%g@^tDM*v8P$p;_0*klP$zfVL)DBsaRNK&zs+kFQk)d7UGg%{DD)xZA04Je%wNC(oOl -ccSCp{{r2GOLqC$F?Ffk5;{+=YT>k~MULjyg3_JVhrA-UO^$#_{rw+_pq*!c0^jOTkGdAObG@?P0SyM ->F>ze3YDRfcb*)1!bl2(5W!Jih6^#zSI9QL&isbMiWt94`-GU1V1m}xCa`yD~M@atAzSZoIjV+<~q0* -QyFmEcE*>Y63C6r+}|ApnL%+}Bsr$20!tJ+~+J3q%mj&|4&ZL%?b-5-$God}~DgmzUs-sQS?`Lu5M4h -`4lfY21ZezbP%yQI7^=`fP1qp}rbnv3h8VZXZgIN)MBYtbKCg`r`ZdL%{3MD5eB259G%t1z}ns}{(SV -k55_y4d6h>L0kXBVx7wp95=F*7_~rlMq5knIVe4eJ|Vzn$LbXjq2I9^C&G9&ryp5CnzECWpI~&jKtc2bj;Z3Z1e7?$0E0Srw@xFpH00|Jj3Ov$*M4W=E;Z?_RUcF~t8-Gr%<~NK56#iZeQ2$$;0$wNieV<>M9 -#n~piJD4J}?+J5)8MC^=N?(;t@SksS)Vp6W&1!9RHFGFHoeN9=411jmV+h9S;Px_Ya~Re60mUnkY!ME -T06p06)_zfKnorXh$`dW# -F@}IGjzmYVP`-98Z#Pt;O@t?Jq(HwQ-1tyoOD2^Xt(|ue*9?8X^c-!(!PEjiuhRma!Q-PCNdej&lg7w -GZh_SP)R(e5aySEw=K_ou6h!2tJ!S)dSQ`mjflbYqtGc>lkHQZ8qmEtUu{N;~5VKMC?T&erJnd0EHO9 -|NjP96484-Ed-W_v@^%0^}lO5`3eR9ManVJv$2=dfDU#o3tz>)rret@A%wYBf#xEhOPcc(H)d!4_{hP -O}-h(+O>Xh))7tTrB=f$@+uRv*P9(jw$~Me_jdCRv~pHrZvgMJC(BhaMT>W$U5(^Hb1yM!#8YFMtumu -K@%{FS~JSU!QUg#a#jAb-oN#7yYzIu5AaoHn&O7gtcAw3CvTn7bwF^wzDnd-}BCNq)rJ#tpA#V!=^fx -d0oW~7Jb+|M7Yq63W~DzS`l^aJ}!pWLrL*1w7PI_koo=W{v@8;BH>}f_Rvyo@cFpqCWF4TypeX_5ScZ -sq^GHN+3o!M0ibxpSZy05g>Ccp0VwTS`W={W1*Uk*muRAyY*$8+BT@qu%kyHfEG@5++O}g>H?@!!8md -y6w3f;F7ClpRqUQ)!{tjIgvl|ARUbNlK;uw#Bh2X!xt{1~@7Zzjr$PGX@Z -g4<74}5pR-yWzBUmE;%!CxZOsXuAjm&NA6@CP;EgI9rvPW+%s@U7uT7k(r{9bL7#E+IbsbiDn4rp29z -E@9mF*K4V*ylX#g<@gIk&P;Or=}8pVUv~_8zUG0fR$nI)XgzasG~ESsI7zmVN&~4$k>!~{miTbA-At{s0tS>RGYQtpf+r8-vhd7nic4lU -J3yLGWC}uLialv3CX)ACc<7#z|7hrTtniJUGx>@5@+H0WDVO3)ryXg+Rylgb4}0;XxKoZDwP{Uu?4VO -pzZesox2rN>J^NEVGY}npTPFzE%O=}hk4}6vWD{54pPyH^l$`rQqo6mt;)GH4Od&@|@+cR*u?UV+G^W -|bw=5XV77_KKsC%e;9BQK@Yf8R)w_Pxyds3`MWV*|N=Swqg9N=6}8@N)gib<@q)wc)L|H!PpxPT+Hcl -R1bi4iH)p4yvagz@zmfT{){X~SRP$!)tX0mD+CrW28_080d-c~`z+82*@HQ}>OT${z16xE9pGdEJ%Ey -qDQuw21Zdp={kz+|LgT*KUQDp@Ru~lj|D4Jp+NYAH5l_&cLMkf~ -`0i$J!@aYOc4a8g%pBI-)A6E3c~SkMFTlRhXPfU&Z__1rrzB451ZFz>Yt%|9C}xzl=8L45t98&L9sKO -E0a^v+lI`T5TNFgoR^IH_v0mp?R!>t3L}nIyk}rPtp|ld^>uzr`(AN6j!Q-+(ah7ydn^^yIWI-?;bi0 --s1|{oA>IOry?(|1*viPF2d%O+a;w)eNA?40w0cfBM8c=Qs?^1#d*{OQgowCscRT5aGOsWOF?~(y^uE -IK_q0Wf@I=n(JRK+@1C6^@-4SwuJgF)_Pbjo2k^(VMxr~w7&su>>x5o%bBez?uDyF+~JC+TE`p79!Nc -9|mZz68%(`2J9;MNun_eXse^e@i9;8OjO0Mp{QTVZC2q3{GO~6_WwtWL+b}NK@-km#gZHv5-R*aO8p^@!kgd -gDxlr`4&OhE54O8Nqo27f -^T?|sIA_ESbrfBx{XFW1g#p4?j~rns!6tX>a$J+ha0+T0K8U@q%9u2%$kG=rg|?cQ;%3fr=(tmEfsa@ -LjXQKJ52~xZPz~V16KL=2H3q7QqiZ|G%wp2WG%>iO14wfEe2S^YDqswdawr>rr`|yoD42L7&H!d12pD -tX0!Hyq8)q1Fs?HP>k*%7^;!2u78lF$=gBW@HEvENuUaY__8$>)HX-C){Rl~sv}yuy@)A7Vk=*MPM>R -(OHK!rw#d+k957<{@A0p;RK+Lr2GDj#2veVv85OUOp1w=Fmy1!TKe$ZX+X@TWE -+9!$Lk6E++Bf~l4apbW!pw~H{COMH-K#%B`7MG5rz;kaXo$c1&1pF8eS@DaGqd%rTQtSDIcudD9sj9O -+;Sz|tuChNS?ABQW@8}?3@qr;|60)}ga#gQtAaDE&G*PlElCM3KYBrAEcO|=!H~s;h+~t=XM&9@Yp}_ -tC>%kz3K(zeI!)RCA3$no8%Bd{92R3A&z^;B~QGU#trMd*NM)B9&E!~Y{&)0^2lH`y2%^+rE-7N^qsc -N^kz;00nds&O0eE>PbF-oCRcWR%1oSJRV$Jhq!7s!9`Zi=5BN%_$5n*L()$0@NaC6N^jI1{G$Ba*^?_ -HjxI8^SB;RwkW2&~Kz)cJrfi5y-zsK=`GooSS$f64Xpa^={sUAp`nERC&&l-S5@pQ84v~TJDcw#v6HP -*79#qkLXs`%kHBKb2SUrC*ks4@(nsg;4#Os*yZ46NYFC1Qas6M(=M38x!iAX3Z?5)YB-H=h<4ukepUQ -IB3OoP>?fpxlMGoEh>m|0&z-9hes$t8Ayh=tnU(=Ex<8VB3`L84IXbz3^w@}%_IPBn3p7uCgrDUe!w? -RPg_>-~FHmfuR8HO0{ItQQ3r;eItQd_OrTw=2aJzCjsmwi-@~JA@shlmd4O%m(R&`bR##gW$Rj#Cqo+ -B9p9p3&(;BII6v?Gm!*rHM%F~dqA|byU%OqqET92Q4^J7{fG2+i!xkI+#%NI(OZ>Rk5{zCi||4!2102IUW|fDVWl0N*v=EzLdU -DW%vpHWGp&F=xXFu}wk6`-c&Y~zn;t(%HzSOWZ{}F=*?G|12aqQ9tS^S?!M+h!AIkBwb0oLHTFGJ!`X -KfMB^-8~3wp*wZ^;g1(L4Gtyt(-2!YM8F*Hq5;i7T#8(49Kyq#9(+T4XM*HTu*+do#IcDJ{D;0dF$CH -TaFg8#ctqIcb#b%|P(XBw*hqK5n(*!`+W;R}(+x;=Y==kNZfgiTgQa -Ruc=9DmAfCtj|R2Wg^7wA!=fg7;c7!R-TVzsxaEBy=53~uEx@U@OH^X8|7`idI10e0f6@t0RZ%Dqs?1 -7SjhyX8A^Rqv=Mf^9Df+&3mhcqtv)1@e>!e^iNd8o8+&MG`VB2!S4FWDz2Tb%uj>YVnnWH|nCJ23=%5 -F%s}Yo1;(8NcfC}hk)xEwz0deX|JuPx&5`uBOa0dY-(P<2u?h33#-)e7I3uPjH}lW5kIstc&%-Ep(cfRqHMm>$5GSw-}JwVl9pz#j`GlRD101&}76C8eW+>G;FHQK(+3X^Xgh ->Va=+o+Oxaywh!NHxW6)G*2#KwJ66SVu=Qk?m$4B=Y5<=2?lHNl8w#&9oUcEMM(SX#mu)3?Zx%pvR|` -6O!=qPb@1d_ZlvrtWNbGpsel(LQ=ae*-LEE8W&}{1lJoNf8wKJoZ{CQ~LBf|#e|B3Sb1M_R$7l-V$78 -tw(yEP7iftdG~4^0HKU#>Uaci*;=43K7;9JZ;WPcjs>Rwn7}B}eB@-~F -D|d&DN2+A@tyf$zNRB?$NX~M>7p0ZB$BnJB+LESSH^9s^>C{CG!;I$EQY&6)1zzJ9@QMYB1fSzzqtzZOQpUpP#mW -%)EEdC=yaju_?>KKN{yl%N#4!*Fp`IE4oF}ElG4Ke7v049s)n{7_!& -4f1I{wFQq+=)AxvO^Y*q4pk83Y6Y_QhI%$mX=m>h4!m%Z@#I|!ZwDCT@+TX57W*{^`f2 -6JLU@Q^OdB~j?xC3!-;QB6?|ax0F5?vCBW^sHJ{*{&4h+N7%PA8 -6w926Oz>Zup&mKpQV2XD^f};awYWrJdAWXrgnQvVMTHvsg>?R&vw4P{hJio9=rvz5s!31g~k(G(^r=6K(W-7Qm}84x+p(AG@f} -1$}vnvah9KpUxUiW;#X$5;CaRp%Fv323&JMy%qmR!`{I2j*(ek{XY?A4Bt+S8tNDSw -;@-STH?{Wp}ADGoXMD4kWSF$emf9!$Qw+ZLunzSYm#>0IxJ=q$@5dk~xP;k7?#)yfM*{B|koIY#aUe! -Lrke0tBLeqN&JvdF*iOMNKaE{4%RXmv65uF@dXCPyF=NK43dN3h$)`|HYnE(ncm4T{qtu~j9pS}>is> -&lixW-=KOB>rp!h4`_+In^iS2R>&l`7((ndY{fBjBzZj5)=N$Ugb!ERCkcP3d`h|66 -Z>II9p*>R{R#*0KNMo(5aS)=nDMESdSNKViwq$ffXeOZH0-%^b9a7{7!6p8FAK}UAOcbY!c~i@`;q5p -foA!J;XluP8Quae>C+H9;E$;3Jf$;|Zu$|aZFN(5snIxfpqEwZ%}8Vap|Mqk^&6~ -gJSqg87Bvm0p1&lg&42R;=j#3Oaz&`D8wCkNm)wbC_7d4picnvFMS!8>8%0^Q;CAMUpaK-;r}?699DB -=s)D;?H5{O05{-~^A&S1uLC}S}VIHh&TU;M(v}6AnEtxZ6h&b -eMdq_w0P^N{uL-vUU%9M#&%47h@mEyZ?TInt1hX%7~R0b`>!yK9$Ry3!n=a3p>G-DdUz-W&D$RL*q>_ -;$*$A>~1c}nzmBxv&hkEB%o4hBP1;qF7z1pXT%yALm%DOUy1;iuPFc!L}rdRTUEtC7)SxnmpOK`{N& -Ks3Fpzg>BNtbg}$em$-ShB9R{5`&E;jo!%UE8$tU_InX1+y(6R|KLmKF`QR~kdW`?-HP>p=N!D+TLwC -|8GJr2)SY@dh&Oqb%0->{T`X@q3aT%j2~V|@w0rTAPfRZ`oF6Jk6f~I04Ys$HcnPT)#lq -FL2uH^NK|A%^jF~<0t7xLI*;HNC_hifoA*HH2d=YB|6^E{A1(J}FP?^DdzDdCS%+vo`5SRFDk^$v&7D -$$kut#znp;ZsYU0$-C`>U!`CDg@S01r*5e-X=l&%?IR=s$xo)VJ1Eb9j>A?{Y8`=Swq|y)JvF_ZSFd3 -L!~6f@Jowb3BnGqaV@&xogtReaBlhuu%^7~)hKiu&aY}xUi)%Xwyef8S*>ns>gx0omgioLMLG=U1}eJ -|Qiqjn*SaWPdkdziQ|(5?s%L7-OL(G$%l%I(JMi*&GY0i1M#{U_9+e$hv<=@)S2dC_F0gQ?+5US(nZF -brmu=e6m&k2T=5HDNt&+bb{y}eZ`P%{hwt&BV%-Y6+m2@D5KPK_qIM`WT5lct0kjuf^p6vG -?$Mv`UP-@nPf^R6V=P$O;XIIuGJqLkv@)Vum&!DlUggZ*Nbo;v2kraurb{-Ksr<;g;_5-!=(?G7?(3E -Qaq1WsrJ}5X!<5qxx5M!FC?0>-Yy>-K3&p!ga(t(9t~G$jyH?%7(N&SN;UA(|KqmclMEKdAVo&A2z4f -_%fDv^Gb7#_bnh+b`DWREf<4poCg-9)|j8{WNI6ak<2pi?$@NWX={E%FfS07*IxdY-4{PogtKZ@a^)V -7d2BDgfj6Pj9XN(_#RBux%W(d60eKktOLiH~MFXDeu?UndO(x7+fV>+35GdItujF(ZQpR=4D@)Ao0`A -=b(F-(h^bepnf6sgHG~o^2Rr2&l9UsI9TR_p+Dt^(GU9n>1W|cfqwzict)|JuSR=KPB-Bvf`0|))Kc~ -hWDng{|(fNc+0W0JVSGt?)psLZ%78W8e*z-7*1H{=G~+iS8xNVYZw+#NNdWW+$OF7&xK)_jgVu-E{nO -r$*l77Il4nxaTN+qHXmu*TS6QOwV82W}u{+Ng-k7KlN7WrfR4*~4 -H)|z!SY|t0eHz|`?2X=Si7e^#G%TrPdnfPlq&{nu -B_*ljyER)sdNw>eluUR&8#1)zGG673=96=^j(3tUe*PU)UvZVS-@zj -j;!l&yv(MvYzI8GSRD}iWGlh}t|E)lryMMCG>Lk`nnhU|)#mbHwFxnrTpL<&6hT}m -0Wk!%zu(LLt04dR3h=hpfCjfSq0tMHP1y@Q;P2l#M<&fbD{oj${P&OGv#84h#Mw!aM%DUJUw-84{qKF -`!QsF}b1r)EC72b*D?+LpKJ7f1FO&X4awm$uN~wTItEP$8Mue=oV$hccwvtsXrK!w%Eo(Jr$YdsvRvf -WF7n(^aXSOjjW2&zGIR?yy^L!(uz=KRKrzOlhsOtkoqq_&UTesIDo` -@UzgVs^ -p)vi0pJ%s2t|^qBb68Ky>4HUX0(75TjzW!cK{@_(lQOm(f4B*^g9p -COEyte-B9leB5E|`m@Kx%=SK+-^CdOoY8}6;t(~;cpoh;VBOos-ofRjraBTL%E@SiY&wGyU{i%3IQ;}MJaZm`nJ_T#Ck)51?pufQKyVZ5@qQ`56lE0R$Q^Y{+P+@1RtV*Jqn}dRboE45IG=};Hs-(Sk!0f@5`660TKC;r{6dMyS>09Kd -p#NV)!ZK6ZKs%LK1>;M`zzn^ejn>Qe+5-QqLHqMR@fVPFG?d8BXt96CPI>&Xv_fWnKqqB2YET&zi}s2 -?IcTCJtay?VNmjA1vhM5+D%$unNv>1I?^AIkPQtBt4t8#xL&01DgIT1r~!An6&~87fCtXL-G;oLyPea -X(M?R^4BZ+S>wYikTsG)S``P}^r`Gn7rPb-$`Gk1wv=w2MRa=-bJpVR)P?BeQQ&18=(hkqB1ucziDyZ -jkTC%&7XvH9OOfO%(Y)wbjymUTLFcKJyg?{3X+R^BG+<;A -h*kdp4n$^GsKPudHfl=o-1GM_h6C)z#`zsU{mfZ+S4iT5T>6S7cVJ2}(*(Qe%Ovwx-F(X2nwta-6%`- -Kn(b6X<|&I;`NXsko~}dJ$K2Y8&x78MP%XSt}cB*|63oaYdWBqPZyMEd|vX??ZznFpu8;g9glH!IDN4 -E!mDT+b>&zI6R1F*FQDq|7QRE-!$jX?Y=nwK+(?0;)>iM)oL%zs+VRJD*zewtZMZLr5&NPrUJj(l;$_ -uaLnyE<_*d^aYd83qP?h%#i&WZUu{O8c{v2edgO!T%71)9Ijh>87Rp0s1(~-K+6Ot}itM6TQXlbQbjQ -vBYloebkh+ER0NVx_%RKgARYiJFXTZ8mu4)U6+vcxozK#ubP^6P=m#tIn-I7&Qa>chKtGcI4emY=n3z -Td?^D~7r|KWGIj{r_VCY8bt#08JwrYo5N*b#;LMFI9MTE_zvW`Lk_ihk{eoglLt&;J9Zetta%e@-XRNp^tybl_Z_U -oAcduVKgYImT-A%GMsYAAQ$e`~I#xk*Y&=iE2HxdsH<>*ZXvirjtKj!`I9j?w!lw*cfj-H($aem|NG_ -`{**vS^dxrW{$f2!SZ{*!06Qs$@FqBiPZ+;Vi{?-ZPPZ#vQe9hU&(i+VbZw&Yzm18j!#UI6UPrs?JwP -M_H;S`3Y?A^k){+Uesb<(?h#-_h?%5v!LkbI3+x{dVQ{7c)cO`d1o#^zRY>8i7Yv*2&Bf#LEpE?E&is -xo5k-WSf87dkS#9iUicVp}t$0BF7Fm;JFN*FEyXfh3Db?F}uDyv`UV8Ek-kb_R;5Y`aDLTN9pqjefH9 -40&r`_fRk`wLAH^2*9H2JdeQ4q3y_Pew)X%nT-oU6M#2Cw`~lg;@)m{=N1V6Q+LL(sEApCgUKcVIvde -BW<<6g4%#@y4R3e{BfOurPuMr!u-)822#Pjzk$^4rrUz=s-ui*L5rt+_*{I+ENZ)PC|cS6P$ja-l)3& -nrLiyO{oljZK8Md)LPLOTACg0Yqxh;7|#>B7*SwcJK^X8?BgEUMzAx_O_hrTH4p3A4;!9qR_X#Tz({5 -Ao|llCLDP(V^E!DM>w2^oH-D70GxXk4Cz^E{`Py+WR4=4yc;<6Y6cNGQn_uI#V#OHtb0KSS8D=Gz$zd -obQ+g?BtBauVTAkEdt>=Gsod&n}g&$Peu4qJ04h+Y|`$3k~nxp%ARtoNn^+b7Eg4QK`1kJVV@=(korf+Nr9mYuKjA~zGu)f(})CMw!P(3MVag~D*)VE(FM0Vx-Y675!}sCuDPfa2)sdMu^hr@qg -}xj|G5KY4O7W0EhmtXIg+r$KZx4MZ7sc>SGo!6PM*Pe$PrTGWq57BlC-lES_U)IFkZl6MwVJ=7xu`(x -O?xCy?X`x6F`MTV$Uc|ZO5r<_cR9Wd(bShx5{BcYWH3o#^C@wwJI&D5Y&`NB1_-6!gS}^JKl>Ft&y>| -8dmfw&Sb6Ik$;&s%YqgV3iUN2+WBisR%?RCJX4?n~3qM|d;$LLwERxludmeZPQ!m?FC9+$#%ApZZWuh2;ECq=;Q$)8|VlAY=3)4 -z~2_x1I05jlJQ@7kkhb_XgO#sGeHt=v|b)I{f)77H<6a@Tz$I_ug@)Y2poX!)?~jHZ2nDJu0a+Ck -s4b;Rg}P-!lcGI1`U(XLIj9ItMA)j@l~S3~n@=2bvphHpBJA`s*$HT&IFNBEaOz-^P^OHazB~UoV}FK -=iS7hI7)*9EfAlzI0S7I&MV|y0SU0O7U?Uy4Wvv1qA#Zr3$VEq}q(H5rh^PR~})<2@UhP8P0t-@wQt8 -YNyg7KK=$~cwJtjRsSjC3}H@M=p1o{71U_ZYO|~H7hNUs$#oQ`23QwBSsA_t6u*joC`h~QYW$TUC?2{ -S)LAqzoohx9vQ5=k)uXvnMW9RUn5M#-rK0tcyJg{d@dL*VV;RV6&JlQS+6t5TTx#%&0YJ;orB-id1sR --Yx&rZD!mLR%(5)({j=oudLEY^VTW -1dEd}&#_%Wtfkm>tS#7$%`JMhga;8?lBAe5~s=U?0_NkLs)WkCm3D_&j38PQB?*7f$`44cn9!=J4B?=!rYAO6$_`_4SW3oWP45O0@ -`54M=|!rz&f=q_yfRb>3hrR0n*J#j|4fNhqfX#xML&DXjkFG9aHEdzHn{d93r8}8=?xQ}N*4=hHZx6c -$cm2BUZ@Pedmj+k%mLpLH65;!GPaqw<7Z)$s2ZIPo-=3!|cYXtyRtmFWq`*=89Sgze$)M7Yi+(-l~#| -nyv*WI^tr4Cxkf*&GAD-()|`Id(kR!E -;XmOP2p1+Lyh&PO)Lpo%eC;SrO8GLO61AJ{K@bK)N^0ht5UzKL%(5_BZkN<&YBQ~ -Iqp}YM1H(^Fg(AkLfn7*oM{WCIsRr!oC9QH_2%2yR4u~uuA?I9hzrQ$)IR$&1;DQUbI -O;M=6KUZG{+&@pHmsyC4c@8KCF}SJ$}}y -Ua{qbc*TqE`>*keS@#WyS8Vwo;}z5UhyBE#&J?fM5_j=bf?M;Onlr7T%2Hje`} -_HYJUIT{H`|x7+wG01Tbd$(GB(gUI1g^QyeV{@Tm240gPjSqz?>WZ2gY`jGsLzoH2m0^@IS%X(t9Swq -1JK0LCf*e+yvj{FU(kb^zlw2ukhBVW|Md-~B%eU?lXx0LIpn0~n|90LCc;0vM-I0OOSZa{?H5KmVTs7 -$GI^|6=b=0HUnA#_>BWGAufSf`X!s3X0-_f{CKZ$e=SgDDGAkpbSE>Ov8*~xqv`{BZh6Sm6ny2t#7Ma -y^8yWic4i?MP~I7mQ;#p>iwT{?|p^=LGAtC<@^4>1|QG4+qvi5_1rz~aXbgGx9I`KIs^G19$;+qI4Y& -<8J(66Fn+xf-AkO^Z#cjh|C8ff-GBN3u68={Q3NU62Q~It{lv(D=}jTOVi~#SSzMbUM&j&J -Q$}=8wp&o)>bQrBd2L{nn|WSNV6sL?O~Aw1KF0~LOzaV$U6i1tGq_7_z(1I3ZX-r`7OH -*ut~qd3yoP8?}$BaSrUevTH7H2%`Qt9clEacjQVYk2Ic@k2HSiIMR4jKGL}FW=9&iG`ypK0gp5mJnc}y+C43 -fG(KGKyiY?MXT;=;f|jGYyVx`0E5$u6>(=<3dhHBI;wVhwk=VAO)}9&1y0Iwq-T}soI^F<7@sEd~wfh -~18f)=MuNDq9;`lA<#(mtk8fR5L#RoR)o^p _Qn%dC{YiAgNPF?A}KE~2 -}-O_%4Q(rb}pdIH{t}S!x&hAaa`@4UKW*U{>Ex2KGncMIZc4;C#01hpl8I$h^=KE@<>C4vV@rUa2`9p -QRishl0neC+f)oyUZ`Nl-*pBzib$3(&B6-M%b8qU|0C};&l8okqUahWc7I&^zxkZ)n|^xD3v4Koxv)r -JaP^p5!*wPDb0SHzbY-OPCH;QJcs`PVU{_qr_eD5|g>w~zH@#E8 -8Z(=ToY2)6wntm*^<8Z{FuGXP`JOnbt^BT}028a$V^0Bp)|PI^8h6gC9TR|1)`F`8N*Et=W^m9jC_&1gW|4GDeRg0nAX@s{4}dHe~OeEybRfWe-tG1wmtHQ2usb1$ -sKNzWW+=aJ+agWInF<*9?da1&1a6RW4*z6$q>)n@V|Cf#3VdjVeHCmJw?qPu>x96XM}!BUqx>J1uInf -vMz(s;DNU0S&S<8a@w90NVd!rI-}FXviBcpc^2`FRZgc#*PmiORC^8fo3rj^%pC8EsbAWAGdezEOBgd -Kc8n!jK2?<#s#{9J1Ga_DlR24fQ_}LrDHYC_L6F&ikoq;|l+{W^2FKJ<8$pCGc5$qVSKaHa~1bHGAEI -gquCat@dM1#0QMsKe -**mr9`!3c*(@e!=nwbI~v4zK7cyzFkNDWK9d94opi@V%xQhF>DMz3d3}|DC@mU-Sc;EyX=5V7W(FvUP -*92|@UXB(^NlC`hu>#crkd`QS=}M)e5W4+-_Fchs!l-qrz8GZofQ)rKh52E8Ks@_hfWBS3RlS70dL%g -x|=!Vp$zyNIinQ31B&`m(XzHeUw<`AVh1br*B9PjD-!S%$V3HL2S?qdu=ZDN@KtTlDx$0A2SNoO(p9_6RqJ{}t5l$Ef@@`T)x2{!GK~t -f9WfMEC=*;y8^X@nK8AEFIKPe!{?nA!UWMn~tepzax3zlHBeu>TbJ0HLqJ7Lo`xr$thJ6Of4a};?mo4 -mfVM`m9b8adzQ*8)40Rvzt8kQ^kV6PEM4%FiBtXwV4XF9{K7+D{!blMMThITtIz*w5B;g>JlFN0b`rH -TV=AD5)YYqjjpW37W8cI#C&Bl1+pNJgKixB|YsM47P+n%U-sfpvPxomzF)s@`L;gg=XiICal -$7jvqQsuEWu8}TVp|zPUM|A(Xr4A-VP%M}#QJZ;p7?owSk1Nm5q9yos`v_kdiNJf@fkAqSoT(SZtAeK -`FT7F73)^&{`cqcU2J8k`x^}LDRqAZkTs*Sqw>NIU>k-j2U0Zn4<8T;q -xUT_7A#-a}3WtlE8XiQ0wqCAXw_!c+W7Er9^i#oyxby-m$_L=BoZGZ5wxAtqO%%lXlR-#C4FFY!Xv=u -X^wCi_|GLe91_cfrPg`wis8`9yqLiBi=?ZA*82fvbGx#KEB_XtS5^IH4vX4{aAY&gj7gBJUc3P%NEyJ -qv-jEAZuPkeUL-Ss8M@n=*d>Zn3*FHkG^#tv83nigo98Uvad!<~&~+L?R+hk5P(}^=TFe{x{lg$Cm%;8PU&fGopsNl{= -aED&JID2hn_Jkeh?|{soPEnG~3hMS7_bL>GGM6tf(gpgmwmGOyMQ^ue>FtFw^C^W<#5PNBw{!0ITRRp -G$JabITG%N-q#$i%bMN+mraqo>^3j+3K%&wjC;JB(UMNdG4kH@_jySWaFQ~!BRZ8IgaRzT2YYoDVNW- -~ZqJkUyaU+tEx^a9a8{)egA0g~`V&}rmm<-6*+r1et=t27RL7d5cc;;4~(C)tIefYRKYkR$2g|c{|Eb -h{N^jEl~0W}2sM5U9<-<#!KGVNPi={79qS*BE`wItZ`yw;zxOUc=+w$ -E@8AV0fDi(MABfz?8lO&*)V_-5bFM5<8vVk`w0{5A9~nn5@f;1*O#*;Lv5-NpCavRcNdY<0m91Jc|lV -89NWFxBJIJVr*$wtM=W>MYwZR)x#BJ($PS)u*^#s?T)3vTf`&Zg!u~ETGM!N7BZ44OyCO@ -mgNRDzQ8Qzp+kDHTJsEW1_PV&+loaA6M{PYGV8H|Rn-e^J+&&k>)d^7HWpJy6e`=73l!}a)5p_c?Rtx -c!ec$hz}E_DuMcgI#(y*A;A%o_AK9CYl(ck0VBgM;ZrWnM=pgm@=3G;LNDB_+PJ4SMn7jF&Msdsov4IyTW_F~Ryw#AI*V~iytkO$cAuvLvT3C-UiNUoK;HNe -&c?&i#ZXy=g>HCO7T=L;#(@)mG?cIL+1q;N@s@bMLO;B%&nkARVrL6|v;1ZRJFQSBj)Fr$?|{>^?Q~H -+Z6R#Od77@Y2j9Y!jP)m-o%X<<0i#U>)x9jC8uzbneh!z0@2zI%6?(A;gH;%uU+Y6Ju*{!;Z+h0(ruizlC44`n-|sQ+sbJRoSZ?e -%jC*iF#Ub(Y?`sJRNO#?CB|302V@tRf}U3AMWh=dq-iV)^Fn8#qOs&V{WO?czIaI1k#gN%(>?f1n|*n -z3QmyHWqH5&V!TZl11vwG!WK-CvUrtwWPBg0$jUzOW7DZo`*)dr1s-@}PX(6@pXv4%Oc2yaiM2D{mZY -=>ADct*@DOls5f=?_5#bC$rt=Ug%Ao7D;b)f30exxW87l5$~^k3YS~{t@qc)^80ItYgHSaF0k#&wma^ -2!5eJ-@djJJ&7)BtWbW(?PjUT{%GGbz!|&7tX{rVrjuDD2pF1mDhzg@pa*smpW$m!k5Ymtm|1JUF%dxXL+G?arszx55}oBhHCA{;_ -O%A><4gu8kbqq;);J(74`69YpWH7hiTb_yL4su^}rpO#?rWad|;r#lU=mFJ53w56Ec6kKPVrw_1sRi! -KkdhVVQNi-gO`DCj(DV0(}(P?KN(Ad@CJluToWX9tqm_$DtM$FIH9)JUMI-=2rwP7cfr1K77TsI8iXQ -ITeJB-mZGAy$g;shq{W@)=GJ`b&^7?wr-JETi@fWt*WB!Y_)Hus_0=CzT!HOt+=k1S6p$kn+^(qZrcE -C28K@!&QulOi%Y!qyJA#DgWTD&-dUxpu-*-isV)qjlQ@AfLnMoqYY>h0@1*hWDzpi9zu*oE(KTF>u_El7k{T=&^o>qC#E>4vLu<+# -G7nBvOuPCd)rnm0jXl$V9i@nvDYB#e#0RunC+ECVmOT(B^$uwoLv?h`xhsz0k(6MdNZ;o|Jsim=VMT0gF~I&Qulj -^)k*Hsg*<2fiZUq?RD`Z}_v -#dTy?t-O$Y+{l{pvf?9rC0Wl_k|9TQ#Xms#zsmM(d+MUXR7I}Xp3{R+@^vL)H5Z{e*nHD;(KWWwTE<( -=-yjd88?@VSU05}-0jX)D>Eb?(E6GolI98HXMd{bL(tioJKk92;b#}_H!@ZQtufyeuquP3nFDX}0|21 -K3;j^5tCnuEX>_>1p8LU9x*`v1~*Re+_soLDQ~l8dv2vlxMC_QQ2y!?i$-bf0{^?&{F$fC;jBAnq0{ji+Tx#W(8JNi -DSG=KT6|eFuADbK#^7Yp5P=>Is=;p;c%zEfL@f@%Ma4YaJj}OKOK0mrad5qc^{IZz7RGk%00s&V;N(t -BZ7&C-Ec(X9<)ig5uaXo6u*j`2)(|)YsRWC*?EJM@HHk$2|>SptGakoNdst-FBF9d^H$UO?ktI6nv3oLd{`(DNS2=t)DGaTyx6= -W;GA%FZLpMqSJmIZ1n7LGPX9nakp3-*4N;6X0bM`uQ|wAMSA_keYZ~Z#4_<)Y?`Uk?b1oMDvUcR9jnD -aS6(g7CtRP=M(?!VS=i9aI%IPg&#+#b2XMrX74PWnJd$)@uV=n}=`cL_{?Ar>kjyF8PflQ-MT>CaV;8RyGHo3a~!Z6Kq1FRC8Z;~eVo|s(gM2 -?$Eg1XHYElm^o2gIlyUsTJO{_?=e5AGd~dK=EB2R`a$nNjj)1iZ!200mwgJd`U;Uh;pyl^xtUGV?#Ld -?AIyG<(U6-4>Hb{yRl+=wZtg(p|aCYXZh57&KV*7#7{v943GL{Y9QR=i(ANd38VG9>V_z+uU*+fri;- -h$&nwdL8;~k+E?;<}C%%1i>*6Rw@MfmV1lzTk_y*Rb1#@l^<0Kb81>W}AYn%tv|FCpV(g`Tg^vwUO{k -0@uJckUgU=Nq2;{_F}y1M5Et=t*?o9D@CU{0#u^X%9@Zp+O(}3z9zkJbbi9PAAHjt*Mpb4< -b$HIDczaMCP9(4wDA}TAyxpx%?@qjN;^L&L5FZY%k5wvE#dt47;lq#^Be}@oNSIml(3g#u6K6s%etDs -;EM0*LWN57`jTMP>y@>aYspy~j@9fD*!;v -_eYK`_Ykd1Bz$m#9UXrpVjeed>acb#iByB(<(?{1ulwD7&{N1>;}E6#P%Kd6fF?IpUm22U|>!M3L?ex -Fp0JDhM%lHI_npMz^C>u^1z)UB$}eF0z^gWab}U$C7g%=X;Qdtz`yMEITQj -w%u;W8yLpaSMpS?pCo!r&KEJN2GhMVLqvJpkn@ro;Q1A0)17;=69M+aIq;h`2_Zj=cSD>$`3oLTKc?# -T@6s& -Yg0>D{r^SPoN`rkTdw&+RwXqc7D~8}V6MyUwc)Z5!A-wc0 -u(zEOJuBI{m*Hyk?CE7uoX!M!S$kC;}c3UKpM1?aqDAa}4s12)d-3u@5ErWlq!K0H37YNw|WAG$T#jb -3WB3IL-)jdlTOr$M_=d;u5jtI=LUd(If$hqiS__6qJBh5`v=FcdVfM>q?SN&YhK=Ie~zX7a;VZgAb#J -cIOfznn)bgY<4-mG6?M49GN6%P-0^S71k&*Ow?NgRLxwM(QEPv`KozL?J@*>|kn|S_gcgi25h!4}Z0n@h*?Ig6rOfX1jSB-Wj(XI;kVmXA -_H%X25j5D4=n9aU7%bzXR#U1-;I6H)=So!R8vccYfj}^12XB(e-##v{V9=;J3HOmNJxpFP^qz3*T+dO -}%Jg4!TaZnC>uq;(?pN5YRS4$Dad=^?;_^=;Q731+j)yBA}&6KeD%z5#6^u0Y^UfGr%?ZqYTZ4gEpkt -5FnOM?K-U(`VCglR!_j!SPG%ihWCybw?d$IDFnO4qU}ElcVN4B4}B3*MpzRgtOLT=O(Am$YH?;(Kw+1 -i>g&MgGj2W5&709j8o9p`dz>chi@}h4MWc?4X#jEG|ku@X2lz!8?Vie|1-WadY9KP>?u+-Lm+l^~F-x -+BMYw`Z239qjIyHPq^<-2-~M!Mq*3c6b4#RB+i5XjAfu*iLdldmV6p~Hz+*^Zg__{29I+eWpm(K`(> -@YP8wOr=fOj99$Z_r(6y_rI|%E~=Ej}p#e3}$4>P#4Rfv{m$ek}T#>=7XN41;TOScgf5<_{0;6_?& -uau>`xIC?UIAKEo0zu~!We!OjI=a+i`m!_)(-QF{AV`mmFz53ImxO6p$~gtJ>%gUV(=BeO5gz -L)yD;+Zv|0%@VEztP)2jZ-D;1?!i$^R+Y|{VLAM -;x4scN`_2)R@&X>Ep^Y7?-tY-9-;Ui#9`?P#%f3qHYK}(NmN&7bcP(8w&314oCW_BJU#R&7gAM(FPDR -b=_0!e2K2NpsLtuTQ2nKE^tjQRQd@|$;Q=`!>EHw6ApDs?ycrTb@a@Gw+`x!;br8SA?w+r?B)2g}oSp -7^*AhY!_>m`rIPw&4_c=IbnNATkR<&W4Lg`YWE%**=v`)3PN_+GQw4l&7T4w?TTPwDxcu`;|LKkAyAa -o-({g`~CLYWUb+rcsUsU)fZ%p^)t8{W)*DYJK}4FHouxujMtyZfNKA}T+w_)D7yn{}&IX)m4XQV~Kb2 -ysUyFhi9>kH>2ZI!Vvo?-*9~0hB$pSnFkXzjGm^kHUEmr#g*YpDLF3Hu065eX6222%BW8mQ?wew)a+J -6KU(C+Bi*JHfCEI?&f3HYGLhf((tJ^?x;1ZHYTXq=FPO|1Gyn=hoj_mH&cAuKnts`9fdyh_Cg$@t2Ve -PYx~>B`#}q);Y}91)D2oNHJ>fn*p{u7K|re;{C{Jl{JtT!!YJQ`HSr^UvkmbIP7Z7Ad_rh07%Zz{!EWSqbe2jQ;;uqOKp)kYl~U`92$mZzorc?!>|jir9UM -l9b65nmdCT7WUEDnT{oxXEY_3c)Gmgy`eB9n-W0XGmQ_*>k83X!p3HtquBi2#aM3B2(j@#c590dnMw^ -c8tFCUifT!ed!{+S7*mvINaZ@FO5^$?@f-N3HD#(>r-rB&&)m7TD_P;tXWiSxLuJ -5hA9+j2s>JW9JQ>i81KE -Z$C+`Zl3i+j7$4BU$!i>jvZ-rS^J?3QCGvD84kRV@9|8$zuYD4>S -f!eZ(QjY-G(9^lwl&?NB(&OloE0XOxizCU$#jE6gB_1ONScR%T2b&OR_aysWZRI(FZ5x -sDOLEgf%asY$@Movswu+uA^Nv76m4-D`&AhZ@r57%PELQLv7D*dFUhwWuR_x#Z%xj?7ThU|m-ja*2v^2KPVI$!Cz$Vf@qZ@YlO6dUqs5nw8>G+$xS41*%TLJ1RU+SGFyF&@!7K -_c=WUS@Gnn^Gao1T6&lEYF=+qyX*-8M@|1o~7$7i9BnbLU0zf!tmJiDb|Ee+sEfE3ITjnsuBp;CK}gi -D?b@sd$eUN0&4l$Cga;OIgWc6rE-qrus|v@I5sj$c?QP@Mk$l -yc%VW`<~SZrmZow%A8|d$am891$?zjz=LL%JCS)gE)@2!$@k5CnD~~@tKG>jAM8*;^#P?j(9c4@konwg5!CJ@8ftr;-7H51o3w{UXJ)Wj -;}&|701^izMSLh5zpuNyNFvkj(0Ul>EqZSm){QoBiyvcy6&luZu@Bxj1%0YF)YM*CD_1^xyPepd6NP*7H+eR+k}EmI=6Y7+k}J7eca|bZW9G -IQ@Kqkw}}Cpcy2SF+vve2hTE99O*+_2;5JjZjTvlo+(yf7^1x;Uw;43n(Ex$v0B+fNtfM6YOAWVlAM0 -q6z_J6ktc!C>cX{0RLJ_z#ba_ZiUt73-`H{#s<939-JQ}J;AbwJyLINClzxOd -!XJM#-w-cV*?_#$^P^AQJMb|Gr#0oKHcZ)v!m8SU)J_-6G2d|Qe*9eQw;2ywemgkAo(1d&52c -~=}I-VLF}ms9FhQ0h?-rFrI)U3!sw%~sfxGa)*sczU5<$br~6i#_PgPRIol2PllChxJ}BP57>00FC_P -@wz?K_vp*9(`Lmk%&@g&gaI{QAn*phk^;&kFbJR8;W_$TbB(S87@aJwv1j9KRAIbw%xx}I(?s<6(`U) -qk_YHUB0H(pN~U*_kTR&a)@(h9CLsOBF8Wh&n82UT{5ZJS|>e4VM$6^43sluffo>@XDXwC=4{>AS$d; -UwJ|QGt`Ty~G8kUGM@^fvW)b=BQgyz0HoVqKLpP@XuzUd!h(_Y30ilBiy`f{rL<&*x7y& -Q_HsQI0vfY?ml9M|5rVb?oxq-%fXY2br{H_j|Lz2=HR_q9mM-uA$FoG1xuL&y!Tm?Xo_v?KtVI`m0 -z7HOk9n=(B&_a;n13PtQMh;k>SLiOZ!`*wDbe3Nu9Tog{M5qkSHbK#;$g6|J_i6uv4nfuxM9q$j|$|H -Y&z)LFqwuNjeT$D`M1>*P2Z9x`EaaRkr_JkN|oq!qvM+j&XFj>HP0zNL_I|A+!@RWe{0{W;0T>--dj1h3AfcFddynt^AxIw_f0{$RigMePX9 -Cj0Mgn%;z%oH$Rz-I+~N5D@6JS*S@0o~ej*jd0(0q+n{FW@Wz(*=A;z!CwU74Urlw+OgLz>@<0BA~K^ -kVn8M0mlkBRlxfM%oeanz;XfK5O9ltRRaDbV1s~4KMp$z*jvCT0gVFA6mYhH*#hPZST5jN0pAyJtAK| -Elz)$?o0?nuZR&!LHxV*2pF_V_IZRFCu=y|e3vRzEsAcz-ek;HBorVxq<=OB>DHJ5?ckWglpa&Ov(`nLrFAmP{l%W*eJhiOb8dCR%dlm~w$o7Eqf*a)^bP;BO#FC7 -A@bby|pxWD_gN03RlzfzJYe=ugAwwk2K+PKDSek}2@#>_>KmVI{;qfsAX#6;yW!oj|nAS4(~-gFN9R0 -{+lu0Qk`YS61+uMa%#-@a4e&Hjo4POkgn@0Yj-$nOz#AnZ^93gG9N2rNIBWjLK|~cLCU>0u2keW|O&K -$_5`O{X9mYkrujVFqaevmq8v9{&K+H3sOU-kN#5O|2fRvB;wRSY&J$8-8B$z4vQ7t%~phOl!){&MP>0J -*esPH~qkrxbNAd(QCdjIE(*pT)rYX}K_D|;Ld?xA5>s?Fn-SJPvhov_4pNwy)BR(_BF{C>W{97QDjnQbSCdqZP -k)lj5@}J;8iBUC?JjVGTQL?xRb57318~GnhhTk;5b09X8;F#C%9N~_w;yhL=O*fP!XZlUc6LN(V=P(X -aoytyY{X}zm!)`$@OSFO+tR`9*^;Ab2@j$Eb$n^i2^3yb?RF?iJ&7_d&H`Y^3>l{{2LRov8&iW>72~3 -VO=cT0e`Wo((f49#s`ru_0^E8W@tu68!?v#JGPd^3zp9?-@%DFA%s7$}Hy-#C##A_SxvCLr3U=%d0cQ -Z2^3p2~y@Gz-2q>+n^oa6eKQW)3Pankj$SjUlry}!c2vE -l`to;x^H263F;e|7t>iEC%1aBBn$rkayPfZJIWe=`2|Z0i;5q4w4`*IeRNU^5u=d55US9XgtFNtp -{f#%@di$Nfzx&?%AAI=HhK-vxZ~6F>e{9`Wv3FMR|)3%*T?c2VCpMS?r0fC -*n1a;y{T5 -a>@=Ph{Xo>}+amz3PP`@;(tEnf29mjAyU|Nl7swPRv+abw5n4e{fR2@@tJPMSPr>a^)IX5Mu-Pyg24| -5v0xt_OKL)rYQNc5^a6-OOADW@lYuMKkkb&CKP^%qyFjA8%%UqM7+am!{^AnwfD~6Lsne_;5LBoJpQ* -oRN{8nlpcjHN~2dla2P%a!r$16`F`&1pwSf<7em5_hvz5emwX?0XqV4l;z(BhO#tn7UJLdU#ax_hI|ZOw%yT*W)Upwsjm?GNFP>?|3mkOf#dw=I{ -F~XVX{InIFnxXw^x^zB7W~d;bAgR|KBtqyG;Z0J%OmqI^DFZy^Cj~m^TFd=;NU}!UyfIf2kE4+nHUQj -mpAe;mT})`hck5OE4P`y8Ej;k&1UMHU8aI>XS)P8mTvShh4JZZPGNHKc;)zH`Z7J4jvOA!m3~B4)`(n -3fl7s6e@z1XG>9yKA5Z^;L8+;OxFHdWi&hjFooS9KYqrTzASUF@Yhs=_$C`u2DJHX3cF@}9+H!%}cv~ -i;7jMgE8Lu^4GB6JkQY>>E`D~hPv87lX2^OD{&5RH)#)b5eHqu0TNDIO*Kw3x#!($jSCBDN?&}OnM$jF|nNn?!M;GS!;TAiFpt~C| -JNwt|XGv;s~vJ3x#dydW8x(9HFL}}fGc&S9Lf=5gWXa0NPGN}zq66~-9< -=2E2~rSQ{&{4)Q%oI2CGA-z&fEQy`{h-{fQb?ktsm -eaRSuRfX-AqD16vlwpl-=h!6n3G~;*EB2EXqdvBjly-HIdwLH?VuzbQhi8=NN>_%vZ|_Wg|~TCUp@!>QXnC_;miMmhdTiH*G^_ZBV6IFAl?wU!q1ENMXHG35OrOa2xlJPTHyPpz^df<~yh)%HV5%nxj8u}q$^KOsAM&(TgExE=@~oim8{|%c#spM!H2Wv}g%cwCO -NKVCTTdrGy84i=R&UZZ)r)kU>Pfl+-L69dt2SI|z$rIqZ8OpeKhkhHZ)d3%v<>y?3J@PcJv(73oR5Cbx0;u|m_{q1t`4Z`Sm9?@1o@DlR22!D+KvQ80zNsQu4A=dRr?CySJj57AWc;uyJ~VHFON -ej@%|t}&Iw)u2SWO!?t(PhNxCYwl6Dn7X75DMO%@SS?JlPUJep(0v -Uzp(c*BC+`1@k?^tngP7e=B^c9>gEw_8;O~;d`Tn%{17*!ci>b5SxJr0=aO#(ocfXO^16+xaP=p~S=+P6wo(bnvf>>c -g}`R$8zz<(F0_g#i`s^e*@a->!NX@uMd-;*-0>NZ4e^&o1HNj=1;LgDE`Jcqc4D*}`dFMN>x0pJ!e#L -vvq0O*43)x1UVN|xzfg8{>!)JD3#zHNvv@asFIslUgdE&c$$$G#Ntvpn^NJoRFEs$g}h4e=V%#_a2a% -XxPPsERT0y)ELI2KYkw4$1L$aDz6_gLD|8a)ddZ19jEh7^Vy0lg*7`c$`WWk0;iNF^z4~Fi^hlhp+Aw -hl*e?5*!&wf`98=7f{`?%CDlm**968$a(c5UY5qTV9`U+_QBU)mX(!@woophY)%zr)8b#zJkDpJj;vY -4afH5|yrA5)gEG^Wbei0uPELK<|KKHrY=iHfFP+Q3Q(HWQmjc=UOZkMP7Z9>Q<_qg=H>gvOdsll_c~r -QY-I84sUBa=h>e1~EZxRyuplgUC&9!L{!0WT73&eSovXSu^A#`ADPRNq-#(oOL$Q43mTXd`(ZdN|4((s*Yr(0GxM^>SKapJ#Ci5 -A3dlI-@3POCwiECrVELoAif%uRve(N+V5Q(cjAMOI|1BDfo8E>Ci(-+D}!H_Ruc0x3HEy8RH6<+l^h& -W>}%kNNo%C-Ur(b(Qd#8?N0#25A~O|8$3;4g*08=VwzUDi8=}O-qHRuc|Zo7m*%%M;qk}aj -S{yn70|i4ec)4>Q0a+9U|IUnuiN_gi}Gdc0~0)@Z8RYv>W0TuJCffG8V3AkNN5aJOUiT^HtHTkCW4*I -b<>{mE&m4tHAy)q_d)w^8k4n7y|u_H4^xYAe|K9MAixLfTNBiU8(0c}^FPvg=-9`5I?hBmWZqut|Rmv@G={QGO)F@t~YJLd4ewePsQ!g)J -xnPQyD8q0@-Mi>#!AdB6mn4IUw<8x;7Idig}#Il(Vd3qhvmW8%iEHFHkDJtD(q-0pf=2)h{usG8s!e) -WbY?B2-$L3_`=46^;t(MF<7|vQDMTj;xH)C$LpfEM3F-r0S#()gR!hqhfhq$u~*e2wp+A>XJGcrvRQn -G+?@|x=;oFi(H_YBtrTc$N*%mS-vYR-&|R8wqviiJESBy#uz{rCZ-J;j=;hZMnCDW!{88HmFvGU&fCe -Vl~g{w(Ai(TS-dlL$JQ#AfE?%BJyglPAO(BZdr2&CG;Rx?+me0{;v%@F1*lhBnc_96oo^n^MeUEhdQh -IK#C#a3{Z!xNNJ*5^Kq@LKK-(fLC19AknTyQ_4Kk4fbX(g$43p>H;&S&hlnCttE%u*YqBI-Ou3k~z1=2TGpA=*thSWQY1#8LvO#z5j&{bJoCj ->?u`u{Y3LqLzLIj{oY>L@xvtTK)n)0k91nqdxxI9wpV#o$TQ!+CiYA&)iJ9UaVBRe+72J!v^sf9B@i8 -&dVYUCi!3Okv~x)MN=aqJi500MDvLw{Z>$qgn&&F8EX8Hd -%dq5R<17*{6Hs|I?s7Gln&YS&^VHSK%{3;qNy2k9uVob_r}%+h~=|%T##!uWlhZhnR8hR14bSbwHqyF5Ur -~*Ck0CWY)gt|0myTTjpytnm&O%L-o8s=H@25Le%7zKh*IRxAfK2zsoIs{q_6n_t)=cKe^5gdFh)0&x8?j=w8jSv=fv%yW<^r -a(#0Z5cja)-e`OZ4Nnx~8!NbP1P|-uz7eH>o&tIaH*Y7mjv_2Vm7uE@cDPTwz4*|+hp-vBmCzJo(lwAD%=Z&dy$}n_f_MLMZwQH5#MXVexu -;;uyFrexCeOfw2;$`ieLVXM<@QJzvgZ2t=isywf{dG{@+di&xZf&_kZ(utNpPq&3>-pCx>743qLneA| -R>b(B(IAzJfNko%r+F&+Y$J@ZbAWxBBDmnpS`RcGmtQzt(sAaQD`C3;lQb{#X9~UoQjAe*eddgt@O6k -GZ(9Ew1?fd-Bo+OM3|SSHI!##aB2ic%hmd8H-t*&)|CSzy0M?FKb$%UvQnn7y&=JP5)W}H-P^mFP(j< -Gx`1{>7{#Gr@#0`hD`#6bJD5%PH9Nesrye2xzYdelV4X~CS>qp9`ZW0$FWZCc~^NmY8B8dV5WfS0;UR ->EZ|H569v=@7$sn+fI$MP1@sY6DWIExM8KJSU(e;8_8y1w1L>J^?=w@WVfv(tk_9bpo -yyuw1|r0rLb*7cg1CSpp^s7$sn+fSztmbta9l4-il-pi)30%Y8-2D`2&NRRUHBxL&|=0rLe+7cg1CnF -1yXs24Csz$gL31q>BXBVd4lY5|o3He43`30Ngy#pR~--xcPy0+tJyCt$jOi2_Cm*k8b40o4LZ4I(}P| -5JMDx`9pSb^f=x-!d&CS_v=owekxho$E!uiTKQen}%}}dYwGiG*Emp{kkC>o{jz2z$M~$LC}?A|2e1- -d^S&m>tcLK#=EzFCqcXb--9n5=zIw9qOflO_)Ztj&!#TyP9J -8Ea9a?kUjeXJS8g8+aGJpJtrVATARgdu0HZa~HUK^n;CJvv0e%*s0y@$>~j_+mrLN8s -Ow$C~sgt72sR=0gmv#5UAIHCj&eM-&w#<1DxKQkfJsUG85p|-Z<+Gd;%O63hfr)Q2^sZIsZn0PxmEcD -%j&*iKoJ#twtQ64GKUW;CTR#3?k%7z)u3~H5mE{z=Hu!8_em<1h^Nz(_p_3;17ejT;b5gCJy0#5Sj&! -aKjLuR#gBmz}JAU)kHu#MPS8&`~uhmJ~zO_0S<;w33w#HWf7dta)7-fxxAqOQzALdRDcf&{7HbH3;ZN -N@1c+;K*tAQ+)y4?5Ac$}8vs5!3|GJ)-2nbDjED7$BILml&?kVs72w7Zq6`51eFXP&0pOldpbvnbeE` -Sa34J5rdVmK7UI}p0XpT<>cpbjAct3v(Aq!%7d0Pl@Uks;-@Ix(_kspA%F<|emVDR7=UIxwrJOGMH5Z -G4&RO`4b2{kQa5YOps0_bjp@&LFJ; -N1zF=UD)kOoj3Z_W1yxn+Ek7!vfqfji=Qvfa9lg|3-jaXF%SA9}U16@VQ~R1^7CAK7hXkP&*UqtS86| -ux>Wg1*L+)N9J%kB>?B9LLUtN(*XvXxap -HK_kF1nV|mxO&!3fEJ!yG1%u`+9xuZ60{;-;7g?PCX@D!UfezR|2{0gs`$0G|hx3Lo#|-I)bO8Qp7Uc -@yYY##l0sC+ZltcL3AYLoLAQ(yn1CH<)_(lR=2e9YEJb!}$j#$X~90~BmLeVxZB4o}YQC|QSEaLV>0A -GVI5B#qOxLepa0PL3!?I6+txFny4#e;Fn3ZQ&~{c?aW7jXaU0G=w~GS&fHwG{XVKM2Kec&9_9J|B)|(RARVDRAUysU$PV}kfNz#V`w9440DmhNX$WxVbG-cQ0(jSI -&f6@2qt^gUu#W-w#2V3V0Q`LokM{z={O5VuKxkeIx*YgN*zZMXub_PP2lxqm!GI$iybkm-z#{>+e--K --;C=u{zRGE4zDCIR>$xlvz|pVscw+#51YaKb-vn^un~-*ZR{@NB8~g*F2=J-5d0eXiek1T|fSumqbOH -coyu<5JCcrj-=k-VlaP}7HF91&k==(93%MajI_>|DkRsf9rg!75;3-|)S-t8YiZyU%0xCY?V9ng*ejx -b;sA+JuxF5j$+I6}YE9LK$vkplng_y6{fpJ{hx>w}<@=C>nkZ7mz_j4y_w$%i~vneRhXd*{zT!i-1g`K$RpSLe@PxDY*@r{A%9noT>b`3_2|y{UwU;}<<-i1q=Dw^lV=&g! -_`cn4`%mI_WSi+U3mmVNW=6|o_xEY!+AajfDh;>uN4pRHC7%0AImYhlviJWc5oE;z|0MYj;!nXRweU` -AK0JMj-(9`oUK^{|A$|mTG?>*NPY$Le}vL&)3!BSWzOgy(zj-H17*%E{gH&kozXwLF8m~SX7m|v+?gm -##E(0ZtKBP>RJ-Pnh9c12>2rlokxzI-)5@n1{+z>&ZskMOnC24&e6H|$z^8(*JH>LY7G?cb(Z+BQm;Y^zOzlJLNiUoIwX0A$JY-`_*&A8CvBe?M*7v~6j|$TtBdJrMukL`pwOqKixWY`bS#5 -?MlD-uBTnq_=qm(qBU9&zCN}OIRs}@y6or-KpB6J0-5-BRBbEKOW*!@pQs8iec$=^V`xwLPAJbSQrTp -4=1BWjUrmDmKY2MmUfdSO(N5$PbYWXbr-q!-h0X7)Ujml+_~ib`|l@NSy{wlv9R`I(V|71*EjJl&oC2lKky&eL}U0dpFs?e?K{N=ny%2^e8!T;siN$>J<6vtFOp;P&|+SM -#;C|e#^>5O-&8CaQ-Lq-DOJZ>gvd)OP5$#!Jo?@=pPbce1Kz37$bQ>x$%a|-VQ3ThPEf8=wNaWokSMU -2gxeBl6*wpBPZxlxjiL*5WXjb4~OuhAiNR6-v#04LijxDOO`?S)e!z22wwr=k3smePT_k)_!tO(H-xu -B_!SWTbqK!$!XJh3Cn5aT5dJ#|UjyOKL-=~9@Pm4T4zGo>eh-Z8tc1J^W5FXZZmQb}mGGD^p(h6u`qd -;t&pt@#Pb&$ndymk1r|^Cdz9)nahwyho_yh1OPw0F52tE3ZQ+N%8zXQS>A^dCzp9kSrK=^eK{sRcV6~ga@@DS(9(-8i92w&$2-ybN92MQTL -;ZdOQ3Q*Vu6ut!tb-tARHkgtNlPIZwkdjL)DY^U}C4U^Hj__R|`~V0)8p2P8@N*#iJP2O`;h%xXekO!}5W+8q@UKGnZ4myHQ~0L8B>r=@ij5tk9i! -DT+}JNPB)DhK-s2pu+A;A4gH9J8J9@NMmoy->PoI$BapQW)6m$k~hJOY?bh@OlzUX1xIE`?RPl%0+9} -VQ=Vzr~kXp{P*hmeq-J%qaU -TD9owNp2k_u5m3#Q;q= -cjdL!98!aZ{5aHgZhtAUB2A1mS>bWzcCI?n%Q3sXf~$6ux@k5Lrx!9}NYm(Oo-iXn2IDtFpZT9An2oS -&od1jcs&KN*be`7~Rgp{SH85laRVW*BF0NEP9z3-PXk;2Hj&vLvf8KjqaE}P{3lmecMe;icJ_D4P}iG -XZrl7SNjf6&_X&IoSc!qjyZ$nXB6H+k}8rjIN(jdSQ8ujSoIN;2ZeDoZT+aA_QQ_u0i{*gnuHpq< -lyPRaB$TxZAw`1I3Hljoj$j;vX;hP?RVi)7upbxc?I;DZmCKDcGe7N#qFTJair4)l$8L08zdYZp0u_% -PEA&Yt;(oH=uboIQJ%)YjILbLY;H-+n#Mbc2f*FOqB5u920OD0vUeNB?kYX9x9wj&2xqbWktL=Rild0 -6MzIprd=0_|gx_V7iq|qWj2$^f*~bPm}lP_t1aWIfaJ~F%d@eX6S6o`$PCUAbbpjkB9L0K==nC{G$+l -4TOIm!taOhjs4tTddgpV%Kue8MZV{|bLY-I5C*b~z&i(o3>q}3A7Yx$odY`e=+|A{u3a}MXMsIKLqht -54Dwa0`nCmg=N`TK^Z~!V>h6QW28EHJp8Y}xb@dwL3m)3FZQCuVM@Z-(Z?Dj9!a}17?KeoN^a+J9ecQ -Kd+c}_9zd_zgrI)9>yH8)0s-=568Xl~lh-o{Tpg3!k4a~HAxCw~9Tgg5ewTmJpKckhmgiXUDX*W1(66IvGb+$dfLk5|Y;OTmr} -$SMA>`nd|aKDX%~(-pf%EdH7oUU*>!^gAD)KY#v*Uw--JN9cRb{QUFJ-vj*S!i5XxVO)Cl^y$;@?cKZ -Au23k#MvfdgNXQf*=-?3wOou;{ewf$TF}LsCyLT^um{#p>Gi<;;_uMo34)_7y<@3)!e+uK4MvpJ+uzg(IDh963V2_~Hx3C*%*~9r5bwYWn4uUotb -wB!8guFbHMt!mq#n+5qM551=VsxpIa6_S -iH{!j+_!#t5c)TvV}55E2OTV -_Tcu}(l8X7Kp&M*81=Ewg1``L%Q@7uSJ)s>ScPcj^3z%~HO8`cxdBjjzvFO-^2 -Q9AlCrIGt79kiR$uhh{L(o}b)QihbBxmHBa{xSq%>k5$gqpjejr2de^AGvB9sC0;Racd7c57}V@q;ix?`GS9$~q`@{jEB8B}invHU}SMDc5$CvrK*bj7^Da)o6>mILcJmNP8(STAIV^#XrW -zNK^m$Pf?Zf9&U6hEY|N4nM?Y2#31Re+Q+Z+x|TN&=#)@2?-er`LF`(8MZAg$$@3Pwep4OkM7tnU^r} -NoPX2LLLEgJzM|9sGQ@!l@BU10=l3go&$h<@<;#~T^fOI9ltKPvIai@3*+gt500 -i!=fp53?*4(&Y2Q&gshZLRl;I0epP|f*lb;fM=K-JaIt?vuOYiD3{vn^2M@B~Wg!a4$`N -wk7Tn;SvSgxDPANo1y;iV`L7u#6Pt6to%b=TY>rz);U=YSr(K -5`-9fX7V?knwK!$mv=*7C9=;v3s3`rovJwJjBPW3rn)M -xE+-X=vI;{CJSCiMXsdVSntyTteh{eJn-p+kFuj#7mDW4nkD%M8kZI*%+1+F@TS>t*PM{VnPY_}h{{o -Nib=oW2J#ybUs}2N^zjCWl@)UvpcHO%2GI1$h!B*Jq()^x4`n|JYv0<$uJ85xp@lfEY3cF@#?X?CSXwwYmM%%vQM>JK`t|2 -kOa^BiW1!G6TH_zudshB|pB2bA@`Vt2LcXy+Am4JHU>onOA7lTEGGh7PS#l@+2gtAqWcY9q$N+T#>od -yml91sUAwxOJaQ_(kXgbJ{rlX5f;ZG{rdIv%{Skqs29nfbDrF+o? -N?jjaEIMLbn$Vqn{Lj3{V$702%%+>NB=UFWT;;YbVSf-ZRL54>_hNU3GB9H-p&2^0Xj -^g^?5C{NvkcR>EFyT -^a+q*1<0@rWGIGmRAACEMzKxOI_Vf6)VAFIBmZ*wkB*M+jXWTKB_$;e8Jf#+Gg+=(yGp;^{w&?SJeF= -NjG`Zl`ux6AeO@PIcuvUhIFzFm4`^v=2A2VUOvix!*$Cg7A6frlj5_pvkxGzSRb -(tEMB~r$>3bRZj~npu}(D~W3}}AeAhb;9RurgnW)dremB*Bkblg3yl`sO6Sd+=a -AuMa@;IW@P}>Ehl_8cK7$M`v@@;!0x_M+%geVz+e!QM=|hu}lISb1yh5>G&dtrGR;!gRU%s5x>#x50i -nTka3$-K*@{N51wpFMLp+1V^0sLWoKJu!KUcCb2jhE*+>a(n4JR|CJIn;$)@&CyupQu3BnJV)?V8DPo -($dnhQ&UrGp&e!U@X|{!(TNi$(kWA>(6X{J`t;LJ(QKlP7qZ?Mp$oX1pLDST~$MltKQQ%Yq-4DVa95t%ugkrk@nYK>y5j4 -D6rZTog+;J~5Ymd2j=*zj*ODU2kbNCxgA*!!DI_+d9;~&g0K&%~_v3VgV31yg6hcpzZEO8})JRl(uXb9oa2802$THfaFE@EPL_l6XKYI -)fdsqNTQkV&m=Q);IXi=d#*sKe>+bM~I)X3dM3PXF=DoSfXwE6q!20#;FR)H$&YY?0> -gx17Fb3$q|Ni?`QBjfpoH<|&rZNT-_Y#ZYUnBR>NsA0IHn(iyQ*plNk|*!J`|c@6jvU!R3^5$~kxeSH -M828Npo^SyO-+qnztg5oQ@OdhYSN@hdLHLB+lGo?X!>Q`+onv -Jk|sRd#Ckq);)EUp{2^~B>qC#h=kuvqvu0`8I(YD)rXOCzbN+{J)&cs5&+$!>AAD`r0c(IY!F|!eLi< -g*&!&`=lw6tjV0(LewaRpMK)t5~lGnQtZXrey#d1&_@&CZC8Ng{SEMwz -f7EjYhQ%v~62o2d{-s-J;tTh>ffh+FzE>8a*-GW2pFz_TTV-(EqX?w{y&TqlFgcn>8zPs(p#_@^Y5*-limIh|8rmFi21;t%XoRi1$uML5NVIo@0L}!47+6;+87ry#9C-F3i3ltM1^+7)zs8Pj?o%Ddi3qm9;e^<|HAK0jKS!|zQzH+(LrLnb)0}qVQc%y>1+C -#8{mc{Y5a47#jfX1|Jc>_Q2 -<$UDNN5f5rA}M=r5JjKk#n(I4nK?2&sM@b>7@qiXNoy;`2l8m2OClbgqH;ux7R_<`um=aQ0=J`e42`r -UTX?Zdczn}OzpYFU=Po^f$LV}o8~3*E_B4DI5d9vY^+e};Z&k@cnJU+niL6Mqdf7L)Td_#Jb@XUv1a< -d`#b5m*8k6k~yQ>0>y0W>>0Anlrdcm7lK -L_Gh)7_goAFX%ty0DFY(gvZ33@Sb@v`CaCLV{|^c7Ck00g~l~x%p*;^rXSfINY49{cegI$dy}_-&x{d -S7dg5qK0>$56?XM&BTPGm*Jpn9e_ZK~V_1oaIr$>VRbxx~d?I@>Ba@u64z0+w^oi@j5*Ey|EEK4-W2~lD8B6y@qu}U;U1}J;%{7C{B{f{ -BNqzJ_Z=sfd*QlLe|TT1oOa#+djs_U4bkBt>9fa;r<=VdCV%eu{5Pdu7oB@v^yZ7)7|ayB5u6Krg`7C -K*f(w{)$zfQu-;Vk^o9)^d`~^~R3S9rI}tx%AJOaBB6KyrEPe{#@jqo=_bt2-j0e1voHqClIe9R)!TK -i#kQlT=;)Nw9z9-J5U;JozVqzZrCU*fZ`@z3rcpq5Vk!yXWE_#)#3G$%(ckg{j-17iWuwPgH`KEeqW1 --GtZg+Bki>@g3$mJMr`tYR~?rZiBxaARi2%Irf+BbU3WGXgA;#Z&exrOe17|cIDFtlKQUfNTxb716y$ -;q4ksXx3N!#%;CI$A$X=Mu^1l1Fs+ -5B3Uv{QU%Z%ylptus$#*lwiIaSL_UtKi(?Q?JIV}aejhrg@0y*KMyOs@#8D9QasVbo*>yI(4jWq$@>FyuDV)g3P73kAKd%p#>h$z8)&oxFL8IdDWjgx -$PzgUtmpukAM|6_@~EtKymA6uv@qMv$oc(SyLhUe~G~dhzp4A;0d}1Ufk&ZlX~^11sW$HKi%r&*AKeZ -1US=H2Pa$S&_L1hDE-ct`qqDXuYVJtu@3S1-8hRIyMs@IVMga)wl&5AR<}uLsCTeNSNV+oqyMd2xB8x -W=9xmUA^dUZFmbinHwYc@0)51H6!6>VnpB3{;D_M*2TzVoFX6A;u_{|dq>e|~E)YX4nyd{q-Zp -q`m`nh!9r~Q}vk2d`m78d?JCnrbeUBRYHOH0+`k3X*CJ7_Yx!01Zm2i{!oep9^`SgL-xajM=6VB|5l` -a#w9^fLAC+ixk=kN=aMoqca^ZtiNvQD0wQxoFX%SJKnd)zYO)bsl*8_U$@%VPYS6P7D9d-bVP5Xp<@2 -Q_g!`WNSeGZ{oy>b7bA?I1pP8E(5kCewLayZ=N1^f4SzwxiNO!?5&jBv+sG}2mFQq^ez6!Ww1o@#`*d -AI{yj{%azUfk$AA1n18UCDivM{?{}7zl$-_QAxA%F&K$*DfUk -*Nj^hRJ-N>QYBVt;yRUZpKee+v -rT_Jay{S4$sgq3cdcfZQJGx1OkO7=4PIuVe#U{YQlsGYWD2enl_X7Gx!kaAvZ=}urIN#t&O*V-8F>1b -^r8<{@J*3;}Y&iP9B?&&H-Ckuwa44m{}K`OMwR_$BVxVrh?xtW7vNieaHJx%wf5664;}vsw%A;*sE&t -ip&?hX58rH^XJbWy3M|I|MVleM(h8+=rBI;;lqd3fddD0t{B}&&dFek!|Yr4Pd~=~B1??d_~5KPgG)j -KG6u#${^{7UV`iV=aQlvre<(Rn^bER>d2VfOMgFb`FSdAM`IF%mo;4Xa%*y&2OuzM8K22@N=^EAR*<0 -#h&+NLN-Hl)8iPgmSuiVCSyWW236nmK3-*+O9)0&!^js%0jk3>hk_w2LJK6&WSpa0D3otm*2zdDqUY1tFy>j-^hNje;Av;hoFUKpiQJ=0 -X^BnbCGF$IOETlHH9vF&4`Oj*FJeF9KH|;{i97L -O@Ts61y+t90!C(Bv)64Ze+~wU*VmTf12*hmgk9oodnLVg4{p11dZ{hFZ>)~tS>k@B-_ixenAm+#uI+( -w?bLVQD8yyB8y4nNkXhW8cC-L3!+ld>9o!0ti=sq6)n^Mifmwo&8X@8JCBH(JwCGwK!i+7x9)qell;< -(SvU|)cLVi&aD0+VC^fU%{N!}Zzfa@TpyPhzMam+Jm+nE6Nc(DleEbNSFi4{4sFr|X>E*irdPjb?Mu#?Uj3G}WKX8Ay4qj9sBE>jw!EyOIxDTFs^Y -%t;u3$kuXnC$b>(9#CKgwg-{-3?pS1FxG;g`DqVzF;wfT+Bth9S3rKe?o<;z}g=8CGCYW-`RL% -iF)vFqf@YJYJ}Rq2Y=&ar$_XgSWDZ1|a7FtuRn^y&GtXA~7V|9o*g -a>x8;H%VZP_)tl>HoQ5!JKP*T9zGZD40nfn!eb*Tk@1m?NM58UQWB|+Y>w=XG)Im -{JYMHTjn+8pT60gcr)6x5w`F`wdP_!2ZcAQEK}%7~!j_U2E7BT09z7jB7ww34M!TZj(QDD3sK*{_kF! -&3uRY#Qw=?WqJI^k#i|mDViM`COwHxfs_D*}ZZQ0Fst9{%)ZJ)C{>`uGO?zXSlJ+?=`Sklt{r&wNVyp -?WcSh-f7RbUla3#}4snN@2wSevb#)^0KT%~q>*+&XQYvpTF!tIO)Pu30^nConcJE|3!N2F3@{0~vwbK -wh9AP!w1gC<$~1dIBlIj9^i4x8~95;5p$_SFk7O35^Yn3#EkMSb8WUlpD$m6@?atNnKXCQ`^#!)vZyZQ&$^Z0R(Hv)UapE# -VbG{=?PA>(rTSIRuJTU(YWo-Ki&az4@>T(D@6^{WZRQ&AE?=(9-?!aOldd*4y5??oBY=;5vuN@(L!9& -5S-$Fm1A4ySG<9PX%+|~0U80I3QTnMT!3|R^I?aj5aoqM*-)FWY+jH@7~d~C`qt-l0d0Vn19DG -V~QqU0%`#wg2RrJEST3#fF*-xK~002mWVG5A|T(j`ePW$CT$5^9$&^mIIrvA)g*WkzZ?f&1~22CV0w` -Pf*0wJ=OL9t!O-NF`7I1(6|{N24AS7D=z>LBGkNI(b>(<(|G%Q^e -o)i|Lf`Tn?!%N_46+NIlFl_+Wk2MLh#FHK{yIGzTH1QIojWYqVg%Uzx6uY*f`#QcX$NP!)W^DtI217K -c9?#hHpR+MUi>Yv?w)zlY^q|3A`N?^B}!wiGOa2vJ5VA5C>Ozg=`uAeS$>&>doD_%&1d=dwxqzYGBpqodLrf@BimV!2Uh+B#bhWw}L7fGiswGT6o4&Ve -}d)8HqU~t4qBYmuyoYx%LNUjHil4;I7!VZLcdR(MI-g5$-8w^1=%#LSK;{>zg$=0 -IYVvoc3pN*_d{Lf^kfM?Oj5=q&jAx&fak&TG0l=ho?vSkwk97V`uuqIB0R_0gI`~2C7^+-?x-Tp!|;6 -97=BnZb#(EQmu+qq+6=Jn3?CfD#rs4}Y0}x|~*c8eE+t<9ShK_%^~iBVVVc4%^X^yF_#(+;g{g;nOyVp0 -=Yfcp5~k2!!xoj4Dqw#kWsedc^&!kBj -J_rsCis0U08jXz0li2}Q0alpq=n%+KKI&DEK?YMLcwnx|A6rvZ77{o -%;GOzgU?s?3(M+8ukL!;Xy_~{pViUef3wCnl2xE)hDYhm&w%99Nk4Wf>}^s=d{?uY --sMV5NOUfFhfDh%DuBlACq`huOb7#F#&g5cO^mJJ?@I#v!05>eUkO5V-rGcm`USCzWA6bifQuWq?Ec6&iE=x@fv}3KQfyKme^P8c&-wkC92 -;&ERPNDERHp7en=+;aJR#&xq<8I!JPf&R8pXzfjC?^VzyXGYb33hDnkf{nxkqAHGfAz4`cI@9q9^k_1 -@_3IMWTx?+{QyaQPV7H5T~XO-t!p22{ljW%1P@UTVA5c#w1))zsUuB#d9|I3_o#gu#87mN)5L8? -0*98jaepsvVvLe}$(!4%E3a?O;ZNss+nrmNqv<6_QAeLJ$5S*M0B<(;CjKzl#+wsg*wc -x2uBX1BS3IXK!AITdi#n8El)X6=y}Nb(}(83;Sjicyx7Wqp%p35*1;lQ}vJ5`H}SajK#U^6i!q|VH`|8T3i9N2-n -1Y(fivbcYTB;R0qlc>@Cm)AJVn1WefJi#{xD6^1-qck}K46^>-X8w~OkR=o}-P@17%< -UdXhKa7JpZ~{8DF4EZ*ij6YAOlNmU9WcOHGfBG?S&CrA*o2^{LO^;WBP`1Al#dlZ-wP1vCXTUr12JQ8 -s3>j$RZrm`d;MZ3}Lu27!1ocvJ?(tfN^k*y{M?q|ioMn*5F_CxHTT(<@{}p1$_)!htv%~m^encJ?8|Z;f5(D1*qD5cs32 -%_RrFo+{{55*b${LoDz7v9%<#s|0%Q(7Bde-mj`lrTy=sqN=fL^WG)6u!qluVNU6#xl@4a)1ZT;~E9^ -E=cVxhsvq?pjrolON0RkK*=CdL3s4>9}oP&YiY!A|Nv6T<|GtHx2Yo$zbuX#oPZ%VNC502;)aFxFMhz -eOEw%SU>#w5LCz<fjc@Ms#Z0nR2upiZnK_Gw+_ -G(swEaNTK3>DiI$hEtuzW5&ImB1W(mw+@Wx+IBEjDem7=A1U6@L`P4pojQRKcC5I0t+opMYPO#I}*7z -NtSiCF4eEBp21f`77%1%<5IKub*$`OY)L*NLq^j9k_1J#L>Ia<4rXPm+dE>$!7KmV7ed$&+q~FJ`u-x -J`e9(I#YrNthW4ptRXmmia2B6UC#gz`;@dUV -RW4rHR!=HpG>kSdMVI66qKb9Q~+wJ_0RrZ{>H*e%xb{yJ7bXA2sCU@Njd-qkQB=mbbFQ(%WRZ9mg~(Y -isG0uLT-I@BP%g%`AL;vo1y4hC4TX6d?R1Q1Qqipfm?U0DU~tSLY>N`(VVxD?v;3Q&UDQ1)X5qQ33!( -BFl)EmHKN(Ml+Y#amhkv#e)n$x|0!Rk+MRLEz{dx2ti}3oN`fPPYJ&f;vlyk4B*&I8KXJEL`8>;E-m9 -6sqcK8!_50+C{*{TjWV?L~`CXWtVJFH>?7}-RO@8)gt#Kag%Ie4?V!$a(bna<%b&m+rnu4pzy0D@!3WwfX`rIn0DC#$XDi&|_*fX#%C* -RD4;20D23V1|prMN9o5r6pJ76PKaAec6bqJ}-`aC?egl|#BUMgdJ$Ki^1haMEkS#y^(>uT~%TDG|@o5 -f2s^!c0l8m!ZGlV>Db^bJUQ$<4s5tQ%Zb`}XzcSki8kUja};$yzLMf+VPUfNAV+7bw*xGf+x84KvNiL -*PlOfCw^JJ1(da_qbSQfrAKYlo5NZRYO&dekTSl!;fG#f+*9PfvId@H2!N29 -_&F9EKwX1Y3`~740Tcfg%m((J0hlEU9q0o)USZ)Z<|xq+t^yTutFW4abQu68%LG}Sw^i7AFqz|EVf~9 -5W)amjs}?PH1(ylXez3K>dX0Wy1&jJC+mBfQ21l?4q!^!sY|S!I#}YB3*+Wfgh<&I#u^x2`Y+LMwU{M -g*cOZmk7W7#iz@&vl_B-RI1=OJuNPE{sn#CTg)1q&!ULN0njJ3!&n#_ -lFaC`Zlrd}PxgIcY5yZ96->7K#p=5{fK%XbZw4qF|-zBz*XhEKy$!Ua40Crh3&+lv%yH6P^;9EMR`pz -7i;#{DrK#2{vrb9E>N0m5gWvHtz~8I?xP2Qf;HmLgs@-cLu8}8aMti(!^MTI=iF@Sg>ti1@py*Cncd0W7+W5Z2aapfD -hj3km(f@zd4=efd!3OokS2Ey>dSQ)BA9n$;LFYnv9rLuAsGIMa*IKhC_5XTz=$0I -NJj7>*k$s)RQe*6{c}B{~G*Yq9}_8ja-_6SQG853mDhPmw_n+nb9Y$9Q(obo{4}W+wLET|f(sAtiCB$ -K-~!)u(${f>e}G7)?@$iLl%p4G&s-bGNT^;SaK0!2W((x)OJig1x<6?1%FI!p_EoTOKJCKehLup`uSi -cKf102kkO#ug0vISdND_=J-C4qN}2=CBG(1hL(w1Dt!xbF|g!G&0hysATq4;yLpM>D -RrZ2M*n4lI2;Qrox4R9qUJu^0E380D55nRfEgEgvdu~%A7 -S6eEKtyVHErujm59RhD2(OIF0G4KMaK&hVVL4rKnhIgjpm4(=uINWUzJv%-sO3b-_D3^%q}!F%EX>-@ -}K9p!iFhBVJoa1moDsq8WiBOl%noX_$YuA|Mn6s~PT7UrjTb^dcD`uVKS>wp+}e{h9gNTXEiMpr -oH1qzh5?pVOm~cQ{MW6)}lgIw@ZBn+Y@&}@sg*x?d%)HrGBKvk5f~xcXtV^_ -0sY`BKxdoLu{2yEmmU6iP%~g{9K-;XVgp$R=9LNL7@XH%%N6fN@5B4W{B#-mYynmzT|>y!<;Iv$?5hN+<@fyV?OGh|>i1%GAv#ri -S|j}a*{*p%xQ_izF**cF?3qZ%Kcszf$pV`vcAz4KO-k&&CuH1^j}gJ#Tl;rVkB@iVAv+YJzvT0@hHY` -IThf2u?_*JB5Y<8-Pp!UmS)-Zm~c2H3e`3YV{U{YNP2=8To#?}@ztF%dsITW -XhE+K5uxLNolJA-y#-zzi&j)!QP%I}5t3eGDR&TEz8Jf+*c8f^hx>v4H+5O!ewIyv2PchEoJ-)B$i>s -YZdzP69}pe=Otkc7I>!Ub{DNF4DIX`ym#Yo~cyGoso@kXfi*!YY9IWa|vbtmaBf%rf2+bH9ZZiXsdLh!;rgtaX50J -$Z!IHHs;E7I$}q&|B=g9;y7w&v6%yh9{?zS%f0m3jWaw7g2~)?13#8djb&KY3Pjg=8fQuCSNt8|1PBPOO1Mgi0j)WrRE -jKMM+YO_0HdWw#@s{up4W{Ffh%3=%Y+`y{t~kqVk}T480qf1AkE!7&FoE_zoF4y|a}c-*LpoZFeSbwq`nTir(KElS4 -T7_FYa)-1!|c+3H6il*t;YN7DrxoI)_#`)@!k1f*DF{{@VEFHU{;Uke{Cc%zL2UAQ7AmfKl*Y* -%Un*=Y0w#WXd)V-g=_jX4vr#emKG65u|ujXlVe(T*x`iHpgf0Q<|%hUUgpQ}Q1c@kc8pwoV~Am%t6ox -2lM*Awp31H7t+%6CAz>IEGjqguL~QQB7`8W7$8B2ttO9Ag+9QI1q?rZW2@+c-Gjc$ePugW(MXPK7v(@ -nCA9!ZKn^8^K{0%u=94To-61&Q%(aR)ff^wswdN-Irl35DCS>*8+oC6etlqa##g_DXOf#0m~DLy*u1{ -^X|>j(bsQI-ylK&uc-Ps5pk$*NoiN$LB(*n`Lhfku1Wzx(Orba4kU^sP>t!5iY9fVSrx`L?$PNO~C -S;T>l)XtSoG_xYG!{_{%fnX2+=x*C!(ET_G~L9Eo~Es?TiRjN-CEM{aU5Gh0pTj9coZJWo3yEbb))cW -#Wx~q?P9H>y`&d!m%Wz)B@fA+fB@X`Nw0GB5kz`5r!(qcHUVYN2xw*IQ7RwaA1i_Y0)pk7jw1P3=roS -UfuWl2HU0r|DGnJpY~dxRkeNz|uPX*S7P7*jq%SU2c0(CzBGFE_NV;0AE^05qTvhfssye`6X%Y6z$`? ->dlcOvEa0OU%_J)*~7)=hPFuvlUd42l#_@l; -Q>8yn(v)jHA(KwDtSlcN|HLz`XVK>hK^YtV1r2?yoA7+vk!Z=wz_>MG3>myK&pQL`;P@Q~460uj7Ea! -e!|&ww_Ia6Dg?oa)&8+21TC*5@&gEnRl8ftUS^VER$<9#>jKpT*H8A$1ra>#e&{SyP;=gg1Ldm3kzWN -sLDIwN&y7LoKV(Ru-M*k+^|xbQAhkE323gRIeUaQ#Lv<7L0_a&^+zDh3queAwdCHnZ#Pm$#PpQ7s+}x -5)^X-qy@ssX7X)!DN#r$!O{9fP))=uM4_fIf1i#p+U%q7E>XBzE>j$}uHW3HSTTGsbJ_-0@H=v|op$` -Ji&H{u0pD>0M4TEmA>KdbHE|1}(?M9QPzXL!0E`@Nw7S2IStAj_!hyQ^|-afo}zn{GM@O6S|#10NWn2 -v%lnx@-7?ri@dIe!*m-aq`AoU4ahV6tyN)=KDs#m~0SpN-g-KNxKs{nzQ+!w-H8ns9q{7oBBi@$0kfH -Kml}6J*FUi9Fj9$uE>=0G%M@BK4{6`Oh@L7d|av1UP`;D=IFE -^0H;uk-%lGWN0Qeq^m+=;!BDq!3euP(k8L$z(jqW?k;)fKN2=nF*k@t+8*xlt0se20q^i0QYrBp93-+IjQoDHiZ5ga~p+$AbJ*89@oO3mL5$}w+*SkCfyQC;NIMAuSZoR@( -sd`V4Q!Gjg23ZX{4u^a#Wx0)*0VS~c=dx+;_ik_&K5frJ3ff}OexF^RXOaDN(q$r(zi{S7uz$~WFDWL -Di!eD{-qLPidJT3C<;Stswf>NiDlMM&zd_1P#SWc&4yd<4%E++z9q -HLHUkiG;*$-#9s1@g#iKQPjtZ=xM@2zJYYkpkXeAOUn&}HG{0XY7_d;z_$PNYuDf43u>hG*fF}9F~X; -(zTr3Dx9maACrYu~t7s(n~=p4qP-ACs6>;&Ets&81SmwJ_Us -`9HGZNO!0^>o4C(0FOM}j%s8lzRep}N)Y{g-SR4ZIkWF7*PDCS -(&Gi&sDgk-9r)(Xov_@Ylp#BCV)VwAAPyOAK8OEAp_oQr=6J!pVQa9$Y -3b-S9H7m)gUfX -EJ3e8r@0UVSi{Pc%C5znd8#B|On*vVoO@?hMV+W!ki-T3qDd)>6(}{heL$^Yf=dd4WyMNsqo%}{N|Y5 -G#x|4Z#z;xKxZ@pY5!iyM@vuciuI}j5NmJ+lSfj$>vCq_Swp||@FxrQ0Qr@y -%rmQVUl1jJwD3r)J^UEQW0{i&A(`>$Hnib{bsmz&>>)OQh!NIG^)X|Zgyem33-lWh$#fpRC6OGg))z1 -?i|6h70`sC=NJ(pXqix}4N0cS1ej{?Qsd}jK@+8QwY1gzbIu_1v&uMS<{Y{pkWvuFOg5`>VxdM|OYZf -lZwZ5=bwg<`;v_`}hZidQugjO;JfS^kscYI+s+~-B0#!$it+Je>LG|m0q4gRv=TsJt+=@D`(%u@Q=IX -I=&P{Sm<>40xvfkIK`ez`>g<5WCUks;CgK9ilDH_5=XzZ*d@yDp8)As8M3?mpSSyeDPCxOb(cq=yc1$qp6V6O9ZNfhB{g!}se@Ol88@wJGsZN+m|X$viyUdQo1rc@*m__|^Y@Z_QxwjgdMf`@BV^-0~v?JJ -ooi@=4gGa+4^tY(-X+|X5iM|)_$u9bmy%pnzKoTbNT6HcB%vU%3#R{VGs6_uk+RgMOj;1ksb*G}&(`f -QC}RxzkFqU;Lsq|2B1IY%s|7mMYtm7UJ?p9r2IM*wz(;;+YIEX5B+YxETOM@-O#$-rn9S7Fs%*rq;=lpEueNlm#(n^;yJ;%E}@EMs)RUHt_=LEn|nW(Ca?8j5$ -HZlZ;+=*3xh`e9B?D`;Ovdg@6@_vzhmgS128LqWJYYhq!UKz*Raq&Rt?m?y%0~Y6)n1Uo087GvAo&}l -@Zt*ES(R~MUYeFUpeiCFZXU(upxr*Z{nIdnUW@T>L{&1G)lt@xLe5(0+;6QU;>g+YWI)6-?+}(e?O@f -1Mden8q2i2aLp2Jg~qDJS-XA^qsnuL(uaour?>H#>294lr2ZMjNJQB>C6tibBxUXyIEyKfwafUa@v*w9>YGaH -yjkm -2lKK|jrqb4Qu@FWNi{qT|e_7=2h;6bBEzW9ETr;!6D2U^j0y%L^geLE)PE;Xm$9x$rrl6J1PfRb)drk -HURj!P2Rjz6%N%Yx-q5Gw#xuZzzd|x?fn>ytNtkPU0mh=OD2q=C$GK^M4rd2d@YY@*+bPr2q-+3z@Cl -xYA1^(!q>1(^Dt}gNNx=QBiw{)mY+|t>7{v5h_Q(P6RJS)<;ZZ4nWuji*}^L<)9effI|Jp)kXodkwAw -$#fNsG^|lCn~_RpT$*{-zIbbhql|MJK7xUFr~0D?aKNFU4xdsMnKF*`YcutFPn6=Fjs*7Wlso!dSt0? -Lyv58@pFj={~nM%{0oEO<~b;mqJV0?E -;fiJy26UP*XjKPWJYGecyRz=pDDX^I#r)hebIEJHY$(grx+FsMV#*yrMW(bR+>*_WEk$GtuXzOGuAgv -!smt_^#K|Km9;V#C${rz)v(!tG)m9URE>s13HJ@+V$iI;a2({Q7~ELY1eHAS)p!Zrg!aXAN^ylff_W! -Pb1A)k{wQj550YG8heDelLc2liR`Y2S?%OL>DQAmD|j9GQ}9!0dzof4OCA0-hRU3`5i$|LZUb?t83uS -ne)|>t{dZF7HIr)OrSG7-zyp{-ns&TZ>X*<_J;Jjp)K8Yk`=l!j3oa-{kr5t6*!@_nDDgJ@x%?6ST)> -~3MTwzPZ;tog9(=o>?Em$2{{u!c@^$Djl#VZdq$^VJ>&1^R<#+lQAP)Jau#~KM!T&;AGE#Ic5X~d;Z_ ->wV<3*CVBRdwCj)@ERjNHeHm*{DKJ;S|$^n8w(a%*BFy`_6-$;<|0iu7YA_MSKAC{eikb8fEmKtfrVw -qaLO5D3*d>e+nY{~ -!84K@+$-1+<|GKQRBw)#4*}Kr6#gBCi*JorBAH~^?CNhG*P@yZ7xy-KG1w#mcx;xNr_m;P4t7MwV -|A?sa2rf@alCmS>74MR`uxnd%Q&}^+|r2j5fn8V&P`vG6aMjuG}&G(l~Bv?9~N`63h4VAZ@$yoXU^;D -B4W^}(xxxOzzc{)o-L78rc+-#?h?-5w2^ypvEqkgPD6Jtf%obUFtFZIjM@PnChb2HHO#qVEy@p*rm9S -t)QP&2;5yv2KCP^OM8K!SMkwoeN9Pm8ie--l@6WqiwCm}S1i9VSk`ss~arDAg2Za(Z*JcW{8I&=4waQ -*NUZrp!~mw+%N&ml*I`bQToMJxYuHnh_Sa+E6S$drufN0O3`_P3;SPLef#JbU%=cz4tOncc~H}L<*jv -B};zv(#(+R=ipK%F%yIbV_K8?RLaTa-HRvhR8X0{d5H&>;&ik!LaUFpfmcT87A3o+`06ykWUKALp~za -}oRTW)SdKq}|?x-9 -L`EpE%4j`GvwSR6}Cv1hw(ax`(_w{^U@90K^GOwf&(uyW;%w$=6pL1Djy$8B(tHmDqa{BUr}`XH`$f5 -t!W@z0;aad-;yfBy?jNqjq5_iU8U|o?2LpvtbaOEv^9^ZjxpAUaz01pV)zZ5i^F@Z3$%C1yyK78zAU9_KmuthNuIFb& -oWGoiGX4_jtxf_iiFM>#6Cv)o1rG)WWkvzA0{@t&28_;olh0V>0ciIunijpV>1_jown0+O8j6Zm7e~w -*mrnQ;huosPMq}#qGp?>Bnx~_VzMX8nDhz;QdEpBkPQCJEvD61gp11W-!e}ei5-scO(_b^=TGCLdSgw -}?S>Ct}G4(90h-GkqdR7@|1AgJ9N$I7NmQJOq_uTv^#B~UhH+X94xHcX`b%WQh&AK)_T|+5oAl?bKf_ -|{K)$ADs&jUqW{rNzHPe0Fsr%zGYy}r+*fmC7LDar8OB#b3tCY3);B1Q5s&deQ7744h8{T!VrREKEu` -t>gT*OOOnr1NNt;21N{t8auAq;F0Sy -6zenTijFrZZ`J+3n1D%(3+C4m9xcGBzHSJ4!;YJ;lcrk)W>K!2I(h{elgo91X7->zlZZy2pw#swoCOs -d$2Y4Ms$z!I(kgV}`k5aN!1$k9TlX6d1GTbjwmhHIX=sJ@{h36#wbet6_L5{UwJ!fJ{)fbTve!@SM^@ -V$W^7vryZStpghhN#J#m|04K1#U#n%q|C$j!P90Jx&?gFqNji!J=xlN5-c$dm^@OImLNK -eAWPpxUVK*vH)~<3)A85gxGY*awN5eeIV#u96&Tv#PjsP9_)GZd=*i=8_Hc<&be=PsO)*;}>LFmOSW> -(Fgx;y7Fu278wx3xOlT*BKlto`T5C)%*t#(ztUJP|w{>FjAl-#A`3v64wFIpuf^o0J2Z2f`^q44D^xr -cC6vM65+8{Y&@H(cOPQ&!P|`mH`mk3`kQL*d(H10&)#TFA48Zo@jwnlvkBX(>GF1X7X&a5HiW37q?Ux -?$)x-mk^6vk$$uSK22shXDTNZT1Jl)D&Ae@Nu%9%DoW1AC|>2_;yU(^G9tp*0yuTWH&SQhtCh -{@$&)siQ7WJpU$;sA$|IqWyrCUorIx6$1FV9);v?!s7)r>`r89+JXG%(~yd>5C{8`k=zan -*a6bZdZBCI|s$oE_dwD3I;lx~W}h?t^*Fi&QSR33)y$HW{m6Df7ZLW^cIS|8>|(KyMpJ9!J;{ha9_Eh@`wl5I3g5yzq7(6C -dJo2J{_Hz&!-hl7D>ZFY7Vg8BmulOW)LBJNHJ_Xv(nzTU~9OV~#o|Dm-(VHzoXa0>;jy9 -S|0QV3!(mssXmo40k_$#p0bc{c_si_p2fey#F^e;ZVa@ho#z-&B#1;wNuE0xNv|mtz;LzLI?fs{D{@Z -LEcFIR{MiRodqIgW;<#X5mX|!q3xgCasI! -IU<(BF&<9U;ZWvM7v1s|(6o#(dFMumG=q{0!tpjfr0t^{=ZhVQu-!Hh&r%Ck*}9mmF%7PCcNW6}(Qh` -O3~Fj0{IffI+eYB+cp@P0uqLaQ{EHVS*on%wPh{6nr{O4mbO6nx(p2|pbvsC(}PZN$l@eg-j+e${5d_ -3`)}2`no?k)TSb>EJ97?`W?*&-ji2SX**Ts?^p#uOplNwrJGTHVcrv*60fVC)^mo*^wwkSpI`gLVGnm -Ave(sIi+KReERnijf*Z=V!nqHBNXha>PBKD4q#+B)7EN`kr -Z6b8b#o72hW#?J+w-Gvp8oayyY1tnJ(1Oi%`mcIQMsd_nPB3^(d@oF-`GH>U0-MtL@&p|@1!-W2xXYP -`c<~>YL7Eyg4^=QhaynYvz`hGK+H<+?*PrRonV=#z{bc;^lFE%!__aaz~`>Bf#A3&pvA!tkY6iJVlf> -P>R-gxECSFFMX#Vqf)ms9|Nn&uOn}n{5W=_7?XqlEGbDq0HmvyxR>ZvYlq@aB2NOMZLU$a9ojv%bUVB -=Ltu}Pf+IBjwNxJ8v9~Z6l#W={}|JyHi{vel8JcNj)XhKA$xW8|%$d92NL<>cOiQh%y&1z)XaV9Bok=NQ-U-HPr!+BG%!@hANFHB&)lxj+^52-P}$CiG8- -=fS!uyo-yU`{5gxq_angahNiFcQ*NY4!qQd*TB4G7xb)NionQKnci^}tj%njXdJ7He{>#{NDK!dff$MOD&LF5&4+2GYfJL%SVrBMAriYCO?C3wdL1JwoGMtC*Pfq;a*Mb^9ei#IBKoBaE| -sc7_7Ut$>xW%QF9TGR8Vy-*aN|@s(3%aWS?=fGCa3Vz)+<=TXuUbenc8Hq&jlTk^lZv)=?1 -EyMRWD;bH}*Z_;v5?h*WbvXgZ>5_=V~+!vUMPTVfxk4A% -kIHBfbwccMevM277uUiF{hw?Ip+N1v187q>l^7dsc)=kxW^1uUHhwGDviU5jH%B$g2?u3&( -5{soRZIGha8y^j0MGvF|FTRvmcwSttMQ%hp6i$q}R))4<^at`3DkXYL_>@q`>NDNPVV95Xf+KJBX;@S -BF=8INMo{@fC{+Ny#)q3mcK@HBC1 -_)jDEHEIEYHR?vxJ4`vOn;pk!23>rph7pbNefwxD2{bK7a`G=wAMmp5TF;(ILblPqc*z$QZN%OT;@UJ -Y2*R0wN-&UwF<$qYXq%`8&SWSjCvdoA@A# -`Lxp@YrK9?a4j4fP)zex)u*iAT5!KFyVDj)AP_w_d|!;Y<)yjw&}wLH4B2n%%ntMpX11RukFs=(S9d=W_~hsDX?euKGS*-yh)50t%! -B!d5csD!=n-6xtM;LJ{M-Yi+tpENIU1t+Qf|`tG-}fl_4gy>s?~gqgBinlu*O;uN38uC=@^RLOIdb{D -GeK7Wps@9J4y#`-3wa}hv#(MC3O{K(v+Lz&>Mq@!f+37wWy4SG)+<~X3mwx}*Aa=FD_!?~pdxEG_$2M -M)FJ7%{9-*erpt4ieD7I!h`927E3SKV5sGbtKOVX#_v{i1uKDtHpLdlo>T^97hIoV|0b4lg=uAyP?{g -${{t=4*BEaiYNJNfnjeJr1N2CPHKevoNQ44Mzn%m$#Uy&V{1m()d8qmmA}0kA;LNMQRWg^WN!0 -2_#6%>CbMWVu$A@DE_@Bov-bn0|UvLwXPeaPz8F@>6GkQZ6F&@t1Kfj8;}^0<0K+a=3@>%uw)Y*9P(= -E-NDN<SNomPH$3_d>&Ha1G&#fZB*Nq=8M*C3{- -Tqrdm->s`S74QLfh7Vs1_%9v!tN11@j0Rt@L^f(!Ph@z?!+dNMi$XjOhl)^$F(=05S`ws}ZC!RHK -xdU}1+O;#H&9Ch1QY-O00;mWhh<1^8?}_M82|vETmS$b0001RX>c!Jc4cm4Z*nhWX>)XJX<{#SWpZ9;fiZ{{R&%-$KpydVU?3=f5PR}k*z5F_7el%xqzB_yW?(N^cfB)UP3;YQ6I(vJ2oww^`#n -(K_sWpoi%x8-gzYb?B&bBNVw^&LWN1%oBi^{GR10)SBiu3C}v6>C|7Xrc-vzM%G!JuKiW` -GoSw;y0BcPef-wXVF;<^c3AmieZ!Z2aHBO4ADSy)$T4Wu)u-kL-$#;41x}}F8mHKB$@8h8sWs_zaPT~ -ZVMr=?Q2Y&jv!~O@U-RkI>2x~5%Fi;^0@eP(Uu`%&IzvJO$ou{>%ozOdM{H9zQjEMiraR2SD9 -imQ;5OBF88Fz2*Xpf!Bq})Ug!MWF$`Wo3-_#E?&d%bz;D0_BRJ7pThq6!=urPu(k!@x~9m^?!PhtLok -JFOVWk+!ite|xkE*SDFl{)Ovv3RT2D$#lm#S)pokD@qd)CH{bCn+taC#l7rFrzmetttPhn51eiVh(DE -i)tl#U>&ECIbp{FmVDQ{i+F2y5)h6-028>$?lc&y{EtWZRV&8J_TDs%ZHY4BCh9SecJ+anBe|0r_XTT -Ud|#-``#}|2;xf;Z;j?G+ILN$&bcz?J%V#NH@RY-%dj?9yU&nL4+85FQKwbLT<#ei{CO!!6u3?3Rwl* -oopi-Wstu6PSWVZDL7TliVb?BVd;B@LKspuM##{dqWT4C#rM4K-_vn3!&ZVCutHce6>1BrtgoObzf-X -K4e^x&Cim4aL<4O7rc(JV#^;_)nwxExlP^QIY# -OlDR^>)?=`_=L7$?@&0+mqWz$G6A74BW$CZ$BM7ugBR_d-P$PxswCuu_b^+n{^Y6@%+FZd+^(N`q(O1 -Ym1r))eaAlSch+s&!r6lcKC;}pmJOF?Vvl{I85e?6P~Z0oYXWyz3jVRZ+ -kwzWzo*MUL7}90?8I2UNw++RX3p@w^ui6-KW|4aK4B-vUJ35g!*G1mT>Vke`LF -s4YSs3j&}apjybAM;%-W>UO4FoRA6deKaB%}1AfYg&Uscn4*%WJKLCDqNt#MK5mINS+!hC%fUa**val -`pg>Q@cMGL1J0BCz(vqit0YC5}FpHAlTkv-R_l-zbE!fwzjUKma&m&e4<(fHqyQd3cnCdAQ|L^@mv;W -d*2U1%pMSccPC{c0HJwBMqQs4#SPe+8PB4G0`1Lr=p>kC95#EE!T1;28^Pr2F&cmm=L>UH8opDJ7_xRPqsva(ix(lm(7p{evWQ+3lQZp(Hx<+E -oI0($Xg#o=D6%DAF!^qCPC!+Ndn8jw&TRrU^#7$A9-*#HPs9@DYr@2$nLe-QjRMAyrKC)r|MD#BubZ7 -z~Px3cfsvmr)WO}MEtdCFrp!bkyHvFgbqW{^@SRYK -d(0$x;vb8Wrg=rP4jZ>v4SF}JDP-c~@5u}*$(D0eet`@43(lW!ygst98-ZqD_*f|IV9iaJnp2~S&)Bx -f_UNmT$3@rj1B^rs<(g<%Ch!&FK35h+sq)3Zj(V$n_O@6?p8S!#H>x6G0h;&ZOr6xa;v}^HH__ZJ8Q` -uMD2lmMC{`_V4f5-C)qNBYzuK@`8<{hF#!jmZ -u4raqvQn~J0p($UZg6Uh;ZFsT3+QQT1U{y@1p0Zth?2GklwIA5Z_mE02X;!ayU`Q5_r&!DDGVn`Wm~7z9m&nj71Ur21H%L8=w1=VzLE>t&j!&&7egY{ -waauiw2zr+6G)up%Jzyg5L#XeAWO9O4VW$ih#`sOG{PruaVvGGjbfY)ws=6Gy7;ng8RM!f|3&f48_wh -PgdkoHJ&FU;oQ62{YDme8<)Qhhmd@LDmCnmroTuIJLD*@Q@}JO{H_O^sOYQb$N!aVY{{mYwaZ7~_ -n?6rn%ByE10$qGhlMBLF79(CYUWIZvz6A!1Q11@5U%XC1OR60xQ%AFQKp-zj+j$*mguy;3yPh@~L>Y` -IR^t%5-KqR+2mkVzdL#aBSD_#^Xk@_4}b&MB~}56Cm92L<(IYfVMp`v%r9CcUXa8@VPeA}}VT*Rj=DX`ijbCZk4t}xB1M&CAh%!!O!O5k3)8A~k5#z^mHE -?yv-{OTnDh+2cd#W^DEWToY0S|BC-$zczkLiEZ*ZxD;~5BfYh^6r976&XIs6are^AB1AFo$DXmhvE{v -KZeEx;=T(}w|jDIb~a9>fL#12XxL^a1!?=nCp#d;1eOQQndF-0L|-C}JKi$eae*jW}s_pjH_c*llwa^ -NaJ8ZGr$$DVpwzc?#QBQf?~f^&5;SF{3pv?)=^9yVGx`@4x)|f*ioy9+4!pX80?>Un%{D6>UwN-?rH% -y7i+QJD1)l4NC8uLS2<$2Dy!e;Y);^iW#-9U3MdTprBWt+Sg*7H}dtS&^g!0EH^cDiuj|Hj@Mv7W3Ln -qzNw>My7d^lL96t}5QW#Hh%8bdd4ot(dxi3E>t}soM}>rhs?f0)m0rS*cy)lp;=IyLqg+fee+#g#a#Nt -Oy~pfM9|by&c36;ThGpMv1!!gnKh3qze53aM4uw{7Tv#fTZd>AY+UMJ0q@HO*}R*hsI56MLdX;8fq2c -HiJEPfzyejhIDeQ)vQMK8l#aFHKNasSyrwaSsE(D-HjuJ*)8C#&~cW@-Pfu6~Pfmz -#8EDTT4C7*=4LA4D))RsOviW2d$NhxPoMFW;PgdwTXB{Xv^r+uGiVHS!L+lgE|=E+PJE`oYk|ItJ^qh -yo;=D9E!Ku+;i6@+S6p?jWet>UraS73eY>??y_cU_KUTH6bNo -VL&(nD_BMJDuA}Olv72m=WJqT!>DrOdxdu9&}*rP)<_tSYO7iF -bCd&YBL&a#Ei_nKy0CKum-_0`*d++($T7!5ypQ&=$vJDBsnoN0~Rr+bb)8c%kydI4<{UHOz!5nTFd6S -PmmgDI2CriB9tTC(y3&cfA-FVR(Of%ZqTleb&IP>{Np(dhTBNkdnZ3=f9MwjwH=Y -y}SQRzO*es<77xC44%@KxWb)_syN+n+IY)fMZ?3csZrVIh7RG1)5oh=R$ -%&Le`qrc=9^JB{&mKx@e{9IJ06+L%y}Sj#Pd%C00yII-uqKwVyLRMLS?ioyDFdk@uMMj4O=X$V?o5=4 -yBWiv4DMQ|UF`_3|+Zi}P8viFPyn^f*s0h6Fl0p;g6=WjjsH39S4e6voV2?5G4k^Dt?cSGbnzj*^MgI -@2Y%Z>)0zd-ny#vo?2%La%20kfYUdM^*3$gQ%5pfsv8l=xsYL`&rvk&eTW`I|h3mY^*p6RElBvcsbnN -6h}b@AZxf@K-oNn@mhLkg;r}_B^QZ7a~76W4{2+eq~=qEKF+?r1**&2)@o(eS5heJIuF8Z3!2p(w_62 -r|U36`f^iQ=c%^iv||HRML0+x`75w`=QjlwLJnL9JjvVAvh<{vOr{s6A|Ff{2U62gItNW6kdB5xoU!& -lauAL3bPA;#r{S`cd$w%}lN>hz$wGz?WGvGrA-x;rHCtW&*jA;L*jEfBNSTK~MqfLKE(kQ$YDX>U1WR -!6#jAbkf^$GyUU&3}TMELUr6h~unQ6f9hr3owm$EonY34~Y+b*eJ%p7XC7&#m{9}ldE6@(casHHIL;t -Ceid=BfnS8@Q@zodospvL1pfQb)1e31NS)&g*%7Cfk0B?8a^iQN(rlAC+8?qCnG*z-MjkXbB%qZAhsK -oMq;LYgJ<45)v@kR@E<5xR`$fvmscmJXP4N`JkA?Xw@3agm|)uGXxtrBMB%^@gBkb719LDS -Qo1n2ZhH8lkAVev_uw7gWQCsB7ZqN-6oH36mcMw}(6Q_e(>=xG>-4qOxmWM~4X|M}WCQ5b_YPfpnDt% -g<)zpABy&kBop_%nuf-6ZvG-aIs}bzBhcLH&nH2%^mS+04wzakDtT*ZpCkC9t54CzQ3m^n#9*VTlMOM -ou0wWhD8q|0e&Uu$F7MP=klZ@w!T($?g7sV|XAmA1%!DgRwRlEjN2S?AD{la_x*)D7mQ -!z)9@s_XpCzW-CoNN(mot>RoEcchZW7vlj_HOmBP39jds@Yp-r-@l|6(v0loj!tsEOaJl~Yx_wQ`*y)7$QCx5kRts!`g}m@zv6qzc7+$a~qK>o -u&PbI^niR5wwFb&THzhZ;eo6o-#`HE$W_MZF!>WzN#A*C{d%&%b>4z83q$<0O-VTNvUk$Uz -W;7Fw9zTc@LCDpFXZNE5>VJ0?~IGNHR?+DXOH&V{D~L3OXs7xU6!-8k556x$+p4=~#n0* -jeKJjA_C4=MOnvOc9``7CT@`tz3|zNXU0O%>~v|;f3tnAg+?NAZha~AVmx#vv!akYp_>LbzUExh=Ofy* -t=XCesVVE>qI}dzaIS)+qyspkWey!2+TATaEW3v*&#v1`);&; -_NXUxXtfdX%>=aek8Rz6?Xy&05CElgw%)jngz>gSX5~dp%t=q%BEW7xkpm%u9=1pK6}z?(7O&yF$BP) -bCLw{e5xzE$E-7qpD-i%N%3+Z#SE3Eag>Km -)jomRVy)_Sj6hC>_{>}|Tr(c!CR?}NO;n^)iAL_O1ghe?LJW5_?cU+fKKpOFqQfdrmD9aDw24X5FB74 -3y1w&}F7;S!=1bf_r~sPXP&}cFYMzds$bC`DKd>YyU^QtO1;B{FBhyZ`dZ4r&orDlZgsuT*kCDgx>P4kN{`_WwE^9MQ -wP{sW*8}awRk)#YJbEa5YDKN*9`GL&#~;OW$1UiVGNF|^-^e~$~B|ELF-qR%I`UJqhIOJr -@J5?9pkub*xpZjR>oGnbM%64M7{IoQ5cphPgmg;9%37Zr^l6J!`h8ocuvsxEL+pm(vw+Sz@$B6`P|Vd -N9Wj$OE=89FkHTBx9w*LjOA)5o%_0n2871DM5@W0#b_jU4;{_F4gRGLpW^L84dc+S-uXV0cW7xYGHy@ -cW}uJvUa&>TSM#i~vA#TN^t0xh_4+Htd9$$z^wmvO1fj;vo3Xu}4gqUrSF&EH -SIJ-sL$`7&Ao-nO|;E0GMheqOD0JFEEFvHveO!z|&%)O#5o$`Wc8~I4`2c!Jc4cm4Z*nhWX>)XJX<{#TGcqn^ -cxCLpe|%KMxiEhAS8|eUau!H1{EiST8r0|#OI(5rWJ6Sfi!r-I2J-qWLRrkysdB -RtrY3it8KYe1gqjMYqLaA16sv`8rxK7JyfE^WPyj6%u;-kad1mIBndf=td1j -uO)ZG3sXW=-`3O`Nbxc!{|&&~bM|0w+Oru`|8doJsZOZS_Wym9Gmf$y&`TGw>n-A#A?py+#d-h1zTV$ -r|cRn#QiTlD>Viz;uaE&9QIYwo%#H#fW3C}UsF+TWiJn$M?xcNG7h^IyW74nB4MpX~js^F!?YAiWjwzeto`Jx>8*rDw!vVjD -(F^!(<~SQ?V(FiT5W==vFN7)LxI32D{w>7E%xLb4exhq -;i@68aKP3*?B=}7sBGsr`Bgh0IOzq9B3Uk8GLvMu+C)5jNi8518Q-0x#Gf(XkZ;cETh)|GC{LnlZNOOV?j*9+c}2g9#uTlHZ+Wg7Atip6;IL?Z?-7S$yy -)4CK2r17zX~L6$rW(8YA)abRI_Wq>nmT8;e|VaSG`R+Ump2@0{LEn)#Br#glxL^g)>kkSp8)U<#{`^3 -dywr=LKjz;67T5#jd+GV}tY5;F^R-ng)VxOG -c;PT4G`hx!nbofN`p;F1;_1VpB2X0Z^wqCsL+c}bT0uSxxqKNHGzU#kA5aVL)e4p{@(c|~cCBC@0wgm -eQx)mdX^qj=*q!Ti8qErFkYh5VMYo>bn`}Laa!M!1SCzKnV;ok@!)Q4*zh9{=o~#wj!Ag#WD~lc8;5v -A1@&>bNwSw6Y4aM5!&Bcy&k_`$~&CW>R9oE#b(A3uL#qG#Wr5smZ&vJ@-P9Aaw`Wf73LqJBAt9+0+$M -2<0E+p7|MJ%30%H{%Ca6o(43yEuJKa5P=TAY6zvpoyhpvxpaWpY+5fEb)Fl( -G>XeST`51EMAFomsU^>bpdc8=9umE`~5J8eoX|Vv;qgD!DoITz)JWolt2Iq7z<(xcVYKJql$DW=r1qg -t?A!|dbz;eELe;!-_DBZJBNrsv_og-!!RJrpx;S_JL=|M+yg+hFv#l6a1{@7LfFSOLT^u#lhehCLEcM -GleO0Kkn5O?3=b`F*2GF$aK!eHnm|^eOS!l~NEPt5$UB@~zl1TD~FQ7g(Wvjq=>-qBU5g+J -e*Nh_!y1mmaXBnh+25K(BTKNvOr)b!NS~v)SKJow64pb>VRUMU)T3Y{4v6K?R1Vi@dUz0*sm)58*oqk -uo)tM=;>*YExk|%@yq}XS2o3 -1+GI)r&KffBGu>5uxy2xt-dxK>OYTFWN;;eK7Wu8B+et9l`@8PMx4i8r!mhlmZ$d&jzXi-CSj)f`tV+ -SHKQ_zna$wgQMlfM7BosnI=r|-CJd(5=vV<|8KP+Cu`{+|Jho%QM(ES1S^`<0DF^q^bM -kd`6AsL8AHqnK&)2_?QvW!d$au(^OK)d63n8c41@muXxl1ado^)}+;jpL -A8$e`eQz4t+)txXOIDEC#@4&D_@$|+$I16huiwX2z`WR%5;auA#z#zwn#^x1?2ca>J(gG-U{3@lCZig -?@sTK!XIGoHJpp?+DY)-Z-89*6UXh$a9k5C*;GOQSwFB(amUF92HuZd&j2=FT380wPx8p|Cb2owV>@K -O{ts@wEwqag4M1iiW%1z{WDrga;ji02`wwH_mN=@{hcvWbW}eB3udZ-S2&Ou=c^N8bhWz(+mf@cAP9{ -w;g{aw0z4CStD?t{Q1Iv_b%B;vjjUF43$Vkvpv}$=-A_)g-y%#MA{GCr$|9o35Av6(V0Sd@!n>?wAfr -S^(M*@WO-cM08P>-RAQLem)Ok900Jt8qy9dfk!<_x;objBom4;Yu$3(>KbnPLayk+dRDIzCk60}elcC -I$sWX+9=Z!-u?vGm0mnyB>cjakR6O-y7sz)F;apyBb7;V9Z8&%DvcqSG$MG8;-g2{@6GD&UNS3_=<&d -)FI7z)rjwub@q2ppeE9-`6h)62U?p76YJX1fYij= -;T}kjggxjoOF4dwHl2g=#GXH^010_V5WNFusANc!E(_XMmD{WkDdyHvc>*I$=_z3U7eG4&S@B2q!l_@)uc -iNj0QBF-pqK))0|;|PL+mj72ilUo7BIe*+*Zsxk9Vvrwhtb}1r~99N49*>bnWY6lT1zGwR)`dnM|!sW -Bue@^zXdoBYZh5AE}d8gW4eGrE<6-hxFx&7{dO8WIJDfMH?R8VmTv!p*i1@4_aG4=1itmv4`9=1kf|C -RmBO>gabb9@M%^Ltqw2LR>Z;!hgakCJbMnolXYX*Q;1z6ezBt#b1aCr&3d -NzIdNH$n4m)_}4-{h{MR=rRqBvi!tL$brqEb>o=moV?&_Rye&(z`h1vMfTX;??C+zIa8sR484^N*ADX9~R!LV@|vB(gV3R4+oOoBCy%Ckn%jTFZN~O1=R -8$GxhREO~f{RB34)72A&j4}|$-mFKK% -IgzBWMbHcrPvxtkK1d5$#IRuDuqB7C6y$4scO()2(|5K7sBMcR?+n1yx>&vIm-%EMIK}t -9rFHXsY#5(>1JHK)H9tgze|BU4CF_9x_RM$zp3_CVY`hatlw6mAtPs^bZ`gi1Wz1(X%#^|H$KDF3F!v -i+++ZP;oY -ui)9Jy*c3cir-I#-Mjdry`()Zh-yVSY+aWxc1Wut@KVjqa|B0m_#^&8Pb78kOLu~w^yMS`{j#7{i^7S -GVC)gTanCcQ|L&)bo-pcK#vVdx6zt@iZ5e3ionMxBKuvcqP~{TR3ch^zh^^u6EN4Snx@+Xea_sj@#QO -Ac0|UO1L}x7>3IrcIMlWtZ}N^e;fYCXZdd%fZ$A=s%#fkgdKgaz8Kk3;~Qf{z;QK_DNpMdkUK=WyY%t -u?uuU_R*XY#`(vt!c`;aDBy76k!W~<2GblgcO5o2o^w%izn8{{W*E)Afk+OJh1v43NwmqsX6di2{Z^y ->K7-k&84Bvxd*MHFj9z%#dO!Inb6F99{A -Umy^U>XwRCDP(?ZUxEnWDY}#ro)%mUIK_OxfKpkPvFF+4ph -YLdkdV?gorZiJ%CuQejFs1GB8CVC|{*wra!dg&qt^A{Mx0ne)@F$09+A=MJhu3zT$1+Xo2+?Qb8dV0 -#GUMa&%yS{=h?t_PV74e)TJjqgOPI#Bm8`*+ZjNC6Ypv2P4fD&W09Z*i7I? -2YT_2euVtX;D>5K6hl_$(;G=ak9#EGxukMLs+mw->nCD|ZfiO>wZ7&CXskdG@-}3a?-N*ftkl#KY~H& -ED2srSOK?VAnhhGiy0++r%_x6Sor+cg==S+*Sn7EGrj@+lmc+nl0Wap<|F`C(l07q}B>ra0u1`5OnJ@ -Xi+`dC!29a>m6p}(!V6|G2b3Dy8JUW!H~Fi80y3EdRWn?j&6%Wg7p4T -QUOE>`sNeNO9$S5%8npV9GEAe>E=aHiCoW$FbNvfgu4>zf(Td$HM~hGoJoE6EZf^5u!QoW^Q3Mcvaxj -{-5o-vi1#(vYbXxS&3}VehJ%k5R0nJG=E7w5wg{@~=Aj9T2S34uCVzhBK$F)?TWd)2t%gWHM!n$ZC64 -{7!VeNDEHBE0FUI%q=oJig*ITSr(lcSDhlz>QTU25mRz^+%b<<25tvo`3KQP#Aa%x@kqM;uiSd*u1d-Ci$q*Il9P9erE;=KoFOMSikHxBwzRan+=hZbaVHG;izZ+MHP?A -ut{F0^SubXpP*e_?_4Phy*8+c|>!8r~E<}g#-})!q^a^zeZIdA^?2E|BTH(h(Lo;HlOpAoJ0i4Wej(B -UexJagj;*~PZ7B2<+KnU$L;ryHwLWcn+?nxq<%da~UF?i>Elz0jCiI`gm{f@=E6Hha+J_JiV$`Z{&=w -UXg49R~VhFMU4-JBDL_9t4HMVjO!FTF))!C(*bB?9gM}^P{rX%-pLL1us!J^grxRlQq -lR=9wa01sVo}Fi?FG2_x=;h9g5g-`B3Q0<5u?2b|hmZcvWn__ZWuigX@lOLZWetW0rl7(y$c$HkLAdI?oOEm)8?FMD;xd^@+?jo2Nlp?o`QmO5jS~{Iw=i^0zuC@Zx7UH?5c*oHCSJtwMH%MlLk(fa%RC-=l)!3AxgnDo}IeY@2j~v72WAET|Z#O<)K8%at+aF1?SNJ4*J$8t_La( -yd?g)GR?j?Br>c^qJ^Em5T!!=eVj7#TnRv*rIN{C|GjYCSf7e9bf!iX2CaX<{i&un3awp -X9qaKSJHDyPxtrB~tb)eNn6zOjOSl+ZLZuRv`&a0$$KzMz@@Szj>nT`>C@i;!9wcCA*hTwndPG-y2+n -~-cd$rRpE>MP@|h -2;UH+g;`AL%GjSg4YM%B#i%V6HKa!wi(J6=sK(Ur%M_kIhzc6%qc6c*DP5IX+rfWXYqT=l>?4) -M4rOhzBMiTio?4%uQOq@-A?KgH%3Tz!ClH|q{VASg*weRW7v_tRn!YtkE`HXp2EHZ#Xjqo2!kBsr*>V -e6&pXSkY#Pt#^k|$RBf}3khFSmtO%Bn2)1ygE5~borSk1Wzt73*#AF1Y{fmc9I+%MJ2YwbeYEgJ5Luwq}Q3m_>m0f`c -KDQ_!wz)1qK`}hJT>Ifp-&&U@f3S#pQ*P%`Wsa=q+X`kRqtj{0hZVmXZMQ@1%?YPLl0(afl7W20*!|i0c*H~{p$a5e# -{LX<_ydV?gd0$!k(R`Z{wtmIImok@43tFihFHFq`qPcf@rm5$}a%OcL3x -3WPV5!$H`l)CNbBqz3rzfKvKzXtyUA~R6j_U_-O}4vuUnaPe5qbllXWBAA2kdMk)6sxBnd&&vLI7#K0H#4A?Ca4>H=-*>v)rF1w|MU*(Ffw_!5dY}3lH41H? -BZKG1ewf-y)5b6cZ$JiD@&eog70ul?F>r0~IbolY`gKudO9W>*vDFDJ`mygSVw*gZfKooc4L#@*`t|V -{M2JE8nI-X>x%ScS7f_N?HxZTWL_WyTcTmlXv9t@(8bdrOoHB@~TUoru8DV?lBSf(Of>Gb~(uKjuNKh+_SVk!i0^2&pT|*o -5>|xu52A#!V#()=yFc2|XS@Bm$fKQ~nh9+rchtegHzBIs-80=gX+ll;-X7PBhR@R+{)Vk$dOwo208|i -s4Y|o(ipUVnJYyffxHt{0qeK-h{NdCdp-Y>9!ylUlg=ySP|=TK;D$yc_l>)dt-I(Jd$wwr+p7R9Rqjx -(T$<`orC!nd|zE&URhuYUCK(H($J45|e@p!i~RZ -vinzC!N!!0lKT;&YV_;EhI_$DrW6w-L*8353B*={_$eAFkT-T5U=;?kXqT{QT+pBQ5XcnCppZVdFiN`{){qsRV^q&omLJr_a2+qsHuW^YAh3+uu4uzy1sOo49Pgx&b_0Z3&Fh;2!fWL3pb3Ge}7fK{{ -gP_rJIqlfO(L-X{|oqEWvhidgur5^I=p=v$!T|Kn20*47BF2e}ut~bvbqd~R=aJLJZ@RBZwg;otmOa! -TW=5M2V%^hPVwg9=;B!kx|DkorRDgcNv@`v_;wWJm&Fg@1i-04#sra -N6_vv%udQ4W}EgpjDf69Z3ePBYZ}P1I_NV%-qBNa%Gf$&@;P?q&Qt$SCBt1DCV+1npqP`ysHk&nAL=P8XIVc22@-tvyNL#RQIjC8ep%*XeJdYzoW}F7fyz2wdri`tap>NrWn -GS$4TKPdr)+6jb02n-mzM=KiDAj%Ydp>k?2k_zlMo*E|Oh)Xd2u1$~C_kv6`(}NW(lwMH;Wt3Y+N`Ee -R?49FxHkZEgCVd@or=?n -K)FjXB5rP~0nE7`_`p5kRGJfvJt+{IBkZLnMJF{M`MN7i{S)`7m^1K3Fub90LaS(_OCgVjrGk37|jYfo$sn&R5&GYiz~>!+Y|@ -b7r;%FfDNMbvIY*qxYopiT2bY#FSGSOjF;51UFzcZ^IQndIbc+R?iL;REJ953|fwy3G_k%CGXdIsXMh -=C%qgv0g(p~EuWu0Tb^1D+^y}!CPUn>({VouIEqX_2av%}C(qR#+*Z}%zr_8xoYAip%w_tD>$C`FkR~ -p0Mmp?^+qc{JVf31uyjbMj6=w=L_f2P%T89q_>XN%s=4qWYbof9XxPD=&!g%PloI+%J -7a{&S2=Spt0fSI95lfxcd(u#Xd;yD$tBov8y6{$z7B{peG;<*z2otu5%l%AafVh`T|cs0K(*DX0aFIlFwrLELJslx>Qe}f-hyR>LrDK1 -8|29*jYJW;}Cj+-0aY#OXyZ3&rB*|o$pgdO+mpL`H%%w^}kbLp3Asy6lSwa(*Dn#A$SD(jODp$$8NEal~%VI@zg!otu -$;E)-<0RkrSr_jqtnoY0m1r#Dhu0GLDFJs?sSD%!NUZc~N>O7O_qp#P%=U|l`Jw*IMTMJWPc``p-ZBp -{c(!!GDjmlE1>)e(pesp`I@g*q-6SYP@R0OpmiQP|X?D82+oFtza7RQsQ+%t_FiJrAZdonCm=2oKG_A -eiBa|3Q`k(eJe*VZnhk5qEpjDtXMNKG@L){Yv!oma<$0pLep8LV!3y1Me_Au-G7Clx(4W+HR1UXk*y! -*v4kMQ4>AIq-XQaX*Y9G^BbhG^08IZ6JBBT&B*BCAo8lfl;g1NtyJ@5;P)7mDOdNdma+UW9!rDJbel# -ETzf{u>%!>PO8F<=fX-+tp&>8rp3mAAZFmNGFCemQPG{APS9WTb -Kz^K5dt-uD_4I8q+E=Sf@Gs4yzfWOm>4W#HD&C5RE<%P)0*^HMpPfv=S8<E -avFk!hELWL+H(;Gb-4ei*LgHW*g9$lA&!V)Hd)wx+ask>D-;6(?7Zl3T@ag%rJQn_dToZ4@%2?^!iN0 -Pp{NRuhPGV#bVH1E`>leY4e5WvE6h!2ExFY^~jW&^{Wkz>~LF)ADs7~l!+VPpl7#kVeUdw0XE}8FCv& -dNuIXUDm-smpya9fs3glh4msk;0x{5NMMs&DiEJ{dV*?)?UaojfDs{aeeJ0V_&`WX=AoFQj_5p=AgGy -J6=Pow$uCf+DwrLtQe~QCG=_e^bN3R>C9@N^( -^wr#-&O_zw2$GEdvS}h?<4ad1zmGgIz4^s4OiuN4k^ko?+oYq&jG=7bjFpdF0?=H_Bi6Z^KHRgEZKch -CcOQ^;l^FU7^>s;FB~V^!`KB)a%H>BUGtzxS~xK#p6H%kB&n-svfcfD@>)rCn?2(Ucx?kqciTcqT!eF -hyH9n2PA*TBXZJR8(eM3 -WCLi-Qk(^caTezXSTGEFu8;mQo_0M{b4;d&y}Y!=$k%mqCtyTz3)*;;t3YH2AtM;iLj3ooWLfCTGz9T -Vgu9NQJ@FwL7NwEY1B^KwOdyJgV==;Opsm^Ig5?K-r+FucHo9)dY?)C@_TmTXmm{#SpXc^rVVhcZV@? -Lw@jY<*z~zMyv~^bW}!0932R{FY`dTYRC@l1nl>YP9wx88&sMtD#TIUPCSGJ&22KH+ST;b#2O6l-S0!MYr&LmnwigzI7Z -_-jsK<@O+ND2rTadcu>@>c1{<9g?h;W2&171C`f6lV&bzzwH{EoHT -FF>KI3a_S^DiE|QZLDHXbGQOPlN?BKCSgQi>YC?gFpmjnlx%OwqcM<~wAwVKoi`t-=TDf9-8NOMx2^Y -yqUCD)w9%_~r&ahYmsH?CWGPJ(n2LWFIC^(SQgO)hbExniJ=TOv@9vSfUz+6gp~(1n^H -0&_pDl3Bc20o^>Dt2`JC3W;GH{2ZKQYk(T%=TEPszply|_6K1&If1H+_&wq>?!-*hHo5>#tAv{Ve-*u -8ptQ=;mZ>U8D9f2NPJ9!OY**3&5i0kH$b#eVKACDH-&m*NC-^9D7Fj+m3`V27FNR=P<0M$~wcz61%(9Y7z}SyO#d+Ov<@by@bBflde17! -^%O=&eVdB-B|Fs=3`vyY2a(=_PWvT6wL2)bGlwSt;f(`h}$tT>F2|wkKQeJvg5ns=*`U5%gN8 -TFoTpc+hd0#`qYTJXI#@c5*!J8UG!&KWb;NF9PiPJZi#{rq?Sx`9l3;aQyPrCO)~X*p7Rsp`+sW!IZ8 -n=G!9D_|~(is5j-w6`OL=(6lAC_&|RrXXLZirfUY@mp`_M_L3v*nR47DCrr`+ITY>TEm3?q;PsmPH%l -~O7Bfyi4FzZGaGxO@RIoY*Pitx#U*ZdUo-=$)UHY)K*4g!eq@WVKN?PA{CYzB87Tz -{P$vB}lvw8pqw}{lrWJ+TS&mE)0M{5e$#+<|mOG}k$agq79Y1&$ -Wdm|o!*mNMS>6PsTt}LY%5ke+#pd6r8H9Z~wb1b`P3U64)@oOx#^on9LmgS5eTX#!QC`Y{F6-9!fGWr -!5I%_mCjj+Z=IG3TnZfYz40UkrRaUlj8m_XZy^4f3G!C>XUiKudi#5-X>)hstoX5#&EP{0#`VMHIRzl -qwElaH}>JK}stgWD0*n$Jg)$H&hwumW5zVaGtGD%s}#DyVEobONL=44isG9RV`2}^OSSvVJ1;gkOwPU -399N6kjCcm{ISBKe#}*d9P@gpIjYa2Qusba{ey+CW-c_R;)hx}FM}Q2qy_q={PNhd5*mtm&N;rg)kUr -5P>;<3g90T0g)^1u{%x%AesRs>_rxo*XDTZ+Le*0^G+S$+!R$N9nRzE3%JRdY8^b -VS!&Z7&Jx}lWw9jUetL}l^n@;Y7`s@PR(n^PlF& -xl*#L#&cmY6!+6Z8Y;H7zWWoJSXV+zoJ)yiT>Z9uYNzlh^9f3(3_f%*47vX&jmDJ-w9O40YBUQl)i892HQG -eEKrTzfByYZEU@Q&owgr&e>4lUUCaWC0d{X1jE;H#f5!rk)?CB_K>U9(wq8_$P9CZF8%X}&-cOPI7d2 -l$7b8vuYjh&m#W2lS{&+ -C6+cadJ1{)8Vz1c*N)aJTh@SNfd($az^Xe|uvL==W8H{fxldIdN4Zt&H5>DXIRyCaK1Rj{0Hna@dwG*$rZhyg3j#7|EC4i~o_1~g5~qN`uA -dR0s-!wTsd28Ew&KI~2H>D>62)S=~NVEWcNBR(aQd`ar;aWs7JP@9;A%;xh(0|P?EKCp~fMHLo80x5SwgoiLpeauqa*0hOwQwr&?mlXLCgG=;u*yDGF -+{h#?F`8!&P9oPnWEuPt8StJ!l3r0@`pS0k+`S8(HP%l?$OkUD6lW78pUXj^W4y3OCQo5*LS#ixt9wh -!=oo&^v1MM&@>8K=bs*X_+G}RdZ>D0b+p!NUF0}C43;0AbVz~nZq8#430z|J2S+uT7j@295vIZ1Dnp{ -K7aWXgSID4PHp7z*|?F{KEW0wVtgQ_<_tR{4&ruR+ssfj -J*WZNJ>ltnRLXwPrY7SckSG4ImFKse0#{k!9@p2=(W!Ue-0=mwp3aH-IJB%I({y_(byP}+NVi41xJG)HhEkcMitf)iLLP-m9$o!;*^&avP=tbA9J5n8SPm8GA!Uwjeo`-90cHxv`I2 -D^9R=_mCrAGTX7=x5GLLry8F74%_weY!6MzE83@-pipCP*!ih)6?hda}7$F2X29qRY&NwWG9`uV-|B9 -*he{i3Ugu}d{-&MTG%qTJaaQzTE?8R1_vD-->>LIV -`3TnC#ruDK@fPcr5P>lSq%Zgo8mQm8*ZqkJr6g0csV;BFIrA5=Ka{%>*KOcc9@wn1}=MJQIaoie)f|k -`vbC0V6mA2;F$RJao$|vs;J*mSKpmP1D)6m8&ppD5|{^nU1o|*VX>L>)Bm$>J^sPhtw)twi6XS5yB6| -aeZN$Xqk$vMDp10F|@dQ8awE5aPCYFFUU9{T3c$g7UFTpJ(iN#3y?YTCwR1oi>k$0)nXC7w;LCGrPqzU%iEv -f8s;7jMT4A0^C3@W3e0*ds25LcdHhquAg}FgAR1+{4V^ifZr7OIb`aVc9+-nGii5Hof`L?#AQ~s06uV -8R<*+iJ8NW78bH5s{0Wb@%sBW0uFU&i!3o6`-Rcx)flMg|4!NWP7x2Z~D)6FA>B_i=vQln?54=cGy%| -1O1q&-U8vxF!;Gjn!>p}5yy8W$G3-5a?)xvSCg^WYv9{pfzCVVh|F^?ne>BGf{P+S~Oa(h6~3?7+LQ` -pwEDbqNm^`u)I+ggKnFIrn``tf{`TyxU;6mBPomaOK!CyT@^J)Ku`o^*(rdcZ-)RN{#(P04V@q)X|SZ -!)!1nM(iiCaRHm2@ib@J_(n&Ll@7aVu&3P(WJgOn`bqNhCPQt5}gELgbV`mss~D22OLoUes^^D&o*L|DuIh>NOZGvcnOSXqI@C0-7CJ-nt@;7K%xCr$1244c$KnA!K^F-JxP*$`(|t?=Xw<`vqW -!3)MTz{3p|tPpxkU#@0?VMeq9`C9i0tYA{&hpwm)y5Cr}iVNHB#UqnR^M*V*Y28pFCv8G#1s;z);2Cy -m)k51;2viH9#U|X|?bU`s;WsD7qBoJ*?}x59Rm|3fa}J7!{Vd|cVq?eL6%*PXf?f_9FdPRTmH5+`?Z$l^ISVGUCN~SB+P}d>5O_VYYSg=fh5_&B*~^U+oohm(1M}_NM|NW7@F}Lg86}VoY4EbjCB~i>Y& -iL=u8?%m9CVOzp{(1VAq4}2OFZ9H?Be}Z=Ngy!{yK1I;^`zy2aO&=AqHvE=Yr|{oHXPU)V&-=segvv&ez>NO?)!ipwURgGbJBv6kA!_zmz-SClq)B1-^hdRBJAr^&7@D* -mx<9i7@fhQBXZoV)i}h;#?XLxL!+xtjHz=`-4ym&y!78I@jP^&9?taAhbkEZ;r0~E2KmKo%xS}%hlG9 -bzq2IT0#?l`5loMyeCN9JBYqYYOF(VoeA4}B=0QuDw=P?(yM!Kyr(1ew+-Rx#Jur8`eEM6-7to@-i(1 -qTE$SBbnZDMdC~FbmbrkXH7Gn&rgC$XQT#y^_a+2A)rX#)QkC%Vx{;Fs~@>oUz6pxo<<~F*_5)TE-2h -Hjy5Gm}-E{U?MPHTPsIu!l^dN;e9m(H%`xQ3Ju=f&s8u4PBts)wA1XvQ9Pe#}9b$|>K;PXLpM}GFMXkE;DnQM`vv7C{P7|+}Hy6*=#0-~MJbS~H;W-HrBLQ?Xnji9{{T+$M_&v*Uh} -L$g139uUqC_Uf)o^;-Hn&x?}W@uQM-lJ~*B}2cPu=Hc-Jivp6Pb3IN-=gBJR6j7d@W`maBNB+&msfBV -tj2;?1U=1G(sv2?nIH}I~c@ZjTsMe7;1votxivoy}{lRp*YPXyQSs@0M$aqmKo8;rB7!1ALBv-~%Ue1 ->nj{Pb?E_>nz9z&ZXWOq3JWCOc@+mP1-`=+*DTxE6;ako+*bEAjmf#BuOZ48IviIx^ZbJE~0Wi%lI>W -+=9Ov03i4$Z=Nq;dJ5Q8%JOnlu7PHK0>J9rlB6zVdPGP!Hq3QZ6966f52-8{I`S!aE7?A$Sjkt++PES^6x=RBve5=tJ;(2YwV|Jp8KFU9pD -|!_nV*>x~$|@q!+K?nuFg#cPJbo6CocF(@7xgWU6{cMn0mOV(n&gYQF658cDsn;Mfd`k0j9n0W7DV}i -gB-vQrk@OuV+9aWYYN7@%#~o8J3Q-mw#I=?S%j -V$4!_NV~LiiQIZx;MY;kRfl8=J|{*OTF02=798=fgW6-VS&>;BAMu9o{^=d3amlZM|@8^gdb%&o%H9; -n%)))Yw#60P9g>!NM1g!|1pR#%^RoSsW`te(hcIBUB=BX~~O$ -{i6fVjSfUc0^!x(xQ9o%*!n;+0=zF~5sQ%~KQM$#dW)>7aLl3A@Ddd84#3;6NQfPAXG1jbnmSbD5w(} -t8Ik`>Xo1|vOV3Abcx}V2Q0H+!*aY`gEC&ly43KSz8bqurFO+U4+xF|BOCU7POUL~l=<0dQMUO3HM@P -0jcLBhcze@uwWdPdq>}bzlN8;c59Xk-aEuv?vgp6*o?Nta}IgE(X-va>IhN{LZC^A7krN3lR-9{+P3d -8*L!QZiol5E3(jW8=3(%HF-CpIrIY3yA1QSWinIL6TpC0-Pdpjoqo3my5c9cuNoXBrN4_$BbUp6hkoYRLwZx7U -KZy$Eu2(?8`}h!UjqGIZvQ_KE>GGDfoH&`jhSBTRvg2|^^zLWaaXI?@E4Z3tr!!wo9UCk|e_Xk9t&i5 -TD_Tb#&|ym!2LO6%40B7pu`8bdAqG`Yt9Xe_b47c#XmLhj`|-~H*#5EXi8qJD_K&f$g*uq@``84HSMN -{ud+DAJai0fN84^{tSd}fjvc(?#*pU+rZ*e^R^wTfkAvu^Z0l4JNS1@BozNy{ezC{`gZ<~TEx*Ut+A# -SNOm^!S}dUCSuf}w2E$ChYtOMi$h{kONVTln3scvF8z=yL$=3}Hl3TSP1kKXwWtg -y#V6tOi{i@>-T;OmTE;*9af-YZX!8=XF<_8o_ftAqPJ0>2GM7FSO` -V_I7*M2MR~hpMzYZ8_dGHMmmQ41r3cE=4zG -)#`4@*O6V)yfk(+h!sH8!`2|d8VcLb13 -p{_zFv>I`Z1Ogf<`mPI$fwIHfD`6n8-}U4O;XW4%z!Jzb@W)D!+MuRbKS>5)5mxw*#*B{U^S9B_*-LZ=IkejkrgXDEy8;YE(HQV9j!- -bk)#R7%m#iI{Jaz96UB8O+3UKn*QREK{s*S9HSw7QaQ~gitATdU(;)7jQ;t;d%Fs(ixZ@=@`fYr7E?& -YEjnnhRo7M)dMK1)c+_i!*Lcuw+%Btwe%sWFMq=rAwNKo5r$uY)S4cR&W+*dD$OS-UYbKz2Vl(U5Ah} -+5ARxJ725nb6h*-FbmFJT7so=!g|;e)d;zba0+t}>PVbgyu)ljipF6Kh>mFQAA7Uox33dkD;F2+$k5LHo!ee%EaFw(ly%yUijnMa -W58v!jtLS>$L`?4HRG?N%vEsX!uV&h1!IIJv#O^qdZp6T6$ -aTkg}G2j!^sDRwn8B@2ci=e2FUVll9n28JMVnI@XmH^K`vtfeFx3ojT>f1OW%4H;jCv?(5uHu%>|fNF -@x?(@T_&@T_@Zm4p_99-5=JXC59>^X)*@+-D0iA3l?6T5<)4S(Vo8wwO1?4SiZehn&PUc^yAQE_p3F2ag8c=50_ev%|!tqz*h?9g#>79t}~2 --log7PNyjUlufZwYTw{-+(7+lCoNB9Qfy|ROe2_#pR*ml&Ur?Ppr`P-=YWY?8EX&5 -IoOg*FD@%$B5Go3hBy6MzuhY3i!^eirhq=PH?_Fu-K%fmfHH{34$&i}QOQt0An!KH*K=Zqe9(N0)DNx -twB!h`*eKR)kYTuRW4T589REs#bPzJo+xL#Rss5*K>eS&?+xb5Ie9uwIQC`Q+_b|;9_tAA^#?fhMWMa ->;mBtSs2gAdxc8|i!tiD4I%I7qxkbH@&R)|cVfy{RR8Ff1OqU1|qpFN{d;c?q{chu^vaPA -y3JMUHcJvx1<~Y+5+BosbiV$%6G6Ug%JeQmqw1CZkj5eF>kTUXDm|wPP{C(A;w-xt{$N8J)4#b|N5*U -7X}vC-7tvv^T74mjn+*$WK6qRGvOp>7Y;<0d8rsT&T>hXtc;kJVH`{xSY&tmNFPC81Tj~B03;7+40cn -7I$!<{`y&NgN}*FaIIYG`UZ*?I)zibv^ED|?zTpZ*-QV2Szu*NCHsTplGE>L$T2Ho^FS? -q}uK$$>Fi3DEHynOu)U|TG>~EMn$)0`ed5AqJdmd)bC)g7&nWJVfG-&4^G5jkCgJxGk_x&;5KexkY%g -g&A1?|odPFtu8p-a%6IqU?LTUzC#Nl?M+|AdF3aN~Lsden|5hvbi^k-q4#E&6eW#lDxFVKkH11{M@=m -41nE6A&&xwO-FoI))9Xg9Gm379sRQq`(2!heF%k@YOL+*~@Oz+skg#+xrrGJ`T_JM^3@B?WoZ9$83&U -q&$W<=an;HVY`C2059yBw|t`@95^UE-%|8G;MpTEArQtw?uldx&$sbzh!65nYF(v-Fxu5Aw4Fk;A+z2 -U6@F9%-dBH -6-$*TR*2w~l$@1_a)X?cx$nquk3bESDPP=Cg8H7bJLABE6_Y3@84{V<|U^buDKDVr9$OwPIn**^NTzF -MO&&=vJQSL@?J{Y5XE15@fzUhKZ;7R0AA|U#Gu|56?YVM1fI;|wv2tVG37C>8_8Mhc~g&(6Mg}6kf3q% -JH4-ybxT`h#31f8ViU{Rb=B6HVZ%#f&se1nV(O95*kgn#aGRG~etex7$8S0tzq^F(_=gYW$%}`SP#f}?&ZpX5CONcGMmuXTgpqt_niO>j@sCHZ(9HWPd^g(2 -vV{FzgVFi~MTE{O~iFRowNvcyu&n|n4ZlQ%=AqE)IL~BUf`jNI5bCH)(^COQjU{(ppST-X~{3<0^`1* -SBD*e+6g};pTs1x*}L4z&5g}&H-Vc~z5UuIHLA0SZ*MYjBq?A5#CVdTMm8Cn=WyupmHryr_p$AH{vrn -#ktWS6chXR+c{+TQkyF=cren(JBBSQb}bFa1`XA+t-DHFct%0rc1GM4Qwv1Vv10CIoHLU-c_5hXw`^XeR;r?$_v31d% -Hd0qUj7DnsMsD#|H&@y0<~G3ZDEy{wadTI|Zyo%8v(>HNp|NZkt`LBcpm>sCQTcsfH_*S}DayCBdRJT -;L*M>w>L3Y;llRn{m=DL$GUj#6j*ziGHY<%w9oQAKy!6kw4yC^J?PtfJvO2(*t)B6Jb8|ETIne(Q|7c -ndyZe7wnoNVg)sM&AGJ->lyq(y|_P{prE}LB6IYH`r=(T_h`=culO-y^%?C*td8oP3|n -Pv=t-wE_S=N~xL%w^9~c$POi*ol_kXW+RPX&PO=8?W7<^LM8WuaBS5Z)zYJse?Iuj_BG`AWWHE{7-2`@QPT!hxulc4z756Rsy=th3~GOO3-Bak~x)+jcWY!EmVZA2{vP+ -pPvhlRY{hrprWk(UC$sJ^i^C6#QQ-BSM; -l?Ry}gg|K{$m7c(@>v<;8I?pU!D(rJQyX0Z$=_vYHF=WijUTmKd#+nVlTQwF(H(ddp+~{JRB!q6ndRU -=m15fetVD56S!qVx@7XVLYc+6#ZAT}Vb%2|EFgJ;g8b%{4$6r5qiPPA=D7)$VXtw -GM0JIIih0q)*RM(ue&$OCLiP^UcG!2&-doCWgAJQ`qjpxL64j2rj&tU@aQZGt;P>h0q%pB3+Hv!*1*k -z+J~2NfxVJg~%@w_KmZAX!-xzdlP^ttFM3jvMH`$S)y4^ic8@#z%Vn+unYn!2#TVjxgg4*D4Q_j(pLo -oN*Xo&wrOdaOWAJaQmF~h$0nTJcDrmq64Gy4iMQMCIa^`93CceBvy`Q~+)7!BOC8Gkg0lW1q*T|tDfH{4w3ZT44U)5#8noQfz-z;XO&r>S=6&-kE2u)wpO0ky%EVMI!B&n -z5yY)M(;@W9*L)gYode@i@3GZFBE+Uvt|@-6=5trCV7kr7FE#N+Rm0cIx)}XIIW^0T&|wYm!sqMS7|%}c`wP|f4_ -9w&&17guvkYh`;W`+>>F9@T@8IEo>k@anRw+gN6J;p8vIOr+)K(gvs>?~xwcT;yCP0Iw8vp{pe^3BBm -&z$S`@}K_uLa9j&iDKMKA92BU+OeIbmXDi|{N=ac3ni&i5VNw0hacJCMQdPN!kcUG{!8cvdVW4t#U#4 -rZd>g{U`;uKj_RqORfVf<$CD;s --~13)uOd4`YVEhk-y=~%-cAX**GcHg=K2$I@hVy_5<0rMCZRo>YZBVLxpqQt+>+2WlY4<>wAd$oWs(O -DN=>laJ1NUQjm0&R6{7=bTva(+%?G$ozsp9WqtGjJJ#8)JqqbTS3q!9AOR&H7j&da1-oD0Jl`{{g;k; -Z`VITLK(_7eDLVr=>QrO6~q6O}tqzG+%#r`K{Abzw6k}rKmR`80#D-(i=H}w-zMh5lSD0D+2r&648L`hF_shyiwu>pKo0VF-+2X`49?b(kyZ*E7ICeCXS!vsGJ(b+|+IG -WqJ5te??4!s9$HY*WBjra<9=UMjc(Z-hE7vdEDk)TJ3K8Y&ef@j4Be^Y&6mFVlu9uO*PHi&4k38-h;@ -dj#B-uR4*8KX3>&FXEFx!6HQQKF@XUukQX;ya4By>#EJ(;DQI2WpyICT0$aSN)k%<56t5*q48qyJE~6 -sf3|f=FV#HbJ*U-Io11sF9*QT?@nb2&)&=OWu>h$$hps#R2-I!}aNxrn}eCFSVBSOHlmHYCeDddhDjy -a16i8&f&NCP4QbLevgaaE8_Q*_+2M{590=jm2c5c+G3$!x*_YAk`_DWA-_G3$#3ZmdGfpEMvZ>y$ELW -&)_Bu$$ZJ!Zk_~o&!$tACQ~a*Q@3W`rH_iBkSJCRG7|oAx`?X~2iW?X$BZk+!>5}#*t*{FEpG|f3KO! -wj|D$%+|G*jgpMm1Lp2l8Jw2-tvZ@esNe?TtQTb$@U?!0Xi3Z7I?f94L1cDPF0QBHqh?@RSIzim?nTteR*s6;@?fd|1g;8{d;=ukHSmS%r+lF -BP4z13BP_Do2g~q98ey?|75NFvbg!b0!ZO^esI5@_KrDz}RE5Fwg@o-9r7SixwLx_*uWT%no$B?Jzt8 -||Q#3fZ+7H+HMOeH^03MOuE4i^0(ZdtWhT#I3hpB^yw -aD;lSj2tO%=M5NU8lxs0Juc;GaT|sD%0wuOOv`2_zxr?S8HS$sZGv>u>4vQ-GgPTKlR4LFXJ_+GD>?Z -4u&I{e&r!KPY=wn59^aB=Xs?sf4&>i^--O);+I}+_f$p#3HWt?0WpfwuiK$c3;9wn|$@G6=vG)97jLS -qC;PAhSP#z-`@ZhJ(dCeXg_t_YB6PchPO>X0HyQ?vw{qHk=GRkps{1(Ot2JUb}Gh47B7E-8MdrqKBY)Fh$h -x#AEHa=j>mAV!k2mS-7AftKexMav_aK`3!Va!Jlwp0vWPKon}8lg=TW)I8xr%_I0jH>`PD&CrLd328f`ArGx1G;0Afz=3TAp -I1J@qQadrjT8w3jRLnP&o$2HvUsxDbJ??x@<4;UjUe~OWi(ElivLux}yM8jnnBlJJJ3 --oGBA{X`7CL~a5r*<=SMTltlG>%Fu`PWqNiY5bvmZ*_iSuwX>_7mPms-#>Bx1vH(A*>Z^B0yoS$>R5# -Q2e-0>QT{htqYlc6^+)qT3Sz1jYun*e?TOEN>1Lk6&C)G9TRoofYcHqnEi>XO7TM5(@V0tTf5jqE*0%2sim}SI|kaj*G>Iw;a1VGStr -|;iH_;y>=c_2{8|mM(gLp*b+EsE|1Z))eDOR{u(N&nhSh?z0zHwPz4Qv_Zd(yxMDfY|brn4e=ZlOhI! -iTLqKJ!9ROk{BH_DnGys(*pGC`JyWrA*DS<;odEGyiK)<~#&V6j&}itbLhjj)MfrcKle8jS>AD=C)7q -Pt0q4wp~4iZLk03()nzZ0uF+a<$&!uGHu{rkFWXWnEA2B_`JUc@wh`93r@Hv59ph$RtQzBI|l^YGToOfGSMLak8_dBJnEwzENRFOKuI4uXw;7GV23{c4qO$E)whAiIc8WcqUG|TH%&B=^DSMJ=W+ -Om$YvbSUR%eUi&Na94i!W&r?>097m_v;yuLyKE_XGjx@FqI3(I|H?!^MI;TrZt2Nrf5H+``mD?un_xr -@+9b@r~X#k7D=8b5kS#ghDRf9#hdE$%>=L5y%l@=d+a|uKH&o-y^r&>!c?d*1&6y{S$7<&#`D?Y#V)A -nmoo`mzKHg`|)9EeEW$5DaFy0HK$U_7rh0Z3?AcY4Vu){K4YPD_6y$I-f|$M)vaXoIo`)pS -(Q>V9Ho8Ea=%DhxxBkr`legoT)qhx>sGJZ@G1C7LW43fKIMGdU%g+zH+S51J;`U{L{QNQOo%@xK59QT -shPdSQ}V5VVs{^OBhBoapOQ`+ESYt@kej^b8_{@awjx<*G;oeXiS@M2UATCN^G7Sa3ZFxt)yLh%I4R5~aTcRzShgnZlf!n65h_Qn>Sywg0*E8m46!W%y-{HXC`!cPKz#^NVuehG##{j7(JKeOm; -+i5;)@v3m6<^=KQ$9VU%UMoE7Wod8Q9(c0sf~&=|r0x6|_gHIH;b}KZqq%J(`dNP)Iedrh`1O;8JG}j -l29^%C9W6WyKbMGD>u+Thh(js4_qSF(Qa%){Y|8IZQEgp?Po4uyu`s+)=zr%r7KZP(kDcqhApCOe1>w -8w5e{$rxhJSDo@j6(xRF>0KA33FP_ECDj*)h;_$S&gp`Re3@)cjmSewb~=h5fz8NoRw?lNp^|KSPog- -eO2eg6};J5e!euChA3yxRHOBdkTnSK4;Vsyc1*j^b*(sS`^B&FNzb{>8<~p4Ug^serPXd%W@eNIhIJM -i}{9Am2V;6_IlnlLyh20Uj9R(CreuLRYr~NoC;#Y2oz*qaTy!|Tg`^{@EK7Y#hG+y=+-^Omn06B4zC% -;EsCT_DHD}<&Rm$2GjEq2^9u(TKo6mgK#L`;5=%RMgQIKyb-#muJp?#9cx?zSBjuD0E{!3_b9e=D>SK -ux;@xleDDf0Q_S$Y!nN)+z)!B35)MJT6y~Q5c0|*G`J_>+QN?+_m9hZDwy*tQ=ufc#xM^uT*R`-pHRa -ld{d5oLIBQdi870m8)UAif~ajTtBOlDo1z9LAs!=-;$E(%1+x$hF-zW_J{17ib6Zt(t?l|)!G6~YdqIkOenpEKpM -nPqiZOPuHWtU2jotxfbY`WD`Y;o7x|mcPYW`}WW4$uA5Y-Qtkhd7yjCu-BaaM=e1Ao4D*AtbiNGWmM9+HwZ2ih2>JqdNBP`u* -3-Fz^*G6}j3f26f)q^Lw%pdAGIAM_eqPRudM+#`6*;{nAesmSumn&#rAgM{V-J(nJDlS8_cBJr3e%Xb -_7IQ_T`7cWSN_HcH0o1Q#52Jo1dkp%O#smY=)g+;-NhYW4H0o-yv#6`do=RO!_Us(;n1Jpl8{N+|bU* -ny^EaXt=ZjCeW38)-_gP|WN6IeX{Pi}@`Qq&Fw!3)7$>7Fk9Qd6sC{;5KOIouXt72 -ONHrc|3H}C?CXTt-M&_sYWoUdYV3=J+1|cTn0otMVTRZXve1f{_*9eKDx9M1nZoR6Hw!b)K3bRw_MyT -YVviT*aC@{cN7+M#Io2K|%v5_1VNSGn7G{S1eqrX=?-8cO-a?qu?HmtSg0e1+32t>uOl%2`lz-*ym(A4i=c2={Oc(vamuQW* -;l8=yL5th4p}_unNjJQ@|b~JaE*3-CtO7l$X7Wu%3eTK4Co#tGBS?jA6Tnu-b*f0y{P~UKI2Tt2lJvo -Um33`i1q1pkG+41pUG)uGRZMSW(;UZwV`oI<>D9R(up|e?CJ-8MhnRON0kL;j_;c*4D7*39ApR6NME^ -%l6U2s)BW}u=>KnFmBE4J;W!a5w*4Z=DK*4Kq~EUe2#B0ghk9ieU+VQXac5Jy%tPO#6MEUxZ#H+qbcT_#B`G_@ -Nmy9}3H)JBiFvP&P~GF4*?ns-zB@>~O#+8KEj;d@$JpXNr7R5@NtDJ5^C$3)qsdXjKyZS=^HT`ov2K1 -Pom+2y$8(%$G{kzIC6E-ItPblGLS=<$r~GFx&n89f%uE*X+bl+k0U? -DB}@($DCzLU!pVT&BhugI3GbYbQBTUu!RcZK#hll|MSWgDtwXZGVj3$Kq+CPU~9XK5pCYSJuzCeU=MN -eSAtrR|SvuwKfx{h2s{tXkS~9TAV~1Jh~gkUp`*XmO{tvz6&#A8VvdJFYpC*rSXQ?{WzGMXGcm`MP4t -ga?RDvj|%q;#hIMb?zZC;Hrr0)4omwz?7#tRwn(sFPbb95smUIOGhqr#Jxijm;7rde!K1HO-b@lFP2z -kr9ChX^T{w)oYzxxRfG06g3Lu6^UtoXh4(WnoaSUj2ytm~cTeJ#}SfdJ#uJU?nm4tYE=ZNCE8{4cZMa -jQz)n$mJC~E`qT|SB;o&K8J(9;;REcwRcT*eA!wvXYpQ$v0%6P(UnL^PPgVz -xCHxou?N(*zG=#!K~8#N2#Sz`_Rdn!zD5dEkb>4i3cPEipgp93L;#zP@@I$4;gz^lOygYkBdTsK`x_H -(Vc+rMla`O>yN4%CVu|*9UJ!$9@CTGY0@@2Uwj$~h62*amsS?)`?MpT(%)01c)^FqLVb*sGz${_x#?nRN8?8yATefvvO -~nH7`qwfhY^v6H+2TdXH%m$!X7^qqm|(v;UD>x`zksyl?Hlr0co-8>tS-hI(^U5T(=jzfu -32P5xMF#mhq#Y+ZXoVeo%=zb7VK}W?3E}CEv44&rq#p -V9hx<~U_vA94cfN@m#tM}k8Zr@Al&9676&Y6vG-?Rt=pQV@u9olUYZ;o?zGpovoj<4H15r9YAYM@jE$ -AJ`|jw0$zn09D0jY^^y+h%hbmW-wzcO@bH18Xpiv&`B418AbeiLG(lcvaENyUq2(Bi@30rlpCdH+IIK -z9AYvJijBb)7TTzlBv{_pIX`$-?qu5~|Y{#Vkf6Sek!u-4p9TF4ytle)_Hllpy9GQhoYLvzsO0Dx6#wY3hvXnc$EQevd{R?jX+V8z$ceK#y%`bDT%!+O*Wtq;R{7*BkD>6+hDefVww -wFD!F;vXSuG?c%k{-8b(iB6O!}jI)f`hWNVMXtHp9yEqae#O7*nvG$}$T?{EM*3M1OS?({hdmAELt!> -G%l_AB=>RnjoUbx-8={f6XHkusoSa@IQk<*2xE<3Jo7bi0MJn2ffP%=Ld0u<@g_3hY9Xjz?R!)cG?ag -bbhBmG^J{?@`W*NV9cK6gf+jSIq>D}?<#zYmuv6IRnW^(+Y!`rHP3UGe$Sh{n>dxmN+jl(H8IBY@a0_n9ORvDdPP{HmZPUm!{{QY;5UDm~|q| -3ir4ee#mWP_->l3>-UB|WZOZu-GxhX-gAq`Qo{**T1rrjl_9zwX5#|adc&zuXnS(IQ`M -u1XpogKeOGNC0;)xZv#v9ZS5x6&bS>NNnX73Gep$?jYmyWnlOR{f=L9^37#Z)iC{g!VS5?Bau6 -~}V~uMlh}_=4aZ!4-l=G&i{Sg2cb?&)>?^w5}%hqtV1lr<&N1S4?coFq6Zt*GDGSrkm;3NKVi5-?SPE -6SaN}LC!Y>2N{dZ&W)a)Zb`_^9&gUeV=R;9vGFXMy(aTA+PFrNM>7#qrlus*B@(>vM -ynU=+|Xp!GgHh{&jNH&l~377u#JDA*}DMTO(pp>J?7AL~?$;j)IYc|77G+TzHWLSm0uai9@BO`k}!sa -q7%VHLmPMDgRiXfN%fUioV)q*t@<`7DIAWNh_=P;6QB4IXw^{vN;%9>)uvj`EQK7TxeiB2_95hz@HP` -C)f$U>nrSq_;hf^7P=5~qM;yvQ+*Y~XCFa7z>EW{R*AiJCmJrO=UHGI>yma=c94?#On-V9K^bszVe#Z-cvz(`ja0PbaDkQxwT?O37q07Zn~gTDRZT~103a@MKLTwKC%S2CJ?PE${~m1j+e_h*txT-2pQ?Vv-!N#R?46r|RPA3$x365lFHbe -zn^+i?Lr0DiaD*8rybi{&>>PS9fbN_v{Vf -Pb<`)y$>~%zMcaCB32Klv2DEe}5Ky+w#t)+{`k@Qhm;r{m}|e71CILy0zs9*dV3x0<%;nnYn#?@9!i( -^{105w{q#C#^s7sQyp!<c@bZhD%c$zKbp_^z&CyG7?HQnsstt1V%)LUyim -qa1^Z3nGWc#}VhVnEpGkz6OD42OYBF2FeXoh(vdXz+-VJv$aVt*g9usSeaTuBHkSLk=k+E>MNqit@BJ -%;bli7g`{VtOj;T+y+Vh@sedOMBB$lOBa3^G@c`6!u1WI_orlT0k&j3P5$)Qw!xPow_@ZNo&{ue1u0% -#Z$5#JSp|t2nZaqnP0K3_*KSnGZe1A7dfJH>5m4gvylrUMV)xnk=|Ejef)AvZ0ppCvEfQFL-L0=fBxn2m8;jTSN;0ijq2ZTa -^~Xd=I+s`v1gN}&6>Ap>E+$(uDe^`b8j1;w(ag~fByp=R1bFSX3``g_IC03(VM)VBJRD^?n6TqJxI92LA2d=wm%yp?>Vd}cWER`ONyRPsSODPne;E9O2D4t)f+HSbvIK!`ji -w5Wr#Flty!WVBXV-nMH!~#ju+ -);9+zuP$(6#SWC<(fgY=Ou(nMNF2We2e8At>15f^b-Vv5C@o0=lh=#!f+Y(pkmbMs`kBy(z(S$-K}m2 -C0ZS;8UFDw`=5#6>)WM>vFmKkI8&)~1uvvr@CCC0bG}>Dj`4Xr4KD;J8WV@s^mZwCvnW;gLl7kY8L%o -+V;Y0do#C2tdXm? -dvtWP2c94`)mH>m=@W)G_GCJ>Iw_$s4c}Cv$?zOIaV$NIP0yK5k0*s-jhUw>8J_glSZP-p6%;&PFsFb -raC^Mq@y7~Udw5U|#l^+c0IJmm2?cX0bhobhgrowBPnjenU}K!BTfy7{ip40Ki4!M!dN!V(lQS(R=a% -&UtGkgu=*ERsU;qL12Kem2N(C7h&@(lIgtMkxhNNejv#pj0lB*O;N`@*rH#a+1m7b-FOR;8+pJ>Lsah -z1&Q_?fcsVYmhDm%xVr7}-9kGEP<#$`b0hi1V7zcea%RZ5mBC5Hl~Qh3w?5{RBoVRFqWsj9Tx>`V$Rq -O(p9N<53r9!GgQ#7s5CYR;XJo;5+0CNjUpKhJEjIC--?ODdtBYR$<=A1{Sae1snnTE8dxnR9dN_p8Bs -JRxo|t8%28qO#^8>#m*ix;pV@g4sf>Nw=uRXQ!G~*=b5>%;m9`CAIa@({rdoNiigjQTWWv6tWT5kSvk -WwI}hoD{Ofa&ErzYF(W<8+*1}Wr!>sDndw<6f)PX_A=_h$HJcD6J~QmTs8{R^?eza-E -#-Ah{Yo2&!cNdgxVfmSoL2omyd2&4P{2{xaK#4(ZcFe`~s3J9Sm1$e4rswNkDJkFF1PCAu;)vZsl%>~ -sse>xt7a59%q`+zHl9bCxAf)y+JWYE62Y%AA>Fneou=^BHGWDMFV5ogBnY5fH(IOmk*7^&=fwXX;5i( -)gycSYfRFE`xF$No)7?t>Hk4>?r=o4{IYgRqlX3d$im@q{eMxl?$8#RmybjB#^N6=a1 -jmk^U%*ik(XUOfysKMq8b4s3hRNtX7iNZ^1VS47IqJ>%5i?Mb7j8#sk9~XSj1M`?{D(_$Um}>c#*74t -VpXs*o$Ea@azpvx%{SS4zz5hGDwf*Nh`QO#W#JY5;?SHsV{Hxt>pZ<4&wf&Df`G;#vEK+lO{&d$i93S -RcoylSU*~wlv-0I*OzjaRby7&$WX*j@mehX4AB<65UFG?X7$8&2;VRFWB2rUx1vVjWaoH)UGM6ZY9^j`FnNmu*%+8|ZVW -wG5d@zg5$j(#V`bQ5Q5FMwC)T3PzExGg`LzE@HMC3d$bUcPm0(bIdKL|^*df<`=9C<<> -4$wH?9v9GbcwcHDw!cOauDQcN0LS##i##7fHBlgy8a;hHO_?b! -6%bHjYrvI;z1?E+qRiv06DF8*6Ns%4jGtIEsEN!?w-D1Z5~+LyF2Qbb=9H=CTim~MjmaXXl#KL8>v~3 -Hr6%U2XGLaPDcuW{t~0lM+y`4ShGx+~FqIu4PG*=bwg3gQ -xBN4Zgq$>!9UESgee3hB=SU!3G?ms6b@G7&ST7@CY!ILD^KR$_PdNFq!{EnuIDl9MEw>ZaRZiYq)=Hu -8|9MPdQFj9lOeIbdn<$D6_ucK59{j3hfnvq+dDC?(@^N{rMlVwcHZf#?TAv1(L&d9TvGcl2k$P=`XjlJidm|-^Ou&3m@4UZ(2D^f{-=(35h -M?@wtrb8+NqvK(3N)kwAmYynF8k!@sV^U2ZX`((sswYyNrqFLXy%U=##1r75Q@i0#>B;zJ=k_sU9mlL -lc9Kh<3~Sy*pw`=(mPTV}#y%6d4K_`{3<-PZZ -NZhsOfoBfWPSQLSYAcR0qpdnBbbR$p^_z*NFU<6fPn%G4GJHa`E(*&mojuRXsI81PWU@yT=f>MGl1nU -Xb609ItEQeo6Fqfc!z(SBgFqU8lK>|S>fr+3yfr_AsdyS0y(7QK*Cjld<`ohF23G4*N3HB0vK(Lx%Aw -dB_20<#pD1spbaRgBWAp{zN?gYLB?Fm{FG$*M3oYEvXMR53YDf~_{KOk63@C-o_K@Pzvf*}NP1bPB*g -6d-?Nk0DbBCYo+y=MAb-(A?PxZ>OZ<$;_YKVo9Tj}nh5uDZ&^{_7v_1|P{^)!_5rfZTt>)3CNt@^4sM -`9F*I-}(GsmjU_tUoHx`p5DdP-E%edd}AM*@tCZ7X0LoJnJ=%A%sIclEl1L@A -WDVZkO+;E5CBV=>6c9GLp+I9sG$J5`VQ3I^{=ef-z?a6Z!^Q~?)9c -jtLT%`bWuDR0m#3@{3!T%NHj`7$BWFp>SGU0D06Z13*RDie)YH`{jv49Z|O-4?kq3rKRl4FTZ3rZ{B2bRV@&K=>;cb(M0sA0|m{Q*Pdz94vE)8{8D -v6^Xh5SX3j)_OZ-6fA%5UGdGDcjPkOmTap`^9G`!mn9XN16ykpA2Jn82WuTB&G(`HtBUZlu;xBNcskY -^R*r=?X(@A(LS;80rHp*`|@wd9X8uB!1)=~t6K9tx8$r7yi_To>UF95}RFa=0$i{}%a6@0361wnI>=! -VUOUANuuMdb#x7g)~{bLWc-USzF_CipL -fW44whTiQ5(8$Q)86GzjFS?H>Pc4`P(jSOPid{@~Mc@wsD@ntse{hT!8fRIsftc^>s{0@xK_G-Jt{bZrp)0HyPM% -Knc@W1}}+E;E_r2$+KZ5(bKteXXfYU$JA;y3k?lr5fKqACMHJkZqT4XY}l}2Y{ZBWZ1m{SY<6lNHetd -9Hfho%mYJE!a&vP<`!Q?QEcUWBoW1dAD0_dlkrhseX7eUTvS+d**o(O)wt0?$y;TsyUYIkEZCaeec0b -jh9bPq=b^DmJ?%O%@-_2S0PR@e%a~5)tvr(UN7I}=b{wFva^etz31VhhqHu49~#-HYF+(pjj%$dVVN= -n$GMT^+;&p*$WELp;qE?vr2tXRR;tXU(X{mNNoWhJ|Q{ko79Jg&W{e@I}g5ebdw -LdKes+%%)g-jXV?ir>XTxj!4p2eBD^3R}zo_(v%Iaf<&9#s7}tms9*p6#tr2{9avXyca>TPQ% -TG7RJ^spfTuc)TiyBO88kT#*h0m{`DZn&rM(Su*uZ^A>zY!0YJVze5W`h>F&;+oM^XGoDgN^me0-ivK>v -|Cr+Mq4<>NqEi(AJjJhc#P3ci^rsZkDTOB}g*BAIPDtvkm7ft_}wXf7{wn>@uy -Jyc@+N@iocEGpKyv_`$?7_XQ#->-Vwbcgno&|J>0FcZ^w>Z`Z|0gddJ4ZL`B6$hKEN)CHLsowQFbJzI -{6>DMZDPH~q(uB`PY}?;!-}+gBy~$HqrS$A(k#(UB41y(5ylBS7cQ9UqkaW8zH_u@R9`krW?z_3ZBF7 -nWRuWNdt_^7OgSr%iIto>B_F9UoM6>Wlc4er#+^cx>|hcenK+e+tk|4$vi;;)h2da%61seRtp8R`M4( -`gT+ik};0-lkaJL_uc(NMF2k$;6WAQM@GRn0{*S|H$b0nAq6(==ct`0+4^#hei6meeHUN$E$5EEK -LKhaiB=Kasw_F`if)9~~Pnh!8m_{3raAqx?-kA(ql;*Qd|jci&9`9RA7LM#-jL(Uide@_(RDAAv%9hk -vA(hnK#kIyyEgp19nngCOGmKC*a(8zLeC+`K&0;mPsI@iEadmhp;o#YAd)NA_}eX*xjm0Iy=AA{_q7! -M(hjG;(oi)sHX)it(}GBuF*>5xO9CU=ugbyJE;QvNy@HMiUuXhOD -GSJwMn+v8=`){2>7|HEY#!U~*)9xRGRyG3WH9r=NGL;P?pQV~mqG(vK3}hG{)5SmgI- -hk0L>gi4HQ<@pej|SQ8UrsN>CIYd`UIyg4t;uktDgKTQLP(!XasmtPOmJC-Y8;dRDyw;!h{&DOmzo9Xt_n1b55eqVAn?5s3qm`>(%&ppRpeDOuLY}qpQ^2;x?)v -H&FvBLZBzc0pvAAR(Z7%O~U`YL;o#vAKstgv(EPImC%K`}NsclKL$_Uu`9?%X+6QBlDzUcAVD`T3F<8 -(h75mHqzv?`+X^&fcZ>BR8Dd*gZl!t@wK8&p&2^_+B=JA7zX9DfTWu -PyKhLQ+(-%RNWr#<>}eH8^w9(u9hv@`LubsS2It~rcFFNnm^>_)y -}<9yNB;@=Ghbuy}Ucz)7YhP$6k2uZ{mUU@AYxLo8FprYTBfUAJwl1JKyKlo_s0F{q#=x`?YV~wUb}3U -VgX~j`Dxl&(GJ-??FF7Uwe9>GkEYpX=OU!i?I+a!nw)NkRs2;DGb76qeKU3eF)bPjTJ#uw{2(d;8C`8 -G;#tDIoJP==dY+SfiJdD+|!{$2QU?YPvW{XY0`vR7IAC!fAPf^SfJwHe)}z_`8 -S6@(ZH#n{n~D~mlLgLKKtymm&(e@@_YC09mu3|!WNu1Oxhv?Tu`n}Wq@7T5`?Wc_akg(W@cbjRn>0{& -ETEm-|YU_wE&S<@oXA!VVhH20-3WPf$j{ZR1azn@?~aevos`KF)jX!ab^-_blan@P5u;KhOEqYuAo -aU1|Sk@c;PZj~Ds+`UcS$egUZN}Me`@O;0Q?nD%0hv^QcqA1ATLSZaNhSg=a -HWi|3L$lf%X&51BiyKFFC(f&3W1p&eb0~R1vG0wvXR|C-yL~@gJT*5#7DCfRgq&jh@_*0+3mG&R_2{q0oUk4mpF|qh2T`>II&}Gn@|~8e&QQ`+OG|{l`M}8;I&jc?tg#WL<{>rJJsRf`x0fmnG%A` -IzZrr%Rk9@d{ubCOkUzifkpUXB0Bp&&m^P%5yKIk;(@u1-=xjvK3gediSpH!dGCUyB(s?R;PNOhW8+y -f2aPvtyMqtSGv_PhxALr&__0l9}<*QGbY9X^oSR(j^thkwV89UbydeLVX4Tgn6V7yW@!7N`$+S7-qK)b8=p=TrF` -GeY<)(?a=5qG1`)fG368&rw9faKdBAS&0UvK7$6OKBGI?=F(Xn5Ho;h%Sj^RX2~gPrrF^HP0An-qJpraprPr9O -iOr9Pug>b6-VbG(a>pgz3q~~pT_s|f`WoN(io)(_@iBf37G*681pE!zzuz^GA@H3` -df?{@O)Gd%s0*s=I;^>ZxIb^iH0ppviX%u<#*KBloOqqR3>_*KFec_t{>NrKiUgL{tX607nFqpsIR~q -xB+*C7PS3n?=i1IyNSLEeH)&SAJg*>iG~fc4Sd~9BY%@E<6wf92)37-YP8+gG4PQV-W0eCBAf;QfHe2o4VG(!G&%njpPiG~k|hV`?E2C5 -6F&!AzYOv4hHhJ~PEQg8mmM4}-riqB4s7Bt{Nn`CerV+8H0U;fc%pbx`10ixZ*Yu@6mTw++t -ribfB&5yekp~Q73%nwIYa~1h4+bucjWqvHtA(+7+;nf#uw*=^JlV5{HcrxZX+5VCmJ3j8fFm?Y3qt-$u8Tm+Agq0oZ3_9O4}?L|6XN;GUH8bp028qg*w^?3!gNlSCX`SUp@ -{xs3BfM|G@#?%cT!8tT$iK55KELNwpI6H?yeQM~Z<3=0lOuRZxLc -pF?Afyg4bJlQM`Z#Nb*kY -(9vblk2?@SsgvKl={qGtp2_JJZn97JOQ`aA6s>oxE$;t~@z8nXg&1hNEB3%gf^yi-pgdH&4{-ufP6Uv -^y9J)kh2PM&E$83S%LRM=>A3gZg~v6)Ug)jpiGxraJ1gGR9aU*XM;)7ybzUEnBvD(O74Qf`5-5J+x_Q -X<4bMsTI_YigH-FawQ))a3D`iOyu+D&*#rQ_Z)xbnP&t(nBN0m(15-G{e(gX<}jcE?J49CeIfGLX^Y~ -Yy$bxV@%^i(){HTxN@EPvXS7K#Opgcpe>ndN2?;5QkB^@N`5{?fMDzZj4I4JRNp+Z$AD=#bI;T3p@!r -3Ge=$bbwQHA<2jC7|QAUbfDdhqBAbXhSU|ff?Kz%@+7WJ9R!MQ%aR;NB6*t8tuz!&dCc!Gn2pINeGiC -70g-f~^$1qB6s*sx)|Q>RY+p@$ygG`1GF01wm+X8;Wf)TIRgnNre5+xqFUaePa$iTY=0jDi07jaiX=) -6*09$xk-&YgexxrSiVx^@qia7xSW`qRWdGE#i+o_Sn@&9(jakWMl}w)4Wp15@Bn2;gR<}6znQz1OHCI9yI_0_Ilzy2xAA^r;fkWBz&3G2;p17 -28jwpy*C{zgVda+Ar#d-m)p$^rg}Q}h5TTwje3AOfI5M6(FPOlHz}PW -A0HnRmHWz_J8`;>(D^jJQ~Z&y^Y};MD**FF;6r80X>5UA#iBk?d2^CCAp_{6z+)wjvYv=`6g)-y|K5A -=@weZ8Tl9hNyz`ET2VN7O?9{f6rap2e;l7>Ra1%N79(St05#u+Ef6)F@eO!Qd)Hj&G1?7!8OL8j45}B -EqqHU$RF8VjLHIQ#b4p9$KFHt6t6O;kkWase^>_``7gz`XtPW0l78nl}pcZj{wNz|3<13kzsdzGngC5`7TpP}cj=exR*Gf8;7>M#IwD{J! -@H{l&Jqv#K)&3x6(&Fw$I-GRSzzvw)MaUR>;u1o+cR;&>5K^MYE>y -pS{8Rs3q|Co=M~kxNsp#)eHgH2ZC^|Wm8QLP~5}<o)qf-I0)OZAPiiL|>p!3YeJ92*X#XGw=#S8Mg2$M1g7+u~WqlXrfOoX{X -lv1q(VRl)HHhZF8+U;}WcPM*-jKXIw?*(*)-Awi&1Ur=#`qg%SGwyhB9Lc@^#eh7p@8NGb -s)rX)=dx|uv})Fv-wdBN?KETIm6A!ezu9z?O&8f@f0OA_Cw-@U3uLqO@4!D_Ho0WF$flcYHkM5<+3X; -j;j)=8n_MzoVAlR_N5%fagZ({?W$<9P87pGFk6<6cwL0@fyhEc>cRjG72YLC>*jG(`eyrJS9z^5)C~E -Vu4IA6eFy_98Ims!~{<@0usb@Lw_l2Zm!&)$O3IRJfA9axPFMbiY+ztoO+TO#%Lq~JFDO9Jc(4Ntl^D -4Es<@w$ve&#%Ur^x@O+vI;gwc)EM& -njnqy0X_qSwEM@{5J_MQ=5B~+RcxUFm$HSH$u+^{R-B^u@?LJ70wU;a!0z}4rr$zd+f24XP$Xx7;wPY -3G)Z^k7(D?7on}jSQg_H_`!~6q`gL82ptdfomkU`eg|vv(6Rkd{+I*M95jpO3zL-jJ?6Q{7vpH~M49t -o+~oW~U-rlklD-eRvh|hqEnU=lF4d6-k-ziahq`+nz!UUeKb}9qUo9La)-e~zYk=c^lTBj3H!xURx$RpiiOsYYL5XhdJ-VK>1?~3|!FvY+jNn*1)hHj5T>Bf54avlHL>gHX)D4C0 -!eIJdj7|sG#eFo^Zq`BI;fxfAr%t-_M#gYt|%ZeYY~6g^98C+p{CZS|ZkSv5x4RKXk9qA1Bq3N0Y3hf -vyiaCdi}TR;hhZ^2hkcSyx(D{|;R6tdsQDo{ypzn0fALEJ{GiGGv=jTsy*1ZA;teb! -rc(A?>UDI2#z7KjPtcOA#p<{w>qfpi_%^WYmbC0D99E3a`uF-wh&A+ZLxULQu2^Ixac?S*d+QUVWY9G4w#~^2eC0q@*Ne>C&acP!}=Z$M_s{0~hcB=4Y$Yh29YQDy*x%A+ -K#Om-P#%6VQ)9S6E<=fIjSc#QcK)kVizdR<+ZQZ^`yU5g -TU<+)@0(sy6f$fKo39$%b20@>Hw|5N&|O8&!!4LcJa9xm2hp_@)mPv=iQ`J|ZN0Vky`P})kA4|sFgeu -5u-c{2a7FhcAFP~>stj6VFmXLESPxl^1sF#ZV(3kx)vOfx{o?Af!k$Bi4eN3B-#$&)9Gb>IaH7KpVAW -$pu>!vz18y^Y{U-98x)(xh;ok!;;|{MVyLkD*lW#XJywJ@hirEzvlOj~qEt(B05lbKSC0`f+7%B_)r2 -&$s`;pZJfwY5Z{kxNn!&zbLKcG(Z25NuO-|(bGX$<3}18+kx -L$%Y_VLEggCd)DLGp0?GjG0^$J|@B-~N+DzcNZQHh|*q!MpME##PZ(hpFFTXrYnRBC@fy0Ce6S$wBA0 -IGafWS>z_fzyk2nV@Q+6(MUeDA&Ya3tnEcaXQpA9=W?F`y -Jl=HfE>mYwW5-&=aw0`neAAD=#{TBiC>yO&E2A}E<1>vV%YJGbP_@i%joxk06#A6cFJ%8m-e7CD=5Kr -n4J2d1+@))^%`SSHESFXH9ZPZU|)~xyWE3dp#tKE6?%{MPm8riSE{`e5XRx~9yDoX3JGKT31pO~2gI4PPbLh_(s!2)HWWcO2fbnlH+h#)jkNxfAA( -n7?AKfVqmA=1NNlFX&n^CkO73f8_hx>=-e|xlD7g7q3WZU@nfi7v_GL_hH_tr+FvFFc?z-Z?s!@Fo(f --@%?9~h%}s^iW}0J4%QJcX9NFGPUwS_J*b->j~3%CjQ23s!&nnzUCcM$Tv{U1z?`EW;eqlSF=B+!bE6 -FdA5QOlnQtrz74u1q-7#**ya98kS?0bXkHl{|UrT&>@x>R#co2I;pjSg#LSE{Q#SfRQ6XX6_;~ise5W -4~VqkciOThPg2|A5k$;(2#=7{74(sNg5&P{rvY|0_!QL-x?tLrzhag9i^5JViT=vixTIO8(wseZ~IIW -z+lf{U5!G_#2e?AAkI@7^^A!anNpK%!Tqmd+Lk}?C8s#ljGAuaTi$RGU?<_Sm}?K1WK`Ofjt -P6KbG171K^1e(xiV?7CRG0#Tdy>sWzT5<8rZv1A1Hvw~#f!zj_FpXvKg1ZZA%v36GaoQ73Tywz*oZ=1 -Vt+_IstBI2XaN3$1!zrA2!&z(YOs$q$C!DnAA%|3nbJiNkAywj(wZ<$YB`?pMIWA*{YI^DM*6xq0c?S)m>Md#XEzwP>nRg;;W}dE -(BLJ4`K5ibpZ?%;T-O>6RJtJGtbVr&uW$b813v`qcCc^8|CA!_(P2T3kp+h{Tzvnln@x_zmrtk{6RTH -G8r-x1-9M9x)zQMuc`uOUcMHcMJ>Z)j;H$%zHJQdq}TZxQ6s{5KZqPy%c_iB}61dL`B66=sSG4Z29N? -?|Vlle46T+hq{^iE_DZW7qy?-q)t$eSFcoWSAVXqRyXp0!2cKj!2y#4o(*_CU}M0cfKvg#1b78@2@DM -!8)y!EB5-Nohk;)No)5en=%w+|^wb1t!ZrOhk7$xLQ#ARSBFzrXS&dszWYD0X=|Qgry%+Rh(7~V!K}@ -UDj?_-puG4;}?Wv2>eX09R=cDhYU!zY79vz$(oEiL7u)CqTp@ZRJLoY*wp}!%)kYSi=m}hw2@UCHtVW -;7k;e?^W;A`w@9AX@4v>Im`Uo{R084{8fvNz;<$nPP|L%$Bk&;nP4y`kQr9_@d`e@ejEz!*)L=6lUm& -F`9~LH7oA4C)qS2C8QNU!bnR?yk=CaDw|0qkh4xkL+u9AzeBy(0A8I==5x?+8$vz`X&Tx-v^um=*xg}+!iI;L!zPEV4ci -&^L)dR&{^0|{SB5j8AsI_;*-UkjdZ~Jox}|@Re}sQrz>I)`fF}c%2D~3o7O*GaLcq@fO#>eY^bPD0Xb -MaXTpd^y=%eYV8A0Wgs>#(%)6CS&(Ja!GYIbW5YVHYY7c?YjM9|nEb5KUml%V{e(?R7ymx88gAJ;ysU -8!BG-J?CIJ)u3Vy{P>~dqeA@YpT1KIN_^%Sf|y6=wgT$BXr|+#kv=W2Tb2cuhwhzA^O4kN&1EQrolae -b-~%e(}GKbcL$#eJ`-FXd@1-^aCNYop^2fD!N+jFp|hcXcY0}Mk9BMixgR70Mj#PAeJz*56 -%!)qi58x30x2Mk9HUm8vun6Z)Z0b_)*pE1E$YTRW!X#Cp9LOep6hCCQz4S6)=#gG*tyFxw*ITA8GbXM -q7p>Ktjh3*Lr4vPyL6SgL7YuKK!vtbv)D#LDu1%!u(4+$?O?*0V+ns7ffW9`+^>Otz~)yvecs#^zy2C -NU<92lnQqZz45)3|6K)@pU}y6<$Y^-F>W61FXj?Tt?xR~W5SnjJz%Q+}I5y~AgOFArZ6ekdFlNUWzcT -dVJ*ss1$e9QB*(o$9kx+qC}u{HOXa@n7zL+W$xY76DxYMg+_Wcq8y&;8%gwf&DcrHQ#E=H7-F92aO6U -3QE?RwIAy~(H+yZ(Rb00(NEOBreClBSbs==LjQu{4a56}4-FqvT~Qmg#=*u%jk}GX8NW9+3As0Y96rcm>`X20f`rb6Q#6iCv-bf^}D072bJVNs@ubjql~ktZhvJwZ9HeR8!s9wjaQ6S#%g1qkhqWps`0}^_ -J`aXYNir(p}O%)*rf2H@Mpsh0_PmY){-$wy+plEtqSm`c5HgUV*v{TmISN{_%h&Zz{LQMK(D}#fqepJ -1-=)!D=Y}#5J^Cs40|*NfquC8Ll4ae>$ -L3;K0C~z{H?!x*fVbRJwA!H0t7pT{&yZArp|CawAf4hIvfJAB$`UDONyc#%O(?;v6Yo`m-^)=XxyF;3U28AYtn!<6g?{u`8{ -_ps|@4v}^v;Q{#9TfYd|4;th-!~vRAeY*e*8_eJ=o}ap_z3j?>jSq1{up>8&|MRy8K(KSW|L;8=A!0- -pe{kFLBqA9wG*_9wC_@z`LnjSzMp=G-bNI32<{TRGWa!WOtD>U%o+YyA8 -85BHzsKhuAK{|f*2{CE0)K~((ae|NyBfU5z!0>d@qG&eL8sZ~0zJ+F1w4b_h#Y3o3($HN9S^&0mg-BR -iP+98zEDF3nkss1_s7XM@Z$NkS4E*h>FstxYO=El}kUwx@9))+&KQN}ouyHUneV}{W}{YsH>9@051%~ -w;yCWd*2cMI)A+JuhG_c2!qWdw{#j)jsNOLEVGYL7E_aP)Lv|C@QF5P+U-gc8GSb_JH=V_83unirU_DT06DB -mD(%XDs46OM(#RKU2~ncuC>mG+F}*8#@(oIQtLE2J+;Xu>Sg=s;&chRA-dtZQM$3ZRNX{fhAu}}pexe -N)y<<`>lxi*-BR5O-D=%h-J7~~B-aLK~S5y1$uJ_b8*L& -+*>wQT6ReE3Q$>-`9>(`Rh?{>BJpVlZJpVlZJpVlZJpcLg{{c`-0|XQR000O87>8v@)W-_X6}12W0B`{S8vpyIuB*H2V|VebyDKfI*tF0G&r(r}C{#h5a6t-aA3S=0=gcH&dAR%i{r>& -&p}BWv&b-c?IdjgLGjoe>Um+L;K`_Cet_wn~!2h|0|NXBA{sxU*Gf4PP>gMxn4gSsN&zk?sa>v55hwm -@D`$0#^-48wVu;RGqK1W&LA;&Kta(Hf>>3Hzrd+)m}Gc(N@E91P&U$)&orU(0z9OuYl1(&pl+9` -?L#>Q(Id;MAG$oH=pc)Vmn$(WyojKQ{*d%h>ZF_PqI*CG)YopKJRJuOQs}RI>2=?!mL;ah<|AAw4Ng5 -b7cDCXdUz2!5;_B?ck(o+Jon{1N(}LWhANam0QD#JDTtnc|PL!>7Fpa)v>ej4ulf!s|2OInN;MOC12v -Y7p*)Dy(}2Va)#?n*M(sg78Ap&nI7|+_zYP=XG_l24UY#r^7g)HJ6p$d$)2o)O{r{Q@9HL8c&CF!~e@ -Tm{6UE8KJ?$;Cb>axVp<0azNHc=&O*1^&8GEudKYR1l}1=05`(T@Emn6xce4741kv)F}L!u5Eh&TH|6 -Is@&C_%!4@%yL+&SE`9d?TMWI4_QE0kj=B;w&K^br;6Z1EsL6CFT$)PG|jtL<*tU>Jh!i9D@?04pP!c -+8IT@HDi4m$g4$P&){;vjGn+r(DP-ws9m<4PzZSQofN4(?K}m&3O82Qg`*ZacXZUP9(z3?Q9FE`vyIy -gzJSg<#=X&KzmARWJy-ElnQh#M*R0C=D%fI`r1nIKaB?YJlI=ut^Thaa!b%;s|Ov#yki`cN{-9q -NbW8_Y4L}roD|o#`bZ1?d=Pe|8LCvWi1|GRw?%Rpe4=NkPmlPr0oap&Qfrlo;>?ptgG}Z)b7Xa$<1Sv -073;Cf*o@iIENQlMi*&0@4nn(K;3d^SQC_vy -M!04D5lCgrx#*M}i;igM_0hw>MHNv9NEdlJBcx+Kacy}5N{2VFW0`+(V&I1LinA=R1y9X_&|ju_0IGH -IohmEDdV9SiN~z&eIuLJ3S3xkUG6pOTFyqE7-IKY|HpA{(h8lh+DW0#7^s^^gW1*#TIiZ -&(q23l{nq)CRqSzD;>?@qYz;5PP87`r=t^)&Wn+Ubt5T28*7rDMJ8zt75k3n*&ly6khin8}WWcg9eSVZTxrhShD~SCmRlxY%A{AfeYXiJR>uoKmpRB~*BaPdf|fM<$wry%fE* -Lmdz}uT(u+0=0%->#&(LkBAfMWkEP8e$ffaN5ngR6XM*30@P7JmLhLFrspq^lhV##gOZOb7`Efx<@6B -}YQxWs9nEr%C4D`2`-7}#|EMMiA8dKW?ybK7!TblVe~u#?^nY4r+$&q*MI8EaBmU?ZEly3GebKqiALm -pEr2SRjQ>FD)sz1yKA^6h>{!30J)MY78fr@w|%;i(fs=WN}A~_0G!)~EMg(Bi23vtSUOgB>ADWc -S~#Cg+#gtm*@!OsB2sGD_6MH%UdX)qCji!MEgYyBfdU-pHyr3X1p1Kyz0QGJ5oiVn3UQ!OCoq)xPl#N -D{b};P4Xgwz|J_DD^2L6@rHmL^=!LEVGTbr@S)PEuIRgAl6o3k0Fd6@GaGtYw1M90NisRh`_5AC&u6t -k<=gVZ4vl#H<31$5w#{i)12ul3UdAjY=jr_G(-2jb#FP2b@JZ^?=tKntZdBg990wEFNZ-w|xK!XHEwH -T;yK8*ZAU_Of-ob|xqFEp~jzcMYstLGW9nNG<1={RU{X?~t!4&T_PrHD0KWwp*E)@(y$y$7s3x62bQw -IKgZ^@lHQ`JQv%`AOPPiEI~Zn(6)wMjC^B>iQ%}tnmt^0K3;8zVeex4Zg3uiK8dU}Xib -K?-RNqyx~4@G7eZlH~9+&KXHK6+>6t(rC`hG6*y1d)Uh@(CaDDfphrYUK};m#Avz?iPY6zKB6K&QxPl -j)08w>kI1g(vL7@f5IN)z;)sucA~5rXkl9^*H4dpe`-Gn~KsQ!LPYHsDYz-}NPK*)!JZLG|!dP)8R$k -JBnGf{ndTB^m;>LO9F3ne*ZUvY~5wM!=^y_qnh%^}xQJNnR0{ctVrNVgDfnLhl`5RDxCdM%LHqVfW8R -CfBhk4Ftd47uJ2@K4$_*{7ac;fenjLo`jD-7yK;~1yuK+gV)()@)M<&x6;VvBNtSR-53Sdho&uyO*!A -nT$1xM2I87TxxH$gDMzEk!{rbKicBvLCy3-KVazl;+PB0^@;Mj&ZUEW_-^ZxE<1Q+?)WE#Co3q$^V`g -@d1EnLwMC6ro{|! -NNK_atQZ%wd9Xg^;IauT*``hInJ9|+NF9icGT90J2-xVxQ9!>~Rme_ng?G}RIi=bPRw)zD_9F)m8IiV -4P$W -Fjks$Kut+A3fCGyf5SXFRM+qI7I-qlFf(ett}z~6_A!FLW%3mmLez%uO`%T`1B)Cm6E9Sd)XJ@3ili! -Rm}y%GzQT^q_TWda71TiLkplnLv861J_uBD?mK9XeA6<>0t7u?$ek2*AZqnWO`nA?LPu&gj6Ln+D{S) -q=9gz+%Zx1!Vcm`{AA5K7mbL3wHh%j0idzO}yI3MEi1h&?f*nP}S`ao&>4%%kh|&HI0E3$nekxovQ1AU=pFnC=3$7Yr5Da3 -K0jg>;kHY{Yda5UkfY;zIF}R&kTIPHguFh{ZV4xFcM-DVtta^>7PGPCfG;l)46~j-uuoV;igkm^PRu5 -a41|Ylo$`;c6^8hqIC6J`;$B4URBI-6j%im7NU}7jm2ey*gc@38Qi-wqXl-bV0;~?yJ9SfNKE~r0|4kBawk@r -41)heBY|u_D7H`VF(uDj9!d#Xn>f&8m=P^6MYXwPZ)1dFF{7cE)+#Lv7s)UZ{7n5${=J4Eu;C8uf0XzwIL%$4oqK5yvn3e8$w?LDE -ga3WPFw|6*V@wi=dQ@yupeE1SFr{9Qe`K-7b%=lgCFIHi3?hx;^wK50v+bzEEMOPc5MW6)$00MP5wLa -m>DD;2d+HLQ9UwAwSBt8;VG85iU+g<0nA?Ur-hzv`atVO14FK#}^qh};=|e!GuIjN%oK%om#T -A8Vy+BbJ+0D^cST;!al8Dbm%jSClEe`00>%b%F5~=r+qf++~EmbD#J> -(}}cc&-&sFsGWt^;0i+EFN8c10FFjAF*!P^C9k9SGJ)<5wVsh*c<7s6D#!04SN+H2R2+B({~ll+B}yS -+j5kT_{yEgvk14pWX<95yJ?|IrDKS0p?J253aL_@XQ3lPh1E?ckxghMU*h3R|1H7qun8Ae+~EtrN8^f -=tLf8)eiVbQ*;#0+JtVh5Tu#Kc7JG|gG)0$a-;}&7dh@H-ypHfLt?oDsRBnQFiuvF>q?rR)aSO*Wmxy -SLlP9^f>&)4&W}?dPp)9J=?8%kWF52sY@#u><6q}^+shfAkqh|iS(G&Z^rtFsZM^N -rMSyuu~aBFnG{;Jw2Hj6K$5;**6U=KvFr#;^wDH}v=y3F)wcKuJ!y@%GI~3pva24}-uDFF!az#yq3Ej -+$bQ^fL=F~_AB~S&e33r0q<%McIx%$?J#eU4>hhSOwk7diB+;c;34*IJFdpkCTV-Q&yb(>T5s|VZ-Gz -2EOlyT`Ao>FGQQuW;hE#t6^jM2#dEgR*y9(_VB^`l5&m}#Ikv#5{!{!O+8-(kjc2=_qgp{W2?y%=dAx -1h2kaQG~mVthX8S`SSE5;wKIDg`@+D@RixNvTxN5sT{Jt49I(?(IZ1&J4EGzD75sdYU>ahW;Bxnt|ig -19okR!Ye(kse^QWammXV5QF{3deGPrFI<7Jrn{MwZ{-hrEkJqAg6#8LGkBZg}Uw?c5l_LN7E#A?@3SS -4!br=>S{13!{FDV43fk(d(+BNnhKqWD$H~!2QxJ>Zgc7i#!f&>S|9f6v1MifiXRGENBCMZ&<+|$h(y} -4!M&$pW!IntR^|`lt-mz5OG#5t7!y(J3M?fQq7}gC`JR(dHO`;ooRG&H}Zs%iGo^UkBwB$8T -}qW1CV(@hlkMvnTd8R%EXb@E`WBpm1Om9yP$Q_%aUUhait!oh<`(r_Qe2-h}DCJ_`fl&%}pJV8;~rClzUm`fob8=ETgaEmK&V0AiLT=HdmU{CHYwHB9;L64;7zkr)};&Tn5 -mz?ioGZ&9xveh`XU@$C{hJcxlgP7I0)kJGY#s!+)M|GW!93BVVpUy$MK`vR1n@2R90R16OP&2~kpD5MM>O%`m -WC9_IpsSObEIv>JDmJfe4lpVZ;XwUx@9h-{ReVd24SxtMJ#{Ai;{kNPpggb=Vm$^?kqL|yQ^0tf#kC+ ->^NmFsXMxDJK-e?tMc#zt>uf-55l7_MqIT$#U4N3s5`8fKWAgH~Cnx)?2qut>~fc0YRJH9_Edxe6Hx#0FtZBl@gR;@oge7t}*Gn99ReaxU_jt>4xHRTj(25mNrf%RZiA`keMQEI2KgvfW4Rl7)8um -SSjY~&{_!aD>%FxVw=pvAu`n@)HRu{XA;{mWK=3wK0308Z(gIxcbldZ*7zVm -)QTwihfLd^S8e54t+_eRmu<}DR9z#`3AHR7O!!R}$5X|R7atH2Bou>B{kw+~>dJeB{ -x*qU`P>@{OMykV$l9sTe#2h4@N-E&D>#n~Oq)tsp+Tt`^lw6cmks|2a(=q_D^D>J*<`3819#B)+RJu| ->-yh*iJGH-ztgNl{_i6YFLrZ3w{3S&Ur4un?qR+U)PuAlG4!2V-1U%6{062(oYJ(MfQiF?azK*7{C{k?^ -ksP^)yt#*pvYS>q6jyFr{)k7el*2b#hy;DBD@!2;cs!D!gUA!Hx<-`SweLXuG1d`(lNESpGFi}+p&30 -Sig=UBZr9c2vt_+Oj_ftb#yzAEgznC=e2%s9%e}!virM4JRxZaE*8?Y&iK`ZsGGQY_2>8ln~Py&Cg~VgpY9N2slp)TirFYt3mWP$R*9p=}*F0bsV^xQzvnA~gS)XA0Hz@q^I=)nUhbP}KCMOo}HD+dDEd+A=)L;Z!$&^T+l-{XXK23$J>XsxdPa>^QSSe~}4GAF^;MrSEEs?kBpWT^6Dtb2son=A%L0WEen33b4auYy -_zY5Ab32C}`RSzOaKNXy1h77P6_NXv>rW&ot9iQ<~?Gs?{p`9d>6L#C|r;#SJ0@!G#K{JMEHvq^748#%x**hfxV -g-=FS~h_EgMp;PK;w?qd=uzez`o~) -0``!1j3*SZ_(K7xXgxfv@Hdewy&y&>tZF%@^Ssk+PC2=|!qW}(Eff`Vbu=@&3Vw3|Xr1AM*#44kU`}@ -qM;ab~P}&+ie#rV8s04X{Xd4>mqCTMk`%n=H`RJ6FFe4du6r`f$le+FFSwn{-H!~ -f>GL#INv_cZ@Z)j#2sZDQ+J_?;&U5wM>=_|Rl8~5+cU)oTX$ml`2c{a}GHGdOvb#Hr@4ZwN8%4!gVWc -1Q;2U%VJgs@L_o)9!A6!0gW+Jkv@8+jM4C|OkqLdKjx@f)2+vMNhhuTa?qx!lTj-MN51eN0RstYH2s4 -FlAFz%a8PXMO^_wV_{MnGL){4!`ZRqJggRV5gZ0IX&pIft#nn4ig$gLG=RBzE=mH97cWUUgtt4hAW__ -);^0hZW+#(vW0TkYn8)AR=$;t0-bLA;2rGA-%$fci4T@Jw3brKi`?>fb!?LvR!ldRIuFIi9DFNA&lCt -u#1f0pP~dkuK)`GM6U?hIG6`C+y`dm4IUZWcMwW%3iwl8Z6glLr&~1a?L7|NF=r;Xru1Cna?cm#NbgV -%=kfEN?L5ki;SLR{~VeGt|yNOEQ!C-}7aw2wMsRr4kJO`KQId^e8%0$jS*NgxOP72Eg`Vua3wzjuoaY -!MsegXep^!DleyI;oztuhJ(a-P`$_}=M5Rp_X08}T-3q8t4vdV@-|8PRpVAH`gMXcqeYoT;X>ah757& -EsxJcj`B`uFXV= -v@+s>jI~Vp?giy}YF&f;fguV@c3_r@5wEB-s+Qzh4PRQaxLP8w|ux! -V@7OSr*;xPYVq-Kf1tmp^k<)6;Cm_rIoEi@X3Cv+lmyK;B=$VqjX#SpkaE@|YFH*%9D+yWrY0lBXMq*ve~>tF!2UFWi&eClL+RMvcG2LX1OErYN+T -7BGNK*JV@<=4H*(qNVKErIQj$@K%KO=oN^9aMopvThR*p22Jl>^-+pL@ohg(%f|j6^$0) -YRy3Yb#EYEPsh=0CTf-JT+2Or3N|wJXas#bLlD9@WKmCAT#^3OUbJzZkc}`IFMSyb_NhxwfuLw2joPE -DCn8Aqq{RM)NFRd5D84_S6*DcEmI~b7z_SJ}jj`xSZ}1HjTnwMy54!L#`tx>anZD4RQ-YRE7eEr^&a6 -9xnT`UZ(bRJd%G}fJGSE3WTyPM1s&N-AQhS8JVER)Z+c>m@<-zpOci6FvJ^i*7z|}J=5gjmJ?qJNkx| -MIYC?Eh|wp!`A=j{XY9i## -LAs)>7AF`MYgF2lEkX@$N&x`Dfi${5&jgb2aL*0^?*saOg)gS4A(a(SJLPSL?n|TAAwiR4b!0#>m!+u -!6W(%(}5;D!J`(zLt6sjQXakw!wcy4q{L>C1x>$YY@cHVvj&k3{u1*?p9n(!Z6;+X>U2yM=>TP|9vx% -oX~A+cf+gj)_``RD+I)#q!}&=KthR69!M3%qqq^m<-N$vmpzQll%Gx~&G{)yxiv(3wUkp5XhWXSxi -8&0nvbalzxdWzslcPKv9cQL=s~FR%$09F4M?1D9Iff -;NKB{B$QV$FQxk+Yas4R*Qe9(NDs4IAm1IKm!=ll22R&I -JOOp2HmQQ44irQ?*kKG9&#izoyCBs%RDrdhl+V9kB8>-(8WBokcYWt`k5c0f;}i5~Df{{qtbin9;#UH*U0!;q|+(U^Us-%?z~jfKPwGh9~7Uw9| -}?@~ujl4KEknKfEY0#+Q@KZW1`JYG2 -a~B=6#i|6c3GL_RqWU5!@9phYRgmGD-l6QqGcT;(K#`a)ch<#TUd2N8;rE3QU)48N{G4a${;=I;+hRqCgf*DM- -FemR8;_kw3h#|f7Z)j8Q;!{zq`oVzGsF4nzhEvucRs~`a=4niF%v1~3d@Z1_)f`Bu)KOLtt7@5Edg-= -aP@+E`W<*KH{Nr0p)J7lLxl{ShxELH}z_d%hL_p@)UmCjd^ApTMuG{|gGI|9qaefc=Tnr^V2_TdJ(C}qh7nu5tPxj35z=aAg$nz(<`-blV+K2FQirD{uQey%cDuV;4VhNQ~lKLSe2zC(@Q?XLchGZ$CWkKEiLJ4iE*nBPH{eWk#5@!F_V+aM**+>^C^LOcjZW{@u(L%SJl2W@Px!bIm3 -sW$;WYx%)%21Q#$1CBeJWhT%_*K0ou}BS$waTmk`ut(bI!-PVZP`hx)RizVHl-pET*Vr~l0L^h?!((G -V>tBXM;!cQM^&BbuxJ)nvpnwKp-4%mZibArDwz*cl>zerQJSt|HRmN4v;TIb7842^UU8*PLECG`E$`F ->$%eMxQ#IP%(zWD{iVkQWV^$nB`!X@{OMq$$sK>uSSO_zv~-`x$cV7B2Z*&lb23mfTb68(;gMq*V9dubD^P;T+LS|2H*6+a-2PBnmx@m( -P>2s>JV=KJ~H*+gyS*in&}3UNW->>-X=VGZXf%m -sxtddBJkmN*KXr~-C+^bjjIkW1+y)FK?(MZvm9CpM-qs4Uuc2*(Wlsv{!n57Z$88Z@MpYFa?sp?w -yd~;Ya+L5(ZBX3Y*`%7faCdqO}R2#J6pDYS$?-xE>(IRk%6NJyRC+TbvyO{N6!bXq3=^-4<9zL&wmmy -%2Q36-@394V6hX7vQIZtYb6m15PrWRG_XFG#5tE6@mVEITvh*c^nGG`Gpo`G;@loW?-3P5G<8Ou0XIt -3nq235|RSLR0fmOSZFtBDWWy$n7GU^(8u+h_Ci<_)gDXPOmS74Z?z%z&nLdO4Wiv;=gHkW)o)UiWPf% -<4p~8RDk9B3wc|QpB$^z^=YbRm7?r`LrKKxCRYX613+{RTi0*%B)q`W6sa*}?Shs*c@ZVS2i^rPFa}Y -js6Yz_htdP$Q&vy9hfc)^F@Ye}{J%M)Z*_gdjY#0oGkOO{r7I8%F*dyW3{LB-1WiT1?vd0cZ#yz(Eu}DQasvO2vuotez+w{Ycn+{*A -F|`2oE_sYN;V;Ht2QjgnGGkAt?A17n24~Z0c?BU37&{-TLiXUIwdrxwD14Gx6iPr#t)}>a}S-xxsRO9 -xp#BUeIU-cx5PR3`WWY)K^tj;bAMDE=iG}2a_)r*W_xptb1$STXpD2C?T9`}k#k2Yn4OI>=KXN-K<2% -8AoK1o54Yn>J)s46^$q0SSMag}SI~`B9 -j?n+@>F*=-74d=qNHGLc}c3@C|=PsRO{X- -=8kY9-CICk52*X_ole2FB`MgqN;q_9p{(VTPj?zPL}uz;`Urr%b{?JMO}ht#bIvqusdc*e*v-m}$KQD -`=P0y<>|W*j4N|j@hlH_`_N6ehHHA@Ni@%RR%oO7ZSB$sAsAjR<+KWI?b-$ZxLeHJ>AzGw<)Z+Uom^;0Ecxpfs5smh(y|(yd7wG2+iGx12~e -F?Fa2f59Lwc{`dkIU7rl3ssoW$zYcFMb29W(R9|4{y*mA*2UN8k?}Mrq(aq}qvD3*jy^g6M*+0-#yxe -oSWhLl73jntA1kUG2Mopn|)>Az`Uh;Bpakc02RL$jc?GB`4a=_eZ -Ow+P~V5FEnwLz!1fK-f$P-@dVx~_;g+)5mzw6h;d-e>|iE7fjvu<1@?)x%V4bL7=%Cp -fFbhncE?U&3C`8>(|E`>lUAD@*4MrmgLCMe=r}TH8?qy%u*?T_nBKdqFC(#n!F4xE|P1V -Vl8E+vm~UvC_hc(h2^WR4+(P=5N1+ozBP~v$)@N#0^1&netkA=wsYD<i -he7@(0ZcLQyu@!F5S39JJH9?@x1!8(hajw -AS-U(Ai$NLeA&oxrIi!r}!J`10jXu9S(c8CJ88BZUedZi?es~m1hk_sO2Rr$7LM%&2WR3wFcx(@uNnz -I{T0@(7Ce28sF$3*L{j!@69aTX7Jr2SzI~jBfFXSi489&ur3GdLV$*A%SkzJpaWwJx%U(kbD(BqB#xY -LTGs7G`u>t)x8@(lVR=4Zm?TIOeRmcTPmChy;qs64oW8#HmUnL$NggFaf}Up14>^iC_99lK=Gn%G2sA -c+~(oVdAT7SaKEn?spH*j7(#xmQ-}Q)Tt~ZoRZ=iY}yiLS;sHn&E>g`kXnhYk*EeX$(A -zMB_=o6hS=W-}y|dh!+LJz~`rjFX_#I4-~!ps}~a@ev9n{_uQLk@35HyXd#OHj3e=Sq}>ScqrC-w41& -vaS*MknL`aaYf1{IXr_mxZ<8k6{j^iCv8B3Q_gN|3$cx6y=bzyK3h_)C6lc5L+J -L^7u@y45}!KRsVnpG(Wl%kb$JDYayvdOfk8f!;nZzq)y!J#l}iqT+Ohp0G#$MvWU^lUE@pQ)R4f}WiT -H@g35=MD0_f3I?X~E(|2)mbl3yZE@@D8wu$trj3$7DOs=Q=BaB!IvEJNoBb+Svx!|sz^eM(w|-({(Y2 -9b}4Jx;yRmdPdo7kz@#8(G>lSmkZV}fkVjR;$zKwBAJ9R?wsss4U&IH5!ElS2Mi_2XFpM -u0!{(wyO_{m{k}cv!DO%W -EFD;c=N4{0gk56@mTs`Kj8890UR)~rFYkzXWx}2*^y1`bQ&io1+MFsm_<&xisWZMY7Od7TuNKnF8)lb -huw8^0f;)TrhrHa?NxP;ifYHHP@!E*VaCKQ%!G!1nxv7VG?ae&c3i)I1n|u?ei6bI*xdX1R+t$_*@0* -FV3|f&t&Q|1yd|cYJ`cfsvCg0C|d7-IW3D`VKVQ0y@DT55kU-6$K -m|Fo_-s*bgZ56NpkAXGrs{@3s=KNdt&D**S8pJz%RSGajG%^Dv7L9ko8ZTp$PpWq^Pn6$Q{dNQs87aq -ikqIqd#iA>7}Ceg#I=79ngk-fW;6i6#M;3wrpefVwty*(o{<*N4wVe(>9`$yF-rZ>eJa8Uw)uKIj{afkUH;{4$>j06F6tc-&fi!w$gJ}bTUQ(dntv3 -H+fJ_#Tdh6=4{nT`1spv*xGm3i#&P`0|*ZVF5wZbt#SSt_=I^YJmLU>rVD3Iz9`41r{-!8G2~zyBNae -fxmz2xeCbLM-WAK9Dl-(q6j!D_CT5TjJZg&z$B+mY=U!NDH&$-HeMk -kt{`6j2Dm2qZFIz5BV%o*nN_1Blz$=4|b;%KSeJh{JVLE3M2A4~WB?@3f -mM^M5-a4(%G+wUzn59}tJqA%>Ue_C7`@=W-?;aL0?DUr-NN3z+HS;}C6G&vr5IpiiDdA!(N^dfF>ojzy^zEdHkGu5p1>S -O|6T**ek#N-}+S7F+vK|Azls>tq0jm*vK)y1=nBZa6%*ueMrHfFlQu=sKS@V#hW#ouLQKXdLeUK;q;A -JWP!&`(|utc#hG)YWO}p&A>czh3b*m%oyoOKa@SMh#c{e9!b3qXVAC6HKZm3-)e4CQv9JLl>TxL;kXTkaX -aK*H4cUj28d8CU(&tIPb+1)u{uT8o5WSQn6icLyqOK^&@%xb)m_!4jD{uY9d|{aLrI3;)W9nUliD~<7 -c`)Qjk#TbCb4R-fVg%j<_cG8z>LIkgvD~oc -Txh7s8y8@wM$!>=!7e1h&TUa`u-=08CpV_dfhJ;;Vg1*TJd_)QT>;Bvhcaz)s!|9DxmsM?Oi$j#z0*e -HVanx5xU6rOG1g5j9epVw0pl1BU|qe9xzDhAgcx{q7{sw%8K3HO0%Y?4I_S^VDDE=DNq@>QPWmU#W2( -v(U&nn`kge`$G+%&Q9=dJ85;ik_!XtX2g6Fre>4}&X0!9!9*{NQ3Qv`SLh+V!&KS5`R5}f(CVUPdD%B -~~jbL0{~ZYuWbFonYs%J>ltw+!haB -tcLNRvR?lMP;R6ok-?s@nzETd6NXQ6K -%i%;@_n=>%r^Vz*dbGSSbTP@AU@L)Q-_)-4!7q9NQlzE?mn3o}U1J5>pRf{J^V~6o;9*b$|y6yGH*p7 -%Vcb9In4|uP7jO*rk@rVZ=!}&i4`jxv&uH|D5G2`3i+T2)me%Bs9Fm!(?s}ySUb$Y>v;6)HoteoARi3fS;FTYAO;o^LA_m_h3$T%FMz@ylJzw!oT?ZIs%asnE0QgWRP -)cub}{Cpd_m}7L5BVY$Mz;Q#|(KIj#nFO@r*Sr;5fNmCJEB+E|#ciw=$DkEIO0FLRYyTH2&N{!Rhi^1 -cZ42lS;k%4n^Vs}@AS}I4E`bHt{K{@2c6blyGNY3z7gqY$Pxyvyvfc!mgHt^*Y{}&Stn6kYb|*h1_tY -YRofF`ZN9czCuyb`c?ndd$c}xHn{#&kEEZ7VpZ_`;v;<$c`M}hd=9kWm0K&w26%2BQ^eOMupBQp82$x -HFJKu`F7LuhHcyLxG{99r7xt=gEcOKcd>}4pezvon(jjY`r#EBHOeKiF~f -5fIhAzksYLQwSXI{v)7G}NbF7^Wvb+=Q}PRNpHsQG0PoXR&MUw@PUVsUwC5@l3xs5N401)3tf&($KIA -)XsPr+~dbM=vW8DA4)_t=YI}<9}Bn3N_;mghEK^MDP$9d>pl5DnxVR=dN)p%nU{yDmpe -YcKy%N{#&nQop(qnM2>;v3rEe+nSN(#J>fdnc~!|T$}WO6>8*AmC^RV}FU;3(5(tBB7s7-Q{t}bRPv#Wc%jgVuCeDV -5IdyXN(K}fxJE9)3uW)$$sAP%`1gRG*NC>9KHc_eIonbBXS1%;d*FxUMgMGyDYa{rn4HLpas0Pp?qJ1 -$c6?_uyPdm3w=FEkZO_0PG`$&7g}W)*OOk7SAX%!|>ef=xgl2+Qi%e#BkaX%U#FY(eTGbw}DYjh%^?B -2JusW>PQ@z9ja%=iI835OzGb|D{I0yX -u=wt(W}u+6FeHoB;NM%^#N1*5iKdb69&B=$i1)trEKM*0{nb}GOhqWo`BQA4Uh8+@S1+6=9+{+mZ#cV -tc_TW=j>w?ljf;@uvjx8yWulgta=+avbvgpCYRQQmNtvQzhVSwVw5&MQh*dDR%JqD0plhMT@f+(xFNR -3h$pDLuJ0bPO6p0|BbUOjSq#oa)8*QtVh{OV2Nl-_L3jqI;_Z>%X1xz2{6XlIftUtd+;9O`SvJLyQ*c -4tf9!X06gTs;?)3IO3~yo;o@WP2QAQ(;++De}`S3@d3V}>d*xiTSj_yI#vBuk6&*&zSVxA?>pwMaT@VX8z`%*LEIK{U}(>=@Y -D~?Trihaw@qH~W0e&v#(GO1U;jZK3#NX}>^05CXjS%g~aI#Mj2!FleyrL6%6WAdZUJ#qXNizCKt -toFl@k-ihJ#rJ{ZUY*8u^Q;X!lk*#_3L-gwWp$RSf2X^LLj%M3ILSMVhuz5l5m70$Q~z|CY*6r`5m-F --U0YRXQRHS{ly=?%CvG9$Dq4!Ds@*{LpZE*3yd?2asBx1%AsJ&P%y;;n!a@vN`ILpHGcHg_=TmVSwsG -q*#+6_7axmNGnqEtiOZYli5st>ZK9V~PHi#8l51ClP7H&Amg)&@)riqrcMig!RmQ7e7yuCy@xdJJCy8 -{UBZd&KZs1$v+Q)F>CXI(ypKB-+B(c1T+#KyI&9mU9yfDrZzCOx+O4*-d&&A*!v*@9mk!>=vy(Z`x~} -t<|Eta4PT)A1QMDzzhqe8Iyt*rtQm7{sd_>xOM~Cr7nw@c28URaQmXc_s2&zo=aAK=Bw63AxJyCHbCt -TZV -Sw#exas&%^LRv8E-vnM7ETK)-Z$U(Bf={Q>Q{bq)N2W@SjJD=CnPH#W<>oWOoa5o^-2BXL`* -7d-&K^ph5tNj*}RVDgh~K(WMaDJq#^>TdTNx5*{V{_)# -3KJ&@D+Nr&E*XWatc?@tTEB4=+CpZp6qj9SX>?uw)+SD{;V`f^hmV4@nm-nt@5idae@LZwNiAp>HnD` -HYId6bF8n9#j85;X^I;x9(fyCGd>iOj{e6TNQ{^%mer%)b`uU=<+Bk6~mf{}*rH0RLNuAe2wR0{vuGn -gxn<$RVl2k8Zcblh%o`Jhz-z?slkbY!pvW4lHklTzMQl`^rA7bs(3qo6->#yK!iQx+J}?#)1BZ!^Ph`OO_+#}UetQtVof -tY`rpv64`GT9lH{-`D)GYLPQ3!ZO2y9r{}CkswMFmr7*Yqc -%miV}fTqR6_A|mcr-cW$Zufh!)@8(6hahRZd71I%#o~DTHpJWKF;vfErm{%GS-!=U6Isemtn^VioH>G ->A&w{y%0!Jf{%$o-2+WZQ;jM2`hO7B_L&LJy+38~}fu6u~bcEPXhnPD6;z169g;MW;_uD+A -gkc?L#ueaJU$9A1W~Pmu=Y?5)(m<7OCm@aStSh{e$T{JoeRE;Rq=UA8-=j8a!48n9hQ+^j7KKvyGh`0 -?Vk9MR?gg6B``REEezWH)?bU(ED(2&XOsbc1Ymqh&zKB9_wR!23YfG8cl=Tm+)GzQ(wI;?6W2DGBpW_ -LISWSDP{kk1~)A^s-ZEdTvw3`XKPmDU`mi!S6wY&F{aDw!T)yOs`>@cEf=C0AlB;XGfU5EPmSKvKr9n -?6O*0hjUpOM6YD&FJMs1<|nfs`xrN^!|WWsOl)s`faksWF?SBY_AWq=wS=cT==@g)9C$zXzK=r=-!?` -j(X|FfRU18cRW$ut<)(bLGRgV|+gPZ;A63?ILm2tp<;{s?Ou8Uo>3GZk0&g^9&~-NQTRMXalVZNB{Ch$US}C*pquc_3ky|p@P5xo;LcG~u=5`NunRr8P -3)%_k?NiSI9>mYzot{HtHzCF{O_++LK6{~`L(17-_`@$_OXTnkS^n%ssmo|kQY%ue%ROm(wLuU}Qmhr -J4r#f^vbVYtk)@?HR`M%9L1IuFKuFyfXx6U7Gm|%D0UrJ;h20LdXXGLa6ce7}&@vCdFyR+ahtU@B>n -WaI&HOa}$`4Z^hFn?4{sn+jaPwt!r=A`dj`IzfK>S(+Gkxx_SciGTzcAC&EH5#C%tA1pY+Zq(cnuS32 -h5o({+c*4pYRuA^VN-aaNu2CZS*91d``$(hpO0#zQBYg^PqKnzfrfXyhji!mH_c6GY87~npZt>DlnwU -ysse+`3OL2+tm{%1H;gf&e3FUj-?T|N&AQwl7UG%>fcx)Y%06CUyMCw)#V>7GmrdRY__n^NF1d;on4R -7gXg$$djQW7MTaM`BiZXez%Mk&MDvmL(#Kd~ejaz>lZ=~u#)EjePwh3AMJsiB3eWBt+4=7%d+%n(jIF -w@>25y%_A@4o_q9Oei?PV{Eb@jic$pe<7Fvm>$R|vpJ2O1__kzOo6w}jTeqE3^$9aKrXMUqbMViTH-0sWX6!-}mmXv1?d?tsdp0^s8nF@==XI#h9$PK2-=<8^my%2;pWwaX2R -7AZ+F`@rxtqDj}8(KRA9~reK3tmlAI~YQtsTrP7sYNq}?u6C*2`g~A)wrwxn`3sH{H&|%p~J!2K7Io1 -^D{fPvKfhf)`Ni9tb2jy`y$_(K0%Z3jbYElRv&4W^6bjxdAAvqOT<-!L5(rES{IFk3I2;LX}M7{ZkH? -uj4d*5aq&h61!o=EO4pvmjmi#Jp^0BHu-bt~)gA1DsCM))0D8HHtv=85dD<-1GLV;#0?oMavw~z3w -Xp3@%C2-|z`sFYe|SR8n<&tw!lKKge6CG6|No5VG5^9Zx-oV=#STk~!Q=4gGB>=3*BY>kgx9h=SGLge -Kg4TP3QZyN9d&qTuq9-^g}r2i%s1i74u9DE_7Z3iP`Y~DywoO_Y!GWa)2*das7kIbGyqG~z9l;_z4AD -c#?qkxvfMLr%#`XXL)|0>@C_P&Ds`OYONYxPTbEaj#5EHkTJdvoP17>Mw&Q0K+G6(v{&g<@))H>bM7R8@U#&jve5%~NQ+@;Q@ -C1!{#d52x*HyJCcdbkT{i#;(WB;D;!P?LMRf1q$gclLkS^UNga(A0AcbjkgXA0=FGkPH3O;ugWG&yp> -3h(*wJ}LHoKD>|MuW40Xfz`l=3?Rh7Zy)``4Nxrsk1F_AF$n7zk?PG>m%>8#Ti1!)97CB^I`w1 -5E!tJs2X*_*#BK;pMeQ`YfLY|(NNdFB>FOH|D+<^*VJASYA6E4_~1MnjX1J`XiakxGB5;7M9kcJ=2xb -YKyxr=$~C+u?1X`k>t^?UrTJ<42`#mVw!Uit~^uWlQ5d+h6WUCLy3Tc-X6J}zdqNr?%vD0<~Fi=h<0= -_lj~`gU{&>GC+;hCs5o=8#PTI?jxIjJ0i5uF!33ZxdoyNwg+Dib^t9#sG%uwwGf77Isk=|2)&9A0raf -Sc=s{HW#tO2@Hh0HlmXT@I~?W%=$GWu@`wq?4BuaZkwNZ&?$pZPe?@zKvt=C({H}@zr%JmnJ8Z -B$sY`I4jRlrK0bmc$dgOGp~2c_Uv@JH+d}D-kjVf`KoXkQ%m@@kAS(jVU%eAQO0Y7wPJ1Vzr{J;XRy? -*aNajCn0S5p~!j_G|mJPu~b7^R#MzM`!;EiStXyO}h_czf`cKS)ZPwmwMV~VoBghnNm7KThi(Nc(wnu -PjlRGa9}-c|^U$|&H1MieSB4J{su8BJFE*eMD~8;z#h;6r#%HWry)8k%!cAIO_Cv=!KjKfWF7QZ%45N -34igW0hI|+X6qz*%7?QMQ`r+FaCL)``_;GQ2Sf`9s0ju)*5eyRu714ihZkfu|w@mdLmQpH3o(gb1N)2 -+WExX49`$zJ%)zT9d9Mr;Jt6f59`G5{_L7!cl$^?h;r&o4F$H%m`~GJ>@9}eIz#UST+wN -wU8AItFGTuihV`fT*PhHHKZy2mytBx0-QCCRIiv(8l=lT{ph~0v|9%>E@QC$}Y2J@5;uA@KV#gJXgw& -5-EMm%|SmQSAJw6oOqfgTItx1BkCOfhWb_ZnUHTwF11=BghE=IonCu=LKC!$lQ7m%~QoyRjF=Bl_J!HgD?-&N{d)E756Ghvlkb+k`%TrTlI{EKd*>AkHzA5$DS|6o>j?w8`p_Vf -flGe_lmzQXZMUel!RNAl310;?zAZst8mR#d%Rr3QG2F|Raxw1rlP1l#bVVLEOa}_)IE3e-DS0>M67xn -*-j2$8aDX&p-d=m4nKP`j3L0t@;m&B4is7*xYFl -65+K4UcE%l%KZsXf2s=b_b}hxmo$YR|*m$g1`%R7%vIaSPh~?WS7AsACTAwY`j&69-U*l -snO${kpoi2(Q1$LChbOvxp8Bf+ncUwE+4rLQAz(jB!)1t>d*M;#IE%Y1s2b%wWQG1aSTTB%gl%%{y`d -;SbpXaE!_;o8OMdGK^dbANRRPeB}xa6V|csF^;v>q1Dgmvvvi?6#KyJN -vp(^#oxMlY!g>W`+dt6jBpN&T){a-ERG8&3d3y%(>C4Znm9{*& -=rz@sqmnlpM7u|A{1HBUu?@wG%*@T`MpaIblJ5a6hzArOxU(!yxH4A1%I3IqfFVp--++G9ZiEB^F -C0zhqtJ_*{L~*vL!b7&QJ2F8`TkI!|xZw@29?%+5cm#-Ay;UNoD!)g4Ryi-XE+5Jc42NPHpGVQJnw2x -Vf(d-A;iG~&OaBW?pFc3Y!gXoD!o~uPo-nJgKL!$pf)2lJ?2{<@vz&9p;O)$h<^N&tOW>lc*8k72$jI -V=O5#55D5)rDC@zf5*g%lDrIiNAA`}9_jAFTxNDhPo41upYls%w5SKjv=X -=h1hXKK?U-!3s{`VbTp0l6ldCqh8^FD{lZwN|DU+4po8%V`ew@+sage6SV$(v9UM6Mn-5yV*Iq)AzVS -xm~Hl-3-UPKkznO+ozaZN(Ab1o7?I%SPwdXT$-nb?Rz5abeJYO$SQ1dLglTTL}I4Y8*A(`671z7xcCY -W=yfoF5`>Rtd5_}VsHEYN--5l+#okH_)%$RMhtE*QrJpFwS&wBGBns&R#(&m2-`Y0q%5gDTjydlB_o8zfR6+I?hujDZ4pIg(YKkQN=x=z)T5 -rs;hZ`;bZTY8a1sL}RO*6!sMR4ZnWLtPo-%QjTB*Z_Z50?zh6EkNRMb6a=U-i=QoRJHpo%XHup3RX1x0m7kAXueCdWkxfw{VXEEWDc(P>v -KupUF0>RUvBbHK`6JyA=hvq?6b7{E~@Tpo2%aLwz?$r=N0#;Wiy7O9vi6|z=`$cRRpiT?MZs#km=%+{ -0s545$T|KWzRI^!c` -8A^9hbGtrgynW94qw1#eTy*WK}fDZX_`O1zCvH!ZgrqM$uJCB~yJ=GeuU;)@>Q8y=m#HI37S4xookb! -LO!XbYf*@hzdr?p0g?`IhqyAGyN*rr7)O9`O>sJoT!g;6Bi5^C}ijFRT~qP8B4#R>!hRxl3wz3Hs#Q1 -Cuz1zrnC(4-f7fFB4k`4LBd!TE~>u8A`FM#DnYO7$Xi$k8^y2Nt1;}{-uc?IO1DpC@aMlpI<~}@mXKP -no3NpD1(>&E-GLk#W@OhV6;-+-HR*+LeD -7T@V_75f#@8ZSqw|5ML1^4iW}+^3d^)2r2GRlLs{sKwiHhj@8j{Usxp%@W&9Ji}D{12i_nl1E~GT6+fNA)($OekNgEH|eyvPKxTyGc-Xrcws=}1PGy$V`(~DdP -Q#kLLT3p{T>|v*6L1SYQe0f?>^2Cu-coS;?2-c>I7r$;Mt-g=v}DkU(Pjh3SCVCrNB+&FGy_eO5@;rxtv2T>Y(wa;=e)ocDtK4qFI5N%BQ -(etwkbI4vX&pQ{NO>`Fax#pD39=#Ztl-$YU2sz%XIP3Jz4~Y32g<-uc2XnCiY;XXY>BL)Cc_ple5U7t0=646%daxwoS`^xx -J$g5<*kHu$=Qpba8Jt=dS3yBCX1vn@nXvq4$xgnA#Say4$>*QuvKjmckGD$d0YXkQ8c(QMgAmhI_(G{ -UbRbQU2<*%+w-W$Q*jOJE}@3$p|IT%Cyv*f9S1dxW#!&w1{dnZY!3CmPBKGQ#k3> -ViZuZUZE+kR2A$d<864|^gP%f%4tu+8f+CJ3FJ~ytn;2=aUaMe24y^&~>==tk45JPJEMp2&H1BHrOaou!wbO9{ilVU#;}j3u+MmQO -$bl7uHY;)kQ*azbxhMf(Wl&qGyP)`I(9rvQCE5p%g^y@f~2|{?}0tiXOFkG`WY7JYdbP0^J~nzn$-t! -{23{#qby1W}}S1!)92i){%__kMi -G!hj$eHsG3a(_#DH01g%_nv~?9ml_fQbXtLgG$pYi9tZgbs>7(vK)wIV6u2wD`Ji0AbKsK4mMV3%Mj} -8T{;3v&R(cX|vGIiTU19=X0CIh4 -h-H$hDiXC+pG_ndsT&;~LKd$0w%cu~EGjkMfYKX^5D+S$<<8l1z0^a!TVQEtgMHXDHpyWRjg#l0#r=q -mt|mi>`5!KfOl6;e_6v+rFC~=Xv$Vbr$q5ZzCte+%@V~e@zn@8g4)tWW2_Z9 -y?gTs`l73>4g>Vv>8uVP!)Cou+Uir&#;_xiLDzi3vruS)`(6Htvr+q^DfWT`hAsPRVytNsu(EZre1e| -Jyf3AbSw5sD*qU~&9y-~CCeI?8+4^Lb(^%cd1Cjio+d;Hz7*+5g$DrXgwP1=K^XcMc -wQ90ojXAV4OiW`9-``ytLd75N##XZ`d)K}^Zo&O_gqj%?bLdOSX7+Sv?P8el!*Lv2XU*nRZos;j^PGs=az -c=?_TIama#`*70grC$Y7J^~(N?f~yp?DA-T#_LL_q6rE?#M7twbOH0+v%C8Q#(D|wVj -@qGbeGs^m~JS&(?jm|Oc1+0k7~O;n7o-u0L(iFq8S)5rFV{@Y$mps>h{LaK;K(zDV)+9O6t7udF -6uMGh9o!)xqMxW2?B5{rZG*$Pbml2yLq8UFA$YFPCeD{+Ru8&SZ|}3*N8(=3?VasT2g2CZRkZ^UM@MiI%f1l4kko*mg88o6tRex -<}KO#i5{5=$lJ;Oe|`WS)aH=RGz4{Ai|mL?CvpDm}_LcCGeUMd?<*K+|JwOX^#>B_+3`&=~9hX_ks?q -owrJ`~{gMl@p=4y!Q-jPl3I+)ZC`+Po0`SQG_=8eN~eX=NRKEacAp##Zg0PF|h>K&RZ&OGNL@ZGihAn -QSL*tq#UtfwaQbnQf^p@1Wo@8Z@hO^Wow-%rMYGVn^>osZeq1Im7F?5Go}d_ac5w@+`~HB@*bA4**z>zleUTVPJ(F4E6R?_T`a5E#i9 -~1mwiR@e?e%YYi*7SGn9IwJ?C$c{4r#ARm3QFTc$jd%829 -q*Wh!rfyuzf@aUoh@pg>f)W4uvbQkeX3PrpGwpKL4mfJbzB5`#66H`mtvx##g5ju+KyJyNg+&Vm%L?L -h*q&07yVIb>THIsVXErSs6HnhJ#6j7=SIS@qUdw6t<@SkS;E;Y)!o^87g@s2n8VIc3~e*ZaZv0@z3+T -hZf0$DZD!4I+syjpjL1fB>rDT>?7qOQ;V8zameMM^WA(oy;qxC)LkQ(j8HucTU$s&+G}um*}jtcI9pOle -U>eDTaIYCY$4)C?~?j?hO!_J*#P-f```*R(+Iurv$sL-4-fz9@5PY&aA5UmEZ_KjS$$Hc8m&xINz`!r -RRoGWB1IH0;aW9d4E4pmTjk@dQux7ig_-ub`p392X2`VO7h4oA6lB`3kYm}B -rI*GKQ6lWF8;-n7>PA=)vP*dA%cJ}^N6hie8d<$qQ`E7umMna;o6a2GlVTlI>`Pgz-x8=Iy{k>Kadb;hZBgOK2yxKs%t^5$1+~hqohnLK+o>vMay(;--eVh6Qs2uqbX -%A#uwL5+N<(`g-qE{cAx)!1^)(Zbbj^7Bud9gx|9z-=E;;N`6Jai&x(rz6xbISZAt{rCia>}G$q+uv& -cZNJB#dFA7!{V)gKIR`Yel0rANy!?)KoFTOkAgJite^LDlojo)YCg#t_p>Fi&Y_UpSs(skn>QfYs7j( -Y4ami$Nrj^TKFEx@gYr&XGaIzd_QA_$=0;9){23XapcQ!2HGHZUG#=VNSgBe3I#hp+#X#rx@{hAA?t( -B*!UrvA3tR8DDG6N@!$3vSLDO=YwUqcv#pwQR5w2-w?Gx@`4XXN#mBKPPLycB9_In9h7yjKiAdYAARLxlsw;j}aHjxM!WND#N{h>x1$9bUDX`s$ -O05?qE>7pvD9%An7nQbnDZOLED{AIR;tJ)Fe2*pknljqmtg`9Gl=-N&C -A9j~Kv&#!q=o%YieKGbsFwc9PzJs7{6IBs8kIWC;}1F?eHDWyT(kv^kJdDNW0AF5T=smd#QR1v8RH!O -$MDIrV9c(Rx6rnPTLia%A0%IPA7n)i2o?TsN44aSrb3#Gjx&f!(L->lc`97mlu|Aulhm}!X -lH3Dz*1B3c373(`q7P!Qcnp7Crj80pzx*h2Jbg^qy`!TVqZCD09q|u`vTSZlCA(R5rP(DS?aSzQxQf^(c&YTgFN`It2Jf4>j!AcdTCx6>|C-~D8;|Yg@at{?)IBdPv6dIa(Ot9EN -V{AZR2l?V{qxyaj~vsa-20eYMt%xlWR+%SX(lcF7WJX?_O31rOh#B~PN|!h>74TxH5}^pT;zUA{ZL}qLDO;;yUa@$$16+KneOkfQQu}+ -kaNr;?L_xJYCPX;?yjZ})pbq2SP1`OHZ{MuBDRln63jEKCFiQ<#nK4c$Dv(7DOIuxjfDv`kPiNCfc71 -SJ#U2f*EHLfYEF-fR>2l#EjeE`_n&atNiNR6Eo``zvZ-!zXT?G>b>P8ULxm2SVPr4&ta;CA(>$J#9$k -b76d}Iey#5rWE1M|%CW1pZao#6TwfD!Ucpo8&u5(L2D)p)JXA1p<@8LVQk0p_Y -Qrr*+{i^k*)5I`QYvf!RxBp&Qa{EX1~TMZy&EdeW -KU01JQEKwf)j#Ed!;~oBQjazQkMU6LXo}trwRGOB2iJ_s{u;ZBN__-zLgogC#1j*F~%??nu6$jqU7~B|70&hAo!Bn$ZYh -b(CPbVA$-XuesWBU3+VV#VA+tg<_v?BsjYC!Lmi!|d}2xp))uZ7ubmh0ksXML(LLq)dpTWQ8gac7k@6tpvou4Y -hBGR)K^ex?sSR2zjM9uR51j3#8C|4PpXySE;k=OIQi9>Em1dlhaGsK89J_a(l;&cX4@)yvM4gq=yb9( -`(!5rDgDlNiFh45I8(>}~%^P7Zm1gu{&LU~Xfn8^gG~>XlGew$@!8}Ep*THO+W~>J|M@e%P%;D1Pgt@ -mg*TNhk&Gj(*OEVT4olKhbF#nP)`3$pDnnPgzSenHLz{jLH6z09sjN7A~TctS)<`<TbZIVvIYF9>VUCgJWiUrd^D3A_rFkvPU8H#(%z@Ip0cJ00- -UxI3EwU_N{`MB}S)usO3nQP*hPvL`8XYjAklKE4=YA0bH^~MJmM)v5OCY)QkuFb3m+9nkvvgS{T~f$p -qI6m0%1yK_kbbweC4>T26^MXSWmF@%yf0m1rAsKe9F{Iaq)Rxt?3OOwrArjKY?Uqn(j|smUXm{7XUYb -MT-HmMYUz?iF0V+J52Q;zxonUw`=m<|xjZgiUY+S`fXH#RbbNfKt0f}GQt4Pe)72!A<2>nTo7uqkn5k -}8XC+~mKgL=kTD}n9_in!hOCS5J)R#|XG&~t#cDxhk*k$5x#zaB88?Us`_^ -}ojCgPObZhkO&PsMcI0uRSYvm>L(amG@1%I;%z=**53uQQK{E??Xx*ZG9WaTqs>lzgF=_p9yZ!E3u=# -^_XsQ0u6`;M+iXh?qXfY0H>;uA*a3@}M39rr!JKKTT_*?~ZWSthX)oh%_}mO8q{^>639%8mO3v3q7`J#6Gf|7Ze -n#Nv3HhR8y<;;HUfB(K#DNt(6-J4;!;?HWg7KI9OKv(Y~q?(q_fw3aON&aU_-e61`FaXs1{w_ReMRk> -)kcGr8IAbdK_TZRY5MIW3uOe5OY>SC0j{g!$~J4UCNeB-LR1D+cFH;W-2=x?LZzb6oLEo|cDx?@vra~ -(YJ?T&ML6lavfl8;?mp$zWqf>#QWKz#3uFm$h?P8e0F^Gy94fA>T`~fB+P$40-9(5~O -lWX;lN_)Gkf^styzN{zRScJCd`-gXAmOaY2-H^J-0t>O^P7CA=@xUkq&NqTpF3m9D7DhwTZPm;+Pjj; -vw2BoiWh@J=H!XaSWL^wyg{s7ixuurvaS>H4mjQyMc9?p+*+#DF^C=7{ne3{& -|j>Vvkg*T~VH{7h%G3Kym;S_dtBJN~5S5{|lMb*WT?{#KoKCz!_Naqtm0;!^)j@UY*j#Q~S0tJzaX1f -Z1aYA&}yy;Xgdb{eykd{Owy3Qpc;VAZ(UE*=*MwfUP+{8nu{ilG-5*>Sm#{WdmO6UDp^b{YGi#p>tA~ -fPFi89d(59frOpL2>;$9B-Qz&DhC?3a(?DAL<`Z$js -K~zr5#a#N3&9kI6kCXQZ&JA|1$ZeSFhvbTtjC8AGYK)jGlq{7ej7*p;7|L)InuhC9+c*-MVxJ#93 -ig1_{jETxo6E|Jovs4Z13D*DC;cTFQMI!_~jfqN3DSn0sC_#@rKg%7aL=?Mkx3EHcB3ebq>_Dvx@1P@ -NfjoxDEU{d(rOid`O&q35b_2oMw)g+12!s(bZXEY*?v-?A09w8UHeDF^dKDs7CAQA=<*4+3~s6@ut=B -p>q^X3mua~sN1O9z0}^(Y0Gh~a$Of66(?D@0jb={T+Bk2NHX1sewEGb!E$@rTrBJSN`aRCi#EM(gq`q -NC7;aAsyDgA+SStb|l8I20-e~K9Bx3_JajAJ1q52-Ir$i}hafsjQoFuZ3{>@Of1 -4lXxTlTsZXs9F?$twW>kgOMFH9)|pGb;D*d4=jsjf-l9$+-qD_ -h$-C`4-MD_` -Wwi{^q`qIPY*gtVHMT-uJ5?k9aI7~U!U^ISvm8=>-X0q;-b*t@%Wu{0a0|FSQ#Nck8F}`itrZWR(2i* -eg@ty{a#{A-Ah3MS{-~YVlFz>UJ0`oql;{VEaLh%vKZcbGTvd~BFd^`n;?Wpom_wDj2`6o -Id>YBu=ZFqhflbBZ1M?rj&Ckqh=$|1L+{+Qx>?_V%4uk(e`?Fvr8q52VK#llA?JmxX5 -va6dY`*_HKtYSrQFqAROdYjjbgcD8*yk!ls99QhBdn!yMz+zMp41QVW*lSB`wj24IoEwxA|y<+P~>sl -H3HhM(ByP!=BBd=SYlk9{;jAl0}^FXE#$eB@4ruErJFt`6UY@8FZ@qs_m$R3hxzjT{_jVDa(M+3+6n)MPpep6cZ8Zo=YauR!C9o_udf1dx_kI|qLaFBvA3f`n(mV$RExKhFO3RWukv4TG --s1KB|w}K-Tyk5au6wFnySiw~aZcuQ$g5N9X5hP(-1-mIYK*3Q8Uaw$^f;I(LDfp6tyA^z2!CD2q+Ny -jM9H8Jh1#eaG4h8R3@F@j1D!4C-QCSUOcdQ|K^?tJ`I`*h+*<9dW&>AR`j*>i_I&r6lL3z3i8r23JV#V!wOj%%VPyBgMNcp -I?G`=S{ELv6ZsueFpYi`oK0m@*myROMX9_r`w%vqB`}K%hpFs(7RO@Q1ZEbl1xQp;RWY$@Fs%qxmv&kE4{YZNZo1jAAD;lL*yZm`Q|ZIE$bkTn17Y6JceekaJi*K_k6+^xs -a@0?$N|V*;7LrF7wzDbk!H!e$Xgg=9*l|AiuzS;W8j< -$$!kKBEU8(R7ZmrnoFgnx$0(@445Mf&hJQoea2SNP@AFUJ)_rZ|Xv;slj4)j8x^DC`J%8>K682VKCfK -;&d6SA@wU%8equ91&hpmMU`A_%u+^23}@Um<+<%$U@m*a?PRC`U~zs64G2ax$`r%Sx)5&93eqzqI3!b -E(MKZngvdY6gOMY*|fZey2?A3V%UUi(X- -1aN~O{r87m9Pl3xvayn0N4|Vn5As`a}=bvz>lHSUTEpqMG4PRT)iAOzzg8%o~D-GHGi -@^=Lt$LU+2{K$fmr~MleGPXjzbnTVA*76=Oy>x@ -tyz4--OBIE(@&-Uvnhm@a$z$is-@r1-e-z3lC@3tSovhn7AY7-@0KrI3WQz5M#f3Mghw6<27W8YV|r! -ZSR?JNl|4z>T``qCP+8Y~Bke)T9;57SmA$rF+S@7nDrFB*cB8U)RQ6*|Y44=$DazhiOYduG@1o_e?A? -@oy0UlI^7~5Kjaq)n-b2~LPf2@`#t&tmujTipwBMoOr|cJ${h+dQW#6FeiK6Zoh;a$V8z_q#L?5TMda -*2wenu+k#v{rEGR>lxkP?Z7uUokd5QCTDP*&^f|HQtaZAidj!1k9uVV??~BxmaAayoX3XsZnYpU -HtmbBOl`>zfjljQol=oR;n?V`;|8HYaC*U+fR3eKlk`c-F_Zd_|xJV|EJmizu>vFOtb#GB|z8kyV9qT -1n3%m9#{BlkbwW{@Av4(uRZ+WUc%N;8!R^GE}^}YA -4x&MI&*FN;{Bac4z_!H}%eCp}-&pi9whUfqG!i#@@=^q;iAR=)AavO=l@^kziC{o -*)slGYh3(w35nNFNSZil@|3AJOq+h=O_KjD`~R=tKlXe4-0DMDvU|DNA82B)AiLhpzOsq^o+kEHP3)_ -i*zav(zpsgXlSgCwYfbFfX2ck`l3sr|cPsry(g+A+phB@yD4%royP9E*NdAm2m-*_)g!^FOK3KWX7@F -Mhm=wfg7PB(B&W0J6_FPY+u({2+Pf%+{n(JhO`D#7JJ)}v@JnUl5jwfEs_vX9OiC6PN<_e!LR`+t0Jy -DE1)2MHg&v*(uORQNnj7McUSjA~@mGo%%Yxrt-YIte*Xn4r{=DYCF^4Id!@a_0yU!E~?e3N+=6nqyCJQ{>?Wuw;nXi_gmcEvrmW~#mhza~s(wq}lRdot%+x(>zS}+4e$0Hr}2i(u=p}ih?) -g7i6Oh5>pG(T;-fGtH7RG;9^*OYOb(SzQ_ydBWti3hcNIL@vhC#_5h}3=ceb)n`}$9Ws^U-Pc6(Sm@w7Mp9nBJ>TUn-DGksky1{boiQ%2NXDE>C2FDB6jovfk<=&94^OD54 -OornFYUSZoSgnH@nfypn(**xNj@~(G>Ya*pOTeA4^2?> -i|El)2dgi0GX|0Nj?cHdX8$i8p>%(_MD77yUn -EJI@Ok%W3&_$vyxpErFh4tYwlPy6a*2OohRx>Y%?fSlL -`J$jKPNj)hR}TEL;gg3%K^wgqoAN={{}kJh!9(bF<;g$qrDJl^y^dDuL<8~XV{2s**0TZUV4TxFH?&l -(;Fn#ARGD{OHn>mNEuUdl;X{qlS(GyDzYH*`VFFT=m%3_R>rJUa?HujRgAtQos5Dx*}18LNklp!Kk#; -Y9wAOVbSuuwCPvQ`w`64edD*rsu(HsWTHxkMXM^LH4G(*3Jzm!k+v@YRSdWK-Jdl*$LQ>Tm{7}Y -#~`}OK)OjT5%beiOFY4{%fh{l|pym_L$dtFitEtF%+JZYia3ufErWaQckjiDKHspe&88Z+kP+vfNG{r -qNS7&ReHB}CI8rkW55rp(Ehlc&0}o~#e`?mcOY)<>)!H?}i2X|j3p-yS^lRo;#*AGwNv9J3vaEJ8I+$si(T0x)==B& -S-W(9)-KYIwVPx(7EtMzzjlCJtM%x_SRZ;#2YCI3l6W--G$M4GNQSkHoO^rri;VB$& -e(4fkoOWj=MQgaPs{i&HS9#OpU(lM`b4Lo_C{Qe##&4582d|8L|HmpNr0P8TRU2RZRgG?ffX(VGu=^d -le3vw$PO>VCmMfuS?LxmXzGrbG6IPJVh-hEiRp$1o+H%2p7J-Q*zc`^?j;}!?<=Pr9ggR9yd3#!!c8V -U80F^oOf0-f_{Z16XFHy)9&<3K;wIejAQJSB;Bj-0?c>*AR#Raq|hU{NRXB$=nO={UtD^OZWJjXKjoP -rPc)I!qx*^mG{7E?=wHy3W3=vn`PFFi`q!Sm%31>Pfl~Ulv03J!Fa>3yCE2B$=zOOKaBUso>gnRYAuL -l>zzwDSqL;BqKpchr)NJ_M!984z-eMgR4AN+`!lldQT|2bzS^e7h4F)YX_3ot5_FZTPEXBT96Y;hvY+ -*s&9&`&fUz|&YRuRLA@WU>b=dMctCt0e2M;AjqfP8o--NCq_;-JtDeEwVzPHuvL6)GiUm=;prMWP`|K -8)k0&!WT!qth^=Dm)*IkL%U5VFSiPv3+cC6}fjQB0;nIF|NUs2Cwd!QFGC(DS+hT4&iQ+x==R*;R#mg -;6EP2fJvZm65O-oC7NWJlKfr%tsYRaYHrU)eT4C?zmS%C#@^EokM6tD^~4Z+dUl;)=WjC~xA=6vdwc! -dLPxT(s}Xtfle6os4})@2gtdFt|1AW_yiwOW(x0MZU_q={B-J)i&gNYxT>=RlktNn~NChv6$LgEsgGa -78n`G0t*`2ZN!h#>dj@^?eG%DJ}zlUOOxM@-H31Z`c<_)=2PjN@0H@2-!a^eb&pJE-E}vy#yyCn<)Fvq{Tew|n-{WqLN3p0V@g(|hl6nlnA&mR=7%Ydg -ij+EQI?TOeBC6r>Zb)z!UJPi<6B(*vlk`J=wdzOR7FQPf#_NiIW(e?+gSvy$d#iRMR}rTLhb*0$42yl -PLpLO&2*WBu)s6!2SwopfFMIH6_VBV2Brfcg(JPu%O@AXR(?POa@r;P_|Gt@V{^V_22s9&%y7alpW+ScuG-QLRDSIu+7vX67w(E@pm=SV;T6I+nEFC -WBlwjz`2i{ZNxIfL)*9`0TgZYh;Yo0)lm%Ur_Cl)%O`)J^sg{>&lL`pOpuw8T-O~d}h77EG!KHdDF7fGh(w+3)uaNB3C#X(cXr%C)?7k#2>6d={?kXr&tJMEWtyqo0`(o3j{ -V}JQMO#(-oH}g=p~8jZB$R0sEGj)zVRhN_-NF&B-g&Y}Z*PU2jRyR^}|a$+iOei<<`J6$#T!NpZrXTL -bMf9{In8wPr9VFFV&pIep+^%}C8h+0l$um`#}MvwTTg0hP>@`T2;sqKPz^*sU^5YJtt34^~cNN4&HZo -`w?m!fR@7mdHNcQk0gF4{lRYAanLA=x@>6!!xeXoH5Hjdv-=a5-|`W(AR5{E$4=8$}%-)YVN%3+;oy! -xFzJ}-Db}pZ_iCb3YJ_OmL@{r6PucEvll>`Y#Bv1)(37fs-=ki#4O-LY(ch-*qt*uLqw;N@l41_ottr -qdyPk2E;*&$cYckEzP?|Sn6ch^Edm@aKdB*m5PNFnLWV -Qf#OL>x{vUA8Co{$t~gFkE<>8#_Wb&zC#wrDXdxpT7%@^Z08C8doT3!}GI?WW|p>LUAG)bD13v*%@PP -Rb|{2QF=eh*8Btj^`KJGUi}e+f*oMArq_gnw*o7kj&oG}KeOt2)v>`#(QWtN>T+M?9i8SG=pbCMdy*2$VAYdwX2#m!D^q7ZAqL${V?CY7YUFbmm_vu9> -fAyFGIQk;|_3pf=qEV=0uGG!@E^k~?#K{&<+dnUvt5()>aH#(IP$bR=hKDyrx)i}Jz?}qx%^PlHG&*dJ}IjBm}>XWB}I3t6z(74VD-wNZN44 -g~B$N4yyqF1oBg1*Yn&&}^D6&KK;(hXE@IG5d4y>RcL+=nTDETJN-Wz|Nqwqil*G{Yt;9xe--?{{`}tgTaPA>r+T#b>wb`Yc{e*goFa5OXzw~Hhm)Iu -m;pb((S1TB*U~dJD3U*O2M8QA>^$IctYtKnorQo{?9#n9zf|Uxsq2N{pHz~ML!50 -9ArC_mwHU)EdnTNT`>;06WPD! -5F+A_Y?vv?>^-pi#jP1p^h-E65bAuUB|0=v1&u!IKIeQ}CdIl?rZDaIJ#N6fCN5oPL&q(-pKT7_OjEL -A{3083|7*__2Zq6=(UsSpNDq+6LFZ}Au=lj<_fh|7L-`L{w ---hD9^=bL^pY(6}^x*$2-hbuu|GNw{dH#=!#JYiMea6E}Oh?w!$R}m#{G~mV|FfS-_~bJZE_tF#d{P> --s93-a6n@v!A3kktA^jy6C5%zXDc7fbO3EoHV8)RNXa4Wqr{yG8w7EEIpa(|KFu#PkiBKIhQ< -2uSPDFheeBWn*GD<(Lz6JuUvP9gXzy(fKi>|KHZouxUDU|(g9Ay}f!r3ANh#+fL}m*9`|hI;DQF9e_N -BF*awMnD%fg~=g!Z#Nlt4Z-8|E+F@J2?lqU?jZz^beHsgOR$ZRu~G^XNbuU8jIAKEmEaHbzDVX;f&sm -Ves2mxaBeS|W)Z5)8#l=5T^T=)IcEF -$7J+aZZ8KBzTbCNHPOXxJL3Vi{SN9G>#?rB!X+AB;NqH&}$?2tpwkTk~ru`5YMBrU!Y@C2;Lp7WPsqd -Xc^`Wg13*Nxef -6g)--x7<-xC85FmQ;Ak_+5}9KNmY5Yk34W-|9}}Ewk#UO%-ZGwKjKZf7+;A<)Fqr}OUn|S#Ai=3|RK8 -@MPVj7;s$cO`j`W@+_X`BCN|5P<5X?wq>=!a;5!`;gtQT(({A?=8b}Pb@;Q1S9u1e+$1ba@Cxb-IZ!Z -aEFMS_c{E1W`MiV0qIvvdz3c!u75a<3?o9ZRT7+TQ{_(ZYn!AI=WM*~O124Fl{tm -rnj&g@fx~=~pF1T#V+cNYr;NLnV88<61Lz|7=mNzPf-4pgoqC;s;}=tzlG#e|B)#h>3}E6?%8%TW2o5 -TxI#1?sg40VS4uGqbc_ZA*Xnsmz08{R!{KyP=`ff!F!7a;FJt8=^T($EAi_2v?fcusEL4qBZ6R#+pE( -BkucLU+JiD1e7RNmxXO0eStvV6M`k3TJW@-D&7>!rC1!O81oI#UQfvtHu4f#A4jB%Wr1k8h;%CG$Fhi7!k3ClNeHZ#|hW -5X^r?;s6-@D#<6xk)ZW8l07m5cHc~MeKH#fKC)T%#Ty9*ZIL*%C;0UiDFaS|#oK6ZNoK(7w-c|(oJ4T -(cEx9cQ(vcX3%TbL+(B;@nJWptw3GS*GH)cDJGN%yicbIrf<`{y%(z}t&^# -mi{W^5CgBMIK{wxki?DBxRzN2%QaJfh5iIH$j!=mNx9e7FM|l^Jl1G6T*~X25yMjC0`+Dl_2Ul^N%|P -bf2BjWPqaJ1WgMn><{Z0VgOk;H}CGh%>st6Yz*~2dq|RK%B9J8L+!D+k*Rs7TmYC;C`?L_meHSJDYLGIwdYW(if2I5%W%|kJW2!RIhXm_K>;MmgpPom5Zof_HM9;mXd -#;2K%w+$sX8XFWOGwy&CLQJ9OgPYRzulsbe>}b?X}mkY15{$?Cfl2x7 -%ew6&KTAFz$PUUwHoEAyG1bu-CtRbbZek4~wvP$^Bc?C|*y|#hxYMf8^;YhwCpKEv^*dk8=Lo)SdMggg3$yf2 -$v@=fYd?A4xc+H^K`Y(I=(1NMGQllpE-msYiOV3*9RhS9unXB@yWE_IlE*D`v8xY2nqWaPINOw(u^dm -lrb~y`J>?&}*Q#JBOSHDp~(MIggBtWQmE1Z1UvE?2bF`Xe8sSR;^-x``h2xrcIlKjKBZ>`|R}T(=IvB -$M!Jw`S}~S_@E^2%)gUw&fLis@BCqB=52XwF%?nfPR%!P+ePn|k --XJ$g=3j~vIi@$vW=dUI6Mdkf=-aIv#E#^<}d~GVy+qM$vFXsHaGiNq3A;mQAThzTf5A^BInWuW$WnL -|ek9rM~PvB9E;FG>(CehQUPahT*7RJKE!`bN3qnXKMVsUYCf_D=qPGmRSa09#X#v9qpnKRj<^zm%=?A -h$rTW@7^=FFjSae-(*7A{=Kp0tl;FWfPjy}BrxmCUxV6}QE*HF+lXNI?vHeaT4n_u@GA@RC_<%i4Ul| -DNmEv8Qiip*uLcdN*f7_H#CNFK5HveZ{+gAYE)9(w2@_UNOJvUTg$v1gunMwIWyjT_l(ue~PPvNzs%gT1q7BU}0-XRp^1%}dzT;A+Edn^;`gNZ;S_%~#ZRF4H&XoB6u -*cEvE>y1QHuW(#jm9JCn)|YxA=W2ehkIGiQ?NR{z{7fJjLHl@sCsdlNA3`ivK0Wucr7vQ2aW#_=Ee>7 ->(wz`82OvMe~)7D;YcbJoRZbHsmLQ7(Y3L@lPf)e(H9{zgf+A?aPeUxy5f!@q1GIaEed!>y0%3JW6TR -(j1D<{Psr~U;PT>FCS$5_-AhMjTC=4#ZRF4vnYNM#a~ -JB*HipgDgF+Me}Ljso~u8m_@^m;ttL0lrLdP$_?%Lx4dU#lA)Ng(k+ZtnIXknOv$ -HRAcK$eb#qUh<2U7g86n_%MPowyADgH8w{}9E0mf~-s_`50oQHuXD#jkOTuU8xhrudyHeh-S@m*QVd@ -yAg7=@kEViob&5KTGjT%Y8#~T4&SVzmgaM&_diU(v_ga^)X7J9ezeoIEhJ -U%x)Ruf4XHmV!Bsyy-8F3}$n3Sbqe#_FALzk57!X#E+%qEwQGt<4nm{BS4=%J$oqsxWpJ!yeZZkOYwo -%psT~e#w0f&8J`%hJ>xrvbVwdFNT$%cXAfhqYZ0H)kB^TV8=u^@UB?jervRZUK;LAFKh}iEvGK{B+qL -T`{RNJ_dm0JJI9K|~!R_0%yKb}y5GDfjFd}}e8NMd?w{O?=I)z%o0OUaNlgFA8uaA$n#3y$`0v$s-bh -ygj-@i)&a)5uLcqsi&jRGWt4e*b(B*w+ZCt4D_Hwr-h{RW8i$0eHMEs5sD@!dKFs{rutA389B$@F7I7 -8=)*MY5e$h?b=ZQmw)o`R>?7gEtJ9a(*yP0I#5jw>(xvezE;e#p>|igQ@Ab+9yoxiMT>i- -;1_!oorPBpj2}7Wm7(bQ-slnfL&9Lx@)}H#dapW00j$}D9GB&osKRJ1vX+m@xAMfF0j7>)Baps2nlVc -HNLUe$KPYnEH$C9|lvj%_ghXgFfFR0Cg(?R+bw-!O`K_D6K@vZ_(^Qy&3WbGdPDKpDjG;s{x##d#m&6Uj^&Ek*e2t-%a2pE@!X_Il -f-yrQO32dvBz^oW7x863tL+d%Qh{wu#FEdV%Hp`F$J}A)(<$l;R`utC?fd40}rrA9(ja4_Sj?W$tRy= ->({RrV}(~=eN~JHw{PDr#tQFMKF1!R@y13PE9~97mmN8BM2rnieeoIl;)^fXsZ*y|O-&8^_S+S$Q9sH3}vIy$PCtJ0{Wn@=6xJ=D=X%YyhOHiYkB6Zt`QJAa3*<{z -_{`DyCEYu(~ghnPe&`h4naS6xlIRSQ~bLr{$mvX6^eg|;y3hj{~4$JXPokXb)2HqlXO0=o|m8 -yv>E^c)a6xU28rAPUeXAfQ{99(_Uw`}u};Qw~OB=zzg`y?-dh>EE_ZK&Oxn0|xu)^}emWz5V+e44u4M -bsEssPwxwd!GYa_eRMuO2jl75+8gO#72?s3Y<|6bTel9Q`qiUPXU{I=OHsO#o$?Rs(!O7>u)%}Fpc#q -s9}pJSJ1ne6n4qr -A_f5-D@j#vR-9Qh9J-n~1RiWe%u`nGP}8aunVS`zBNp)#CW3UO>)$e=neVz*lsS5P#znPd -qV=`kmK*_~D1IzW@IFuc`0(;=Av@J5BJjUw--J2b!0j`uO9IUp{c)fJ3L#g^e0DYOtayM5Tix^x(rE; -UCHx9dp~hef#zz2wt_h!nh$f-+c4f;q;KX^9LV%a1YI+`Ocj?`PW~6&8a`-@4x>(7O41VpMAz@{>|Y} -G;r!?KXE#p)kN#(Cr+GreAljBi^q){7r|sX;gf+5Odjb6E+|)D^}?)u!hlZ&x+3hHIddZF>+8=kG=o= -;e@Ei?QL2NHNaLDouIUT>2rq&En{U3!w{6?T2|vDd>sEpH>C>nAS6_X_j~zS4j~_oS!XXZg5#WA`;ym -&4%P%k7vSrIc8nRhRN=hc==jWSOu3VW!<7(iB&l$oxbm$OKUS59F(4j-YyAZS49Ex=RDE>eH{4=Mq%< -5jfdJQ8UuKebkZ+K;8rHju;j~?a!`q#e%o>V>pcbKcHs`y7AeI)FlNqcF`BOuA#FF*eHV?D{;c}mlH? -%X;4(@#JB{N8)-ZTs@eFF&Ka&LSMqc^}ocVT8j<#QmfA(|8JbxnAI}y`VvRQ6}08{53i>v-UQnMSBTn -*GpyYdfnae{u%uD?Afz|%CRTOL@8+4y?eL7AM)|xhaZYE`26$Fg&jDePEZ{d@SS(w5ijZn$^#JPhPs0 -|Z@u-Fz#nBu^`E00E?(458Oew2=gIv7{HYEP{Ui9-)zxw8hJ^e_MMd?69ODK4Z@>LEfBp5>Ih8r5F|( -+{;6KXa(4j-(1?~qA9u#%uwe|K;BSKP)5LQ^Y@%*e8~COBb-Mb;(YKv&ck+bKB$uONpEugx6 -_>eT37cD)s-&4i@6*AJ9g|?-Me@1VKjzcN$n-dL(l+x!E^8x^q?$&JIpBiPe1)syfB03;5YDv9Do+!4 -gRBypZb<_^ZT5~oZvkADCgH4<~-sc(Xf~E0YpRJe{$Y)JLfAu=KR8i3y=O0{Hd*Xy})04wX)FOKT}Uo -4SFz#K>#}KX~iH2b$H~A+e{@L$v-un%8{_YC#r#^#g?LX -w7`Xi3FX_;tp4!)u+AXktLjSkdt$Qk4w^+JQF7kDOr&iVC3Lp;g<_zxrvqmOYu;%!MoIMs!#cXJ-P^N --_CZSm?pefkWea#)FahPI_SIw0#U$rtz!fAkB8hjzyOx#1MmQPA)S=W#@Xg=pCL9lw(2M|vM_3IDTa& -vNQ#8oi)Fdo?*}S{_yQ%`2%?|0*RZx;(Y3toKLLcJP|bfOVwwRnNeDOJ|yci -+N8caWPKj^hOE=n;x>1gs=%Mhc|~MoWKU|(OMyS+q$wScd&qTDIy71!>yV?*4sYVm%^Ss^wvXnI7mg7 -$;F(#?`HhN(q|Z1{Ae^lKa;wjvLDlC$Xp^W-?L%!+52B&_R@t`Fv!prvd-m+-m_&d?CfoOQ+p*;S}57k%H*i;jpbEr(BwEC>Z82x -rMk3ZTAP5wuY9N8CTp}nZDz#F&$ca0Xb{b=touR*(sz6yOCo*hf0_%@>9wM8TO#s$&*MWUf;eZHUCq} -6jwe8p|!__FL+UNSqDFHSdehy5o0=?BLI4en!%L28WA68_ZQ3;8E}RswI}1qhsgH|hiM*2)BJy!-eV{ -V!;Q{O?&dhX0dj*h(~PT1YfdT|j*X4NoZ=9#S-{0u8s0<9BBf4Vh-XDBU7xz=Jkvq}v!{*uLiFA8iKu -FpMMcZrHGazwp8f9OEMGbuW|4)sx?T`;8xaJeBV%xrV>7glM3;@G8;p4^^MhCOv5%!yhXc!`J4I9`jbyS5$z7fLe0?vywNwHt-@Fc<5A29@Sr{)eb&w|oTK^1({o+*SsPbT!6Y>Dufh)>LlPj$}Kp$ie^Bj!pP!^~UsMDf8Q#rWT -=jWT$=fhi`z&P-cD-oU%BSx%w=%I(iItcPsb(t3z7xNo#xPkZT)r9{thvA4I0QyDmRJ|F -b*qyz?UPoHlKmcu}V{8U_p)z~{}IC&~eRhPJk&(E)sczqaO!xd9;R1L_FsL(}?>KHGinf%!B0gX?doT -abT}r6-pxS@JIGX?%P!6f}T8SU05lA!tZVP2~v*2|~8kuU{|l2d}|%n1M -Iy0oot9V{8if!B`vh0CfO$0_ma+CfqMlI;9~YAu&|$Pwm~i*U99bkI$9jk9^(7KN?^0V!jA`sBAfnEp -Vz>)CVeWPVy#X0DTmAti{pR6VZ->r)d9QdF2)U(n~LiKJXv^_=kuGUK5|3)V9r}K5_x!zMI_cW^&)-O -80NX_zmM9wEt8eSHh0^1_)eG-l(%Ar(!HIXU-hawo+Xe{TtdE$hRhks0XN*C=C~_ -d7wWhdhx|F+D-2(#NCCzyR5p)u)AynH_!zcLS4A~?z=g)S)!kIe}J3zA`aRav_*IzKbRBY1@55hi6@@ -G8sj<-505{Jy9KGKIdjnKgZZKgvcMLttMAat~Po&ryfg;xPXo=X) -dk-Q^!`MAJUDDJ_6lW1lf&27mCu2ZemQ^Idxp4zdJUhaG(YaM$)nXk7c)e*t%azx(```ks}LOY}jYLt -F1h`+>F&{gFEz;O*wkoB6{JKP==~tHXFfx3)HqaTDy28EyZ7+RW!XJw5*`xV!Lo?-$+2Fz#cU%asYb&{itG)8bzS)pI;^cUlf2kH~A`ZvGG1Lq$85_PM*m^zo8^X6S(KK$a?dGo3m8(S+Onf_wvsi00l_7 -_22ll0y4EmpAdufTt?f?PtKf}RTcC}>cyyMkjCT&y6MPzTufyB#C@6A#Y!_^9{)wRf(uQCwFXACgc|r -KJJG+hQI9rzI`OJnWsodS`a&LK1M2mPbk( -H?2|^6XepgbtzRHrv?X{)JmdG@9)lBd%VFmpi2G1mBzd4ow?`Cx#ynO|IXdn+BC5u^*(X@MjRiexhW~NZinjP<9doZ -!16)6BS69dBw{^zxWm9hp#J(!xjqR; -X4kd1qSw@H2Mx3M-b9`q563ak^HaP@EHr9Fy1eq7}HRgH~}(+7J6Km -K-#KIS^qfyDi)lI0W9`DNZW|4D>(+nf)|M?>s)YS7SNH&V0l6JA!S*1F(ZN4jZ~nMQ;sn&ElTLm81=pMaXWH%HWZ -N7bXi^^q-KWIy&c7$mf0dsx4~hBRIEx#*LrybcMt!gUOkylxb>9~}G&xwKt9{D;L4Wt|-Tos-j`+Zah -{xeW<<)B4AbcPT>=ED5z;91%nV1uI(npuB-O>INV{hHMwQ9$X9hJyo-@bj)?qPTFeb|3=0ePT{&~{@^ -H?Cc_hOb=_b0S3_EAq#&vgzpKQ>b%L(?!PQS7I)X*V0q5Ff}%e#jl<#vPQ07wr>9Y%7i{{n$XAgrqz( -YqyHEEKcwjUe7?Ws=H|+}E7)`}7_@fn+9mTlcv7}N*-FL-+1wtyYQ5FD(R!sNU#hS=O30Yvg@TRclTi8|BB<+Dg$q>z=27z@P0>1Z@$241pz5H!dtJl=WA5s -I9G)a>pJhj6~^xsv#?V5Pss5ii(O-q03kH#t5#Wz1^Alq%j0+f -a#wn&)`+NA;j2Cp^8Bvo>!`@jhc)|=jhR}8s`1kYu^sZJmWvKjO9$6rezjMyZ~}IbaKG*RGWqGxLITX~;m;c!`(6REXQfJsi3R+T-+7Ygq1@1pX)#3Q60*T2)n7WW10y_l --^N>+3sykF+Iy=m=XQ?f;$lP%-eylP9gihY!nIF}9JKlfo45leVM}9p!(~CGJ2F5}C> -CBlkYMtQy(jLG6yQzUyj= -JAR#JAF2DV+{1Ibo+iu-!-vT5rILdILfd&GLgn#8)~8}UOsWgp}m1%eO8Z}sZc66eN- -A%}sBo!05*Y?)6IyA!vQH;_9u29`-0EB|U)T|zGH?d>uiWQ_>88e@sRq{iaC7m_mWZ!8&)xw+Ywk)QA -j(r$stv3@}LQrgL;9BXKxPs)=V>IXqde^iY>x`(YtPZ`S{%JBsr5M6ZDKCQ1AFS`h4=V!xBhd!8h^DWiN5fY(8nK)Y;#%@-eD}qR7bjgyYnl_!PZNjsuybhB9O|4o&hdHvKTDgVc|2)ywXYDi2nI5^z+8iy*9}Wj9*H<)n>MJX%!r7UdLsgH3OUeS3{_ujz -U`ePtT)kn_f|BaW$Nb^SN9z`4dMf=@!3}}1`h~&l%teoSGjl%oS&t`cQ)qKo{ubd~_O{r*PFxudlxz+ -KH#In)#eYJ9+RcJW;Mt;3ur62;C=GRAgX@tU=8{HDd{tc6ORIYs$J`Ae4;E?-t$?7VsK`G5DB-)W|(os4ux&P -RG81ChZy7ot24aJ;k=R&FGv*p;M!MlK -<{4gNiIHa%7)3_0vCb$nYK(fL*=RM|i~~l8(Pbo!ZsWYsZwwfN#)vUyXy#lq%}h5v<~-ADE-~}W0<*| -0HrJVDW{p{IHk++xn|Z+OFuTm8*=?RTd(2+5-yARp%@K3V)Z%mFY4P;9Cq7Tycel9bv3R<;(;9K7&Ej -6$5}Mq=lHarM?d*;83ptHNG<~j~rl;#3eV*>sm*{zVfsXXb^cuZhZ`ND&Hhrq-@6a -WAK2mly|Wk`L%oge=s004uS000~S003}la4%nWWo~3|axY|Qb98KJVlQ`SWo2wGaCz-L{de0olE3S(V -C&5z6-&%VbG?0j>+U&;)A}~C{cNY*UB_i839=bWBp*pxQFix#zxe<_fCMGkZTI%xEngc;6fgh=gTZ`Z -u;UFM54~|VNz&^*Z&6K$zsEPdo!*Z3D$7?za(z>I!K=Ra{OPlAhVaj~-j7Kdue|g4Q<8^J>}@g@X(=X -Nm3h^T@J?6NO_qA+*|b{5h49{Fi*yoK3A}?3qVQs9FRhAXw5T%pc3lWD7iopC&V}&a9=$p|K0ger&y^ -Ra6Ytf@@x|HE%l8*2XXp45%Jrs2Hus`vx~LX~h$1hU=UGt!`~-#~qFGXjBA%7Kp8R1Hm*U$O>Km3Bm+ -BkzO;pKT=MsxC*-ohV9}f+!kzuVH}D&BM=Qk>j+;vg2$HjeB() -FiX?qdf@#v$=?9Xfj2DyZ-Bc2H2b-Vs#T6JlNlge!T?;xh-(GqC@rgaHWL$UlV-ebZ!W4B%d4{QBw#h$oTwj4hV~Li!{r`m$RRwW?>wSuu}i$rmyCk$)I?@6&RTr@FL_6S*d5ns>aJ@^gC1nN- -3RxAFa@=@t~k7yJo!Y=ooU-eG^0)+`tE8p8SYWAS_{F5;Zt3dDI?9~tOyUkN*EDBjcKP&`%8H>Ol`Y> -yeZ@dZR8-VokXe~a@PoM-N0gEE%gum7_j&owoLp7BQ5=-g!bddqgM&FpKiI{p3tf}NqMDZvIBoKUZYy -&`DVx573fGb^uNu;Cir>p4o;mh~m`vy!6H;nUKq?5p(KzFtR_tVkE57Fsg-W|Msd-7^M%=xx3p^NX2k -6xX;K8((f{(9*5k;q@H1HCXn7Xmh_)bsuDQ>xVBu9_duG1v^%O6R&JLkMA&6fC4-5+5_j|z~ -Pydk4zq|Z-c(vQ_4e{$;`rU`gF1~r(?*;T~WW4JCk&ET4?>;Pd;dSsK(+Lcu5sSK`CZaW6|K(5cW2dv|zu1)}gt-{loqpq0yXe(Q^w6~;DOixvLbNTa%}^bs>T(- -tn0L!3>P_yZ-D`~?m!{t!0sD;FxssvA!BWIBFwWq(A9J;;Mv~oz`OI3)PP_!0+`NQ{9X5RuO)Th`qn> -R-d@$+o+jxenrD;6O!a-bn9Ue}*qwb81B_unW2(TKHVaWMW|bUhjRH+dt#a5(lYjA#bysIu+|kuF{ev -x*t916TE^(=9Uaje}*#PeU>k(hX3CO2MB&(4W2An#LM>B!?3*x?>RhnxS4D`6%T?WDl2^o_WluE`yJ! -SPI>#?|igi(Y5L;>1NW0LSuJFJ2c7G(}v4hsm+`b~VygxX}h@4ROYB&as?czlbx3^qy?E}vd?&3bf`W -w%gHagds){Dcd}<|to({Wbl4MFjx0qeU{CkXVwiQs9yFOjS{d5TzHK9-ewHp8sb*)F9{stw$y8K?j0o -dd#SDMYlL>z&FdA`1yZ)>(}&Yu{2Uu1R{@C5X?++o$kK@H4#1o@MOB;ZwBqEQQ&g~pb`-5fqLSU2a%4 -2wot(v3(?t- -h~e3v*;*w(Jqk<7#{psLHKnGjFdH)B*s|JiIP4*ldz9tU15mDrOFo6bC<@8<zzb~yMOJqMM#QACH@WMFCDXKPLXqVs0Y6 -)<#tlwFQzxewYVJU*jJ(6+d8_-yi5X4wyD4jj%&n)U4yFpmooksau_4FDG!_M{H5O1`N#GOqzCf^^T9l|QRh7YmoazWK -rJGLo6)x|fuwCZTC*gZ2P0}V)Z0Qt;NJ9 -dz7CChHGD)i-=@TIktt{`ZS~8VY^AA#XcMuavf6DT2@5*jjgKD&01!j?|Shee^;hUSNcD*nqso4fOCT -;I1G-Y3=A&_etw4l+nji&B(u$%0%zUv@F}xCT7(^NHC}|j5aE7#3)X$B^6<9V^F9<)J9*aK~Nb -}$ZBW|f{X|A5RLZsl*qQYnJAuCqEPLlPX}4(9@M(x0+;Y{&z5m_yi*v5DDKc?u!24BdV!v95PuLB35N --qQgRryu(dwu=g3zEV%lN3;c%8M(e~0_>#{(^l?+Tsqw^Llbpwc&dfl>`OO#7E+vwMTlakEg9?G;*St -*15w($=O5Nr5aXQwUNpyyN=mjBVCURNeUs_4ug^Zp=c-}vuU$L>bEchVE*7eMTHbTe -Ft^gLxuj#X{b!qSz#_CaL1)OjF5^6mj@0uhx8#G0!ySZUW9~9d7*PTQ*^6h_Splq -euFMUN_B3zhUNW}>27WEdDsU9zNSJU@%*uEwqS3c6M2eeKz$N-&fq@!1kmyLXltg3;tp3gg4RTLH5)=i?TIM=V}%W!dBMf3& -MYSYWwMMhm>+Y(4s29AY)JvxQ${LR~gi^JDWVQyOz!H2_}fQyV4*Ma|2kyWH=R2k_TDJvm9AurC`%Fq -{y>_Gl*58W6x#$?6UrS$7q?xN-v;jrFu1-93*BMQZ>T#S=x0<2gzfJVMW$%0({BQ{&(7o)KzD9X)~6u -m+y;IW~3N}s8rPU9cKa_?I7OYL1J)Fz`_fj*)c&}+a!c!3cFfMbw74}A1$Lj~kW7O-HQg -t%W9YWP{p2m#@OouZ$tvDJ%d3_C!iolqPcb>%E1olyrbk2-#r4k-+uTI>3)yHpj6O+9M3#T*@KddFOS -l1?(4DX2DZrjRXRd>Xs~j9a~Euo7;yt^?nV8kVKiG-DeT;DuSEHb+Vt;a!0U;gToV==lTHZjz4;wHv- -VHSVp{b2r!HolgI@y^;d;5V$45cT#UX2u}0R#;N0-rhw?W(8hPSH2bwcLfMtG -Fl2XkL3~;-;r*-))=1r!eH;mA45+m?pSSFww@GR)(uu>4^YFzi_UU2RcKzz=r%GHJVW)iVMDzipdp%2L -SV6;i&g`-&jNxXOwZD9|8WfeJjJ#W}^pBYw{Uwvo&2psN)PZCk2U$YOe -Y%ezzJ*Y0(zk$ZXWO#5Zr-b=k9<{OWDxewpFxPtB@*g+pg#4kA$wQMgR%sYq%%At;Z1?kL -9Gl$h}}gA6Sr|#@*>R^*EjY0WfblkrL_KJE0vw|g}6@DZW}0V2rztfe1388_U+*remVc);OvlIOT&B{ -@!eZmO>xd`Z7Vg+U30UdhEL09cWFVgy>NTpqe=K^LXwyr_Xb#G{nLEgw;k91_afyZF?%$*z*uDfqj>_ -zu5t!6I6Nwcr>rFG$qG`mt_xpT6Wa9UY9}xXr|W!{IzIj)zQ(qOwdC3Itk)=Cr=~l7HTCZ2-2Bu=hsI -O}X!o*zUo~EK;$B)jK5}b~E7)C7!sP`J8o!^CKn&=Gy%p3?`kz3vr0ETD_1vJkiTaT@k8fc~mUtwuhS -D-zltIw8AN9K|QO(|>0JExtI&t^>0^1**JkvAwOB_sfYzrAUx=b5Qd!#B?TMA^XsckDj6u}rDKfYa}H -=r}#lfzV%eLx;>Y#0VY?+9c|gyVhu2!CMtef$WEryhzbKS1$-BRrAE#@-m9W8m>Xid4%4T7zH4bYhOl -xLVfcJaW1tB}|fWTq*kkcTO`2G&BICF4gHvludLo4N4zbBSDQOZa^joqOkHzNfObA@_WrIiDKbrUJ7hg;FktRRb6Y8Q?FrP=D_LRu -e199rR);g*}o}fpcRT@>`2z$vJp~iN0a`CjrFv_x^8)kP^69a;jUODGbX|@M2?DBI@C@(45k^?!dTxf -8iJtR)y4ifF-?J0^{aiCU`G6fSCRt(^gd`;8(6*S_nIy1VK$^TpS-(l1a;WqZ9|~82XEtHZ`GBy;VZj -uwTsF)8b(7ksl*`U&NwOM0pt_hL5cJ_-ycn>0@H4{^-KE!yNlt2A{`VpE#S>Z4(Kw!;r)i)Zr~_V@t}Y#Q-b_JH+{6gG$A+-i1I%(54#9$B?g3bf7)LF9!_(=ywIdazV!ylB -%_cGDN3&9zT%V%)b2FUUyj@GJ>HdZgt#HnPEdHPZZf~Hj2l$6y-0^x0^6VW2lJRGaeS3fW95EbURvAq -yaL#*VzIOcBSGP>O`pc|#y -2`3(sK){a$)C5JLw6k{RxO*s}vG48f3W?Ap>aRy`}D!-5r1}hgh1%4#2R)={7bO5;%QZ4uS6!if8B&8 -zzKvp#pSgbLUEuy+5`N>(fJ1(XJiVmx21_PVV4#}B#Ml8l8C$wq+@2OwmWC5m_KHY)qY?A~U@g`Jbye -JBhRv^Lg_eqALIBSi>ZyT%Hz@F~EVPt$FwbW0`mixqfI_+TiAgA8fTvoT@X2@j4U0y!FVq0rf1ohNM< -kH7on2U7xrz`M=q6+3oNx^GGj`unhgpbccsvbtCu@?LIY)v0Inuf*ueOX$`F#By8#(b=uR`p|DG_rc6 -S!%S=GZ78yppAB@#=x_KNgSh>wPCJkgzLN2HC+zXsI{!k2nUJpiUjX>?~11%YJ8He$ -)<02IJJ#r%CxF#RrR=$Cl@L9XUk}{Gz)V>!uyIo5c>-|aeD1VuYA+X=98-vc)HKw`|6;U)tff;yE#xT -I}ig*HOdfwf4xv@8uP?lr~@Rgl9DCqP(u4nQfWQjOs?~mmZ3)iiNdTT)2w?l&(n3~@Px8zz}3i69Swz==i{$zfE~4qJe1^JWJ|l)Z(H1 -a4sU8a;#?*-yizya_}2Y|XmW11ky?fnUJI%>ipbU+qhrZSFBX+j8~?adq|hAm)-%@bC>ngW`>1BPn6f -{+Q-o7ByL9zS5)#^ZXCsIjjHg-=v%_RaL*H@B3&3dLb_YxNs3YypGrK8R+Yf`^wn ->r*SeXHy^y*@kR^Enuqpt#4RY9H*sE)5rq~gXfAEHeDjfeuCz%9{!3SP#KgbHNl<oOLmNIJ^xwwwBuwB5AQu}nsRhjoPYD -dG-7K^j3R}NCd%P$RboQb4g$hwm!lZC~DAg`{McU3N6rQ}VF)q|fax%n -LD1K&`zC%f%EvZJ{X22T*YrPG_xfkj-7#g+9nA6bWH#0F%qRP$iD*u5~|orYSkR*XhELcGZfMs`mZz$ -;@@mTi~0(G*N|64Dg|Hc!#jhAh)Z&S@7RP0$URtJ~H$N_jr;^_ShG?dA~PtZM3*Op7Hr5xTa35!} -mZ9ZhKe{M1CtYR9Q52SmQeV$QiB`~E!l?jDNS` -hX2rK^>jZ*|$qy#)~wMU05YOPFgO5M#~#e;3g5~?4n5xWF)22zpoxx4S6>uwv134P|hyL&Z(3g)!}~> -DpV?aLOIX~B6-%oy0@99bErBYOKDGIYy0oaMQ=V*3=p+ZR1V2BQn~Wnsi+DWIN>k_)ArJ{>!bkLA^Y) -t6Jas03L%sY$vA);(eAK9lLRH5X=azXo@JwW#`h}dm$*aIO$N?0uB_gs6sUn7UW#wcy!b9lCLB`=8+z -(J4?pjbX+Z`u>^y(^?8Q@Ci>DhX8ce~y(IMy$$cyaL7>4wN-``9V#{B@7d*3ty!_+kmbk9UUX#(``l1 -LaKHX3U=_WxX>vpTY&+uCsW=ZnK*%)feWhUniTLz+%qVdOrzXQZN2zqz;3bF1WCR#I_GYjGQzB+YPI& -*rLf*I^chxWhtHLysy#xFcbCtGURvJ{T6?cES+Xdj4!Fw+6dL^nfZTAK>9Jvx3{UhNtMH1C&lY+1b`N -%Q+KvZ?%UgFLbe;zTC$8^P}ddWoAcgWP_{)eJ9)>HzkmpyQ?d~V~Xp+uUmAX#;yVNyLzze=2O;Pk98QFET; -xH@HWvdJ~F?$I`zY(Ow)DG1b!$cHaC%Gs-2{a`0l1;G+Udk_O(n+L3Vj{tR>;Zv|L*rI>B=WiuURIpZi>Hudf8ilxJ>l0BHb-LZ}0KcK -6f{4Ni(8&B%FnH8{^I^E=cr -1)4@|$Z>W0fVv?qy*gBEWbrXsFnXhh8=`!$s4S}&&&I9qPtGb99D*LgFi5HOQLbJe#05wP39tw_%xlazj#y_XLDAY$R+-2A&xTFP_FEAAK_Rmhmd2HLDtvhxIftD~p6 -o6u{||IDx|$(GnMR;_*Gwns3F*&Y2*SY7ChJN!JeD)CQ$FhKTcyjLK}bsF-!IVSr1U0#DHx9e)9iQRZ -1Xp}W##`Vwp|5juYarFx_NF!y~3*yfL -=r$Tp4n93lME?vI!;Us8s=Jcfv-Yep0miqH~7Cblfv5a2}NZY(TT7!Bxf_>$DXqWgDfW7wjqwDE2ukO -VIXLk{XT0WK1`-%7O2E=$EsOVmRf;kBVA3$%K)cgi@7?r8$#qfKjI(IF55T?fjMYxb@W}SYe;6zDh -A`$ikeiL}fR?k~ZRb#x?G(k`1&g;@I|J@g0?H+{PZX^hb>*Z(ntkQ}sKq(?dGNT>Tv)r0ycj%aNy$11Wq?wy$Hd$9p6!{Jn05L4@kY<#vnvd#e6~f0RoJI;aVGcilh?O^frw?QQhXDs) -ADWPyqELSVF||0>@&<+>n~<5ZRW}9_;y(XrSFsT)k*~>tl3_n8C{?$Coek8WH>Lkod(gdH*!MQEsETZ -EhpinQpnNw0her8Ln@oN^}SVm^|$AF1uuDx)OIIwum|PQ5`uYqau)Q~`+#0cyo7Oyy_Fgm#_j`tc=mWePlO^rkEI&(FKmIbNxxFgThGo8@cXo+Vd -Nl!^hdieayzEAgw-aC8{AC9Zp=M_C<1PmVr3Jnfxmo}{F@hmGvFm)JQx_PiOyhovL4fpTjf5!kUuCqH -UHl`su0oIdXr+k%_I^_lRG_Mk%?NO~26+6_f)|C`nbHp`^jH|U%s3+Sa -Q;hJNCaUh=x=2HI{&soW_h*z2J_S4Sj7bp5O(8f64K<2vnJzbd`yx`+k~lFCMkiqemVXVLllH*b#q%! -<%F{*jpvdY2jfDxmh-=H#URVCa&~BD2I|u4`mRA+5+LHho3tu9mG;lrB_0#PY2ERma$$q$_s#FWr5kthvCF -q10cku=2+Vdw_&bT%|?{4`hqB7HNX(IBvGLyg -`o`&;NJ(E(;Ga4{|L)pCs50E-H@?tN%!IbrB;$>C|aTT$V`Vdmj1WWH=MqVmtR@UUixmIIIOU#37o5= -(ip3T~=1g8axKRmn{kvV6O!-WD+Pj(#_op8}gEH!CCKrfJ{Tb -qAI%0NP5>Kb9mI^9i{EDae!lX5i;-J!JI2iQNt`G}r7p1S^?I*P-kly?9KC#d7+oBEf6gaLZJ`8Zm09 -z3k0ls82vd3?^!;)YDLMm|wwB0(6o+D7x<{(XKELNeeV(kC?%eQ_%13vdWtY)ADfvBAgYFCBc!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFJE72ZfSI1UoLQYC6B>Q12GIl@A-;R -&JYB>@drI1@dqfIc)NzIQ^gL|{yi?e&5S&w=NRKhud|V&^ea=vI{J>!!?rFsK`l$oqoVOL@?g>@tbKs -RXh?3DO6by#6vA05|8kw4mX=k0)5}<=6yq-L26=gU#)A5mzLs2mu6f%9rO0N%LTQ=u)iph_Xe82$iIO9KQH00 -00802qg5NUw@7WzGQr0Luda03`qb0B~t=FJE?LZe(wAFJx(RbZlv2FJEF|V{344a&#|kX>(&PaCv=G! -EW0y487|shy;b5Eity9h5|XPK?VdE(xFLD!yw2qooG`eL!!6%?!#Rr5l!+vJwC}(SiD<+w3RZ4 -J7}q1e2N)1Wm8z$rgQ3WB*<4Yxc%_)7WPMkZyg=2ft{`Ck8lWIY-=h(%9w?Y%!c?$ -&*zO-U_fPwW$6ZW@J~o+5?uGo-SVtae>p+=G{Z>^gG)OJHN8e-X*2u{1i-2HEogxCPPm%9DW1I`EIfo -^D&!mt?FaCftQ(LO)^EG>rs -K8JI1lBrJuErzcg|-6C@u{4EQfkOY9=!XMr1a7ZgEJhGjJh;_YpHzo#qNWDNH)I;)ElW{e04Djf0(O& -Q*eqW*IWMEq{*GUZg0mj3;4aU!OnYXRpk>SR7@$&5WlMNvV(QD|tR~1Gov!Nf$*ExuWk={o -eIuo*>BVpTFBVVk{~X0dS$J*50V$?KNOY%XM$RXAZo)kb>f@aU0dp{x;Kon -j<`wpOQDAv-sNg*A;a#!6P)h>@6aWAK2mly|Wk@us(aK01001&%001Wd003}la4%nWWo~3|axY|Qb98 -KJVlQ7}VPk7>Z*p`mb7*yRX>2ZVdDT2?ZyU*x-}Ngx6bueG^u*R37I$9g4vyDGj3jHrkL+Fqff|xcYN -Fu`GY`x3-v0NiXFq0!lGX`uh(6dPr@OkUy53zqr{?iDkIkxD=jHa9>DtZX|G-~PPEStFi)w$U^X+wOk -{2^`_T=fA`EYH`+e3R@mF9i5X>YUInpagIK45Oss>#42SA1ef_^ergW>W1F=no4V -S;<8+_3*G3*>%Lc#dxJ|js+Limds*1ugg8GxK>#|}{sRC)U8i_qQY3swY69a$pVEN^YzAk?M*R#d@k8 -j_;dH3Pv_vYMus5^UNKdxRX3;2F9!JR`+RLj3j=Ei;1&%0{vKI4w)XZ`~?6DR#jK5Y4xQw7Ql3{egOHM&DOsPaw*o&npIW`+!}yHFmX5QgTN&v1O6OCRX6_-u -<|ws^M&TonOd5Q6tP6$PUyrOOz!zKX -!j{|i`uyp)-#mSC_V1n~vgc&5$=cx5$3=c)4FCY^Y7hpB&hP<%MZ6IXNDPR=(`QaQmfZ%h!nrB(IN{Z -2?s>l9t?f3?LX9#@7=7b@!6nk$I&W4_|Loi(6zbt3 -L%Zl(%jI`iv0_Um75+paFvDW$`8-DugPH}8WT>Om1V!H7OD3UQbFB5ToTG|$ZH+OBTMW#dW)*F>daDg -=s>kb)`Mt+K|tAH-K8aVa=|B*(mV^&|HD4qQu(Ipt;d#GSChz -Y;qqMhEqRYXBf^@T`T(@+R)~|fzE1mW7`G@vY+x5gkfnqi{Jofk%;vD;`RGiuV1}MKsB0nfnIwSuju( -a@E6STyou~J@)=pfnvC`f%vs5tpVrIi%w!FzvR}rJBHJ>()lu*@MrPa`1(JpY9FsJBW|n!|+#zmWw*i -mUHB1S0I@*6!4flj#pF5uZ`OWt)LzcR6Fqxn-rTokxuVBZJhro-VgRE{UlQ>%D20X>i0xbS-Q)Js_c8 -tPt0v>{VpEn&Y7xD(6(r-+e%op4Q7=D?JgB!4Vj8WqgiDDEOP~0>xhKbehQTLJZSF` -S>;&#f)jiKP1}AO%wrdNx-8Gq39s8a1{}A~>@!aOxA{MU$ATn+E3Ouq*QNra7m8Z7!&tV}L!U@e3`Bo -ReVBV}+Mx)>*j)Fxy28F32Pxp{X0lMj*JL5IpM@VvY#BAGGU0cF}-k1%}uM4ox^(e7zA12vZVpF4YQF -n_~{d_7EhcmQ)5kc?Mw)00W5uNKI8XvN(Xjh^!(w5UeOM2<4+iHajNgr6hsEeh&on5T{wr$eB10hJ9T@45-`iKUGIo0VF)$k7 -L5d`vZ)?{Xr9Ws5*}Bva-eB8=xC)M0*XKb=ahkMz%TqOnAXFaE>5j#%?y4@>NWS%UO6J*9E0UK(Pv=5 -lC^mM!Q+32MiThv)cI1r@2k0$siGMsV)R6)Z)nA2fYwG2Q-@H@R%Ex2AK> -Jjha9#V}+O2?DRC5;sSS%oc^=0kt?99Si8TGX+W!+ZDD(=J9{Uc8-U?&v7F{EAL3Iy^RpT7qBXOC%P6 -@X1Ekd|JszRYtVmD -Q>HV{Rqs)W4`>&u1M8mb@T)~XE(?Fv6^92!k(et$qmmw{=TOQb?ufJn^EvcrNq%hD$D(jxASPgxt-+z -c3NO+sZ73icos5BzyAHc@XG2$75MbB!Dj8Dug#pyLjgzlY++b}LFp`;*XPO4 -jK-W?OzseruvG>m40*1E41od -mU>PlZ*r(mQ#>}@lO=tDSg3f2ZfW@5N3f@K9ht`leCs0NEEhL=j>ArRSIsq;|5ih`_ue(kbr*u0nlY& -$fRhi@s^+~(rRv2F*FZ&0IqlJ2CH?Qp#M!@+RPY@o4KZnb!VMrz~_`DG1OLRldLBl+DGGZni~<3AVqi -+47*=iGE52H8S#6lj6DuBsc2=X(W_D8!TqF5>1$_(|*x>40bUC?QTYjuUZ5$hhf>Vl1Td6{b64Iw^o7 -5`45Hz-C$l?|YdcApw{tp@g|pmhWG`OGb`Y#5(GVJqzh2ec1HdpaL#$r~S-;5mgWvgjT~6mmrZK@mq8!gJVcpsAwDB=);3sm;z(!(E! -DJOWHS*6YpgQ@X<1%PnpwPLvuIu9Y`UXB#fsx1YdO}w=fLE?1Af --VAFbI@qvKLsB{m(P5Lu8TDlY{r?#CO9jtL{98edJJQib&WxhQ;ar;;-;n}hM8xSM`kQgs-N{BA(xB<;(wXgAoAYvDS1Lp4Bab2q?ua$8Ek&f=URvCaDA)csA$SJASFFZciat8l(i -Sxm3b=<6^shcrVycQ`$hZ=c3wfZNH-M0(6H9*Q-v|*U -J+TM2w|U_pQpGz(`Uf#FVi`Tw3$wSq2W4o0XR -fs0S_3WS`qi{2mM9yY8O-5^XVmkf{$lYH!xf$8kN#8$wR0QN$y>E5`byjI)iAEj&vu6<)(B>eJgKrcog^SGG&X1gZ1YqLWmaWjZ?ZiKRo>GE4S)p}8 -eSL&i211x2NOJ6K5eJQI?#wNkETZJVQwGe&_~Ze2hB!`NI3S??vdUDJ0N(~obUu(Q3ZuUDPcUd83nP= -b1)HbPLjX*r(}<4<;3yMiM#`(ezvE9uRJ?f;*xf^5Gi}`w*qs)hkmSB|ltGdF3^owid9HVhQfh?_r4M -^z{}-dYbAhA0mH`(ysjgD4_cg -R*yRUPu9}(R)+kM*%GewT@^4F>LSKea8~vPIOgIMexb*p^{Jiu$lmDX#Z-|&*iusDk4LD`)z#wEn -xx2i%1_#pKS_o4oR7t?9F^I)FnC=)qMF**MqY1eaB!EsM?0Vw!Y)Jy$9i9p34Yg}jY@Dp2X&6&r=J%} -UDJF7<$L3Vm())N|%@6~+et{L(4yKnFF2s`&d#qHofL~Ui-^0Z^zT-HVfo6p)4g*&KJ|4J^#;Ny2pN` -67apo|_4B{>~`>wcalA;Ku#(cz5jc`n(#EDQe(YvC;wFHzV`vkj&yMoQigx6~@UVnZ4mS#e@xX?OUdl3|IU3wAl?@_v^wOD7s6P*~-e2!soMw(T -F@5d+a1jn|%6jd-Q|W3lmZv&mN38hl6b9zn0i)K_Ki)*cl66xl!1xgs|i#h7t2gid{QSdv%v-s%u>FF`&@+=nHJ -G`%NWxr=bKKeGoC_CIEIMTMWAYKRxl}zK28k0{0>vN4U@;_f4=nPYmvr~pS7}_?!2#p++yN7D24FZ99 -alIS68Iy@d!L6FLNVs#52g8D%^TwhhZ-Yy)045nsZOLDmAn&u@dcTCYzq~gKlNPN7s0lD1&t_!J55iq$3F)HDXNpaMM?PU?Qu~ -`kCxn30UoIx9%-}KezeBRDq+?ZlikcXrpR0ac~7Y -uXyRjHVks^-VCU!+9AnBkrH>VZ;;q=wOf!UPK?y%x})m#*QM#(Ue)-3I(nKeq4k?sFk6CZN))kSjQBz -XKqNnkG_2)JZf3n;>c3cP!YTjB+TYig6pbkF?12jpplGx^_kw#%m=C8R~<+M7KUGfu}vx5!xSv5wd5V -9cw=@a4|D0c+2v)2*6SbyKBi0J8@a^fO6=d6DL;2WV{7-!&?(f$NL6CO1^o%RM^;zNwkgt>D5lEFX}? -`0vPa~8DvWo1JISVhnDL#-ZHkqj_OQ=aRIXTEW1>bU#lcETyP?j9pw1b>NyDR?!TlcT)P!Z|z%zWc5h -Ur{mWg--AVMlI9iqa#-Q=H<2+Zo;OOaSQgVsU0-W6cx$)IB(zsj0iSW`p?r}0_l)bY{F{U`@vA6px}`&R=AO{zpFVU|8}EIc^&>B^fDwz+31n8WBE>8rkC1Dl`ELqsl~_zOTlA9)MP34etxfHKTx{wknm-;qV_uxa#gcI(+ -W$ce;ftWE}?WON&F_OugFN;TZ+Ci0|_Hr=d$`Cg5Y!{BN3645C0aWCEOoBOi4ce@b>wOKmJ1*$%0WYAl4yeH$H1P5K@XHxJOQUBOFC0iOgc -Ji2IvesO9Waqk$rA}fR|(0xAMjo!7B;d&SF5%rPfuSfAnlX-4}+`M5W!h=qdLY3 -Nx4N%DyUbu+uPQ7;L)x7q~`o|0_^>#GY_ez&gvW$O!lclFOJFMOpn=ee$&n!+7UH^MqPz<5cI^;Hk_R+%U}L3O*rDOcLDtn2b|!pO0kuZH&y -+@z75hsCdq^0&_AsbDY6(xY5|U_#tpwZVx6zWJ6_7BlcG9eRa=c@+%5ibU!kIRaB>qHmy_ekt;0{HDy -gzGYIZ3HfYI$)>S~&;y;k8PGfM34YPZBi9w?PpB!F*SWmN_$ibhG++igPz@CDI0HYU0Zw{{Fd_xiP~# -)faKx#+8$>n%8>g$HYxx3BL)R!v5PG_unWmCt@A^SDbqEGs&C}n}i@D4+O{aRVZ0v5gTu_gm02I6$)V -l)^2nnPHyxzXZTUu)H8b0JW!+GecLtW(gqo8Sd*9DFkj9o2vavJ{8%d}{(`K_DL)(p?Y@D-iZxy0DWd -;A8&MsKUpd^vZ+Wk;ZoH6{6}dPp|*6ipif9Z$}pBeu|3NH^94jRlPU9rj3VztDfnFK1t8J799P{9C^n -Eatix6CLp(tM9QW5nCcn4^Gx7TN^uRi;Tb#gj1moJG?_Sjq_aQIJWm3&&c`Z1+{gi)UXR5;eSZzdOIa -&en*cm-LWGBI}YK22aW&+KL=or9X`^;JI)?^em6XNyx@l5Be>q1VjMqlTwq63FN)~-iLcM}d;&I_?_u -dio3)*7a?{4{4{?G`J|Xz-?-<1I#~#;Nsl+p_y-K`ju@9(S#P4)?B*o~B4K;jhxVVep3L;mX15oV98_ -n0(_q6TbRyDl>+Q(V$N}-@n#9sZX(tc3RH*ae9#v(2x#HleQSOW~>&ETjVka87yS5xzng3-M+c?OjYb -DNEnD>8kHnoQPorsR8pB@%_NEx=|p$Qy#nW%!B$+4v|SSheF7+VEjqxb9UVM@x4DkJl^8_Ow5&$@1by ->awH=gWBvzsm(^ovYGz;;yr#BoT_~ViKcFaUwX2`}PTN<*|mG+5y$0nQwZQXZof)=3v4^T@31QY --O00;mWhh<1_767gG0RR9w3IG5r0001RX>c!Jc4cm4Z*nhWX>)XJX<{#5Vqs%zaBp&SFLQZwV{dL|X= -g5DW@qhH%Z{5s5WLSX`D+tWPOARcSjW0Z^K>F(;9Di`=z6 -5}N-9i(_UsSo-q*%Y(N8INRjf(~L&4uKC^KhYU9rP}AfpyTGm-Wkktuc`8VOP0j_66wbdOE1`;In>%n -B_*fvTo!N!>(OQnM3a0t{bC%Nr~v+`EqU!pRz;vIhNQnqQERmN=zytmF@y!h?M3-?Pz|X}=`T6}AFIT -8(;q8#0p~)l3XDcIIVtwWLthhqVf@8%l~;zl1GQ?n3K{zo9NMqJH9?V&QPtewV9gnBO7rQ8VYo -FML0S8p?6s`J7jP`YA?M`$O_hc51E4u=0g11hDFQ|hpGp%yn+&)m+{HG*qm7Fyfq}a(tm3aG!n80McV -b8Qfq%!2=10AgD|PJBn@bW;Z#X~kdEQ=kbnMuy_rQ&*;>DTn0pXz^p`FRJelG93gm+0-Tb~M&;(8L+M -rK$4))kg@y*6ipp1Q(J6!GVOQ`Ibw@&a?a-v(PjgDKk(>yEt(KXPoqKYF6ab`Gd<^6%9){b6%lcJ#-L -!cszSxVa8ka&m2Yi;Ekviw>@07Xmq;rlB0W*u5&(jr@6aWAK2mly|Wk?Y}NFeAQ002&D001fg003}la4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mb9r --PZ*FF3XD)Dg-97zx+cuKF`>()C$s?6WbmF+aoLjfgv7Oe}B=)o1G<&vJp(w~=Op#iG^vCvkfBVf001 -_ZU+u3&Sht+#+BoY`5fcc&Q+-IX_BNk<|IKA9vWicPU#wTn2wLaU+R(E-PxhR;wH((ntUT(0{1!u>1# -Ue}DIGYzYVb0mxtW0NN5oc)toyMHAcZYiiAI1kkaa*u3ow2>652q)GZ+(9(3&Dy^ -f%F6*lFsAHGAD2>PUl&^l%S#g+FHC^WqE<<@<`}kS@kXF`kS~DYx6u?!pmS477M1Huflv5=R;^2Cir} -<$nUn-7w4`CSj6HCTxeDQ7`Y37ZcfLvY5={gs0 -(>d&Abt>o9je73Sk*c*PNj6B5p?`FnzmMDwZrAPEaxa_brG&E%Tr0`|dtBccUgavLnozM3UGy?~XN@j -07_aLy-s-F^JuK(1!B1^CH%QRXSL+tFLK8AoGdBa9xjn?NCR-1~sv@~AArX~Jd0$??h2e;w?dPTuT}4 -`4?WSp+}~h`!&z&IU0UtnoxBNc{$U7KJIBa>kdd;*P=aVSagi_TqvqfFh=xrz~28=_Q{LCn(;Dg@RS4 -1Or$Arjf_|8omRIAtG#*XP0@nWQd@15)GtbPOvM{&CCacaUtLV_GLRetd|x&LZG*bre&PWdca&Zz_Zc -o?TaAEZg}nw+^g0ekQVVv7N@?|cBmK)5b3kyz4edd{|H`oqI7<~iq6lkT`0YL@u%a|#BKNe{?_KbxY2 -^0pNGrY)}|B5&3`45l6_*K8IiEj0|B$LISNYV_r|~oE1+mm^oC6RO0p*0Kj=VFu)j$cbb=yJk{^FA@B-FzvI82{&e(V^5&Prcl)petqDMrXnu){GK1Hu7fA}9iZbWbTRqpyB -%1%B+(P*hXkJA~*XWeWXMpyV8gkf|QUUJ+m5^0l3kc(9k7rqyGL-uk2 -OkMSwNGQ-)e({@N26aNjL|OAv&x#loxF-^8Hbbn74#6H&l -QEd2rMBw_E_WW4v&!TW=8fNM2h`C?#FhP7vQMvh_U$! -dz4JsKPmj3%(RaQ>iU0g=O~vp#XTRWl_Tzu=Ep!745owgxjv)TH-{%2eDb{Y-+qh)AhOaU<13J9Kcc-hI0zj -txRR920%(4d^*^BpeZf8`nk8vC_JaqokiYjACisnd9?UpIJ`Ck7DMNSAUtO<=ZQMN9wL_8{i{Dufb14(A030u(54sXiTEK2L6DGGX_3`< -`;kqjkRW*2t>`=m@z0hCt?5F@7gxNS4djO50+vunpum4uZFAFKeRp;@c;m|pi=x!sHN3@`_Jd8PHuAW(H~*~wOop1h@SX7EE64 -i(L`!HuWD~<4ErQ=R99`qBMDGLBrDUJZ=!CS`X=6LOanjMWHh}sVjvbK_*-tU5)v4t@O#qLKv|EX9vn -<0ZAG344&70rnPE8(#p9LtC?fO^!9S?nGk@9 -&=O_6Df9d;i-r$wg1zNhG4!ju3=OBh@e0}e7g1{ugZ357J6I6$9a@Uld^6>VA_r7UGjQK0V(25iVueq+G1t(QJK;g? -|Kff>a440@8Ffr%p8dZNt7;F^e+aT21z3-f{=Lj{1$vn-jZ<)BCwEs7Y(u_oHnnB=#%L*Dbx!qGo=NB -@1H{(Uj}&&kE}!MS+01ONHKvw@;;ip2n&mn|51@BGQR@1H?$fN`PzB|y&x=gs~b&i=o3bpPGp+sEj1@ -H}|^E6s}f0X-qRRi;CNY{gT9-gqjrIy)X^bwJ#444jLdo+#nVWms@7qHx7~6eyb4C?w#yA`Wwk645W; -FdXm!nU?s%vJmh|Ai(q~1R~~peUAjAdY;ECe?S`Qp#Z@WeG+4n1Ja*9!m;Gjvf0ITd$i@A+Rn3-tiwkLN-I -;+oL8KJ)z=Q2skF&QlogH4i9LVda11*dlXo^WFmBHOi6d&~o=5|bR?@SqiXq^1c)rRK=|Q7wMUBi@ND -_;}SBt2u`%2$`tlANagvuVbQK-B^DGrN_$q1l?Qq7m_7>X&NrUToZU-MLfipjY&nt(`_Vw;Q=jaH~|3Y3wCFJ}{T -#e8q??YrF{$GBVX_N%XZ$H(%!TE3eF%4iTIe${!vzGp8CivqY>o5M1sShsY00~N8iLPJX(8CTLc5IoF -*+oko3^23z1_S+`EEv5fmG=!@Dq^5Le6YCr^**ZMje2ESKPIu;nOfA55!njM3`--?-{Gj2e1?gf(u+v -Zn!)AvJT_OJW67(|55k+Sgjl9gHbdTHR%-2H$8^(py!pI@pY#t^FB}ydqQfIUh?pp;jC~&cM4?ZnA?O -5D7^Rc0&Uy!E(zavsx{mGddazbPq#7)W04gu!dkI{H`u6<6Dm$RwXZ@ObVe*GnMSGy- -aQWiJ)A~z;_`TXob=6rSig^^-+H9c%R4~I1p8bWzxXX?rET%+3uWpBMRj%o>1U=Rf=~Jim(9r4aOpvU -EwsIC*h@)-BqEPS)5N4p?mVt=&5)rJoc3Ntvt?R<~7i;}L1eYlkWIlMyjk$H-ed)eqJ3&Ga!tXwN-6yX(l+N&k?D|LxAdb#2Y8Um_|+UaFi%HGC(|M0{!b7+q(YNaCYRXI&yC3lYx1^kwK*5uLEz0qqeIsP -4@*N+X$Qgu%|`rMZ<7)5`r(C_mB!{GCIs*C~ft@ykRVEydb1?$sGHxPKgeqLpNWS|Da6?XI%gSV?Hdh -6R+%&ed`wy>K*!I7#dprkJ-cdvI(&p9y|~8!b -tujI4A5EyKf?UHFI7qJ(+JHpLnt^c)Z;xP-`8HN8j97?VPMuRZQ*H4M3%WKg*w=KclcTtVvy9K(PUo!xPbBp->JMr)u#^(}!$i)U740N7vA_D{1)1B;;^Bs6t5>R&+2I_0Hp8I_~}vzA{Yb99pc^ -#I%Ymo&ab;~J*DWdeg#5g4New>n_f_8q^~;C_wMS#~4n+{gPcVn<^jRu2<`%Q9LpvBZ8@vs)+> -Xwt43BRk_<$;1&d(IevxtqL -Pf0jkGs-#^?r&1PAvv$JT|4G -N1}9mF8J;-c(^Xxuy6GQm3BuOXj;ny%&CDC( -gH;Uk44Gz>PT?2ATlj5lbVT*W;tZj0XE?eT;NlvKIXJf8~9jlNRr#KlPW56p>~1@qs-=G3e;uW%3>-rPPekZOjcbNOwErx%83r7r38H?U4#rqUXOwN6K7+z|Du0gCh+>ijunKZ -<5dNo}2dIuJF9>CO<1H#{0ufa%^a{5ajq=w>E;Abm2{x6x+S&41)QXGGAF?9HfNJ>)Lu}GK?nkm -mM6yk%R76v|(&3uF;KDoQm8@f8vr=OYDH80wi_ZCt$lmfTdwP!f_Nlq8bO7Y6!vuL)RKdO}9a-muk>( -L1h(d7oRj?bn;#MYze)y1w^wl){~X74dHXe^OPrA!AjVr^w&X+;_QE>7TQK({_9{fcx5BI48LT#0Rv+ -iHsGK-Oo}IV+NEMPgzwmE7h!;zs!Y>Qz?LvC&R)I0cWBi6c%(IL0OGx76YcQR9?pcO6r-Am1&HN0LH8oVH6rcDb -pTb9b=FR*r;bh*`N!ZTAhqVU}?FWf=LfU%y%T@wbYeGjA+#*v_1xk84!;Yrd)R)35VEU?$=A -9d)wqbx<*Gdng<;PHujTf@HLsREDa;#@pGzj=Nxa6AV8GZKam_Se+J&i=;Ei;HKEfYHJfNo0M+g+` -wJQ0}i6O!~dP6^*ft{`6qx(L*}VWQG5bX+t{FU#-}4~oV@*|IarpgACs{|GFPt}#f@Hri+l?VjxYBx& -g}MP4K%tRgJ~KnFqn+cH~c@2}%Hv&TQnbtVk??{n;fMN$B$vr9LlImqb{1CSmtO=i$AR%wS=)rAV6<~ -hu$>55nzib`3~ao0-FYayPp@&3<}ZdE-dc-O*JG1x~e%rMU~1h&QkbLu(;e@5~?0>Klr61JO5$ZeU2v -4bG`PUXd=)8t7Rdeyc@&Di~o5tc%;n3&jUhCUae -$Q2Vct@EzW8z&8l>}t!^KnOP$XA7JXAWoOv=;W%Lr!of)u#t*$V+gz8HA$>cYsqfx*lypHfni9Vev+nTmNFHKH3asUc~Ex!V@CorV5L0K?v`;z-6%WR6lI31(Jx-xjJu?T=}-Xjz>; -Ua(VwJs3-o?A4y5H;`f@Oz>>RueaSWcKas&eu^4RxTO(1Xo27mc=ke|6o8XoCduwyF6>R8?j7;HRhOx>vr^sj(65 -#)j=q4}bZ*5M&%Fumau5=X`?CMp>ZeMRrw!p7GObedimxK3Po2Ns@i)dhiibE%)Ls*))T$)W^Z2)9Ii -%P8o8!=6wNQ6T>R$@^h+%g%ysj;f$~tx^{=;+z)NiE)v}b31a2 -RAN$Jt&~*$XNUC|E`x8Jf^OVcLKk1@%Rj0{b8NT6MlYS`pUo*$@4vVJ_XfQoi)+A`W^3Go5>7MMlwDM@vUnF=kjzOoGRZ9rW3!PoK<#?@j -sf1N(4_q((H3Fn5s=hXw$h1{Li6*6S;5a(ui;Ob|SbIvCh_OO&IU!xD2)?AKYDlf1w@F7?QSOrS3^Q4 -F=aJJh0RSosEKh)$2C!|H|*$FXUDBLbtsTHd;cS2E@xiUF?b?PCx=0-t7?VR1QoGu~f68iRZ!xu1urK -<`Ku#xoyzp?8d+afvX5FBYnjM$Umyq{O=-u==dzx?w}t6H?#mW@t2dlZV?8xREz7$c2tZY=3h>v2+r# -J(DVS3%rR&(AhE)`|Csi07e1#bRoei&oJ=-lO2r=0P(BWTMZ&;(V8SBHa^}xkp%NYlVDiysTj!$U0-j -!LO@qzv}Xg_AQQ%H;<+XhOo4t6Exze(8yweX<1z(u_y&8ArCMPd7E@Lm)>CSy*Wg(KkMHsB0f{e0ck6 -KnAk1G?^m?|jePO2L=U~)&Q@g(LYfQApLwwJq0F|BK};DNihoz;73RwfC{=BP$5zl{g*9o{l%OHr -dcV7OH2yT%KRAAO^w;+XA5JHyyC**$kQ5iZFaQ-z12Uv+4r%Nq<`G0lN#ZE -PD2tsMu3|ERU1q8dN$mp(NGxGTUsYx;Z}eGh*|`hnT6jeBOI2Rm@;~am4Wny1bEq22phOpJ&&~tc1+o -&Y-Q*^`6POD}v$oKOkR&yz^FTW5spdayfT&M&&p7CkB`wihoz+q<${co$!b9kzNVOf<&Nu)!h>)h)80 -SasBAtMp@K9ET;XbdZ=!otwm}L=_67g9B5X@LPg)UO`u{x3bPSsz!DD$j|54QP{V`vE|GEBouoNWVi^RRalbPT;nrEg)>6)G}~Xw6BC~dh?G&`|HQ4iQX|**IfY -ecw&8&#_!T0y?wgp_6lz{)`P0sPgU$k>^#lM+@~MICV|p?D|B%?al##SESMjYm+;8;$mnLAo0T$@4;G -Cy)l8eThV*J2m{XNFHR7fYK-ghx(*)*XJ`8g#!&`wT7D0Y*JbU(PCG*F}dmW*=x-j(6eYXnMsH0sw#Ogby}Z -NQez#fINo+OoSpU=SdHbmW3}RqE{)x=I_{}_ka>h#omr`&I1ka7VAO4V#twSN^3U2B=UX1?QQa2ix$n -%yKM$h+Dg+6(39a>HqJ*1T`L+K8P)h>@6aWAK2mly|Wk{hzI9p>`007fz0RSif003}la4%nWWo~3|ax -Y|Qb98KJVlQ7}VPk7>Z*p`mbYXI4X>4UKaCzOmeS6zBvN-y`KLuAlIhGPxYdJ4Xee2%iBu?t5j-AJLH -tD9hDkae}H!`V1QgPhf{qA?(0OF05oV4fO^V4o)i3A1%U@({&3}!aBzx}hZ8C7v{na-2J;&x;6kAe6+ -U)&by?5c{o&wJ7C&dzRh92aHuYFSosGEU~v4|DkaUo-W4Fin1hdI$4)MD@z3NXn%6oJhfYao~PwiKbof4!gyK1! -xA4(k__r$=l+@(QJKu=3?s$P@F*DA_IVhvxdo1U70Do$+{>G*{2FTjjJkt3E0@b60|a2)sN59(D|3Jb -IQ%1-RQ!~hy3FVE{04eWcx2NQdt2_&Twg$ec$|MusFzGeQI=QG3&sm)j4@-1kay)(4CE7y5icH9Km?` -&pGse`^UE>NTvnwpQH#8wc5w(0E!7*Qxr?6;qw~|37w-YkilM8 -`+3k1hc6#c4#%3cRDkbASObqt}OL&wqj^2hWazgb_~7VuKYDTS`ry?e*F6PjfC?66qyfgepAPvcG=BgX96Y}`Iz6E-K0iI -VID_AP7=^Qo8p6Ay^F!$G!P(I{pbs!!o}EHVIP6dj2lfQ(o*XhLoDzHLpa}kcdyZdmJPCsrhX=<1?;N -X}#c}R03n1{(=yF*ti)1v4(rcuYXi=nD1y3_tyBiywPUkF%rx6uL%DgGkDnTL#jtV4RT*p-u7n7^>b5 -ahl_{PR)^cj{CR0pv8QRhF-lFupr{1131ZzBA_yPd)JgPji4h?l?@3aEfjH6Q(TxwDfzLSEYScxex-O -@V`9C93uxFW}c6u;SrxduMlh_i;4*Ztv0V-p(+3umcP_>b@+}egw~7CSxkri~a+lOg@+Cl)xaAqv@Wp -T|`fxK7RVd0{WPN{yn;ej&H-Dz-pQ&*W+a|6L>`etG(pz(|+(dk51C$I=M}XK7N}!{p&|(DRT8`bdlc -xAIqi#7_3cmzd51_D$R#7XXS0FzUKNHG+L?eNl|3E`cb9VNnNN|PINJ3Me2KzY;07;?cN5Ctvs6*3t+ -%E(~acIBw18~(P2^K1($;Y&|@e8|DNO-fPfvT9F(`$^ECSu!L+|ivuS=)BAEf70xEhk*olfbErD^J

JA?cAN61#0**OOxNTse%p51cN0S1WI-mm_H -@eE_Q`!X>(0TH_NPx!9Z?DHW>^R4O+M6Q%43siUuuuGrO8b~U0Mt!X`aLy}C-HS+4C4YeXB;|Ob3%z~ -4({Vxna_!|K6EbP;w8KUE{$K0vdbL5rsYU#$qRzr`M9wG-2`G8sO6bLg%3L)qaUJ&)I!$>MUhVu*vUZz8qf2|r*h-$@cHT43#nd%x( -}Sjt6e0MyqK07uTI{MVDCA4PRJr(DWb^0c@sF&ES*HNY)NB-5)z0Xut^@hcPsw-Wd}YTv)Upr(=TSlj -pM_UR~J8xP7YopmcyMLuZ!35m-KphZ6F(y0GRVcc572{C{P^T&U3wUVPOPsBOCp^nYBl#Z&VPBW>+J^XmBM5 -Qz{c~R&UE>Q!Sb3gqjHf)*bW&^G&rY$qVUBdqFn0dhoj1ro9XUoICU#qDjJzkTxtbb!MbBb={ulxu| -Z^rHj)P1s9+!yl(eSO_R2yi)s-T86>mYPIzv;U)b@I76_wyVClRyO3I|@zUvS1ja&%UKPnwXo>))Z!* -;Z@@&-c+J?q&yzbx{NT5CsYqwH+hE1dcB5U3HoyQ`G0PIGk=uy!07g>LdP7Wejfg3~|i69rnO3EGZk~ -um&enHrD7IAfjk{$9Y^}S4gCu#2*#ruvsI-O-Y#kVv5vqZ*7zv;^w_~CHHmzTiqVZz3@(bcrT{iaMR# -tuiNMiCx&!)UrKn@yt`BiL}ZydHzZ+9xYV4zpG8-VSS&-W!C(f=ek#y?y=c@T^AbpEN3+n1*OCx}4{6 -#Us_31fE?7p5Zv|0jBEr%=t~zKo2(l{f!*y8EBAb3M2atlIV3lO}OLz=yN<@CNyOEq>AT*Z)xg$boBC -=_QjoixQkL@&5CUy1WUsDs6G=D1x1g^jUy{j^tRNTXuXKgoK!sO+g}+_AmkL9tf-?F79&gVM4_ -;_IBD+*f3oXCNV2{3d`|j-M;!q)Am1KjAQS|Wr;d9-zRg@7-{#qtzu`%krsu6)%2qH2o^5p_b)`;xvH -Xx#k)`-A7tUyHSoEnJiJPMBpG5{!PAhNR?Mnn}g5ZM{7K!odj+tN+56m75R?_yH_dRU;a=j8x-C9d^u -C##ToPJnZ=9)5W8{FKbB`@oy3%k8HfLrFJYqSil4Dl+ZgmKB;xCOKNv;~i0h_v9&Ld?u{3hX -PW13t7AJ4kL@L8|xM+)0MnzHhul-_qbo#%N5TPN%j!urvs6qBaDU7__ni+RVY&@xUI`hlv-W*Quv0l` -nYUNcmrNUCL#MdgP*M(1PnAnJ<7xs51x%U%x@_mj8Bj66t>i2OQ8w;{@1;cnSTF9_+&G@EuwM`b*LRu -<7;_M`3Lp5Fecdmr`|AFZAYrb$D81^=|$fGuvsyazFMi`(i7PbcVwjU*j%pxA&r^i7V|g)$w36sB4&Q -P^^z_JN-&Yg-C5&;vnnp5F{q2TL?pK_4eqMVDA%ukNe1_^A)tqhKfQ%6xR^&xvtFF9ixoGJ>rfp0Thg%LLa(pHIeOd|4_fXt@CJ0;K!&{{1e)*dF$J+rxnt?WIx)A_ -caqFq@I|NaAKDKuGcgYReKd+Qr4~AUc4B9@7yf@4$!*%-uAd#1-ijfEDT!=>(-9fm5$9l;8AuV5IPq< -xawNG)`yRpqQr`iy~Q|tRM%o16WSso=4r?$4}f|s|B+Y0qp<iS)v0-gS@87? -V-htGdUW525G_X|y6<)=k(|lRbQ8Y~P42Jh}tz}L02wd>B_;9Y=fjSD|HSA6v_}p1q2^or8AI2p;xF|gLD<$BnBF|k*eW_Ep3? -zE`YM`3_Y<~v??dF8S!F#fb-IID0cYh=`)~F!0y8yeE*2+-6`_$@e@^K`1r}*zG8J3k2EBEn;(%k(Px9T8)`7+r;q;>?gk&M(&fTI`x(WR=FmUQmfXV -)8?PN0FbAhTixU+q~*b-n$4lYeploFW3cE(H_^!qi2vOl)od1BQI4m<&*3>JFdWAMWfvve^Oh%p8yJF -73KhAF6$_o?>4~ry?fcqDjA>y?rT1Swmp*5w=Lr`LL@GWv^q{ErH(mVR3xE&Zk`^+e=l3iws~~vplYD -usf%7R^58M2v67({rTbD3-;v(0>fPAyP|^PvnRigiuUnHx3gWr(hU)0HzF2s?YQf`G3m&fok5@h(ubS|9)q=-q3m&H}c$ -^9zf4g`OcvL1H^hiOxFy5Sccx;GTsHmxn|Ae##ECAa+*(nX(RhigPI8;=-UEF-(|7$M#wq$8v`3KC?v -JzoP>%E{Tqu;|v%`2F&fnV69UN0wNC@JuK+rJhWgfGvtCeL8N{uC$Rv`=lM12I_Q+LF6j7koX%mM#NtAkAxQ(2?RoksQIr~N%`^@8t=W@DO^UGGJno0ri|W<{YkY6&M}l<(bPy -nsLRJ}Ifho>*^im$QLwEzy7U)BI_8)(8bvK*Jme=k8&yCZCJ(?zfAY^vLF4%(dtTTP^7Lk7htgZ0T2$ -GmYpvlpi)$G_M -rJZy>b_T3r2jsC^Ip2MDcp?+MPy*+te*ZcPeZ0MsYv|Ys|9>{icG$fvmo*kVWoc+>q6mdfDU;7M`i_{ -u7^lEU4o{>2_$93x-q7MG#$&+3^7?XKkCNvU86Hu{!4zP^!ByRG+;SprdRVJ{mE(2AiSPg(BNP%FtS- -;|ndV3l%OZDjMIlUz#8Xa|&WbXsLdKxd#(l$=2neyeI`Nirof<5m1QA -%cY7l7*M?@5$c4OSBX^OmxenYTSU_Js?p8se1N%xI{ZNaSEMN~*~~8;xY_5fkJL#An#XN^28GMzfIl?Om2zpL&X}5`CrZk{0g -J6-$1svjd_EAM=bwwP`JZ5wJz2MDLUOYA!SN#?yB3Q&+S3o53nPWe5@OCYtFWdY{7BDz}%MQoGub1sWPv!U@-S7Lxix-D4j}I;mUj!W~8$a7MfY%4_NAI2;U7 -X(yf`!58_2KK$@!{VNkM9O#r=D~4lPjy3yKNqJ{eZ_`cmj_l2KZ06*X!A%H5!TWvh(ZC!|#6G9X|YZX -Sjp^b)p9b=m+GEzq9-6!|%T9SoT!C;SE3xDsi^DodK|W_moWUi~IHW4}04WYdL>2I(zwi`2FMEsElt* -JbAfZmK91=(b4(o_S2`2zuz9}L#mhLfNmU!dwnu7<1^H>%mobkAr#4C9-}i4pQ)}Pjsvm1Bkv|Lt(EF -%iv^7^RjM&+-w%y##GqZ;OUk}}>&zZ*)erW?38uNhzd(ak?DEJ`H3ch1uM!iEJ(%WF9)0BiA!;#R?gb -8?B{wYfn@FXs{@#i(jm>~`-Ll$|wZjOM`YE5w(1^(UUi70S6a~gzu~UO?=xofa_A$U^<-kFew_5qQLY -*W{4W85RIPyD4-?iZ@Dz8M1F%g)8e2x~e6gJ6A5N~hdVp^^szyPl{Pm#>BdS|`q5W`=Kpv(zTgmpPzm -RH^0nk{%oVlC6tI3Mmtzup4G58n*W*d>94K3tJ0sn7bb_u=j~8mdvLnBjfW@v;g4!dZR8empk?TAd4& -oaX5ytw1-N#AT)KCXlI~K|c=w!fP=DygA>;>5f(NBl@f+}BpqT_oYzl@#A3ck4?{lQa-%~0e>xE$ZZv{m+=sKy6_u-3DOMtdbPHv+oU%LtQTnW -soiFi&jSHdM57xEDQvVxC`JY{VGz2)dX#;XSK~8$^8Yi0b^{AslWM(ut7JY8tlBd+4%hP07GJakK1e1 -j;fGD?j&c_h-c&bNCdnre&fxj}j@{~cQnfT$k*hFBAj&fjff)w~)PM3F9;ef3lWd00&^bEGX~y~EzUw -2{;+$gF?OWoTJ&lf&p69V*%}i~A55lU;1UyH4rJwnNd9K-!v5Lm9bC5u$?HKG%Yw(}O$#c#UOgAZ?Y% -8{*Y8uHsIBt4u{L!ud5{PJ+Z ->AU$P06!p)blvNiJrBh=YBB-yTD4{0g8v0aRyl3$~JCc#5Y|u@yIOCh5eBE#@%CrdTYv@VIwtpy9nM& -=QXRm4UT)RQz)NMxAS+8^S%Li8O`S1BPGVleyliH+bCUlfmHl3xZ*1aRO=-fF(i^u~l+A98af2+6pXj -7f1?b$<6pT0F6UfG>mB(G8~+>on4wZv0& -RXvVw^5Rom7zY7_W%ZAVp+Fr`q#w8i~=`0OMpd -CUCQ6#fc -3vMav^Nr?w6Bcy-Vg>F*P@BqB3gVZF-$3YHj`jsYzfU(1UmBMqb%r1d4SggYxBD72r)*2NJ~09HrX|9 -PpV_EZGBFs=ozP@(BR!ZT0874hj&Y%$WnGNRnfh}zLKrymzO2_lHmgA^WK1A|6Hwp9d^&4(B?;9?3c3 -P_?|w>ghb5Qqz#QrSWB;8gSL+M6p1l?G)=mu1KM@GU`sBKAAFh`vv0@WGM=RyYcN7Yb+_s6+bYz) --z?;LNT)IuYB~w9$hQ9Jdb*1iTSj>=zVf%CdDM@x{Q;d_5A&zW$x -R_kp6cz}}J6U8W%Yt+yv5wxs5Sw!BUUQJ@y(=~DkqDe~C?N41)MAxOsvM3h)TFz%{btv(BgSm8sP*&a -23t*fiu-6zwCxOrDi{#PxaxJ9b(zK*fp`LlDdn-yMb&|Ktn3D);ru_+WFbj~); -l8-#$D{q2eQfneUn61l>;tmd7-4~D^Vdf%_vtRbqx(|~!rp$(Jk;z!+=HKz;u=g*Xb503H*Pc~X5nu~ -SqVch2N4V>PZ?go}b&8RvSzTnsj2n<4dUwY_M==Of_m4)R4uZ(HQ@#L9}4SiD#f1Z+P;R!kT%JONq&U -Z6)uS>4|*B3=fY23cmC-=*6q`E!(I<%#?wyCTVOp(6ep4u$Nn8KS1>8-YbMiMkW(tzynBU-C>sYeugr -wSBL)DgTzn^t8gs;N~#^l;E{?dSR|pPi+xcQVi>rj93oCAe8{#Q16)u`7Nb+KbIy($ZO43LDCSbgDZ1pB~}ZbsaG(>2{L?OQI -LREssv$iF&y#DbNtb0GJN-g(jvVdIn=bc4dq1=V_Lhw7)$JP(J$dNvH}e-$M36=jM>GKX$gQT)*mbT; -+vHikVtnrXEbAL)$o8MA7?eoUs0jwupIBF-HHBYsFCQn5uNBwTxVKhi3a!*mEt|L<4S?-0)zcS-^H8T -l2hmZ4;fQGFcRXvm(J)PM+vq8+M4a)26fP^+&>KB|Iv386h7H)7W{L1n%F{;E_!0Eq^C0UJbOCBWnX3 -!SPuU6!2|~J_kY_-ZwO^zx2&@TQ+Nmc;--6va&Fx6nTu`Vxx*pdSbbpJMIrnq(~F9UmGilj|2_F_+~G -lO>k>Ut;bScuer#dq!s@;+~F>|HnO!OnrIXFK&{CRYKc=7G3_n*@*fXc|?!Zq*>7;6m45ZBLj{H0 -5s-RT+Hv$=-`VLyL+=1CoFCO~OR`n@?kd9h-o0At+_F(t1J3;QO2FKcea0Y -seyc3hz&D1iBDZ^+EC-n{`cfE -ybt9(n`BFHR<4S1RX?(&o14QPlCa6>i{qq3*OVo@gYW9gxQMc){!!1EMh5fnpAN*FEP#B630?uZtLsO -48CU^WfP;csF-~yX6WHnA?X&N>VUa}dNDw$u1BCVWfH*aj_8}icrN7C84S?U!>*^4cGtoI~`}08^+%S03@?@h-td5+3F(B<4a${d*FtWzczrwF*n!HODe9BhjkNMImSP!6Ib`~njL+BS) -E5+#MrV4n$FW9fxuZ3$WSc_grExVdFteYc>6l!ojc7dYgtD(vxac|hEE6a{VFU;BwwQ!~&PK>A8mWMJ -$Y0cFg{h?+C0N|hhrn*L^46~w`4~{aU054N!i|pv6`I*F6rRb!X{BA1sIeSN4n0^jw_5th!AGI0e^kh -?o(x|}v#x>g-T!3C+aXeTmP7eQmeSYT9j7g_bZZ#7~uVT -|$Y%RkDW?uUD2bgw|e$-T`lKT>dJLS!_eEq^qEZ-~Wc)Yl`}PuWsno{|DClEjUp0;Zzul_K2BLqy;7Hj-%g2{Q)1?okn}9Ngm0hMSzjWPb30{3 -KG=&aPVE#T(ZOhoBI?*h^eWu53C!(zJ(4vucKhy>nFY#dsbtUQy@6sRur8H6!N$%}ORESj -GI2EOV-xGQtjT`!Zbr0Xoa%d=l5}wkHWej6M4(g5acL{~IuX8Z$t6Uf=)xmzJT*8+bh?Pk>i5E$y!OC -K64R+Le%K1oP;9Vb+z|!D3QOnS|UdKVxVd%_EY|XbJSQs8r8aCM$x(GIqal%$w>xSy+>?@m@Wy`N@q) -zmy$WyoCwSWOFLW`0UYIZ*M?M13?(NNw{&^_j$P~a;YeAsd}k{PjP-K75zp}Es( -(B?N!CdP(0r!wL|kdr1GoHM67HS3Baw>!_VdE7KI`i(Pdj-ClHke{w;G{1%@)<-FX4kyrSL8mr|qc>S -2vD7?l9a1qkOo&vzcD>V`qzXjmjLuao?N{gTz|eenmEb16?c@0wn!_d4of9Ke6P*c%@24*plHuBv_V3 -PbR2f^PaJ7s9#;-TGsmSUEt!+N)%fnp)H*;)3_#9}-H7Z`I3|__<` -vyi1{$c2KcoBp$rKi=ce>z6VakFJ&w+288ieAGZht#Dva@Hw%kZn-GTZ=?Oi1bG%{fm)3$*LrG7vE+n -mOi994c8crG5A0OgEU${s7X84k*-$%Esf9Vm*|e$6oUc!eBK|yjbEXeGwK6Brw7OPs^ -0@^-hv5lA1!la -K>(P}baBZAq+N`#NG7L;NqYL$Du8XvgIo)e7`v4jnLhv(~1IBWiFWFPiq+j?_$`10sI<$=>B`4mjb -BW(WqVS7jk^gz3I+#wWXDYpj&YqBwHb03iHa-p8#=+ItYma`Osg*%r3*sX%5uipf88)wS737#Aw(h_f -GP8=!?l06GnYiba&BMPdkET+ATg*Wp84~<4G!KpSm(%?8DPb@h%xvw&)>7?WhL_s#^{pf==c$h+yTsv -n;Rf|O(*f4+?16{Pmp46%>2WdOK+Bg^Ke1fetc>_bofc47H?pR(nLaZ>{U8{!LEL*FL_1OiK+%z(r;e -PLF$P8G{3gz_00cg*LDTS^*BXti*vhSO6eQRq=v#E!>D_gQ_&+b^;9&O_!bp1)Kk4#%D*?5YPLXGI-$ -8DynkFJ_-MO?c#Ez#Jnulo0K50(5ZxYmqo0Q!*5O_ujzTUX!vNcsNk2I=1F_QNNSD4lHQ-p-T!*Bxw? -VT95y64Zx}ziSDy(a!4@@h!+l)2{q&iv@-Hf)ulI>+z*Jbd5Hrf}`>1ctEvfG%u!2C%^5f!8DHj$cVV -0$;C{;TnGpGS)$rBwMdDic+sdv}v+-kYvN+I|t5Ra|3bhBDgcUrkkKmK&cDip*42v#?; -1=#?v)a|MTCD8A03nI)lFs{`z0X(CcI1Uaqx4+5hcxW`0LJ`lEPo(x!hGGuoyIl%Bl(fcxoM-vgrGU5 -tzX_E%qlvE)czMBI_pv=WdzT|wBTng(hiem@v)@9yk8tw#eW!IqCh^g?}QMdGTm@KF*9bB6|51au_aE9quyEy;)?tr5^?FJ=x<6jCWy=J?hCM?pKw=xSP!{G3$#oLGf2 -MT2NwWRRSE{(J6yQ6RNKHHfa%mfVo5??gdh(x3<4?kzmT -x(ziP^6xT`p&5|SW)uXHY0r>einWb4~<;~@`ZY5Y(61bDsRA97Z}vEQZhWEsa-Y -RKRwQJLVSTemp`S4aopbUoW6yshJhwt|Xit)4I!{UpGhL#6mqY4a)oIRo-2gdGu8hsqhs9&DZ&Ojtp8 -WGlo1lVzu0fcVeycunVb3zU$OiEzRm2|;skdA3zX#FQKK=V^wq=2!TfmDmpiP8M%GBAD7{E~+O|RRRVTuj^NQ5)!9G6VQGCGnCCrWK9c9}=MKCU=oY|ar$v5#AP8!w`Qvg~3 ->GWWlTlPUChZYfHmM{TvMs-B``Wny74SUjh`*e-L*KX7@V$FMX@EB9u$P8QJ0~!5q+^PdssI@n1-I?b -78m~N|KLKsd@MV<{*X$k1x2Ix54CYvtEnd=QT3G+y4BJ7x1b`hgy`LRfSOfdZel0(TfCdu)cHH!SHgJ -p-Hp!P+)y1R*h;M5XP8z_DpXASEMp~iDs+9Do;iIQ}!^clPS|~^<_AhBlRBodYNE+A~$5XA_n@I5OVW -0%VKOCLqm0(@5inp@|iu=blJrgU5zpC!f4pw{m(JsUus&yIY^YDpNW@eWeKG_@YK61*4Q>res`)F_Y& -;_KtQFW<@PdE#=RjO8PuPj$nd?%i!V@b|EeEi)<>%HMBrv?kk?CnCiRl~J(>g_&xytn)P>UuM`-uK__ -J>30%Wj(_KRgDK!+k5!%yN`ZBaXQrnA3__C9vcN~1FfI|ulF7|P>I1cPeJJV+jrz`BnCk(hOk8uI2gz -)w`>%bY>TFttRtDn7+^7-Q}PR7b63_;L_K%=(Sh<4M!{QAcbkWnG4bN$WgVR1{sp~2M+)(4m7e62$#R -hb2c#hDvehlKrICG)ePJhwy8I;fRwx%M*%lrtn9OsRg?am9Gdjnlf8@z~m0VM`c}(%EK|M$~H77u3ZG -i!)=mUX)Q5yL9n}f4+ho$kY!G}+uIF=}KxR1QJ!cKJPM}ecM#ynTrTUk*X-Yld*JkfCXqc17p2M;c3v -$1MX4XJ8z4D}Ipq_(u*Mc9(g>+n4)xRAmnAk;}XAxRRiKES1wQVXml&*y~yqo;0XQCxTU+@q%t_a1%! -*ewW?>fDLI`yukRZ#3A~K&@4ImR#qbQ6rcxvuT`FyrNWC0JSJnlNN=R(h}afN{q3E==D~hKCqzGX16> -sKfvQ4_*cDg1vHEV)-E{7B@`B(LMeWB^FRM#Exs5i7Gdk1y3}Z;a` -jIRZuyQ$;xUG2w_sEmcMh0SP}n^eRi)YIG`tq>l|tx~B`H|cy{w@Y{^_zwrsS?ye -jCP5YcnjK=D+gr7b(wsCaxQ?UiT;=M*C$Pz@%G_6FLQLAREssJ*$f1zVqO@5Ds&6r&`-|^2=-S4^!HL -By6}vXemdG{hHKvM^ud;Dzs(sh@0gDi^3NCg-yKHC=gY(nlgR`Uak-S;i3$$+rTJjvCy^7;yI0!nOE;yrv44jG;BgrSeQb=DXBhX`HeiHC2t~x1dGeRlG~f6=(;MhoPFCW%w=niYz0g+s(Q4Qme4+@g -{~(_w#)OkZ{D1qT^zm$ZJHoreo7ZMhcWqrZO>C-#FTRnm8oxj8>tg*j-W$p8j`>pZ8>zdO!f-}!FMVL -07fPMVwjtv5Tv#FEgeqKl>~)|eJZhXPjojogzXZUT7zp48HxVPXNisCZD#S!R>F@t&N<6ObZ4>cl4*; -85S|}P6nmMd2_UwtE`yLu4Wn&xG*6tTN-Ca=BWjLP_0%|@8Qu<{_ZD8szoV7`jg`$Op1UhX2WHS>D4j|7_YEf`<39uSnf9%SoSi{~23v` -ofgzNFyxg>pY?JcxUT3C%c}Qo%h{8rh$ch7G;ARZ-HOi?)KLD^HVoxPXD;bH*!x5D}t#; -_I?nK3k}f3wb}Ts}|tLjq{5iM}yxT{-3DA*KvsH)}CO{5C!zlRreq7n7y;FPJ0w|cTvIHa6J$YR_)p5 -<$tvp*4YOa60&C1qR(2+_CbW2Cgr3^$8>4J98c3bpot|=thf3cx8bDxn|8|s^W=w)SWDyb3iQNilNMK -!qN3vLFSU$ChRv=T5t7_ciLeWpM`~RjG26UnaetA+U8f=QLQFs$Y7x9w)(zaJ(QXoz?RalEX}G8Il92 -={x{Q{kGi3U_Iae_8=6QVwo$czlzIo?npA5|OTVN&?BZU&dE49}K8VKHAyxe{&S4yUsa5e&IW0Ke|DV -1fB)@nIx-vmao(9vPJI(@%JqQ*=B;^-jmB``Ej2PtpRT%u*n*c<2PB}v-xeLRsN+MBSSwVUK+ergFn6%KLntKxBxa{Qc?#KogbY -=!<{FOoH&lAm2=5q^!|4Ywhd4*+Z`Bdf$`3T9%|yi)X+Ve{nwv2hc -!klN?uLM&MFz@4#s8nZtB%8r>`R@0A!q@m?gsp8JkxOK+ertFdllZ55kprRqSJ(k`s5`I3+)QJW#KUr -t()fd^*JSsW5caiF~yaG^6LAVG~`>YPRFxp9GJOcA$X?I1>|qrl?n!s_Woux9pQ$?O}`=L$j;#nLndTr^9{B>^hZ6n@Kk?2Nj-21_Tlmku4CW>qjaw{n(2DTLvme6ve&F8BBWg?GVJy$=+)?Z~|5 -!1JLwDz54YrB43``T(J?2ooc+Jd0;joE7mDbD*iBDAPs20;Y4x`Hj%WNrlH;IBb~TSS)nqrvWk<2T<} -QMmi%a8XYwLupbQvyAP2##f+oaGOjY*B+B}7s{>3iAhQ${lh^aY%JZ~{slf8 -jc@ExL6VEouZF)$pinvd058IC6HtZ0d)AZRy<1MQM>jK;79i>Vuf8EuvJj96oC<5QCy%S6oW$(}+hwW -~RSb_C?VL7BQqbHz`py)=;pV^9TDkVh|%4v$})`^!X8aRXZP+B}Q)blM<-tB`DWKWoQjodF89-?%Z$a -D7ycZ=Q}<$H`x1=K0@d*AX4UFdGC#e4wcglo8De^DT5*uZV6EM6H(C$NQh5ILc%@gQ2}fY&*PI0d|I6 -969?UVBH6@)tI=EPud@W+|D}4?fA%vjA&!%NjTPM;(ZtLi|PzHgjQ>Dy6M8j-OTT6b#1s-*M`Ba*#`q -c8V$!6?WY#24Ar!Uxi@C<@{VtMb8XXiv`KsLf|f{^$97f|8h7*PGT0aKNv!2Ome_oDwu$?8marBC5?? -%@EO{eEF`iMiPXYrVtYCG%FL7;ms{23kvg=9Dsf=+jLciBO7;&0>AZ?0~fWC*rm9ISdXfdH$D^@P!hS -zP2T@^(jcWH6mewb~TwiHIz@%W(|2&_P2Xln|VjuC8?5^Ah^uD?&xwN^wOKMig|8QM^6wN*^R(qXqpZFPwCLPZwe?EG>Is^Xv|rX7}4CcGz(Gs4)J3z5Bw~O1?OJ;e)$-ejL#4@NTEy ->3v)^1pMT~-rnxVzO?sYh`-u~V%S3HYaAl1GbY0(Vi!XY$F2;FlzX>vU7I_@?bkOVWfaNLz8?+UlA^r -v4>#W_T_WK0InfWQ!u@F13y8sK4}`aclW=o@qq`g(aKw;xx>66Fxs15JyL|3s+WsbLbGBS2HH*krU1_ -B4PmQ}1v&c33vVZ)dNIs|e(z;}31=euQ-wo9HbvKF(y$VGylNVAbMUN^6Wyoz_Av(34^Rtg{IR4TD_iX&G0>CAFUo}^caO61y=P7f{Rfvq3m3RJ5tLRmMU8;!|T$UUlHhd -l*Bx4UT||SO4^qs8JCd$900pUM~&gD548FbU2Ji98>RJKF3<-Guk%n5G^5v-%TdWwF-j{-=1&H*fokF -D!$+X!{YcsE?tCepT3vY?-Z=tiUm7b@qY4?oukIU;s(L_J8EA>3tdzsE!&h%XDP+CCs7{8EminS?7&X -vadR2pUa9N{bd3(K1#q(cYI~C{Ir*$eGpZx4poLsGi>hqsmRLAqRT6}iwws@KPv719RUmm?Yb%&>RHf -gA=>_qjEmpLuT=XL0yE1m5tTvM{vrJVI<4WI0M8bN*d@r8y6~_Md_u)9 -xw6m$BN`HpUvnsrx~M@Rb*y6Q8&w@>vC515bndMi{@eat-uw@CHzN8U-$~S0S-+9lvkPDm~%iequcd( -_$rxp#M!rT8>)d-;vCs0qbeT(0h|FNlFqWcNJbcjqjWJb5Gg;Ui!$Q-f#|>@fpKNgo5x4Zh^VM-rQz) ->SP%=HC!Z6%Hav7I0v0>SZ&B=HX3NKuDDK9H4A0IQC|qMSwHaX0#T>3Qr9Z-)_9*V=kJt*sUjr1Lf-N -xL%2fGXlhZr7RPaWFr_|Cv6~s8Q<4FrBcBrQk018iaFdYzwsj!oxs^O{+BUKz~1G7W*76sX-+haU(QB -e`dWtk|%&9;|fL<5L=xI=z`M6T*jCyOug5x`p{oBO00*ViErP^eB%MJU -IrjFWhCKDns|Y7zk|_$q(ylyF~8}DMFG+~DQ2u)d15Xid87k{`_P&KhFkiPZVPBsfz*p6kZ<%9G(RMi -?vnBx770>j&*W&)Z#V>7gXlNx{}wW6c9n^?iY*vE6GzH5%3>LM4}t2(S3L+&KiZ17=#mAgn~}QgU=%f -BaYiR-<%TLvRLH|Dxsi83N|?S?NL@FVz^DSEghQ!Cjpd{y -mYA0+s$2~X~uOp^*T!DQ&JSdX4@3k761-EK+rWuax^gu5c=ui|WOdUHbO3VPmpr5k}`kh@_-;Yp_2s@ -f^y8%Lk;5n}2pNrhlgx<6&D&Umwu-8~lDV?)>`$mh{8+$#8i3v -(fB6Lew$;^KyXd(tv2A>^gxG)knt)$bfXPq{{0@{-{E&-sE^42K2-4N4mh+E>S0l9?z?6lrJPMQQxTN -7;NZy@};V)+Hqw315h1$l{3%M*j&XWsedX5wIs<1jE%S+D^ri_`A*I=9xw{i7XW6Bh5~8PNkbp4kTW` -oN3^deI3AajIE6zwsxFh@V%rLnxtW(+zL`gtM;S)W)QLx|vr5@)8@;j^y4th(i&JR4!ulez^4aygG@* -jLhR1ke>;oF6Ai#+jonAojUwgVnj|v#B$+x`|Ddd}BpLWm3=bX`)kH2^b@o>Li|ADYrN&oBK(QeN60> -ho!z20qR;Zt!K_NSb1Aq5wxn(;1s3gH*}9lTFNm;1&-V8dFRpY-Wu9IU*qL;(Pkv7iZQ^UqvvsQse)d -yGZm|dPVy4K6x$(cni2ep4&x_$>B4B=Xq@ -!XW(-(FL_dC(5HXCE<$;eMi+oVj^dwE)nVM15saZpyTAd=Ok990_HE)f9z+H%9&<+ovHRdS)CwBg9uW -ap!;N{xp;r8XUyz-=<#)Zy^aAw?qkZ7m6HDA-z{aKg%pC$ABhKw9!+Ni@csKl_b1XQ0>234|ROMD63? -hWhIrdDtW3Sago5m{fpS9w3l=RB)@&C#ocbvA*tSdQoE1U0$;OO!fFL|85tNzvIhFkqKWOjgULG8jsV -0G|mUAd~-IBGyoMY}Z5$F{T*U>4a_;D}5TboMox@i)&y9byNV-l~3Oso{U}}Ui>82=vDXek_EItE0dh -&OYUV}hb&M-q`X>T(p5vyJo>$=or&GEsmCVT=w=Skte!x#MKVJ@h;l@r@)P6f%jYPEV>gg -B)(mP!2Mz5$sFlGnMr;b{?!p;Iq2+s=l&kmoTp1t6J$oKDmx`~Tf8M10vRt!Ud?`!oPr)(kd0#xV%bF -IXr{uTM6NO96_g0z?v;%qHdG>)KtTZ*V|_6b#MlpAUk`e}wpQva-e&#^3)GER94t_3D%A>T!s8}13})r_ -VYmqmgH_!h274U;bzje9&g5BQK!R(MmST0O^bVBqFNZQH4nj=?VerQ6InbQpiS6-dfakz)#wd0MWgQ? -lYj(>Rf2aPs9chNJb~i#GI%b*^+0^xww?->gAjL$^m#%=bA{I{eJS9?fiBEQb%eV%Y&FE!{vN&GQpq= --=Y@WEi8B7iPfN#t-Xp7)j0{gB`*)V32=I*|HatYG~a>qc8qkF(*cR|7 -V3(*eLGHDI(%5rZ;orG4>a)wusDKEd%$X=k-#hC%qOS`Ys)%QMIIf)$_3Usq-9JJ#{4Z4l!H47Q{*8h -zyXpmlI^am&^J5w&VH4?~!w|x8D(h__SZs3LAlyOX8mIf2Qil1kzU^woYN&NezJdD;5J5(%Eo`{+uN? -%3C4?kCRJ^e>_1i5nfbIHM01dyw+X<80zzXUChR-1THNSCZ}4Jw -Fw3~ZcQ4;HA7#Ljc5qFKE+0xQFG8Rr|T>CK;`B}k0r1hmE!l`}qLXi+UxgGNq_CdHyfiLgWuQ=j0w>% -mjO|L@P?>TzFdyo2L6`+eQvQzOJ@%-^?;rz?xyc}U@NptJ|IbIOhKuO0Wh4|{+;_(t371*+fJVe2_Q7 -`%diUYd4iB~2VrEs-Y<)G-uG6Co`9U4cdz5MIvp36a*^t=-&Q>j6=uxo34Xt1kwrVI)sH(X}W;la0d) -B?{)648j?(qtk$?RHSaBhbVimC2qCZ)ohV1T;FG_p4>$!(}N|3&#Mp>Ca# -pp7Qm-nr95wVHz-!9--HaVLd(oFxFuM>*yghVT_*refLd~f4Qws`)wx$UxGE99Dd&&Kz)Z-xWr)nY8$ -I>V;o)K9DO`=3GvsJtH)o{OsUSIZWpWUWwjX{%;#EfQHFE0rW*!XypzdftMv&ys;mhaCF4E%LxzJs0d -@tMy7=sOD2;z~Y1D`OeY&mz6NwGTdhc+J&@!be|Nb)r3X5gmK4VaJTII02XQW}L*>qfqF|zDj1(ub{g -XkzDPLgahNmVyFOl_8yWR>CkbK=HYSR9{?o3($Z_Q5ooGnb!nToaO>{VUPU@^;{i>g4Tlbl=QBcR$+g -1{=G=7!3|y@^lqm8T}UGtyUbL}cTRiRL9;B(dEf`q9tHEuZ -}2`42L}L?^qS<$C4sZ_LhB|JgD|g2g=!X+z_MCKVg3;@#q<<*N1)r37YE%A;=l!m6;3zIz`((mE(O?-2cL^qg@0BzJ`3>Q~p?k@|f4cnT0 -HckH@3Q@$KAmj<;*ylo@&8S4`#%|qQ~t)wKBBH{K~%V=`z@3uvb+FKcF5mzHk>nV~M!E|Wz+vTo!0#pmHHEGPL*pCzw`ur@|n^Mboj>qHT -zbGR4wbF-t=5~~rfHz{ah_7*ly>>I7v0>94jd*9CC0b@`hRz^jMjaJ@Xty2@T_mboF<=xkr6%ra&o+`>tz%PAMGai!q1FYx39J=@ -uJ1zYEk@U-+c9f{(KAuLZaw^ocxj+<~v2=TpOq$Oo^>%|F>5_4x7vix8}!~5g)%b2!{1m>3+$+zwE+> -{`j!t(G|&1j#rG=dXr`kcg2S-p5Mf`Wwhg$l`cqPHSaYxgzbOW2@EKH{EpQ;%AjnM%w8Owg~-gw(-y9 -acXV4STi1#G+AvWqmCXy+>SIhHEI}j_eBv!!vytaNwRO+DW9`>{cHD-bdCi*ZHsn(wqrAaEU$*QAC|f -t|2^b3$mIp|K-bOI6cO&n$oYrDrymy>9(qIDcz$-L!3(br|sHMpTav@x2yab2}`d>q_!rA)c=TzMB`z -3iOO?HZp{C$w8%t~3S-Wf!tzM5TI+&I>q@DZ>YO_dn!W{Tqc;s!>uYYcS{mafff*s7^?SRXRpIX3Z`6 -^lFIqmo)eEyQP5%nW>HUyYA9_n|xde!LuOw^v~f&3kS9f&&^hcgo1P32cDAn!91)ZGUcM$-;@1@oRJC -CQAVYN0JyRNiojLWFO5!ZU8MzZHC^gc-!d)o94@k%{~H#Zj6$pxlenGtB`U~T}xzVX?lT -;iRFiPnG236kVU`E%V>MPwT%`%OOsnxizO9Z6!~;HX*yJ~?F;&GK!f!rTzVCtqJjf)v8zPaWK=!8Og- -J-inaoYAgEgBUisI1CB@CQ*IkVfuC?Y!upUv&-0HYATJWe1Qv;6Y(K9P^q>caqn_U?0a1T27ru}=Gr*yUH&J+fA;i0&3(H^A?ye9d;%IdeCt|_z4u}7$w#B*4 -Zm4y|9cRj{$6PWBzI9F`Sizin)__`SC1tM$>zw!Jzuelq$ra`{vM{r&_E -|^Ll}`B9Pe24FTY?ElpibljQ+@Lae%upJ$##g!RDPX| -^C>KLT0yAdi!I)q%7LG3N6nLUNjw}jV1K`^5;)fL`-I3XTDs(Rrv@{W6Fw^wPm^t35p1jR&am!V4A~~ -!)?+XWy}FlgGYp`avvsz1FJ9eUOfG~A8f6+q-2P&9?R-eDjZ2{JG?xtDs$JF5mMQ&8GG+H7&ZZL=W^P -6^rLV_vtX-Qz(K$XSfzTirHzja>ug)z59_)CIx3xZwwyhM7j1!;;2Xf%dT{fVA2O8dsq>Me~0CgQ~qJ -eFg&2`oK{70UuZeN1bWjz-9rZ=rJuf|V{AzA|AKh}^0<2`soV2VNdKDZX9+D_1vh~q5s-=wN9?>MoEO -yoprvFsZhew$r}R4}HLw^|lCnrIO|adVTs_gxshS#{W*rBYJ^>q`n -SGiGOwu&*wibCNhN{b@p{9TfgN=sbW=R5DOuk`GCc9zECcHu -`y?oQGOIAtgrBpFgrrw;LTN`U135JK^^SIKHzkWQ&P;cPql8M_(9p9$Hl~vE%i|uUnULl#f;J`V1-*Yz(VFIx;%Zrt2MBBdnZN9WFlqsX(?M$R`Gf3KSmLS$_?m!0THxj>mmcxkE7#`qj1- -fLYJl-|yS2QL;mF#Pd6(X6<(o7LtWid*97WFaRj-Un{OyMSQNTgD(F_iMxPs>{pA1V<}Lt`CsBirc_4 -n`5q!-}JE82N`Qfd4*%9NlUbM#Mvb2s);Qt*U#VAqs51m|Fn5>)K$5xodPXcs4zHeZHj#eKr -wh5opF5=oQdtB&dg|iIWWQnl0TP`6HN -C!bzgxl5P?1)($#PvJGYZs%)%tcqP<*t_R~J}6r)XZ`hq9gnLh^N~e2p!b|71Z8JcWyCizFV_L6}!EDBn~r)`Mty+6q|xi;+;4g{|i|5 -DqEcCO)ko5qU8qPdSDsX*3M$LY2LU%svxI`GLldaDkOD91{&V`o%$wG5B{(Y$d+WEP6tjg?HRO93KBC -Rs2v?dZH#`iWlOGhO1JDdLG>*D}W{-AY%}usV%X*A!Y#T{e$2P -N}g+dnjnwn%!rLuC3gcBBPR4hacrsi04o4f`OY5-eSeIy8!8kvLG?*e!lRN%7d!3Z<+nG%LpAlufIWu -nS*wo=7qnNL!pQk{2G*t~fzJGnwnnZ5&WJpY7G`GB@>I>4zDES!$Gxu>4X97Xnb4Ye(`DrP>%{HCiW8(h;_os$PW>P&JknKY1ujawoJCJRGCThYK_^gj8;N1#e12GKQbn=WgAYb-Ag -o(hxLW7x3b12~MU(hu9N}{RjwSLAp1GEN6WNqmok?20ELV$vN#)f;br1H0)gK@o8I<1Hp6q$H;MoVnX -qhiB`{c6RK^wSbLjgyDN0pKFtcv-kRrEGIn=@MVXG8F@u+PfwrD!SB-4)`A*>PFy&hFTp#}0vWsp&gq -2lo1gWQe=1nTnEII(iv4Dlz7bv#TvB57ZWczA@smnWtHLn|gcI@5SNJE>BNbz)m^p@{j@iz21jitW3K -lV7!h_aATYe~NuDUZ{7F~ouI1te=7_eTVD@af`Q{Cz&(A*zN751pS>V6YG(tJYLGbzuWdg|Z!QtWiqw|X -&pj?{!H9B;VBMH@V?2rxMP{xE%n-TZ{|pg&eGCXt=AtrOmVi}((*1se8Sr9g$H6bN@7`K7iK-yG -%R8~giARK)Q(`|TfBNuCD$FN(<)Qr`hBjG%;ln91F+BOWNjWJ+qg3rSw8KMROG*7G~JO3^_=t`VET+u -xoQybBPXin0c%+J4TykL_h0+^>e$O?$3M5sl~?@vPiHkEH1iqDESghNEE}KDm5NbIo~`!+>?tRK@@{k -9Ri4;AJ{<99S}vf74F#kOc1kjxgS%Qiou}90AJn5r60pUi;BhSk1`Ji{VfxPM)O8oP3=5p;GDK_v=>h -L$G98ElC=^n=oAccy(5>mNvlm)+r2vWwF<+F{(^LEFH%X#RhNSZ(sbxCfKm_N~<|Cb#v&HeFVdk)Y=yX3|ow(DvT8+GO)@QyAkEwx@TfK$XG`k!8I -aAX})vGQRhS>NrQwcpJx&U(>Sn}yq+h)w2j1|Z=dD^*hy%HO-GH{vUru9%a?O=MaoYi++M)WRG(Q`;G%X5m%GtEKH9;}|snR&kxsG+{!NtE?r_l`F_X9j_p_k0@>Seh4JO$3%-hrrT -6V7U$g{(|kxG^8USF}qPiQgKA>$ -|F-jB3zuO1b%ZOzp~qPXRt`1K3PoUJXwWXnB149z_a-?bwxnL^y3K8$8WYaB&3Al+dUeZS9d7)DoW-X -e6|p)Bl_Io`;^sA@Zy9##8#rZLKNTk~sdAnT5=Jv^O1$r6W?N7?jRLyKVv9a=9CUhKlT`G&SQv(p!K` -d@LRH^ShmWJYpkCdYGZ2k|2K>A0}#xLC!up+(F(bvD69Q_FR21k$Cg0v@P90z*01ySDv8<7qvjP6|QG -m%KSsUZvdvtR|Zh54OchxLel1VB)_O{*q?u)m08gc)4lk%f0u?OUI6+nY87)tNr9__xMoZV73tzwK$_ -_Hveb!jruoN7_020o;;>LO{o$C!*z(+BU&>+es!xI&amkOCpOx?5ZmA#ChL#v`Z?YM`0ph!`PJ>O1D1 -1L1}rZt^6IY2+SzpK*^s|76#3(iKlwRMs|py^RWiq;apkaOcs_)}%j%6+Zslj_Up&VId*p9xZ(Nj13h -%#LDQwXK#<$?n+&j*z3Z2@}|6yS%97opZ%e6IjWm=YOSfwzT6=qKa`D6hqa@FJ&ahR3MLOg-ODTH}0> -iYKW4l6#Uxse>B70Gl|DHH52-$cQX#{sEn9lENUmv0~T6qVrxrZ1Yv+>G}!A2u{693hsf?=agpSL#@d -a=l?H9&)ZPLvw32{-dUb`E}riLRi7Ep&6CESQ=bp->ebf1tJj`-h4#y6l^vmfRV6Jo+F3ENd-$vd;W56<-IN;_Ie!~gqibb$tL!2xtyw!t3WnY -D9tT^{-LBjJ^>0Q^PTQcP$&!%+ZE5M}SS42K11}_o%F-{L1JySv4=(*P;I$$1+;2V<0Xk-bd>X_C~4s -RxM@H1ICO^?wctUAt%3ahi9wgzWtZNA~lcChrhs$3e!^UYcvh?VgXPcU?3Lo2wdCBmHe;dt=3=O%iBY20L49{!q)M_=-h_eg!IGMytI?UJRcIVDC0Qv`BK8)+5 -06So$9Uii==WF%J+V=5dr(8F-Wy_pTn=-*LcV+TKv`AcQT&qKxYs}q{=Dy{)iKC9v3b^nnz*OI`4>=A -ZmO=MG&nq*u^i@3Zkcx+Zx~4?ddT1%}Yd~dI)yyS{$^x%KNknvA&g>q%!&}Bsc)Xm=C}u6kG^+!wB}L -JaDh6*>{IIX$q5=>bJr14(jdEas{Til;#73dOIitte#DvRK4VgvKIW92B;$DU$p -Edx?lp!3adQ}i7Un?~=@Swa>wh{xYcyT+B@+w^Y643wQU^+k_v6rHBZqf%@f(9tZReNUdcjM^4ZOI($ -)~-;v_CJ2`etM$-QL*HKZ)v?;$?J!M2^%_cy6bQcJ74=wC-Rqu=#SigaJ??ZFE*yS&ErC&1_w8Hgn~YqA!g141ztE(fzl=7A!=0T+yWgXi3C1@l$k8q>Cd+HS6H&^cwo(0xR87`rOx2`zw~MCBg71 -ckAYz6@=ae^e5u5iuHQUI%KoJA(lU&CzUdjB{(p&UGk+}}DW8bPpWy8{|;Z51F38h0kgYc!Jc4cm4Z*nhabZu-kY-wUIUtei%X>?y-E^v8el22>gFc8J>`V@nDNx(7fVWAXqNT6*FE#$DUhf; -)OkL@9{Wb|j9&8P3k_U<;s7hB^u^L~1glu|tHfB%9>he)mmOD5=W0;40g;2op^;qdgZKl}t)lshTngb -j7n=tSPtuxCYS#{d+e#Yny5-sHg`G=7{|XDq@@X#jksnAm7_?R<7E(CRXiZ9$jyI9VLgr3f=xd)Hz|W -g>>)0|adR5g9QJItg~PI`n8yAFM{-VsHiMCHaX*88E@m%o%GTzQ5{X13Bs*zX`#nJ|q~@Xm=cf=-?jw -gziR;C)N5U7ofWQmbW4WME-vk*UR>Ax?3SRPtgTGf>K>hIiOOojn*c5n-i*aT|rAniX34D*-^u^%2L9 -hZQN!ocDr-7>y(n)^HQnS)VJ4}I703nc@^NM?h4(PB5^#OwJA$IVn909FeBz_$b02ivE}2ft1uCa(@rbr!IE2B^{7!kQr_ --8ITpAgd?@tMSL6%?Y&*rNX{{T=+0|XQR000O87>8v@kCt+f%t-(MSyKT3ApigXaA|NaUv_0~WN&gWX -mo9CHEd~OFJo_Rb97;DbaO6ndCk3Rn;SQhF8E!)f}`14NoQ0x??sx^eO8uAmu_7=y0~q3d%elbB$=)x -la-mQDrwIA_XFew?;8M;EOqxjyZ!WI6(a&b00;yk0s-VI&$hkaep?;NLmvP3TQAw}v%KsD%OXn;W&B( -H`*ANyu9G6kc8`09T@q$d{9BroaUP__y?afWqRf-s`rf^Zch7r&=)KK$vHDy6^VNllhcrD4H$f -hRs^I^muH ->3u)ySSf>dNbCse&-e^EfQi&u4WzXR7Z>bSSu{4>hRw@Y_c{D$#2=Q`MHi+O6{}%+Gf5O`7cDvv^k~< -!8xWL)LrxzSofTYBIamub-pxG>z9mdUliSqU@$P8w@8yPvsXCq<1LVDw=Y=-C?^_!~BrTtJYR=9`C~V -k!dFkc8dROcOB>DnJN_LN$6?kZH;O8RzrPAquqnDw~TvDVZ}h1(FET|;fI)+PyhaJ4>iq`;?=Hv)Sul -xa(Zlt&sJG}cb3#h=5e8Z6un?)hQUs(-2KUT5W5oUM-3+?M{P_adts$X2~olEz9Ri!u(PQ|;WL?w(?$w^Pfxx5^GX)VK?_s=7N3ho -=0x!JxW3Ym%MU9GZ%PQYj*(n^+-|F)RoBhz=gE3go-NhFxr~KoyJ&0Gt?455AUycvgYKDkYQH+fMX8kf**3^8?^65! -MD4wYFF(F{-HWog(9=mvs5tDa{a~MGVO&(b)rImysM{*OdDO|+9at!`ZCq}2opD+iDR*jYr%Km+_pxj -zN2_vYID4ub|mz5bRyK}Ki=->m_ytG=oQsNo0y_Am;{;8F3wTG)zdy=r{3;%B -wm{x&hD%3<9sDJg)~@N^7B@4{J`RWq)n*Nn@??#8~X*|jd(Q9EA0u{YS5kyW=fM~8iygn_EW;JKz}dA -8pxHVqk7lSyO446@coY6P5R{7{H}-K8BL#8(Gp#Oi51?5+-(r -_~4kF8x8zZCeKIwe>>rOX2y-R)i#Vw&s!3U{(G1E=vJzRE4_Mg11L@GgV){x*co!?PXj~ev}=SX?#`< -!PhS8s)pXfzukXysuis?|G`!soNQ#5)eRJXR|~!wQBGGg7mf4MXz)8KZ@o#Fwe!b2%=n#Ju_yf7BW;< -MqM+C)vrQTJQ5Ke;_kOG3!P(gZt)?D4d-wF?&p%u|_(pl0rL~a}|I0vaoHgr>9O-s6`(d`-#?o!1F8o -Ww`q`Q0y`^>QFZRk;`+FLw3La~Od`Y(=c#sX!mb~MQTVim$j%t6=o%*WEdJoSYdB&?6oWEnZ9;j)r2d -k2RYQFElI<@h&+@CqCna87^_o58$sx9s?#+9u8165kf_Vl2NO{VR+Z6>6-2@o}LAzG>Kl -CSH!qw7PA{}HT{PH7tIFh#wMeXZr#<9AyExyJ+m!40>8GE16{7NA)xPP|PDh(=TdO_7m==k0rFC;>d6o&0)xe% -!LWPcmE#t~tS|xd*gPei#mAz(mD@dn>^x~M1(imUvvK#-D=Xth^vqRA>LN+&g0=n!MJrK3Ry(cV0WhE=j1Hu(t5qdw@FdzO<{w`O6;}UzJ6woRA$8KO -ou9!j@W*#I`kFNrDi2i##XS?t624UMlnBTSQ#J}wA@CNO!jg?8k -=={?JIB=n%elI6b~AI4fnzoKqis!gR?b+QzRH6N(8xshjV{zapR&8XZzDqU@9dw$xc%Pc#KgFLm4pd8 -g!#IbS#v#eJUlisN~Zd>fGzSeYK*E64PyK{ehb9IM)+4B`d*}e+32kUqjem)y~GdNM>b&{n@2n!_+@x -C{B%oDU%o9a3b_M6_I3eu^aqHUm8H_!{xwG#YtvpvGX@SnoM@E>7e^iN@7^!Kwc{$~7DEQ~u?7~h=*c -Y8an(MWW^R-f%MpSu3INs88<;NEUjExXKFh{SVi)3m%-At2>lcW~a2`|KQZY@kZ__NrxXXsfHevt<^2 -{s$Y?Rdlypdm_!2SGRecWl@%Wh1sjyBY~-#)yISJt+peoICtm7FE{@nA7PxPcaq+waz*YR61JkgLYj5+9!; -SAdTPEk8?B*ZT8mflwVg8Et?%vK(uSRL{uu -=^5UtNo@z0`;>mJ<)+=%yYwmXWORAw=u4iQ!T36kPL9!OeyUBmj;}7XnvABcqe+3(I1>SKk{@-LlFWfuJCDwW)Z>or!bf? -cqy0Xab9?CzOu*OaNJ&{lv$G@k^uZ-epTbn>efgW538(qI?eAvK1Xu1mHR^MTxTmPE#U(T?$1nFmR#L*t%2|Bnr>*Wi@=m%SiOvl|_w+ -!;fr6&jzru1^5l2B}vqZyas1Y7?#L#GhV!q}aNeM>NGU)-9dgK|zr9RTt{%LS{C8`st^$!!C+f$xc_F -=b5D(8dR@B^&XZwlvEtTOPyb@=6Iv80O<3PRdO3gTJ!w(y?eUBN{m6O|1J05ef#ar&CNGAqi?c -&{q0~rpMQH>;oIYkpxhjxo_+g95mf(q^SUilT(A8FhW&p3Ta9f9Z`G7v9^<6Kx5Qm+{PgB^&5G$O71< -R>SgG*e-q+()gxOxnUn3cMuvU5YCElqO`S7n*Z4WV&f31h`*T-tB-S1Pi(jPrOLK6m6Wb!ys^~X_)ZI -E9b;`lgGw)|Zyv!gV%{ya_+oxpFr_(obPZj+M~*+~ljm@!3hlA>C^Cy0Wb(f~mfpT6yw1}(Olv{5VXy -2rVtjN&3ZN>C!Wjw+gLcVf886J0&re^gC)p>~HPNWXrzqbQGy6aBr5S_<;`gybmGFbPkoil9hN$;VPz-KQx -|OUZIw=9m7?oZ^JcDTB8>rPlAH`$HuF@rlO5?1cO(f_0u9_NS<}+u+m?MyK#`sm! -$#A{ZqndK4wsCkRgTI65IN@pc)X=*cRH)2KMLQdY_Oke?8zRhFHcL#^XaP-rjU4kRa8-V9FQhbQo(6Z -r87{Nx0FdIGQWgHBSM8t~02{7IuL5i|0yI@>+2laZ2bu#S7m1aC5TwWD$r%by+XiH -I=m9j7e(J9>QT?-7wN+%OJdbtX!d9Qkt87+Bk^w$M=u%syRB*o$ZO;t2!$`lt4c6PtxjvpAu!_GyspI!$blol>87>2VQ -Z|KE6kRX=K8J}Qa$C-mn3XWn(R*Sp`y(z@g?qjW!tzkv!R -bA00z`Ut8sOs|H&mm4ee_tA&%-=R?eY!q4&^s_ypn}0$Q??$&EB=mVxyB4xMT&nL&;i#M9~^_m$KDu1 -W{B&j+fk~-&O8hRZhnV8~4sCrRSf^q`_{@WvbsN%G9wuDpUiGm#PAO5A`!RkK5l)0R@vqzH1|q4^0s> -D88~fVtCkf3v`r(YK1<0xGNs_lsg)vj~@4~bVZMJ$h`jZaqpqp6<5h^L!w%$j^!)1j{mQt;ct>M-WCr -Z)zJmTF&zlIs^k9OOjP@CJM-6}-|s(dfAtJFzh5okYFB)vD#^Ryv}aoJk-axTzh@1?VF0?x3ndBR2TWQ-=8XSXi1tPudAyR#gR5@f=BnFWSx{=!W#4F-X^|%)= -8w0EgdmCta=HF$@1u7sD!2Z_nWGJ4<9{x)GZTQoWkaLB~LH)X#Q*Or5fYM1G`5RsDm?EPe2 -}`B53f?Js2*wSU$gY5S{jJMgo1Fz~ynr($}Usr@MM?!8L)5kL2nJXgYU9qdYI%Gzp{bo$st4X>fm18> -z-Y=b=rfrj!zKpvBrfvOkvDEXFEs5#vPQ4EU8zjX(wI4rjbRb*ma* -1!-SN>-z6mwrZ}I;s9HMU3yogP@AnLCLRavx%#A7ty_2W#NJ5yk50;W3x@8l~u`Uu*9~_5x7(RqD~_u -7?VSJM_zT2|JDk;dU-2}i@(%w$5l_q=^F#J3jeLq;lEYqz~;W|Cb*8Pm)CT(=9}KDQdOjH`_xCzVbHu -57l4m%?)e&OcY@hyI)2>K@vHvrtlv}r9rm9*?e#7`y?@vH@ao0eifTHUj7E8Cf(Utj#xd-C+@#oLcfJ7cguuiF_ZR(?<=-n@FPnljTin$W;LsRwr2ZwL12lOJB`jyA1*sE79HllSl -5whhfqL*F+I%`u%9&p)31@b1-HRo9E&lM3b;>CtEg9-h`bOok2*uU8~ZZsR9?u) -wa^Jg9X`^D~`XybWT8%#Ct9})d&J67{SBZq2g^&qta<`u)ixhdAJ7&XN2U%hyFyxh>pzLBwed{#t -kwb_U%h@-3&?CdLtXVM7-*J0zJK!UqT-yF!3cG>%V6NuFP=p(!AQ4@06MK2xjU_bIap{{!N_9a$Qqa* -Spzg+?Hai7cFtJ=qw{IclzrZmopr3ApX<`kUOl;Zf1&lFvOSgSe9>$!mbsjk3vDc~>wkjnzW#Tg71ba -5D0fj=&@sw4R_l|CG^uip{&pKqTPwW+E*$8aA2jIyq~8k(2*TBQwNw -&^*pav`1ekHgZ%?xV4Pd`6D?5a?GTV;O=2DF>na61veg##@@F&5)_2vm`*P4&YGI4hGhY6oc~p$i9ms -B_8`Y|OBQ4g6mDeMK4+8`I)o3S53|hAuuQ@J(#SbkqkuPrDhG@8jw+o2h>^0VdyNhcr!!7C9J(K$bMi -Mzp@Sg$T7dGdDLV7U^MRX7qbA4YKLOb{}6EhR4QBe)^JnI5Y$D^xY3X*W((@8MbhMwB)qZO@Y{(=At1 -%lQKx3C9CQw#85hD55!4te_dS@m|8RZwANEOi -Nh5kygeFXIhYlvrUx5^0_RzD2vI5afT!6(&-LkHGn(vao)u=~17gonq2TL4t7$|~US?x6;Lm)E>X{Ox -JlGgDH-|!d_9xN;==QS@3`Rx2FxasFELg8&BizbI0wRaZ2LvuQq>RnfKC_m13lFKJcKWdLGb^0uphA#>Iy<4*=8^Th9NQO)vQJ_Jae -{`B~Y!l3T6Z&_0uN~vhqT|3X5I650vhE=UQ7yI)AG*=4SUK&$AoT4M=`X@j70W7DeBsc(0#xv&hCS*~ -d8c31f{{+Qw<@d^Ph^xecDL0+-ZBHkV{R)`uPv;(cfj_A{!r&C`Vt&IKe;_V9q9UC#f`1rD3)mV`Vv~XbT$IT^l;eh4FFhFDE$4TJAg_5~ -zCijA^J9rBh_GV$0)&;wF7Ifi&>tiJBQoslYZ!iRWUw;0e)8IcSC7YXBo~)LFM6F9NSpYKO6b{g`vkw -^6ez7kSSYvAQG)qxv{NP-|d9PBo$dHgpnk@6=u!XFnG4@wrZeJA177ZEsyx=|ENc06f!zIHO4p2X-2b -;H#dT;m2%&a!e3v8OEf#)nVW}Q8^rNap7bN&64$UA?br0JYMBe2h%iHd;ApQL2`^np0!iBj1)Y}t{~F -3-h?8}JKZ0-BL|@lnJ6h@T7jJJ@b%N=5=683gQx7cv$zFbenud1?wV90I|<+5k1NHHe0JB@fMrys+oW -jPHvz3~MuiFPuRKpVgBexio&J6Ce6@3^RWiEiIDghcvZnIk!lff6PJ%h8BTn;oL6%Aq{q=wPx*p$W@D -#_QKu?sBQAOOs8HxdC%tg(2VcPAdJnnF@y{LGN`NhzYRdlGnUy)a+u-nzf97llJE~Ya|URzBmB4s;5_ --FKRc7YPT*EfuyS7V;-Ko}<~KAu#LL6BGN1In(0wtZ-5ZfbUO2PH(Zu#d|P}bnFe!E^ZxWf|Yf*jrdvBGm11&i0Pe%@ch_&>ms6p(`2t&NG_0Je%o$(%MG}bE?To`AsO7ka8nf^aHI%on^oHPHXUw@MQGHm@>#5qkpe-^u*k%>P -C`#p`a5xhUkO=>g+f_e>dyVqud|LpYI$i$zWUYk~43vSKezOMG@|G>|M{4GPYh#|iYw#$gRxddWocEx -k#UA+#fukEo}0h9GL2YC>2@bV4X<8b3tN> -e?JtEUfI?5HoAuf1i5#i^vJ+%wBRMD((yvE9RwpFntrbEbKoW+*>`#nTZJnU5U;i%0BhR=i{wj4X`he -2y)&?HmiwnB -Ri7{jI$~bhYOKb_vM+~Gz$6|6b;@|V^tr&YX&M9rM)~a0hzTsC6uq|fDTW(N;wW&h6xJ`sHgR1< -ozKMLJi%_N&eg>68C=Xa&Y+r=^+x#CC~?3hYu -J7(HgnVSw{o|}guRtJDWVf^BwpK -eHyY6%0wv+xP~)ow+LU)}Co2es8nyRb!WiBWU=y*{(vV?Vgyv4Tyl8On7i_abOa}AwfN7mP1C%)4XX! -xFccsDUmsm#yKOUF7MkYJAkLf6{#Yc$_k&23Fpk%%0)^B&Ic}qFDBx!(J3Qclrl;(cL}_KlCIjU&55V -IBO2h;TpL|#(}#<)cxt)Zip$(DtL$~6^R7;FCe7TU1~W)^|9Vm(ZD*aluoYaUn_kLi*ia0T11RFu -`?Q2vm|O50N2#H{bZO8x_PI$A=vx5Jn#d>>!y0EDxwdCuwsCr410T_h|iQQu)r&|0jqMydnW9}dd`p3 -ZRQc9rR@>V_V&z3wmNySNNWN~_8d6I#3twR(?+4uz~@CfYiLq0Z0;cHqIv>0WwRZU}wT?W@odZO@?E< -1`z2I_Ddq8LK}_;}2|?&^iS9Wsl+i1j`KbA%uODy_a$S#^0Pp$iAh5;Kv|g$GRG+5p*T?@VwsnlfzR0F~ -M%sqW+WsKVrA0*@jGN3o+3q81waL!eW8HgSRcQaW4vK|cXTX+J|`a{a*aXJ+*WTg;4T|G@c(rdvM{(8 -#Lxk|+)Fc4=0geh)!8E%9EdJvyWTzrubtQuqS|VYvj*+IY_c8)fghHr}Is&qJHYQoS+oN+vL$7$_?*u -usiq`z#kn2aqHni5vK6KR9BKfhcGajC|{>2^I7{Llzduo^B&s|0!m|jO>3pY?$$S|2@Tf=Wv|gU%Y7C -q?kYp2PVB-Eco{#^t=R^QwaMEE>o~!B=x;?mJxM(FP-HHm7wzhGyu7UD(kY!mNtPe^T8(iwD*wB;Xb| -Rvo)}=$sh%0!PU_kZ8M0^OfoPhnMoM#T*C*8%_1$-Sdx(Fw^kC( -w#J|t^O -Pr@lxm2y>IgTU^Kk#~$;4DnS?cK`@1v$qBk;msqUaqzp+KD1on^MOs{xX*q9tD2#t`m_^#LV_K005Vg -O4~G*|#k7G!XIVY95thSBm08aC`O-$&p16(onShZ^WP9I)-+bHzz778aw(cQ9%OCjC219M%-9}j>a87 -tJaHh_WFWyOvcBcnb{H$INCVy{b;ElLJ0MPNVrgXfZA%t49 -ds$l?0gCW5RK^%nWenRAaKz|HV0s=d8kLY$+kgob?4wMgr-Hm`ntL6)pkCj!wc1<@(7AaZLj_XUVRB5 -(jcP+x^5bkU&BgE|}9kRt(P6J_19-4Bn@u$lQqu&Y7W8Gpe5`%(4`~g$pIwF(dy;hFP%pf3k(j4EHAv -v&$PMGRmf(B2?G1QL;~HWF}gqB+CRi!?Nxw|{bk4wKTk=QbE|-(zS -YU8?{WiGBA`=7jaSOX$j8*ypya|$@J;RNK`TxaO~R1mim!q61QW1jB{$WKOl10W) -|3qGLLMmIWzuWHs(XDA0i`%q -csh(Vf-&PW2nht%!uG$9LCJt|76!^WaTG_Ke7gYavCuj@+YSeGnYT}BMrD`_%r7+z4sK7!Yc7=VuK@s1fg<1g>S*SHov4z?IwIUS9SS0)! -@q}e;VZivDStuZUL->;#(Ox_cY3kQs$e|hGd?=8Q>!5_AxzPmW0AXtOMwbL|M1p-l&QgHrf3PRB0yDK -wAQ5l`t5=vb_yHjMJiGO>p%MHd3@XQ)KM4E>^yUvi9d!&<(~R!_0O0f1J0Kd)j9~8Q%od3q0iW3t*qE -}&>JMPb67a@;;{|ox5IjH}XDKrJ41Y`IrK!5G#^D`lRBV~>$ob|a3xHmD7-r*NlDgabM&O$WYYnXKTx3V$&&kVklc<5b?tQk72>ADZCPFhr!C9c{jFSSxhVbp^Y^mew)d1a_Ig%Ylf!G&-^0(2yK3O?51Vie^N#Gi#87T+8%wFf{b@A;ZUj4+u -UYIMtJdrv3@C;g*em{0F^gu>~mb)gigYLKxV1Na+Y^0!wGx1OxQk6~Z10T4q}>;-*YU7Qk?J`|P#p2p --PR@b`d^8J+?@Wq1xa@p=0a;68&*2F_&4)x7)!_?+Pp;LOg;1K`a5C+b$jaOzgTaOzgbaOxKG^CQ3m2 -BW)EuI5LuGv(3w5$sHPcz)afzGOJq#A$$r6Pqi7FJaltL*_rC@tH83#)lh-&@fBrZG?vV++pzFXYvhA -J2$(bX=mxgOI(wos2Hdv=X!81e93gUz8}hu8#trybVuzTz^!Uh40_YoijG9yN5~fF1i7%eaWQiD?(%t!0PtR}h -PjL#)>; -ey4xmlzGzWok7P@B%OKV4DlYdzctKFawi?d10t!yAOfUUx2Tcl7KCIz&MXQuWW{v6^?jHxFM&6W>hf+h}p43TzN6=%OH;hsBZE9A{Z1K95xMJ!O6s~s}Xkg=|CcH=?N6sd`TI&JK(*q`fe}0|FY17%xtO6&$+)QkFEB$T83*j -pvYL^bn~YAY~cXdTI=$7q&=vi1&0!KPGsA1s72psTifHMBrvM>Rr@Dzlf -A(?U8I9v@#C=a}^2G+iMkdkmN5l9VyZ5Zm?C-K!jk3;WZR@mYY`iE3uML0qhv;q=bE{}JOc#kD?;Q?R -!bo)hzbj3Mey7NCMI~$NP| -~^L-Y>3((z1Abl9-oM?E(pj6O%SB>KYrLV+*In)mwil|jYNDC?Cpr1)e4gfiw7k-3B}TyVj!1|ZQcwq -7A#0s-G+i1=|#xgdc6E?lVi37F6NYwnS?G}Fk%Ww_ -)$=HHIS5zcl-mqLKN%Ad*U3Kt_Wtt2wDx`l*cJGIw7cN6qU#qm{E=1B+Yo?$g6^L6267H_9BY?U}W}w -9C1~MF%Y5zz+j3pKM}EtcT9}=lP^DGL>={yso7JGB+^_pxPWfKp?fTj?(m?CID1`A7|bW)5_4_fP$Kb -_h$#@KSDj1ciX*6aR#CzA<&Sll`((04ru_N`36DOCy`~;)*q{S+FamE>PVRG2qoI!GjX3C74{T1e^+_0)ZxMg^a3X6K2v^zqjz!? -*^6zWG_bmiz?9DwA>lVpTHwn?Xq^y@6)Zjm7_v1u2~*<-y~NE6GI*1VGIpM?i_5+)m}elK=D7=w5vP_ -2g6#NmKJp31smNvz!Itu0uBiJ4*$NDwnw?-~54#n3OupMTmXKQ(kgl$DSg2+Z$Td&qfKp> -)p3yEv%EqkMH*W#E3SCQ8oFhHx~$$GQ)D4v9<$(lZPglkW0QejMcb>$nYX0yXi6a!WGp@@kE2-2>_wi -4hatQwI4Gqf-?Sz|(R*JQL$BZ)U$NnP985*Wm}k!7%6`(FEBEB73xBp&Hl3ojh=MVCiGiw{QxP?YO5z -`$W*(%%6E!#k2}@U~;o3mD9)odSh;G}3q0bUtB$ue{B&X#ksh?Pz{f<$lR(End?Hhg>&4Ik4t4?f|#b -G@M)m2t{>B_(%L)Sif0n&k0PKws=KmE!^RV=+hs_k%d*bE`U8ONsEj1XN66m#CtZjjze9SMJyst0(B!%Xe -bjA8+0?{bZA&m$^;4_zo<)(A}UyZAl|hFg9sWJz#XQWXBw@RU9RLm$o1DD8g -uOPRB&K7BnY*&0VlE!}f~_vYBGPsi1C+<*@V)Q_b>tcjY2Xo#qP1*hcKISkfhm$6kzjPfij2^#XtK>; -D+HHF{kE?yOv>nxQ9`NsHr0fLhX#mNByj%SmMtt~JtKasKm+}0^*oU3Y0)6R>6HxXz;qFBGgv|oH@R16mj>yX(LCg}zAgOp65bt}WWH(L$#3{oJRfv= -UBe|*dAset5*k{^fpw#Y#_~3vl6Cb}pfFTtx5Y8A(Hw*_1M@Uc_UWYnE?g*^wS5Mg#3l -NMO$A@!i8F*FG=>ooBcP?pqSJ_U4}{=pPuWYN1o;$m_!Oylqj3<>48Ty}7NlHJ3wSm*2qFg4liiW@NY -bSc#t?A2$##wxPtv7%nB~6&tS0^|!7j1ylH@9QkwQ-JidT&}0D~S_pGd-rvGhtv2n-Q@f+!7}ls53r8 -VFoQA+8PMQd=r7=7j=IHaZxL@<^QWo)hL!FltPV^W%9Kc?x$yrX8uY0AobpZvs_BFkxxFyC~$69&RCHjD0C1Xeb_>3wl*+F`;b1S_W(NfMMoSA3gqzrhW;E)Gqo2&rFWS86alUomDlk`9q-nI -7^;nwJ5gWac5-b`LjZmZ=~PH!S3qlMRP(K6Tu&g5Kf{8^j_7kW)v71jYmCNQ2kl7>`~E40&c#f|Bxd5 -W)n;X;_VvT5~LsWE$fLQ{RY#LYUwR**4^FAqTKNw(N-*y}Om!`)E22ibEbp)~iTnk#B7)2h*{^ARwev -eb}U)5JX`s;KGH{YfB@7I#1dt7_U79gcL$8zNX@H30=5g&XY!fqJrbOBi#3{5cKNUOd<{#d<;q&(&eQ -kz_{X0CsEto-v8ypZ5B~_<&eb4*(MK!V=^TIGZw+F4Z=vFZ9cWn=T`0fSg0Q^ -mST4^V~ZT~m%NhO857D!Rh{U9qYY419|$cpPZJMsNeyK4SGrp421=E?e^+RpP@K5qW(tT#OgA>WV$lf -e?{3`1PX-dSI)90MU9K(koq=dtDl<@d3y4q5MBIanh%iVLn%K7vJ<5^onwr?G(JV8g}0NymBRZdvRbj -o)Y3!UZ1tI^fQ}EN`L3rMnS<#kWC;UVa;N3WT@`dM@DdDyYo?-|oY7vU-Q)OZ5(ZDcFO7iAAItEX915 -G{*^)Mo@%Q6!5>cyNhKatkpFucNd+D^*K8@pb|2J)~-)xu8t+b_a)7py{{Qvy%ULVRzs94hGgI!r-e! -I)v>Ca^e~5_ID!;*Hde=)l-P=(xeqMzNJvYHL=RF-8*<(P+~3;4r9c+RE}^5fkf6{k2n*~;9HusEhymJ+yP-gbqBx55eQ9(j{WPqLwDeN5+Z -YD|j*&uWV<(I|)b5xskj2)HL#)8OXeD8M_&XPHKK-?ZL8Ij%u!rC}=&G5X!$c^h#^BZj!XygAYa$eIZ -pFe-E3w2-E0A}-MFOi*gM&1s1XhVd`e(3;{w5v~+i^CAS`XG3?Xy(86oGlAYx77?RnYK2(!mHiVX}8# -ac^AI7SmfDZn<1&1qhZ1w%{#|3XbPlMnVEne`@n -63r_MTMNyGb^4gIsPzms!yWS;3Q2}DvgnEGgMCt|=50N3zi-??q@+()~t=%d|i$;sR(NiWahV(gAzo@ -Fo}tWedS#~yi)Vm$Fro0az|NM5#rfa|~lCa0Nz^@90V0>rqCHNczk3c-oH*kZ1w^N6aiUy1{Fi1;=8d -yzGZ)@KVa1lQsggk7pl`L^L!P9gTJYc|p5eJUc3pY8hEP^=4=VM`mIi`GVjkOiHNiAUf<3wtBnB+S)~ -bdyGmpq)k(@PD>sce4}zj+yt&_cT;Ssj|pP%Q#h*!QL07_*+5rbX0~MTBN~cltareKxBW3<)l&TAlC*xFI2IIH~2PGM&yU! -5-%{Hy@@wcHf|1(gTd};_Ymx_)Oy?Cl=j=ZakvvKK*$r+thAzLkzpSH%ticeeP5F@ibZB0W@+x{GH?P -S(-n?Tsk0wNgV5D^jHY#-C3OGrq~%Kw0vt{%D8lmtoIqA^q$*3ys_|+9hAlk6Z -4Fxr*un)SYtD+s2WNZLTBy`~c^T8RSO*JB* -lVJ4K-x(r4Jpr>pqv-f4X1FLZWK3ktNP5_(LeRO?gb`Tq=c1-<^K -2S!qu0=d56lufoHC}oK{6Q(nf=zSM(=PUjsP|tl~1J(yQoo-BLQ4EP)hv@0EAPtBvK0VVYF{9Y784ZSvDh~MyD4PC^PE8{QmzfvQHBg6~bUdWqf7_CtJB=7CmNb=XGgtnn0P$lWD`1)^@O_3XBq*2nCzg -OScnGk}b2kH&%li|CiCkQYFuIjLBsv_`;lKeQ#izn2Kt#B-NFTD!otqRRpg3D_&S1J1F=RNs@-<;N-< -P0@%*_Z`w>jO6U?@AZLk43(&(9}}peqv+PB$ZvV`B$l8O~QJVn*;)ihvP(l_F#WU8NWcuEbS}F=O~Dg -ngn)f1>cDiZj_OCDbR?#O97^Em*T$#tS0`IV0@P%WVpCX5lam2Qs7cRE)T+yS)5y6 -yFVuc-=&}eO{f;U^BpX-DDG0A+9;J*BT%utWf2K@?nQ7$BZfCdio$@@hXRXhtxLwpYptRXO*xv5Lwti -KAaL^nDGm{H^8zWstmJi+x02VvqP?3y`-zNn1Tk`vkq$#Uw=&YvRg)j4_BW~~qnmvIdrnh^E>hk=83_ -X46_jw`BcVh)>W3!qT|yZ<6z&a@P|$cUqRa(}=Yk0-6HVc}VM5K|fjYVsk?Z@Z&Qn_$CYj%lj-?{E7_ -Fp~LJR@167>fXArtk1+=-{;A-Xa*J|Z~h#uL5BjfWbLdF4}rTUOABP$I9K+OYD -_0yCS`QR#Tifv}}^1eEZ7h$Y&s(O-_^!|Bmr#5w{&j)6cbc_CBPFwR!0+Jq5gvWfyi+Y%;hGf&PLws3 -*XhlCb@=(k_M1%@%UUS%Fk8MX7mZ;_o2JF&1fYQ*P?t~cFuzP_3CDYCtajr=6boDc-Yz&N=ABY`>++A -KEh=O-cpEgV>~rsv_1rcl9GKnw-q+Kb?GJc5c0%YYD+vM^%sl^;%T^u`RP&oK1~PS0pX45mAPD}qy+C -Nf7ATy9Wf!pO(1Fb*Ae*^8~3s$){Q4T=pk1^lpLYd=a0E;?jmo51ZHF`RCp2Lz>%TF79!Y#z!RB1N_; -S!7YHIb`^j0Q8BjF@rhUkGXKn1(ewb5qO&7Fsep~N2nO*J3c6`_yb$G7*@gQgkxm&n?D>ngsoA*so2; -qCbD5x0uIT;t<(x3vO!ihiq?rNNBju}DC-24eTCqLtTI+ZR1Tl+qfNy6uBb%7)*$J-I(WeyagV~W!=N -lX^?K)e(BFE+sU0G1@*A)MyaF7uROUgImSTSf*`eTLn252hD?@jvgv1O9%IQ-CW<84p7@1HtWiS^Y&3 -{NxQibZUI}d;kNKD{PPHDqG><6`oFXs$fc!2GT -$3uoKJit){8!>F*0b=?Y%1L?z7Aq-cgqV4nEKcYqRhczvDQs4M>{+v{@a;I90vi*?D9AO>zM5VS7AgUl|&99Y?9Q%2CHBnk-6hA=zfo?t -b6Ns}h9h%awdz=aJ}o6Z_TKf=CN2FQ4XY{~D|18lj)6Ci}6i{Ol~?~F+CRqH-Ze#$tdBiIqnlS|usGJ -2xi#CdGK2r)!fONCfv^lco0;|K3%0$zMZHdizdsB14G2`}v%b2EYUJ2eU=zC$qw;*wK8sjkkNvb;I_M -}VyYE*vO7WjDm*kyR|Hk(qM=TzH}6`}ZY+E*8-&X%syrNM*c_T$WjkGXkV^m2}FBTh|IopFkhM=T-=D --FQF<%R87bin2`t1ZB&tAf7N=*5NwX%7X}}3UKS&9Asm!B9zvmIQC6n6!IlV)0+St`O&m`8v=?chc-^ -6PE+)+D|1>fPpf(obJelGMd8H;h?6Rm*g;0jMnR5XMDUUyrohu(WxkKUB(O0?xOPxFZor&Sq_-eZBE* -6~WGSDhz_VWEO@eUH5Qi$|O}2@H$a4=Zwn%(8(aiEWh`q$J!*dB;IAHN@7*hgzY`wqQgAzeIJ^m -;cfBOCq6OGB5Qa?aL(MBFxWh%Izs-*!Sc(wMAJ7qCd5ctU$ToNf@hOaVm84t9~cf05BZSCRKQ=iNE!#Ns3@F~N)(%P%91?6Qc!0B8SMyi665j;c^mO`-U6#&j3I2htg{W>;J{ -r@k5l@`R_z0ezR+nWYL-m0{2L0-%Ih}QkVYe&IjS$1S$Udy-2z@DR=FjRJw&=LbXSm>Z-#KMD)yCv~U -vi=g7wK~V;svB`=i)^`@IAXU4r@Znb*GQ(W;3NujAQH;fAK-Dc8+($Cp1pA<1ckgcK{Sz*b(6<&(@hQ0(#GV|BNgaBOg0%g)cHv2WN-B8I0`9FZ`G*?k{|a;}VBe18j0%L;%f<%@=#r&;UEm7))j30QOx8rBBr4+0kKXW@v!P@bAMs3X7MMCwN@x$29&G=!?=Vtt<-FE!u#IqSc$ie)13!Bw)9q+;{Q -qBfs`GH#K55DU?2>1Gry5NJ^q+i4I9|ksTVMD+sEo=hVc?&~*)3&}T>YEN)*Z{D$jVaogj#}6Vu(sVP -+MP~Y*c7mN3!AsFU{@r!qw!!qooQT9gh}GyW>XiJ2g@YX;H1cAv&sC-G*i<#1&VEU-k*6(ER%KBgX&* -`Z7z$jXc6^~1{*3LF)m__TQ4sSbl-2MuaD^q_XD-^YjE`!;FGZi@5}m610PM#9sZR>49AUV42jk!FyV -gEZ>CuPVW8*d%|z)x40JZArsYPz)^t;x4Tg}YTI6imtA);%zFO>T`KtxbmcUx{Y#FSD&z8bk{A@X_1< -;m6{bSrh#|(8Pv=&WUMr+};r8L~nM^k6A*SUudWg?buf;}reA{4G$^T}5*8DK&WV!WyOf%bTFvMmVs;F{P*VIhbZBZ3Xi3yBCYa3Xd7ky1nO4|T-wYgdE^0& -4GEr$m4}Qe>k&7QQe(d5W77HGdDOWM*y97rRJ&3~3d6K4*Y~qsXPf%Sl&ACgniI;hN!$h=9F0*7-O^g -2HjHX2G4U=hqpth=W&%)h@O5rYpw5&Ol`d49d7RMBS7wD%=Kx!PqOdHE!mW!#K^#`LF|b2y4Y6Rla|>h;Pey7sZj9pxat3!pk^X -W%)KJ)qlHpko9sfQ;tQgR_Z?u#8EdS~f2$Et18wZ|&3yRbWU+m5}qW2f!dXDhI4m33^s9b0e5#@n&&c5J#Qw%pSj?uqSIL8j -wrl3x$z^MNZ16}Hw5HbTp5=O*0Z`~LmUpjE5G<0S2#bF3Aykq<^TTIi|4*g-2MX2(e;0zI+Oa+jYv=w -ZJ~Wc^3Xai)aceGlr7p<_wN~vAdyOA-$%IDEyQW+r<*Kxr9ZtIjO(Q$!Lm@|<1E!GUahDuuuws6BS+u}0V{ho -OJlzr)UZ@L|y+;3MNF*%PqN@Y^0J+84`&P^-Fy(f=Vap;I7sdDumt1G}%SK!)zsu5@E2jf!4rHq@k2- -Cr4a@E{69jX3ukOPApxX7VF4qfEPAV)58Y>;CYIWfqIi<}zd)J2{fjN!v=c -)yHQFGHoC~1bGo(`RlKb?%lh -$&*N2cTeYm*YSrk&>&LyzM?K}P_AVdyu6xO@w+ps$p~t59CMn}>@$iu<+7|rl{Y_c!zx(#vo12?&Zbs -i^`TARJx_w)DE<=UyJWY1_^F8V=I=+}Rh`)@GDH5px} -Fc!Jc4cm4Z*nhabZu-kY --wUIXmo9CHE>~ab7gWaaCz;0Yj@i?vf%gp3XXDSBj2%Jvle8lXxDxzByT1gk+8>QcY4; -)P4HDZ&d*x@gzZ6cAUPglM_oMPyh;rLRF!vFfWT`^y$-lU9U^_=~I+0S4CMz@mW=5>zaMyzYn7%y-cg -L$Pc4+p3aJdeah0Bm2pdqbUxZbW9{GysuSvJjA>$< -8-7B7uAb#cM+^e0xDRiH3CTc=sVO3{z0`e^#;EUuU={5mc#Y!K@@m1Ah0&5FFL3 -LknGeAOh7{%2rP5-`HvGUQYVgs#sE3#P{XU{XXWF7F&S)8Q#qOaI2iR)MvI*dx~x33Z*^8_d0FnZ5ca -apnFpJ!}U)9mK)lF{h=eXEz*%e-cbGR7+JiZrj=J}l$7zGA&JOQH&8NY`-%eh?l3hPKDc9`aF%=t -)M(+@K6h}a_03s^NXr5+llBE|cD$f4RW=PX@0VSxl(TFP6XVH;HxYGrzPvG|x{(G#vJD?gXsDRy|_Tc -^5p{IM!ryNGfV1eh+YN}wQ8NB>*pknPs87~k3FAEs1<1|Oy0V@~{MQ1QtZv%FGBX7QSpbmK5d69fd*s -K8BFUpqrjpyrphO0r0lpaRUctMtoktd2*_(5QwKILrl>C+ft9P$b{h{FikS|y%5OJ}uG4?lAt#aJ$rH -&mnf2_)Ipn*tACR%4?&e$sga{GL|TjE$Sxhn#g!Ux=}s=$%$SZHq^d9C`}~@MTtLdUdMx{Ug+G9%z-s -tYMF);iyt4yum)q9-j||lK2vCmpi`VOQW1dKQN$!nD`TsD>8xg3^&n;SQ2ps*vX<|9*tK;Ri$Uyw(0# -c&R~v~EIFVCku}_DCq2#68irD_Y|hsiUry~dc+`BGV)YaG%dz5+H_hs!7qC$?-@?G<2Rl40s|7Ep%&6 -pAEv#q~m2rxUFOQhqIHJut%VE%jo`{ttP+(foGFihuP$GsU7(&-X;7u%Lu^MoNl|_~zJFU+dO)X-Os? -_H(3KUruowanpCz2)82pI%2T#b7~Gw9_R^mPV1TE!9oQWopQc~c*Evp;6(*|d-65Tz@FkT|E2@ZM7gS -7mXTCPWu41yE@yQ1DT;O-&nsC+C2#WxU1SmTZ;5Bqg+v*>o{QU@=eyHMluXXXl8jOIE^;R~1VD87~lg -+dbHTwK;5l+>PGbvj_jMWAn7EYB782)HMVE6gd@Ca3si~g`A3cf^i_d>Rf@|baQsGiyY|9DHX>Z9K0< -rC*~8Mlp{rY)C$rt@%`z?JNlc|Ir0p6lfd}Kr(@1YW?3bcWYe#M1Dc`cJy4&M70M3`_#MF>(_%Z2?Dy -k=vi$i5Em|T9Is~}DZE(x!h;gw^7~fBeVACN38htb838P0O!A<_DBCzx -85Gf1pV7-7_&rI*yqd%fVWEt4omR`n02Mz6}MTAgKMiQ$NZhK9voIb*z-EV)a;IEU>yW7U8Uz3utTav -jk!Z$bMB6tMP&L{tPgBp*`hm-V*#hptas1b@B)JPT3=x_0W{p-Vj&$P{(C`5-(u#iG=-YQG1aU3wnxt -tH`r0PE0~tCEu^2C6J}U+eSaG)AFJkMfamomM&p^C6V$dFXS(ie3z@Z79_5gQkk2*9(1hf?e}kO0g -3)ao)&$Dh8qx7w>>4JQ0x9SOWE|YANhE{sGikwT_MBFLB4IPGIKu(K4K=!iQ8hAD$-TL$0fN9|P#GTo -KODsWBWVa>0CnX>4b`EUCGdeH+Om3jN9~r%WUwqC0~H=21Kew9?4F@%2_=S*W<0ZK7W+EI@k;cZKKR( -O^|ZoyH!U`z1B{cI(m3>UES^pYpRkfgaSnZ_%9GDPzaU^H)>2R_ksyaPTa(l2h?|c_AKfDb5mEGhge* -%!ixp#$qfxctw&-aK>mzJlab9Jxc*ou?-n8%?_=LUj2$|7+R4hh^fR@!NOJ{_a`=9aQ9}qhSU8_-!*2 -31IhCu8=r!`KU-`9Xx&=ehZIqxhF=|+Irujx!9uNVuowWpFoD{(-D%)=p!u>Sb -_7h_zBh=ObBdui)_0($87@F<0440{h1ILe# -i+acVw#=uOZWsgDAnfo(A<-}7o35yTysvQ&2Jap5j}Vl6sAf`x{i*09QLe-bNE@ -3!!B=ZcaiPU$bl+XxpK61I(~4s584u+oJxUMirr9hbDRLK@F;+$1c;$s}E{Y6<~H~QdC!)G%X~4?kia -aE{vyHQIR;o1?4s2IAo?Ya{CS%EhC6?xaA9JT2<-pA@i!_-CIe3BwsR<|%DV|(Rsv=91b!A&+tG&Q^?TT!w3^=ws#;rl7g7JQ -=qv3$ -PY8H>t+b)H$b))X||BA0vncP6Mi^IhaQSKgdZp2c@z2>sBQ)@X5oY&%#HE@jhXY$m-K5dO7D#F7uQnhrCV$TpZq!P32G_+y!7NLrN;jSv}j#M -hXR{_(**z`(uVR>n2049>zr{a(6gydh&|7eMGzZ#@F7wI$cP5 -tOF6N)r~N6#`~+8DFp#ApFb|^upOr^#SwqCb`3n-PY0-NC)J!a-j6F?Tw;9OWOrM5jTu^blTo(^yBBJ -4X}^V?^|$=Esk$T!ViQtC2h454gXOR)vT#U6w!Jc&}Jx>h~^5A&ZXSJZ4xM9xs71PQKV{RkjLcO!_&u -)Cl5a=VvKavA!AZAQ0~{qHT2f4&of}*6v1smM<6;8na~Wa%M4^F<-yG${GA?{rMXgE&B}CD% -dv@vALyZLe6m)(&6rsUpSkKJ>3j}+Tl%09CGtm4ts3?!y -m+$4v_wuCy1#k@|0S4CMKcJ4gRD)Ok(}`8GSCwpz#pwHc_->Xd<0J)y(V+^;3shJ9|F;_V|r-RT9W_!IZGGk)B^wgH~!mMwwVk5(>W9T)h10iV) -@U1(od^obs^T00=T*m<|_~M>Ow2*ES0a9#SFGlj$!!qR{S!`A@^u$0Drk>AKu{lGCM0CclB2k(MVrB^ -HRQeZ#LtGPjnd9MD+Z}as4k}sjXV-|M*J%q?Y=>Rw;BPC@)!@fhJBR#e-vd;46(nNlmh;R)-$Hr^i-J -qTfr_;z2%fVgi^DAp!nc>q`mnli7ylC>Q`E8X=>)Ck?l0wCc;?EQMX_j1v!$d3GLy{+)r83Cpq|LjU}fKl>n`8FjXfI@_K)KNSUPVyl?u9e*9?$vgPf6w`(EqD&Vlh&ObK1fqDB -R)&Hx#jSU2p1XkPZdO%AWrt<=av_d*)pDmQ5L=4ZC3peCmjAnf*s6eD-p~rH*rzbfpd)aq6X7mgUlG- -F-ifQCTLE)zC@;BlENS0wNT>E-yFC~vxzXBkmDOGF3{&;pNOwrX0082#Xv|m^Rh?}kcnAwB1#UKt#xK -$4A7@p;z0y)T7f`@SO8~LdX=|TAPdv-1FJ~Leaf=TdUxLOuH^6D7aQxmE44&pW=t531*A3Sg%B(B3m!>ALxN=lTaEo-?x2sb)_A(>#cNt< -RxCnUuFyFYKHnX_a4ReJd8C*qga~z{BX!>W4HpfUNE_If6)uD&8L;>zNx+>hsMP4;?SCW;4rPIXF+zn -f&2)f){eRyz3zZ=H+p)`W*3AR5UG+>*v-%~Tb&o{Ea5)q=VT`o%gqr1!C}qJqkS=W$sWgYT^%aKbtvf -LP|Tv^^wEtCyQP|Agr9S2o=?B)@}Y6KY=M50=4<9?qaBUp9(Hw=AXI+^S>sfK`Dn9fN4x@_G~>X+8=NC0q*v)rA7iv|aGEoOfiUS%Q7(*sH;BMs7h1@)NM+Y|rFuLMMxDLyo8}xz6{ -rsKnbK{3EEcTf{G-Ik(xei>db~Yx_zhbwi{T-SNS#qMIDVpK -D~;-7Qa%nNkidAT8GJd6=wWxPSrZUHZ4LU;+^tMAj%QOMdrJgq{|Ck>Y4KnxSRe~Ibd0!#u73J7C6%v -27l`pl3~@y6RuOsw5|rrs=z+(Cj#4TVL3ecPy({F2x{=QdIsyuTpQ@^bqUXXT|LkjhRmPRidu$6tSfB_R+P?*UZqzFMC*v|=&4D0Qc$FzUV2xWJO0|q$U;IVWCABPaj^+h|j?ap9{WxQbc($GJKzo;&!@oD6cXsaD7{n -;lF1v%!6eO!+go_g7lzKeT8%Q`Z2P;%sl3$hJQY(hkF(XCY%%JpSZg_ArQ&zjm%&GdAbK1tEKit1xrP -(qr)9n8Jq)sS`6p1>zd{91o7(M*!U;q5z;e&_w@B4h%N0a;atF8|d!@ZerR-|TgC-|S~}-vo{hDpj+1#bAeCE~mIpuco1dfs(?l`M-thrq;H`f(itQsB!bE}4K<)M$U=GRmK(4<0`T5#_HB{` -BDCp`PPdLDP_Z`F~u -a5DSF{!}88fA-?=m%}h^~wkr1^xT0?8BDROSQt3mJOb`5d7*uP5GxLZTOH)p@G|1$7!c1AO9z$<7n`w -Hs#N>YW(Qb!5yMOuc;eB{WnV23<)ZR_ub)R8E&l6~-62uSrD~xxKgOY=kYN*k1&Xwx?~#eV$0DC#jDY^SWr{4yTNWPnXX?SW%>;Y%Mq29>3MH!A9Lsy5%lsZtN-G-bKE$yl3TNMYQ^8>urr^AH3%*@;gmn&@Ip -8qeuB83+wCU^lWuHktOxMnKk^LsVRD>_;yzGUlhu{oy7-NKqi+mdeM=rnoc(+ -m?-2jypq}{rT$0l*hF+?!rd(0h`1ZcabmbZTt$CBc`^A5cim3$&b#w?c7fO765q?V(T2%TTI(8|7AEq -^_zchqJS+M;zh7A7Ry?F*9?W8Zp)gv!yADHS<-sHe{QgFMsa;$jMlf)wjD6<4jWL)41L-rm0mj=aN)A -bh4p}0c9AJdUAD+H^IjPnwhEp4Dit?hGIziB_LPzSo##4#CCtJ+npplA9;yvXpjz{ZyKKYCL1V`R2W< -a-aFvc#p0bm=mCLTsy3+paAg!&1rho6$zHiUuevA4o%=!j}n8*z$sodGu#gurk3NrCP*=M498e9{Cc5 -ruLQ5EDEnX@%ZGi5>GDIs_3*>lp(d!5;ayJ8sa>50$sn{xj!|R%#7*ysfg+@1&o~I9wNIf^@Q-WSUl( ->~iN?C+*N_L&A)hKNpGt -3;1gM8s@+Bd86wStlwQtp7i)PzESWEc3Gw*uhBeS-a*{i2@Rl6nEdrX#PoN+Vyyfpi301q7oXpnpd8u -PL#s3Y=0K!JAE5z{3S-8oXVpNd%Q+CqVNlC|xTDXd>8yAFez_EJ@!vhrJmW*CC|3z6WeJbrusq9wmP= -$Rwc3aa!%wC?4TV20V+xu^dxX~O|3FPbA^^KleC5H<#-R5nP{l(dJ$piK;Ty^)U$=&4b7hm>Q+b$c|m -r1d%C&hfS+)g;pxRT-7jU7$sQ7;QIk1%?0rW{H5s8FrHTOKR?h!jSK2sR}Cc3QG!ammIA#;q$EDTel7 -zH&`(usn2Qa`S&@dD6ivEV)u{i&ymka^|-3GF;Oi>Lu0o`a-pSvzzm{-Yh`DTinN_f)zSSPhgE#_r;x -R6SRBWzq{fottNP<=OoQ>ln(oMww>sIE?K#aW@Y@-R=>dedS%wT(SI_w0{OjUbACHQ%5#%Y-o!YI&QW -$iDZZGaU|-9dBlGCHRmr}0;M{q9{-(&^mF)yN%X~>`dhpp)BlbE4+~&M(sHppnV}@%xUrOwclcdCqRd -`izS*J5Tg1AZ(j-jlWUHrH%YWg+9{4R8=lmI?pa@;6sUgAvvyg1$1GfGG#p5x6^yrb}aA+u5PU+dD$A -Sr-iUQ6CKZd>A8lSL30AfKmvP3lQZ4Hnd(nX^*P9Gl68t2NAcNgQgO_0Jp{_}M!bLa}eeFZqV)&{go# -80So^O2#R!LQUE_tA!=?iobHtK4n%nW*(lo+saa!4kGcSw%WpmUe?T6dDL7uZ#MT%zHnBv4rY%%Ym>+ ->UGRm-sgNy+I>bt)zH`NM8D|+^dS!ayAsl#DT7Gbh^M&rqBbw+B-J=MKP%l!c3sy*Q~IK%r=A -tKT4j7O015uMW;9}+C(7#SF_nGI%e|#C&R%?8uFNj{Yq@fJ81SU6hKB(@^mG^YFrnDe%$zZzqI5|pzF -^x0qb0TRXueuI|0vR=8~Z?Sc<~fXGTrq-6A8P>g}p+s;0G$?94Q?Ym?M3O58cKFYtsMbp7L}_xzi1}_ -|RP(B!+~2wkp6z0FT{s1dB@GU7sfL+}#ds2v#$+9r&C_n(0){JB?3?iEv@ocRP&8WUpF{5A@XCdP$7D -bTqSi@2OHWI|qE@dx2T`t!CU3g5oM#0;_bYvP7S$RGivOWbJD)wSJ)hyxr_s4&nOUAz7dv@;{GUS^41YRHo&kLj0F7dOg(Bdbb5Uxx9r*#WuY1yxJ@ubEvH+! -U4jSFCM8c20?elRJ5=jA|i{jW{K5)o2A%a{gGb%QBZZ6)DMu4e;%P)|5LhBoG`>2(T2wZMr8PVh-e}> -?qZ$jah@8;STx=Klq5bvx}xb?Udh_&Uar+x3d)ux^c$rmifz+uJVP%mYZ!fIT0Ba9O}B};F>L3G=fje -|VqHryr<*+G#i(xh=5VEno7;Z1D&l+}SB+WgAo}ho`o>)5?vh7P(i4f)iAZUcHX-sS6Sq5b3_>o_^m+R -qy+Vgi}}+8#ABrCeG;==mc7ar9_er-rqoXFKT6l2cF-tw8KK%ZNKFvGbd -a7x1@b{MsquzOPZ?H_d)^z*n9euCEhSqjSV&*{U*mK!cU@8VG+u^1;738C(e+G)); -s43$F2?<-ACijb_Y0U3^{7%o2TIrfP|EZYszhwLsHE{Cw1V#o10BZ9*FsFdDu?(wMb$p?wgr9;=99qR -PeKVMsR_mfUMvT`?G>KJ@2V)WCnNANT?^wE_Biy#qB^VH%Aw)Ecgv6IC49i0Ss=+lKO|U`_K%09hCCD -sU^hxr{`&wcrbWs3jFxlKn2z-Z~V@mg2LZ9hp)3W7EN!Ok9vL6nk&!`e=nN<5qp&BZ|(NA@ -wDJ51@oY1NA>I14l+sk0-E+q=7x>SaNBrE3je$1A!WEK6YqPVvvjZiOF>4_K1hAd;lNd}5UU#oZG`iq -b3NmJF{&p=6gBT;}8Q?t4dCs{)D`vef`n0(nvCseinqH|eQq6a!hPt}kHCZLZ^29tapG>s7{k&;79)L -!&>iF|v&*+M$;?kV2~OvLap&Umc2I$70pcdi8BZI;i|Ac9qa*w#Hw_xH8i8&AoNItzVK+ImF7T}bt~V=Ce~JAUpszG!B}tJaT9yBTU`EwroQPNaINhGk~N&(9+Q@TzBHj*N*&^gToa-WLWumUpP&K$)=*xFa9qQzoxOhGRNZKj5aQ-GJQ(5B44Spd?U~amhMU@|r!gQm4H3 -$F6H8d9W+p0PUjuyF7^y-Tr;Vi$zJns{CFZ@$^2CXRw_}SL(Q}5pB4_ZBJFEnF_jO<*&eca;p{}28nb -hXDUPl>2&J2ygXEd#8g%IBz6bvS)ZRrGPRME6J?CcC}*4Y*}@bYwZjT|Qg6+nh!tvLfp^G{(Vq1lY3y -JJP|7SH*%Lqdc;MN~MZnGu=f??K!6rj9VxE`C;2`_pIPj##;e~_cKOw@<9{=+%Cg4%bLjo)YYy)p0oy -uAtP5-j?%?^|yq6_v#8f|}_;pTn4p$M>^ko7(oS$xJi@4ny~@vCAMXX#ILg=S=~b(|zOvxJcKEriMfw8LHQ8ha9-4ii=;B+V0VesLW4m`#0v~ ->jXTsMrTA`tb8P>O85%zvQ3)T=?FG4ZTESAz4t*j#l?EX!c`30M|hV-zRBmyJB1=aT -{ZJ^edcr~ZEGx8&Pv!oUUCM&*@4RvAHadolu0cPiu1YpnK}mdUng@AeU1}M*Nu(6es$c4Nqu~R*Mf#v -y2!h$DUJygg+Rh#^9x7?Qyb9jm;*4T7HxAvFBs4LOr4AvPDE6ui}RZNweSt5fX1BBB_$_!9QfRg&M9w -r8@P*(O6#Va%?y}&?texS^V=xKOAHn{kdJP64^ -3yHBp`F8HgRQZG2>i=Qk{rM)+Q~DiMQSDq+;}4USMvaJR~#Y^~jxCf27sCQw-4KBd=nlH^L6w{+m9Li2j*(--#?BsJp4 -VIXglCiKi3HSRQv_eH}$dhFP7ngJUw+}+s$TIM<2@}_ZNo^MNL-P#SBn|tpDYA@*uT6iLFfj^QEPuGC -d*0ld|GdBq&MG0KGCV8WgQWW{c3(rG>ot@i@V8Q5a+bL_aNZs21w)8B_;)6h(`^LIXH#<*dY -p`x+&eZ;YvH+=I81><@_fm)>yHa%Zlh2*dQKiM0!z ->P+gMUZYGeu|d?1D~rFv|!@$Vv*{CNr;=E?A6K6H&;%hj|lxEOJlD9ymhX{^uOmTDPp@7XjePHit0It -@S0`A5*T!x+o?rF0*ZDd_dO#_p~wh3@NlF2g&wC4O&o@^JtIcs>jIzIaxF;GalhPp6shb2%r>1J9*Xa -;6pn+l5yWwGnwgZU|JlLc3bUc$adO|0DZXrs{$NU77+dDr|^ -QiN9B*%rFMCBjvHrvuvHPqYmCI5O8&-+A(l10lGM@w{UQz)~)Bb{o!{g4q=Xspoq6F2?&97dH)XMTll -fX#@n+e-#88l{yYC8mv9u4m_yIZOuL6Pj=ZGm3$TX4@#v7DbuX=S -$Nt*W9;JUFPb28yj}UFWM;Zwz!&2QIEST!5(v{CA&;3Y~{~?`O}{bu;@cstACW7 -+wubcWT~`@u6lbBU?|g20*WhV)e9G3;E@Nv)Uu=A)imH@^{f+9^bJhLO}S&dSTW7111yl)&JHB`jxQ2 -&zlKta$lZtlY16}z9pZfc49A{`4IP2G&u7>69+hMe0I54Pf)TuWA%nHuHT?O2cR0Tn6|CyNrh|70cA- -MEK)_ndWRHHnQspxp)5AZ^<)VL7}BUWX~hnm4(8ZfJZ~UyxDrwCq$MdH(mJp&yq%kN+<~FF&ehq!O^= -aL*ll~P6l1%j*JS0-I?yQ_SuGGhJMh_I?&9^5wY)z$FQN)~NyPJ$!ed|~XGKW_T;cSxmG8DVQH^kWU0IW|M4mBH`+!XSWdEY;v04^Y-5ejeXVtx;IZuGO#O%qXMlsG+8OzBqN%BwY}QD0F -tj3P_9Vx1MiDc9J%!xcLwKr0CRx{ACLAhMl*({?6eD}paX`lumg7s(OFH5{AoY7$JV$plrd_!608NL;oJ+0u}2X%jDu+ip6Mecah{z*^2|EgwH=UZL4=5sB?47!kGq*?N -I@GZy)AJ_E!S9a;ApW;2P*iyRdAfAM9)J}Zg~R5-aTwptYx_h-X9D)6)}O`w%@zLlP97^cbnbDZ)yt| -UrJhxUa(PVvB#oV1`G%f$&a@bV37z=%q8vg+Z)w2;mi%s1alyqftG54xFH#e7^2kh2(F!Y*0U(Z5b@^ -f!1p3ZVcmKljbYU#%Bt5_rR{s=~OcEh~H7VY1s!dkrrYun)O!ITn^WvzhN)cUL$&^ao!;X!J+mr(mst -h9*}E2O4MfG)5%vYoXITeM*%-y5WiwI!jF(mbD5$A;(3&y0ZcnUB0$?Hb);Pq3NGGhj28AL%P#8YR%b -;L^Riuu0gaN&F!$f2xZY9yhJ6s`4{KwS%wF$d~HqX2lm**^Y2FQ89A#BoCZ`+ -W5Xw*KGK!e*8i_Bl3nG&1Z@z?LUWYf?ONiKNp0#AfpNxNqs;dQ3l#*a}@%djoqxk-WOg{Kw@Dc%hE;R -EM;gUfsO2S@e0(F;(ZxUnC3W&;WkVF(Kz(-+-<3PL5i_`n}@VIvVUPPk#Y#C>&eZ)G`)vmXGTh|qB{iF$N@i&rqg2l61^CE%qwNYX%Nz|)g?s;)pmqSX4~%qx>VW%TeQUk -hs}VGaq2nx%z7=9j&-zi$UEqct3S*!1f|VUv)lcleUM|p1BT7G1qlMtsflyik$5BhJhVbUrp -N{;u*4Idu`!a2FU4%8FA3llirw=AfKc?qz~e`MU~t^XN_sjnL=pKu`;$dZ-35*0q#3};Xix2E$=AYfU-_zXtM(OcDi=YA+KtK|=-I*k?ol3c(VxlmdT(dz -zOB@|O-uSiD)V2q8Mr|R!tLQM-mM`d<9xmc-!Y1QFIMzDM8Z91ObsWwA#VCTI1?S$wwsYF!ghD$a?^UrD&eDr*EQ0TBh5U^06F -XVoSVXnJljU+q`?55<_4Ukn5{_y!-HlPZHn@u+GS_HCW=xxtVjjoX0nYBxk#RN{;fbqGPX}S?<=VddrPGVg;MceZco(+j6xh^Tp|X#D~t}B5$Ks8l`oemae>y?{y>B -P|D$SAXL&ocP<1$>%EtEcU{tbpmpgbd0F-2ak!jumku*oDR^+D@e|TEKj0z%NkLelJCjAKG%JL0M8y? -{B?@N51AcB?rGp!&t#2e@7c(~3S~Ye{9@y(Txebt>g(}$SwvwAVrs=LCs0@@;nrR8yTkb~xl>Ql-j|6 -Or-{DxlkJ9=cIp;Jtd6HX}I+a4apb$~Vbasn;fzj`t>oL6oV=-^B8xp+~e0AyYi`qLx0Bz@X#WxR<1B -|q0D2qE2;i@V2%pUj!D7&s_xUS?NLv)H?1yXwOp=e8fM38IMhEX0PnJEXI-NY^9AgBW8LwPnwPL)rTn -C+Qt-T?TF=PcrkaTzbt+3z~_xff-zB)s)y9dFUhL-GB12yLz-Z>NyR`ebYGKC<_FOopds>?YMFO`>P~ -V%Oh00`@rsHAka&7=0tKMr>JARSC#vQ}yRbFvaKFj8P?5r7xqu6yrnD+6>!5Ya>MNj?=#TW&#AJD_%r -pTtd|dampQ3{F(D6fdQFlg`8m>T$xv#HzI{EcYYT>6}71Ys{)RjgfP~(<;_i6>aN6#2kD;GaJ;uD0&f}%RPTM{ayyG!I9(e=6=uxr+O(r5%A3AztE9-m^Gk4Cgp^O?A -sw3DvI`If_I9Qlnja4JY_#pOB0lkgo4xv>ZT{L;N*%_|P9!)%kIz;F<3znnuLu&84y`zJ}-P>Ll9OFb -*soYFu&v-dpOp^^lg;$K&9?9=ffw#7E9m!M-*EdFc#06vag%#p6uHFMHD)M5AxHwmLToz?Z0=LX#{nR -+{CmmmU{ES{a)X(t1E#bJw&>L}puuapOGAI))A{^k26w*+CU+wUQ}IO)5-jm2*hy7qJSanCtsS5kI|p -y@on)ND{rkL%lY9&ETCa7bN_2Rj}h2c8=5i1q6}HKvmjg8p1ZkZTVT>$N+dcZ+pX)7;r<%_o3D)hir{ -bsF{h4qW`kS!snCtP;3xk^Gy8DNfd)8eoEkC@Vm+k7SJUJjKLY>sq9mAvZKk2jaN&L7ihs5UzQ@qSaP -TzuRuBHn@wqwols{FsGe{T4pVU{_OTnWV)2(;}h;GHxoVqKd#Z`txA_^hCUaCYVAR}eI#$6xJ!5614^QVzT!*5m6ph!Z<++Hq|Fy&P2Hs`J*T0 -RI%n{T)I4Q<;=2+P-!h>6J#gCs{hcQ)qy<&0bH1ze21sn+bFu(z?g<#e^xfP~Tky15Xpf#7Z|dcMxIn -!X>)UzOb9f@)c2|cBF>V)HQ@DJdb96b&E7Ku2+i7>gx}K{B7l)F^v9h`RFLU*e{pVkjf10OXt}8q`pa{+x0;LC%*l$fpKF7}igx=I~Y -~JqNTIRI(aM8Z&In@`p#}vJn!zz=9_McYi=E;BtI4JBolW#bBV4P-Psh#GsgcW9VY?a)G3-o-bNQ08|F{LS -QXlO~T)t3lT)x`~_z&_!2rkwM8Nb~hRH9sZn{hiBSe;IwlXVkS3Apb;V#!3h|oz%vA8n|Rx}bIJ?BbP -?0CO?+`b%N_l?pG(>g2S4eN?2DP$Aql&7oKSynz+)p2>8YYSJw7W)_akrM$~E+QgMY< -H@aEOJCY5Gh_y=k*F^Q8BuOdISrPQZlP|9o7$co7Xjk0Q#-xkz^2=-aXQp>h12+z^#Q7&^O)PMs0Lm= -qXWd0O4q498cJYMwQ}yDj2SMAXg9NMWNMq|CMM6Ni03Yo>^@sR$JVby(fS%fvU+9wZVUUK4CAi(^b4? -(XunF6^p^?O3ejlyFJ^UxUuh6`jm6S{42#DyPxl8UdXt|WZtg&zYIjfZGc=LCfwH0Zr^wIHdq0M&h1V -Q_#a%%9J0C7g4~*^ka#2MjGTiW)_8vKKRV-PZF6qwTPF_4jsk)g+s}F$9*-d5U`GR`JsBqp>@YbI#04 -g}IDR?WMJ1bR$PO}O}vRlNKiTg(vYzx~AN!08!RAjMz=YTh#zkbP0(=NX+;Pxsz#;gd!51*5!Lgmq#$r|_R`W$cNK`OdkWW@*QCam5)Xm -9L!7wZnU8nuQUluShnZrjW*ji@v6)g?IIXPV)C@_5}$(0MoFEw>hE2aSQ}T0uUO`sIGNSJ%Ea1gP$=* -Pv4GdUC}{_GZqm%(jM{UR`a~b6*L;&|Q+A5>7iHluzYKf#UZyd!3mm4m~j1CweUwI~cjwG|!?FN%_-i=4doa*rLFPIij5RK_vu_q;5AVz9s0HkOc|2N@a&cCG^f29BaME}d+d+KdU|1HB0|3( ->A$u<$D&cg_FSG5*bqA&BG7Cgf$ZDhYAr`i#(D(SOT0%fg|-PDUG5RhI?t9ehhK_!YV9GyZ244z -JztXp^IHuBpec&Ma%-^~f;@IZpn)uIdl}_yHCKCamn>K6b&O)UfGNUm6hjggVk+f1V -YEpn(_kwZFzXpT(jEd_4}Uan{y&5>{i;*7zZ;M%mVQ-(O=9ud50Kma}XvD*hmzJi#Zu^%HaQd+V(5`Zk3M={mPS8pN~4brzTUg -8CqyhOekhaceShD- -D`siV;&M^ONeWB_Yw{;Q$R|DZu~?j`_4$*~`sf@W_a^*Kd($tvrJy}N<;ny`L#V=ptxD*{kF?UK+Dp% -MJNrM?HHu=%7cX8s)3cGwzI0uwMDNY3IlbPTcFXb}EVbx6&sEndzIM -G=MjSG~=`x#IH$EcfEHa_Gmcr%mhBd02<{nagm<73!yYCo=Up4-_BVY4vbzvKO(^T>u+C-6kXKsZS>6b86U^H~o4 -(=ZveLENi9YnkH*RRm2Vn`c~nbe0IVC8q)E%4(>RXkT0hcX2~u+5L^bOZwI`7#S}z~veo$G|bj-s#!8 -X3m@1d9OlyP+cxMo3}MbPm}HrL&g5y+SP=8roXDPJZMD3vz-I)IH3!==(gQPu;SAURF%9Nbwj+6ZP3T -Fi@exmELpIGM&(T@{k-p}AnuBN(J+$f#V(i*kTaOcU_}NmBS1*3L+{D%1_1#|`;Qq -}+LMeGfTIKg@8TpA)aCC-O=OFW9%PPtl&QV-$op7XOR(I9z4E{y_*X5ONRqeTAt+TGnZM@^|HGL+!Rs -m87PKbXT>Zzv%#91q6n@==%uG=={$L+A+E|JL*tODnI|59jb+m~CbbSHP=j_uZs4iq2Kd~FZNA7`NA! -k)q2%U1o^k|{YPeATdiJO}#nCy2h~XSIIMZmsMWJYLniC~9E`Z|cnRZuFrpRx}H+MQN{AKma@5iF!9W -;f$0#Tv-yGv01zZ=0o?|qDN~1k0PFZgS)^Yul){@@qHx3Z380RWd>s>F%l}ES2W$riC1KC`5@ayxQeG -X>J+I!_MZBE=wkp6OjG_41vOwNM9pv#!=yJS(ZYPQ-@FadDD?Gjy{_OIIXbg~#HGKIvx -c*HUhr1T8gIn&TO$($;D>{1u+!V~>yR9-HFBsK@Ry4n3f;u@-a0oxIZ?acqvQwsrS4{u%jRp9Bh+czd -k{mwgkSA$h+Yrl09?r7tEkvTTnG%k8e8VyW&}gS2vq*ku00o)Ue2QpyH_^pINLUtsQ;rnL21K`06WrL -phoNf*|sPZm1rF2+a)HyK|nz8n-p02w0lnageyrx*DhP_c_AovgKLn42sPjQI)$LGSwQhvF}{dkvXe< -;|7v}oP;k`^^h?J88l&9%9@=B17_O<_7)RYdW)!CSn?-gIbL(dpS1@x1n--H|w#GNtnp@hCsP~*&+N- -cN)DN25*}9%AikjHqZzj!9Y&AUf_8PYgis?O|9yIBhdCY46zDNq%K4c$iT7;#!5$-}v?yl@NOmR*zbSZ97853k^8)O#{BAc)G>)(u;aqu*9@g*@J$>w&JRkVuNDYg(3!HEeFWX;s!KWfJ8;gnW -jzUGYUgkgWG#6N~{q1SG8Jd1yxKehyN|p{+s&$Xevpy4+Qqq)tLn6xzo=*~BD$sw@82P85H+bLS3S;A2Go;_?O-vt6;H5$8 -G}2*iyO=Mg$(x%mdNAmR9UL{992MWu74KH6CkM9aODcSq6Bzi3=#g#D1L=LEi2r}*R}hM01u#JkH~!y -hZK^e$@to{!a@A{w}`_pard&p*!?PbJ8^IU0@V1=^E|xh9d66W|pUA=%{r15ir?1QY-O00;mWhh<1Q& -nXNA6aWClKmY(B0001RX>c!Jc4cm4Z*nhabZu-kY-wUIX=q_|Wq56DE^vA6TWxdPHj@6XU%?%3$}_Pf -lfzf&*0;;os;x@0dwHo%ouueM612uQn$Zm@TX#wR`vFfAz`RLzlDpk6t19;ZjqXPGg9gwXj=RnJ&bhh -i9)^dFxVdo_clYbf;PAR%FCPYRL;X{yUA$lPi}mW%c~~u)bt`U`i$QGovOhT)bv8n`KJd+Iv3e_qI%W -R6S+DwmuZCWmFZjT}Soi(kNbuyO=Do;o`NnzUOeT|)%NLKX8+d2xBTTQIpB!)^;>4DdyWkX&;S>otmE -w?{YUET}#&e3t2ywtP%1?KVzF|<Or5lyhiRzP1y%z$b8XJy+mj -_8>4g2O-IXRwg8w#4~mn{0Z{F$mB6`63fYmW9?8BL75plC0Pc}Btz>SR7`V1AO3`)jy(+7s2);kg^5X -|kfsPrlWbu{oNBH)(HvTlpK3QEN}DJ!T2V}#%q(}5rNl9b6j2&Sc9|lYJu3Xs)u;%ya2aB#G_Eoi9Ok -o8nEAzMhlK!vMJ6R!Lc&RIITbj}BrRh|8 -QY4YB|tKJy+XFJvf%mO>U%fIF|ax`P82i$G04^?3*(o^%LS4pVN?mL%E7psfS`R7zak3Fiew73nzsZC -NNWG<03Fq3ua1SrkV9QqbV}$b4F8SdQH=;7#nI=!cInrWp)o{gjnVo%NZe-S;R6zEb|Sj86lQg#4>6`na44 -_^HW-ZEu)RVbZPXaR((@_(NxSX)m&hLau_?a^PVVX;eLV#+|8;T*KnI1#znJlJaD=G7fpJnq7WqRAn3`L$EihSJ&btBdd<7!lpR%zmjDbm>2c4Dp -0v_EO08=1C~Qh;e?>|npkk3`$aQZ1b27|IPAWi~b{4ML^kDp4_dCDTdbm9%iBsV<}zIF)CXvhot*=%d -oB<|GqXnSf-1!f-;83H6#*3SMetv|d$M7-0Zj#a2Bb^(-(PT+s3(*9Nmv`;!CbD!0Np37cBWt0*C>!n -YifDKZmRNiwC?s>pI>Emx#fjc&}n`B>A+mfC~zBQrzefi|ZaTYl)G%cti8-(~vb_uZ5@l|K1>+84ez* -Z!nAR;P&NQJ@|$P8O|LzuoZr+eJgq98xHtWoA`6X0|V%HT0}=nq$sLzggea3-#=xo?3V#ABDCaU3_}L -@a#DN?3E3FuF -NdqK|c!<+@SohK7aFxy*7?C@^}!VDwgi;~fzWPz;$$>%Jvw -IlhQ1zW`*Rf?gx#Y78#J<E+<#>#H3bEuIBa@&&k!?W=l@4<`&hwAj^e$z$jd*sI1Qg`CM3d3-YHV1^m0hn`y_`>g_t==nD?=OrtNQ<_3FC7Cp`9Fj?`Ohz)9wUf~)n^~EhWO8dKCp)>7DM+TUc -1rqwsSIrv(ANuPVK`Ko$dB=U6OV&fwdPh+uw>)f&kI*KJZJn0Y8;Bj`cWohG&b@`U!^F$u`)j0rM^D8 -E%`XQ+ifi{}`MGKhG -w!QSxiPdMozDuPzT1&JH+N*V2eganU)AH1y4|k>Py1{$kM&Q^n|K7YM1*v-EPB%f%q>&U-0)Kb<+PyjN<}-j^q{7Y4a*{jh4k#n#Y9#`sn|-YezlW?a=R5M~!UD -{wcOA*FM4r)u)qj@Y6bB^SN3S`W@w>y@}TSjx@&bAEp>mvVY}D;~dH|9Lf#t(}ly==|+w{czt;XV -Gjg(}SWP$YFVDQ|ne*n2;~maL^C=1CJvBae<_B^+V12uJ5C;sou=+ijym6*f0{o!J?-lu6kyncRQIQ`N`FV1*4xqour4qSTFVn8?CR==|G2vP@$Blcj^w{YHM*gK6dsMq280s^bSR;D_i$81a)3v<7Vz<#R=$?zb_ -7S_TTC7D6qCg{No-j|0~QRl`Auc8GQ|2Zf~|QF=iWqSHj6j5-{&r4GbIgN}e1iar38LB~}m^wl(B2-9 -JhfYL~W5t=k)=pe2T7=^V^v;n{ILE%z}i9=DNBhcx5)rsaxIPx(P`D!EzX#{vi5hjizbZV?lR5MVpU^ -!|7MFsdmKnSx$6cGc6%vDm%Czh{ZX90v6fMsDw9fbvp06&Y=LbBLJCxKW3Gf7J<6*6N;D8>{Pj4L--? -SUc|R>Mk7GuN28R`4^zhhfPWEg8d-v4%WrG14Z~fo6*-1^FjnxkJ`**8}yb6M)v#Q99g%o7_`w^0{)8 -`%v*A>&Kw*k^>*^D=&FK7J~?sQHO)VOO7PrNK77L3vvL*W5mY+90zbbX+Yr+0OJ6R12A4ghLGYwiUS- -DR5+o+u}`?5Z~y`CutYUhx<>4^+Y(DnRa5LWVy_WOu0!aA33Z -?oVlp}fYG6=P^3(}t0|s?c6H5VBGYl|n#F{0bu*ERUNgeyF<{@=rP_n`yR>MmIu10dz4VJBL;6;Ow3B -MXyRZ~{g0v*Dy0e%heYjEf^o^r1NVh!@7A&?s6MiYdf7$#Vn5K?r0G}y)s0nz}Fh5%^*NCQ9`AkhGc2 -I(A(FmFlxzSK;Ew)FC*jmKaBDNN>wG>s0s9IY37QwX$uBG7G -Fak}eqvmQUxE8^+6r2E#Vy?yooo)iBo50l~=sXqJ7b5XNLt;gs43reHV?=`a1%oisL~y9{02F#0ehEM -*IP^Fs!~rd8vVo&h08Y`wpfrHfRJ3re^J37kqei+pb&v*~AAu&I~AsdrLh8Y=+tIaGaqYk|14Rs(^yXe%IM%{gq!taKn6HpPPA -`nGKQFQPLcc9k-FTo%t0IQ%8fsxLGYglzj1$8hI&`5xhfJT5;2V}aCgEpWFo-QV#7((jule7gz$X(-s -VgR*?{D5p5pwnWcZK#70L1G;=JB)XVHAhy=-IhAS1ywt8?#)Bcm^uu!p-v0hp`+ZCm7hY>cU3A$8i6K -YsT#$1eWDRq0Xi&1N9C}jHFX-$4jm4$t_B4Qsp`x5WhcOzu#PL6yjGny<_dvM{$0P6m9;IG@x9*jHS> -PG{EsV>b^RM({l8bi82*RUzn@st0^}&54o<_5)v+yniUbw4m~5|hZ9vD`mC98B@tbSYoeGmoA~I89{< -fm>NSzo%R@aHYxI%i>P3rp -RYz)s$>k4MkJ6P#w~+r*e)d1<*}uDI?CR|5<#hJy^!)Y3n?L@`?>>L=r{CXvMz8IfCGUIZ&#P|Ti=kR -92eFy1>vy6t-x1Syf_Gvy9Gh}8`R?+n#oNH}H2iroep9Hv7QMMytQNz~&9oQG?o=C6zV~m`>#-x>aJH -NEy{v^+{TJ(oqa=ER*LTl}ro*uLs((`Mek0yX-;4D_y%YuSZ}at?3g+V)QZ^k%>2lRRDL)2Qtv7d-5U -u^9^^JO)_Vfhb$TeMxe|uOD0;@_BNZ9T7vl6)TLrmlf95SC!B6PMjh_g4zlPF2eC&!MP*n@4$z~0E73 -s1s`gnMG_co_T0S|9dDj{A`F=2oHnra9|kcdwR#&*w7-{%$m`k8Ym=weP`$ZRdguMX -H$(qnG2Bikk4olZviAyF|0kz&)tF9<553dym3kLji?@rFqf9ukFge}-e^0ybkjL-wm9twfWTdmxqsb# -(-AiH+p8~dw`pbJ;L3rR|A*IynQF*S`E@le= -9zkLWoHUsFOv0m{&*27xf~_(XXPm&{2X1Xqc`1JRnb=0J{O>EvCX^qh1+L;aanPP%oF|jhIhnvyTGwt -#};v!TIi$Oz8Y$m>k6e(@I2~Ej(U0f06mUapZk|rjp#gCjS)3aBv+*OLhN3@8rmjo!`CaH-d*{#Cf+^ -wS2jp?%6w6?OJrS8h!)dul%&Tny? -I-m<7*m`+|!X6kny?z1?M{$8H+ui1Br5ruF$;gt;8+#(z!)$Anw|`Qm+WN;WdhB -aqhrA0rc8A~T9RKoa{GpUFy|W~DzbwO)k&mHiCNhvd3*8aF4lj#Cn}&080ML$%Vr(spEqvkEFg@~y+- -yOuzqPd6n|IsV|2>SqX`em`#i#GrF^Tt)f3;`IruI|bQKG}goy)}hDq_Ff+piN2aM%{c?{Ti5Nu4A5^ -eYkN;U0AQM>|%LpSp$J6IwwdFZ}t9Nv{tj}BGM^|AN -o5^>dx#9Qf*Es68fV;>%fxCSm09c=b<4zFPEA`W@6aWAK2mly -|Wk{WBhzZjk006Oj001KZ003}la4%nWWo~3|axZ9fZEQ7cX<{#FZg6#Ub98cLVQnsOdF?%GbKA&~-}N -hCsJ2ALB$txkD$6PBmAKApyZpH1O4g}Ba!6qX0$2dlW0U>w*WL3#&jS?g-PB#xg_U^4!Awt2Prs)JoL -9v%y11CHnpGt(E~0c<7F81^le)-Ojku8C2hl9OPV2PD2hl1|r^QTMWN9O+B&)aP2%=2O(Z#jMXGJxt( -_0Ov5x31nb61LBuhFu&7Pans*)-+FG)qO^Y;CDFoM~F?Z!+rI(Qn>wZ8g> -1{#FDZr^n%4BWma%2yXO-T2HE~sAPBbJUZ9})@nW%m6*MGe~4@90y)i+x^_DEipkGn+Vo`){FKmXYim -1tSL8#r&CxZG4wha=#XM?hH7}}YRf{OiqvldXWhI70s%p#G16m8L1knCfQXPU&X;z2ZtU&a6g*gn5bvUGN#g -)A>F>J@C`B54G`i_gBU#^dqskpAPD0(OKCn!D-b -`BttiQ7(XmCc}~sfyWZDgb7bW|>$d8H0fWt@Jja6q -Q-h09P;TCYuWgr(KqWp0wDBu;;&y<$Ir*W!|-1qe -7w`|Z;7lH`@b6x5VyR@FDA?KLU+SHWs&Amc2PXXw5bQGT2~QvMY^wtf4&CEl+d{ -iO*)>p6W4AHI!$%<(Y1IzBzW!^_UkL$_w4{TDQF39J@n3=1@a9(k(~2<#==Kj`f%m4dq0)oa&a-&9Re -0Jwpeo?OSX|qaDt~{1fD)@y|!N>UT-k84Zk@9`?n!Y?9C034@=Shtc5)u~(aIhrQ!t -%qRiMuEHlU{$T{qj*PPHtiLfBXiP!M5Pw7>HzPTktrJeItNA2qSJ^hBUHHCRG_{Xo9}W<5ehmpEedD**0g8i^YeX+QxVX -^gD4YukH+`6oG149-F7{d0RKJ5ZY`>{5Lb$7(9ZCM-4ftBBFvWx=EX!Y7P%^)o^>23gqx0G{)+g;+l86H!aosbKb5R_J5{UJ;AlGb%VtwJJ-%=Bc_Nb34(Bxlu0)#71WYjBw#qRAQNs%F_V_b!Z=7+bT#d -z#>cQ#vib;HnmzW*1y;JhQ2B32E0>nRxtN56$TE=SB?Jq`SXX5qAm-F>X}N|j{a6uS5ZfSNQHz>#89EscG4Lf%nG`79J#H5+%B=1Fz8j`JAP8Gh>Wd$P8|aWiaIdAyQ3|7W=eyL!nI-`^w8K}xhSd__{R#n*A~$7ZWp674!2Ias4kUM(J;~Au -n@V;DYUdaW3E)YfbP~n=Dt#_$;8pf37FEiMR7%hO`?}L?3M11DzlO1foY8#Gg~y}7H0^{8lkq>B#Ea#+tDXPzRjBvH7 -KS|GE|RuAUa}t1xv{9S@snHK`WJ&xK4{zt&&_$00|G@t(Kai;~-RN<7$081{SuF#!lwjoEd@>92 -Hq@d(Yd^u>dtsF3i!#=QEX@YKNQl-LKg9-EvZ)!mXZjU(y -t9r*nHQ0G^>>Foj0>BFeUr+*mjdP$rfr^CR3#MF_sFE)pNBuF*PMIb<~`&x!U&sOMsnD|`@yGmejE;W -NS4Qy86D$HL5=?z$(oV~>wHFp5xh>{U_-qNKTo4*35(dWrOnQ(u -_o3nsjTN`te113V1wk}VE;V*fROFHwZva)2eV{KQ`lMT)*8Z=_;E2emXh7vP|7jTTsI6tR;ZI{5twW4 -{{R~v%nY}lhs;ET5V+SyPAZInqhwkYwP)Zp3;OoaCNRe;U`rp$^f4cCkJ;7T$O}i7MgVT;@i!qO4Tu( -&1vq`!2eZ&(2>*9az8Ig$B#@=Ib=o_6<+}H%Q!!CpR5H0vhY_yOsZOv{x8q~fCgWT5Un!YOk6z*Fe_dk0GAd5TcLfAs%sx{1;+E&U529<)N(d{15K@yUzO -7ey1(Fv1R-7!L!B*`*4wR=d$Jbs->junidDeZ{+>yXf0_A+Uv#4Q~Q#)6%mgOTQbfa%K^=0nnaWoH8n1(bBttfNI$tjb>B7IAr?Ej!drQZ>E -4!}K3uvzt3qtO98P|JIjOfP}6+V4UPR=7K0aT}Rl##ayOc()yFu%^#9fuJR0~-=_obXzx5=%v(*Euyj -fZ!yovRfE~U7qQY`?o`+5Mxj5293C!#POXn>-*sbpBAs=V_Y|5VAILS98SG--GL$}-d>OkvZswXq;QW ->9~o(7Ji&2BS^2G@EwK=$uJKYVHy6ohY`#u8#xJfl6Qwg9*&$!QS>G@0iqOXuEXNA1M9>ou=ROwdihj -RgzcnxK4|tyG5mj``fcAUiFv9ki0*eva%$m7g`h5}nEf0F-vsX-UFkC(U;-VR~82Xp52`{+=*c7P9;&@5mZj|OJ$H{c7jz7-PZ7`JuU~I}81LQ2(GQUt7MRUv)iNBuNHpFMEiYvSS%X36KrwxElhpYR%FQ&vGnN~HK0{Qj+QKxy0B{?ZggKd_4C!?*3VE^kRZnFR1D-ogt|RPH5eT*M>`VPUmGT -?9y!?o(C)^egc7>2->3%Rf$3^b!0ifOeR|?8Do*l8A=xwnh5+xb&8ot2G7N9<78krUuanL?@vWgk_54 -&Y(#kFt=KY^goO>JU9tm9AFM!D8)g74=kB2XN@`D1dB%c|RWLWN=xDpi?gGFRHndLi!Hg0g1$uuf2GP&x0 -DxXab|d&^8``Y`kD%Zg$I!$`vksR{$8&_$!%%YB?@mFUS5Va9&AFR4bjRUG6_u@#C@O`NIWblU;TKbc -%On2()J1d_K73DQlY;#3Dv5trNpu~seN1j5b>KUcJEo*c?m;c<6==co#vJK>JVlndSHTTPNOQ0QXuM) -jf<%;63KX%kPgQcW`MGmgNsL^%vEcfNF#Bkn?xMZ}p6O}w^{r!awoEhZ4Wn=S_?@MK5wAysRQLo`tnk;fDjB- -LyMTHfb)c5Tm~PBGY?#5W+N#Z5ij-FvbZ+s6QiJEPH@rqiW?Q4H!eujvYH23~a*TlIz*c4r{g+D-Jt@FNRAh`CdDy(F!m8PjSE@(gK%t+is@dIj^?SB#Y;vROp3ja- -J@54IlTx;je-|WdUXY-!jr+>bOVy|&YI#9O{!RCqcvTf;Wx8gvZ=uz$`s^rxaF}0ilZTgEBxgJN*^a-14sl8Aj-7Lo4LGjk9ssEw^bXJ-MDrxgYI3Sj*(o>4*DwM3{0^D)RgC48asTD -hYEn08vufluXFN>+rGc)jdC5wPfNp|8c;`7z>tgu)`I8sJ-FV+|^nTk4t!aemERYEdnHaE61W7gi?XsKi!LgfqLn6_Np=obI>t?za1o?|N3H!%#1cHbxp;ovzvcz+;;Ea}~SipTx(ui-?nFPVrP-@ -~3zRRnj=5Mrdtt|XIXEgb4WVEyCs`<>{Zvl=W`?kWpkD~Fb2eJ?I-ML`sZ5_Ct&eUVTvHgF{8%%m~-f -x<9(Hq^0HzUzJ0w?fy&FA03zG-6pc1Y7pQBriHDNuv%p@xVZ((v?F93Z`XQc7sZ}%Lu1hrn$ykds#|a -D7{V|GXeEIE{gkZkTz-rG7)#AC<OonjdI4@XB)_yTiXeEEF(d_USD{@fX?>9zax;_Zp*NgTg3@OvD -x9uu(^-lM%1-lM$`Jm_)AdH_5F?l-O>|5C{G`y$PAbplOMgE^-F0ebdoMsyNEz0UR@o%f@58lcSZIg3pNCZ-=*s-hFOV|6TxoACdZ-5YP_RW>G_-#OpJtW3<2vs)ao@GHrqfX*6oY9rWZd`+o_DxdDoXMb-Ed~e^^ -J$}#-T{H{NVZ8eX=S?!Pb~WLBR=DDbi{lVfP|{7<;&t&epMwyVZ$eP^niy@!N_}aXBM$oCng=^ZGk*|Q@uQ&i&%PSAVEWI+R6?vf+Ux-#0Q%u@ -2HbrK+PAaM}ooSAcrzBu3O|l -m}Pb;I~?>qJIwq0K^$JS8*q;?tmoyRlo%yu<(gcm2Qv9NkIHt_llXx*|_c-#1$ORr|zZ+OT~=`c5f^< -(;l5$@YZ$cg#X6y+OTa%B5lqNO*(_m!olZXfJG3FOK;8<-UfHR9jCqe#ur%?B`Aoy -9$VXh6TWXP|41IrquyL2y7JUp&tkt1if!?<1?FJFreX7AMI|f#s1HIfp0sW5CoIAwM}oi9OAp|N)rgE -i)c(i<8;vAI`eezGmjl%;|HC@Z~{3(`^kQ1ir@B#&YKD&Bd^bH*AK^b^mP#BEIHhK -@-GSoVxc|5-SBS`XpjvQ2r)xJo|e_ax=7(1)hl@aZEq#K)>gBR^N*ijKyXpsNQ?;*%Ci`g}rciY=gjx -fTAlDYIZT7OxVzDMufAeE|!Wr>Es0(1*xk~tx4_l~p6A8JqOWj{nA_ENWNeQSe=P@}Kqz7q>ysdF93b -F@n3fWdaRth#PD?B~_8{9J6XS*CQ(-!oGxnna8A8tW}p7dxGxZ#<}oW$g~p-D%n;6|T@&PG<1PKG

+d`s#M{_rL#rUmh&M|5gjEEyMEfH<$$3!S!R!NS;oCdv20DSvq}7^$;UhLS6CkZ_5U -SZ0zss1>f2pUjpYMD&h~>I_nxiz#;}Lup1kwxW|1V%}?O;>L3aEq|GM;%BCotU>%FCD{yiyzd%U8nbdD7@3C?^jb8tG5>L6$oFv1D%3X~TDD -y603GECwX-Qah&xnfcUye#he6Kx&8FO!-O**~fA%Ic(Y%WrQa2aQKQ}@@bjAE4-lL+;3J+V#pJA^*q4 -1QCG@H)M)IyQ1#IhtmPmuSXoRJyGXxiQMh&ZAr;GStI3t^tEh*7(OpNQatjXm#ZVq;qw@kZWN-xZXXi}b -|;G2j$W}@=Au8H(4cq4!uzjikFg&N$Bc9~G`*H>Iaq5^WVAJVOpjFhOc5SKLM{mUB5bdeWyZ#;G1r7% -a1LQ8|M?xCfq_`gZ;V35uJWf*!C@MRQy^VZ0KgQhHsZ+D5De%PGEBJc_@?DWG7JD{l<1wcE#iKy -cJWnG!VA>J^q#fEfyI2xi|C6X~HLKB&8O3p9?sbh@DLjZ}3Q^746)f=9WjedpsI~dQULmz0x#n>%B?c*Od^3pID7ZTr$13)m -43;*?jvz@`63{I(xApfz@nnNlZ6JFFHin>$p1($=nVy!%pm8GuVdiBm1`IFRP*oC+@HqxD{%BH?fjp -8Vec?wMIIDOJ8qblwB3%5@DQ4!>r3TcQf(@|W}C7lPJg#J)p{Io5p~WbP%|uz1Us?W?dTJ*@%)_i!e_viEzdELh2+myj}^d%=`XR@U2_miXZf -W_t{7C5^#E&n2lZG0rNt==N$y_^PFIZUN{G4$SzwFVEX_JV%)CU8yN9MLjd<&Vgy#2=|cdQ%1dgpIzX -&Batjph}4nJaq&+D+IlcsU1#Z)nSLZSfgWC`?qx3TQzRQfyWwwn>!=Go&Q9yOE-15_?UVHu2s40&nj- -)j?*MB08Wyruru@%k@8KQar*|2zBbNog27yTqQY@6aWAK2mly|Wk|%_@PQ%|R7lO`v2+nJRdEkr^RYQ92{vefZ@_q`8*1SOGFc6AaFAc1>Z@&dI{S|#zwD`Mp*ft-gbmzfCl*_r8+$+IO=+eYC;i!J^yausSzQ)*wN5(#jL -TWhtQM9D^$&5A4x)ncBCOiNE_tW)`n+fVZ3PR8mFnb{71zj^=e#@;lI{3O=7i1U&v^}5-m62^N5Pjea -P^Bn5)Y_;_+Verr=N>wNaS-;;u^V&b3yBa+5-izE{d%`o!JY?p*6loeNU*sUlS5IH!y;ohN2CqN9bVv -MZIu_$U-i-h2+4w&ux93mm)#1q_i+7&C7Mbv29X%~!(c{3AF)Xf%SKWiUfPXKL0Cc^CvNH@A1wMrT3! -&v-z8el-cMi+*9`^gRGB|(!_4KU`l8Q -7J>AH7WD8h*gft{9bKUJV+M5OFti85Vc;f=V^XUO5`O=?;qId99DG=m+N1Dq=kh0Dh^dFJ)Q4Du%tZC -)B;RePk_c55fi>I(_Abl5$m@&6BBQIHCGjqzVi15BH5D@{+9wE2a^;7TT5A<>p$Jr -hg#~@768kRc?a?Jx76&<^!*)u|2uvEg}#Z1qOXYNsN%H9xu82p-^txd(wOfSB;8OfSiYfz2 -YicY)1V`f(nZDvUukl!9HJrwy<}C01xY=Y+niUIZzn}=2Arh1(G@DIPL-sVDa$&|l9jcWFZe|zEh1{9 -!KlshXs|xNYejV=LK+?D0?VON3Y`j{*A7Zd60H4Bno<(4x`-EKkeX*oE`bV*B-l}b80H`+Hhd_nh?}5 -Bbx_aO)Dks}3N+H31We0T(7ZN+?r*?VB>h|VP!Tw_D6D&@NR)!Z$Gq-YmQe3#2?31?s(DcTh?GG4nM! -k7l!(bC{JmjWMkzQv8E|n8Xd;Q)Xu_(y6f=?w74GRui^Qcb89yS|%q3iaX4>%BtW9LEvYD%_>Sc6}$a#xY|671@uO2$W=ZiFa7_vOA+D;P^2ru-0h}=xJ=I -A14ZW#WOCiNw{0FMq|;kK&~QmnP<&zqE9ga%{^iFvY~0}dj(+68ZR4eZj9Udib9tw?;wH-Ft7@gZ~$9 -WrLjS1_Kw!z!n>ePzcOJ6rYlom8bzKU5)Y+OHK;EWzhKm{RUH8YXfK3+hfbTF16BGjyp@%mKz`jzY|Ra?%do^{saQ!3z -}eP~y+cvja0RYK6k$ie@(9!kihLzL(>Pvcm5J5Ljy~AMh7D|;aC^qZpp!%8unZhyj|lOLN4ml#&UOYu -tpE=jbVfL>TSjYX^=}OkwKO591Prj&@SLuhT-qzx3b9+8VAa6`a1@bZ@I){#GGJS2gU!GlSg|-*Zkd- -#-NC~S$%5_>(2NZXVZzQ1uRd6OZY!rO0JhVBXH2Jib1|zKiLJSyp;?##4%xW;b<)itQVc5)Y^1C%HYm -l36?>fMwFu13`%DYZXAp`q=`2AlYZgHnG6%rJqPfU5&L_LLrIB-0R -{$19PYRr@+Flb9~;Tpn(CeiG!2b4Wb#X)GPbi!5<1UNj~y2XcuLzVIhZc$MaWKG`w8nI<85XF!Z$aKq -)G(MdSEo9g9iwgqpk&Xx`5qdw&P-&+HC+hJq4(1%jImYMdj*oM)_L#cd=2pu+x -##CaR8^@J0NZ|JTnTIfGh@8vVziXa3bhbtFW~wHvuIhVrMxQ(<%Zo9+epV&9$ -KX$6FJKO_RtbwG_7x?&bF|HQQloxf6g_%#(>g7pPO0r29sdzpuz<`ba!J1wt-o$akcJnqku4Dw`brPa -Zqz9r5)7!Q0g1DJn6il(%1JcuRg6f&?Z0j+ -`BWujCFO3ujQL47ul4-)aSaoro!`LxRs=MBzC4JFf(!7^6lGStirX(M2WXrAe%Irmdi4|u#*5Jn5K%9 -$0lT7_F7Qtq`t*Y<^{rQdok%0t|A=0&?@O#%x8i3_T#5gn%s9dL|KZn{40Dks>20!wKecXl&ZJS61PL -C2Ja{ce+jJR%O(N6L=>nqTGLQn>C6ru5-bYZ)&a)u3BTZ*^us1!)h=qSsQN*kG>EXIKPRI8_}n%1HQx -X?t)@pCR$$%=O+6i$ugo9H6qk=aJR(J46FPx!8aoHFCOFdE;wI -?cV(xG%PY{?zICrh#0x?ni<1Z#%!U5iU7aXwy@zsl8)QA!raDLB+ka>YlyI`wX-VwPn<<)CY>c!V)ss -t0|5{=%fxQ)p4Fl9CN8tz0tel8zb7hox<>e}H&k$OZkF@wpjXC8y+a|s1A8n4r!Fi6=Iqp1#QvInd7d -~W4k -{Q4G}GQXk$+1Q-+o&`4*<@EQE77{^Rpyui7Uqd4ELzRnG&_wZHNnE8i@niM?7HA&4u;HSS}&jQTzkd7 -VX;6#CJ6DKawPR69*pw`PoD!0F^nc!CdKiktsq=c7kSSXA@mjF49dcR2$|xHZJo@jd}GdBUEJ*s!h42 -x+ilhq&>_>QxG8%nnP2X#v%F{rxn)ni%gr0QX4d>h6~NikernE(a5(K7f;BsQ2f((mD8xr52rN`6NrT -KwVj*@^B-+yzVU@aE7Sd#5e+XMDly5QBIAy%DB9^Cz7ue=FNZmM^lWyua$4?o^2TW&Ps;mR&#TNeOY9 -F76->U;)Q$k<^5cy4n3L4wz1%j|Ly@xeR=2*6G-mq_2vb();I0!}%f#~ujH)i>vIZ)vUa&gw9X5X(QN -9-s6H#L2lQ=bl_OF8DCvb8$^%B)<>}+!>U{wte#4Sw*GZjVSo3c6sYB?55n-`?&LV|^G#?ZbvqCyq^G -UcyZA&Scb(4S!Q$HKTeW5D&ei0}+qbt5feie8;B+AI2!2A5k%WD{+KCu}BxYJv2VtXz7!g}JyB7v92v -g$k63>IC8|CYwb0($Ni@Fgt1{V2$4Tz@&(%0S0glu?#c+`d?DY%dmn1tpn4@9 -WMoDh(K;uJZ(;2zPLj~&;uh*i%?yN8rD>EuJZA7eH}W>Wh$Yi+Gk12s-DtI)h;&n0uDceI^c2tR8lyK$uR64blT2>X9Zd%nAQEI*CQwXW51l*52T`VX=2V5e -t^8&fV(}y{Psmuc(;+cJr#-oXFS4*!vrORVRMV5hk^5ob7#q)zhLFcstD>YiRyy@}74d+MRKb-F+U28 -NCFjyl#JLu*(?*_MC>vCYijpkkJ=GwXqvfC%LC$@6 -Ee+AAY;~VSe@Q>iyNvH`gOPpK5gmB%D&uq{ylFBj=>*K7}_oH=lm`{+FAp>xp-bILlCRdVsg|=(u=5Y -&<~+e*v>V@WDgQBV?LNUo92fSnLc1%i-o%^dX4~KXg43;et387@;7`mdnA=d->En>JQ7#-sLOr-@^PwV<&uQ`!%u@9&YF^d%uf~;Ip}4u&s!uii -pbzw^MsN)OPc_s#QX&t5(i`@?uCni^6w@Gli#I^I9fGF*=MsQ>Wdtn{UiP^ptBO7I|v$D2IuK?L>J9$ -;SXKw9nvYPpmcaKl~M-FbY;m1z%az1y;hTDq-Z!SJxcp!&_h1k76xhUN@+Ghu2xt?u8RHM4NYPkYz6Q -@;_b-tEYx)VqoUzS|O5YVD&m-%*dfv0C=i9_FZfw>Sb6_hjE|UmS4CWDX&(-kR5ozDLfy>PPxzsAW4j -Je6}QMH$vbcv+Kq?=2_@jK6nV=a@o3b4ctD$P9)Eot0`?ACse%wUJ%8Yum%<-9xETi#iwdDCnC5aASk -%09Rn-ZA3UEt0=3`yK9-J%;$D`aWQ^=+tM0_gme#+y1X94b2-gXR#N(W}scB-BsHUqW;5kmBCIg#)yM%@ZbBUVCpE_>*kkgAMbZTqRcL&uU9+g=Q^L^bI+TB4zM!VT9w7Ud{ab@=z?5)C -?eli=t*YxuvF!|}{;qCeB+3<9$hkcg!HLZ_(iT&U?I37KZ=D{sIu8qlgUr>sM>;&g7MFz^%|6ERc^5k -o#;Cy%zF1Oo#*n*#G?Q2cR4yY#WY -v(5hw=l2aZ)sIh8Y%+9YKQ5G({J_H*dEPn~JNh)xevIbyBti%NEb>M)6W+#Zl4TZK>6VtqTNNgyp3%MCs6n9)-uy1=(A)dKl!B&335a -qlULI7=dT4L*CQ5!J{dtxEL>Q&mXeY0?aBnfBbqh3!cFDYr4#auODW?zn+c0{h!(7wJ8n!=t082?SqO -RqUa&@AJY6G-99{i7(LAJ=kTbnW?T((!_Oj<;_eXLAKhx>=l`b%P1;w_UjE;;Z{HJwC*p`l1R8!;KdA -l1h|!VX+#C6!Go8;BJeki+M+)+bM&;j0xWui_h*OU5WiBpnvI76cg_h{c1DW%UL(!6Ce4*tPCr0uX-6b&<~=BG{?|A@xDxH$5MPF8?lMAHHREz*hYo_mC#vYdRE~DR -4|mx|t4NR=S%sBOOcJqI92kOW0iqFymMoko^`1K>P<=imSYMu9&i*Vm&ci~@RnO`Pe>4?<$zFyD!lu7RH2!&L!4IM7g~_jMW?J;c#0*Redcw+zQ7?2thnD_sGRtv(q#@Ec5`XysfKYAp&fq#Ll@SmRiuK(!KBg+MeaB{qp&L1@B`8U6TVERI0hGCG4iNl -B}8G?x6vl&q0kHk2gKOAq&NemzHDt{x4x}%{L1pze0>GJOh`N4N>pfRDMCVVnOg0w|ZcTR375}O6SMH -!=ZmLl*hq#05saBf7Qu7xByTq!dy+OGY&jj1vc&{;Bi0$K}(t1Pz1o{@^T)1ALb~+I0E!9EJRpQhM8q8uk`PTMkpojkb0L_eO0SM6VNqn -bx8h$0|9R!nlP;iwN;I{cU)HvCaYElpqS7d(?%>sQ|5fZ{=2?E6BR)?Q^x~3X`f;EE>&9SDPk-9jz&J -D=z-aj0@w(~{%M5Zn~W!l+qyK~pnY@CZCo7@1N9k76p0RJ{;YT0|4`P0l%HkI=ZYagmySYJNzq?MQ~E -lZ-q7F--Pb&ZU5EWZ-)Gg9YwalVcx(|fo)PK*Ujy>XXXoeVr^F99n=R>U$qir#hyh(vDj%C#&JDR}{zPIXkvb7XGG4vvbG${?)dolu|{_A_17BN -f%N!qXWA-{b>fbze08p^@=a-#^V3tEQk|ck>8oT+8m5PTK*UDF-X-tv^CPT!ets7JdyfA;=3mqjAU>i -3ula=}^@aTA?@ej_UDo`Er1cN^%|Dyc`lqbl^3;E4yQ(C98=9#2*CcimvO6xq)rejG=Alg+P((qXtx@z~R3B>X8Ey7&VetoFJC&{m=56lpQ`R -8#~%>&TG;DwQ2Uc4QALEg;63B;~9k>lFzP-Nu5U-%wO#P0pzmTAagB;fFPmpp&l13Zfwi{0m6q<2T2K7}~7-zT5RUtY*xJ{6GB&jdfGA^_)O`c{p=$xj&Z+&c`D -B@rNE2mwEM3K8#<-}!V8Lcz?;48v=Y=HP?F!XoLB@(Lp-^2rGX{7<&wlCxSlK#}@UIAC<@Sib9fMozQ -TST%x3JH&uCof?QaD$1fT^~y}T;@}xWzBtW==|@b#I5$y9PxvfXvGxPjEUgFDBdd}@X`n@$MDg@NIp8YQ!iVmN-KiYpDDFORR#Gv>0&IM>S;>A}1J1oNG7gzfeVqJb(sg( -Y$}ft=y<3-z~?lMMMhCTawvmQKgaW6exQfwFO6#NP$txr+(of!zmsPBc9=AXy8xzd -{~C-Qn`zqF9cX=hX;=ZYmN8F)pv%*H;9HYEuw{s%C>Wy$hOTy_;@2i!;urPILBXz39%b|mYDB*GA-_t -U$y3ndVz{iNJ==kj<5oX9*2#oUF*8YK3}ae9 -$Vo$(|T8B)*0M1kn}keta?t}HQ3qp;8qpkz#G?D{vROCSPq%p1@YL$3Z9S(ks1qa+hHN+N;lus@w)BL -c+*oSTG2H^4K@*5IPhgyv|)fQ}Z6EKl^OSn>H@tkvB94O^<%Hov?j(+Cq|jcFX0#PsrC)|8hUq{p(Rb_GL^#B8an;iNwL2fo; -|s$6A5fipA|o^DD~#OJQzsE3N#_Ux`2Br#(9O7bP2`;d>4%^2pgb@xI@J$Op##;i?RD}v!#t1BFxk;t -J*-$$wy^nQ4HsUvgjAbKqBsZ=_TwePC_;7fAKDmc!7e&Qy>V7&_{_FNTtbh7<|3nSg=#WmJHy;R{9jnss+|01obP8B-6#RtOF}+l9ag$AEM~5hGCCW5m*RNJf>Vx@6HEJ-A>t%z7+bJLmD -jAujLpe`Hc=g!99AJsz55P+VC?=k1oQpoj5>gtGlVJmj%$x%`lHmdf8AZ -d&j#oUB^x{_}VmR&3-8Eu4zAF9^_YuIgbF>3%dAPMlA=bL2!;z=h7(bMB6bV1 -Q^LOPX)3!+ATebQ5IUuOMf7bMoCMg8^nJ2ZN_DR`9{l6{aXvJ`S|vU5_Pv=>h!69C@tw{g#Yw8R_w!D -N>0ohbNQMP%I0FdGytUeDyrS!bSsYV{o73iGG%Qq+fbJTNzt!iu|JYKSEf30m0~7XS~P>v<`5cW08)9 -ZvxUgmd*)uPM~uFwVgmYNUrD)6riv023OZ(sAiR@exmeMN~fmeRs+{hD#3MX2Dj1dlv*x^>7a*I$-v -L6r>d^c=*8l+Vs2@;VpR<&>n8@dn}Vm~Yp9G^33$TWPnW7LYgJWq;!lJ@Hmvr{y7l~fm)c`$OA;R{7z -uiSOC5`(@wBi>@ZGz2;a!Ztf^Vrfk9w*z#=gQqjm{@vBC-H`>M{4(1|Q9hsgrub9!6EH$n+xz#FBN -O5t>4Cc?H(TE>zWN5ZFYLfdRq|nV-ThAr57QWSAme37xok3h)3_3+!ED0Tuu0s-K4t^7&D0Nsckiee> -j+#|b!(uecQdAK8C3}4|5}h#Q@2$8;@ls^k-Bknn`gt7P&^9mwXz1@hHy#{^8oKVM4}wDrLOeYe7tX; -}#@`mPv{|qOcq=pHxjt?fhD#RDcO)bcz1z!}MA-pRO9Z@_fUO1I5FzEu5RXNlJCYu -RNY6J0t?M}N9oTMErk54K$A*|>gKdH_^D&6D?0htp@9LffR4a+43#<0RC3ay|7g -nvxp)Rc2NLaNj9Ooi})dH(Q`vJ~i$@u2fWRqInbPJqL`t~t!+s4uCD7Wp8i_43gNt3rnVv^lUB=!5z^ -d*vdZf&_&Mh5%z-blG3e>7&r9`V7nx -Km&9L{LtPSo!%6&Qz*!$atmH@Yhk}v6luP$|01roK0e$U-KWzuU@(|<_f55F#Fhi5ZsvCIaIIdT=rG& -S!Ahm9ITGObusKg&b`aUzBr80%Nt>Yu<0WJmTr9}^K_usf!;PiZzJnRD_ONZdA`z`l*)lKub``K&!yWDmF#Gj6 -sqEPi|L?RnD$_jLQNI9Y{g>jM_bjqA7Lp`>Mlx@LcPR4L2$-Z6;I9eAGOu`5mo$l^g}?1L*wuWuO{U; -E3AMNEBNvxN|G`~f1WV4{D7|wOKaJ@3NhR{+oI`ryX=|{|@2@>h4wxgK{aWVx^Zr6@EblBzc(mw%`;r5$M|kuxvXd -Wc$8X$8ue#)y6j{D^M}tsYsACe_vGV69#}{OStHopNtO>R7Hb;Kj&V-7K?GXUrcf1WUh&;v{MGMMNl8 -5R^I~C3Glk!aBIxO3=ss&l~sD1I1M&0rLrv@7wT6Avvi`I&k7mi`Xr-82KJbZi3y$LTi9{}WVVrlNR> -D!uVPn_DQG5+?v*-~6neMP=d9wVP+v5+6x8Ti0Jz&E_!4fqYC8Pg%Tz^mRSoVP5&y&N@$0KiWS1maxs^X&CrL&5Jw|%369SOE^pf4r<1gIBBAqTv0ASb*PJs@l*Xq1m(M>E&4fH$ -d)=`Ntg=LUj_TaCLvciwZ(nK%9&Egb9bj8d0cBJ>5bK7BB#_oll$`EEaMT=B5#T}uXZ5zpG^r|XJZ -1*hyZ=LaM`+k)aNO%a>;Et8rx##`#Ej;*qTt1s#PVsUM<|Mh4`2V)#VCe4=%*Sxf7 -=;Qf{gzaul5B+m9$CFTIjaU6WZ`eA&U#_Tb3#1r)JOV^JCO6h@=&YW!5}YLrR6g2AY8MgGcTN24tJ1` -{>Q!`CkDDchcED}V)EC?r_RL8^rdidWk|-Zo0Fap;wrsowxyqCl&~(pt68qK?wAah2cMlGNG!O{Y!#( -F)qnd~jnCi?2z2NXoyCkuRtFg&TwON`F9JnYWaf#H07b5(VQu4Y=)V?A%e}gv4a=>b&W?VjEcEP83oR -zf~1~9ytLE$y4G7sJEo2T)N74l`+0}jX#-D=ugeiv4FC2XX*iOo^NM!!eNz-#(p~!8^=ZQpJ6~}F8IK -n^vp*|fRvUmQJaid^!w}u*EE~#mC>A;VU};ib^_2I+!8Y4*T}+;Tv~|~?kA2l;X6wl_%R@+LJm)x!PS -2cc{t3DL62Q^Y)Pv^Q!}`YW~bD0q35CFd!+|+VTkL_Rqw76;%nBR7043oUPZfFkH___mT~_`i$f)1{ -%=%hrz3z?imTFj*Aj9tJuAPNQ0bsvL2P@msnaXPaz7*}xA5v8EfU+XfHp(v-7RufL_DoB1R~xtH_dVaa)Ds#W+2PrBL&o{lZXJI+}0mS;8$0zIG((Np;E-Pm+`8A?=Vm14T4R~@gki%lEXt@Sg_qrfn(3`w+-tSxBcqU8HkED8c{9g(%y5=q@40hc75y>`mA6r*I0MFWN)MkN#&7N9$RK^pY%n~I6bf?m4s$1&?h?Gq`I*SX_ -(67-uR?!J`3@SZN&U~vn?9`sSjGLpr|RbW`jJTyskWBYd=tO90E}=(4h>2}3+151oj7oyDlk|rYgd~s -yE2d1zf{IL1yr44)z;t2L_vRIR#tS$`$arAS9AWGA7b6J7qw>)t)V|Ves-dQH}RpP`(SDvFg4-UZoHV -@I=3nU6TGOX$G?#?6FVsBprnJ6jc{uXP*Q2ao6V -qv_mp~>?mYACxAB<|vR|x)SxwukzlX-*w#~kI$eSFd(PX-rm&Qud--p1UI`i=k|GH`AQq#&tkojO1>` -MW8*YIyka0C76;HHC{=5Vuk$#`q4Z3P?XR@)k}rHekcv)Z;;K-H^l+WI@Y+V+s$?GB`c*5XXMywDDCs -|!?0Uu~>HkBu(3)%C~wm?Kxfw9AoqSh!nQxJXrNFSk{C{!K2obx_hlNugE;CA$tKl_vaPQnZ~4ems%= -N(&hI;`?`;kWQ8P*-_-|)ys?Jaz-1{rwl0Q4}9Wel%woR5&Quw>cU{_-B1DKC=*y92bVA@~&EvnnzWAK)Pk!h{xdr%)tT6@Cxj;EN~XW{9h+5_^a91l#ixFF8?%%Tyv|)rv7#)G`lc!Jc4cm4Z*nhabZu-kY-wUIb#!TLb1rasjaFfA -+cpsW?q6|GQB=wlH_3`&3zh*}T4OEHtw2-kLmCLQbhg>Zq(D-M-J<_}?{RVXqnTiYb;EuW{?=A)%~$x;TN~Ysi#s --zbz`qr8^6G-P=x3f-1>Y(`1PjMvL17A%;WM^%}X}n3NNHQRXkH|9g1ij@v7t|(CZlW$~o&Ef%zuNXj -Bxcu0g;BT_=0CB&XyphlQ;j~?@UDGe>5XcsQ0AV`<6dLK*GRn3iU(6C1HFRl}R`?#+ -*Ptyip2e!f$6`Yid_F_%2cx^|jHs-XengEA$j#VX(RZ+5;d-xKnk^dru=KT5dX* ->7hPaP4~&7W)UTRuDa@s};uAffg!Bl#)s*?l9cUL9R>kM$Imm_Wyn68`&N*DKeGT*?}2)1fZXN|&MMB -1w{$ck>m^%b-NtGr0*rC$4A`DP3U4+2}<>;=ZtkZLCR2?y7x#L>j;`8(qBYDS|i^fOKrR-7QP!HiCE?R+ --2P}L^lbN+5^{H}6c85DYm>(mdCoP}YZ_4YJ?vKxEXC^PWJ|H}3g-I -9Nr;qKs5LGx15K=6TmyIpMJS%m#GFp3C-QH>(J&gbKz(kHaj-s%dcWrKyDV$z|DI*iLx=3o_W@07N{; -2L&I7?>xme-n7pJ{RLSJTV~Ed(%gbTO9K)*HFT@NbLW%q;b!rSdT*B)*g@l-u*L*7rRdvB2UQpYaVp% -ic9nrhp=jG;f`7Pte@1s+o-Y8=#MwyGR@W8o8It0`shd_Rs6N9!yR>H0eHMkC$;q%st#ws2U!UA$^xF -lUtYoUj17y$iYc%~anLLhgx<$A=L^$0d$hkY}l7H+{Fx&;C;0n|i#Ys+-e(B=Q;uAj3c)s=-;_nQ74)P_C -G#2gDzcc>adP4IP(scfAOCd`N@v}(r=xSZRG6z^_etRQHnZO9KQH0000802qg5NB{ -r;00IC20000004M+e0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;MtBUtcb8c>@4YO9KQH00 -00802qg5NFspy#FYR503HDV03`qb0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;SBUSQ)7D@$IW -W)89uCN?TQsdFwQuL@62&6#9$plPGn=$zRQC#4h@P^l$Q&rhB9?Q-GKV>h-NwYP^>q4sfxGn@v -(JT`YSZ&rS%vp?t`(YHoQUv*zlO9KQH0000802qg5NPiBHP9OmQ0B`~T05$*s0B~t=FJE?LZe(wAFKB -daY&C3YVlQTCY;B~+m^yKgmD2qC*-1OvwbjWV?;}0x*!Q4yH_61LHQ0XiEPze$ZW7Y#y#!-VHdsHDFb%f#hP1Onj -4mkEr5@E4)eQzxN_gHVh_(E--Z^R|HVD_;7aL+S@3s|1=axo+&*f*w?D>ZEYtY2tGghhAVF>VHT@=cf -@zr`M)-Hy+xMI@PIuGQ!%lk6HGl)OZ0Cs>|wt7XRvfTICQ6UXbM*f3gy?T|dg4&i}|%D8d1 -@w_D`JLn@gK*i#87bQ7f!A?{8lzl=60nAKIZ>_c9Z4-H<|k>;(#zA*^r4V({e^%KiK(?hbTqwnJJkh` -;qsfD>q&6b>5bLtmRO9KQH0000802qg5NELT^I=BJ=0J;hQ05bpp0B~t=FJE?LZe(wAFKBdaY&C3YVl -QTCY;Xh9;h=dE%~j=eOuXk|Gt1S`fSw -n@5*^55$*UKX+}A<3Nhn0fQ@o(C$e8bWE+*-itcgg33ymau!HL}!8a_ZcbqGdEnxjC7J0sst*y1mL-vtjAri%;_ -d2DuvGqH$h_697h8jAa&FMBKVz)WFJkc-4Sqv{Icc-TP#kf*bNj$x#I@$dLPP7Y$DiI4Xt1WkfLVVfF&>7)`S(dW8%o|TxiVmWGMmIPt=491@jam8NB=N4!ik^ -2|gB$X)28WUlC4?x<@_lFnBjIv>DLD^gjnm-7*5SS^>`e1&L!@(4VhyhMD0o^;gYpV$0_8snLFr+v!-}RwW#^+bE&mT&ohk4%GDw#p!lR&)do>WI5OLeDJ!nIqaIH)!wK5%7?{kB7759iv -c4^g4+NJFox-U>TxR3nR@ubK6P-Wr+#d~FK<W*K}&KMbek9SUMCUL{^W={xPsZQZ^d?;iaJP)h>@6aWAK2mly|Wk?X2%D|ul003|h001Na003}l -a4%nWWo~3|axZ9fZEQ7cX<{#CX>4?5a&s?iX>N2baCzNV?P}XF6#eg~xbrADkmUgaW0Wp~L08J!jlw8 -KwdGc;BN<8Q$Hv}$_e!!8Cvuu}8yoYVM3!`P&i&GLrPUnryjs|W=6Mcc-Y9K>T^Us`EYJOT45hdeMkq -Okg%pJ~Ux8_&O@bGR@C}}K{*0i%l$t;`Ie9rOZ81&lsPKmM|C4(TQVFf?BVVV`l!6 -a*AcnxVf`KpA>)@DXYk`y&F29AZaGp&`*W)B5#EJ_2BFOQRBYMX`1*!SXS~1?M61p^UUzN3 -h(Jr4VO6&GLX%e%Wev_SbX~dEm*&+*)?%JJ$ie1ewcP^Dy6}yTP{IJSUq|&bRa$UvEhj3HhxKIlwS50 -h{~@)&@4T(OxtsIosJ=$%^Ukj|JM-dk!-$#N~1N@fV`pW#ZuTNDm|JB!&m&!Q39zfk@UM;WSA2)RERx -=gUqt)kxAhIzNh#%ojRgFMlos*&IlJn1V7>v~swNYM<5}835hs9{KkR1)ZI}MuM@l@v&u;J`93h7 -;rhVHxxOqPMH>?PBVr+&sQ5F!^er)=XTFaw=)TqNw{0bZ{%?P6`oG`Ra2Y)_PYbvMMgJ!uQ$UR1}uwr -S!xw_eImScAjTh1w=6rMC~lJ6128rveeGh3RGdfc*nGYO+I%v3rG;eK4dQ=@8S-GJ~F7p*0-XQ5HV9S -&vI3#N)c1)qX*Eu==KQH_=LW1Jv$*2QoV3S?>;uQ;L=SwW{AfE?-(`{Y>nSPl8iSH#I{8@drB))X~f8 -`0)M7`Rw96O_B}dWtz(}o-4cFC;xBJ{u9;Nn|uG-Y^bQ^!6*F^)~F_n@?Y75cl;pakIQ)aD_Vr*LV%MK4`#h#ZBRG$s;0cTbs2dY>(8O)JNj>_p~?x|H%#qBWHl6Yu=U(EwyiF{T? -^g#ZeJiW}UxgcE~zfxL4La>vo}2hO>`mcRU^A9!)edp?SU+_6E4iNT=AR1NFsIXUnH9{57-2Nd(K%&x -BMqo1Yx9ZFtbIu&6ldbsRJlEp~=ERH+5_Q5Y3#bXRHt{pso*4VE_Fk_vBC;cfLdKM4YfoxTWMWSL$1m -Ve}Wc?lO67gM;uVHS;v)zC+5FG=Ce)`$^tPbsrj4N5Cm{ -=@v_@9^#|mb?LVzKLJon0|XQR000O87>8v@(UWfo$p!!b1StRjEC2uiaA|NaUv_0~WN&gWXmo9CHEd~ -OFJ@_MbY*gLFK=*kX>V>}Y;<8~b1ras?O98Y+cprs@2_B;gVuoHVmFUNkQ69_HlHuIcB|!l&hqs}Y0LP+sA6m7+&!mEIRznQYkLkuN=NxnZTU#HLM!$1azJXr@b>ajBc9HNqEbPZ*^@CEDW; -%|t5h%`=~)jpy^wK&X2?sFE&arTl3dTR6i`+j`pFM%Lndo8Z&rduT34^PWvUTdwUF{xIp<3dn9NCW2c -&`OcK?5Y-2ooMg()Tx~31h!=B%%xfMcUxOg2rxNNiopvW~%-3~^EPUxdmZDeos6?_9p*$qTSRqU=wjA -}|xm|OhOyExbfC6;P(Xg%DNDk+vpqtvA;E8sepivx01V^&6>mq4O_=5deGJFrx%e5?RVi*a81KFDI*g -`V3&aIV#*sUa6=GwsbRbD;>yznDPF)-e^50HAI -Lb*12t`gGYDQ)<-H&!ysMH2HBrC~9S8ax`FRr!66Sd}L{WJ>(ef|3NEqjs-lBlAoXnZ3?HEYOFt~YiU -4O=qMK5oH6ACVOABQUP4`$cc%DREQ{ci3|gLjzFSNxvf&O6($zz}J^{gwJyr{L+A#{F%v(jZAuI8ki! -}`NjskU>;+4h1`>3T%=1^ -4i`u-CD_YigttRM~eg(AlkO8sEmML=T!`G{U&5FS8T!jmRlBJ4s>v_J$x<@jc)ib6dBj264Sn_^4X3; -gOkYc>e}b&g6L=OkqbaY?2F)`(a*cA_U!#e-JcO(cTqPnV9Okgr1>ysclPgyf24^)6)Rg0BN|DJoyU* -A@7-GZ*1GME`hn4^4@qQ3-ak7=JZ7BOLd6J>sBRm8TV!v1D4$MiFK$0QVSe#y(5Qg3@LffH7!+-DEgi -+bN)fSKE{B?KSef?Pg-D51|xj0&a&<>Sjoh1s5r1bh}hAA`bdfa`DluxknSp;MNdd29vZw1!N$&mJwF -Ip6$fgGUPxwc^fT~s($#EpkfJOD{;j{LIi*JZWx-Oh$9$EIn_Z4QYvXqH>h~hQ=s=QQQpT8`-#=bsnB -n|jMn(L!3Zk+%hmO_uCPz5N;jzWlCP{+JP1MU?Vi7-H -g$zewCfQv7o1OWmq-0^*m8CFLfVPsUaQjA -Ag0ow1Wm_^`2~0p?Y{i3ioWZKv4~3+Nq-(G=hp%A!30d>|jSUq~jgojip2(L_ao!7%q}HLnIN1WUmML -Gq4hPFtQ=@Dl!qaK~4eCQ79fM3uF72;Tddmibok2LQ7+y4%9Z!3RrzPR*s=37lq1x{IgXS2(Y*LpmTx -UZ(gVe{MFP7hf9${RVoW%WCsmcmq;L7D!oqG>JH9(IA6l~3eMMXzDa50j#Mi+L7Q#JrQ|S;f@wC?3WW -WJ%q(@_EnlGg;GHdeZl|w+$t56pN>`~1(8~UZ(#ivLi#|xw4x7FsY5YTYuRKKe4m6;p4U4&^Dwu(!#~9SsOzG4ztdw}rm)I59GGz)mt8q3SNKKq)geF=YYH;G -iSAPL@Tx54)ru(4Sz&?tueNJv-uGq&ZLjOi>d -Q|8fSbbv4C?Xw*tAOLeLps~uA#K=BlWaOsvg_mU8qmp?UfvBw}sur$c{K@njR6yr#@ -et%a@m=l0U_9l@4@U2dLD)0tdXtYf{Hc^B0xtGY73$FJyWXeI@OnfH^NOfAe~()JpvxNf$?&`5yac+d -VpABjNf#nEoR$HRyPj!qK|4y^7}S5efU0))0-a#RV&l{3s6e~1QY-O0 -0;mWhh<1OujHYz0001B0RR9g0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bb75|2bZL5JaxQRr -bxunP!Y~ls=M=MSq4WTPTXA7mx*yVJ0)uHH`G|UYo2C^D`W6H8emw2n2#7kxo=74s52zYR^@d5G>x7TCgfU;8hSQ4)K|(q}l%Y&G9{V`p6w+mehYlTk$!Z%|7E1QY-O00;mWhh -<1(wOSR>0RRAY1ONak0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJWpi+0V`VOId6iU6Y -uqppz3W#D3L!S@n)X`AAq2PA(9NN+U4+&iCrV_=NOIEj*LUQPWaUi@nS)@y-g`5a+&bHV5bY2KheCkr -dg}tngSUDJC?>yvMm;I7tSMkHs&kxB$K(;qY*L$>3+LtX$6_&Z34lJ8*yTKSNr1LpYz*=Fr{{<5*yz~)fY~1na@qJRD+PQ}Ix+Ue|R^D;=mDDUIO#|7hxJA#78`%+T)D;z!? -`~_QD;aZh313)}Mt!WSGR>$e@-I#Qf&276oU!4pCPKZB(4v4|b$e -COF#+HBL9k!}C2l%8uuAJgDjfIt3(sz&|w3(Ilt3*Wk6)TJ|2rq%ceJdCL2uP|u5@R}*jWNjKg8`Aa* -be~E$PCt3R8kX^t<0b?Ee3s6e~1QY-O00;mWhh<0tF~j5#0RRB%0RR9i0001RX>c!Jc4cm4Z*nhabZu --kY-wUIb7gXAVQgu7WpXcHUukY>bYEXCaCv1>L2iRE5WM>pD|&$l@c<;$Q>7l-UJ|w3EC-gbim}n!Zq -&ZM1~;TpeX+(nJ2Sf;*iInj5PV>i5@?z&dr(Jbb?_(?E}^GSa%4>j!BA&=l$t!U(oTqB(Wt6xuV=)I( -r4tYHu_0BWKx>Gk*~z(!|Tg_dlf?TIKW2B$;=@-FLF(W%axTdSe}$`vPCgMG%*XAQ}bpGmICh1Zg=yP -cJ{8QSH=UhZCk^Bv_bc9LFiOBBKVQq<4Q>Bq4_&XbwYoL@MDGfQYlt44ZoqFX&~jQw;(Qx+>e?6Pqnf -!F6h?BiX6hb(gB~CE$5m4nh~bpJRA{Lm(<4?f6job7)>ALQ{1$?;8v@_wk -KOb`1akH!=VKF#rGnaA|NaUv_0~WN&gWXmo9CHEd~OFLPybX<=+>dS!AiXmo9Cb7gXAVQgu7WpXZXdC -ePZbK5rZyM6^qPhzT({R$m4V#lY&4R^?rcQhqu^I%8pC8IR^ -MOBk$8$P(x>nm(7Z;7d>fmPAfN_F_#78iLfKmrxN#RMMIh{LLjt3fm-}n;#W+g;YvjnE{}tf^P9YwOW -BN5mGclOAaz4o;Uq98Tn4@n-%14LNQBhCPh*!(4U--Y_YO0Q{E%6$3y0$F&C-?#H%{0kUdO?d<~gY9?^nhh ---kJRzDDVCWx%Pk3#PYB`eEK>4$kP29AdVT2hxq(CKe2HLjZlu3k+Q-?zvq1-nrM~y*Af#2iJZ}H{?C -qJ`(31c|`m*+oIw(&(1!L$E{5Xok>dks!jrd@r0Ogt3kUQ4PswqREb3An`SZfo%L?gn~}y<(u!MzMx# -*}L)$|BasB$$f)$*`{CCK$I-bG03h#)}s!t)ml^JZ=Tax7xS}k3?aE%2B6KrT)suWdkdi@j?gym!JSQ -l);f+Z6&XgpP0!myO|mWW*{>9)ZuOSla50*wOz682VteGX~R1nL6YayES&12olHJ(81WbwxAIL_M45-vg($24Ub2x4=jalWQYCV2>UX{cQj -=P9~1NSLI`cW4P<(f2xG;7y2E`xk#X-0n@!9I -sF;W@ytbnVkB>{*E1`K?`;?;zF`|aI5EpCOCD?QJ9iFj -(axAy*Yv>J++Ia?4?iA}d}9m@LwmK>`su3;Bw(Xbe#n#%09LYC;bEW*Bv!+pWRB)= -aLUYvb4e3Wif2K`h9fYTO|*Llt?-2*8oLnZ`)YR=8+Ui?Xmpth%}Dgy^ssz)fLT`OhRcxuz26r^h(w{ -8sW}pBXJOIgmi(eh;HzAuvT0B^awM4r(zgAv1n1y<^Y9e=nFZwU(k*7YleNCr``MMPHU;9`Y{TI2dmrQb|^y601+6jNl1YxDpf%N7FYpTMd&-pXjCiO-903`9h*!2rf-RwOjGh?zJP6@ba$`(;srX(;?FcQLEQY^R5VVP_CXN)wQ#J2Y -y+u}gu%334<8Uu0zg>01_zP02NEln<|8;b^3{PE=D-a}o|gdLSZ?(UbTi&&0V!Pagr+Jp!hDuMa#wK$5n7X)68{=_T4cg30iA@v$ctkO6baKRHuZ`*V=!I=v -}xU!4HOo)pBC$^kPNnoK_FJS1=hG_$*(2kM$A#I*tPc(?qaqD!-JR?Jk3i;DO^z4chStNzIm4rm!K-p -ZoypEc6U`fx9wl*3z!fc%P1um}+aX0;sCDG3JDJcAKO=4Yp;I -QC3$vkZ3l6gJ0cep+Fj-t#+t3Z=8SvN7%6$3B`lu50iyN~e3J=9q?k)YLt(G>ux7WhA7KOy|@lClScB -XZUqu*s9@LFWh#3#@5uSi0@B1ts@`xK%b8`8+B|9^|WY6B2ap3HH9z+Lc`@-XH^J^aLdX8V9?Srct*wNN_$zvxySQGbOxqUywrE?C6xX?(|u38@&&e#%m2sn_6Mo!Z#FD0E -%jQPUYnwh{Ie)%htxIvV+=Qs&xW#ba!*T=3QPzz+(ffn2MKa;oRPOa0oaN6<@DRbL|zvChgk!d#7J*QO3XLKg8-tyyP{JNU6{wEpJ;iWPeH+JO -_w|NolpUGf!JLSbz#gJ04dytYqu_2l{N*)o3P3jvGb0|9P6g3DAdU1Byj#2Y=IYIh>!07w$s>U$q=qz -(ES{A}Z2B4^htZOUFC*`h`vA7$@nlAh$=mmTfA#VO@qov1&7>ybIM}T@i5(20$%8m~WbyOjdj8rQ5d0 -817bN&UjeMBy$K-NGp7tMD_al49-XUcC=wwip4|RRydkaO2@Cau&_bt5ttt&{_m_$RRn7*8ueuI -Udn1nz!bzI}3a3wZU;tsLFS5)a~m#I?|Xz9Zgmdba8qxgq-2?K4sfn5B(RXsNIXJ=hx4BVxnU-zah$( -+`)87>64ij#bG}zaJYerk6xJLkc!_O=Esaxr$&>%4oYMq!n@GXh-l7RF-3iUR8+mUp*)2bTAHGLjMd} -ix`pexIke)S=RWp;hr=}o6|19GXn&}7ceUhjJC+_bu>`25>TLA1qJEFfcYV5u#iuF56UbN7|<9oT2j-wCm)Ae2-Oi$3G)V{+_BB|{i@gO`~Ke(25$toyNd!}@pCn-ZP3iP -Rv87J$T9<}vff{aIW^;sH{gR}&hzbM4(Dvw+*Sye<^4SRPyWavnt@CTUowOE#pHoQVO%%b?lS{i@lY$?&i(9wd&{E!eM%*5@y-1L)qSP?!6m!}Y -Jo^IRpis`wo#zJ#vwy2q- -@Sf;RkVCC`6?O(3$xo4o<@UXhoaPZn@II~wJ_Ej<2?r?+V2(q4dA*?T>7@YLH8b;m*|{LdGD+Z$Q{4A -IJu1G@;)A~7c7lf@dan=);YNI{C-)z^`~9~Z;IkRy&gZ{R^VYo$K$c%;y@cfYp7m}@IHL_K<4k>y?IA -o&lij5KhDWd^ZC{M%4@u(j!^ir0FM~euaJ<0-ZAaO1R{sZtk_xio%P*8^}eKbnPocbKB}cB<${q#L*~ -(Fgo-p5{kgij=UHlm^*m2K(3flmG*Z5-?)KFGiSb`hO9KQH0000802qg5NB{r;00IC20000004)Fj0B -~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2PxVQ_S1a&s?VUukY>bYEXCaCrj&P)h>@6aWAK2mly|Wk}>)U -l1As004ak001Ze003}la4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*f&U487}D -5IWg_=O0j@hazi%9TpVG1`H?yL$T>pjVu|G+@=`z-$(f?wv%8zbUN6UNIpJ3(JZa0Aj`_eHx{!DWK|p -MLF}ASjmNCpCs4>&>7>yKG+O4Sz)VSxRw&0<88D8ON}V{S^tLDpv;)+g_g3x*%dI!xs#^8_X__1DywK -i_NsE`p`3GZB?)51wq`YEcthQLnV^)c}mU<7j@cR!djfL(!0cCQb)}q1$G(BA{as;Wbaa2 -0P)f0HeWM#0~?x7lpPEy=O{#TL0DLO-1oh+}fsU%p{nzPw6_VvG6m{$JY6K=Np~j#ca&w%>i}5?2MRVaKJm`6S7T?;>&FF(PQ3!*GgZRDwMXUy%x?*0vs+{-Z`Y%`aqMKW+4Bm58 -Axx{x5BRGxj-Sg0#OhBx1I3nvAEvP)h>@6aWAK2mly|Wk{e*hvnh~002!5001oj003}la4%nWWo~3|a -xZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bUtei%X>?y-E^v9BR!wi?HW0n@R}8#Z)Lu9O`f)PgLz)fR1)3Dt -b@x&f1}%+kHk9acC_8D<|K1r=l4Ut@gX9w18a_V0_hzWkXmky^vljDUTm^0or7LQS1Z(GERU2EFY6FE -?uhC-_;K5V{NnP-Wu=cK0KQHcnfx_i=$pw@twQ!Jo)B!=qvIv7dLTae+0O4teEyxs}T+P2?0l_hjGUd -*qU;*JCy|xgxOoC=geLI0`{x!(j+-jnR=w5r{YKpj#9VP^t!N-Q!R6fb`sk(7x`sFrNWw6*HA(S-*IP -M;{2&M|?*E$!rYPr&cS?@%Y-Ma!|tZmr3db4FIBQjL{Gr?BxMS@N|MP^g!P`A9-N=@( -GX7XQNCpj;fY${W2jjWUKC`XQa^#Vl(3Rs2e54X}nSzsVd~8jMCHKj~Zkdh=@c374Jc^Cq -iP(Mr-0Dxx35;{ifhl0YkA7?(t)7I_o?#ed!bj6VnTq!}fnrKdkCl|{0f+@g6eIYd<-o@X!JAbbkiBm -Mb|bdKRdgptKvGjvCn>C42&|W*WP_wZckxtBFfO9RKsVVS)p&YwMN>nrted3%Do}Z>Qn%7CbG09$grUXBg^Vw -EVTdyu61q05|gaq7~LD4GmSa?^RH5h%Ya?$C3`HTi`ajstv0`B&!_O=!^1)Q4NYbW_>T|q6VEq&(p@P -+&knlS!v_<#aae~dR)y-FP*xGI-MtB8rOPfdacP{yUGTWI2^oX1A`{FWCmDQ6a9BjV-rru^nn`GnGc| -lRDqK0@J+E1fiVe-7e+hj2^==>rXRuLNf)U&hul>{TSlW!ivywB!|3M~6F+j`C=tSFICiKiCB*4d=Bc -Y?Km*_w{Cd92PYaV(x#w$+*@kD9P=RGtqJh%4RSiQ2Tm0kfbSkKe`TnfwONr|}TDhb$E!*+mEo=sI@r -2}8u+?vv~igB=H?KgDhM_fL*YOY{>Q+v4>E#G3hoi!Neq-3)-Or}H1QkPcZOkTf0C%wEJH;U4qL>;ru -nIsowWIdiYD2ww(7Ir;0?>=VM9k}-=>D=pM30o2;*I<6kI!^uK`8*wdcEUJ~r*1Iu8(9D8JK(nk9*es -)6?pDyzFL|n!g%L#oi#Ty3qyh3T}@69n8Cm)zL{lM&CZ02I88^kgqp@Ys~up@pDla`_qShfKSLgeD!) -H)1m&|B$t()FCY!-8TT;DBmY;y6J4Xt#c!QRAv1WYApf`H^Zz3HM+MyM(b1r!iL$Z{zlcJ&7zqhD=un -NaDuVj%r<9}(q)zUMYPH(<9niUscwkvfQ8+I!?ywNfmoXBg#Z(U#Irn>ds`7t-`Zc!Jc4cm4Z*nha -bZu-kY-wUIbaG{7Vs&Y3WMy)5FJEF|b7d}YdEFcRZyUGucmEZvf}j#6U*Z<~h%s$w-DESc1VL?V1zgX -dPL#yvbX1PKC~lYkeeXSf9C=5{a?>A%38*98k?)J|o3G18ZP?x2vNdfZ?(Ue}Y*k|zpKDdMM%>xoQ?` -&Fq?W3lvbL6`T8O(!8qx4dpDYo?R&I;C529YErqJ@EhhxM?b7$_i;>0d1ic;0u@Y-m($FjYy ->=>GQ;PUxU98k%xgf@y{P#{=q)dWJ~?^9uGfP03bx_RJJD#y8^I*)6AQLmi<-ec(ExC!=0AzjFyUdre -%STC6~ZHArKl?S!Rv*8z_$iCcFz!as{yc4Y}*K6)v%>%HryCM0Pu*vQ6>00_&owbU?Bkl0DbfX(GyUd -ye0NQG^``fSr;N8oshW0lO+CC+m}DO}lCU8#Zmhh`?;@Hupm!bkMtrk0&N;+1CjQ3Sgs6u;Sn1ZKPg#{Z$?k`>__Yc~4xQ_Y_0zYyKwWTxrP4yFJfwiiwh$RrjhdW@k&kJ=1o3HibWo}%|kX -x)idvT1--Vh%Rz3?^eLS(R+K)B^O%TyD13RK((C)2ODS!X^yY;1?Vb3vT$-J^og}8HyPyW|l5oi*%7& -aegu;YRa}sOZ3&?6XygB*L13CctiF_48}MadOk$}K}fMn`O=~*K96CGC6fCT4sH47GY(+t8FX-*JOZ$ -c%PZLTNUvu9MEf8*QM&^GO+Kx|8DRBsKT-sZ*r*TUu;zVFYb@iG+7V9N0$6`b1q2Hs*LdMzdy2&FEj> -@kdW}S(>wkzG)SdvNQx=LRC|DsDpp#lzH;=*HdHjW{zl5yZkglR$bL|2Y2_!z!MC}#ehA6q?_gP6iVL -$xv1H>G?RxKQ+p|~c@TA^V;E({cJ5amkLSXTTsY(y_=6#y{x9DnMvI(B2%LaRGZmkk}^FvuOgo4Agn!{QRKgFK^u@@TG>6yrX5sNo#cVVNuh_aLcVD=4Zr8>OAI00+XuZUW_w>ma1RU -%&nb^L9hKTn%$@S5oHyPOJ+KZg6X~mxPt!6ZV}i$Y@7K0KS)DmtSXz0mZY0W*0vb)*k~S;VcowW1uAL -Ch|$4ylVKW$Nthi8ZO^cep6A8#~9i-sqy5Nq{>Yu4$yg9nKynp)-P*gw&%~D?RL9j_jRF~)w8n~FJ3( -RNd1EYIsZO@!@>-^1ZF&mvlwaurQwB^D)1$tD>N+B_?}n*OZSF}NoUGFt$-+nAr+KZQ|hx8eDTt|TkO --hA1QKN^%g2U({2K1NSc*>lFY7C8Y&vCu8et_27LkqQwe -i(RB!bH&+hH$4YoB#MY};X0Wy&>Wbp@(aw+6~!Y+7~@>`GqWCrJxrgGnJTKEi2HYCgmK0G;mGQ#T3@e -H0x}+{(nF>H`)NGuG>H-Z<87yNy&kJ5I3M+Gt?|Qld}27z#X&scmAr5Hlw_O0+P3bo4_q!ZIJinC=UE -GVwZ&Bk^-lAtjF6e@8Ug7zixI0+{TF+*2kvJcLM3@znk$d^%mDh+r3|^M=%Vi&`6-C-vQ%wlsUKs%N| -a9<+e6dw|EQm1^L(t%Fp9Q4k`RHe#y*gDxTbibcQXd&2ZqlyWIc3Y5O0d*4bULQ+fx^bqI(R7nl7OXG -RgMnWC5P#qwsCfe1yh)v@PV6n4vtY@~guw#U72r!eyk{ZZbV*~)#u~0?gh=JgPo+_7d3`qEjlnFV5gb -gjxmj0nS08bF^2MTtjm_%Wno<*df-MI2_FD(;rg3zTKf>%!Lg~c%;wa7gTJ+WEzBXI?S<$3B6>e{bby6_xp1= -72_BHoY=M&ys0FniMBMRoH}(7#~w^ibm(c$b5#7XM=g~OxrKlFb#%m=s78iOw7KXi<_RKs&1GHPqs+; -HVojdZ$z7tJCGSPgkBT^K+F}+(-MptM`*L*d4%#{+wE7(o++vezFcT;+lfS@Ojf`c8z0HP -Dlr%6^kXU~(T4;>pGl1qEC(9ZgH6&1XPVP`95Yc^3Y1?=G1XmgY%7F)F)PkJ_SIE(C@%bt$FV-OGXw1 -e#BuGucuqI^d!{5nWmd;|xf!FiaSupue#`C6Ld#K__5Zhz>`+6NJP8(v^iD6yM|_s=e!kdUWg*55CLs -jCyJJBKA){vFnYO!wpovs@w&=i`^M0n+B8f3K9kM=d|Z$isrUxH0!+rhVdqiI1|x5 -G3yZqgn+=Q^tWX+;6Sd>qBRX}(o3*{AGrTgM%5y?0Z3>zjzEZ-Ys6qdp4|dYRVbd+sLfEpe}Yj{xaBA -bdGHjw0bJbnY}29#h#D{8RW^Jj%ODSsL(}kj6?gyeW203%_T -YsFT1)s}d#8le-di`U&j4!N=Jb@%Y<=pZ)f6JdDGJ)Ng_&Crt82@%>-hPM+nyJ_l7i40?Fc|2J%fVTPhKUS~M% -3RXkyNeL;pZGt)t>&XB$q&z+_%}$#olrE_TiYpCfm%+Rm{|S>1t6wzdS -Imm{8d8K%omz!-l@CSjp^y#W>lNRJhxbAi72f{)v~Sr6q&foTf!YBl;W#+-~-B$WBkZ{SCu*Z7TAx{) -mXtLp3~=tRou+3|TM4$8o?+{r1}fUg<%+&j&?Kyd|~%*!Xcu$_N~>kz(<(UR?8)?>dQ(G$Gs%M=tR_! -k1h1Fi?7%wQVjn^#4E5G@jQ3J3*F4A;bVW-_5>{M~emIxub=_zm*(y!|U+~Jbb4=js0szli{oiUE(2v -FJ_*@Nu~%%PAlyfcC3XdI7OX%*$7~0XSH7R?yNMw*F_gwdeHEaPaCo`21jm)3anMVtk;C06QoKlaP -M*ATRSTI=-OHF;}92s4agM&EBcbu*4p8J4e|?1Gp|iVwV$ey>CG?Ae*?y8qW}pZU~QdoOT0qRMy+@1@ -Y5f?`ek)|Bs-fCglDLP)h>@6aWAK2mly|Wk_KvF&vi%000#s001Ze003}la4%nWWo~3|axZ9fZEQ7cX -<{#Qa%E*=b!lv5WpZ;bWN&RQaCw~?X>Z#$^t*ort0IWJDni$f!BQYi(yiE%6s2^>@C|FL4JSqao}rT7QAXP)Lz{*!+maL&<)p( -Eo6Tk`LFA#X@)CY43N>vgyk_VEfM;s)p5PxHSt*QM!xlG~5w&I(mu0~ERgx}+KR?v#iqxdxn8P9Ghcg5ZtfM0|+MUVGWrZ0-$DDV6P{ -z~t<~vz(g(x1urYJxOyeLves#S(aPs9QekJ!uA7O+!)&3rb!?+BQ#L6wlgrj=umjoyF}Sq|7WO -Xu_45Ssf87~ulS2rHDEp*vh{$b`T-pdD*Isg2{)uShd3KWWmP&frn*m*aG=QUP)G`Z|-KS3h6R(W>p_ -3!;sKqMx1QxOV0EK^~{Xm?SB2{eu<7ZNMa9>JH&s-y|c#5J?=5{2u>*M?|4Lp5`*~V(}!dp^iA{NrgXJtBO~zei?;DikJoF`lqcUndD`->>ZJ5SA8Xw8hZA7&@V -5y{JgsOiNE-$^Sda&<*p`^%OfA175Rc?Yit9?k0YQmiIJWl<=}z0_M!qdOf;ka{g%wjlc0O`ri_6&m9 -O5=;D!98c4yUszg#9|ICP6l+9>sRBO9sKQx)>WMF0yR`gB_MSU}Z<9j@0cP+2SgoU7Mw%Czzrm*KS)B -WOHkFlvy~8FLS>s>A)XaaB}}qu6xqo0~i_IJQ7(P0j}h+)Jw7J`{asttY^CWDJ7wCh#Oghz7s%_43Qekv1Rr=m5pd+Xh>vZ?-=&RsuWq+su&;&`Yaj9wPR#Of9Dis;Rrye2LKlwic2?&!;9 -yi4X)^ll2T*>P;ja#yh6!g0zc(D6c$k`;VWW_hawrpi56iOpL_A%K*8e>&8E{yPpr@kEt`Z>_ejA#GnoT9b=;yOUL9wxc;#HF68?$645>v*Q|g9I)WD~g=WgGY -&Q-aftA|vmEknc4dB}9CXU*Y2505?TQ1%9*{Bf<*H*bHC#;>dT$d+4IveZl5_+D>rD!Tko4nTUv17wd -3E=C(ruO!ypFXd}%=!UCFt#}CX7=WNqJ)JGY8T%mB!evIDKxWd0bJCa{YKq}}RJO_P`$*3b#%`$)-GW -eOX25Wbq+)OIAKl>^j}8@}DqA>lHWc1K7jG)*-CH8Dkrj~UE7ncY8@*_zV?k{=|9=rEg$Vu+IM9fR{+wsZo -hX_|yd*8Q{jjWO}>nnL6gjz7{H)2wZgcwnxE4OEtqs{$75-9{mAuc1fA1_s9E4DcMYf)TWo}S6llMYt -RN2IotRzV!l{4i(=Ld0v-IF1UB)IGtNQZSsnz;#=7D-fuYm4b6u4XP69(EkNua~ze!NT;hMkd^tmyFl -N>G&Oxso5q?prEve(<7{EijTSPG9`%Mly*&AHes-}a7Jq*{llmLm*T#nvHqqV=ZA!Y2_R&wu44I>}U|I5<{>2j66 -ZkC%IVD1XU7S^nO@PVVzwvhRx3H^OY+ve&FL@UI_n(e`eIla+T>w0ZJ${47>n9;Vh9nK6I(-&Na|9zEgS}kK!wwo4{^{FkrjP_#xg|vde@1 -=^sqgy5lcFs#5Vs3M{y*2EpM>DOa$kH;&H|mSK%3s`~)QmfDSjeBo$x3#vg`agD=g*I=DA9OKCjOE0q -b1xW4q$kLDD#e2LR#hN|TSlasiKBFB<++VvWC-OnKNs$JqL>_Gb2K8Smgb}X$c?{dC@dL5X40)tQ1cA -S)Hbd+j8gDdZYEAO!*kvsPXZ%%u6Mz7GDx%(a0>#-Hs5qjgS3-S6fHwb2iU}j(yn&f+hk_6Ue(!excP -`$P^rCOHcYm?;PTiT@Eo!Zr(QiYl2+-&v|EueD8OTNJsstE=c*4i0jcx< -0p_#+0Z>Z=1QY-O00;mWhh<1$&wD2X3IG62F#rH60001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y -3WMy)5FJ*LcWo0gKdF>n9ZrjN9U0*TFzzCJ4*=!zzm4S^fg@LYPWE~)d;u2$NB(tSRfgCw@xB2&;xsh -`@q$qFFmoAam5_#^oIdg_y)zyY%*=omkHOn%RZ?;v<3B7BoV#isgz9(dvKjckbl?mCE`J!5~tjIa5Y0 -*qpg2Xo8rr854msOoM`JUoW-~ds=PeN3f@hmg*;QM4M$ -aO+lTBL|zoMX$)j6EuCJ0_p}`168dCWUgkW@;)WHgM4B+^3^}{1N*hC50x-=42uLD+^*ogXG>H6+z%e -DBr;-s`!5E9*&~+@ubLc$pYGs;E9cuZidqnbv2=(6v{G?n0?q&x*vsG;7(LLW3(QRTG!og<{3L_|bX6 -z*=Z*OU>j3~rKth~5~SQWl+0A_xRK&%S7)(_q2u#JyKh!-5=49GZ;%pg>;2*NF7qeQ`CihCl@Ph4VVUgSD{PD}m9RN|eyv9;t-T_J&9H1vvv^oDue -zH#{%b7ARTeb;CXIPoF-KW5VxQ&B%s6kvm2x{NS)glP~a{S7cei^SnvZ)WKa`={G!SX -0nSV-W*Hyi~U;gKO2Iq;#`_$nu&Ofn6*lUTh#Sc-q$Py)oF8`7r)GGDcjNTHw+7TWqlyT26$k&7ux|m -Rx$4J8&(SL<$yb6ia`HiDSM_mBdrcGv7UJPOFlePQp_*ilPt7db}1vxe_TvN}wYU6@OS(2`D65Spdf@ -e^*t7mp1}hU=^%%pG-aZ=ynTTvoe6L!ft>6`!4_C2fCDcdwXK7|cEwZvdB?z|%#tqw)mK- -cpn17uk3cA_B-2zri#1-4IIj4Lc`OkX-)Cx3vrYBDMj6@gRw(05C%Z1hbT<$Pn&*;onm884I&0B9RXj -!l2d$0(>#w`O5ie*+h(fK5_Ea|s_v1{8@AYW88zB9?-V1Um;beMAf{djW=f -^+Yrh?80rCZn$ig&QM^4PKw>UpOuBfd9Vw#ZjGTguQ0aNA;orcY=JooWV6=a=n|A?%eVa6(?=c0*0Aa -E{5K#Y0PaXt?qBqhWsN_eGx)0|^p{FpwlYutr9w6f6qJ3Q_j3HK^PEib08jh}*qj(rpWMhic=BmDcJ! -#A++S$Fx>%!zN(og48VtB8j1!D4myHbyZdHR7avIvFyvWypI$(^C<1sv`|0{N+zYX#ZXZZ`e8(_>e%S -Gj0lh(U_3&A0TLikBL!Q$P7d+dujiQ1?PfsfUccEhBr{>sW0~OrlE{EyNAt1KWpD>=oFXcRclAyT;lv -IF`Zz+1!gP8$w0%`6a6C$1YSGx#=qfQZQZhP7@&4BnlrvyDdIu?88}#oj3A}Sh-LF!)BLyV*BX7a-vB3$4AThoiIB02Llt)=ln!0chpI)m3$=Osdr{jgw)?MJU)jPTyU^dXoer~xv2LF= -9ID^g8WwNg8kYYZYZ!47M>sSo#w5hZu!o@k`0M%AP4?ma`mc2U#y_u_i-;g`RP4~DZK2>k&6jnz3 -V@_)0s-JExn>j$LLH3q|s?=@Z`+s%^CUgO&x*lmkGqyvvQT}RlSrz`DNnAH`~2&VYXBy#2y7qlI~vv^ -9nafTPtJyR$v(*{WP^^{#;_Anhlt{=>vKZ-ha$x%ZhF%ATQ4372AatE$4>}(k!WLnNH+<&f -(;dl{-w|i*Bxj94&4ymO6PNbO&@;UuMz$&r$O^691(JFUd>tio`0w$&cjCG;v5HwsO>h9(F%-nnU{}m -f`cduIe~4WkC(A7PJ)j_w+$+sd0V7BvZXVgcL28a#rPT3VP!E0Aae#MYP|w_)+Jehrts+|8RUPBK-LH -#Bzu7#-JncfQ6bXHt^Qe*^)BpPPD@J(c5YWuhbzSYYO&n?>u5Ha$xtv^>qx)3d5*yIwc1S?TIb% -bGf}Qt?%Zm_GYuwO~sJPYoGX=MSGv}Y@06u?tKONqHxO_;SSne8FkEUnW{3IJT+i{Zxr8Ay*b -<5puLIm|uKpm!83g+WA5_m!bS%Xdw&^I5_z@R2n5k+g3UTk5pM7=l`x4^!sF{hC`(@u^W1l_kUFg2=; -%4AxmDVF=^Y39J8Tp=Ql(>8B_L&~)Zfm&wj8fFKhfthO8;5qWn4eljdZ0V25F(8 -6>D2;Sc2Q$R;yPjLc6s`L)V=?Kw%jR8f41DRyAA$) -b-<3V$IqyqYWVTss3}3$}Nl*&cK{|8MaW_ZyxJn(6uUhqUp0Rlg<_}cixH~d}G>aZoCnuntI|5jy7t~ -(9LTB#lkYS`2B7>8YAE( -gqF$g5?cNT-`;Q=kpicw5&fpUZv8Wcb5S!b{#JsNhtNddD2srGo24e580H%}w0#Hi>1QY-O00;mWhh< -2UpC$Vx4FCYoHUI!G0001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJ*LcWo2J%cx`MhaC -yBOYj4{|^1FV;mVqHEm02fW4(LSz65F{LNYWs2uUEJ}Vnwbj#uTaJl6G{x{P&yL2f4dkQnHad0&Qq=- -a9ipGh78h@V-o&oRL*o5#DUKWmRXz4XM|Rq-lJjhtvpKoX>UG)FfQF1#6}`!T|C%PZ91w09Ng7_48^Ut7VMRSD=!OBGHLc0AV)T| -QS$)q~K`MZtoN*~tTHY5NL?c23kStS3_dt;rMOg#FwqhJ<(mZb^BB&w%E8$y~WUDNJcRQiPs?7899)+ -Z!>umuHP~C*0!rQ6@+ztPEGPz`5YXas=$_)z4QC*}An4BnsR3-4&8l9ENZVNi0B}h>c)NnHSZ4ElnVy -94m2dE)PBuP`vQ4hAsYC_2wt?4_E{x@0`05J)IU@}=%V6!-0HFZ<5I40Q!*OJgBFY~5mvHW{Z((EqdS -y{|UQ)Ee*vN+FbR?(bKCJN_X(&hT^is?JPRe?xJU~z6`vymE3SyjXVK?n>@e7XA5#!HEdGTo48^(?CTbuR1BovY{3V^;Lq^tpWm*Z&c21w(^)W -^Ow^LbHLMrF3WCw8d>96lr@<@&YQQ7ODdz-Mw!wFo03VjiFD$8NKTil;DO$>Vd18~&9fK#x2IAw7c7OyAd4aAbjn2-lK>j6}EkzD35c#rSpiC -ewazZj`g{$&LKjiGew?L{gT09QauH=n#7&iZG}TvHr!lW_nSvKToc(fo`TNIna>QQ-YpgF!Gz5l9;4Uj09?q~(K*z%P^skS@a+uTsy!UhK1-pr^BVL&4F~W)rYGbk-LJMjFu -@xt*%LKRpSlIa9Eq9Ovy%|EJy9`PhQQL`v#|47@hvD`;!V^3xx`QnrDwSBc)`ot8$BPAdK9+RAtNsY^ -Kf|-6#k$?b)!rADkm!`~hv&Itt*!4L80aOZeAePQ7<(ldo-3=z}SpzqrLdy_!yJT@$t)H%SEt -Fx#sikAT`bMUa)QXrRM=g*5yz6XTYRo-1{1B|8(Jr8UE#I$X4F-Rk$?yd^Y-kZsf< -Q=f#rpQjBcv3*@n;h?jlyt0rSK=WS&*_l`OhCI+BkVzbg3k9&^8d^;SEXV{b`hJoVd`RntURrvGD;e} -HW#L`xkMay{WZk{v41usG%d&QyujV)=X-|IC)YVo1J$-imhxpLvOx7d8GJ%^PNfd?ZmLXuLMCZ4B%>t -yA{A5UKjJ|DkT!F_C?U>utWxti_S=!!{lk53h$ht-jD^SQB7FzQBTrsQN*9D;Z`xUV7PtAEQ?13EG82 -gQ!}6*W2*&xCn9twO=MTR0j)bYG*JVsAqfD0(O -SKW<7|-(y?fZfhFu)=re^LB6gt^v3BP8J^EO0(w1FcN_ds1ksl=E5hoo?N>|biB6{q{Y2Zvf?c#O!bD -lk)uI%lC$38<=p+>!H%S>s8r{9U>*G7t2X&4fc=)$p4(Y4w!v@fLSi{NX`}B^TE9j=k0vWPz*AW@<{^ -$}B>%$|it3xx$=Qe%U7SIo0RJ(RA>a1JQjZD>sIU1{N#bD?+KdACR;Kn~vaulvMob%;+RA#J}1KQx6~`FZl)@~}h7)Z?dtbuq*Lk=fd#7N+6JXc~S@4t_xT -wUVzKwc;G}wQMOuF)ojcCwlOzn#fauv1rG$`XxrE`|eVwBc+*s-%c7;X!oA$M%BwK+iWE{T0`@#dm>} -T+Mi>e+WGamBTedT6O<}RICd8f$3GBK7{} -`E=s`eoG^N4>_6*}NuJ-?niwRKXmDq*el1k|%@&&Ut2@ejG3~M6h(7Hc&f5h#vbYiHa9(sW -3>e0mEf@(#e9`haAxJBF)Q;U?PIPmO -(^WFy`mA2r3IPMY*3gs@Jf2|yHb28fQ?{^7{fX&iEvlQ=W%pIXTMu7YSh*xH>=#kMR93&$4Lk%j)tQ!gODvekNQ+t7=$55OZqibEn=CFit(~*t&&%BB9+XTZ8Lr`;Gf~t -k)a(wvk>rYMc|m(@nK&xjt0`%+^naky59tsEvmgAP#g@E}D24w+EoCakv0c9ZYXJ6oA)F@C@F?AdSw~PtrW<>4jqlQ?kb3D|A+^ic7VNqJ!3bQs(2IX{a^3PXRUIA&Xcqo -~Ko1I@B2=F7u7RF;IrX~pK~ADacTCuRD{jps{q9UUj`EDUyk&)9FSVbG$5#RV6?A4Zjz=i~{low=jz} -pW{Uo@yMie+7KnAvs`lwulH(`M1&w8f~3EOPzoo?cTxqB$io#*w#W!M2__Dw%XJM=r?y@efUX43d!;Z -gEnMT@dH#up>1+F80))6`nz%g~U@dq{4~XnIuZ_>!H&tO8<7D|q8pFy8FePGh@Gq{ret&58r)gYiT81 -_!ZL7QxAve;?hGXReN)UHeA6p1t&-HKpkn*l_S1gwF0uwkwOYcdV6u9K3}Tmfq#6rYJn367IVo?rB!{&_eVi%0N}NNM3YQ -H^keQqE6C!74lK1^OOV;WXG^}PjPgSR_3zWUX1-U<~V`!$EngQlLa$y2Qz3&)`>W|Ks5FAd7BVvSD5N -%R7Mz%u-|188d(Hn@q8n=}+oRW{fzJ7OcDo@}|$GFNQ~NP7=|JE4k}*l9UDP`cNnVWX=Sav}`b*{?(LF)$Q99 -y~+wPAyMU!pHPK4nf`ima`OBy)4{V5KAlI-WVXIA)gRXdfkzA|jR2S)@o6wfYUFF8L1G#Vh1bi#jh|k -C;U;fYOpn|dbcz!{W1u9>Rx2jQgM^@!j -t~8CnVk`e;55fz+pNNzf=bo{(2!;}&A6O}*XJj1s*2B}Xi*)D6m=LcnPSKn^^j-kt`ve=J~;yb5rm3$ -ACDc*YJUlrl>Jk+|{AxpDj+Oq@9#YK!BP6SAh7`GMU-{X!L0P@oc_CFbAomp$&%$_Lray0(9(Fj}Cu? -W~4CreV-N4%Dy^N&wpAVm#*zO|dR-i9llSA9j)-nbdhYQ?h=bnV{zMyyf5?F%ou?@GY+8Z)v|u(s7{= -Ey#J&PuSDRp#`{ali+W7xWy-Snm}`05xRqIFQ)l537-%7Lia-mUFn)s92YWsQ9rn0k54#v&<0S8s%kq -Oa?qm}306`M9ZX=wJ^3$CO9KQH0000802qg5NS`)+HHHQN0EQL-04x9i0B~t=FJE?LZe(wAFKBdaY&C -3YVlQ-ZWo36^Y-?q5b1z?CX>MtBUtcb8dCgc`Z`(E$e%G%!RS{I`Y+=}AFc&D&Y}muvV)eQXsbMe@ZL -^_8jii!#MgRNGA$6DRI@`+xB$ml@=XWl27=|w)RZ{ZQ3YCM}z+Ppg{$TL!nW4$}_k<;0pJY;%}}4uQdsRrGiv*W;rk*$?(AB -9oNnr204EKdnh;=>h&H}iK{3Qm1HUq`$FFAUl_gEAMX%J^D3+0q2TdX2sz?*eYZEw`-SGc8nU9E?njBX|&Y2e+k=b}n)-WkL1^ -PF$@V<_@zt+k5yCeu>rLQS-k_p%Gb#FvC%X!LSM|?s=TRJ1>rB-GI__&t!>4FwHN6;hsM!RJue(cqR# -cP4*%e_zADx{5lB5n7ux*frx4uI6)r#xsRDW0+98k$E--ZiRk?9>o>qZ7s`0RkL0xv_dFzFF2x;+$q~ -RCmU4*ha`li?B0ng7=Z@rrpldH^>8e25`9|pngyO_G5hr8sXqTLOQ&_Rb#Ll|TwX9=CpDe`6;Ai(XkOB|e6+_-cxP_3)Y8DOO%F{DyjO~4biN6Gy~IbYBek5M6RSQBMlpuUAGpC1{r;4d=0FtU$@epZ9tdlJWCw7PubB___K{ -@IL@=cL;I8oWA!LwkUhcxmG{dLatCyaL0jpPK7|h-K3u}<>-X2cLF$^!4#$_7d#4OSB^fGl2*yn(0n7 -jGL=O6n?nx5tPuCQSUmac4H>uN+h5@UD41!JKq88o01!5~raSqDy?`p?1OR-Ka-Ov#E*`dmug6A2!($ -E;106fB7ttMLMpJQsnX{!XM<(rVcg{xlHHW}NBro??$V6pBj4H9@{PJ)#W*P=>M#w-h0aR)wTaK@;6B -xH<&gWxiQFW&G=7#Qia;A)tBQX-E=^5U-}fhXeF*XbOo>);7X|FeZ22Yz&me1~dBoVN%idjPlDZOfj{ -P%S4)RobGott&XSQ{P{}_hW>dKwMAz%gsEM?uId{5%u8Fl$VL4^iZ4=6bGYA!<}bi --ZS{F)Sg=ZfN`8QPv&zBILzl$+0*Zi@iASmRO;Kwi8^_@))nQg6zMP76xD>?p|kweo2%uYZ?DPxkT66 -4UW9apCvIr<)A#fYhQ2e8;b@(E3>RJxumliSCr+J3!aug$cq%id82)8o|G!~vU7L)-vFb390DU?RzLEV$i{6=uKb*Sz>X*XId#cmdS}PrezvuODH#ACG_fwtkE!N*?9Gk!Ugdg*2=Ji2}gMR=}O9KQH000080 -2qg5NX^4EuL1@D0A?Ei04V?f0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo36^Y-?q5b1z?FVRL0JaCz-m -OK%%D5WerPAU2m(s|xK^sC%&$AZQ~8jnXtQf(5-x*^F0QflE1Y0{{2U@U`T==MAsLoE!u%>XPLZS4`5XNwx^F=lex|&qP_vMZ-Ti9K} -AVTs`dBoHR;uaicp;lH@t9Dp>D~T%{M6=WkxF)|+%r@O$&_)e1j1tAF*->aUfXJ->YEKH=xZ<{f^$S^ -v4de7jEP329rpk&L~i)g6;4v#!P8tT@Pq-?L!mePY;u+Et4Zel6=Y*q0=Z7I4IYh3n_*%ZpWFiS{OO& -`2PEH;vzB)LP>vY}d?L^W2>6T0L#ASJOQ+W9^5j^!I1B^X!k*nzmRJwP+L-s&VGs*SuU+Y{y^>_GM2C -c797GEnq{Nq|-;(bn&6)BJ0e|k|ZhFmZ&<%ZO#Dex}xGP=i6M=f}L&Tfz6hBSJE_nE9rjEB%!iCh>}2 -%EPQkT1_7G)OgmrbKHKfpgN+(c*D!obz=}r8Ed6%FXteM$3gtj0S0UN8DVHt!?j09jpylcl53{C -S~XRWGLYmz>yo$^|lcNcH8yYgCUvYD}P)K4Lc-~`P`EuANvcN`rp<|d6?2O~a>$gHSn(~$OPWcBq2Rw -zpgdFMG7T;+Mzuxbnb0MWn|IJm$uh!^}xLOqBUs~0+t@`eFL^#}L_p~~6)rM5IKs4*@2-vID2^VH>77$FkCY#L -?5AncRRR*E3vU`j{fEY05Qj{^k?C8A9A@yr_G|WErd^6zdZe -(5>8y=faHLenqW1p~{c%IIpRVkVroE_YW7hw5m{o=azan0Jz?QsFS(f!w{K<&WW&=oZbJEs}24v8Gr+ -PBX;G!Ho2|wZyu79vlvrs^#&4yYxAXV0dDX@J;n3MhCy5hx)@(hnWPKkvO -L-t{#FNoD8dDCC5~%#dX5EF#eAXC>_L|s6PtF3GUpR;k#c@%e^r=T3Qo=CCSgNvcF~xzeZ&3XK?MhVG -3{j7fexJH36TmyGjAfa}McA*QMWrvIdvE<`5s^=38K1QK)gXWhbMIT`a+|4jWRfznJ>@kKh6pNkMHOY -SnpFFw1DKJDAR11Q2nxV;;O(W&3sY>w7bw81}xXdW~t0qwuU)9~DTY#%XpBGv -$1m>~xSJltQyD3Pfb0Bl>Ih=aruWarVmGE6Ok(@&UPHi5)ws>2p%&Q|LQB)%f4x^`l6b?uZS8~EgCCU -WgNWTaqOote-tPs&cNMW>(~IJm@6aWAK2mly|Wk{W`3FG+z004~z001Wd003}la4%nWWo~3|axZ9f -ZEQ7cX<{#Qa%E+AVQgz9-@uG{kx0la1`C)W)FXR7z9`fJe$-K!ny2XI;C2auAa3VRS}c0;E?3HF^SNXjh;p<1&=MYm -8cLaTv2$Z7s2R6o($rVqvw!t327>rddIXpLZ$A@>MzpnI%A1pgn3dU<1RBi#Fgs`(ejfxvt!=JV*u2x -(YlM@Rhx(jSG*jE|C%)v{QU`!p%md1f8a6-_1@kJJy84{$Z_Y3J$p)NXvfXR4HtXo24TOw4sB -tjj3pOsdfG8VlR$nUL`d9Ap?>h&>T%K8x4gTAo6Tj?g%xS%{Wx9Ja`^rzLDR>p8Jk`zxHG@sWUcCFPm -6N58cMYueiyUGe~za5aM-M8Kn}YkaOlZh~E<&&Fod=zC)%;*ZliFcbvns!Kj`#ik{s -;6hRF}HKSyVS>}@xj~QiBzIImYcH$MIeL!`{sK@-0|XQR000O87>8v@W=STQ?F0Y-^$`F7DF6TfaA|NaUv_0~W -N&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJ*LcWo0gKdEHo1Z`(Eye%G&H)fcH#g{Iw8RuAYBrzo%_Mdo1} -jLcx9vrU8+1(I@O4E^uBBPCh3rNk*PV4GenlXu^J_wkOTJ=bc*X0v%^D-E+56RT2b%lMs9va&Gq-xHR -LwJ<^z39AZ`sT^ifSkPRWAh3FK8Zdn31ePf&A+u;}8nkPjgNFQEWHx|@3`)yBxtX!nN_*cHqy%QqYiQYE=peQTSIh{qyVXtN3e_zKX**2!b5uYyoyW(EvYHLL&KSEMWzOr6Ekwf8nHT? -9=SbBmSWEzU`S>xmQeZ*DbAQqL7w3y6EJl0V^0wE-bJ(O~|-mwn7lYz6iT?1m;+ -o>I?d{aG5GB200Ddk&~27&=g@cPB8E+w)1cOyeM*bU~2AgYa^r}k?tCZrLcw)-tm2xvJcEyp52otFET -J#CXKt<8h%fPbIBJhq*4qM{NzH0*b)pi_W=qlX>O5? -O{kleoK@?R#CWSBQBI1YjZqa-O&-oeH1_8RFhiVCI-|L;(ha4L2I -G@cj$4o&526{9||oC>bU-04_?>D_xT~WgLZNbtgsk3Ad%BGio-*!iq+pV%Wd`4RZBV&#o_B`dg~|QTo -;$_e#JcVXx+iNP_LJaRYLPi@{9jvzOgIH6RFam*PEUh5QFLgkc#G_AnqlkIUGSpGF-4ai2}Pf<|xS_!h%D+Pyf#@^&JSx`|WZ5wzX*9RED7 -Ykp_FhkMk8N|=Pg$r{s<()LTIE$buWLTF>pmUzSYrW2#5%v}8+dn)wH_B2Ssyq42j|8}tZ9)Hf%dLLA -NKQ)A|**QTUJ+=nY@QiwrMr>NP+HIf%YM7LlCEk=<<0E)X-=J7;Iz)P#eYlXjLLJ;ZFr{Avo(aX`UsO -lEjB6$yX;iO9??V4q7UOA;Zq?HDk{7ub;KitJjBhJs^6W2Uv}Es{g0=bd^~r6_UbB-&BXx|Y1o0g^xp -iUb?Vl;#&kmEW&i|jv#mj6H`Iv=$zLEn;9$Ajw9@S|2r&MLnb>SI_)IaW?orm{J{57Oosj2%-Sr5hgD -Fz(~sWL9<#uYSw7+ku4ALvTf^EX7a_qReE{0&e`0|XQR000O87>8v@Ef}!Z!2$pP<_G`)FaQ7maA|Na -Uv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFK1rfPiYSXD;Su!lyT`}yxPxMEu)b^U{VoM?)-=jz=DO3hwn3Se0Fbsibxhf2>2dzYDFl^ ->N#Ju8~E9pZid8A?t1vgkQp=Yy%Fb*j7BVRdlRgF1U_u{8y{|W-@^TBx4pSut+zgGuG -Yb?Yx@&yZ-f5>g!`Ksqy9E4>&+@yHH+=<^;aL(SG(PIAGB;29Z2}VGX(HemMg>VoYp;DOq9YX%tDjPUAg%DU$>XFpmgWQ54V$o)97?A*TRb -)|-hELOpSL044G0pBGqhRcctm-d#^r(ZG>A#atLmtB*^AC%rq@AeE^xPK`P+qf(ST7|pcTx%*A5`OuZ -dGe;3O04Mtn_u75tWM!k|2Sb4mXd3xkGSS$*JKCM4;0*TUG(z4bemu$6nSff^wk+_sD?a5iwhhA!^C4 -HgK8H4$z;f9X>}h1f4n8nr3jRQ@1aq=aHE7qZ*+Qs@37QWK>LP8ej`I^MTj>=R2IH`g_2=&(bEyv%C9 -X9ZCdrDi_d?w@5qj3VZu*H*7zDlZ(w?6WX)kGhZwXZc6cl=-LMO24TrFq|^|JV#JT0BhzPordnYg$IW -{;>&ww3Q*1LrgdX1RF|*uite(XT(G>5rc!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bY6WKqen@29- -S3X~P48q~k}TV`x|J-;2rrvjYC^ZF7OiHP{hpA5-*d(5Dj{vf^SWS};F?J))MAGa4R6xyo>fIHQ^g;8 -9L*kdrXL%&ux#R?6g`b;$(Vherue*VxhR-asi6!Ibe~DKXAez>k@l2qOH~h?4sZE^g4R+j7K?)I2&)v -HOlcXZ_|1aAhwq3YT#=i)V)hMwBm);z=B0VZ1n|L`E2U|bvq&Z5Y2ulf>SWX$QKbb#k44K?@6LW)CS( -cL@LVrrurU`@De|$dbyRQvV7czF(e^Ck71vo7DJFIaILy(DuTmyh$trE!bq;i&njgTQl2t&u+YBT)71 -L=^=e*k0DX$=k6&083%Mp8qkkY1s*hZIij>ot5c!b{6EPa1IolK%5CV-E_CRpbOO#sr -+Ch;x_|x}fL%}iM+NISHyntm+$YNKhUoro&;HjBlqB06~MZbpC=@S)Spx(_onZf}+vb>dw-?Nc;ybIv -&&_Awm^r9k{-_T_l@BsT(3vqYzR`j(U?bPtufyNdqr63WID!#g70t#?bF?|f$R6vR1f&l=*EfXYc;O{ft;V)>tT9Pm#Fmw -;KloQ#%*qVERGp{Pbs*npoc!5zz6KN>(58v?^bWlRvXM-ywWw~yPsTz(>4w{e&FWn35na5tRT83TL!L -#q%3$v#Eb$)>SAFslxc@@oUqWjoi^x@2g)i(q`m=vm^ITj&0#PX$kS+|XQ>j)pkLzwOk{m0 -^ljP$D*(g$bopMQC;asRWKdrw8nu)?tkS`d`s+Z4wi7#oL;nI-svd;5lbWhAE+hPYMG3CYrIo;F5y#28PZR!r$7$!qX{Y;%} -AOwJ4A1vJ#0B33V1^8G;*h7|x?47S{B3UM*+)LzVk&9^(vkU8P1o5XcWK3=ZVpB+63JP!JLCh4dP{eC -meN)yg8i}9|7_(AzD8$XyWAIswZkc^H$CllMoVMm47()c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;ba -CK~KWN&RQaCx;=-)q}25PtVxA@XE{G3>R_hb%Q~piM`bZ4X^BYJBcQWGP5;+A#LNPbb^4oT$q>m>v>z -r~B@^pRSawNl{ddX%rL%Vf9ujL+F#1yfIL;_mq@u$25~7C5>QfSwg{?0Y$lvq6#ryYo3*|&bAHb_P+< -ZUrm>*BEQLR^TjG9`QmE0e2<7dv%A~&I{&n|TTVaTjUjPBJt$;zBV4OSLK(VA1O$w~oyqkID@cIMe2NG=?m5-OVyjIAhQ4ZP~)c-XU$HcO2#jFU^UkO -B_6t%4nsjXsuFQW4l$5`+nYc$u*-2TX@s4dzS}A&tWzRFqwkj!!(IVr|@e>#E0j`0$8g|52qhJ@Si{k>7_%3&MpHz{@MmG~ClB;LCD8dP$8@Slg -e!P6~r6tP5w_#0~`|?8$CIoo!&R<0McRXVrOq1~ctV7FH0WG6|aK2d5knO`Rs=a!omgnAqNKMLR6q@z -!uzv3&hqcgpXy<)E9D_~1Kzb7%qwt=?te!hN3$+r!vHI9vGN&6N||w7zq30N3!=K)Hn2Plqz>Sq%) -VpSHp}dCm2I9Z9YhPvz{cNedkU*4yfZmFv`wEW{J+!r~(Peyy$Ssl}nk5;Z`<5qXm!hrtv9mv~M32dF -vAsANu@Uk`#K<&>egjZT0|XQR000O87>8v@sa4&mh5!Hn)BpegB>(^baA|NaUv_0~WN&gWXmo9CHEd~ -OFLZKgWiMY}X>MtBUtcb8d0mY$4#F@DMfaS-2dFpz5(5$pYz*vCXdKB>+C;X4$n8s#!hk0~eLw%eX#s -0P(ZXmg(2^4?xTmC86z$*%JiSOLtrL`$=H$^vQe+p!n9zP4a{bB=CIlyNuWd7?^XZ4m^=RNT!}CsC{^ -wrK{TSafW`$c1ugr;unw^NjC^&F$*rg3nO9KQH0000802qg5Nc3WblYIdI0G|T@03rYY0B~t=FJE?LZ -e(wAFKBdaY&C3YVlQ-ZX=N{8VqtS-E^v8;k}*%iFc5`z|B6#t5)mqufuT|d23Qb?p<7j1ZhC1f96Rzk -(EfYuIH5_K3U9W*clX}eg*6p$UNo+;$T`TWHr9dIkx`999`_mS<+mhhbOw! -)^9c1t-bGzYePE2`N)W-?N=&-LbjiT4*=+W_BWFeKU?&J6l7+G0JWc+dwcs8L_`(wce -R4DuopQSOGOP+9aK~e9Hc=Wzp$C5(Md48J0yq0}X^O3Ce-+4n9w=G#Vb~PfO4yFg|eQtCgt3Scpwj*& -N#-zMYQNyn*AKoUo35q$`u?Cf%mzjHMtRSRMc#fl=l`+)&)Jp?n~^yyH-^9ot0HF*b0%whgk&IIsSp@ -$6C=eL!;m_#4bSI-Hz$MA#^m3<@iJlVp*BLfyNDdstj2jF+3`gWOKHx@y<5gOo!0zxRfx7i*2}o88LG -{m-v%g;rhUReY0+N%fU4OeLKvYek!G=l{hoP)h>@6aWAK2mly|Wk@7dovEe)005c<001KZ003}la4%n -WWo~3|axZ9fZEQ7cX<{#Qa%p8RWMOo2X=N^Od7V@yrJ`t@A)4;h`LcW}AVrH^a$>z@t -rel`Drw4`s}jw@M1p4^WK~{oyAa_IzJv7>zD_x*1xvzq-~&S%fhRV;L6uO|-1{9|A{v`)yr$y!7(-N> -t#k4YK|d114uR6d{GK-D4OiNcMx~zwUkgtzYSXq#va+}W#B;61L-Lp>uQr?dcd&kfQn@l+!O^%8uin$ -2_AB$V?$1Q|mBc4fv6y-jJexF|>*Nz~p%O8zm%mJT)>}$A^k?>(m}+Wfl}nmz;U~Ol_%DHf^n=45J!6 -}{;fB0N7RQgfzOz5`r$x!cA!27w&o9B_BZ0hI8N6uZU`R_vzga%$T)q*0sQD>-^@a<6kHr71e#iTZu6 -#%Tzv7vEdEo<4O9KQH0000802qg5NJ~#{&K&{(0Ph9>03QGV0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZX -=N{Pc`k5yy;aL@+b|Hk>nj#MQK%ra*8n=CMSJYYw;&*BX%#O@lLAR4&fj;bhegRw`an^hEY9w5W;ujg -XHOty+lStvlt8D>x&Z3nt?mQL@w@)N*PI^8n -`#e&|iUP)7Xgg@J(T@>Lc<2T)f-e!a7SPL@x2M0F7oK%O@%0kZNm!;sIO+#YZJ39dZ;*+>C=*l&2I-J -au0;#zU8`)yXk2Z}vs-t;Qz|44>Xty!1XW{G&PPv)tAu -6tjXLmXhTDF9(zyI~V*5g`-&C=q6^`7I37PiS6C4$_6^Fij~Q0N>t3Ai-~ec6l#xmf6113o3qQ_^VIM -}RwaQLrgC#~5m_i-j-)Xe_5(epgPSC*iDF%;nntYJ@>LDNhYCJzpJnoc!Jc4cm4Z*nheZ)0m_X>4ULUtei%X>?y-E^v9pT5XTpI1c{qUm>KJi!+zF-NhcT7 -;K8|URz**-J;hQ?1ye)D7G?D$F@>Qo=pDzk(6Z1d7Jb)hc$|su|!cM^^hXzK4G)V87ovNo0}`v+U4wL -97&SVXp~f{xLwLhWa}-Pv9}esOQq|XY)Y{f@T0Memv_=Ay=6$paDmB^I?^#@Fj=3xhvprCctP-(eMO&!l0cY6OD?oUMhP*_9T&WD9O!N;bygn_BwWwN?dkq-ognqv1E=DoL -I{e@=q1IYZ8`L9ldeR4q`%@r35B-d-hHmN9&gIO7G0oK9$1+!tbP*^3GV*diNI_6ycpg6cibDI0@;6; -Uzn)@0+D5edS9_B5A*H-G&#wrn9hx4@+`VsGJ%vP3mb!e6ACnVs}YXPQilN^pbPo@K5hJ -YRE=iwHK-u4ulK@*y!!vOG(uC+ylHX4Ea;GOHZD-N-_)rB*ewE5Qt}1#>V0xsHf>004f24=dH)tXQMK -L)yj)jr-lgflirG1Z6leI4{69ZeF*Qm22<=D{El_o70-UB7jKM7=SJc^Ga)_!QBAXf)y19goD>H1?p% -IC!{7?e9%5e*6^yLebKoNti)%4aNzKwaT|Hp117=EJg(VFRiz>0669iLJ<5R-zObcxLt*grycWR7sHR -)`k*s$@2WQ@gwQ!7RcSH=^a06#&gD(x1yixWKQadL0%NMPsb&DJ35>G9jep`LeaG7J -1HOy;d5+;=-t^wIX-#N#e(sZBtmKDia1&_m{!FZFqBgfy!ZE!*v6%CP|*KKb5|9IDY%;?az!iC5YS1= -;cmmgZ9BV*)d_YDqD!-iCmImD{i>8I_(IiY~;1f-PmYyMds=#vf(^9=HAo?8aTQ~=H_9m8)5SXJac~& -N0J4C!7lyW8<;3Tn81S(wDX4W@&Q~?wH99oSRYSgIE;JfB4va~dF{+uL<^8*qj?IMM@8sZOs|8_c1WG<|9w;sN@z^A!{mBhRo_4i -e`S4_W#Ee1Q&KoGLCd`dr03n|Fixt0deNlpY{5%t;{4COu+cTwgc9lE{u^uWPy)4>BK?RfT@yu-~(wp -oeB-8KaP3avzO&X7w=OkP?8`AG?vRRiRZt4Kw83tkK7sNzM~Ri=~Ze1!&(Kcv$8J}M8x8}k_ro(~=a< -$({#KG^LMr0<>bkm&A$!O%T8hGW;wIfi$eJ%Z#>06E(o%yED4>Dt-;5N-dR9?^xqG~#*KBX|>JM(otQ -6ezk+jbO43xAQqr>h|50KN91cv~V%|d$n+QLu_H8f@yh|Ts)|cli*SG$=|QI7A2x6Rf{j8uwEI6IgpL -WM%p}2ji{DWSZG&%79SkUa18EE{FQX;LIUiTSr>!mZ$Qy#c@PJm7l)%XK9}rz8CXz`UP~})$x;A2ahy -UzBQ1ho++pm|TM9HGi(iT8c8u_{$NM>ZHpSJ4ZVhrRyW$p(vDt#=g=$Iz*1cXv+&gL?r^Wr6N>|LPLf -}IcjFmo}7LdcxsYYC)uzgI_U*;(5fk+8SV=YR_t$?anF5_LJ9YX%So6N>1(qZkohBBmvoD3XyB*P2T( -9E<8!q8~apM0nW@#K%3^}E9n2%qbL#8JXM-Sy>16*|w6QhS^q5&Pc?XzmFB{04Al+BIg=hXogxBKHWQeNsem^bN2bA@~jefZ>SL{MRAF&HYG+CaL$?_cj!dyDfr@iw}26rsQktnA -0k#XmqT89>pS&Kq0W#Q8xB!Gf9C~_nUO?`?X$~m?H&WbTRjR2A~ZUmf17wt@C_vEncTL=MZSnjMf$vQ -=x-3AARJLUxZIaG3F*$hfQ{O!ouKiP<=c20PujP(VMtNXC534`g*E^Apj{y+nNN7HiIDb|}0AA6`i^Q -Bv~9v|#-xLThs5^S{fZM&Z+3<|C*3CJy#uk7x?)3N=x!wBm=jIU6iLjIyvOCgloVdYILK|avN6^Kv~( -%5K9lc9uUgkY}G?Bbq4?%4c;J-c7}r(fmzWyg8+rvWvZ9}MT`N7CT|Px^TG1hN77wH1sOVv+#o -29cXDFv`ab1D{Gysi9D}#zs5iy3;X8UB%uu6@rItMLotd#LeusPKIZVs~*1xg9El%nwN -U5^N$0>Qemab^+}yEUTdYspdaAN5Z>UQtO4Yg`{@z?bcl%(vAUe4TLx9if>kkGVquccCKB_14Dtk2xE -^jDMu0RjAi3y9;jNF7V4 -GaQKjR`Uodq%RU14l+fZk0ZQe6M(*=Mwaj&Sk>8KLB;3E7F?DH&=p4PhM?cGSoO!VU5I;p%hPsuB(Hp -RoCy{op&~mf>0VJK@`>QAnUhpMej3o{G>4cu9vy@tBIcz3Cw=fhWBNv(cx~U~$L??jUV(4XOnkKH#jx -E4&7{%7v_?-TK1nrAxJ1gMed=*x)x>Jn(A=ziIf=fP#ciWZic!ViC}pRY1}~HGBz(y`7e2fn!V_+ahw -Km?VoOfK=|_wxeU7d(gOp*7@IRU6SMh^VUf74fY+-A2INNCL2f*n|MI>Z8jc$jrd!Q>_mBFE1Rz%ig> -`xL!xXbNNngK;kQ}Zr#-#`-Y3ano{)eaIe^2(79B2>W)3to#$HNUJ -+!JO%?CX|&4zy$64BBDr6^)@yHUrqf4H!PZDuXzNP#iwC9uM}I2;^1enxmaN&S`8A<&(a?J`vi8U44K -Zj15Ctl7w@J{=Y}^Urc!Jc4cm4Z*nheZ)0m_X>4 -ULY-w(5Y;R+0W@&6?E^v93R?BYNFc7@!D;6q(NT{nOIR>zc7HEI~c^n#`M}f9hHW5m6NUDzizDrV)t( -TqXVw&O(XJ=#5g=q@B*h*!9EwvPt!PNf`f_mQY0-LeaL6F<Mb=x?@~mH -iIW=?B9C$xVL}&iWh>Dm6-+5eSQjsEZh62I0%B3U|PciFJB!n;kJ9lw=OyiLpuX(wL)5IBrwQn9aNf^ -YhXqed<9!2x2Vh>WDbJRqMJAgT>N6O0AT^<-EA(lEYJd7#vfca1GBO8F`M5pK?ECqexnR?BKcNvg^@; -)&zz{F!u&~=O*Zy1YJb}ihNzb=B&kaNR$*GQf}{?2%P>w>LTZfSHUQsOxHWM9{97rhxk02^o8d34!2I8!G@{K8N^D$P10FED2ywPPMo2J^>H)SGO4vk+~w=5^!#U}N}ifEndil23 -BFDVz$4o)5q5a_4qJ8pKi!n(#T?4EfdaZ4jj5YhE(pRojY+ -deBZ&#qMieGY3NC|Kg>>39Pl0&WCH6jIK~fCsjFkWnDH!@0p0GowdAWmxM^>jCT-H}(XyC!#-g!jn+LC-j=-Qu$-yli@DoEi;?xRHv)gJU+qBy%2 ->L+7^cSN$~#vJioIHU9q)5+(zdPr0;NjMXdl=$#t*MM>FiVx31{+!pwEJYm$@gP`5&Rt0)vm7oxM+(L -G{&n7FzmxlX-IQ9O1fv^RC5tqp8-9n%R#4NnfYZxVdx+UTLpC${rn1I>1#{!CbslooWkhXN;d{zyyp) -*Kzet!M{Dda`+SNAnuRNm=2LsMjW-qJ9aib;d4|lR{1fojKaw*?YuyNuGOkOh){2I_ch5$)v{r0Z>Z= -1QY-O00;mWhh<11CcJ)g0{{RR3;+Nn0001RX>c!Jc4cm4Z*nheZ)0m_X>4ULZEIv{a%^v7Yi4O|WiD` -erB>f-+b|G*_g`_igxV~4?Q0>0ZL~00w?X?b7{e&BylAndRFc=`zu%o?D{*XR3)4#^=kB{--+^gF -g(pyR_R@aLQc5gCFVs)Ve>pOyej9gWp+IWyC%!LSUl+l!?51L#b+fuJFTs-zrDSMiVQD~{JS)&=H$&~ -dXDgc5BIFX@J6k6!DlC<2m$OtE?}P}VMz&u>E#IEYsFR^K{zy0JU84+OID+bwCVs$*esj~EAtAulU*Q -nGIA*UrG#`2W)yr*aD$ppu6Sm$nVFdwX5c~A%2=}S?-;%1F#a20D~c4UNoQgcTRk_J!nO<(ylFpUX1b -jy_ge!}Ss)eQSoqdY|8IccvdHOMi{buH_T`N@5?CDoMXz{ -}{+&4p206GY=LEKm4-0yKf&KpN6QkF1iusx}6u!O6%mmFmrR|il7o(()=0$F}=4DZc_J(etuwo4b4^DRGe=Us=`ocI_?Yjpqi?Xm)76DJcR4&Kvx_n{G^eL%&2 -iP?pfjA4@q3QU*bZG>hFS+oj>ZM+HyH)u{p#q%Q($el`@BuH2CBDCJcJbD|Fru`h)gF;#1=|bYXH|p5 -&;1eQe=9)YX&}Lp{gp!>bnlym1lC`8QW*u4*4+OKda(b%SHVG8$}TCFEvk>rh75W3xEqY@d*$toh`Qh -Bb_0Lm8t03!eZ0B~t=FJE?LZe(wAFKlmPYi4O|WiN1PWNdF^Yi4O|WiD` -erC953FqmMFA(sK -?;U2Hj-=?DQLQ#>Ef{lYzV@aqGXs8p7ZC#7ge*%1!be-I*m)*Rw`Za+%1&6XpGBkaI4vp+r`YO^ -PK4SO^pmrS%R5L#4->mu#(GCkiUs%I|<+669FLfQmX{KfdRw_aK^zo!@6B?Fi!GwB{lG4J=0!>v$HcN -!)Zr1`$iQ48QLFz&Ngxnd`U$H9s+#YI35|1XA_2EPvCeot4L3;c#dYa&P1oTstks}hmrydS;4SWmHU> -NYUy0ZgD|xenj&NZ@y&R?;%kG>o`@C_orG4T@C~F~KSuSY;0jSM&s|o>H5KlOs#5-3i5QIy%AqBsYSe -(|#UKqqwnRjK`;)LP^wInn3AXK0QOBoIx8Yq22)(j~j*%8$kZM%(Fm9NTCeA5j`nZ@1W0}mF!@-r4N^ -5Zce9aajPrb8*XWH6`p9692#h7m>ebO%R3`x~&VDyNi46z-#{rz&pydX&+06tX|;?Reo7c0a@i>G_y! -LZqEsw**@1(Xd6jIx5;WFg3(YVxuw=8)HoRTyNl?C2vJydX7++1O`{l?YYwC@gx-n4NmJsvW7N4%?la5=~kYCk&H7u_ds2cQ-E|Jyi?gDMr5^5nvr&WEi**W -z&5lhSN9xh3>kgH&28xjP9^@s{8x&^&NmzO_XKTNlGl|wQpS$_1Ip$^&CM;QM{(bwBpyQOT*=i0W>(D@x2ja*SU&F|v0AKB&1c5%%G)a|}f -aPL0#WjuWDMWLQHvmWrKH^{oVugjv(gS~YkRl+HBq4KS+tHozKTjBQ2a?sU*(`wxS2Y*4r+E@BC+5vC -Z&|Ubhs}A_NZ?~a=b=iH*3bErTO^S`Fh{Fjlwk30Lg!jZtp9SCIS1LFj>yzyW;!0abk?SYGkg!LuP(U -Frp*73X=gKV+{5OYbuAL5)24lJ1H{JA;baCXj8Wf0(B`TTS|(i}Y%ot!=KToSL7z|?Gpsbn^5@g4Muz5OX=8aKqW%coZqe23<~? -C5R6_O%~0e8IfFqk`F@h9S>w=%kN5=zmZpzG$E%SATwB$I&<7!3kAG_^W4K)Yst?*N3_)TOchs*8>xy -MFlM<=*56zY>r+s1!c)hLT4z;4*B`R#r5@pC=-QtN<|`JZk#|t%5$}HVaD?tt=W&a{HgxUn}20&I3^aG?a4>N3__tKKP@idia7MP`R&1`qnH7RN6H24j5$+$EcM|hL7Z1sa%ZJ -P5#S=td&G#kBl{gvmH*l@6plePRcN^)gHXw7{Hz&D>1dYi!=IpIg#TTA;|E(jxJi2wF&z{d~&0*a3x{ -$1Y*Y*eWl1(ON!hu(-90Xck#vj<@uETgVB%6pDu2~`;Yf`w>N(q -H*^Jr0gxT3-+ue;przJ0Lkj^-Pcmo@oHQWgHnk(o(hhM332lG1?=AJknPG46g&E-svH`nNy2cRC951a -Vn5q-4(4ctB=+E`@>G5wSHUEdSD?%r&Lzp7m-!ai$N$WUs!t6=`SMo{FeQDsxJA;d+*5ukYMMU0({lw -o(Y#d;@`@N1IUqt(>?anfa+j|1VcLG~C5JZ4B=dI=slOJ{lc-7SD9oVw6hE*X8)hMYjbf;!4w6k;twG -tse`fBOYSSX)#PUs9<=?2c|iKtG#0=+t=bal|KhP%ec9b@C|-R14{h0M0yfm3%l_#aS90|XQR000O87 ->8v@yM^$*2?YQEf)xM&CIA2caA|NaUv_0~WN&gWY;R+0W@&6?FLQBhX>?_5Z)0m_X>4UKaCz-mOK;pZ -5WeeI5DE-St*YA1E$G_lE2qRon*c#k6arV8^%ADUHA#8B|Gq$tb}$hR1j#ndlwKKb;WW~KmgA-B@kAAY6^5^^WB`2mHUe%zTKiCzDUenpKD{lq2EV%LZTL6CE*6#4YW2d2S%e9t&Ut;M+U9{ -viYsL(UjU_`%L;#xsCO0nub4FfBsLFrN4!nok9+5G7VIon?^%zDmf3>UBc&+C%}x;Es)Af2=?q&MiS}bO>%c=ZH#$QrX{W@wJ|ow3TC{%O08$-ur`+p7?bu+$=r -xfz!{rHlqAZynl~3C*H9RvtSZO|`67Ct+os7D2O6IF0?mG?5A8RJT5p+*aai2|5xfwF3n<`QXZFyreY -m#xRea=;pnQzX9UH9_aV~@hec^_DV|&sBQuZHuS$<6`Wt)i|(?PNcQ*J-NQ} -XksaGh%24wc5RB-wE0mdT3KkU0j4r;&!Z@QH!0UQ<|rf0dP>mJj%d*|cdV0%coRWSNLDGpN@JB%+RMj -oAeo|$eu>Yky=3xrpD3^4G%vRaxsAi*nCR%lNG2ym98XN_W#J6IzpMelM0^y0*_SOAtHd6*1|llHYp` -AmUVuD~H#$a_8k@ILOh`Dp9A|oFoN3W;F*Z_7XHh -`u032zczr?|%=)g}pC_!F;7PY7NuEdW!{=77VK5%A?WP!M0X<-wc)scYr>uT63XC3@s#N8Ma@nW&Rikpov0NLg$Pu`ATXy+r{Ey3We)}rP`b8=9!WTtCZ -_hY<|9FOiaMf$a8;xvBG#N;IH%EYo&`nxBQ&a9{aS|hmDFs_?D?B1ER7fs -PRI_DZ-WBK3YS4z(RCQZwF?Bq^3m4w#pwj6f90rfWD7;W+i)i!F9l@B6ALytSz5wS55f60NFr9O9&#) -C9tO&d9iUVvSgSmI3lgLASds%{bSA&!84oybgi@43E>Mj$bOlFN|w9MQycdz~Cncdpw?)|;Q3Nm(6!hbGQe5@9xouG*$+pb0CGPR+24dVD+Y$*AX6X*QicCe=%zMRg>^i%j$2s}?@A;$I_ -ct~#_ojyq`Z$s|ivPa)r%%5x>eo*Eq$BM$(>-=aPax}UxMO*II(T_Oro52xgTLQ1+@ci -i7orVBKLZ*y`Wuuef{ih75@TIO9KQH0000802qg5NRcB6**yaQ05}Q&04D$d0B~t=FJE?LZe(wAFKlm -PYi4O|WiNAiZER_7Yiw_0Yi4O|WiD`el~!AC(=Zf%&#yRS8YH5GjmJp^fy4w7FeE1NfKcS5C#zLAF1A -C-zsJW;+~(3wv#Qo{&gDDbxww^*TgdaOF^$4J2fp1&Wq=i0ipF5>pQETdm%85>zQri2NImPdk}tB{d| -(EpwYa30PVGtH)&=iBd4;$%0|pvrM -m&xN^Ug?b0%65H58LmlJLdF1kQ1S^bVzhpmQM8Xyt?g>~iZbZ>(RtXDambvE4lqaaS$WA!SWzlE!tLD -dJ7F9WV92<+EX##D+!r3-u3I=fx$jB-*)!Aqdb5G&kd1D!#J`enp6*laiW57<#9hyGh(_V9&Ur8A&tKPlfo -bsp*P+{4^F^13ppRC732xdQ$OpCi>+A99$kre5r|K<`2Qw*wgVniqIua3&dU0C*(C;U!9#TSK0Nbn`T -2C_mFN%Dr471KkMImzZz0#`M@?jL5;)&zI`=${yUY_D?(?2pQVhh-5rtx4{! -AYS5B_gL??H6`9|}t&*(qAs;t6MVmNoTZlpPJOz`-HF5euHIaEo&Nh@Im|=1nD!*czvchJaO}YMqmZ< -#Og~3xEo%w{eJO@_Qc1jlX=kHX5Ekch|=N+-X|>15ir?1QY-O00;mWhh<0`70R}X0RRBd0ssIa0001R -X>c!Jc4cm4Z*nhiVPk7yXK8L{FJE6_VsCYHUtcb8d391vYveEtz2{eml-|-zTedwcENs6H3uRfD(o0# -4Cs8J19oxuqA^(1DCu!%>9O67#PkMUM8N7@XU?78#&OT}bhB~wXdx*A21{o0qc?#f$0`9!-(($~cHSm -fY$l2-)uG`B7wCzjU{r+*wX&Q0E+AoM7jOlYft-2 -_SvQ6i&KK$CYYpbx>BD5w_PKu_g4J_eI$FVs}lwJ=D{&S6fz`)p}6vAPK_-r91NLK -ROa+i#E0*@-H~LBhc+3w^Rw5xlLpnvB$;w`+uoGuHc$>eI;%|&dx^s!<+qZ}o|Yx)oE4@+On<6$U+2! -ezVLpA!fVNu!XH-TjJz6Yh%Xi?uTd^}V%Orr3gPf1$n-NJdO;T%3?SN{P}O9KQH0000802qg5NP% -{i$`}Cv009C303iSX0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rFUukY>bYEXCaCuFTKWoG=48?bU3URlz5J -KtRp?_{D99^z-DaE*n(im(j$nwE``#MoBtrzP(OFut*5AUPTF!+E__rACQ!FQN4_E1uf3^F1Ja?C)(A -|5B#)$OQl4SXPHPs!}zzP)R}rG9C9I=x(bS~wvt=JECU@OC<^7pBEyB2jitz&KI%LVT&bffzc9Q$RuJ -QJfFCH3L%wYlk9*(OU3vA{KDpnL-h?svGG2+0VfmnAj(&1F4fas1jOh-8U8aBitCMtJ+0IVr!dPcQjq -t|BRv%W6hxD`b{cwiP7;zg?I_Gzv2pe_0RfI)|y*m>NTN~-~m3DzTGyrYi6+#RW1-MH;l?_+Ff(ytRe -Myi>dP8d+NZK`2|o*0|XQR000O87>8v@=4ndf%K-oYTmt|AAOHXWaA|NaUv_0~WN&gWaA9L>VP|P>XD -?r4Z*6d4bS`jtby7`h8!-^Q>sJi+A$Ac)1~ykcO*F`DXlJ&X -5M@AxrEOLo1nLjK<;ir2hO(S6Lm28jw0}ZfGG|MLSH`bMr`G}t1t~;C?*@cTEeEe)PTM#u=o3$XG{Zo -qGG)JaDDr6e|rk(i!Fx0Ra-KMG=^k3#98$E5TOmj=!gkAVze$bs*hm+WBQ4wNXCF2#t<3sJ_R>1nflk -z*`v*gX&9sRECkMOS2z^ -Q0ZN654ViSF1hA<*?XtUdF%DuWGM*a_U7sxogIFdNO9!x}?G5ARC%D%ap#!R=j}c6G_&{7s*vcpMmOy -KToN7mOjVVaKV(93FB;vM@2wfX~4O9KQH0000802qg5Nc#Px5_16n0Eh(u03-ka0B~t=FJE? -LZe(wAFK}UFYhh<;Zf7rFb98cbV{~2JszlY4uvi%<$=JQGml+F*DJzA9kRl1GT+A|SO -5i-JskX>D$g(n)Kx~~+H6icT5u|b_8(|StR%-HqPN)&eSfYe;u+W**lJr5 -hbr>{I1G#aPz^UddUJ{t!W9>hCKxnDyLfmCDmq{h7~c^)rr(5ec!uEB2!2i>zFpdp&s2N5$v$jsefCfJq=+G*F~4|6E6fGS9rm}&SpNl&N4JFE1{&u7!brD&?WAEprj0Zt{)3 -N3k7L?MBjhJ19UV0Eh@6aWAK2mly|Wk`*sy<}Gj006BV001BW003}la4%nWWo~3|ax -ZXUV{2h&X>MmPZDDe2WpZ;aaCxO!TW{mK5q|ft;3^N68db<{QQ*Lc7szgXilDijBqu0p7y@I_v36xqh -oq7XvgvO>GrUNotz3%cA-1Twb2#%2y+^Lsr6jAeVgx>mmR6)Hm#mhoAZ=YRLDZTN#l%JuzS0jz4PC;= -i6KkL4HL5D^=OZL$_`RO>q3)#x%!8r$;cIBdhz7=>+I}mW);?YO>nM^Y_LGEhRafM@d)x(T#zN-G!;{ -f6im^wlG$h__=e>9s#UFEc}~hr!-XRBR`RMExY0~JL|ervg6#g!KL4pAp1>?v$EmE?qwJyP>OsC6;RG3Hb`Qe$MU9vdbtg ->2ZeT$Ql#j_As*q~y51r&U>WE2GgU&uLY`R!zxVhbXc%r&N4rB1%d0JLKA`tXc6Db&FE+I#{07_od+V -hSlnfZdjBWIlTa&qY&Yo^@KpQ0OiYyN=X8Z@eQq7HWPx2WMU*mk^VfYAuTl}*=SPo03lj&I14FhC3-B -^ysZkz)D@l%PA+B_ctxubPcrP!u{y(O?f$vp~yvjY25a3&)h#l2xnJcx -sKqKU~d>`&0AMXN4@aoF0o{P$O#0;JD{bd^V^zFY`r*PY2KO)5j0uIr$uASP`f)lJZ=;lIQRZ21YgblECs;Dtv377e_;Il4SP`f@S@S4~wpiK7xLAx2au2OMo(ccX;-bQG>`<;|m714+o? -Q*OyZ7Xm?sf7QaVeNB&G6{C_3zI%;t11$uya_}7#TS&VBYX$@YZSENTbuM>YVn39iQ38e5#Ws^n>Ee8 -(`$AmN&22#m6zM7@YSM3>njb81Q*UOMSI6gP`Pb76e_y84sjnz}`W!qR22US?r~ih00h_jabb568ui0 -gOj@o@&ej*K?#!?4I?=O7o7;cVEe#cDmdqfbvA^md3aeO-LMoS&a)tm!?2#tW3~5+Nr5B`xux7%bU!MRtN`>75KE{BpA8O5$0Z=h5b_sPA)j!Z0TKxC2vEnpjX0nIO4%ZUN4JR-a9D -%_d!v+myuV%cJBu)@QxDLHXc&o5&XKsJvfoP#kiN^F5tLzQt6zUd`t?Dpbzpq^~W0dm34qg6J=(R$F|ZxT4CV%{X~0BC{|VRP+Er6m-Ip&28{$$ -o?)PX_>Gu>@#Ds}J?lC-|0i&=wtbJKQI_wbQzR*2Fy#WLMj1m=c5Pu|b+m09SA%i@kXu)dPmz35VzXc ->&!=l9cqK`*L77ARPV67lX1r^n{Ty@z@z<2qsQz1~A{OE=t<`7oub5l0E}L5VQ42+LI2U^W@q<+#xo= -^Esa{7Dl8lS7r$9YL@JM{eN1Vjpf^=wKr}4*N=;gHVvzb_$ALm$NWFArg&PgGnT5wJ99clM+Xl9kR)c)o(uYkGy=m{+6I<_$&@PPTF?p?Z*<|X|ZLSe~N)Wv-wxe -&!00!XeUGI%`&a(L;JR`JzjA>QHh=VK?)QG6Ie6D!dX6_H6&`ej{l@7B6p@R;YZRj@+r3}B7b@y#4_; -c4WHLqs`o25A5Zr@o3<1n -o6qh4&f0JdK^yZV;}C^>xMBD9$?Kv#>~F*z|QXIkK@8FC%zeIXIB0AVEl&2@i^G_r;dE^_F*z4C4G^{ -T?S0FJszDK{Q(xpY)i(@{llcY>dov@oD9*sdk&A@E)CP$I}DH9tPE54zI5bnLj(GOOBs(Pyd@ddG<+| -#D>E%qM?ac#Oea%|s`hdo)zf-L06trG4{?&_duoHv?dYf9A5zb}6i~Lj$Q9jx)VxZ$hkOuVf -nV7hdk;_4!sIWkTRxB&c$i+}6s8!CxuSYLE^JA53c+jCI&zrgReU+o|Dp2yvIsktfj{9@w> -2nv?`7%M3>5_Mf4Mrf>b2AU?WuQ3>SQ@IVRzN5j#I?Ep7bb+W|kNRZevx}cg9Pj97p6^&f$F4ME_vn5 -M6zz-`zpUaDa-?JH+Fvn~U`(da%!b&~rXQVBCEC^+epYdR=kCA@2p=fZ=1QY-O00;mWhh<2v5ufvS1^@u(5C8xr0001RX>c!Jc4cm4Z*nhiVPk7yXK8L{FLG -sZb!l>CZDnqBb1rasl~-AF+cpq>=dVENhma$N<95=Sj8fNOWG2-_S4lZX9hU=(kd2fg7y$CoB>%k&fT -Tc)$)tX;NbK&nCw7-dXn8{gTGN6c_|3PtKm}cqQji>N%bai|ZwQivZvlWs2MCwBW_yvIH9R3o3DJj>@hG09ajme_XPhx;%IPgth6SkNw3HxFuB0;5sU?Jj^Xuk~6URVTv|?gK -Ppn=4w99{`(BNFC%93p7FK2$k#;J`@x5KHRWdBEl^%3VPK@D=?yD;aES{5ehe${{vZX_@+_vI!?0q-6 -9FlRMTokQIQp0Q;T;(uJP(2ZO#MK_m~ptIL@lTLZESZiwl}3+0XAg -rd^ykj|DL-GK{hwpAJ03T2D9VcSCStW|+Cw1^kz*W;&Q3+6b{WbsEdJD;KxodwTiImBW`X$A!;70#ee -ewahvrSKhR@*;^wuj3_%O+5F)^+U0(Do%v(d?)D;FT2FaB1+ar>b#p91I{zjLBN51Dm8H2!iG6A9o5f~s~1aJ+JW| -MKMqGA9$_LwLS45?O`Dv&nagZ7qs7Mcn`lXTDzU4df(COB{6B!vKFP?~B0w~iE_ -+QMSZ+=lP<5#nJ5|5%Z_FJGy{3;r~Z-Mr1*$>0Dc=MSG_nw)5qprG^dm7E&TS>J+dgEv@_<_E3J%1k= -EmM8HPN247u;d>xj1zhC5*LC%I5Up#GSy)qqFDo(F5(fG(q_Z+>Wj70s;z~@=yM-}U=#d3tWXfCyLhC -BhgaauRgRGe5cFd?L^5p6fcB3bJ+gvW+p;$mybeD2(Kbofcm9{^i=Qt+o7H`Llvz~I2W6&pgC>ixuSj -QPUz_Rcp11&y8zoRsFan462}~3)s)bMEMVv&-xiYQoNHr0T4!8#PFxf>inw@2>P$#_B#WM{0f_DYfjq -l;pP30_0;y=&FNj!!20lXiy^Sa#}+-Y^4y*>0@_3RD-x(jOU3jlGbcCcF_+PP;BJa+YwtmJOkEzDrHh -g|4rVney6Fxxvr&}e}92=fkRl^X66k}I~W8#UrpH-)!$7;7}})hu+TbA>3uRFJ-zj93X;uW(*GJchMx -Lpl@KTAHjGs~(Qfa(*@+q7g39omw;njO!Z$dm%@HZQwy0Enp8UVgD-u_l~N=uJ@-knE4y8K^EPxJCrk -p3jgH|mZ#`Uzm^10APQO;z6MnIRhww0f{?*3+CkF`ur>q2bVmxkO{v3Mv-qB(w*U_NC)*mBdOJGpbFh -TiK#)+di?4n?HF3$w8g^V-Qki9*AjLYc-OWra>w?fm@`qukkpj1x2D>5)^#*4es@H?nN)x@V?y -auMXR@b3)k5ETcF@q>@!?Vy)Y~OdYaD|3eQ-Y?|HQw3aNST95bnXL{o1qqf`kQI4fTJxNl!1;zR<9%( -1kk8CrVU5wnVztXRsD@J=2!u8JvSuv-^h4gICM=9T~tX;XB6L$%X`_k_&#t39Y+YGw7CR;Eq1?iv(Rebuap>Lv+L7i}F~I(GS3t{#+-1ZS%X<{^9sd_Mkq+15 -2|Goe~;JOLj>B=;;3-?xjmQP6t&9UWl4*JbWQ>MM22&gTdWZS8C%`hZL%zlZhg5X%VJaGbzLddJC$`rK;M5*O9KQH0000802qg5NPIg&S%PT67QC(a!x8vC3Ph$ -T5t%Fvl`BD2SZVe<^1;RZak)e!BCXqTz0czl`U}wXfzuALO0MlOuk#>D!I%HnZVE4S`AYn4~!!C~?|Jsu~boKdsS-+gJ@q{%xe>ETzWU%Ywy?u{9+q-S -+iH5xWxP|H=N@}{b{0PnJ@ldM{<3fahHCL56#Dm}QYt7UR=ak*~RwY<1U^5v?kn?y`iRjeC%p}&ulS$ ->@(V&i00=Vb#O%B(>|4_uGT{z~tvs!;Yp4pa6X)bc=&oL(SGqOs$u2B6O`n(a!eak9yq#l;d(%?hEE> -2a+oWP2EbjgznBTx47KbtP&Aqa0Az}1QT+)GfoAFJ&%gK|crZ??>EC76i~t= -1@)^E_WoT-VHPB^I&FJ||Uc!38T8grz3UF?+Sx -D@h0B$8nBFX!v3Q%;wG^zlsmWlia=!<(}UCQ4UYizz3=Bv6|ffm?0++p}y0xC+XeU?5z5qdwh-4lTm+ -bm#Wgp(+WY6B{Fx+VT-8QD%JhATrSRk8t%)r;v^Ghh$G?_~4qW4nVVvI^ugFDR^lA0H1=(7*sk4=_4tIh -Sz2r-0DV8-s>T=XJGS4F`7mgHeihe>EJ9`x_gxA8X5oK1|*LK-pDjo|!NfO=Ou|HJqx!6xbV>32(=i3 -KePBdO_pPlUKRWWorKz|61N=}oCx(`E8k)bN@`ji%6t^x&Gpn~%bBfOU5=^$7xmL0;0v*BK!y&*v4mBhtm*k -0_oc_vby2AzegQh!a`lyelbJ;lKU`e~`aym2S!eRXs8{RX=x -_c?1{ArrJFQ``^D@7T0_xa(+eB~8Z2wU>Df8U9Wr5r1k)=zLmbz!h=Y25vWOssi)(c{X_v(S^7>!@%X -vXXkwc!G3JwPXx=<)O*=)6U0(?Yqg7kSwFj>sz3u3pQJ9?d}?Ey08Wf -eG^lc2(E7_bCcm1JX6h1{>YZ3)dLN66F>)VGJB?zQ~Oq>q*j+lufX1`KsN?1(J{EVs_EmkEmSv>%dsj -S93nFadLEYwHb9Cgu~<%OwGFP<9Z#L1^L-X69fTOPO1hqNdz2Q2Gltch3_#CRnc}O*Se(ude=aR4CMx -Z0?W=LuxROsswBwq9XO*vFzL8_b~@V6kaG|q+1aQ>$fwsfVBS=$RxWqK}OD<$~kPY%XybxNZj~@S}8@b>2KnDFdCh=)ka4kVAZdsm93c;E(!l_X|cz?TG -NfqxZEUxoM}lMThRlm5IKlsnlczbBP2CJ7Hz~^0~`ywxG)PvLvY{e?Z|5$Od7uW=A~8dC&S6)_R-|_<>dAR{=YpLwXFJH9eI5WUMC*f -XFrbr`@I^SA7MBGq**$K7RjI>%m=N=wn-j4$rxwhI_ZrQnQZV5{=Qb2b*a|4;Ff(hMlmp+>IG+ZOSmM -P1#qNVi3}ARNcb{;npH(r(#xf&uP|t$y@+uyJdtEYrAXw!eAc|j<88Xslwcp4w`oNJgf;uG@_nF7&Nj&x%GP3aF6aeT)+bl -8}#Kp2kr9J!^S;%=wu%_&4Npw|^duXS7 -uy-io}@UovM2dtUF(Anf)%NqYYxeW|5e4j8S(QAl@uVSA4cerzzVdk^hCavNo&z%e&7%HhUu|gLLZK- -zLz&VNlgS{y>zY&=|ykHElNse1}x2@IyLM>=#h3ixlUElLiA0Qz=43`wlrFPvhIcSa;Snm$ZDK>HbV$ -25y-w$lLfSG2=QVMie2(Ku|AA!{3U>q!5Xy__S+Uuh -<^er+->Bt8bEI3V6xtNgtda&1uU%!eB`r_>P>3^L+c{F;(2bHUe<#z2yH-$d$Q~m4ch+3PyM+|!SC^1 -|4-aa(W(~I}%c^6xPDQ>5?*=?QOiej~ZFR~H001*FO=W^5C6yF7rrqZR>C`zsPd@9xKoDg(;YAAM#B- -hvsvEMDAeOr&rBaKUU4uDLA&;A!wqS0b&j6(#BE`V3Bal=m>-@7@2-9k0j$zI@eN*3#|6NdZ%$Sna|i -YuufS1UXKB_i54CZp!LnI9nJ&$Q*bL&EkBD!zYmCZ7Ie*-(RWPeustut&b}a~pm7K7D^gO3)EcZOca7 -paDwnlYF+e$S57+`?~5=lf_{yp?g`1ya*LlKyF3-=D`zP|vy2Io$vv|B?&3#VvoPAY*G#c?j`eCZ=hyC -D@{#=1nc?tsn3FtS?B@jTyDX*dru!n0(PXBQE!15;C~!$*)BL{to&q)4X(N@=qq@txhJmJr_I|pDP`P -_<8a?eU9H45YTET_;@^l|BuYho*!x8aO}w>lana@>nPeVx!idb%111@7~(dvDU{k+7SD`^a*Hj_@}(z?c4G{{ -!g+C6nCQ^mnyQli?pbVS6N)s_-OY>*DpzgKzbpR+7-6`Z`pkGnLJHej+g3s3UB?qWHeiR9)5`UFjKld -4A4jl%uUaDcNMjP6YGnvdTX2s0Rs;t3xoFlq%3hz@*qTl!jbboS!uvva2N7!_a7X3VKTT(wX%`s{}+$ -zR5dypFPkw0(laJsNaQ+YKX!IZ)q>hF$qZDs`q1>p$uA*I3wtb@xy!`^MNw2uPHyW28^#(0QeDRyfL8 -=galPBM{If1rnNd1v&-M;KSrpOAnnqs4hcbTqKP6XqPd%Z}XBYHvY`%m9Ey)BF{zv4)2ha^o=Dv9f_I -EDY~wWNpk~Ijig+umdTo~*$KJ|2H^50fH%XaI3^vNjg%#L*1>Mmgc-PGN9SWsvPW1R82b4+1vbBx%j# -NR%L6-;7f3YNe)rCh)5yk%_n2@1&!7GSCLF-qV>v@AG&Nt@(WoJL4KHOts -BAzaB$D=x?TSLQ4i*WaoVrwW0RmI2Hhuuv&jdUh;>TcrqS8$;@3iHqI*!Mk5#sl)u`hc2FSaQ1)N$}k -ln@^Rdj&RUDu=s>|>tBY*eG=Ibf%6V4+kEcWWu@@MptZxBs=gw9;`yTbcA-7#=U(j=Q$S=PvnzzEq}( -N1pi^;%`uK21L%S?giZV}*F>+q)m@%0m%G&WvG&JAv>9mp3S%r#7-hgpU)+IVZcux;V&^e73oC|c(9@}8NYOV1wGlqFTp06Fi%?M^eg$zzD@vro*(IxCK3CXP?_UrkpP8LVxubv$b-FDX_oYSp< -d!x4s|5nrO6HNU$8j`(S*VSb}%S~reTE9pn}%>w03*TXh4&dR80C@oz~UW8X<=}gPNpuEeUWvtptJbT -DA+pl)<_uxVYxnilpdhp}WWIcz&S1)WLcVwRZY2Z#P`(Zs_W!F-zi$-5Q#P6sGY|h25w(d7b`AXZOBp -!mcEQ!7d%cveJNkRjs;Bn!_F;)*m4M)2|S*D-4aMq4wRz<(IGv@)bLSBN4Rp;|XW9ETRG*QfR{e_4zc -DFGPlfOzG*bPIjm3NO&1u!j&lmd!neA?vB4e%iyry7pN1Yu-Iz_5BKI6Yw-yycTod(Qx#oMAY$eRXzx -K6-K9izGdwF6^c4V-PtlmEE1dGR!No3O|8RD$J?`yqL5*_&(yMGCA$_tNbs0-P8+A4wjL -31ZZO^&?@r^#4?`EWLJa{+z^mS@FKAzXAJ%EIJPmfT}JmDQmuu+ub7J5f4!t0JKv#e1-hXMrIJ*F8k! -m^p|QE){AO+nsVf4V7D-3FQdy~WFn_-j&E&4P|?^puS<+84R!kdOtZ{?M^Hm7{uZECjq&gH`8 -*FBMH8hE6J6phRbuC6+$Pm`f{g)T0vqrS8qR3u=)@Ge28&{QYS^t&%OOBHrzO#fVz%5DY%*Hi=0Mm!?}@#YnNm?_( -k=K!E~l`t7wNWa#)}(gos^ZYLk_K;s@LbhoK@U@(cH{Rsx^IKouZuPt`lJwY`1LSMnqC()$G3?^TVI~ -$^t4v(_P|DX#FD;m_8>rVZbU7gUz37K$x!UAo3%T^a^&jrRip`-TEBmBzZ+bLD0f&y)x#rT93+WyUm` -lz3%;Li8j4>}goV?NMF@9mh|%0Yh5`v>&wl|L+xVv4xNLwm`xni+4kjq34;mQczbRDqrM+#Y{t4D^Y^ -dZ(s#6&|>EHB4IH&z0WcEzsa+;3ATbHfF$rNM*|cpz7RRPYvm+tTwKg6e}^~hxeZLx^Olx(XL((_my6 -(`)lN{4XSBV5lH91bS))TgO`UkJDD0@UC8Ua#AJg8%;Ho=574Nj)I0HOfG||tJ&e0lzw?@QJa2>B0b6 -3Mzo48;nRfVsX(~PPo^=yVF*zNRysLWLZN_a9mNR@CsOsB!Gl(~rQmF8{BIYr9U{aPgge~26&)MS}ciq{tM@&$gnO^T|z(hYc6q9d{N# -~zBUJ|2(Nrp?@RIciEwmq_YXMj1F=4u}=B3q#!cbexw&%w5$dm={OQOLr3=`|2A@Q#JC1PCT>J+B1Cs -Dk&Sww!g}65|L6~u;Xj!e7-E_wi)uqG0!VCZc|!nU+f1R&_~h6eIteAmD<+At|4xy?{gUf -Hpvniszr|7qD_z>53fD{9eM*M~x5a|1c!x$Wv&3Vkg1E^{`9<$MlCO1^5gHChdQ`RuXt9^8W^1nlwkD -uMa;h02ijCMRU(vhLQ_T8`t6sASXSlSOB_?zcF)`OFC!k64RZ!O) -{Q;7Ip_>&YB(;=Yxoy3c+tZk;K@97>>F`h*|FmuBHKGr#(C9#wML3Z4(U(_5z&y^6npc`5g(5=rg{(zLx!1rFS5YH(MELE5pp-9c5t>OAIV_!h8XE -cNW^_a=*n<4=r*_Q3~TU69BuGofm69|puH)u>;3I7Mq*D?mfO3V&HaA!W9Z9{LrAlBPQWyTXj9@6aWAK2mly|Wk?S1#-U*i001N{001BW003}la4%nWWo~3|axZXUV{2h&X>MmP -c4cyNX>V>WaCzlgZFAc;68^4Vfzmef$X){_rM -@6L{YKAD`pe*gaH-AORGB<7X?+*>k7MZAz{y@Yv&X3a^%h<~34FlKT^Y_-f~Y~)Gh4gD!K|AMt^`kSj -H&V$R*AjEp9oC%hHcw-ob2WEjF;J>Fd#=e_pH?k0xO29%{G3N--5|%22%s{0@EDn-9uBwAyt%Sb{HT? -~GFgw*I@NDGB;QUe+DKW<~*=)_)S;f3!!4?&#N2lVOZ^q3quaPrv_bl>ss`b55l0H^?D@9`k9+!8CEX -nt6*Y&1)YOELx2CQah_V7cTFXfTeN_Q;%8j2axMw8nZ`&D6Yj>NLUGf`csWlk2K(QZ{v^_6;V(a9Txf -LqcS7Rxaid3-XtimR*1WLU|37WOovK^}@G*YZAW&&z8&Ya|r;yFb~?qrDt!Ib@=d$m7K{jYWl72ci-$ -gz~w{&%*Vv!&d(OIoKX{+0H+Uv@d&XKZ|K!cDNRwMc6Mo!aC{}O|GYbQ&yVQOk7DmDwz{icu?ri&wh~{GHiZv}Gg8?2VOfdSaPXZkoi5+~?rNwO -&MVCc>FoC8i9H=#GHJqhRL=2^iDU+F)_e}*rE2*>Lpu7@;rNaFM4(8}_Csa1)?@V;53TuuQEwim3=VG -3J2n!m$`533CfPE;W`&C99@jy-!8ZkYd{IIkXMfCFiHB2+sHe5hV7!zscln^LvlJ0lvif9s`7= -86kjx@81JO{At0!gwa5=Ab!==u(GNXN~0%)=N{8Js>(d8n`!jnX@|X89fhK~tcOH8;tAoPn@gCDlLDD -Jhyp)&k2L_{2{p$@(b#4gVlCBRf!8UqF+!HTJbJW{at25oNqBydji6O -Nz4cEm3dN4icEm4Ydd8bL*Z;d}c6NfVg;bq_>A3gEB&9usdc$;`3r=2}8LDi>l#Tk?0RB>+dpk*P6mV -G|)|GeWO75=_cvZEAvcZW>v3w^td&0l=x42KvMan9+%mLTap#%}H7C$yP^#nBF6U01`8Xcf0_Yti?dz -RjM*L$*6w_?VTrB=n9q2f=i=1z6RRlQlPw^BSb5;)QPMjZ)>zxr>a8{AhZ0k`dKC7RFyU3QG;PoN?pf -u!nJue)v>-e((neOTH><5!`5v@hiG$kEA=q+Pg@R27!HKDfa601UD%CrEf)$dMOJ7`C03~t%Fn$CmMb -8ei#eYmec-`dT1}`T;BV)9*l)B$zqF~RKd;e0DpfgbZFW!6TB8^wyM2^eRmLw8btxR6Gs(58+po;bghWoQyq+lQt=<6m}?tBok+MpisjyFW_AfYjJU2x>}e07qAFMM}Gff`xs^0%Ii?L -8j&!5zPW2iZBB!I4#3oq`3K78~yGD?{tu9iETCL6qB(04=uH2`YhJtkVgW-E#~l#lBnEm-V4Lr5Ek%v}wx<`_P -{UnBqZ)PoG{q@0)}^;U)tCnLNqopkHgBO>P1`bi)7Jd6mdFdo^^M=wg?M8MD45Z0CaCux*}T4>TKU$3 -R?nMh)6k{9~01j-|Q0TLUiXql5omR17$}dw&;W82X3p8!>SMIRQlR<)MbWdS -CUX2+X?{nX)k;fyY-?(eDj*mmv&+WLVQx~lN^~8%p(i3|%0rl7$nr;Xt(N7`cJIQFlv_8zuu_aizPcv -f56!VdJhPAwf{f6E$Hp}ULyOD-o5RrOhQxJf5l1d-;Jc=L>uqa~HD}MD!)z^8(WUB2fK2h%MBohdX&F -q8gf(HKH6zbIAs>>!UlNaV%_pVf6!QrQI8`(<(2qt0kC$ojwms}c_fN85@B!pU4GROn8MIF&0dZ3VRZ -63hns)-+b~tRM;YsK1fXHB;M+j_8K1PD5d2|?Vdu$vgeQcPm)HBgmO_ry$R~~8gw7`GMJx3mzLPl6)S -n9}){p{y~(*Tlv(FnGNfuHZ*ZGc}s&nSp~Q(3_(-kDT$N2zA5U;PD6*I=@qW^gyvUzFXcxAT~KTS)?b -9VrknTZlnmYfCwp7`EPEY8pM?zx1W}QKBeTB^kPhS$e_{u-R4?&~6q5gFQ*!wxLJQ`8`<-QrS+LUa|{ -z+p=vLOT1zS&4#^hu#1Lv<@MC2rrWjdfZPp6Vm~||U;dR2=l{#Tev$zt{l-e6ud$5_{3 -}P4SbT0Gxs+-bDMVYRH?kbrwB{*v(De@Dz90HCWU^7wgW-T&k?mkrp+x6OlO|A5&AEZnxs``Z~QfP3Z -~o{r2bl#0Hcq|+;O{-wpX#7pN~(0=5>c9xA2NI!YlF+~5sm$jf!&$Evt; -JsC+vVD?F3+{u7ge0U*(abf3;Ag(Le%WAyAu6?y;#TRja@NQ(t00h6l`^OP6C;1VX0n8+YFga*6$b?| -NR~?6f?bRR4{j?BxQo-U!;o5PGV)AnE!)f-k!L=q(xC{rMbYV6H5vzT&LCQ=yk-T!d4hW24*!IYptJPNk+9?ZiA{bGhI!zBR3N^HM=twyLv=*;YhSCiFl(J -xs~<;i3%+j5O)1hJ3%-qn3K~c-CdY2gJsugft$v78i_LnHqStfqK(%e?k3IT8Hh$n -mDsFEw|QzZjW(d!EaA-3bV{2am*=`F9NY^O=Kz)W<}#TcThK~>18hMRp1r8oJ%7$pVT%ka_d;SV={73 -S}@&0TZ1(ZHr7EUU$O5sFX$P8Y=K_!hN9-34`zn;)vlO1!awm8J1TT9SXKO%;mqaz9jh>JPAqQk5p!S -eu1Kp_1~-e;va`3g_5QkLY0;K(TC392wk0;No3<*@b<4Brn^w1;@E50tU6tE8-=|(Hl%aj1HF*2-#BL -1u7xA4Hd&`+kO6kWvv5jABo95Qp2LYW(#}RXSblSX)XUO)%YeH(x1*K(Pd3tpRx1OKNyDU+29hX;GLK -o!T%#*cqET=TdtO>|$PY8%hJera*1KXEBOiOd4 -;$y=Y5$PtjyJE+ZRhznu#X?#gZ}|gO9KQH0000802qg5NaJp{4a#x=0K29E03`qb0B~t=FJE?LZe(wA -FK}yTUvg!0Z*_8GWpgiIUukY>bYEXCaCzjtYkM2VktqC~zhVrnUkoI0bg{EZ=)hh@q8)Cm%aLR|S&9c -30z+~_a0Z?MNWqEEZ-497-PJukAmuoF_IVCAYY~{9>h9|5>bmz}FxY#Y&oA@yB58`Y-Zb+fdHVWjZ$I -+c+k5I7m2HwI%lf>W=gZ`*To%c^uGV>36^rDiTwjzGJYO;BaZ)#~^x_=j`yui#nfg+I4-Eygq!LeD>%GpetV#4NMwfwZLmz#Al!4}>}*p3Rn|-UaCLdUS(i)OQGHodZm3tMwSC0oQ8)HYpp7$FJo9~CFPA{YN -I$Mg*{+x6srg=9<>eBHt!WDj23Fjg&bs)pzG?Cm<5h-dkcz9oe*%ELb+bL#OW-3<@zGxKVP34($r1f@ -*fe#+ZSEznx3Fqe@>%u-#(D)LU!IoBa=q0c;GZOK8Ip{kTxHYiqFUh25xkTiI?porF}td-fx4vCrdh( -&$FPwA)IA{46%q)N6YM$I0;gNtLP-n6cv^pe1!I`C13dm`klDum+#SXMouxlFFDu&?4$*QSO^V8)PfWmf2zIl2Wh5I$^xMD9HHIPY#D6HEl{%IaFi! -oSkY3wb~#~N|-^tvvAEi~JQGR(z>P_(MoMYS%0J>Za*#Z}AOc2O+zEixZLMKQksZq;73*HNivwPh(-nn0ZqU^u$I|XJ_oj608`If_w&^LyjK85 -^t;|uK-tT>D!Ozl8=`NCkWBiCk+~(3&s4U?beaw|W!e@7E5p$p+O}{cm1iZq0nMTNatV75reNtjev&m#=~#JZ13T@9@06xq#V|N -I?Qh1^;bfv(UE44H{4yvFk_YJTkoH7SC-u3lSV1deC)d`!GYAzAe_96`c~_GrSii@Tass{TuLi7!&7vd6Cl7aRNOo3)M) -Cf*$b*f%2%mZRYmtFB>`ylLzRHzWCRSe)4M%zlaXM`0-yd{K>C9d>UtW_~-u`?0@-d5C1%l{quhf>3{ -iak3LTN)xQS$pZwa$uX>PQtU(mIvU9jBwq3ra26hY$PBZ{`yXi7SWWT`mtM!x)1vriH7m#|>W|+QTJV -^eL9OwH#o!}quvwtM}qaliR+4qNU{`l(c;Yf`D#O^u5_`5gx;i@Ty2gz_g9Cv=dE{mJ!vwi#(4zzr^A -3kd4(Ki;wHU9n|>i3^Bz}cStN?0I)W@^rS(pl3sSY$li@d;{c8a)|Fg`MX3q2?2VgA~7x>Bmv0DbN}o -d-R6s2>8!18+O6+0>Qysft(xxgfAZ-JU+pZ4j(;!^4aIV`S;)c?)N>A?m_zn&92&96-_y3WuqzEx{AK -AEsN!XI>~+lk!qR#t~;GBwfk=m?++g!ggvj#y`-pG);U0i$(O5({NZV_&L4uhyeJo_+M~F0TEJ+Fga~ -0T2AiG?{WY$a3xAnAE5@dZNpgJByY6v9$(HpEY~fLa0%SEZ;33cyMVS<)9%;zaZweGikF}c3 -KuWDq;C~^hN0fqy;ngcnA2oxj@Sh&lR-``s;Wfs0GRGzU;34_bsNfRlQ2PEALHY-GDTULflV90~!~p1 -O=!Uz}1=(G|&3X5*@*eLfbv!NAlS*`|8JA2JrP@5c -@b1AM{I%Y&%?$Z4S;%T*3u}sYvs@$Cf&b3x@_cG+Rc+>xWR|?cA+Ra1IM3%>AS8fa!WaRMfcdfw!Ic# --leTP8b!E96d#{QcE1k2L4Z1>8FE(>BqQMH9^D2b*yX;Yx9979x-J+qZMPnHBV1R*|oHIgkESst-U>0 -rOY(Y1IQ3k*qFG55ZtqG1rW9gF9GBM|Lo3JHw3Gj|OsM{KJ=M|l$v~W1agf$?fRfJ08176Mhj*O{!2< -von?$D*s0jfXh9cVtBH{~gV1b$W(p#Kg01IpK*{AVzq`Q@^>E|!zW?unv5&6y(XZQbyx9&j7SzO4e)k -v;mwk&bSj$@7uc{X1~KC0e`<`c9^kMEdr?`XfM|?<3?|8dz%veq@7pF>uX{Kx`b{u8*qgd|57}`3`{o -)UVMtF+~d}3=D>cg3-@B+Nr4X{4|B9Xhj_-_wQfc$k0W{Pe<8ig~wqEPw0VyXdW%f`8q?63ld)o4abQ -<9F3$u6dw6KP-eLU`pRRX{@_pB=boXtO8`Zuq3RTc&^A@1M$e8fPY}ZKaN43Jbok)xM-Cnu+fm2(|1Ef%J_ -s%OcIe4e~|oBT!0VD$!g-KpX3?8>-HsKl>x;a(DUETxb9AWT45`Zx`=7S(zhUxel7ixv{jDzHtm8*?3Z0G$>=yvvHHAb@K#LTFCs2$-{oJ58cYylN6WO9-;PUf=p69P)}7bfuKzaNNg@7!@#J6!XRT^tE~ -Xriv?QPgUuyv;$ZY>U=jqw$9MTtA#`e>=!FBN7Tzcp1!!;s+(f9%tqu8o^U -d`6(c5(z27I{RV4~;e-q)+udaGd+_=DoJ -4CUo!xzw!yivIeX_r3nVL7%VcJLU^G3WZBpB0x+@+o^lMz6g3OSv_1eAa<3UN#_)C%5us? -I!1YJ_$HP--e3LheK(?x@r8$J)MAC@)+^7@qD+u2-km(eS_<)k>^Z~v^uVg4Uufu)Ph7d&7fDuoRb9ep>uvig7bb)-DHsr -^(~7_A^Rht_uL3@I!!0&y3_QVEzNBe^oRR1cPgUP)&e!Sv -`xMp&;-7NeAbzJPEdW0j-;J5-^i=zB%yQ%B=u9_a0k+yFwlFqh$hAa|fLY%_lc0t3jQic>%YfvvXt&~ -=P+iA8S0j^YU&{4^~J{67yy|IBXR$t$>*J9CU3Fs9Z(dk}6ke@fB4HbiR6S7;0%(PS8o1 -y4uTKz(SVU!RptaVonHLFvkEVlA2X6PtJ9Eb5}IhHGQLPX;8(CTB&C5mf_Yz&J0ji;4o0kqE7{GzB3K -BvSM`vB~xIU#Bmg{^zSV$fR7r*YUw;CwoTVgh!w4DTx!lKiN|vCwzar=gFM#%cDIlbixn&kJ)+_lbBN -VH^njPv2L|tjo(dXVVfDpK3^90ZMM+N6yzDNmG=`FtI*Q{U(n;s%K|veZFKQ$c2zGn%K`&$_|55T#v& -eW#K=&{?_}*d$R5Wg?mAEIGqn2&TPw%PWI=&pvsp)Qp3P`3#AjtJ9!rm`?y;IA8lS^KI!mT`k}~914G -1qCQzne7Gb$MqN4+m(V2DF?IG7RoF(j&|v6hv(yGB_FG8DGh{*m{?*?YMzaIRXQ=h4whlDo;Ae6x5>C8Y5-8Yd#jxBY|jab$9EN5?*v)7+eE62s=!f5yr%m2 -$A3m#_;%(7fm!Oc!Gm#K7dS~c?flee)Ib%CjN{!-i2p4%vNH;O=q{HQl*95jFB7xenlv=F`C;V<^-;^ -GsZ$qcU*JN8?%Or+q7WRPld+qf~s27eykSER|%8O~d`9{m(_?%%j9Qjm5jp?-4#F97ji&R3OeHYB%hp -#ctzQ&@dWD`_vx6N)i1P%xsN!8zU&+OBW&poo!)JihYlz68w9Jt3-PZ2aXfe@ULPf -q%JWXu|JX3>^nSYc|{Ibiw=_RK`knJD~5n%x8 -KBdSIU>>NpmghQq!e(t?v%ZLP3&NjM6wV$Kwsj)}4g`tEkNE*;suZOK5515cG5_mr)NvI!diGrcUL&C -tXapO3gXasdozG7)6GApf2IFKf9Ymv42F44q$Ua@{f7%Yv5dnm0b^_4)Xq@OD9>Wiv4H(@CAK|A!3<9 -B`R|REt_HUt=3RG1j6vk%421M}%N$C4>8Nga!H1+2EB9WXev0(?6I-j*hogrP9P!QEC#xNw#O$48#!XaialR#q|6+G6JXaj-TC$!YM|f -N+n$+7puwOK9t6Q{dVbrSJDEk(c0^jVwbtvtuM5CqT}ZnPW^ZiJAE3J@TlAY(+wJs`A4Y*h|s+>;9|4&7bcf^7u8$cwzv2dg1H1)>nS&R -AMogWgZhTwptY3xtq(BchsyP-6QHE>6@kQc+MzUN>RAK-}#XddC{yj^c!d?#E%g-qPgibYbEaHX_z+f -nri9F-oC>fZ!v!Q5wJ!d{LNh15RgERfI5DvfJ6+jk_QzNf*O~q;Zc+?elXuR&d!KZB|{FhhSzB*34AD -%>D!SEJJcKj`E1+RlIp@+whvd;!c+wXfNC=KOzy-+?M#APtO2mFP?T-K6Qaakr9g#;V&Qp64?NxpMXa -?UN>I;rF;|6A?C7-YP39HKJkQMFLCMJvYJ2bhlI#((j%?u(y|qV|@~@I{=m+y;09kDo((5WB<)4Tl0w -SG4VkHP8v2aP1Yw-llzg_=?TBp{*6W|5s5M0x*eas(Y -E5uNvYQ;7|fy+*h?{JX9zpSO8A=*C?U>x>g4po#Ua84JRCcqs<4DK1X~nZgxN*W6F5sR%9H};qMDpN4 -58cT-7|fUWjoEh5++SEFK&@k~*-osZX0j@)C}PcQ9Ud-wh6_^gv?b1AV~Qr|}w*^J7OzWAQO67U_;FU*(Nnzjt4|Ra(GAt@Lwq;d-p$RZ47jkbl!(kASmJN)>4P<_iqw -gM$HWVo8t8%@@Fnf&yJAaDYip>A0jVuk|(}uBpl_cr1yu`X1#^A=VMzh%{-j@WEMSA;IkZC*ODkVZ6t -`3Xp(E#go*Lyyh*bBew!weMTUGi6j~F2 -h;;N+i0O_Ih8#JEpPEY+@J3=}IM-Evu^IkW&yNRDEH-Mbi0}41yCgj*$*qAg9FXC)+7z$GC(S`|y;WL>|n=?IRGiN -@uzf!;`PIxpA1W`iWdDcfm4BL;r1#ELezOn9_eukOM6yS9o&hW-H=Vj!xrrzHEIY+rM8oSY?Y31a7P` -ACCsQ!AZ=Q+H~D9c1w*ZRDxWEsi}#FZKu=FoE$ajiC)xvM7?#oO~}P!YQW(6K#^{V*U(bq@re#8`w@Tc24 -SZb0iL>wjcIY)dU=fp%a4F774@DfGabc;BH@*+OST;vfqg0puQqor7DTF7WlKTih94Nump^@vBbcXtN -5XKntWG0UWKq`P1{Gl*vOtv6RIj#V&_IE#Ga&QB@xck>O8V-fO(aFvrDOU*WaCrB3iEVK-@~qFGXonP -3h4T0ZDQL!eM(h7U^eTfO7K+2WE6wLkMfOMP6ETmf#cLbn%4~w!m8$u#q(s3o$c6!zHW(Oz$CG(Q!=B -6*oOs_FK==uO%u33#J1;Zq)LCm|U7AkWD$Jap}N?_{GW7YI(=3^&J)l%-B)_fA}5V)GG`a70$3(W0SKa>F%dbeJ4V9TYr(uV+}z1+DjNvod3&cpw`x><{T{r -0d-8oO~fj%bJIy!%vChlWQJrh^)HFMYcz3Q68HHvaHX!TMh|4U={?j*`j?v`cQ^2S@(MfXPvZFW{>-n7Nk&JgB?NXkxmGl%jHI3 -QT()TRTF=2tkU@Ic|n5!QkuU%@mq(50JE(tu!J?Wo2!e*6-B$cCX5J8xxg1(;Cj>>{Ltc|fE?*RW?&6Q4cD+6+hT{39_@_SK6n=CRw}~fcZ}(9dr1cg0 -vb)SDaw-Q^SNn?%N0?iV8l!)=;ZRlMwIjMUC!&0*qy!BmN?@^up|8!~6e_fUBxJ#$fU3 -Jq9`D$W>hK6k!J$ax1RW8KCd@TK3@V!S1m&HvDQ7lek*Ooji!dZLvn`7RO|_nvss=dA%RaNVGIX-3tU -NGA*}*D}LJ=TnVkQDoE6c9jt)q@tz+OI-qP|k#2`t*0ybP)vkID-4lAmdUQ}neSw!XZavfzG`QzS$ny -pJ-anONzN4u?L%W?CFdeFZDQ*vb#IPLcv)dgD|GpRE}+0Fl0_mS|z0nI*#ooOuEa^+-b+laA%gTP9sW -=K_MDzd5C|E6J>kAkEuF4rxd8(Tr#F6pNi>>;s?`Oa#jS_Rz^3okbg%VAYk&!C?|w_q!qUulr>S%0_jbeAn^d)toY=@x_8=?>u@t6lYgpz8yz1U>_0vUK-Cl?i0)-_&PBWf?y)B}-Xmda|4 -W}ko))Bet9I+!RBb5&M-YYzuU1I>^_fq-wHcbf%ls_ja{Yw@wP}m9&5}d{PN77&RFghGMO(T}^Up$rQ -G)nHUviPjW~Vx3FcNisa*0iir|{?zbh*k1D6pU6Zd+gMQ%pv{X@8^;c?Xc{Z$9d%oY;_EJG>e(k^6{` -z4>+yQ{@ov1?&&3#-)eTFM;bH#fX9{8n>Rvz>EbYt_$Bw$k2x5O0?neK%)Hx?I#iQ@QQ&D#9gg9J!3* -Vq=7|83YK&@Q;&jvW8|LbQ>+%T0Z -GQBTf$MS7dEqu|Yvz>?@xYJvLe{-$T}fZ>IIYcHSpJcVA80!bj$RVjjpd2>6eZwyPGGh2%=x*hUmAk! -4AdsMM+Yn7_bG`>9h*u39M?1Kue-Q<8G_a)!O?24s?!CxK(kdX7N+lm1EW@0h-4u4AR;9Dc|#BaaJL9vt{s@?e -amiV`}JgA{i=!_GQsmMHx%%BGumJPHU34`BiNU|F8FQO`Dh&?K&h122yKafhJcAsn+?nN2_y7_~ioMt -TN?snq#mXeK`Lt)&{Dchl%Z3li_71xB`|h+vtMGef6o%TX1HmL -|k)|l)UehOK;Q}TLNv5;>B`C0^7MHn9eC+3`lpguHhWR`wRgSV)jFml{+5+i=L;F%~u43wZ*y`i;bET -q4jnp;I4|C>sg@BzS*icJ9Vaeu5+W&wy3Wb3+t?L6IDh6Um?S|N|0GFuf#sEhB@@5QV_%G))pn=&W(M -<8RpFD$*Z&EY?-6_uppCui?wV?ytyf4C7o8f#>y`wD+ySvFz$-XN~I7XBX-PE8QOO3Rph1hw+g#}T4Z -_;z=UNTY%P%F$vzceKS!wiDUH#ha!vPRYTboQJ#Jz=a2I-w3~Hq%%}r&-l-RB84WcGd*cluQ^YDt&JH%1nJanq^+ -h9z~ckPCV-(_f%<;u617?CWroJ2h6#v~QD0mu?qKa=SCZe<(`zDK*tn#gSgEZrD#TC7P+T&T-;0@ztA -#fZG=^IdPAmhU5#>js%;K=g)5^_iXTuIpgi2NQLwyPtd=x@UY+@-+j&#RN)h;;J9s5*&2hV#by2rgN-;oCL4nhoxz@Y*S>omEY3QNw;|?pwq -#jjmhD;ZgMIfkW9>;X4C;B!Jk1fE9BSL@m}#RKHvtt_c{R=7ObUBnkR~x*U^SZkizNvRWgpoWN!Uo?K -5?;njyw}-|xM5h=#gTOTR~@xzNg2n*t{$7Ur@jR>^t2OP5Al(`5wdyVE98DnG+o&;nH!izXD2DH{UK$?4FcA-9)eaM$+u08UqKb?^Csc4;0Mkkun?iVRIMf%5mPm}YLjMN=Y -{y<%zj_(zrI7-oa7I*QTaM}s^Nz+x`w&f`ZlobV7jjU%y8^!TL>#>BGZB3p^;OwXtZcU74$d_P;*y89 -)meiCRrwh~G`@-w54C{8f6pXrAo{_v6M5KQ~j2Ul#y3VT*nR%nl^z-G0e7Gj0l;TrKcrm_W*JCN$@7A -aa8TrHnpD;I!M^!sw2o3_n)*0B2Sj;X}iUegz&^e!(2#rh}B<7IMBALyd*o&0X@ouGzH44VY>RCimKt -!X!U2P?ApQMJAu8X#tLPJ9K}&Lip#VQouSH28jVgxsCONBZy)-;k$7>(w7Z-pYU|)+-0_d|r -72_2=q0ol<4DErheJLwM(sDFAnJ)P?81{*_w{f`swbDZcb9(idmHVZP6n)!5F*z_SaJHM69Bl2e3 -LnP?0(UUfbT3Ut-Gqv6pSf`0Jfwsz5pvOgAOf@CzQA=_s?n=$#W>l+P3`(YE#H8>37k}~iO6rNJujy~ -jw`Poep5}0Q^gCLzo6>EWol8_iikK9T4b4rGeQOPfeF~dwrI(}sbozl2$ -vSwc?PH(ewKYUoXtkg|G8rJWaGt^u{}6*dW$+`;$Y38oa{6t(vEXjX_JT{Ru -9Ew(Des3e652HWhUf)YZ3n}ic5P~)*_v7IqHsG8wMTsJK#2`}XL*>5s49nb$b%bjCllF|{zKHEQ?&^c -YlSCMKOV4?`|5c-CB%(OX>Q;uD0g#4)e%ki`{0Ei2^qYiWT9+qgCG+h;SHR4 -JCUP5$%WSMjl$PGeRvBM-d4A6?>^Wlz}6nzRvP-S;fZ$BFThn)l%mD;h=qaP8Lj&sel&SE#gLw^Gxj! -QHtV^x21V*}sX;}X)MRE)NJH`Tb+N;l}sRd#XJwr!C9yE{1x-;cy3#I%9(3d4cXT4NiHrzQZsstd528WO!K|<@fll)n&;P1{$QokBR3=pJ90YUt^KkUFYA!7`5G{HLp@^#&0SfL -pZj!FANn8L;UYBi*qfjAeA-&S~6u^TohcC7uXp))w*@%2t&ie>VwG@KVBJQz=U1%0N^b8(`z;&h`NMT -|zU&M2E;sv&St93@faii?(A@3YR3&G|NSU0K@HN2^0B -RUT9`0$meK>_>(QHtLN?l0v^i-guH&DKdbTp-`3SBCM#=;4w?N;Tfo>wHMH5F-LAT0Nc^6r@O>tmzJ}F!q7WAY( -6$hjlO6Giu}8IcuXNl`}WVFg`t-m>Kc=du4!Mni|$Zf`m}u44Ln@CPVN=U0;gt7w>SzjdH3lQN$0P<> -FXp&oR{psLc{`ne2ceW4&GS>)_K<9j0Y6q11fx=6pl4Z8zga!U%5L0&(?gyX;YOTHwXS%VY5A9A -7NgrPKL2(H2Gdeo&86&k@y?{6d)ozPGw1eyr5X -BXRtF8HJ=a#;7ZF+e574iN&*c4a1@)#>pDNo;N&Skm>r}I+Z$~<#f`4{P$^t#H#Uym63Tuqp0|)m+O| -*q7ovNcN~UEDH074UoQ+ajJU{^*;A~bY}WKcR=G~gwIS7U4C~mz0}8ecX9`dD_TrV{O)1&<1&4lIfwL -3)*PEtqHr2YkD#9DQ5-VLWQqTn!eYDxXs9`>k)zSmHIJd-G!?g($$(%;)wwin0E+nTrRJMw2C -{|B1Bhj8fNaB*3jG9jSdUAYXRZp!nr%9m3@ga;-PGb3q76m{hwk>d{LZ_v7@Z -=6&W>@42-du&i_<&ui&hBkxyePPM@CU&#frFi>J_2!>@7_kfhIT785gU8k)pzlooExd47Y7mF_Z#f7j($0~2bovSvpJv -f4(!_H0eK^P@UCV>>fx6usOW{hTlBom&WbH96gzN8UID4d`OTCzPGi@Z9Q6y9J6r_}M< -SWRC4_1&Ldy`17rl+T_%`_rN4_qfiRl2{m)PNup@EaCD9N>;7}aJC2dCvlreV64`8r07wtVP+(;AUhB -rc|;PC#ITJoUEy6;+_l?Xq$+zfgn*hwL+P)Nt!^fxjzc$X<8fk)tr_3%3(rlHDA_THxkGJt -eX}j23Y1e=1i9^b-}N;o_dKB;6nbkb{Jhv?OC(P&3w7EP2TuO4xJ`GQtcEVud%OC4&v#BtPKsoSzR{E -%BOV>PZEVz9^~178K#!FJas6wo@Bqr~PM=txlp`r&Bg;5^%~LBl`qJ%$0alc!1>fvDC1`FPAJeo);Bb -?M=0G=Et?{KP>K4ZB_|hp4-Gtmjd~ha9jgooi}tuF$7a(nr{8o(EXkCV7R}&S`Ej^eH!^`V%+GQaO%ily&DwYYPzY~{0!9+4+Y*t!WXrgbF2n+LO1hTb|a~E?zz;@#o)1W9v0 -BuH5eL1yms!91c1sFb&Bkav$ej4>2vKCq(763OqwPZ6mi?#5o)`EjSi&u5SEOBbLv%rj>8p)4`C?QrZ -Sgx;SsRLR8G{QYc+pUX~v8S^N~susX|_nd;>^m%Ac@8VZKG%Y?l0x%dH%s`8p*T{U`}t-fYLMm~JQL> -20A>c#GuTXTSXojg?}c7X|FnW!vkm28!k~YLjGGt%vRcMkI4}0b`BzXz6@B9^PPnQ!_jXtYi1EBGeD? -ti}xIRnbr(UJ0a?y^P5&z+hmk?C=48Bg+|p;tHz8)k^2eancWr4#fdFo=;7N5F!xE)72py-e6p|1F(% -Zmqo0y_NObn(T_Gdk}gwYhCV>}UE__~{ePH7w$9#8kA8Ud?oSEf-2eHTH?MspUA5W9IEM7mz(A3Thh(^;7Vq+{nxT3|+Hj% -b9X?P2fdA_b@lMez!f?7j;#95#AJrzYg6Fkn7jC>kAvPm@oPC6hMnw?f*C316!KSIZDDE`mbJo`&YRs -P_id5$w^fl(q>9DlQyQl3wOc6`df}Ad->`e_kcXJvTc+z#^mW4DNWqF&N8ASH!WgKO+yoinBtQfFK-@ -s5*UIJ2dY_HuhgrR*j&{e7$obam@2RpErGl?>iR@+e+7qI+mlom@KB(b^20D>j4$x^`1OTnn#6lzJP$ -E`g#DzcVL%2emI;lLefgf0g8(niFQ8%SD4$+a!DHQxYi$7G+51*X_>$<%f;kbnU2 -Kb4ckZog5^FiNK&|i*g?|F=5({^_`0IXu{xCFMWhU$|Azu4BhnU4=CYld -cir`l2Ef*$gyU#D9;)*JH)T`MioEm@Dh_bhCyw3WU~>T=Z;}Z^?YOR+qLJng=fz3QFq_V6|f5b=o!_= -rp@WSt>IoTPXT`lQ%gi8jNMfV|qHXnYVJ*a?mB0F!p;?%|&?wd2*+qH}cOu8B -uqY$#_XK64WnpTxbVD|D+^%0cf=BX2zzfQ?PAwqsqLi!QLif2Y1OZzAhY}-KEWS9sz@`<*9W=&FKmi8 -3CP(5jd7Itg(|6y>U;gqJb=jzYx>#@IbD%VgIxak|l`UqrM(UnjcO9Je5vLFBVF|TXbqH&vij_8s2T( -9zBklg-`FT6g2gO!f#0F1ST$UOU`#fLg17+`|(!Z3@PGT&Ja?}qNKKVZnCEk9&z%Kk7#XSRv9M)R345 -uyiye0Ab&&4*lzliRlr*BRU`(%QeY^)nnYJtRHucE$~4*jtXg%5GKz}f^pp7L@n3wxG-Le28kYD%{Yg -4h?LYiL_s)K`2rmLeYt0_98|ZS*s}AJ&mT+bQ<0Jir0J%dKg`_yr7S3mt!`n8%E>7<+#mX^N-)|K8Q?n|9{=yB`~Q{i|MY18_u0vV; -TT_V7u?$UnTUA#*Uggtz57&6=rlj<=b`st#vr;c%b)m|>85b8OL}d*t7rf-DRJKigB@vR+iiYSfnHTE -an;%516rC6qIyH-~p5n!A$BbjVE1-1a14+7#bTfu}Z?Wbu) --HN|RXG+CQM!pKmiWf-AX(;Dr;Gf+9QE$p8G;mIQ}|RvO0P1he6BU0`co?LBR -GPWZ2GM{mM7i!+$*M^o>$Y+@xf;&8i2Hm^&08Uw7$wQXuQy^<)>I{8AFK`1FcOVGC#jTcbdNlz|T9bP -AuDL$xH-hgjB$HCSr-r9)<0DY>XwKu?>gOygq!LeE#I$J$aa+ePIYtSLV{%u5@nGQ{A;faY?`9ufsbf -3#Qz{Oo2wPOsK>?-);dZA8dW1cqlkRv+R3nN1PTNhRMmajM%78EehsC-z}8ikR2lTMk%*Y@}!c&ZqIR -W!uSE55-Ovnx1UJnt5m@=6?m-U-CRMQ0JlBWu3LJQ9cf+jnHQNO6=5xV_s8Uh)uX^+l@pJ{;Agn0Ks6 -}HB5?TXYYB+KQY^sjf%b=ncv?c*&@ag__zZDY)v+h+RK -i(NE}{H1HqScLgpnfsn-p+5U4w?gd$Xz+#nR6?yqCQA?(I9W|AX?0u84*TZs|GGHHv5gx -ul!vKXLmKwMS1w!Vm5Zj1_2BFzD2b;)F8A!rH{u6Aka1kedCF1e-h$>Ob2zn#Cu5>8_WQ( -S%QFrF^_E5CBXl$qp5E0f+c8r)r%#}EwjUZd~*EAb&o}s(5bSnFN-Ss_Vv>@9Tm#H`2^o|jo)+{(|kz -HQA{r#9LjySvU!Vk5Y?-C(ELQ9nK$9!TR`xJf(MIyy%B%4E$PB{vW4eXtHL5XL!=Gdk6f1>-HT$tleg -N*m&xZbuk_&AMNVn&4yFWIp$vNa*msQIk}E*g3l7Vs<0I=8U8r!i(!Z8GNpF2$K7Dc6AqG83TpDctS@ -gOu>Ifqq>h-YOzzt={X~@G)lm4Q3@*DautcR_$A=9)wb#{*$MQ(mNKCAipG>Ygjd5VT)xa!yJ}Y;N#RRg8m)fw7_-*ZAMRA9 -yr?WMc>NSH5IThLFDONjO9qkAdmRU -|zw`zL`U(*%5MS^&li$OnTh3FjpMV2?`El64si!JtVU2T?=$nhpu^QNBq^ELVisXQi-JVq_-6*U8tXgdzla+1%t`F9) -XVxD9qs5%#{fP=t1)d1IxDM&u!{lsyZ+b1&n1?~QIC}VXYO~7yoGK(cyTi6QwO4j$SvyG$Sl`h>lQol --h=z^Dvs5r;eOg+2rIjVy!R}!M}3oc`|i!r%Ws0{{+{{E>lAc^+=hH4CVeqd_^UhYsPZzP>S=JIl*aS -a-lj}Mr`aAq_AV^?Z_th7N596)90ZWt>g!^go^LS+JcbUGtYcQ+>$1bnOFQVmg*ps~|O$2h9n`QEt`~M`F^WCv ->?7E~E*YEzZxUp^+1RliMNeZ~WfwwEVFg5@CA2eHXYa=^sYq9ZskGY>GZMzt#l3$qmDdAMvH95pZa;C -1AthNQK$PGqh#Q9PrnlUF+3}JCoa*)yTp(d%E>fy6@U)S!aLTg3@*Lio1;^9{0f7F%Fkl{c)2=gZ7=# -p!)Z!97Iy1`}%|TesOYy0PTWX-e^qO(TIcveFkiHN{!TQ2sDU($B`KFS7!>MK)>JjpgR)~9Vf6m3?@qQ>RoedPz^i?iHUeQR7ieod_<~Ne3@$(;uhIuw<*c8wKN%Vc9VbDxXk7X{T+55%H-nJnamVH)Apojgv=XZ%NzLvRtRMl${B -B(EM3S%Meixa2H#>$WpNSaOPxAb2@jDqV_Oj*{^gm%lVL6%q31d1lMLRL+ssEHI&-_PGw(o`3GQ9iVTZX0|4!e9G+pBRI8Ki?J?J#j_4D5JHG{ -C^}ka?>!i5q98Y3e7^{%T7g>KujO)SI;%TRIwg%V1W@C|tNs66`nj^h_5UczAwpeoVaQiDi?^-}OoV9Z_kf}jmCB@S<;qPpi=la;!v_+BQZDl9Zz9z(rjpV!Mp7`AOYm3a)oqE4U*G -Mxioj^TD(M^FVvDfFFWev{1IFEw;H$QRk_R?lO7MK9`V+Fv;J}j5%B~(0ca!+}4G)KuAcj*KN1IcRD3(!0K;}A~hTT)j6wX3+&miy{T}0|Pj=EqdUO9ZuOUdeocE*^vn`->nDPkpRr^GDQe4QAt|+FP2kv#p>Bqnz18F4PLL=)J{BU~%VPAA6M69o{s1281sm8-I?XxP#6LHNKc8p5^QYJ2$sj*-f&Q+2J -3A$>AokU{*P8Mw%7%pJOtk^^U@JB9Dmd>L~e-OEAsaHOpzobCn_ipb=9KsaylQVaGSNdvxefU8xk*y6 -x2Z8?N9cZu7R?B9Jh$vhjCm`JJoo-*f{Fm45VJRT(;W=u8bir}78u8e>I{GNo57lHy1;>GtdS_1yLk9 -Eb*+a99CGE3`bGsn~{Gk2I-2ZNN|vVucq)sz^l5QC-k!vNppisBje=!n4!X*l{0`Y^aF?F^HmJx7^Bg -m?Vy7H@5>BD3bW#xOAUBT~Y+^%5ctv|90)Lc^(LIOx-iaj((c6+bn^JL!GcKj?O5H;KM2>eK^1161#L -+5*R)D#zbh8*s0CE+^DeV~O#nn4iX+0!yLeuEIbS%xpCLx80n23ducllaRS0F?QywvYn`Ya@oElR15L -8^ERvlr|}RcGk)iIX@qdwSmhOvDdK6{!o01)*|$P)eKZR8F>^#wzYS?g)Ume=QKK*RNVh|8z4K}ol!+%@GPd7UC_b_MAO8!T7JNU$w^|KP--7 -FR!h6RT3lSU4{jFucARYKzY7)YPJM_-Y2mmZA0+7`a;)q>*7WlD_rH4tyYvi;H5k!$p^bO=GJv8?9Q- -@Gy2O7{3%P?f`^K22rTA+HmBEfvJXh0-rAd1O5vX*PlV=#LFVcZG};T<+z)S#-W^Wge)szIt2ggrZ0_h4v -$o-&#+*MK6$Qd2_}xdc&u*( -II{r_Qz>|m+HpjMhhQCTV;^q4S);l^#TY3p`4TO*t8|e4n^4IBZ}n^@AstwFBY)h`j>?Lr3_0rhacnn -yl{FfC)qk`h7Qn+CC#j-3`)2^mEQ~I?YTkEa_mL3|%Vi3?^<6PcVJ)6`Y_4+9mo42};6TNb^?v!9o6k -L00q(SzBdaoHOZiwX(mXfM7^t`dVIf&@5IuPD14>$J -4C2StM-!af)~(I=ChEMy-3>7mA&`9qeFO=EB@(I!$6WlBiLDXXmNeSzx*BL7G)K4@9IuSxM|lks81#| -Xf%n*uVkY7WMZdvhhFdTu4BK1cTEjlo$1NP_R-`Pern%x&LFkK)+2Nl$T8SN^>G$P|2PwkYBurT^IcR -OSv=6DY$zK`@zI;et%QHPh4Gm~`(w=6h_hreSkrqY<>B=o3h?llo1T~px*2#cZ2WYc?dH>;X&>~4WK2DQK(b0lC -tU)uD+f)>AOXW(&bo*?(mQs$j#BLz`=ohnd&d?$VIycL%(mkmFf_xTx6iG`;ubL$Ro3i1S#pP%4(IYGwrZ`d -Sxbp#$u!Zr(iEtfrEs;XQM$p!vRf$rF4b1c-d(L`>Ur|Xo|1xSLdUdKQ{?Xn&RiG_6KGM*YrK+bGNiPIsGJf|Lq|+eGc?WI%*k|MGAAj1+=7y&x73Ig5 -={>8zdXM}w$p3Cf+g)R;-$NxEOnE)_556y1n=2L_mbzub+N2hg*vPDf>UMG_wQ>QE@&l -SYT`-K0G=J(yTRU@o{M@n=5GEfcyM}E}Hec0!M(iEqGrFL;_@Fuzy-QXC@2hDOx?a8JHUreO)-2RQ;8 -5Klaifb{k0QeE$04{~$rcgdAv}K`onYhXqJSlA3UUV&=@27*Kq;H*>c~KT)q=I5zyB=tm#sj;&+Ig&9 -10P6UHk2&68|jHY{uT{$}iANVX9Pqm84pXMB)XVc^%v9e((zl!c3l2uHPgO4~`5`B&i3H;ECunlDmMS -1Ei}B@R9+(;^QJ;k14bdg$=iH5Tq^UYes2rDIfEB(c(3h3K*K=$j0QI^zO}oe*cP-4OUcLe|lZ!$;^ -V7nSolYQRem3e081i-r%vJIh+Ot++c;RqSI9Dgx)nSZ=Z^pSE~7p)Wq>D1`b8CfA0$+0_fkbONmRND7 -0=m+{61hfNezlD-zOycZBfLx_9VuFuFyIZws-Km -SMFbM^g(fO$uT(mU+PO267dBRi~srD*UA=U1Ftg3rX5Q=Af=HGqt1ih#wZ>F>8jVe#5v$tPZepd7%FgN -0MZ@0pK9Z>JJMJMJ(E!my->MhKQvV`o((YcS@0!N1ADk<}|NSbw78tf!Ma08>}an~ -M)ZL>GiSAV{Rq#PuFDR1355sP7WD|ymxk8My7I7Yp@(rWsmlDk6Cy`;r5w^pD;v{?lRw?H>M-E0z2tr -~NmhfX1hp$$J&J|6{Oe9sos4_jgNP<*5rvl$O)%9QmMJVs)Z=W+Wa2JhX%3TRNh7FVCj47i=nNQ*;t^ -BNWK!PA&3r*-u9@YAOrsbSlp`~r(@pRs8-%)Sj^Y&>yt#!ratu{sh#rk)oIyj^?p{P6q3Z(qHhK0kW< -E()tVE^?KUYYbz%kc2z?nb$Xt45T0O1$8B}-Q*I-kUl^NbpH~lL*e#0MYjd|ef!AXImT|82ATQr&uKR -!!y6N|AuEsB$Mgmnr`Y(@j}j_0Plcr{kR9Fxo;$Y6a6MLGJW}{UA*!r4#ZKzmvHz*3M!M?vj_e?n-Cl -Gm`z@l*EkuDICJd`s#Xjt)3D0v-X<$6d4D_@kUyzWK{Fmghc>=zl!iJpVYkJ)2rcXPzS^ZPd7uyMH#< -w?@^h<6YXwn^^p;Fyx5L2l;jYy%EfjJN+5%j-DjgAb;SE83C-Dz94+5}2hQS>53#5lu;$q4c?mn&-h( -0u&Zh`L<8hp53oS4t7Pz=Ss+EiWy)%sP`MMf^-IK$PeTtE#dzN>yzoUXmc?=M9D3q849LB{u;ncl|7j ->%3A5{@9of>^05Usg{IY5VZ_J>f}y|c&Af$<)ew)G+LTmeOUasDX;S-g%>fT3{+IlWT5Cp5P%_K%%Qr -rhh;IXTD!tCUvZ7>nXbv9(}uT*e47%BvSwK&$GeSi$QLydp1IVF$#^cS`EoBLHC2^EpfMoF3IHw>E?1kqpoj0!6WQs&LC -P#WdXXV$h@7&sJy=##*-Oq(lc6Lv@`eVSmKfMugd?%;BD%VYfgTLTNe8}hadwPGT`~<#Moua3VM)AyJ -xbLXKzv!BiNx~HEBpxX0DazL6AWQ=b^B04jqsl+MJZFqn}SmPuphTZbk8Z*M9$ZIvn}w+Qm%!rn+29i -Q9$N(0~}ygR|~l&4H!|`IPI9;a3*NHRttUxN=R2M-D8#WoF-SZsL9j@T9 -#0>hCi8cEr_-c)O>fsL0TJPu+%j5}Xul5yxlcN(2|7#q4^*AQobWw$40D!p6h_<#aZqR6snawxJXcbv -^dEYwIQmb^W>u$O&R?X3jZOO^1ANcjO3_ihs4r@;RI>&>N{M;edj8K0E_R1qvoZ(%AwYqN<9f=j%%)O!vfa1iU8U`Z5`>x?!lj5>9ED1uPf -eQPO;F8CWCpNw1(S>(LB6^i5M$2t+hMBYt}a)e((;Zx)i!%m5u{oEbwh -H!FTB!wQiS`_KeE}@6L9kq-~30D0=v1+lmSG!)6shP3j$WI2(NlMj(l;n&P^wH|=t}FMDPYQALc1=0< -CU$Gb2UUI!!WMVFI>A)*FQk^5}zT_k!4)GxWSyt{3BF(+?W5n=SvOX5k|cNahG+F*tE4SLUE@3s2|X$ -Ax2HdF7IRO=(Kd+Sko1k=9WbRTM|B~-;+Wn&qYi -!LbF>J{Gp=uV#S5Z5(LtBpNsrv --*jCVF8t%J!AZ$%nf5~MaeaH!WG`Dw{9F@M(Yz8&Y3s|cwGM%0!=X6;p?IApi^M;CcoDwVAs8FrwRlv -?LPdF9-OJGkHUy?y@zn;mXbgDp%OEH7upf(i`ZuGbyGCn3sqUi4wASw7Bk)Ojuh6jttqDIN;F$08A>%j5NTbV=koP;`%N<(AnHbQ6gB3+mV -*(E(8N8S>IbG(}B@gkT47l~$;VDb%B84z`mnGbbc@Eh0+(y_rjoA*mn;%S%JQT++z%cOlFunp<7mG1C -KpFntw?{7zr-v_})Beg#Y1wA*zP+E`%kGc9`nSO-{bx#KRKZGp1pec?&#%rhmqIczmLMu-mJv -r^zWeq>dmXzAkz0Y|2W>yPDT$#3d(iSWC=y~eSi4o?a`~3u~+{v`RX5E!v9Yu@Sjg6|M)+Xk%D-C`pJ -U;Dk3F^J-)udYrZ&m>)ERpFP=uAkL`P}{`ltUvp*lci{aqdwtBXs)roEO;??u-zU^Z3=0W;Cga3~1J5 -(Ni^UV}*fbknM7cv_q{p$4}aJPPW{J-~49*h)U+Q&zL#qJY&y4vD%3MGxVurM&Q`OEA<;06C`FZfgbf -q{*MQD(4me|+`!F!=|5Ir`@1t2c+wp1wUC@0l6epiQ)ZzXmR~_AgBbV6WJSjkiJ;ZBdc07Z`ND!0IL? -2DM56^kY%o_nJR3i1e ->_cH5sfTcWF-~47YCA5xGIWqbd^uSYvCT|K-W$0cpx}Pdd2Oo|;5sJ=SK`x-pDVJs{bsUgCA;>v&~2p>`QXLO0-?xA-rQAjrhj0}Hw?SjeK9eQ0D@;I^Kl1tm+8XK0eh%($p7Ll;nH1|)?Si{&;ObTiMN3t%{de&C^X -+rZ&Hzg%K!!{mwo)T0{dFcR)8Pj0U`#*JxSIz7$&6p)R-po|SFzCnJ%6|Nx%mY10G%Pa-4MFbyTOzTQ -fw!Cn+jR`2m9!>D(0Q?DW4!|q55k-%7-r^U(P+M*l8mrCQEkPDF{c*0;JX*zrCcvB(cMvr5Z{pxZ;Z0 -Oj*Tlxh9L&4~4J^XeHA}R@o#AY;G05F$hxke=rNvFhBU?c{_agTOj0Q~|Kib4>Y-s4Ijq@E-&GV*iE> -Q>u3EmPFf}+W)c`8>7<~LsDt5m<5FBayGbHN^HGO$6bh`2$c9fBY;R9~&=kv;fo6g-3z%W|Ae1DvQSVb-VZD`ai{3lvymf0>9H*A}E{CLNjzt{{F6F|~3vf)HwnE{NM -v$`ItW^&p6S&hMI9Jzp1VGe>qFmiymHC8Diyd2QqIQ!(7rubOoktZkm7F;tZ0zmnQPNWdCEzuCHEl?f -xKbsxU&YR7=>)B>A?x#J02dsLMV{Jt>c9(HBlJGr)LMbf8rGSl2S%si#v|N7FX3arWWRDu41}3*h2a{(igpo{!T -}3ejidJX@D^SqukHry(!|}*V88h9Q`IIO(6NF!_Qc!@LGn-1{X0f!PB^SZ{NymTOzaSelGikBEf6+jo -7;a8-)3bVoy2dM{RL>He@!|53R$Mc==h)`fEDMEi}MqrdZyqZ@arSy*3J|lw)YxnZ9%R|f84^6%zQ`e -lTC!n$^;*RY0g0+)Gi&&(m+ekJd0SQHY8r3NU{{d;4a{e5mgtSKU -xG$5YiuHa`V9=)k*LfT4rXqUgD~tDzmdMb!)BqN-QBy~W5>blXPnUCY`*dRKS6N=UTCy-=nF;ET8s_O -H90sczyLCYadQ82`?~9fuSx8bZ7>bIrNtZfgf~!}g;uw*ps~NsFf+7(gpH0BloMY~BmD+5Ma2>y8HJ3 -5T1LX4J_;)YgjJS!;t#71z?k4|{D;i<^alXxmMM*@AYLoC&N_QYZTIK|CT8&dPIfy#HouAagGtA}I>N -%u6eVi5oL}L{dvx1{2;HLG7kPUzzgqRYnRXcgAGXuz6oX9}d3afD{a>TPhqwDju|6DsLB2MOH8FG(H1 -O^Tz}ffOrSV~E3fUo&yjPd*c%YNJr;wDqn*4!VneQH?;^QDq`azw4+@QYP0qNrg_2rI16+iZZ60uULC -)C1}@tqPoo=!UnB1bl&$CD8#PLbp7B#Rz&bY-hiqxWk@#-=pcF{SEO>=+ZN1;el3LmO2~)v9nGVz@RX -?sH_tGXH7IQ^?m?*2mLzGJ71@{!YJR%30hVuR$S5C -3WVn$U%tkX-TO0;j`Ha}qTkju3zCWtMBD|oCP`3uW+Lz#H;Qw~Sp=jc-^ohOojNIpOkrJCa-GRwqF!f -&=grOCtF12dIkt%KBb(C3&DoULJ2t}0WSpr^ip3k>^Bmee$8$JUoXnM-NrI)@BWHe9>7#b$Ps9=-GD8 ->M}W8$>$?~V??m;7+S^@&*`)AH!Lm|s-ooJbiI9v+Q(u8p<#+Ee;(Z7n`a;t<2-GC4A2v&Wqm6ucBiea?KolCd|DG|NtASm8 -|QkE_7_+O{m28EyD-}Tn~IZ6L6(0?9LuEJ@7gAx-0dYWX#{lmS2NyzF(^{g!UYrP?UMx}}B#R32W3oR -*KZb8{7ob2Kk+CHkt%3sbmcyS)q@0Q|&EmvWI0TF`*%vZ^y>`?}XkT2!7fEbP{TJW_USLJy_i&I;AG` -HI=i`dv(e@KA9kx`D-W$1K;dNNcD!uKxYa%AimNbldj27EFf0F51n0izI)Z!hES;U2r0cc|jsfq^?V7 -%|DXy47A`#LQ^!qmGWDNIY&e6gnNeC|mSkCByHHjU>4!C~*%6r!8&Fu6w;>vLle6Cf>+GKc+&J -$F;kNpDv0OS%5zZ^X9FOLH**&RjS3l2euYb3JX1h|daor~dozR5V -U&Kz_bx9hT2+vM@5(gMR>F!_IXLq-*i?J93Bxm*Z-H@>R_nBU{k&iB!_&Yq4b!$|p`TM7u^=U(!czx; -)^?yXG`b(5f2cV{Lh4YNR_8kTzh*a6+v3*pAtf9MOxJvqBw?5)L>PR(I_V}6fR`(^>CalW*cob{vV_E -}tY;uuV&P#c@(J4(LA(5w1PJL_7Jie2mv*NwupoH3Xg=?@z?hx8ey_Ds0r;CoO{9e*)%1QcL83)_XLG -O}qU+rtuT;3bHrj`@&}{nZ!+d2hYp?lP-)c)&mkRqS#jOGS(3_Tm>4VdDDc* -qgyZfhrqTwxWj{eih0|nOn|rE7VxR!IQ4GET6X35m8OpQDba1}9Ja?tWz2zK6^1J%44lr!vSR7hOJc6ZWgZWDII@O-r=;+>t&x(+~z*~Z?%=sO@A-f{`GX8o(iH_=e -f}XZ#91zKSF5Y5`22ruyCnm>=@p;kHHIG<8aHG=C?<7nr^%PLIn<4O8RL#Qmmtg#JcWhcg=bjA=(pH+ -hL_n_&hhM9yA2wI2&?)awiVY34eX?yra=KirO>RkD(1}jCX;@0jdZcIRkYSApV}tboEJp!-pE*Luy0o`9Hc*sl(00ojby0xD5VsPsA*cQ4BGSE?Hb@1NNwQN!YX9ZHnByr*X-Vnj#b#H)WlK?^?<3I)@wGy0ncW7m9#0sCld}^qB&Hqqz#VLNvck -$Ca1bEY9qPoYOgZvBYHK650FX@Aeo(^Wws<84`jo3Z4y1b+OHNK0N~gH&aqS=^@>U0({3AR#jv`k1^n -(-?lgX_L*+H9KECh{SCsM8%GKDW-xt;9sz6I*T16^}Qi43zz@5+}y25t%k|we>n3rfyWLXVEGB_{*Ls -q&#@7cusypk)CG2s{uuL?wsC982cmG^Xtn3Aa$=eH58gyo9SRW7T=3>&{u3l7#IFHqH%}T0mc>l*n&n^YcYLUnJ;nj~Ebn9GC5QJYAgAdjCI$;VKGH*8Xm$JOeD`568cq4taXlP+RuyrIb -A*ksRo_vl*W0tG$J`J4iKENr>+s%|)0s;r9Zk}VU;Nriy+O-F5#9HcuJyJ}aI*EnC31PvpI+H-sLe_a -i2-mPoUqYWZ=yQ&w)A|2~+U9YB;mKC!z0K!u9zSMbA#)ia)4pWSB6Tel&ToN&EL}QY4!u&1Xk -b_CiQj*k_)4tI8Efk3h1+DQQpBX-D6c!JUz(HA%yo6O|WkihLmn?o{;#q8t0`dS^tn?WUNDt -1FU1$wdy)cXQ(^dYURY3--r;+H&5{r>%21QvIq(CMBxCh9Q&OEJy8pK98WJFU14Yju!>%RQRvugvOm$Vktp5i`30eN1zvYG1dCZ=-|$Ag?i0iyO -Lm>Sh+|}w?qqADXC4;B0J9#kU)VZ$tcQ9{#;5V;0>`*a*qXxBOn{5 -JU6BH)qHBJoLzw5eg5>_Q|3m8|Lg -F7g*8pmstmzaO292EF{i1YV((V72^sPTwLSbukH3ABYTg1{4_JA7ffk_d9f)Zo -%S57TNTO9{8)gT(TLa$!IQ^*+SpZ+V9^Bw0rn{IQ(ymvgia@@DFpY={M!j2{^OL&Rquat`!+HMaOgQ* -TrZlkUu=Di4`Wpuj~3WZ<6%!=!@hu|7nQqBQ?KHCXZ3l#NXYZ-r~uL8T4gSS5&Bq;|j(oKcU{i7u6vk -vt!ZFTAr0uiruMnRe^WpcNjrI+=P -n3H$tU_ql=rn-^pnre)mEfa$^e$CJtXC+|NSjBU&4yXH?9#gz^zf6DzZ8oGokMFFXKm4j5Y$0tfgF~@ -;4jJi7Y%bDlbM0t5tu8;#)i@M1qo@7~`icp;hFT>C{`BU0B6x?o_Pi97sx>i^R8BN{PLfF$Slky -7^d5nE>=Vh&6THI=rtuAQC0hP60PwyiPY>LNJG|qds{|LAL`-(JI$~Xgm2k*8&(G&_6#%G&%fmR0V)= -L&GphI@5v5ekv-Eqtjrdfv~-};*=;8_`(2!(!MMf-A(~=>1n%9@h))^=nHnT$%;O~TyaH)6Uu*V*Jsi`1=Tn&R46?1bo}!aDIpQ&V8)!-{LRqhZW0BK -$o7O708y?j55cj6sg2LGbV6)xk+Ha-s@zM(YgL`%Fxs6kzjQv3(B~s3d1E8hTJ5C0wagJ(l_GGzh!a& -unz@tz;a-_HaC;jO%K#U!UzS`MSDaAKeB$kuj=s#A1PK9WyV%Xr{^;XyRDxLy##mVYr04^jZg_2GvT; -Xu0w$jkH@Psym2XNV1s?UIY+wY(bdoASfE32~)Yt`etlPL}^}UCS=ONc9H*aXvL4ksqNA6=??ucCdY} -XG^-eQ`glA7$`N^$3#)kVD>AY#QMD$oOR2RxPE6Hczl-Ea;0YLLU09WnF_4ebdPS8p*q1L~I7Pe*T)?_NHC_3XPBhcDk9K2Kg8zWdXw=U -7UityD1%$uSsHw4y~=a-tg>#z~?6gPqx-^p-VSc)60!n326Tf4(gOA{>kdEPWjh% -pV`d$@YX-7yooi>w1m!PgIBh`j6};;KH?J!?OB`U9&Rwf}m9^=+hims`i{FaHPLm0HfDr9)e93_p%-YP+v+#%ddys -$AqTXljh%c1r0t#Tp$n@Vne~zKN^0*qbZM -q$2G6hZ%K{#>_Nk$%(=9IQ^p;t?g*|*)f7s{NC_md=jR*!t0k!?`29(n2+wWe#e)UELI!aXbE0)ZM10 -3chKp8Oq-K-=orRKtrHl)E^Wg=4<>jWcqcX${wh$J?oiV4U@h@-3*FBX5MB@H>XV|i5n_BX#V*bNhT6 -++2bq%SZD4Lv}L=yH;m#}x5HC*UfFb!2{Xh~}o*Oxn!kB4V4sr?lqJd08BdTe#;dlRE6FcPaI!Lf^7sfEkD5x)Z--Q-7&jy -rpvd<7v{beg -(Zk{RIf!G6SdXFi?zn@>EZv%i+DCNUt3j!xZ$CtcgHwQCK~8W=)-1vCE}+o$I_Dyvj4 -3jgpg$xePiPXdq)@F`wCzW;l(EC7^w`9>NQd9y^7kph|OGbyiW`~h^`F2?SJq3u)>7|XO2*`9SC`0u6 -zj@kuk!`SF^VAB&16=NBm5fK3%i4cFwn|xkp_R@jmarPP05Ej&#=54ts(zCvsGtT0-HkeFM=;l8Z9Jy -hvDlv4X{HaLKC)N#Qjs?(`k=*(B#2^}Dw(@gi*o$KM|Mzt*y9vWU@ZPUL>H#XX`2!9dIaK0MsRvXAXc -A#-IasiUqCVuV1UeAO>Nkf@BQp}t3soP(a=Nn~s+9h}&H2X8}*sMQp=eebszhPAgz -oXFeC|lKO_Uszh2`d9PPc*BJyScafktZ+*j~pI@#`bA -!fuD$tIc}0zwAS;8aJc7c~Kh!V*wHj^bs0vuR$GEg)JUiz_$)U>r=8+)2Xq>MqN<3WKcIis=u@+7&OY -TMN!5`bE`OajG(TAZ{FG4iG=k)PMNH5N)xWpZgy@4gGno2xNf7}8N`e0#e5CjX{-&CCYIiM<&_M?EC- -{u^@NR{+)tFF>WeuY1F@%*QkKbYljKggGG(5?U7Bgy+v!QMN!qLDtXd(?1i=SF%k{^y!QfBxYMAZ2IN -$uIuXB^@<>&YGb>ailJ}P~CJ>u?&L_tippIFF;xS<;JGR?L!Gz1(W9tS{r#C6*w6wffa1}DKv`6F{-0 -|^_r$xsg?ADqFY2ZzAZIR?UsbP7B$6jO!2V>bZ3sW0m~yVbG_16Bs92|lw3{$Pu0X_Fe=x+A9C+aklj -qy8VQ2)p_*wvv?{1!hLlWT0h3fZqKAP)h>@6aWAK2mly|Wk~v3ag34#007Sm001BW003}la4%nWWo~3 -|axZXlZ)b94b8|0WUukY>bYEXCaCxm(TXUjF6n^(toSHm{S>l*HZf&N9C@NJ5S-?!q6Gfmky#bo-ZjJ -u?J>4LpiP@d1^&!H!eCIn?a|L5o?Kxe)NHEd?+>LJUU`82VGXYNo70D|V@W-}({Jt&{nh#S}eCu8HuH -cdIf(k)d36jA^5ED|wGofG^UEz09W{^=KIep$p0`djH1Y60sgcDdXjs|i0iF-`lkG(i&J|3D*MkkNV?DSwO#J -|nEbk|TkM`KHToXwL_Bb*T;qZhWW8VwyKbMGxHZZXk)=bd|v}^OKvYJ~_=ws9Xa|@Slq;i-B{sLS-RJ -36k+8Dx$Eai;#L&Bknem2qz6l=>OkH?Y3uK}>*Z~${VQJjyX(NWc-iosV;&`=@8&2XFXi+gPadx*<%}isEvOFiLM1g26$ln`M -YqLZWNegmNsppL3GO5)?g2UaYQbO}uD)udukQw2;7N6tvdc8QtEmUNX-aYhhhj)FHrg%Z$CD{C3pS!i=mib>$z&h9LRMgzJQ_1%6$>zNx1MPa~jyhzMkd2%} -T6kO`Nu6sS?0jlrq9)|^d~0x-NXeVr4q -k(9_G|1My0!&CqYemNDM8IzsN-bxdr&NVx%w(mlQ`Y?4=cJfopvr}6`QIAW>wfNdN(rw97OA-fxOPS#C4Cc5-RSb@>Y2Q>2kKJSD~4MM*L4_9@c*Eamg~W1sK34pGP -#I{Zz(C?4gKLRDEF$V8M4HDF9IE!^f6LFOXJq;&e_+tv=_;>^|%C@?2NWQHyv;jMuUG@>kFg3W|AF7T -OoC~w@f)sLCwA!?G#gww6gsV3nTgd{3oTIHg$t(XP3z?C6$C|UlE&g#!nrO7f5~n=SBPnP)h>@6aWAK -2mly|Wk?UhMguei004ao000{R003}la4%nWWo~3|axZXlZ)b94b8|0ZVR9~Td9_vBZrer>edkvUVi=Y -LN_E_qB4~or)Jh^ikuFKu2m-^P#ig_@MKVj$wt#$q(<|Bn3HsW8K>w*<(%I#by2MFS6cJ#Lc6QF0v$H -d+1L*hqFwY{EE}lSD#{DmZN7D|#%vo43B{2!Yral<94T$m#C$NZ*R2Nn-0gsFg4-qb -ckZwcmnD%UFIb(^lrf0T=Nj7+JM@TUxo;g8>acbFC@LVPzS%E`*e}EL=xIoT{6M#PvG+Vk(3D}l#Y7- -c_9x@rubMViIX&JVTqCH{;Ix;lh5MjEGPrjZZ8^*xUm>Odt!;?_86sMXPf$w5g56|Y$>_D7h*X~;X<8+uF9eWC$=ZdruQ{S-bwk;@hCgbRv}qa*1ksyJqMVtv?QX+-b3(PxL9Rfx6@xDNSM&Z!E -5R~zpJ`U+|T1dkO)ss3!dpjiJh;u%w-WCb5#=5YXr%g{7 -qo8PsR%fn4Oxxm#SVriE=m`n#N33)z%MMmNhtxoo5G@J)m~TyKwVM?e#~Rh!<9v^Ko0r;Jytz0?Uy>& -qK_ld-h(LMha1%;v6iAyTjNY;D%0dP}$esrN58HLQzPH&{m`@3yUHE~2fh%3w!|r6`;2e?c!%-NcNk5^=gG%1zWr;T@$Ksk -qm|A%U12%GWVd4ZGAw9;fKG*{*QJu5mHkQCAC>(<+3%J8PTAMW-XC@UZJN?HAGTjTN3~~baPD&a0oxz -B2H1m-8NN?Hv<*Va!q9!yedz3p=rw2W{*QsL8D8`sU%q$j@p0pL&wB`IE`h~L@H03Dn&21p5P$f=zz4 -Z=tnXH*FKvlioJED>_56b)BkG+THKRqb^GK|(@Hg-ql17q@wvcD?(+twKT15ir?1QY-O00;mWhh -<1AfAKI60{{Tt1poja0001RX>c!Jc4cm4Z*nhia&KpHWpi^cV{dhCbY*fbaCxm%&5qkP5Wedv2HAt{K -x;HP6+sh}E7LXsS#n8v7eP@JT8pxY*rGsEyV+yfq%F`xQ6N9>mnZ2^rq@{@=%J_%9&tF|&x|Ra!TIBJ -SnEnPn`h9u`utm;iJ~(|bbDviX6sLRZo;-z_(q^MATuR(1UnvXU-5g%O-|S1Z8?W{LMRbPXr8Ik -Gt`|)5ST+?@$~vR2J143OFQ|A=uE)kg1O3~JwMs@Ireoq}9QZqbl#r -JfhXA$7DN4|0+;W-QjePWA;`R8?KEFS&mP?%0eeZG=4bZu2SIka+D2e^Oya(wG0hWK&yyJOW<^7Ux+& -`G6ln+INw4F$6QN7_id7aF`=zK|ELT)+A^>2_OMX;F394PX#MH~9SVO^CmQ_-lx7LwpzFZz293;va+2 -e{83@x3QDWp_@ZybYG%ybFZ4=vGg3C#CUDEP#wC0$49Qw=&QpzQU9oZI?ZXR<>=+0{{Rf3jhEg0001RX>c!Jc4cm4Z*nhia&KpHWpi^cXk~ -10WpZ;aaCyB}QE%c#5PtWs7?B4cwNNfnUrwd%1{Sbt?9^)#oq7^7W(|91Y~)>s@b5djHel|!l&Dqx0J -e9&Z@&3<#`X;i^6kD5tF?ko+=ctQ5BD$@d9lhRJaJrzz6uGyyi}*ZuQriLdvU(`)w;26;E@*_A*IMOP -&t&6+Ylq44V!!^68w*{B`k$hg;!+C5N3*YrwnuKQ9#|oC6}(yx@yH -tcoa8d}+hB$X~>gFOe@&_`@Mu36@HH-p(3a-zzY?v{MDM&~(82v-R% -tjnVd4X0x3Wt-_;Y*BMi9KsD4i@u44J;EdG(8%W?VoztI%l}WxKMHY`%oW^+V1u8Kb -Xn-b6iQyfcZ$!_sk|N*0e4do5Eckp5VzbQ)g`Flcg?F=9Vdq$uHNifZ4`u;F%09F|zkXQ!_1pc=Y1?8 -G|M|kox{atfjiiLk_p}K+ZD+j$O|!44bYYrd( -T3Zm&u1YqhR9y}rIfYoD&|sE3-}4*ca(G7)lKx;7ZO~-&KQgG$Ky%WtV1G?ck+s?azgIr2WypBH6C;uQoju8Gm0&vwl!~X$~~yznc!Jc4cm4Z*nhia&KpHW -pi^cb8u;HZe?;VaCwbW&5qkP5Wedv23Ztd2U_Rn6a-08u1wnmWXU1vT?9!`Xf4VnVv7PP?Ph^KL(n9L -yhxG)P5Ux^k`8IDciU5R;D|Hx{mfWCf|L6vurj5t*H58qtCJ@_6GcamnC9B*^`?d6)es&(eDoNm+Ss* -maHgzH9j(mn=jaH*3uSlOIc;ia4RlV$DAHWVutTW|V^Nn-YS&tQ(X|TN4U#!iwO5 -5zP#KF2*T~a!wlPk@m2NkHSszW;LZuWSVxz3OxQ4YY>QrXd+8F%Q=jhNK(S)0!W$yq -BzMUqXMbUnVbQqQ^FCEBM9kksk0UU+zxm_AU>I69_?7><> -a4^6#B-3gQ&Gs5s)DX5i&h;wwXJ#u|7h&^|K}e2{(B$)>f_)4yMIUSe*sW -S0|XQR000O87>8v@9yhpO`m;?LgzaDlxEG>xUg-H%iMmmQL$Oi5XZm*jhw%WkHtLf+liH_y)tFW?!->%lIvtoxU#t6#A -#RbT^)y9EI5qhzK^FhXU=(6$Nxz?^`v!|yg-iA0_o85RaSa9ov>ufGpe%y5U#E(pJ}P)~vw7bD7gJzDj05_l*i7)3Cn{8X_R6pK-mf6HGu{K -nXNkD%QiO{{p0l*EcsXb^1I`nq8SQ?Q8eX&@(B`H923PARB#+0~R!;iPVuPrid+675*R>0n0mFF0eXQ -nv>qcYC*jtej907eB)003WbvC_LPwbYD1IDOqA;aRqN%Q4-80pXUL{&GqHg?ZZ`Q!CDj23Z0P3^4RvQ -D4cW(i-HO=?xT-M{D{U9%&1uMH+KxXxM(y$Zaew2rl93bvNLD(VhtUd|%;8c~2bA7L6aaM9q@D#n!I3xWYEzx)j~&+gpeDGXC3!81M$|NI6E3Q%4|N)LLitL#d-6 -VkuhME^r$KLNj(h_5*e92>A!C-jDBMcr$m=-i>9&4dAP_ChP<8JAK_={9P)h>@6aWAK2mly|Wk`yB~fT4}2t!;zb~U;vt&Jq`$qpdk-9d10}g>I&C>)nZ)hw?d|P*4~OjN*%4c2u}p7g -tgco^U*nU}=-}XBREWRVqO8i0$2_k@QI7u6{ftI8Ygw{Q7T1XYJZZ&cTC!>_SXJ<}%(J3mW;#6P6}!? -{SzQWvl(AT>WGY5HjoAifBL>8jie)9Uv -Bae0j%jMH$vblE}qZVV-5lhNtkXFjL;bqwmzCDa(uO(=Niv5lk7vQs`9-if?aj-afQ)UCG42-^h)~YG -6`nzFzZsxpyk8SrxlxoA5(}FS8AB$Be+7oEN2-vIPfTR|Ooh21GQLQNio8Z%*F5xrt6sqF3)OUcWhu- -k#iiJ7u?`ibSfULos44cnl{w9Wl=*FGMcVI4YNGu@RA6MX3<6h!rAUS8L17DG~<?Wt9)C(eC~r_d1H{CpCMDqB98)mI>MpU3Cx>o*O>=EqYOR%+}{TV~mvY=lpI0y7 -6Psvk#H7X8Q#Y#(FLVgIxvM^ksRQ%k>JJ?PV%!<025VYCHx%d8!N5s*uzhM+~GkE4~y -o$9h&U_9clOcvtetOJ#Ey_*lY+Kdy0&k}b%Qs6Bw0C)eFOjAo&bk;7Me+{z>&e05p#B-ybA!~lihzzo -ETsAdE?`*^KLVlVWUmmf8bIt7Pz~I*x*DL|EQ|IN>#6bp|y8(==9M)9IxWJrL+#}~RX5u`yU5i4nm{**wd1&~bFup6n-OOeqz39uAFAbrF|ISDWM%0GiT4lhNH2~ZNFadVx8XZR{=U=%~ONf)}v)|yd@ --IfJ~38_{*NM%!=j8LSmgcKK?I;<*x5?n;(2-yyIjY(He5H7F$m+0Au%&Z-zcP1Fe7hAeExcokQx7K$JNdUeH~C+FE0(RCnIso4Aq*`Ab= -WTkA(4z~{P1Sqxc65TMpjvV&i=9IIy-5KSgz8SnwvnxTvbc4)M(MMBvo0nPw_Ig}hiCX|jVu-s`3xj6 -V=9%JIh#;wC0lSgZ<18#fCAscs4DWPx@WJ=aKOZa9HbGE8e;^I_WVOiqJxm?~Qf-e&7ara5rpjg@w!> -cK~=SfWtn6?~BaFJy&>1suv=`^n^n?1qsOYGDmmwUo+`X~|VN~v>--nFN!wQNTqd!}c~o;@SvcUxZEm -JS=hdY{7rk|!`956c8bPE{jpxS&vLs~XYR^w0Ovx}%ehZu=8x3Uj=Iucw5A1^@cB(qi>`(>`rEQ&5wOj~D)^kH5r9uKuauv#Dv5x4U& -Mbry<2$?5<@ut2lSNop%jb0s!W5pzz6xz*9M29537T6Bz2*+ftKy5!%R^D=-k!4+5*8H@>@rxB3K>+3f(ELU=gk;W5r&q;ARI{2URkxKPEE>YFs7%Y= -5CDm%SvRbRO7B=5gK|rB!X)<8KyQ?=nFvZ^{O@_?e6g8HWDi0|TyOLpZk9rtWD|QY9Rb1ilJ2y!oK$v -7(@D`I0!o{L0adRryEyoi&7)|M@C4wY7Aepv$=?)&UlLWAm3?7xaSOP)72AsoS4oAL*trY8V!a2*_+TA^P_U>yg+l@-PanLn0kZ*aO&5 -MsoGQ`fb^h%BIbH43Z@lta!Q<#2vTEH^?PiXC)J%H~RkggC7t)YzyxBheva>vqWQ@vDO_&rd_J-GjI_ -9lzH1Czd~3{VT?I6tr*7^nny1`8jD88*v1x)|4@M1oIf>&GWNTbnEqSrc+ZRjyt0D^+uK6Gh=_H$mA& -XZ0A83-Nc9OS6$x1u^j9eJwxh89Y{gW+p|wDB+az;~K!?~zN){Yqe4qqH8if|Uf*7MUq(r_hBgJdv=X -MRGV#&7rcITdD(>xo3?%=q%!tJU5UTo&-@;hnnFM3H -JTl%_x%4A&*>)0xVu~#@T~<;{r|010GrqoS!!rHcH8Acvf08N(w$jVWY@_b40A|mDY0Xts_lKS6N(KG -B=+%HW_7D%gnWya`F(1QPtWlFNd$LHkw&SeaP9rfy^BBA)PJOiD`z-@k8*2jjI-$eu!B=l)NDa3jqga -I}6Lw-7QAcI911E%aSsW{NSkBQ?l#Rq*7_vvx=r?pK^P9J>q6F?COPa!jC$2Na=>C; -hoj-L#(qQ4zwEQX=1M_NZYUDQyx;__t4G57{M3f+E^>xdfvSo9t@BHV}Go{5D?bV7TxTnF;x%Oqe%Tg -_tPtF{VgW~teR7S(!xr6w-J=;dK!UjZSrzK*Xb(d;8CNjB5(Me>Gwlu5Bs)?U=f^?(}o1N#NH*^g@wR -5;1oe6*F{#}uE7@3)dDTXgsx5!Qs6bmvZ+M}a)X)|vP%pFwi0iqHCZB=9Hfq_s}TA#Y>raGs>qh#wFl~CmlLNYj<-;cMq+{rmzQUdh`xUvc&ag!fLZFNiFmyl=y=KqIN89{a*;OXOQGEvy~E_RLq>#h?WZhzT8)T{jnC8 -l?}}2^pLFFT-PHN$|3S#RNjU#*DCSXOo;RuB>*{aE)ju>` -xSE?GIi7GezRxS}0WIw8!&e7nD9rmsqe+G_mSz&xY4nbWH(vARfPjy*_r&u}Xtg1l~NPG-!a@F|Q%Zr -P%)0@V{kfZo7mPKotb#84=OIf{Uk2-39C=PAXQm1#0yH5EAHjP3>LOJhWwKm~lt4gY8+M7@~xotZ2it -oqNBsn?(lvyEvqEhD=I}@Gzq=BWTR~UNI$57J`>oi543!RRM21*3qs9Q8sbJ>kDDboSaJw7Z>>F2}w) --|k9vaSiyl@VsN@~T@h`3+pZ+Gi{IvG>z5J*rDT3O(hv9z>c&Ell5LTZr&cFEX~}va+eAO2dtbYaKM* -8e9l~qzshgJD=jsYtB|l#^G%6LI-x=R0UYIM3XsYYH)iSSkxoEaP>)R5cV?RlET-lPl^5a6$EQldkKTy%wp -x$T>lj#DK^v%h87WkP)2Vw^#m-;s24<>DdqA4T(1X*@0m6N|$B6&Dg=UU_+PScLg@ApJ7d7mw44&Hiq -4FUczSAun&QvP^Rd>THy4o!eLD-uMkAb}4;5@zOiL{Exem?#sWN+!dEDKtMPT0Yakv}lAiIZCA(Z?o2 -t0x(cY7d4*hTTZ5^Y+p)dMmoGauBL5nM0dQdYBXo8m0xX#eHeQ1BYa#gAU0o+sIN+KunuW39hHh1`b2 -tq(NoMa*_5s`}k3<=eZGM_0#WQ0nZ#2(>sW|GItfn4`%KQbkxQV44z72_@{lX7Y2j}tq2}ogvsYV*t` -%RA917|)jm*Fa()b?x$fJ&H0y^p;lhwIclw%G=7;#nYl-pWZ^SaWT4jv<(zR#ez`ab2I%p<-;b7=b|6v}z1#}9@qN`9RSq -#eQbUnL#du9_6IkPa+vr-6JJ$F_$~>C%Sk`3Qn2_+5)nK?24@PA2caoPAJzrKT6tw~T%?i}4S|59#=^ -;PO|&+XX#FSPk!;a0r3+Qkc$-Z9#T=M1I8{5@&_?8#sfvI}qNKeHs4o_ZZuh>TV&{BNz2*CCHaT4u@& -q9!!R+au5c6t=&4Pr)`MaWwccLChKtE2BKbaY!7Wpy$>uGGLh9zrTBKAf3Vx6ED*c3PbYfdsI-G~--T -AMCt!Uqkqf`KqnX1N0ZkqSOv4~eh&wmr?!otXhl3xV)A0iLk4_z%|F90R%DxqB4QUn@Zos@R60k}PyX -6pGCxWMtNp)+91Gpe@pg>(kKxL*XH(RWhq~^^4#H6ldG5dz9AD=yP)kE>iBrVm5ffmuA^|G&dK`o>Zb -V5OF(UD$7a_0qEjNEMEG4JOFdbQU3(4G*v9qpUmc=zpF_3ErIxW2i3eK`a3v&qQ%K%@-@`>XIcJf4Bs -5$y2Gub)5ndHT*poo40!tJnixsKpjdXdw3SWZ#BcuQYpBRI4?-e0ZgSV|&D*b>6-A9NXvPcHU1P#M^) -bqK_f4FN{x}SM(c!Jc4cm4Z*nhkWpQ<7b98erVQ^_KaCz-lOK;mo5We$Q43 -dK+LzYsX3L+LD+iLCBu#HGY4^0uS$f2|m$z^w!R@I{Y?VVXJWmC4}rf`AAhz=A*&d%%m=AqZHzqb#uN -@TGJp|a`zxBMgsUcGu1X#BN8W6eO6GWhGxCkSRsX&_fgl_AKy%rM8o8n8=*cm5QNug(TR5XP$9Xt`M0 -5V~;>4&Hoo0P_v}h(&>RN#fG}J_y20##k5(VO)xMi3fc!h;V*(Jh_-oAXS>oX@mr=keM0O9tNpaISkk -Zr7fK>MykRDL6Bey-v4e+c2v@bN@so8+gq)LUYK4O0DVZyF`clwipgbBB&4KB=nld-6UKy*j|blRn^8 -o@`Dj=QEpn)}hyL4n%4sNYy{jSuHUEgQ9o#)mPK9v&?)BJ074jNPr?04bYqED|IN=E -y-=kcBfqDoKDsCd{{1cP+L|CBlkF4E1X>eKwfN4EHm+Lhmly=4>{G=PDt`(x>fxG^%LX;|I=*f30$fB -0>Flx>7iu&930I7TcOxOQPtL%44Ng67MVLq#V0lz*n0bNVT-qo`Ae96RA{_V;T;CW`2oCW${9 -k#2{D+>gGKC&}KQQPivJtIDh-s}``RI|5W?rbN2K5cwYP8WaXm~w+y$eY^pJ_s)3lp?T>o>bBr(=h7Bidq -jNQJd~lqY%kfkAK>OxBkwIqB=hBx80O@>$cO+>YYd{Q(30gA2cRXGaRi5kCe(#6a$!|W5i5X)JRjfx} -462SJ$)Q$>e-8n?Q4#GQiq6GHSd|GbVTaxo2!n7Fz8>tKS~tsHajc%96E{m{qaL*eT99cP3w7tYOo_j -l3nI60qCBO?OEqM#UzYgT0FMkXwFf~g=@x_XQv -PEyn^59(ql@|FjZi{d-dMY4V8helIUk4LK0!g3>Q{E>N12@@8x4J2w^Fxjz9clLi*{nWM2e(gAO(PR* -3k~O<2v=pk$;bR$+zrfV -Y>7>0r8^k_6{CS-Hq&Kr6Jhqs!Vq1pL@uVL#T{Cx1{%`c;7sMfY$!_ -fvR!^C1BI^6R*1RAA0cR&zvsb)<@m%6^)0B4TJnKGC#fR!ORuTI=3nE;iJD{`aa6%spCi3KrF#x;)!p -2Y&W;K?~v=@@?!x^T5q(3!kZ77N3Zi-&$jT^iiz5;VwZ)7Y?)6(3N{XHx^^dUmsA45sx>x5u5vKMl8i -q{g&)K4>o^hj(_8!}ea{mlsNM*S)Cc{NaTv@S*Kx>em&5s{ygSpL_`m#{0eZKClojy)|OSqH5MkYOzt -|!=t%o#R|={*G`MNwL1fYHmRFXx3kk@sXDkL)s^7U-c}FSh~GFpfc4jXpzG|K?jzf2gYV_l?CkPl`pg -)=yu|x-c6Qf&pKaa)pGx)VWPI{9&o0kyjy{}d&g5fRE|u}!i&r$?yLgFzvW@Wq^vt<`0smh9hxFN{14 -G`>!GX>f4?15SAW3zfBcC9~^_R|zF9F2rAq?*UCv3d@9O%ZgV^f0Dc_+02}}S0B~t=FJE?LZe(wAFLGsZb!BsOb1z|a -bZ9PcdCeJJZ`(NX-M@lJ5J>9UY7=yWBDi3YOS0FX+q6isKo`A2pl#Y_BTHJ6ijzIu{q~(1k`g7`arWV -Gr-xP+XNL3rm3=mPHDZ%;DvS9EtIcfm4!;ZrdwY8WC4R4kHag|CSq%OYK7+yCLTa`wr*$p>zc5@Dnwf -=Q2rLRCC)~)gU<+PMbD=bwm5SYXibQ`PmsP0@E46=9!oL~0Y~B}KFLF64Co_|7EK#&YMmip4L1E449-7Y-(6ncWp}@QzRZ69bn`L$_2!q$>+??+mly1mMZBtVr_9%nqt$9P -0!=PQAc&Q78g)s0{PmZ+tL*dH%^hMd>)c4-)4V3EDK|VC3LmGiedwV=qL@{h2X*v -Je(9uu2emrlf$J1M`IsJ!Z8)Mr-Aeg*1&ETm1kZ1W7#vSlYjK0X!lA_}7=IEwyo^zml3tCI28%qHjQ{ -@2?$JhTV&kmZbFx)luwcRX#qEVmoTyrdH;r#vkQE(Ag%uE-@ -5>-QUly|S?pxEsF&H>H-{8W4NhaEaV_&H6k_ySSBjncEd|8Xbcl5y6c~gE&zC@E3-ezi0*Jmf2wqoP5 -eOha_`R&QgQs(0;s`*ZHfni3hu}AhXV|i64Pby=RvZFy7p8@Zl0oQw?Y-c(sMfBZWg%bD$BQ3ZXA!x6 -4!C(w>k1q{PJ(dB`Eoqv&`)SxL*!e_xpor1LL}KH38IG^$~y-1nTrRJhrEaPIqn#=nd_fNx0koKQI}b -CoaOY#47%QT@nbK%+yzoYTWdI&Hh!d*aRV4Wk09@3)22`#9ayLJI!es0pH1O%xEHFxqm@7IVjvhX2ed -t(f$rDEzkPqUK}5aa|2q-k^23+P=oxLj^8+@()>W7uvgt_+AMa7ixQ8tx9wl9;Zs~4xYR6@pMDW>XU% -q^yWGvgJjXep|D!niuML%Oz8O;G$p8RwXW4dVxpFrKhzEU$cQbG4F(KxaIG^koPKcBOs -PkKMK?7z`VGfe<{|8jBjGAjs!IaK=t2_;0*}SgF%m!608Q<9C4xCcY}hop{%sc>j8Gc>FC1&GxeN+;@ -zwE5hl}$~bgvBz@m7ScCg?zZH}+jyNi+}*mF7!G$X2DTRIGLz5*9kdUyG6e8O~1k&?QjElhXt3y|Ptb&s~$S$tI_X+&t+ -b7pVvHm36yJiK$dDO8Pl@$X>m=UvV|pK~T{mN(;lf(*UJIpxS#G_(*@yXjB?}v6GYL@*;F6|z_=*EPA(ZG*fz-8vCLK -dh>XRUBntO9lq_70wu>+?^Kv8#@Yy!529}r!l|>m&(AqpanDNI984;jGI}`=lT>5ul{lU>E`nM?DjI* -b)TD>*OtmL+?48i97jOdN;|YZe|i`eJ!`Xba4{UIa3>EMBz-863yf(rALNg@EZ`;pC?FQVDS8xev6X{ -5Sy?Oa$d$La^@G~c(YUbz&+GevySM)}FQF^$s-RM$BI|qCnUvL24`%Z&K1S5ke;l?X=#{V5?@93=7%M -$amqJg8boR_5l5DqW<*-(riBi+};*ZW?fKR4K2dyZlBg8q`wPx)5`2|d9;huq%X`9g0ci@6QosVzW0 -_m{JN{9|jT#DgsBW?-c{z^Rsua(OS@;&+IJ1$%Tv>XGTz9an7{x+JFGno1LChIQSPJtEg@8RlhML=1p -A#0Yp0{;4Hy+P=umt8E!i{xXtpBa*)sKI`%GCs0cR1QY-O00;mWhh<2aDnMzG%K`w1LInUH0001RX>c -!Jc4cm4Z*nhkWpQ<7b98erV_{=ua&#_mWo_)eS+k=^m#%rwuLwKmrjDtr#5~BXE|`S`NFe5$HfD)Q5` -#ee`gXL3jELNk@nzOI)@9b6mKnLj9UO;^{8{f>%a@@4`AzA%A#FF`4*x0Mhf5Yt-wyWYkEb*q+VmTrw -*4i$#9^O)V~75HZ7=;3`k&uULsX=3|IbcV!zisb-~PwXhqM1<898k)A?RgbjD~RpK|q@(5Z|Orlc(*i -y)@veB(;g74U=qQc!OXY6yMO{hDJ9`w29CSLT!+AlOP);+NAL&!IxpTEb`+R3A(}14a{u9WP@Ux1lu6 -^@&k%&D14LR8#vie*>VvQT`s~<8=BmZ*ai#V#zP76#}Q$AgEAW`+~DvAr#4Bn3BfhU4GnK%YJ(w53z; -rIP~?WdHgUM2=nYP9Qe+dQnFun+&wj(7!MLaH6c}-+0lq6iMM(OMJCLnx -Y7b!1L_2&e>7n*jy3ygo5Xp)w#U9pUaEPj4;O`ZB*+8rzN-VfoVX$NFmwfJv?C7AAE60jIka>N9GhRkbNxt~}nOdQ_`4D -mQqQplt)5r|sQXXeB?w}v$%jIT^!G4}XM9;YBs0KK{HuC*p-NPK8&S#c0kL%-O6m&!ws;P*YN6wyqX7OOuyFx|WVuK2aqPv7^Yc;elt3a?Ep4(zfjKF -q7?Q-ZVKLgODq(vRryIm;C$=EUy6cUwH?XR{;9*9WdaLA757z#L*P&a-1~o`kI-@8kWY%oG>e+X5}Mv -5R|88`LJf$lUuOr@;lxdw8*gC|81`roeZewzEW?-G`^BS)pZpNbQ|x*7dp+6Q -Ve~qz_T|V(oT?z1aT)%70kw`s}{6ujy1R#(NgT7W_YD0i -knA$MeCPFqewJhj#_$5C;e-v*Ru)IeAF`Ep3M+F*gLVCjh9wD3T?Fz_psSw{F$qQ2PFZhwZ214M4AAm -nmv;p%3x{SLF7cTIK0F>wr@tR5sxCnXyTELOnhFsvc!N4h?m-#I}5)1I7LO=MSa_ -s9LPO|%OB#cooK0XeyzATj-SSFqMZNY{#EH;%!m^cV5p$X!o73>IU0vCeDCujr>`Kxhh)1-LfXLk2CsBf|5 -PSVVBl-?Gky*P2(NMfd@eB7Yv37?*17h79)d$r)brom2OuW>9nX*Ora6kyH7Q`q`Yo#>my;42nChyla{iM@@L@tHIxoX+2iYiT%uRXVL>NVGJsRTjs&fT -{qiU*X)o-N>^OA|Kei8}PHl>5z5CTXsZqu64>R;Ekh7KOG9PKHkrWCau_L-|AFb7#4XVViEB|r1_! -LEQhVy7-{sH52Y^bp$+ZB$9a3M@%tqCQZ`XOdw%@=ZRfq -Vjhp7h|l<`dB>gEWga+#m0OJ?iNbCf%U&BT8D~uYNO=$UPKH@Td8CQZRWE%`#L(?9)j`1r;!$86ZlwNs>(hu0X$NL@hE41(!;Hfi -ECu0I8DK1b~8C&Ij|M7hXlo27XDQMawXo1W+s6P{=~e5LxC7l-hzdfGK=KW*Z{f;0v-As)AEMFO&WjU -qb=#b>II9UjpveL=Jz6FG~=?>VUuD3z)W^iB1o{mO2})n&`rPuK5ZN=XRk?;>?;KXyd=569n+|U8nX# -C+Hiu=UCSSA5aO%A0Y>1Vn=srE5uhD^x0T&ns#Ex?FV_LZ+zFfv>}T=)7S7uKhg*2>T>1b -Nf9eCz>!f@>7wG-L027uK0Gf{Mq9=_j4}Rv@eRSNhPZlddoM3SpZus@g&ttZVns{nL>4L6)ipcMUSHtnN^6Rp~{ry2m!x_}xW}_Jw|;PI+cbgq88Q8h7Vs? -{qGPov(Z?UbDuk#75o8ds9*81mYPln2*hUH(uMA*vgGLbWiPZ#rFcN61-|FGhsO;S(0MDmy9u#l4 -&dc#;R`@N1=KHEevh>b}xBQXJfyXez?C6AUZ#(YJ^+eF`u06(#!TExZdj1+9<9K>pv@^K>e24 -p0xHaKexIR==e?~mfK<{tcyu58{O;icbJeSXb>M2jP1_ud1<1SY{X`(E!ZVo;sc%FE{G^V3`l_9LE;r -fDQ7+1?`@r488osG3GS?6iGR(NPq2W~%zhI+^44%)$McZ~Abz%H|}Yk|fc>8KVX%kFMWKugYE04e%-0~89yH<Ct)YdJ^1$;Xvo?eBf>@G5esRb7!+@8*Fp{f?jOmtFY_7Vc(8JR9)h4$M#YK%kn!n-sRf-;m -89BlQ96pkJJCJZ%PyY#{$I90j+lE`fqyLgAY@={rDbNt>MKw*-kagrfXioq!qLtvD^QHr7|7@=W?BtaXFd}$oS$)X1V)kA6#k1@VT!Z+Fj -{fPs(0!0ZZ`_jBH^QCdH_*9d{0lKJUY=cJ|l35&}VC)DWp4i0L1_P25-4Mj$%p~E(^$Ij-O#WKi0CEKvWYOl6aXE@zM@V2P)c7D7c;983& -@gz;0A{(cq&j4|dm?xD1Iml;U~Xr;PWN8%I;N`^85F{p2L`G|v9;ksX}cd4|r5@tq=8ga5>*?fbI4Xj -`cYDk!e2cXpf4#M6OA0an#*@`&qhIutDaajQ$=vEbeJiz7oLI0Hp_{S~$!w0xLKj^>m0AIg_|H=bgo*(q -~0Dm;Q{?h8w2C|V5&!#$y8#pTIt2YKT>V%-j%?`tf4sH72w7j3e3gm)`og1G36D+K|5 -AS$g)xIW#QJIkfxj==TZVR8rPQeOy%h~LaE>(}OqXrA0L98UZ(UaNHdTn}d0pjHZ@Pw9BRm2gr79+t3 -+=3{R)9*uh}(;=(NBWi1Q=S@Ubpd`i5*xfyADJ00;qmOw+c`5@pKGeHssV3DWW0F=OJwD -)4Wljm&Tw=x=`YY>RF6+Esb?%q=h(Lowv4SyCrsTe_ -mDNsXN!0~gz=d$SgxZ-197fG*rTvl5OA8i)_L7I(r`!G!MKrD$*AW8q0-=qiN-%SPw@Xv*#8UjU1l0RyD*hK?5pJuqT`Z2~L?V5Ef6%PL_ubh5ZcfT2TDi>ZO&i|IfCnDbtM#rhe!Ga+hJl&w^Vzz*b0Sg7pHiQ>z1zZMnfolU31|}8@i(+2>fAVjbCq -j4Kr)tzVcQ#&&nKnKuiTeMTd1Czk=E;A}Jo)<%@Sm9{drZBV(rv2hmiAW&BpZx7cTZ&d6w2n}SEm&>2 -YwY`JueVP``AZAPkZxK$>x$|=OGjAi|mD`o;zTQ)a{^EEf4qOeu>xf`#ws8B0ANh3g36TJljpTUZKlv -&&&41e>S4zZ!ddhww+65oCjEr638Z1jpl^d*&bAGFUi`h#}dpFO^I1FML7BDGF^Nk%yxHIxv|uR_nvT -dP}qD_)1z8L`jn`e=x+&y>u>$VRfoJ=aq@g%k@|Lf+@B{sLUHOsPltnCmHj7@2tshj)=>FGJ!;f`Yn~MAYZbywMC -{GO4sl!M>;y;cjzD~G?oslQTph-mLe4wH>cM;tt3!8oAG8|mCYJ0Bn;y6EG|a8Vd3|4>$FWcZwIsAVl -#`ug`(TOUq$XS+6yHuVmN+`@h~;#0I%|b1bwwL>%D#o;ZvPNhnoBz>D|FfZy04+E2qkQb)4|9igwUVx -eF1LXBc>V~TN6HlIFb?n&}+20Toj*gz7eJoMCS?cZq2g!r^$N&nHqa%gYmaa#>k -~M*=I4wL}KwQ7~@m1FO?^P`etxZ&>kA-Oy!wU_OqHcdXw-M3cDczJ^k85Wesxw& -d9a5sYP^5*<^yeDGQFL?fsChdQT&woQ=+uGDu21HzBv_#f#XO>6#m`oEV$zg?R6+TQ--TlDTj>%-}AwMvGGXTpVPu{5+)fGh -rhBd(sVH+uvcInLl+Z(2{$RZN$E{QY%*#I^hfvx!(Xw7uj3-b5-|bDUh)EvWN~^d!F^(}2LSG~SK?Xv -GCQbb`IjUXR|yyrLw;dfkjdhN0av8ZmlGylrWe6W2!uoz+XC1e!1y4t#h((5JTU~GF&P58dZ( -GKV+oc#i7IN|sQ2zG7Hmn+w1R~XJa+?B(;soe4vM@ZlcdzsR;(Yw?0jVr&bt~KHH;iN -pHCX7K=fz#wBab+5wwEhBea|#r!>aVCT@_It~x`g|K!0hQo}#?3}|zK( -it?HME;wxO>F2UWfyv^ZZ6TV%kt-MZRwMhbPld@DQ@2jWv@*CH?I&-P}sCA($dYA+`|U(Xlzft;~4ab -m0-G0F?;Q+qZ~a^(yjq*dJG7+=UBpNqAM;-^?bFLoKMWB&T^>mqor<%XIj+2wIHExTvHp+Pg)_#FtG+ -OZ(dn%zcKk9*@tsHAChy{XK({dD}6>1tCGhLg`>UX(E_f|s29Y_YeM*DE)Qc -?Tf?3Y{`e#gC%z1PF}CX%xVIM9}PmsP`aLJ%~#xAZD4|n#OxNr4MkuLmb=2rXb68lP6T}fik$Ss!KTb -U`pa%GE`^rW=N((RgU5aqCly}So-jmEfrOEENUYkM$TUp!iird8no-<&z1Ofw_VJY)TrASyy7P{1>$o -{Stu;Q?83ta~=?d9&$mZvRziPuCtzd?ZiGexWW~C?^RjizLl3Rg^AJ~}|3MB5fT}{>Jn^>itZ;^fm%^ -u>3{3_zuDm<&sfO0U2Ct(V^6X7*wENNZ=i%;u-+BB8W%;O$amDvit*$5AZh@fFj7ZjQY&uMS%kWo-w8 -obOTD&ObSePrhSo;SA#LFBPX)RyiI=lyt3IX>DQ_fAi(pvX5=Ug=jw+Ils=mFZPXrza+DbV-wZ>`d_r -Ht%i%Ph@evH%ke{c{0{g0Cf^%bQ7-y(7e^oPWTiI;3`K*aayg~s0y8yu1E(JT+$dQ{1=o)z~x@Ckq}UxT;dYZD?I{1U(pR<0EDmDMY -vozd!_kB*aks12(wAZC8Dx$n^;05c(h@#<-!@Z97%!;N54Iy)f=%iAoLXWP5pvTXT|&O1OvJFcxCir)g|zD_8TwWQX$@w -xfe-oFIfw?F8HG+T|G#!6y&W@nRTmN8b|l1WbTt~Ph!CupZOE^?i_6am|f-x1=bIK2WAFeKl7qGZ -}WHAKa-yXm`;RB5hId6b{%Mld*j)>L7;kj?2T0lIK51w`F5D4E~@bu#3syyPzaM(ZVFwj?LFGazVcY5 -@h4|Bc0_zx1k=+_fD7cHa&7;Bt5&Ta-JVYIlQQ*!VxNSHTGyvj{upP93EyA&s^D#oF<_5Ib>v@7{=+n -t~8q59^>cQ?h4Lfnbyv#UbeB_TKr|!9K2=Sx=3+)X<9z{1~($y4qipZ$!cs~u0R|~8D?n`UcN -;stC5vlZR`H0-yV_vco<3);`J`NH&JZzRE(z8Hh>&ffGXa;sf@Pv|>!R!*7p8?wq7FGeZYaJ)Lta&Ns -i1PM`eUdsP!@0P%6=oJ1gls -&P_P{Wx#i&U<;-q0nt4Fv$)o5xlD8!sgOQCXue^mZWH_x#Oi@tk3UCx=qx0v)0I2svBZRex1A+Ec5ud -fc8kBRoW$J~5^igG@6qXbhefp~eM$PF{DryxFS9C40vY3;36fhiXP&GP;6eA4GqM8a}P&zJR;Z(&HhH -P{sfX2?9HOn|0+d`UB&Qf@l}*wm&PWyl!0UFcItNt{>M{1BealcZ!Sd)mr*hRRO8;8UpAg~H!)+{e<= -5L05MhlA11_*iXQLz~eiFxJRgAydC&wkAJvUeqPMrJf*@QE;^vR(Uc$!!uaOi`iKpQf><%x)asAKi9W -^nOJO+yiwxK>x?Y|(hEIG=yDtmB8n`zLt)mk0ac*k`wT9sk`4tKm&?UgEU-6VAT$WCH<)qsx --Nq_$x%M|89(G$7<+c-cCXgbNuWbOAyZf9=VR7skNp;z7pa#RCo_%Sj}>n89Tyy{xz@ao+Qt(o2Vd -pPB*>t?it -G40F+viZRg5BxrsTLSo{xkI+$nO~>rmYe1$@x3@_EE~zPwfE$cIN>VW -uM)D9+U2yJS*>>RCaFvhv%mAV$`|5acvtq&=(Q$6m6M;G^eqQ0BE;yS@CN|yEg7}Q{m^LE2U)+R*3Pb+e@(=|*!$Pa;sR$81~@32rkXMBEo -GSlXz7DdsTn;rb2J8>S=Kb{90A=Qy*|$HXB$n7+bQcHCQXp+d3 -pYh{}+sbPr||X^0(IfM^^TJ -QO`zpm+6p>BdRR*hx%7w&@LYxJ4;9OZAtp6M;|fhHX7Wknj|nh&Ky;;E~A=FlwLc1gRxIcR%jyz5$k? -e=uD$Kr^KYb&h|>N@ITr-+{Gc8?NXNFk?P1>NTL3U3^{qsI}$W?4}8DIFDe)Re0%ufwEra;)pB -JX^b`NxafgMTSj$bq_4$UZhls8Ou5HWP~@-eSddm{87a5n95I1fS -@{Dh%6^=aFL)xUt%&qMzOSpV0D{S&r7kNekP4Nyufq=WG#*BuB$An&kjA^7rdvUp>GoTLDNL(u*utO4 -heWAqYL1_Nc;CW9A0FVMjN#B -{P;DR~3&D?gu17o#GIm&kd9FWLK9w4uQ<%;Jp(h$oXJI-Q}vg|+^24U@lv_2X~DdRMUJr#ILBj(VblC -$jU5fP#bmyso^d(RVTW{kkHhTeFI5U_LLqzfNyr6#7n$E^f}nQ#l``WoxnWbK4lrKZ;QswkJpu*wd#P -g}P`=Ji#(5%9CS7|x-Qy^+D2;Km+Vvh%}RDk-bNq&XaikkQ+ipby#y -Q;Q?}z#to_xRD_gW{?#eKdV&w7!~xAi%6R}=!P9u&e>Ig!{9*-{o)w}}_hId$FA4S8E%$E-SHeA%cWE -S#U{bh?_cnd}=B>-9*a?z0Yw@CkdK9*Hrs)vA_>(7pM}BxASvaC90cU}E@&z#+z8`l|1BKAPO;uzgU} -s!D9ehQS&eeFLy5NnM$KKv-juiijMGL7%k6d;p$`2(b(N2B#jeKQFYtWja<_eA1eX}~O(IXq)SZm2G6z*b34THio#9Sf$X9f;I|B1C&1mN0TN -BQ@w}=VXN_I@Q^-c-ei^azKbK%l-PneUwcY9`9SpzYy&5?k}apY~&=!+J1ac!HKjo0k!V4u!+APAct~$?OGrb;#|R=N5eFK9YZ5`OhojMhD&iA -(RG^8kCWx!1JQ$GJYPHKZaQ!mI5t+{9OsATiVyNi)@tRbZs+Y}inGjZ^<7f4$7y|GB%Ev2p#ds`2%)+ -uj|zJokbo$5XTm9OmY<)KFlBgp!kNQ4H~sLje{P{J;7tbxZIE8U~QN;^!U#{9P*)ZuVWeZ-UFA(!dw(LOzavFWxT}}L|@W;z}9f -wQ74scxnXYd1>H_6UtH_p+qaQ_GcrApwMYoG!o*qhi9VZ8i5skuKltO0Z<-^0>K++7#C+9&f8;T3OPQnQdHfD37YG3nOZ=IxKhk2Xy<=KcMmT -*MH2(gyq%cZ5jh7U`z||G(a=2gfe{+@8dVRvp$N~9Yd^r_)j{oU*DbEZb~$sRc4WFbZrUvy#9L|Ic#z -DGV`dQKwpP-qe6VW|xg?A(ZZ8>ngnO~0VjQ)~%-0>WY&4-~xf{OLY-^xQjBSokGMlok3b>f5tHZXzxV -c%`!QSJMPVwfG%!a5^t@?U)D09tB41Kl2p-Fp8VU>wYw-8dfSp~$=latC}h=aXFn;mvB&OCPl%ljkMd -4cI2rPRT@qOI7KE3DG=q|iOgl7uNiBNnjz#8|Et@3-6ZiXmz7H9y%j{$IMJ17+L#5DLtQ4R*e=jkj$THC1LjG3+-=*tK_QyaFRp@66^Dhl -d6CUO9!@x9u!1&KbgMquf2y^**E3h{4wE6_UkboZ8uV%qO<$cKgmn4->&j*oA3Y(H5^*8#{rzG -ttPGuk)4)rb9ijr-5?rmSUYD{G{mTkt^5NiSdi7x(y89U)f5mMefcUR?M>Iu3D42BS_rUp -|5BDcNe>c=0b-eyXOrzA2pp7w0&UBnCj&LGe!mEpL4i}g6k~M`ap;kQJMA4U+#$RQ*s8^~tpesWx<3+ ->cIBz_5Qc!T^t$$R@s@(zE8>E{0=rnOP|tXy*Y9@A#xN&wTrZAmx&3f0g%m -fK==U*j73RmKv*k>R=@ckVSU8#rBw&gyX%tjI4W -B2}}Srh1*gjdy6`qBVY)CK7KE(WQE8L{0dt_^MqxK$(=v`M4EbYptC3;ouXoIu^&+z0biY1+;_65KVF -TE%OgX(-ztE(BLDF?T_%CypeOKmg4<^p%ZnQ2L7I}kYbWU?h*6O>R4C%Y@{y`dRX;5Cx`o>Ip0fDs?W -Rhctd*oF*jEMxvFbUjklw9*rT*`@+PDs+iS;8$Yqt`N7{zm>?xK^z0JhQ7FRaBqD>Tb2hmxaX1cGBl(B>dYPC;)x#8U|Cj}s=_tHMYqQy-pJZe8nb)oT}H5bAkaP2wg=&eecQ~s5j{sGxs-6m;^Ur^^wM~_LPW|lqrlU3^hk;|&Bv5f1J-t;QMW -i#O$?E=e;QkuJT$}?t`M71oBR|d&$3t@QtY%otUou33 -0QC$Dkw0PaG57EM`tkPaL&|j?}y%9?1Sob$j-h2eDc?>}WoaSae5B!bqmsg$c7O=9uxJi^1{!%n*l!6 -FX%$7-nT3(An!IOHr)}EA=_`L4g<z#CU@;&hPLeQ1_I`-U$hTT-*J>{r=g5%!)P9Z|1ZYS<#Bf!2o6S4=*BWl-bqzIyNj`I7Afy2+Sdm=4 -sdq#8vo${f#@f;2I?_~gT6F#?EJ2%mb@Ut6}d(HkmW{PyX~X^)W0xl0;4xoikJ6r|+Z)a=a=1I;$At}AifBp&Yk#b4g8i9}{JEuRNp$|)rL_GquN(R!v0KYS5B%p61u!Q@@ -ZUL+!9wLmaDut<^At+mB;)94pxglic7LfammmA&+0oB^j#UxKjm@1M*&dH -5}*1~M6%Fet$IZPKHDJOdNq!D#QtQYIF@Gmqr`sr6Ocdhx!-rSX;uja_)uyyhb34%1V3g{KMaJk6uFu -|^SQz7mZGD;Cb^_1?g<6%vQ^6go%W9#$S$vB_r;t-zsq(1{LOrnIej@Pc7Gi3Sqv}mcJN}j`KSc`&kR -46IsFVTK);VKgic@T{HhE3ea$EsVQL2_mZ(lAL|eK-`ha!o+m$Ow5#JwYOcaA!E#Xvd#n9>^6BtlRr! -{NM?`D=6upH}Fiejczv~&0S42N*FVq+_lo^X7qp114B?;Zc_MJCfAF#lNRN$fq4ftW~`sm~JZhk{G)_eHcV_P(&%d9nhv5lI*1 -bgIhXZu?j%&SF4{gx)Yi*P*TR5Gp&4Z)Do649tU*T(7wSDeLjPT@knA1!8ca46+W^NBov=IlY}PvU(% -d4!KGtl4`eFcU7Dfy48#3nj4o@atu+e`wTBY9r%d0f3L<*`<58MiaUY5KsiglUX~x<_xoD`)xw`NOTn -~avCWOATl`$A$Gk%2!EA2(z{<5$4(`^(z`d}-PfYYv)Fa%1TP07mjSJgz9hP*KsK$B_p@$1Ei4H&3M& -;O1ICqLu#NY#Sh1G%;*!*PtqCV;?EyY3vendts-}eIHuP&y*+WFm~gJ;t}t^PEj4DYTGGn7xG#vjw?L -vgQ-$Mnzv+0Xm_(ICo6^Vk0I`SiKc;W|d5ZTzOgBX>wO;b!2JnNQ|CD&sbO_NMb)!>LC{o2Vh5NdtP{C;Q(}lcKw`X&N7}tpT5;GtBd)GW$V~sIuw88F -O3Xi*37SX~wx!6T;hG(pIT8*E#XaJWam8>fs{`7#K@0)=p}^WJkIhx=sZDJmi1ms3IQo>dO5nboln9=lU8A -h(@$VAJcWcXcXzMvR(2S1uLFEpe`7e>pn -HxJ#BX?2c?lOLZ#T6No;fw7T}{QE(WIpNftAy$)20iI4P)(r0r@`S0&cHj1e$ZGeFl#YUPOU*WBgogu -DyEF35G$iX{+#KXxr5OV~siDSt9J58Z^JAF>eiwKi^T(%bs-%{qX4RA -qfgZmG4!FUW^j6?P;ZoI-#6_mm7T9OMybOdM2PR7m16U`OYzA_dLdw;C&(l8|wxv$UoJdr$q;o&WbRp)4T54X+%YEL#+Deme -KZf&xSwbT4eNKRpCY1Ij-+{Lm!GZ;fOvr-w2A?5sK{fNb~s&c0@<1!TyEa?o)e3_nAjxO2TzVXJ(?ier$22ngK`3e3v>b0iVH!t*C>D2c!;UA4($l-tKgwJ}ce> -(UF6yZ2deW^{eSJHGCExU{XBZyZikR_A)m0q1?i&q(Y)hz|8E+f9)xs)y>0HQI(mooquB4)|f3SR+ju -+S&S@-zPW0*H)R;?C&nyDgAkp$3dX6R#2~uY7HwjN@e4|CC@0RS5E>P~q1`CTj6r1Hge>2wxkSfI6m_ -1(i#XJ6ZD95_EC0C(%+21Z|_LR&f0H4tD6h=!dIu#)pGlH&N&D;j(+zsFB8%9~$-aeLwV3Q}tCE{BPg}?MC -091qNccxR9%0`SE6*#4WEBq02M%;Z!r6cTpRwxN=c)hq6X4iO$S#t? --L~6Wt9V(k?IuKh^QH_I-A-rt$VoTHQyt*}T}~*QR%uTUL>4j%TSTS3;tWSm8 -rHn)RNP(62VFUP8Z)YyjCZxAVKgbo-ga*dkqsf`8%Og65x&5%zR%W%UotqO8cJnb6@nMY7v%HV -f^y)&|(ac8vagE(ax%%)rBid;H3O7F#_qJFly`8Ux-%$6_DFseq05PjUYc=an2(s9(j)mS2{$;%mJ=J -dEU_U(Pbl5w*-4JCc9ZtuTzg?=m(m(-08S?~~ylS}aZXGdO3;~Iw(~8(fh;!SdnSR* -#hGnv9cTd$h6>)Elnjw{?wOOGa-xJb^D>yZ7_rN!Cwe=DS-Zv08Qz9r2$`m%)N-wC(c#p(cyln4{{E<|y~j=Xp-0zt1D!`)jC5~}g{rPu4*!antBk`@l -EPe))g0oyWUukoYS#=lbu{Li33|2|v3I_1}N`G)>BVhX?3VzA&Y-`SA94E|S1Z$JY$d#%gJB0m5Szya -acRXf91?JJ62;+qJ*I5`P=0YQc?U|JN?TWWB$2qy67D{8r@9_m+{iQ*SjmP{si`SK&`^_4^bR`e@{YH -^3Ya+NXSrF7zD`|VpjatvMmgkOpbpe4x`Z|$qz=rRL+4{U7T&^oq{_q`aX8;A3WCO|bt!%d*e)`?li8nf$A0`Q=Q0K+7_dzww0m0AHLi?p&YWY0SAr}N^LYW?H69$gy{VLH- -G%#hRZp7EI*ag0Ztc3gWw+|+~>#XzvL3R%ydKSW@iviZE@1nVH4<;z~whWR@0>%G4k`9wsjbu{h5DRg9zj~9t`9`}60U{YKiJBLnZ%wRCWcC*KK4io)S>!xx4q*>Wgu2JC6uqH6hgPB^epE -WS>CdaiY3*m$tj^kX1VFS_ID>3FJzmU2anzd+pPS#}vDXwko%#b8GLz_Cqdj+N@jR16u2OV(M6)d#i- ->ALn2vUHG|Fiv0vPP7|b6b*d(rkedEsCI=U_B!(^gVL!`#?rnRjLLYYS(iWc)rCqi2^ -*BTLMY**NDj53<8U3wqpZFkx-CQ1N+x;y;^P6-2M4j)C|1EZwO{>HU6R$6;L&Dd$)4j54-@a5RdX;IQ --cnoXFR_!7FTWeMU;xJ!JppLLgs-a6^vhM2EWiNRpzy^7h7d~_F9n+8SJAd*Gc2(ng2|S>rxCj78iHA -(by>K85szOO!4@87FCt~g;*rDAS4z-pe^2(>%@e&sg-En)Y>i*rbCKU}i;==b(ICIw!z%p~?4%kuek} -IR_x+Ab+>)h`L5#j@iMO~Gz>7BOORmjQ%J6M>3-MFiXj*^7{Gg97&igUumsAn*kSSgwuv+`%R?uQw`DkquWGHQtgmJ= -7`Z-t6=eMR~B=#*cUf)5w%S&h~cWgUCgvEmwH%wby9ksp0g4H)*0)_jrUbHwy6r<58|JOA)}*!1z2gT -bSD>ya)BE>>p?r3&5|ZI^Zgv+1EDj-3KPC%Cq8%Yb2eJ9ffY?c8gz~TQLZ3pZKh^l`U}_KpAu90&IWT ->r9@Lb^6>sXdBa?ZsuC)IYEVv!U$&$c*KUU_gmDvH=CG_duR3ZqRdPo?!rhHq_CAKJP}+l>}5UBbjDz -VskoT;44qOtZ&+-6p3=I^T(`R&?sC10&nq+1`xVrjhklxv89Pl!+4Y!=&{>hEPV@n$n+twD)@7~H4)r -UhL%)K$q~9s^ejf$(qUzCHL*AgXo*&Tfa!4S_N<{uH!s@tK0n5By36RDjk$d)lmiY(FbLj5cTx$xlZWqtsxaaEv0-)FowKFuY -&n&y)ohGU~CR%(PoS_$sM%|X*H+V|C=rMM1f=OuWN5I7LL0Pn^3Yuq_YG4?(N6IxP_8|x#RxE;mVxlG -61VQNvanGFu>Z&-+5ox3%e_<^wU_GJL)N0`5(=0R6rN}tYDb*jhI*?mA -w;<+GJJWd1k?DCen|kJOctj8@VUp*A4emydx%urvqlwuAf`n@!n_ts|&ZOEII+LRFq(S7G3?Vu#V%&r -3J47Vz=7jAq$w4^}m(%&wX3!;l4>b^K(>e2=uv7Nra%ws8ER|&EgLclmPAAO;H6 -UYj#!|4{J$kje>+D1%f0@TF#U~49m6-RsgT~iwa6_~$|PI3y|R%A-D2(a6?zZZ8T>yH^(ke-c5M!17x -}=EEq%N4ot3t1)k<=sFeI}Rs%VqQ4$`fS8vmG2{u9cC?yX7KuCGm?TMN?4+{mrwxvOO(TeH&2oz_Fsd ---vDcNd?GcBM;X|4{3hsC1Jv*~BO5UCt!@5B!(%RZ_T9rpsSZCgtByrXPkM{**HPX|!JaAzD{HQ-A)e -X#LZx0lvSS@2_U1M8AsI`HzTQ-6HmFf{0&S>B!ACHKXx7IqL&`NRnxUorZ{0i;Aw&g_K{h(qCJ+sUf3 -4uCMql$2}~FGy-^wdSsJxNMcXSNM0}Ry>-7x*Xqoh4Q6?jXpZbeM0Wm`w8w={vI%WuW)mOk8_tmdNdi -~4f=~r=zZ=H*;?L!K9OYUq&B$gGo~VWsEIw(@&~MS4j<4q(I=+|_iAQV99KV2T7&ad4k(hqEa~|Ur4Q -*;KR~u*QYWh)*i=aH$JVL&tVr1p3sMHKAjF<4zxm_?FfiA)Qw?lfEF`>}%1{Z`D#`8##M?#h%C@^*H! -a!7U8sb;HTurpW+Ux!P(xWJO7h?zNa&)U`}yUH65CF-4FP)^Ui -zz5HADR6s59YD=OvjW#`qnnGXVtY_^5*#c_bwzflRD9`>ItdCmbHf}~b(OtIQc$e2Jns3!}9J~|9;2}wI%# -?D97U{MvP!iqa0#cUzv4)cw2aY{*Yy0|cP>f^5)1sH4&T;EU@2VAN@6jx|=IrIn>)4>Y`NzU8@aL&^3 -#F0k9ekN62=VDv)+WkcU~9?D!}5`72fpi8`m>Gy*h1y|f9Q1kQrTTsBst`!{JfwE`wlCpSx9p1c-J^U -8Rcm`s|j&1lbv@-!3Bt~<O@KwS;GN%pE-WriX|5*2YEJE_1sBf1rkF`DFv7O$S87eg_ -Dl^wk&&I+)1cEfE-UJmAym`U-K`D3EooM_H{&n_~SMRFL{81)9e&-P14(kCWZSqn%Rks5D>KD5rL>Cn -2{M0s2b*5Q*ETiIfislmIVY3JoNU&;4-ZWzJ3Muz60vT|vFAcK_tY}(Vw3MsR_JhfOqyx}3_4%#^tAJ -O6YViEQJfcfTz2s4c|DaOzR(@(hl4t&5y9MS>L13g^Q30r*cKtI)Ig5yWjI<_3TF}($(pVs;OykDQ04 -_dK~msix5UuYjLfGoK4f=BHU)r-4?iiN;j8?~V8o7iY|eM406Y8}ZF;^fnl&z`j9!Fb{&Sel=oHC+S9 -SkkBaXbG=dXFy4BsZn>(Q$JP@3oJ7Sm+FwiCCPFyyFc-`G%8F-xuBaU^O)|~fp&>keZq=IwNrSU>z;KQlcsA%j-%|iGI7Tgo`(kru+JHtXcRooM^= -11O^cm+(I)efi2F(~gg3ND80UZ-g&o=raK&t9yr7&&<55-4OiEBpkund --q+XdaQ+w!r-I_ia-0F8D3so4QWgDET`3<*FBU_Y&FORUf-z%GQ~^lASMu*T1n{*F^dL>d%fQ|1*dDW -yo1AUcyLsXA*R4#7f}Z<>Oan`HjK_8@F0FC5WwG01mcUa5~< -EO2hC!NwOu_Qe^8RXZFfGvsTL+kk`-pA95H} -n-&~qf@#zy|99bjDhvv$XxV*o-AsiNAbepG2wN25^D_4{-Zbo5(&zff0N$4*8e&MU2JKXXO-N -0+~AyZql={&w}i_m}_oD>wY$GK8npQ44qQP9^> -E8x~hUA4O@99!ZdP(3B86V792Strp05%WNYR^T2ej6~b>AybeH63bh5R?$1IU}ty17iH)bI%wQ#j9uX -N@{r?&@-wNPpCgJ{@A2+KH1Z-Hewka?KCchu&5G|L+R!zBMn}9lYs?Zwpo78f3qToWKB>!6wb&$4%jI -44lGg`?1BEf&ld7N3^9V(Po-9rXa -`>3xMk@5nA0H^-Yc=KyNnCoJBK}ymxZzza==r!F9D5w~dp-Ah?u2L0!8GXQvVDCvu&dZqUSt6af9YV@ ->Y@PWRJjwHhW7ipB-6z4e96GiIzLGM87mNV(T*#XBhq&crJ;Pd_fdGpfFqZhG4DPZh-lB+1+<->D?}= -f7@;V6Z82$Vp}^;|l-{8`+%yQDn8#ICC&iCTsfz(>z;p(r82{i+$^ACLR%oW2N{4cNMM)w(-JaudbZF -V}#rndcu*|)EGoYmMphxFu1VB&-Z!eu?L##d;FY8KjatB`U}fEu=;309~zDuwPmmb`QIQQ -f(ToR2-QQj_JOLq!{`gh8ch8<7X;dCab7UlIOE1Hd++^sYaODQoDzBz^PS<@w*gEx$GY-Ta(Z_{)vIg -4>r3zu0?!k89+LT=b_y($SVD*h(Da9)HtYDLzED5F91$tB!17f^4|}A-j0Vh3^3+;Fo`x?01!gpb -FZBUPDxhco`23bk(Y&h8U|gcnW#O#4f-aY+YlhzEmI0%FBXaOVg2bzl1 -<(7)Pf96GKd`eNXG$2knS2TR%)irp&fm2A81Qk@K756^}t#o%tr(InoAtjSTeRo>9+DSKDyY+M$%@1HRPXNx3VP?D*Jr9)NZ)rIKOrvS -zMDqXE_s^q^K%=u;E9eAtQi2@_%-9hh8yj;SgNt|B?F?^x8dQC5glJIxX&rDx=<3nck=-HB%1eC9Ssj -y}M*r$$p)$wqQZ3C*EDbk(rnW=|&%~7FS@OAkB{Y58nK -N5X7YICha#v*{k%v-!<+^*2cuT8Ry37i(Zm4`wR$_W-`VggQ3s#?%Wl&0$}x}=~yC)#;Y4BI!CmsCAu<4s2f8g|~J&va5 -V#rsPru7`u?yXwvC08(aOHSQkFv(e_c-Enuni~h-4&$8_I>J`6>O`ZPF8izD-v#so(8z=q4UBBVL$9v -n}-}Y$#j%3EBSH*1Vlp(wYD>SvGhl6NCgUu^w4{_GjdyP=2bwT~yyJ!szHjQX(i(S_BcMWJ_a&IDIwg -Nhd?+I#T`$O*G4!JMQ$<}TBD>R_Cp0oho+nsk)MU>iF1lHFnWa~D=>AeSfZ(7>`DBi|n;E&Op-P94^^ -T_C)mEOV^c=vl`c5M1<+h%0%krH&23ZS2eRe>FuNUD#F%}ScvAAqM`_J8R1b&<_CD=!5z4 -7FU-lxg1oXcq_Lp2+Nc)YZr%z-C@J(4up$Bx=~y7>+Ot{0@0GaHu+;`sE8hV@P9c>mJeRL{NhIRP*o8Ztb{3(Yz|&|)KIw75&kT=Nn?MAcshP;EPGOuG{D)rM2M%)E_qlTpB#6Db#q$wnDU3#{gA9M^-8B(z7SNvRgi$i -Gi0Cuwtg8k)w?~3Lu`x9~h)Sa`SlTLL0w3XHq#D&(N79LR4K!b&!hPZONAe!Bj9Uzlzbpz$|>d;6)fHeL1 -w7_94HnX3C(asjXC6r;^TQ?3jebG9Ug$~;rP*R%KiN&nu#K+$I*DPa3dUKUB1Jy!ji|C*p|E^4a6IyPf6 -q(RK}S)gk>QwpNxE2;(QShBbQ!f3HA6dP&b++(E_yTspd3s?$SD$mnQ@gCUUaTmYv|S4i;`%h6fgk=8(unWQ#rgst{VZ(U|7t$y|g{MSGYq(Au_{ZpXc-TD3vP;YVoU!@^*88I -p!bLZQ?0Bhj$=#ras{BVF3p^fl_Hw`By^g$EKg9*ks{0?MNOwH<^o^>Fqu{j~)wj0wao-fvQ_Rh@1WC -&H@L18rLmG+ZEsye+ihajZBU>Of{{Car0PeI2nfbz7c!UUtVq7z?l -p#g%5gMNBXDW4XY|mY2G7Jgu5{kXg8Y|D;<53BxYwhv5IwdfS>&d%O%g{$(dC!Aj%QwrTLT4Xu{q8{B -t74kQ62L|SN8jOVbfSPCdzp`69x|xo9f7X8Y&1Zh(!{ -{WWQjJfG?-lj2&NRoVz&13U8Pa82JS%;BR0(Ej%{~Ke_Xrjoi-lBb#LpoxPQEISbFngVlWakLk(p!5a -93RbP4usiv7M -<-LW}YcoFHLrSGODA6QjUu7Wyub4QuVj{KkqFCX%SApy>L=PdsRm(aGSCOGqeF=N%t>440)*~-R>e@J -Tk~^a|LpJuzs5ah4?-Px*4i{-aAdrYwrSJaDqgYrzGo@k*@;pBIyp*Dg#PF#TP*8O}Srz-ZIsHk;Zn< -sCo}xN;p#t(H3EshjP(tWc-?V!*(A*M8sSr-nz>*z5A&O;T9HlHBbomE95qrnfkCKr81hxOg?mxly*C -YHh++Oi;g*z;m~O%BHu#Ed;tc5LJzFcTuaLiDB%EyB@-)8J29pgY*PtBPRVXO*K -LNeX@rJ>6Dg@sKp2A;ESFP6rZ>rMUjP45HF}@A^rt8kTRnvOt6}qv#D!BeZ_pY*(+-jKH$>Ij1@`nsr6STf#MrW=LsXlST(JI^rh8 -p6=^e^_pRpd|l9&=SL>!Km)GH>jv9CG4b_s(OO`bZ%mz@B -$_Gy2r`$fb^kV8qVcWqeda`?R{3d9r>8c|CBxq -?%36e8Z+*Cn)#5^l#y5-0wa~v{%V%H1&F}2Kd*|$J@9Fu~j8{w6mR3H5;&<4Jbw7)$l;$LC+dcXsV-V -3V%Ht_xxcK@9N0{am^sndK&Jqc=wi&P$7hPP^7Y!$oEMnAr*M3!vRd|wy{P*z%&d;C4`G)$1VLp}0GR -2^V5Ihq;%JSdzgcL$^4uIroQ;As -%A4p2AYTz;dN4X)^v4$=dxATu7ogxGs1ic(8VW3@ME3h^x`<8Vi)NB;%&(EJN3s%>-S0J*@N_dOl)Z@ -Rfe8q+F+>oUUeZJfV5X{5eQIjB=xx7q;K~B=A=h&RH(hBw;!YG3B;*rR>cDFCaxM|X_Hz+EfS6Msw*E -#d@NsLid+!w$iACuSzDJ#ineT_Gk%F8|xuZN3{fi1Y5RBqP6V_XUJ^b*Des{jzr(--e~*J$?_v?HZV9o0wY}2eB)2Bp;5F=!W+(xt(&G)!T(e -$aG63Y}ZVEF`!^}VzP-??MDgIi1GrW5% -YlyBDc-zygazm_f(-#Y(oR<8Q~z<=`q--M^uc)i>@h8BJK5T5#YE!tlLwio}%1vKL)87wRI7|-{tXEg -r^<$(H42W$E!S^1}ml^1+Gel@z9T?_c`$batuKXC!z&vZrxR7NWIM7q#I(sgt^-P=_3xv~mvYtRvLf& -3>><#_3eY%A{gI1)x1WYt+iZKy49sd -in3++Q$V#qcQ0OMSqlN#VshLItecq77*7csWfq;>2Rlf&vP}v<9R=Q^=bNqo=Ayx8FqP2=;Bj(b+}&| -Svpq^(Vv6c3JCrZT@Xg-#&t8(xfE3N&HxFNrdx!^w?e_>S6RZ>{HgCzPR3Ldz%@*Nt|vu@P*~>xwJ>i(Xzq#8iHf&J6dmVAl~n9W4P6~W&2&!^XTg -}Iy8GBj(E{?z&DI5Fmev6{Ft6*i&V_9ycD%ASra9P%J*%nntJX`SGG`+Y<(yCCdHhNnDs=Pi#iq)*7+ -WL#Tvf5=|W$B$si-JGBY1#);FsC@m^mWZaIfUsMD$PAD@v((e5h~u|Dbm{KoxU -bN|N=dR5M=%zWw?#(UatRsrg23V1_ -a5(eb?h#Xo5$t>J&;4$MQ6#4ObafDM$Ie)Th}*&lxD%a`c^lcf4&uAI75sqd;%yAbZ_WhIcBuKh3|5p>E53~QF}`swz0&OmIlO@5sL6Vwe**sB2vGh00DmGZaim}pJ^NZAP}(C -D9jR`fK6)iccsnmSIFOM48IpLbX%;x+vdY;=))^SFTSK*3USg4bEQ=(_9gAI>@9O2ukG|p7oBhG8Ssy -&){jzf`!>K&YtuiPxnoT}0A&4ntXP;m2DEVS+fl$3wJpcn-GbEz**z`wx*T!2*YBihwP112{XiKAP~k -H2s!c2Hm>V)yS8Ayscb8L+R4`;jf5_9pZNn+Y_^j)pqy(w4mAgrJV!9?v&^?9%JAepcB|guk)YPrr_PeVXi;1XNWx+v?XM4qdg)0<6tMZ%M5 -QgJ6jN`j@K2&yFnU#|#Ib|(w*Un;B#9VTN*9%)r{3yy+$W*$c-lwR`%1zRw=&OJ#&nA8p7a&cP1f~fi -l<$x27q%%$`k&(#h -XjtPKf(MU)DXnM{Y3L%hI#taEHU^<4QOR(9DrV-F-8mCh1bnGiMyuU)z-tB<1ADjjE@$g=X{lCarXCc -^y@Oc^6*9oca(<-M0$NE=4V}66K45!LT_SW|ecA>a8C>(cqLi%}N2Dy4d?hJLPd@75y)(Kl}%4{N0%J -$0qjRE+6QNI4+uLJ5(>A085)UtCyW%;l6Q|%}PA`WaDq!l9KimqkVw^eqLez=>h}%yuy52V47bpZ6Cv -Z0)CK6GzGZo-LwxWK90_C1AW`_&!Vy3Y$NaMLg*?t3B!Bks97&ZkonlnuXiSXzdS=_ud1;;fDFnFh)r -;`VvJ**%=a^Pv(6Lds7jq4ZA~!y*@5mTH^!Vr_&Nyky!JHQ;*96Ydfx)61U=srQ37xk>4ND^_$(C2U3+3nbLz$=P(JaAyvtC(UmGYrox+T-1A%(Dg9j(Wq+cf@m -p0u1h2lIDZZmR6(8Sn!5n_~_Sukp=g0!O1iHh6xVh2=S%FH}I-@0#Ju6&I{1hMk@NlDH|H|Lu2=2BP)__)hh(w840ddrBJ|$ -q8f{_*EAb+B${6fNOv3HOZFrB`{z*`~$=g2HKm=RnZF7-}w{eEe-96m1g2=|qw(i>Tv+v7ZK-SHY}*M1Qc;qcOLe$~=SFHW7#)#Yd9 -}}Fn~CND<}Zuf4k~H|MwSWph6C{Xld|t|j5jarOq9Wr^!FF7RnI$!^%RN~(85G2eQe|eWCQKCU`j2_6 -$(+3mpD!fCO(C;9(!{wB{GN3s!op_rTUre;-d}Jw~?gkJ_x9j(XYhrp&wRc=mx2klR)U?kR+G~R3w -x>KA%KhXpTNmkd$g~S+&!90)%~flVtp+qxIv2U$m08Y}Y#7o$H+ikA>E(k?gzD_>90E%5*PYRe4V$r5 -`qZIK)K+*o1fzsU@ibJI#q2Y_)F5Pau*7^vaDjdp?H>^^BZOcj-hrs~e7KOfn)T0Eq2k|8*cE|f1T`$VP|su&i)^P}1ytaTNh(l1!o{Krze8KTqE$I9$De_}l(akK9k@K%afB?I2BBJ|uh5cBwW{5 -~_9iJt2HDz#=jHfe9)`)Eu$qQ(eBpZEd(kbB!k@mDHlE)+%O%tL?r$klk`^Lkoc{^+rb)zNzBvd8|}# -*I^#*765!zrS7R_(3Q9`)z=)MGN!7F6&w+O~1;9Wg=^nXm%7Aay)ATgWlZNtiu5Hj-F^|gni2kXe^j3 -;iPDkf1D5`bLJ$kAzxMV_A!v$Ox(sYDCDVBng@!TR(?rpAVwM-p}_7wEbps-qPZ@#VzfJl7jw8^&+a) -U-uym^4-!@L32mhY32*mx2^cQN%s9pXc+}7l8gY?qbga~#2b5aQr#GuH1M)0Q**Xexeivk&bO-DR6NwP?+}nrjdnKu -_R&`_S3hXbKiA2=ZRRe#SfVtY*XW^6)~B4_fk;TwhPRY)WT{VJLi_h*uV~H8}x-&xvC|+3FIfZ``F1g -!(2GvdY&VGak~9a7N= -)mrhrV{RD_lY;O1ja*xG-YBW1Yh(v3%JrQ=-sl&Pb10>`Z5%9qB#;C54h9GgOh`GzP)8N0j0cy@Gczl -R_$E^0{EUZi0eCY4o7(Z_Po)tt$a9c2WjJy0fjYKnNN_TBWv$M;t6iVko!FE;?!SBIbvSlbJ|!zIXjN -88W3eF3qeAfG8s6E)gj0bXwyzs;SGF;WW{plBCYX$6iDnXJ(I&On7rjoe2LQjgcb(F1GN`?gCBF6<`V -{--Z>AYf|K}p|C;g+BYcnC-O26OCfi{7N_v35%cjKZwi~Q(6^lx_m%5wi?!=Eb2pIbm6TXA=#xMZ;L! -<9yc#MW_@61!b6wKZLD168|+Fdlra{zdLyMd3zzQ)cVnVv;R+h(~+VSF|;K!Rh8ONbKWVn^sq}B`ANH -g-mt>img%{Z@pscZtyPpwO&JrZ|O{U7efv3t#ln>8xuzM?B>dNF=qEA+H2Jmy)o~V!{fWx@yfi(t+4% -hRKLmA6cc=XJbWHE2^^2>>&uMx-8Rdz^sl3RA96v4vg-0QK)5uZ-g -hSvC@LLzoPq!8ZpX3ks3nt=8Fkj2MUh*sTS_2xn<}Tiy(e6e&^%@c+E=aMX6f%I`fhFowyviiT>wNaL -bw@&Bapvfgy#`a&d=nZ{rY~+a+q^#BN6BYO5>({7cY8qwV7i#v!SKsvRg1TR?zr4ik4l14b}Mj@>L -Duys*qqeO#9;g6msg%#w#*i+aummjD@^8kLLiw~dK^UF-@({`oo%rHvp&H+qOf8JP$UGEi1n2FJz=8s -1=yqR=$Dy|<#Ha^^9#v3#~ZlNT-=Y|=S0@XMzQFp)__3DEP27tE~IaKiTdZtbDi4Z|G-mf__@n3b0)Xo7fh7G}&!?(oHuAr?>SOPd48=BHI2*ZoYKCQj<)nt= -%Ufx12DoLobqXBGE4llXv-H{JOkUp -`eoIYovnG`s@=a!WC-HK*&#|)Q7vXBI^82opTE=Fo;L5e(I{9IH1kuIldwPWd-|l -YxoQo*YIpX(3mevu!2Ehe>lr)$YAkyGMPK{7u^iw}MISWwdMUhk9$2I5ViCOi7$oM*a0S_;?NGP}Ho% -RnA$FRKLZ)Ynak=1N+MF&Tg7y+kXo{2+)H*P`$~tOYp|^&EFUYPCTAo8YzCL<@XzaoTz5Wo{@F_1I&+ -A(>Rv3U>0v!Uquj7+x=X#S`_kwm!N_!%xN5p(LfNxr)Ismh4*KW3OJJw9Skl0XjyxNb?=|Yh9cz8529 --Ub(2hGA&7PTF6Q^|NQ>MdtM#R3>kE%h)DE*u6#WIvz0K%e`Q#7}sEoQb@7QcZ*_(nKNmYvyv~RqkOc -BiFm45Q^9R4bTsN4)a79ALj+*il*1juh_9#PG+;nO_WD^0vnAj&VEzkadSs${j27~vh|JwzYu+}z7E# -%nWI#prE#G`T-gn(@FGzlV#Fb#ZnCtKF -xiI9A~A)6Z|Y3L^9%&=i^Li4~`oIKr&jKfXlLs**F-U_fx{O#4I6Pmil6A-c^O;BVP&5T7wR+TPS$Xu -D*j-0zC){K9GQ2lF71$Yn8FwtlN}O;>wsv3cf~{N$i)yiOk*)%L=%whfzjc;BABS>kqa}ir{fuP~g@& -e#^dUm-k_vQNuB|Ah~#*c(R$pexU8yOHuo%RMm5A4MzX_%YPpP{cmy7{$?cW|Eu5;C*J~rZH@oz{hy) -XUmoKdZ2Z~&KcwH`?LXwxn1qD4X_R%@U)g+=Z2H4+v`v#l;WoyZ>}>^G5}n+pQlrnMBi6+oi8tWE_J- -1Rp^x@vR3_X4ob?RrA-8ye*n)@^Pf~o>qg$7I{8!Qu`*`abS8R<8(bil+Fxx-TU<-P-I3(G-Hqs3}Hd -nmeO(@#+B)p9O;m8TFh2M=DRvd91% -)0wmGrR4hAVjr&5BH8W_pM+2QTy7>IU#l|OLrjVxpJTPtljmy*7+V$Zf$Shl0e^thQwL7qZLoo#SnI( -p~}unHIF>|af$!9c)DPYA0K{qVw2>e?wDFbW?e6vNoe!a;O{q)LDBZUA=@1hQjo{Qt?$3-GNlHzcQ(u+-GHXo><+T>BngtI7nYkWcbh?N$ger50{np22=0` -Au&7eH~eK%6BOgR$f&;1Q+CNj`D3dt*pydaPlNt~rS=l=fvy%^~bb^G3vs(er4f#Of^cpCW? --r8^~a$${2&=ypo7fpx-e4G21Xt8(TVt%XkTe>QrbawnA%{_*rJ)OH{=97sb7rs2|A`9NV%KWOlECB>=UF5 -_n@zUVrEBG{@B8FggQMo>6z8$Fw4=L}JosNJpW-+j@~&wyiB0Ha}!jg@r*lMBWTgH&z@&@mWUB7}D}H -D$Y7o!JagaR*=LX)OiALfHcOdQZ^WoS6Acpr^F6fLu$_^oShaJnP*?&%ld;iwe-G>{Pjib%JW -AyM#lKAp7okv1t7<3X1){d!jR!|0F7MY~#28`b}VMmT%hUZx@+gam>Cazd4t6N6Wd`D^~Sm)0SJ_?cV -p#|3J_bc2mK11+W~(#+-IX82u9&3gA+JP(>O^pG(jLFMNkyEDa#_8)GYjIR2 -E`eAKoTvM{V9l6VH^_5|DJdK7FkBk3Lot^dS{Ev#E%Z -_~+(Hu#On6}f-iYq*{oO?P>RcsnS%*Q~cdk=P9RFmjWYT@Sa?ffexa{cxKN`7SV#?BOLz@1}jbR^W$2 -;1;A|o4M0p&5K?)M1_NIin7)b*K>qr)zGxy`Pp!KryL<7KAcTzO_H!e;;v+*JbbcaF?xk^Z2r;J=&UiJwBz=-*S7Bq7p%**{h1%&x1R&gZ9fNdk`~teGD -~U;m2nRhGh3Ixc9U}0yRyGvJ@5tWoj`Un*EJ-kvM%d)XQL#s5)V<`eW%{k*|PlOS`Wn6y+pX%JndtC_ -KyWC*R@V64`-GrIi_O_(Syw#I2OKg0pRbD0SqL3@u0S0brp-2ep}h&fwS59nD-{pk7Ym%bHaL^Vhk#=>MY*NwK5`_+8;Kbnp61Pm4z*~utpn%LFsRSd6gKj -U)1Tb~mZhp06=8NpfxT8d|8#GIF;oWt7cX)nA}6Gb-xsGn-k7AGSdS>~6bzjqgIL1s!Ma^n=(!a0Fs_ -f+@pxpJGd`c^@a!KYvGNkA;RF;TV~OJPiz2y@Bc+|KVoOb)?|nC3e;9ol3C^r3*L#7|t_dTDQ??*&|PadFQyLpbz?+?s&<9rcg -dVRDLI-vWGO5BFV~ddKQ#)u$O7~HoJ7*^inl -Jc`l(@O-_hRo>&j{~KNmp3&^&SCrqdw9Dt5?TnUZ2e21*j2>6d^xoT@2MWev6h%o2#uyYu5E>^i1YxL8g&d=;{8P+)&K9ggR*=``0Q4SJ9j`_2X-%3UCj|@_P|A!=9?1VquSZ)k -|(R -WR>&)U>Qx}f$?cDNe|#?MBZUruV| -d*E+H)(s(i43ku7ZYelAe)Ni!bqI4+5{VVQ4keej*o#*F{#UMNY^lPNonM8K>u{eyPa*`?G>8`Pu=!F -R)aI?*yQP63V-Uz|PV3n8P)?7m8cq-(v5d)1%&aYRC7Im$1-IdHQ%ED{N^O%)OJ(q@+f%+nra3@#Rke -2e|<^8Rk>GNy459Uh+gsw=>2|;t>9P<^?mT~x=K)5M_-niq{0?T9xbGVmJ_B}35Aw^Fep(ENu_b$w#n -}No;hhHn7Sy+x)=>L?tR{o&=_`Lxl@U7+QAkEj=4-eX1s9abv7*DF6$Pw-gNf*s8!c!)r3*C4MSga8| -Sx4_M@V)ZX9|-A%aa|GbcXnIep3!4{-MO#NjfMGFt=jkmuo8;tE1@_PZ1dez>&7R3Pj`&Y?{l}KE`Qw -)*f!+;q&{m+*sPQqZhtPS<4KHHkQaJGg}7z#ZN{Hv`YR&mhi?U`|CwN@8}IhWFz%y$ -r$ESPslYfYk&!_0icS3I*;Y~-(1x^BCB#dx(c#uGVKp^z#hfSK!s?PdV_c{MF)6tc -oBr$s<*pFM!T9kKVt?bFpH;$xkSy6ZYMmCu*!a7Rt{U1&C$ImJ>EdK`|E?ctlUWb2J>>s?m`-7!X3dc -~I`pF%L-%nKLxBqJH`Oh5x|9hADB(M7G9KXEUZ=pvK3?lt>rIkt;09i+wRW$$y>=Jak@;WoXWkAC}>F ->!a7MqTRA6b0nBdH&QA~B5v6n_kZ`T>QmG>`}o(jgJRND=*td~!JjN`SgAg06~!D|YCOWHLinS!wD$w -UNeP@F7Sw0N)6BNGsh{9D|hq@)?Uj&G?Oki-ZBfD*kPnUmQX0lyrXwJ^GorVljT=4bu4YhfPe4v$hz^ --LROqI`j^MZ*evtKDN$&ZrJ&(zl@i5$uZ5+CixwX1c}eK`N21+^~QIRayiK>bd!e)*c@Mr$>Z6N7&|b -~qKMqXvg~O&1R%I8y`$^Q0Ft}b2ZZ<90PqD~tE|mS7v5m|`VZU8??|i`58*Gwlp;O}&X_sDU-fjB1+_ -*Bc?WnT^@CY*90YnbbwQhtLi~J-&faqeKjn%2&tdCrfZ~tESGZ}GJVEkj?V}RH{ciAq1matx{tALx?! -_gr-U$4b-rdV`H85wiUUaqr9!*2CU4`5&`z`C5YL-z?9M?mY}4SVT+9&bTUJM7&Ea=W!ZgbA<$2 -J(5#mY+6h@$q`tmrRH&$<7njNJ-9&tP2CBqU&{HmO8*|IP%?+Llw@G}r5+AYRiegi+f+k07KePHjGZ( -&-d^3FpAbTgMHui-bj>ihSfd8nYz?;fpgQAvz(9gklgni+im@9BUaxT$f%M*1_>LMSP=E|RLY=kJq;v -@D9vFNzJ~o_G1Z#qReT5%`L0_Jf5MlYV>4UWnrIl%=`(ynK!dTj6)?w~sL&zjsjP#^M=~oV5SzHD1df -B6s;_dXvk(7hZPaWWP9&9_!yQ{Qh22^9E$7H<~bg@j_&7cj3O|Qn=UGH3RxwG=P63Y2Vzy0*>727mbk -O@-i*1QANvP(X(ZC*izS%7Q)8yNj*=5?9k#(zaLs3zD0*nx<&+dWq0#+6Q`N8MawuypcZBir|@|&&t9 -xH@D!@@;W3VaO3x(E(AxwX_L<#S2bpv)uW85ETdU&pBr;yC+T6MZJK*YY!hG1{}S`l!32pI5fFTXB647HX%XbQM1^y(9(Fes127$EZm$`-$3+ybQAF~G -MW)SAvn!SKioAfK1hkt+^+@k!#9aw16qzt-a0WGP_fW{@?9f`AIk;@(gRsHA`fMHKjB^y1_szja!ktJ -8x1&S_!(KXqDu5;}g4e!c1%H_+Fef7IuD2UgquQx`;hO=`U2AGcjN=4W}qbzc8wTF`g%`Zv>pzMI!~( -^}TlzsXO5d7EQ~c_A-^Xq%2-j28>~FsXC|Q%cG}4&+NSGbcpk@cNYhhpDA#7low0)45#cCB;k!%*1_Ynb(0!9e(n%5JbRZaXB_4c4N2 -Noq;PPraCqo=R|$;h5mF2Rag6XB$nBttsP&-v#v_wkX4v!2D5m>Cb(hbJ$2Dj}-m9DE{qU4wq4S8CuK -2syxdrrs3!_c9*332b=FV+nv8Y1`R%&^k(I=^vviH32}_&q0~KR${7$^TIDw|tDn@ymPQi`kFf4Y9fm#{RqafQ#NTD69xM0%vJ -EcF)hiNin$Gk01OpL`@W@)AS>H1SD`i^EZ;VMg`E%{uJogi+^!57q@ZvS)Bv=ntM43!*tEAqQj>qo?^ -qcF1EgdRfg-Fv{CfA!0aR)&tkpbq%WD*)w2dg%3jG7558=-XHU&i3Z2!g -X_D2l5ct&dWU{2j6E3uY~Ti5jeTb_xI+sJKpzY9dX4HN|h98jqQBpwB&F)P>tes2V_f*Tl9joW@?P#a -xWEk4>?nHGl$XL^@ly3TJ8Ddp5Q=sV1(X0bMA1g3BMmC^Dx2HEU3%FEa{mozpTa4vUMzm(%6`8o5%67 -a}fkD;-}kOPmyYKhHgIdYkkYQ%w5-O|}s0H#@gIZkkxyZjSLWAWg~gf+Pv -pkg>uO{cmzFdOE00eEmadUY9hVUrqVP|L+al&i-G2*x~Kbm%|v|=4%-y&Ej!<>@S5?^9qNkd=1U55A* -U5B^&+=cpN_;bp6Sj`G4n%AFj>E(Rwq4_xa0}zL6#V&aa`#71o}tH5g`X2#Wy`0uh7!$+A6(L1HGNfX -kD8&nO-OUjaZI7axeg04f9}fX}fwHOof8O1}01U=c`#08)euWJW0Z&zSrbZhxh_txr0!6&X+#DHO3_|a*A?An3t{j%)Lob0>LF|Bv7Usq(ZXC5) -SyM-2~1~=mY=*QpL#}}tzclKQZS(Ydm&@VnxcW8ar9K)BmSP$dF0UExXT#c}{3*3ZAYo?X?LWuUeS3G -kZq*&bN*}k6at+n7vXo5WJfCtMqU>yOS1tPJE8QX8$pk?E?H|L+;8|YUz=bzpi=vO!ApWYkjS2yRE_v -T-5nM#%>P=m$@aoQO!j~=5Ul7uc*od;|eq7aq>^r|om8C>p}m@0uh&M_ZFM*`{Wy^6(|eno1LI;F#_d -1b+C(`nbss54U*2RF-O#B&K@nHl3MGtd-KX&b{i-nUVG;GCmo4{haKc#6ZhlYNPLaeEn`X3A}>TXW*K -Lp^nIO+#)jY`d`ZlhcT*0`43Q6yg&gh-~VbNzN2t;dXWJu9ovE$x_P& -y*viJt3-Q#X*62=Lma)N}*|c-YRs(fbn;?IpOB{UDsof?uPpF}xVqZc!|A@5lAWOui;2V>~du6Cz=n; -t}!s&E_j6--Mzs8;&Yz#VD`92o7HSByG0)jhQ7%^bmrma7)B6jZ_f`T!RR_SG%)z*H(g($M>EwTw*5!3pXJO^RT -d&vHMCWvKOEQ#$9q*W}CZ%A>s?|r#x&8n2U-;rOZvbg9v3l5Cd5v-K-d9b|T%S{3#uq_~%Sf+Ha9qx3lOwO*1Lf)+uwk=?z1 -rHKPK>tMs`!jFG{%78dWt*fUe%)3+X5>(^r{50z@?9GUXD>rV!6@E2Y{B#J7q9O!_hcQd*+UVk-`}O{ -uFL1;ip>1Ro_ii5ch^Szdhp^=|81%d5Pd>FAfVXKE+Y9LM%beY#!a6$EX*B=#h@%6>HWOKc^I>O#Wq| -|oN@zLKWLVyBloib8R>!J-B-D9Ti3#3p6U3gE>nrzo! -dB8mcZG;_Yctt1k4bIGEdsgi<9UTLq>FaD#g)cz`>Z8*8GG^6Xvg{$P5Yx!%i2++`CPi&lNU54vW>!j4uJB^ux>xoW4~m( -f7|VoeBe{QM!v350+dl0g|7Mo=8ZFbXG -N1ZRkEkx?N6`ik&3xx5hclP*pv3I&Op -VI!2irJz@|W~6#qbA=Rf$&@hSn+g^k<5KepotQvDh(~MGUWWuP -du8ocxKQU`$yIyyy`9y~HiZX*@rBU5)d>>?!WjA;D8ef^1CuO@99sGAh6P9T_EE*!ZJ}xAWFj2w#@@V -Od;OO)r2be#ib^p$}!e#YbEgqC*jRZ2qp*)h5z4|Dy4cTgy7CS@5KMG}6wd51ql8c}{v~?8$!=_^QUm -CUA*oHp@~DPWYZDG@hEhc#d}bgmi(0-x7uAY27HCU;0ZV0Q%{lOb_}`^ZO^$gZ|U}{>k*9uk-sC04M* -zk<2c#NC=}-t+i70$PO({1;>t`9}v5Mbur^Z65`1n7Rx)Ug -m0LsGDfdo-(FVw75{GhEZ2S;O83|FyVdy;%%`+B81uE4HJ_hXHjd0}`IglSm|)jqXDFXcO0Pyh-+hCb -cab&M=%E53+C0M;hIUul**sy87IUSV)jpIK9a@)QOUIIv8e_j&j?~SkaG)_8NneovItG*%amL7BjKpk -k4!Vyxkd=7J0er*=HOCCo_Z&mZIzYJTrBkh&5)H1%5_*>(oBvOOZIF5Fs6Yye;oWN*Tp)>q>7heXZDP -(jn8G5;~3d`DtPHHZiMDfKxzEc`!}l^vQ?M>raDU;;NKmIe`_$Pw*4;=B@7kBl_tR{GHkdYs0g8zYj0^I+TLb7ecr~AWCvU{lno=EQXK^p?4?-D+)mtwCH|0uGJaqZwrtFM_YRs7YGALRI3h({2cUxY{lInBZ%q5chbi6nOCRo>In|&=@5${uEFlp|@~fqg -~}*Ew$lMh6vQcS>h%H7w4SSCZ^=upzEApiM>2V)65;sO+ -!=B{m)uLpG87FIoO0s?u$4$5&SSAc^kB%=dxIMpURW@%LLmS5^Az+>wdLpVhLv;z2sQrVbM6u -g$$Rszp7{WJYlTU5MamjF{Ix&oG0%8SLnAaT$iwYUKw0Y-xUXe0$-BVz4_#@D{%FkVZA6rh4f7|>q~) -csbvPr&{ef~)?{fUo2XkMeiLyvt8TFVG28Kp*n)i-Gc2RIZqevS=;=pGhpA=LmBqmFEsotvxhboYW6w -?p#RG%!z<$^SaUvt!Wl^{;k5pr?Ra-A8iqXz=t=M3+;2``?ksd26+8fe^F!amoN)jBA<0A?<0}onn7Z -ITFt2nm`8c~L;0sMQ93vGyKAu6-goYe90C*=e`6B-LC6QtW3a!HnAhJW=JPjt44q7nP3!H&x^Oq6=fU -Nkyi>_`V~@e}wFJQB%fa@!cS|WM7!IVzv(U-$eSa!ObW}B1V76>!lny8OeIuPG>C9NzR!V4XlPZ14Qs -+FJnNFdkT`5uG<4I1?loY!Iy6JS}f)#V*p*)6mW{OFejM|;=olw`m4t*C@Xt`}JInNMV!e1hubX1RnS -3+VZ+iFrC?+9(3LRRLC67>|PI>=iujgU&XZ?`TaSI5IQvOIDdBC<;~W7C>)fH -APl6=R@sh`j6BGTUe4?u&)a#0gh?5yddNIGyVmqKsIM^3$iLXDEPyF+g`w9yV_<4WmNl7HH=M(~deGE-l_&1Kw2*UqidL7z6G@+TJP1WRO+W%!S|N8 -sCGQ$5SPy2-faWoZA?H(=72iMS?9M31Gw_1GcqQjM@8 -z|9dAk2=@bw(7ZOQ8q|vxy9JHxs|_P!@n1%9#Gn6gS_jEEV*rP*-R%;QIh**}#2X<(*miXz_^9=NO<> -70;ixZPo<#!?v~14+p)|JIR9lPebAf(l0Wx#Kf<~1fy>uF(94#vhDKy&_6~F{db4{aSZfthyF7>=0_s -KAK@{W)?YTHx34+b9RzN7#b`pO=v>&#@wN#^>ZF7jL~F+a9c(`;2m5;JudP6e^nq%;&8s@VHlis0Hf5 -+>83p-{XydJKc5I-f7u1iBfFX9XEN$^*RKa7wvP}{^uU&93%`-Vi5{=OTVsq1l+)QS9-`>-{rIIx3a! -YC$n<&FG40+5TTzwf`Og7y|C6SM4e7xYsEQ}*pJ9XOj!Ct_&u_MwNPh{f-!D8x=aP>=&kuZc5{9Ip)` -NrN)*VBZKyj1fK!DqI!rz_3gUwz(oI+`*!;2+7EDw#-2RwIP!rLaQnLa`jz$ns#@_|3&Pwm1^ADJ8lu+s -c>i~#*pfA&}A`PV}7tec$hGJ)f3=!E+|%uX*i=_1yTLmNrk#=9PrVZcNzZP<=gU|A(4u2j5#Tg`CM>WH3E3Ug#a@cai;apTv#lW_87nyJv^;dbaZ~ABxNUVrHIJ54%^sr-GL#Vp(87UDlmIv+!4&ch1iMy;fI`;3c;2odN*$kbgb-*JGex+xV5z3)Ac -?tY5Tu6aUk;Z{V@(y#CF!pzr4OZ>9x(H?QyQ|M!|6&^MwUT&eemasS+BQkhlW1JMX!j%(GOY=1e8nZ6 -zsMqf3h$%8A@YV%PabDStoQ&gkRla9;{evX?&TkNxThv&_>eRPxIRJWlw_LTN0 -DXl9u9<7S9V3B3UQ_*{>36yx!p?5_CeXUA;ziua%ARlIl0&wOZnHJSj3ix4&{~{Oh1k<4mMw&3DMt+L -(_-)Er{7?)%^*#wr?KJ?-)^Ki_%@_;CDC>d!7{Omerywh-5>XCck&UXSvcVDJotGh0wR0;RI+|k`VZb^pp5DQj+UJl#+X-hMwyB@VtqhIR{w7(W@Nh_{xHdbF>y~*edWN -ZofT5cb=q1{M=w7E4Hs^>a<3r@~soJ_K=2za(!Awum_h*!#?8sM0`3Q@`=;ATDX!tYX&%KRN -4=v$z%bH{X`YQ1Y6n=%brXTwYCFqZg&>{Xqas=9mxJo^{tFe1jJG7QGt${=FWEdeeYx|EzX*1Dw{iG^ -v*YM^3@K^WU1%Q-c^|m@l@T!RM23CFrH~r#U$XZKSLCX9iS3>qyl+Mp=E&t$PS?0V?SrVCZKvK;CZ}3 -k0+Si2W%bZz|lCyh|UHMVd;|o0a>!_hGSNa5ORTjF$JI -2Or0boPDao6C&b1?gLH1d?k+H`pOA4{6?rPH!1BA#u&ETpY0KH-4Jr`70~9aq7hYD+-hqk#On>et#L& -&`3myS~dLgX+y&%93xjz)hADe~2UP-bIU>RcQr!lUjMZAz&oGb8Y_;CgzHy-nGCdF5gf;M*gM!ZaInZ -h~K6H%13V)Z0zG;VhZ+e_;ctx0JzZ*=bc4}^OWWkZDWbaRuKo+iV_bsdrzPB7*{oWrJ+kP7MLAM3jCl#T_b`Enji -XXJ_cfP4j+7qQO>KnU-4Y`;(hJ$rz&+7U%~KcUtiBIw7cYrS7oT*CnTGD^~jM3-BNI8)xt)V&p3)V1# -Amf?w%f%D}5JrJrKPkW|ggXI=rBCvhm%fy!V;*@b=~-eu<=bFX{d%T{C~P#5)aj@o%sl=ym#<1S&a(Q -gD9-j({{ehd{T*bkWC-H;4-!2J<0}P2qA4W*jw8vD;z?6UWPd!?Iq#hxeN>!=`$dN~7FllFAV(B%W@W6w4uWMR(k#Ygl3wDk(Yu8`YhSD%-d7RK^q#)=@Q=5SdZ&FNMP)6PAd`fQvCqI -2@cBQ_4KM>^1>hFR`H&g8W*j>*MPw(gda+-)_fJKpSNYhFaw;@P -|gAir$^`i__2*WiG$>6KrP%4>u&pjww^AkHAx(%v$*1SB+9mI8 -9EMTQ_83DcEccx9kpu9~a`Mf{BhdF{*V2F4!gK2bT+^8C0%JWhEawmvI|sh%?5vI -(=>pZ)i<^AabGe`}`W^XX=y47#kM4}e@u_`eIQe_MxmFWk+VEl;{FzO;+>(pC()_Vcz0I{jR@+r>{!U -?1-}8tnilV^RYICsIX=Vf>&a{#e_gU)JGD`4@FK`HiJM2Q2k~p6$?7kK;>gZf=uqv!jdD$`VTK#k@FE -hu2KN5n7&fnhd?n7Gt#KdQOt))WJJ_qCFFm(^Gg57+KjqGPT>EiFt~z%znF{_R^WS9oy|$k-Y%hk~Q| -c9nUr#VWN1&HeC@|4$?unk+^hoC%8etV*ao!K?(P;6}->*=R{1~_=MP)zwgk!jx$4HN8P>;8{-;R*8V -vtGc*^|G4&;PSXNk`6(9DMJ|gCR?80qT-?#BgPKLw7J~*UGRpU*=ZN|RrpIX)qHqQ<{wA&81SyP5SM% -m$z#mV9!?b514NV`bI>{tvQrDZ2li*UFrFH|f}i4~`#dO(Jt3sLCenw!Dmx|{vrhK6C;eWI3rh`;vUX -6Kw*PLo{kiHOM5j0d9KXQ8MkA#yGXFShLQ2~|CQs!5@5ur?WobC;aeDrLxO0HK63_g7`Y2gYf1D%ex@z~>Rc~76<(`~x*%KbQ&r9Sj&$ec*bJO#d5_?RBfDK$)PM5zz6Ga3mJ7jtcwyg{F6U3w -h2Bcrp&N2+^CTNA7@afsr3EP2$37vHuX<(SpOmdS@TZJ5a}bjWKqqU1*U)8*Y^cL+JZK2~1mPk~5R)^ -Qd_lNhv$p3Y72fg1`zrv1I;4|>a_+S=>5)fAV%9;v-GTwbJBE2leuc8)JrduLtzi*Zr7?iatQ<0e*wG -@AuDpd|=q!MdOi;^mwRyTMr&y*7u@^xg)2FC9OdYH3;qc!YOY@FAHzNCk -fm(Lm0y+&(`x!z3LmteYViI{bVbIxVyjo?Q*BHXqevApOs73KWRT%UhJvm9; -XXQZSnyHzvG8^>n*sB_B0yC3;@NotMhjo}(_-A@m(MJv&}=GNPRb9Zn~1TeSBPzc5o6g;$hI^K%!-ho ->AAHNr~OS)lS>g&%b1AdxR^w1V`OrA_O`A*Z0?%sShGL<4)fvzT@>cnzY2+9Pd?T<%VAW+g{8?chXsw -E}6pgePl`oW>!K^AV;t2qboDVOl5l#TE}gN)H`=x*RyF_KfT%&FtodW->ueQ)DG-xF=Mlns{EYqm2|@ -gXL*~Zo;P2`h)WHMPD;Hw`=TCHSXI7$6)Y%Q@ILP5_+OuJy$P%-p^`GJ;OwL--dIAyB_pNT8Fz|uhfw -3j~uZqHZoPEJO6%CvqE>W+M;#G8SA6oKTeq1TC%4;=Bs^q;z`+er?T(krv6k_KtS5|W8?Ase}9{*eMde2oDlvWp6fI3`?vG_k -m*HH45ulEps>XsqcMi2DU2i+7Y#;n3Z)o^CMg{KR$h(ZAZb9z6r&)eN~|!ai2gJC -o-TpNK$bR(0g4)sKjWZzj|TmFAfZg*;H*>%(z_uFfKC|$>a*11H~os^2}OXr1**X)$m5dmiuFo?bA{0 -F`pO{!;8iKLrkYn;hh?bIDnUmOplBSfR|N`l45;re*CxqT4k-PtGll}kgZ|718y@@EdF*&|&r;^l(>5 -c}M*m*g;rg>Pb_oU1d`EbKWU73|63`~HQW^eL#_Db9J$DLypFI79MgPPbtz)65zd*X4S*0^>&=%=KSi7hJ3*^gjIJo>&-yR_)ME3amFQ=0F0eRz*?9QK$5;dys#TDSd*ztzXkV=WA~szxL)W@ja~8Q^&r7$BQmD+~{b;af$iQc@oJCRHNKr_NAt|v^QzyXF7r+oio%LS_Qv> -R>@a#16WJrYT}zMY&f{IFw`$yFbyA<>Eo7HfHYlO)dA(Yj;^L=MHPDOQgc-2&e8I8@LtSxkE<2kLro%%rsoE5uSDmq`l5uZaocNW>~^CbQY3}k1CsnHOkPgBEAadIN -y8$j)Fjl=H9J;mI*dldb7EM7vAZN05-ha@f$Phn%VzH`=h(0GXSn5F1B)$7NV)Q`BN?p%|41a{-u -x>b0Kmy44nSM1C=J*kb0Kl#0(O@mA`cGN%!!yZ#!%dheEs?_J!omEVZNx|#v7j_-(P-gatGYh -eIb(Yb0PS4c`8Qu4A=IWr#7)MbhJ6F{F^Yc5t)vML#jW;Kv5OCL(HM-{yuc7+ZbPANTI0$kp`9pzF-& -naH`CcfYqRlfS(CYwWe#CD%5s4qFt4rwQxRrp7Sllx@D;3!G -?MIf{eP0#%6ma?9qMZ2(;Tf}a;I4q0~-yNj97HVneIH=G1GmqS#H%4(D7s}W(z9WUl%@Ptt0?i!)wSS -zsN`*@s$4p#u1{QA)&Lm5rhA?mnVJ-7~@~1I>>Txq0j%`)!X@3{Hq{P~*RSB7$-@G_AB(~n__dtJ`1f -ia{<6)@Adv7kG(KdRPIdRM{M-v%rw|9eWg|=AAXowb8cSEf&Y-~w2Ex_(3Z?;-HnyPeGlqti@7OK58> -|?$6dVosM=_+_@KwBUJfjQ$6NRCh%9Zb&<4s)=gFiNQg<@NJ$bDkpg__>|WpPq6_Z}v9iMu -L$sWUMbP<_3Qg108Tu6WjJkd0gD>L!?Ks?q5IIoSK7;PcmJ$Lk3~Fxj!n>a8#+B*|t~o4=d;9Ee<4hb -h_qFp6o3SGuW%GDW`u)PXtm@Tu4;8zh7;mDt1qshBwJ%_=-CmS@pTl;0Fr7F&yCrp3%7~wz+xdj@&+diBjC&}UgqK|0@rPI+M*S!fMdUK5`om@+CH}&02nM%0BZocl`$CG;t8f9X!Aop{6ql -l3s-qa0$iA8z&;rQE=e+gLVei{2&|AGD@pubgLnYgzjpKKBnrnYC(O(@NAg6t0=?g|)A4&CLG`nf2vB -v7#(m-ewbiRSua%a!HnG3-BvtXrVGv-*T)Vj4w{i?{05!{)Wldn$e{Vnm@yBf!uKG?~!_js&agj54Z7a`CU^LYwzokjydxnVSx?0c>k -&rS1Sy3H)j=UU2cv*1Y^<%wpvALz2>B3r`ca%2=Uy|E)A#J1;N$T!erj>1Ja1t?GZ+cH1(WGW#2Vmma -h&;#GyR&`ur!GLV{r8U9#?KI42OXRRj8hRYykmJXB6K|`?xXYF|J!?Vl9f|yk)BzCd)l<-)%^YFC`+@3<`YJ+y|mdBo9zOBs2*phpfoQiBOB` -1)*fT|pp>`L8YaUJ>E!vrKI%A?qfGm_j==YGsy8h%#Pt6rg8EMA{Jp=(SIS3CC*Bca(f -=B9O@xB4y)#Vz;k49cb}T^&RGP)_-1qc26iVcwQ5@irBuO;u5NmSgnpW<`2JQu0B0^fl#`o6M%~G!@s -7?jl)f>`}{kWUS#v-3zX!ct$f?eMWe+-_&M9;^I&p2Y%RgnlvCK9T7%ACHl8`rhyTKR2HLofG}daQ=9bAG^;Pd@-5~!O#qW!Z?gkI71*bhB73!m -`EGP1qkP3g|;cQwM -EkHv4WI*R0BGF_u4U)Ar(zv`Up@Lz;O8y}|EUmFL&1fexbQ-e$dzgpeq$z4TYG&pR~b0)ea%pc?)Zz< -}4R(VBXZ#iWnW14zYus%ri#wj=y97NV!7onqS!l6FhzapJMmm_wza3iVi3JqPiYps~U6W?y*hyiJ7X{ ->nN8R$eSMDW-sBW%2Op@hGRZ*~=cD=0kOZKHl}1f7>+-1>FkL$x;%t6ir5la7^=w;A&@URd6Rq@emSyLljxfc7Kor6;ew#dbkjF=h1guh=uXPo&*FCej6B1q~9dl@28}Tap@_6m)!Y$;xIx#GA-i{mIVmxs9aWRbFCWe=GKPXfgQ1Bk`o|X -mb*BQ6GCWPH*i`RLU>)@zeJ)!I-Iv(3&yG2h9!6rEblgI7N=?`?`9&zLZU+NQiBC}}N -N10zoF6S@x3eZp^Xr1|Agou!Byw>U=Fje{uQbPRK@D4+x)&()%$#zFe|5IZzwCh4}f -rQSMjtOw;6R^x#IS}`nEGSG-P_G$+?jTZ$~W#|7d_tY`f_Bh4==8qm%#34>w+h*7K0 -Odh>R-)$U!L{Wq69&jR;S4+38-hqIHi;`Pt^Mhr`NAi&`XbohbRI*p9FEO)B%^EwBr3ar%eo{2#EH|H -E_s&3yjyd|xdoMi3Yd!!SkS2)qQM48l+-PGK;G;TS~&y=nSe7MJ9$hL8e)IuQecWMKZVm5PM`vQ;qwE -b3w(m#FlkQ|#|03P8-V_L5`RTGU{GB?}YKNWNG>X6+AO&bq{qNC;>T<5fliC4Pk$!B=bt0febz3^26B -t2h?G&r`r=F5&WWmULx@S!^`D@+yKF2n+&EAc+-$9yQR`PW?%*q%_Or7YmLo208n!Azc}ryH7@EgYBO -2p>9;;@K~1Y+kC(JRq~xbEO7n*mLcsY#rR+{Ieay3^76Tn#oa5$oEflVfgbLi^F{pm9c+TGM)W=6d~X -te{4ZV1}hto_jtB#2*aV)gJ}VxV#HN6Er?rwLnkdP9xAvA29;+S#X8{%{Y#=g(+#_W;5fkm ->ke)U`p!_^*&E;u})MXxuhd@XN9}?uesbmTa-359kths0m -K@jnWC_ov&?vM1!RAZLYD-X@S7uneB5(?4R5N#nrQ2;uhV%XV>-1;(dKXC-`@+73f>C6;<3!=vCOLjK -n6Ee<&K6eK41RABFdZ@I;;rU*kd+`j0#RLQHb;1>$`kdYGQZ(CszLLGH`WEJgA;!<9Pi#%De!SJrUoy -+UL6Ky=-so^|O~bdlRoY1;XLU(-TRW%3238WrJ)PQ*!6$$`P4l`GYyd1acme(7Ya(`cisySAQB^6i$F -3dRXI;z*h+1{8AHL50N}wNsdDrhl1_Ss9rAYo_RMPqNHLYt!hW_a|9AU-Y8bq4`s42h{Fseh{)17NhH-*f3?mK%qH&U@NEoM=@RA}J0$y{Ue^1BKph!=@(T%NzZ -7c@R$&3QoR2Tz^&oEsTq|wF7d}|P?H=raAP?8i1Z0Qm~GAPhYS_0R_vMqrhiUG2*B_NDZ0CZ$lFhCUj -N|z}Ex!7eS^eV>$TCqb=gC|HJae&|;ZJZFm&?X4bc0?i&UoJZ=;{=V`A@~zPgKTcHN-ZpQdN~!gYBj{ -sZw=xV04Q&t07{dUUB=p;eDtBWOly0~LRgvn-{^sX-#bAk`$zbqS;0}i^9KwC8tPoUYglY`wu -(!JgmeH(Aqh=$W@kJ2yp -VEl3?-5(MVLBlCpciOnvX&l*y2N68j(f1_e^*h3QKsz)jjT=y{@lwi2ZqZmKhL?cRUW{5Q1Y|gaVZ|4 -45OtI^;T~{*IfYn=PQ&ndelJ-eEopwA9r~Q`CGrl$;d~$(nmrN2jd>WDp27YQs__oH5r!#uZb3tH -w&Q8^xqW&qEjfGnG002wnj~0>nD(IA!TxqtA19k2`a>N?c@>Pu{g7c*>kRHrb+YNQ*>zltA{pc1;&DllEYgCjP3(e8vkkuqxa`?DY4dI~Vb2J@8bDpLV@Yu -pKA9G`8=q1S+`oxizyWJwPHr)$MaXILE7<9~A=0vHwrxjh5l@-%qqUYuEc*6Zfg#VhOtkpUKF -2;{Ms1&jsHnRXR8gDqayQTaJYux$&oIuz-Xl2xEdyP|;Q)G!ZJ<#62jBeH&w=5;JM=UD`{A%3#f(vgp -_Yh`A~1|$mLC|g#Cs@7G9*F67`4QE1o5qyF#-(OV%V10Yq2PcNh4rjDv*`U1JHA&z^;%P=o^TMZ*5x= -0s{Fr$}M6|VPj57ozPM7e -J`NGHvQ2)s)9fSe0&iD;_27`<(Xj_Ou_I3#rVG2kx|IW5qz_wkKzr>7{&6Mlmz~`VWl7qMW -RMuVtb2p@EQ2gG^2bYB{LQj%+ -14(h%vGfO$9g5Df17u-_|Dv@<2f}UlkbTi4s3bx~)O?8#b`RNT^_p)70zHJFNw^PJWXFYsSyDq*KI1{ -Kx?Ky0=^LaNx6c@~h74^s6nJ^fE-kL>hoc12cxM7XM$zy1$M;!SWEjsL_yQ4MrJe3sUv1NSHLjej?!> -FIvZYV1c-<+A6*iLNq0cg^BZ82QG$YHJ^C+KI1njiTsty>?OlqyLiy4k5$$=g0TColdV=H6t*MlU)EAqf!Aa;?rQ)S1`p{`b9#4|k7@i1f&aJpV1LYnB-qi39q;5o_(e -*IqlQtq>o6o;x|f_WeefW=0>!wrCtK#hE6wO?4w2Nd_wGXOVK-y{PVzl)I*RS43E -HKRT3A*n7t&lbOz=Qye`{8)CqU;{(N5;f5q)JJ8Ql53z${v)6n0A}sP$SE9@jf$Y&4+w`MwkSCA<^Mg -o_pRLUV@ov*MtwDC7Pg7+#g)?N+_kPye(S)l5j)Xds&<8QLT1y4K%99+nrK6$i+_fDfQ|e&6>K1o|(W;#(y8|eU#6_Nn -^Nqh~u-UhKO__l;wzzaHF=cFL(P?i{^5RvrfZE6cpF{^@wT6^5kwSc(9Q6vo(vm6|)$;;*J1Odb_c?z -1~NEnpumQTu7Bkbisa&_a1NsvNCau84A@GlLoco0<#e<-5`j|}-ols>TPj<8)`x-B6Td-&kX&fpmB!#_Ns`mxK9YF34i%R^P3Zfs#5&5D3W_v-c*rzO6pu+!#n=EXGGa&jnD^QOG -kK}lH1j)DqZ^2*>oZ5}+9_5^o0@=?dDjf5Sp%2Pk~{EhMF;*2<*jvyX`&z(1<=_4AoQ?v;cC}%6R==- -e06XxopxFN;-ecW-U7lz>7b(HxM4<84e4a0%Fc9NII#U*DhyD8zj8jqmpEb4imQUp=?l@bo)afn^b+z -=_hSLO^K;$FU>OUNo5UPd*x_V#+wSY6A{NL-5Q5sE60&X1=~ZzDVD4xUlQVtVk7SISJ=A<$l5r;{kEp -(@f(vxa|8F)H#$hETND3?U~eTpu~e0ER83>!WPfP=dW@&E+=%ugco7A=RTd2D>$u^IZ_aaqN-P3^Dm( -pzGrbeK&dVeV~1)d>v|EecLr3o{S-pnS11$gbuGy;sc6&q7F~R$AGl&qD}s!%E$9r5H=@&Hq2W&OH`( -At_4+mC0x*-Du=s6P(7j-O5+kS>&wM1j9}xTgcS4_t+hzD7-N%R4jI)9lv_iVYHN5ZUi;cbmexK^Jj~xZ9~ycZd`gOh>2amduP-x$TQF2+gD#pQvWyeA(lusa_6tt -DTUJDyzW|%+7}Td!jS1=V*?n`yeG1UR6$uR^zBuDjeMPS@FNd$KL>zh5~#{`dc8?~b;+K%&@W^`pOD${OGy=r5|HFG@ML+2e~ -7{qsMX==IP4O#b13~+SqYynejvDL*h2SN5iFEa9%)ZqXE -vX}DLI>c?$FZlb>#oh-Fr$(pl0p|6%s3Km_>6|6 -b6wuBDCCeW_w#mDdcmT1*n)Mw=+46}#IB93YgGc{eq$5bSI}(!vIf(6e9zb%fi{-Ra4QE_d0!N9TM?P -Gn3sJFvW6S&h+w`7JTkjdmmt`e&0XcF*#y*&}wu+Q!n^mlI9XZH*GJ2&jJ`vv{%hW+jo3}^`%JM%&bwQCyV) -CRm!DZcVJNY__0*EOeL#=ckFV>jF~ofk4xTZq>~>fkb`ZkH^FG(>Ii`y)loQgFGHDSMwkjL!aHo<=81 ->&JvYPcJ@k74Oh*WW=(MaI5XqX>)2qF+D>zc409c1RdkShhr(1hGVwTYL -%lAx-J-?UWqij5)TN*c0q#72VF%c( -v%L=Z;L*vWJguc5CLi?+4f(Lz6Q1T#kh^K|7FiBhk{?_S*5LD#Ck8{3APH83H#3?9ZlR0R91es$5dEq -Um!DGmGw2?xhp8oApynm1c6pigXDCD7Sf)k19k&e51_qdOGoLoVF-qe?xd5z2 -scj;4UhKdnwk5`jK@p(yD{tVEWa=FJYWWhH)>OUEEwJC#+Cex;_->DT!LO@0d -9_zn0)@zUS=D-qQ!hsIHKMsPX>X>JIe6LM9KYKuHQo@kzSbHEpG&)3 -m+Wo073F&zf-i_`-?m-!R$)t;!&5{~}+3#Wefn-@eG`N89uI16Ka5P5L*E`K}%MPy2s|%NWj}AjwQ3G -)yuKg3_Ow(JVpy0&gj^!rnS%9g9Jm2&l2Hh0i<(^GTN=9SnuXKF>GPK!SwPpj-*2HoYaR<97l;t;!;t -05kwwGXcyhF+wSzm;D^7L3d0F1VR}D5PAV!I07kfECcePWj)y{g@OsNS}mtvA(x41CK~3E;%JeS6^sO)jzsa1QjuaVm1T=dAGCtep|&!Kjs+(NBCae^xl2E<`|a1Tr -uo-`Nvw(4Uf>bOSMsEH3+sG_MwW&>!U^auR4^Wj~3;AbA0C4do7%`=gnguQf*OnrzNlcoSrp}7hg9z^ -fKNOh?8a@R{3&o4|GdI6scPyNAsF^JvLXm5;wusXZgye6S>2!YDYbprhII@c7)U-2x0x~6$P+(Z;n@AQaY((%y`lQ<-oGS?r)hBz0PUv{re+Lg7o^Iqpo0+F#n@91G>?fur -jZ@Gun6+?<5t#2QnJKn#e+fut^cD3h9kzJw{c2gsvI=d;ksp6XmWi3lbk7vHy%o$dQ+qlLWSB>?@Zu_ -uBL3%Ye1AVA{zN?F45=p)vlW}#~kJR5E -9?rRFUYvhfyDIYgC^QbdSp(+hG}NJOS5#-p<2+tT)1-;l^Oi^}kMW$b*gDkg;Zg=~^3;!p!~X2&!}Q98LO`8IDUUp{**C?U%zMZU7EUQhPu%S`GS75=95A6O -_Bg&5iE^h@7p$O)T^Phg#^^;~a?5jMs8crC18|i7)^s%C+|4pbojC?W1g~uy>2I?U0hK{3M@x{!r -5?-#j<30g?~y&COZjtYuy--lbGOUGPZ{l}s?MaS+G -ZskTfS#}PO4ZJ_pyZdg!!#NPU4$@Py$;?DX+gs+$1hy~Pqgp5nAuqE4<$BYF_2tETJ|z4RSLo`F_>W} -yF51Jzk)E3o3JsCFJTJSed<%s8HV6*t;KU34e2Tbf$M8!g4h$~&q<*W0azFF~GO!z#-_IPnIX@ndmC) -QhYw?#KFE6Ng-rsDPuIt*T+Yy#>Zc`B?9_HGAfv!1aGi3eWC2YSs>d%zz`y;+3Z3G3w7>&UMh9DHdFa --Ij{w;%nj2l!r7|>Rgyv>VGK;b=sL3)KE)Yb2cUhinELWWNbMi%RqZ@5`$hQ(3G43w#HyUH -4srid4R)!8zL!CqJ7TUmSe~SG%QD}oBBfA;s~hBWDKK#Di0@D(Krk!98k^WYok~WGBWTw -L?uhXA{P3%aLR?hrN8EE1E|c9{s(7MkJI4lh(qb{{Zq}G{f%QE1T=3JTzs#hmj2*mz=qrYW*QPZ8Ns( -K3IcVISP7e?{Dvug)pWCOmNT;oo6V}}{&Cv~**_Qv{@v9;-;uh{R`Uzq27PNSGaY0lXH4A46a1A|LhJ -77X`9jJCfz4-r&pvmE8g^Mcevn@IBvqm07)~PzYO9k8WP^0YhgDDq^`6A^DK!-Z#=rN#csY3Th`H)K` -xI@P6VeX`PlfSNX0!A94RX{m3Fi3GRj8b%Z){RQg&WjZh7mG5?{C*;!*2(y`eD8#kNZzZ&Ypx+cbGBG -YPuHtk(#76j+FI^f|uoG=)^n$d#(DygrFjq$F6_?36jYJmIP?r~o73g6v1;QcZ~hk?e6tq1Wlm9NEXv -y2S5FB@RxT -wSTU!NM?P<3;(TKx@#&n&(_DozS>t!r+(HSh2z$7=6$O26smizsC*jOJh5JlyI?e0Y<4t3gLPV1b+}d -sLU5L2E140VrTI5F!GOk>4Cl2hc4+5S*HkEsMiDI|iq@Ju#t#e(doOXLrl`ywLKPenD&2BD@Ay2ED*j -6Cr$ldMzwKH+wiR6eb=4zA8^%F*fQQ1DSK##g^xL&MWj%7uEF@X&`_vSK5&eM^PIaHa_^Uh&s4yRS6Y -m+2v40LzG>`Bd2Q?f7Dcph(RWwDPLg`X{f8FFRF1(`@;FUBN9lv{kpLG=Cw@;V -duO+V_VB?M4_Fd4sy<*h?-oy6o2AvpY02M?EDwJP53L -`2CY`p{{nA=SmhuW8GC_C_<(Efh<6>ABkcWzFl7+6a@5v=px7GHpdwjIjw~}l?{U5=_wwMJJIL3Rq_# -!mcD$wocWVNT3KWcyYX1`E{R2<#qAa}neT91q`*;f9~%LH1U0iOw5ZK2>a@2-2Ei-!F -1#A@~%HMiK8%E9Xt8UT||gl9b;S)TC7qwt@0R8q;Em!4#U|#9AH#Pt%bZzNA$73+;aKcDfOwIpI1FG+ -FEE@Ja@L-w0)YFQjs_iWCE9_on02=61dy@ST(fEsknKP|90i -gnl(jksy~e6xq%41Tvdu^tV%1*RDsoi2FEFX^=qD8%~R{5!$~{Wa->kj>>pFhhEn!%+ZK0WyE8oxzigcQ^XX -5C^g8TJb(W5MQ+-6$hJOaI8VzY?o$Tj0NgIwFyFe4S(B9}yB8xHxzg4;{SI<4~v&4MwN|Yy_zs=j^?E0pN`PyFa(~&b@cLhV^ -ejsxH!fCz^cKyv$eAf(!QVflvBtakxkzz;;LvakjXqdz?lA)31hs%FIBYz7+LRa$iX88isAm0W^6$U6 -&3coYLWlKi>BhEh`V6%2iAPCUQAnPQXm|h3Ip`bRFt$hv%447C-0!qfLpRlk%=4Wm7%a2oHJ%$9iKm= -WTFk(PJ!BkiTXhgHMX9EnIM*!hlcFSpCr;{;Y9Sao&jfpX!h6n=|lxAxKKD^M%UkXHXP+M!i*48X}_d -=op=^kg74A9qr4!wB&T$U4jCd>J5`iqTv!-q8U#~G^&^}0ge?(l63!Y_>bzjo3?MwNWmN^e%6>=nF>* -(JI5Rl^xec@%> -t(z;zr4P(HqtCqi!gA(nsj95_3pu-8M9c0s47v@y&D@IP6I&S0xMLvJk9XABH)B#9FFJZYhork7OuuB -=X=bWt)CD}6!9sO-AaR!8o|5Zxb`&vtF1rU63x%qeNG_^QR7T>;sp~D5xg?q9?0ITu6as$akD)|3qtu -LQ<(*2kpBn2XAk;V|qXh#=_3W)clJUKyJvz;_Yxm-G ->nhU9Ud%;yKs|fy~}Z6b+m+^Sjw>Y`y9G&{AYgbqR`FdX$TXq-0}yrm11Xj^k{g|3_|^jx{}n5`D;!e -vwdKK(7rj*#-Yj^eG*MfR+Q`h3M-cm6GjVxNIpBn8o#aWptdVwcf8}h^l)|ejL{03m2B-!N1B>9#r~3 -o$T50cqG0av!px6tCN~fsQ?7TC1M_&>)SKJ0cXzK^-WFS4GELu@OZVcpFtwYOP=vPx487+=>+iypiaf -O;chGe=z)z(NUR4pcN%Oi~D$^lfgB)?QSYq^8%VZHMk@t$Wzik#p6voukn`9W%$J4;8Z8=C{}7EF*nsZ==B6O;;CUk0eWryP~nuEKV@PA#JIO?hdIRG -XLop`Mx-)XCxxM`uFQfk8?CWvacZXN5}fx5B?a5`aS%76>$jVP~MjV!Wj6MAW0NB*kBY*Q3%T5Fav)IuM0vY(b{~mPJl} -?P<#S2UeUFm5CN4dWEF{|^a69A=T?xl-C@nDU>V3@ybX+{ae#6-4a#Z^v(8#bK(2;NK#BuQo%>lmj84 -G<&>ZZypnQUWA#e!{RI^A5uoWXg+J#vSabgslky&eI3lG#fi}=> -=Ja@UWvFNd_8d3$wmkC5n$qavEc4G#q8!|RP|a|YnhiSf5l`HOdd*4W~0i?b0P6xD|FCWqPu)7^Beg5 -#*ytCA35xMAGu(J%vt=V-4O(*fA5|^Ki-tTdrzPrZ_3}jC(w^K<#+DMx2hEA7cO=8>p`Fj>Ed3Dtey$ -q;fDsRaxK<&$ajWlp|>6*>0a?j_mo@1vy+7Mw!dW5`ZQ7wXGp?rv!=bnpZ@ksD -5_TmNlQ=GsUQ$cD=ja6XlC^sE*s8%QJ)bFEibb?o=xe>~Ka(5_0P8E@Bg)g56CI7g#LQ$w^J7gu9Tl& -b&e`Y0R56YR|2U!9AZl#w8T<2JM^|gKLm^voTb8Dz13+4lUkA`JHf_AmbyS&0zLHZi>q}xomv!l0OM~MfrOPi%t3P=>YXKrDp>?)#vW^7)+T&Qq}!z?DM%Ziu2u7;%lg=Mz_06Pj&E->k`DzdSfjoN||>xDD|>Mq3vyAv&_GXGR;)egGjG~Xi~;mR*#KY2z~J4!)JEc7^6d3$#UC=M -qIXKMMCf}6Ko+}iH`v>g;=m@af>YUTEXcIoX3}hf=2d$VHP}&gG+I5Km29Pg&CHc -HLXKj7~UIR4H(=sto*aCTM4r~aJ444BEBp^Rn3VynL4vqt1Z}u}!ASMFGFJV8Gt)_S@NuuF3hF>lAmS -~?@t&A3Cl&$`HtfarDDKKb>q@I6d0nFaMlp#avkDcmO<}BZ-& -&~A~n)0eXf#k2vDhnhxVY6V|T3`79%x{DIB2cpv9Is}hFMH}H35xeNapwB?cAovs -LUX<6g;O!YlXCAmJ*w*8`Z8)37vbiy0UDpXxh+n%OADW=kcOOn6cx_UQP+mi6&Y=R!}bKYK=4Tt1+3O ->YPt9n7g6hd?iu%RkOR8y=S=w4 -mnQG>jPvS`OuJ=KJidY?si2lna-H&n+^ieO0t#4+WTB8^ox%}#(g+FtlAtj?{2O4la2~l?l(WW){B^H -=j1>y>~7Yc)y3(KuGr=xt0?o~kaBfVjxhpo!|M9B_5YIaclF1`Qr9+iXtDlQQ$?hA!kA&vgnmT!>uLB3`ZwA+0yDkJExr1rSk0w#Aji##ksY^Wbhz-dBx_ -8FDUOB&|!`cH+*uA7h+~1ru)A$FnY{MucmL`N38fiF8e8Fevy7!2pIsrGS5=tLtu_{{#71vSc9;39_2v -uL757m6R+*1jc{`6OhV~z<&a?FQb6kq!^$!G4Z~BAX&kIz!+H#4D$pOC~^u^K#24o)GsGl3Qv$Mx$%F -r2p{E6XJrRQrChg)c9@z!Y{iZc{G+YKJQoaPJTFh2ri72-7pWqPZEzwB(v*=g?L%nxfN#W;6656QawZu22oC}AH#W%?48QB -r%@vG#u%CoP)(hJ_H7 -pK^ERr70w-N6-$HU`pX1nVWD??iFEijs|m?!4<2MB(9fCw-^Yj6=;up9ymR$ne+d5{2Fvoa5vlgquqI -QtWw#Bv3K@HoQ#*%vw|X`V=TGaGpqQSR}9L1c*CV1h|-(QC31+xZ)9boHIf+0N5RGSjjWCn4;Oz#Hdn -JX$%*TK+{xT9&+1db5SFQ#YJaBVi63b-^?KW_i;~&+gi@cs|tWw#Ag>sVjLKPqgY;Jt39l6G^(hBfIN -Vw~#>8PhJ;>t$#VR_EoF0{pIo+G_$fPO*iqQ=AJq>Xq7&!-73rjIS(&}E2Zpk310^X;twyCj7UA-h$A -DUn{5)j&=Sq;h04c!T)`eHyfyW$bqIAv4rQ)!TDnqq!7CUeVz41heJIbTOd!5)U2g&$jvl`AjVK -BK6D{-4l$}tJ&tScdYAN%ul*uy+kiGQPMW<{qDANs$Igui*<_gMJHLEl+7QNZ1v1cPBP1SSxIz)=iEm -XMrYf^m|eXoC6Frgy=t90y%30Bvv}U|isF32zY+MAr*E0;q{V$0+EF`P}awivYw?s|Is{E?_zcuhXIN -b)F!y7RNJykW2!6@Rv|MU%P6MpQ$o|10uXO(BNRdB_v<)ZGpi!2G#xrQc)O8c?RAaFi;YJ7FS?5xQ-f -3fXEa8DG7@A%cU*Qx~wc+u{c_;?$^#u!wQCf6y}#$s_gcaTV5NJ;SMT&sBYcoh56mT4u*fY$uFoTyq@cSryO-UeJaJfcb2a328QgS36Q15{{JDt5uF-V-tS|9q37-y^3hRWgL1P -GyP!V21FV|OSF(->(J~`u9PGCiGD&3<)`nnxI2S$){ysvxh` -n5SUslnt)`bA@p2_SCFxDXmmbWS)V|ktmM3&7;@6#cs)^I)a@s=*TQO -T9-Nm1!D47&5Qb&fK5-6v2q2;Fjc#c>S>+sO^OP0G03t#p^!B(Hzj-X@P+U43oowiZD8*_WFq$+l{eL -C@f-;K#5;gf#pQ>T|0@k^~v>1ydIc%U9cZX3c6ZQC>;_dwDon -?Go3Ph6Ut+)GY<}ywp!^d=ua(m68Ci^EYGq{2vhpveXNtXKQ4OARwO&V#OF}*r+vuov#-B2$&KKj-nF -)*)RdZPGl{$gOv8KT_O|^5shEl7_8$}T<*Ts;Ou>&R5-%cadY@tslE8o#urwy!Z?*+9<`K3JYip`Fb+O;Tp>Ivv%r;*uhhf7*qGKp3(|tV2NT%rNAeOS$llAa*|!3xWhvsA9 -s_@BV@#_J`#Q?(hc%=m9AV@D*&+>KxH!tqV|lYQ%RRUly!#>d%f7h>f1O4DUF{sGlENadmzZAF_JBcd -BE}x;1Tfid$1K{Nj!$JnJ=M9uDt)HqX5h=6$A|snPF=lhOeJZWGlZ{QgxlU0z(nPnoWc&}`_ajjNEqR -#(Kfb`NqBs89LGtKXLn%F$4CjDRYNNpS!o*fDyAs7okJS3h`Z-g+0)dN4I-ro)x -H$V(J*Rv4@LMm6+2N&1un58w~4D9kfByhn2PTK89bYukv&(Uk^TxUH0mLKlY_|9ptCv3FTg_4!4t|3C -+zW?<}&D=bLldK*DizQR4o%+zY)kZ}3rzNjPPGK5OCU?D32CFncWb`0y^wi@?4#- -A5K*Oj#t^f&(tw$p5L9S)~C)EnLmA3pl^kk%g(R(FUstPuFTqP5R+*_Jh1YqoYVkv(!D~6`-SUmwcFr -d9~tB2yyS%W#S=2=&IqJYV|!aKbn#fEQ%v)Ry)3FXj^y||J3uPWa3!H6j_A{L5@*j#*c)`g>I?cG_t@ -oSMbr|IF -f2EBmr5&|bMs6;QQiKJ_W6#Q5%XQ8+-@1%DF^z;;HhrcbNR9FKu>BhaQNKw(betH& -b@0_QaUN0R*n;1L2q^+F1kxOh4562<~6s`W4Q>URlK!0!@)SDoML{n#Pq~j0DwHbqA0czaPT!S2Q?>W=N2wKuhDqn*vMq`+ZX3y -x%Sgjg?|iZ^iCEAN{-y~hJGV?tEy&B^0PXov){2HqhMaT-9Ur^HF^be`uH|XoA3bG`?Xv3eNP~6Lw|9#{P#UPK2GWP^C@9$Xr_50v1+tZcQ5%c$2OYghy!mlO+P<=gI9!TE -jtBUndUTE1!xKIt6wSW;OPfh}r6p~)>Aedb1TkL3LRk>6R5H{!^c(tF -2jXk=BEDGe*z30;+0UBh{+{xE7izsp#(#pN>;7M)s4uQxzCCvDe)l6Ty3}7x_6NBB|NO%~p!{!r&@UY -u7f@XAFCtcHIts9vTE~Mg@h+8vbTCP+j?L)WTSk5g)c~7uP+Vowb@E#Ru$f%P%u(5gZzBxUTwq)ezSe -VrbM!y5pF#C<0J0_qz%-v|WdZ8}%a&zT%Rhh%Nhcfa~kV ->ho*-)g?l|y2f8!BJ`_k{M98wzc0WNo-ySxS++ayyqxV>o71vQ5Bbw{tZ&of^1%3sC!~foka7vGz0D! -7^zxEk7ye6DBKm47p_8uW^>(VR-4wC4LhLIS66!6Eln$)~3g(H-3%ds?TdBOPnrS!wbUr;4uv(F -o#dR?tl4dVt?;yhk!}8<|w-y>_~D7hAn^szY1MyC-+@y&-Ts^wlLTjz0yx8Za -`;}iP+fM!=>n%Sz{qpw2g{|}gnyELLR5~Y$Rt4|n;15+h^u0gzcNu`Tv1<#>%5y^~Ta3Ge56jxX7h@M -KkNy4a1Qo=o^371?OYv6+++?mK9j^T3odEsL7v`_NFVOFNVgBm-0{!|6)2?1pKP#0(tX_L}*RdB`r|w -_`!?$IWy%KXi?WoogcDWpct`U3f9_@$Gj0hKNl>7ORgqe(FMhb01K|3fO=3yPPF)0`^Pgs+MaD0a8{< -5&iQ5866nw*!r;WlR04&L(1ZB@A{^O@|H72wV(A`CpcUq_p}*S2u52vbRRLIp367GgS0byHsDBZBd6k -3P0Z6e-)yDcMrgefBR*$I@tDX0JJm -3{?6|dfO>|W%bcG!E?Mj+!p2?wI@bD1g5<^V>=xWjOzurbZ2m-&eb{c`jj+Hd%T(fLS=Z$^AmlwS>;U -QgP)EY-~1Z}*YC-Bk>~&Q^1$Ce$qyxhzc|6Sqyt9?ibiRSCMlRENicna29571vWng*2BTr-Q>8nwa9m -v|DI5?BY#kCoWI$tvNAK$Og&QClr~;rrF*{n1#xp<`me3wgLHM7o1D3FLfCK@r?e8S0tkc<=npiUqKh ->BatHOR^N>l_=5eN=C>KJOBM3I87LC~w0uS&JGaXwx<^T6jVAB+Je6VqTF=*p9jbuh)UriBlI!>L~}6 -TSq@q}Y5-A}DY++**}QB+fmua@L~za96QAt|%6^Qtc0*=r_N;m7ss6Lh$C6^k1kD09DV|iAw4p6#~hE -jpCDTwojN!F&bcM;9Hg8Lx{d?Jc{_YLi8Wsyq{en^s8(9)g?l|y2f8!BJ|@mrWI>(Me_dZQaK@omi01Mf9)=iPOKxRm3JE*O5jQp6%K{_APco9A_g*lq?w1(w7jsS# -F*$xTU7V<(U13{SNev`1<=tbu_Q6z%^_oveanC+3kg8Z1;NLRlcP>Sz_zm<|edIZdLLUxia$ZJP%I13 -)ojWcWpi+J` -T4$dIzyEetWqu1(NC#u<$bz{J?BN|p{o(!l7rTADVb@*0eTkwtfxswD;S}|$92ZVNOq8ae+zF&e5ins -(XwXg*uR@XGR+L28D1I@q-kX-HLq8Pk3fm28dHUi_31ms?q*L%FW+YxV~+;w6&7zKcW$P_d{uVa5O^p`;#3- -YbcuX9nQy)8+iW_{`jR|M{NJ{u5QhmK*bE=z!4sp1&Q?*p(Wp<2ET>yF8h^SFh?HS;%x#+^8(7Ibu&q -Yc}8!@@`5qjzQWL<=#G&QaVO?D$bwl$_;jrNiQ+&g}Uk%!ELk$y=f?$<1XUY;1z2bzBl->=2h2^H9AF -KaA#HU7<9+hjPE|22QD`ZhkSEX5&v57or?WWZw&~X-UEGL%U}UR~%7K(6d1 -$x9fzi%+RwZiiQY17F4q4Ynr|ry$@aBZXkVMA`QsCE!)>Arn=SV~LTQ#N4v+iGU;cAVD7lg-KD5MH`$ -#<}>1zcjLc`sk>j-4RyPK>)QdKvl|BQ6(qaB6BB+SpNti`f+$oTn6-8lK$}`N9qQ5Zl|Xg>&kaBC@rY -Q|Pd8?Z<}^36_lM@n=RP_xEJ&TcXyk-W2JO;;3;r+Ek;8Pgx11Qj^XG -O`^14o6*Vc%)BPJ|rB6NHX391-2l%@W~kuh$_hu7}8^)e90o7j(2_?-kq&i0`{-t6lF09TOgOlV-dt9 -gx6n$2r=}ox1JP?sUIe=ONuUg{2;|*Elp-vd4+Tn4{5h6P@TN*&Asz)y677Q}uAR&YP2qiI;Ss`aAi8 -<2_!WV|wpRf=-`y*@ -bU-ELD -bsd%dA~3hKzeZYXl?mjn{a>w|JzaU+Bmb;YxCt2sMS}0^_$IOz$kuc(*?qo_M3$2=XK8Fh -oa%m6~qQI_^%H3i3@we5r)4`Om(pa2z?%)4tRXw_vHZ>^-8=MeDPtKafaSAD}ZeSsqiB{dCzYm4#_Lu -X1rPpKD|RKW(dJ(uEX&_Ys{N@>vtOgqy|5<$v6!=X-BsDXp>PSQ(A)tReJA}kxT+a9Pw!h3}E`Hi<4{ -f__Ty<*N^#rs`r+{S=&iPpc+}C(fAv!k?jRhd!^CbMlqTpo%v>!pCyhjJdFB#U#Tf;pyNqcQCvvOyTgFx&ED4? -g59Ef~C{c!Mfcf$Q&v!G2f -X6ucedqQim7cxbG_v3Iiy1b -;a%VVqf(8G7Ivftw}y}$-JwWX-+$-`Wu6RCDj_~LSRehHoBEXTz&#-A+ZV2_4$lEbd+oGgTH1X7+4p* -=dYrcS3|r)Rxp?-Bbc%j`h6mEoxM8S%I0=0Z28y4achW%s~vPnrZrh#^tyMqTAs+rGB%k{I5hu2BIJ#ka)B_=WUNw!G&a|CMaa>=btGymMc8L}VlHxOP -lJ)WY^$bM3k2Iw^LdCeufW$vQJ9v2XP$AD;n_tm|!h6j|Q?@oq;!Y~QxuU7v0I4mxdoeRO|H;QjAB&I -b(s<`_SMAWc#TO;Z$2Aqa*c1OqcLOv400FbG1U7=hCm@}){>K@@<9$l6<$0>ze~Ad>`!Sp>dzibDa#L -=li~`$Y#Df&pe-0Rk8zD4lt07DKGYlM6=D0NoZ+yztfK{}2X6Hp?zy;5z=Rx|!8o9bH8|Ks}7FwqPVM -q5!rSt1y^Gz-;0IhrqR*T6rwFmd>nA;Xulg23QS5N+g(}1bmeOt=7}ux|s_!7|{FXTY}GYkZIgn4;kM -HX)4?5&PkhJSy>e2pS{FI;VmBn@Q6d={Q`yu|6Ki$8S5Oe(qEBWPf&1*uQAVlcUW-5LgU#FDD@qHAkSlNe46<~PJ=A*AN18t3u^c4y*0ap?Bssv@uQ76osOSg -j>^VM69xSu+OMYr*E-3RyjlA2|gpqI0z$4#JN+!MZ#YMK(7qaCGu&%FHYGd~Ht{D$-lgmaj6xJu!`u -yuH6w1_x3^Pfvwp}*o6-YwMcQ3iA;w-&ZHn?9E|O1Qt}H*o0vHAcujYS)2- -pOaa0o14kXs$JdT`dH5J5>g?rS2$J7OKCK%GN&9>Hkj$l3(memu5;50h0^|oID$f&E)!bx_R3EvI&w` -MYo0uo{aqjj7oBG~BBmP`(<~-i_~w-%mtDn+V@^Y_W3P8S*PB$IY4WYrka^d{GJbGsJK8?A`z@-^^1< -gh^5K+onK@F*k|8x5@msMw2T-niCQF4yt{RPgzgk8n=aTq3SyC|{)7ps@d$w*nsm^#TTTTy8G!yTi1u -+#z*7pl&SK=%_PPKj&FV||cV`-F)+JvT;sB(i*RmT76zxP2NE$P5RWvD -JmRJPP9r$Uk)iujr!NeEyclcLa6vt&Ka+#?H^4E{m}e>8Fy2Si^e=>&z?!u}>sizV}^S>_;SABokzK6p9_Oq4><-Ten-W&tvv>N0AW8S+nB= -G%yqG<6#`^f$H42g2$O3%_Z275p7wM}7Xj3<3)^S^1PJ&OOZ%-zIU^TAG}XPgWjDYuERmT(hB%Qvqk< -$kxIHMOWjL4+K8^cy@qtFIbp;%$`Xua)U0|{sqb#70K;g><0SGzkcN|`IWW@`a#^&Ppsf&=a+7lq`=eC{19EF;s}?h8yld7M)gh=m_13WhFaA2PwQL -;O9Ax8^_%a)`(Fqw%|Yuq~KdK1m9^PbslYn(MNzyx@f9?T`1rZ{?KVQP~*X$zK*BZ(x*$+linF1bPS6-e@kzGOpo@HZ}%UKC$HyJmx1`OV#tRb>t#t -{%$fi$^^l}*++xsH`!@S9|fij4;0#!A`S`?vLG@cV7O3mXnx3$c>O`mB(%%PquB?#fCun`CSTmMiRXS -L7>LaoHdOpLgIuOJoJJM{WHa`sE7CDMkZ#14nd++k0?n>m@|%3N9OE%LdD}#w<94 -?)_8;aHJai4!{m%1zh`$6|P|U6#_?G_B4P6hk<>ua>T|1hp{}q?11gTr6uc*Be3BMEDu-?VR;&ySpcy -+jXh=TOH*X!qgpGA*dYty<>#JdVY2+#zkI-;ub!|_3AnNutIK)apaaADpPmC)H;z9JX?sT*ei;91t$g -DKVEHn%bq?Y(pFS5%dpts5FF)>N9axJ|JG$QiSf68i{M1$QCofO|E -fr*xBDb6_=6AG<;g*!R6qTOVSoOsJ5}+_E#yfXly|!yfxE3x_KsxVPzxF;?FTga0W^j||8~utF)L#3o -XVd;1z-yF0V+1?!waZb@5S~&v!R%+w|o;^9bx95kxP-f_lYP?%mQ=B0%>_9lFUB4r_03&HTNmPFA+2M -!prGr?0OEWSm35lBvXMr=hsKwm8|>C^6-amOA$xd=kakTur~C;xOwykXak(^oqfKM$@7+$uNhoZOJ~RU&F6j{6hNZ+7Bxzh!e~N3)S@i0a#Vue -%|(*{KqFPe){nC%JqV35}huw@a5$y-=YrLT2c`oin48Jr6&OaTqd%w+$eg%s>>*z7~e -PL&O_r$hXhC>ZY*jq6+@irzxnQPcF9a)+W|BTD3Y0txEeB|hl!Z)M_=L -He?spE`fmuEL-WQN~>p6{sQiNq^3{celf*d3m;4 -8pYE(E)dS7w3JfDB@9tCoCpz06@GCh2?yvN~HLXv_hIAm=!y -v_bb)t;gqOgZ^n=%Bq!7ELju!&>-`!bq!7k5n|RYn!k@}?63^uZ|Jd%?|+W8p?@HwSAwIwNBUsaWw*( -4s_PHb{cMO0W$olkA{8m;)+ta#Zl4nV79PVKolF@)19~`*65`9;B@2Qsf?Qbt~23BbYd78V8(!7F;O>7ZaVK7I?fwkik7G7-RI -_U%D2)O_;A`#b7zK$&qR5=)=xr6QUpUB9eaisB&(DfJ=TLw4p0x9&A^B3196A8e0h3t^|_-J$*=A4 -a7efC5tU6&_C(vDY>mzDfq6Zou(vsIhPpKe=rqZ+=r6HPCBG5^J;&+hTu1!r#Bc9UI6OZ$j(_;e(vAg -?gyh+GQJ(zaBz00{CvAF>iiemQ>w@9iXd7jf`O+b54tNcX>V#~=9k>z)4O)=qz^Phac7@!; -D%c>_i65K-b!I6HB$>#nTZiedJx~b%h+tC~7`q{>-6;`+NwH;5prPB^iERN;3$_Oaef2!9<`^hu56mI=x1%zN!=q< -SM_z#}+uTUz2Y_kNuZZRZHmnb7Z~jN{k?lyM6@xjwN|WEbC<;xtS+f$@#LZ{PSf%;5z;@m$7P -9{xg>Wt=e=!`DYx+A3UicxYFHr2hoM{fTl&mB*NK??K=fpqU*KW!XavxQo}VjwPuy8<0I<1PWGA+uu} -hYk;v#M=c(pAWa``FmAWsZ$laF6A!_=oqE2qRSzzAU${9J0WV3Z0w&ymwJo9Ziu&3aKpxKOQ2W0Db$dS@qJ%W%cs`C;&S1-O@AShr52N~Cw@g)3{czds76*U*F{(`!&5Hl*tvNg*TR#*dJmWrAv@i -`H50&Na1--+NSmr4n0YS3FSdgmAbE?K}E8Imt~3H`fR<@2?#aq{o2WoCsr=H?p5k`yGvd*@)ZD%r{cZ -KZCwzlz1#}%tXxrJ3C%kcN}xN%1}QBXjt90Kek}`RDL!NSQS2Bo`Z~OR-CW>4jt5PddAxPJ#{7W0xJP -*TySys3$@6X)-Y>dzFM;Uhf`O?)IP*55e#_ecyuQ2j~pD+T!y15MP8aFqc*3k9iND%F%&_v;iJ1j{XQ -o+Rw*XS++qvF)q$X~jEqc@J>1 -kZqV|je?H$>l4@WZ&`twKM1YMbras)0{1Q$B~;sr*Jb)DT(h^2#tr<$}Xq8ohM)z7zFFTa;1$s_POzs -=GqiImpg*^D{>Wq>EYn3<#>J8EqU|sUH%{P$(#bZJMj11 -G&HH`*1wopCb^hN=B1pv_i=m~*%G6Zu-7{0vyRkM+`oFGntkITX<6J}M6tYSpq;l=MA6GIS3Ur0`S5I4U=!JC!<`VbR*xARs;{i93Ao1ORgY1y}}sF#u$-3FwNdFAD9I+eaY##{N)5xafEuT({4`rz^XJvJeb%jMN2c$JwD3U(_ -Nujga=L$j|pj+>Jpr7%mq-I;S|>zC&wxqzrj}U$%c{R -$&@LYRDWWX8w4a*$Z^G-S%dFb2zItMJoOoH6ip#e2?;SrzhRiDdHq~ -v^zUMc|rU%2ci|k@LqD<|71$KI(Rc>41VNw7H>6Af!@u4Vr4Lw$1`Aehl`R72sx`m<|zJ2tEuB1C3lB -2cm6830=>JIo~YqkUU!yJ`{{n8w827v-0Q&B3`aY6Dk*a>zYQhdqufb7B**=H*##R}E1}bULBaIMC}n -5BW+4YN+&FH=xY)&@T1Gl9KsgWeYpWR}% -^k{%#ciB3ChnC-{5(FFNQH&TSSq-DgNqooqiDW5J&;Xw_F_IG8$P_f!m`|Tw;}Vh-5&01c)C^NDZWG| -cZSOobUGj>&DtAcl;!wAv#QfM?A1UqG{XCrh(>woz!ZaAvSXOEJf3Wk5y^Z(~ -}_5p{#UH2y)1i0C@-GpBoa=(Q3rSpXmRPONfD_8vzG8vn1u|3k7cjX{%76r$0XK3Cq) -b5ybnOuidVQy$OpshLa(S_U|WHD#u9HtnaeUa&-&z#=jLZ?mCbuCBC-~-9-iCy -K(fOiY3CLDy~=Q_G<(1&@R85T6Z!iRMVy#Mc-V-x&~aapHtNVx$nKoQ~=^gTw -Jt?$GB?NH-D#$l$v-nMm_-UE8!#{QCw$5NS40@jqQw8oO1hV(^(Hy(j+{*I6un466hhq3pNPMqY7>3; -UxC3N=C#JvbIrZ)~%72B$Z$(ep3dZ~j#+k_CwtJ+mSXc-;;?rm^V=(D7XbexZOQSkG4PqXY={?`EGGt -GF$h&%{_{4qEU4c1qx00R*sE*qlJxjgy;BN5PD=nKtMJZ1;X771RY2-bnqL -y0J&K!`ZWXr2ug)=_i!{=AMbxj_#XtjL(SEpWq -(J@Bz6&E2_p5~ -#x9$Thka`Je&qxFU9_P3EP%t}l`qEU~1N*rO1OhUe9{eum)4y^0~*JN#U*4p4&o9LX}9uzlnxOm#;C7 -qDMBUoBVjZGQX03S0`2JEyr^n@0a!jAtcI!5!kPGL+zaphjcsgP=6VX=KZBk=T5EnB0 -R$Ot{r0nP73JzZwtKZU3$=+x6ZrHiR0T|a+_u{?!G!}ofVK>Z+=ucOfWzA9J>EU3m#Yp?_J-b|1VMN5pcMD?7t>$HOm@wHD=b&m`CGa>Dc+%2Ck;;(F3mp?QrR&6@llr8CcAMYn4;SN`esV~x~-Op_kw!`)Alb0Gg4*tz+k3!KbG0D_$9%f3@ZN95UhWpdyLB -}WLh@PB{;K$_^)lHt4rR4G2(=5{q)e+jC`>QKkUdxVt>RlPrH)}U5Giw?=&1${Z5PRdJvwJ^<&$zLBw -lasFCQdwGRQ|zlq3o7oWQmcR=g{Bn!ZnaEhHDXYkzsi5q{WB=*L@H^bS*9$@u;7l;iIf?Ji5lnoos8Z+&Z=v59Y)h=3MsH-GP2lG;k1V&s=WP -z8~@peOJ!P=lRB;AnWaYYw?U?-SYnFEQ%c!iZ(e3mJroVp`v%lSo>Ub9bcv$pS=jT~B?zvs4$;fT_Jrk7H9-wdQGyj<%DhbyYA_=o!M|4#*CDG+8=!5?xlH%b489sI|CuY>77{x$lKf9?L`U;p^ -qANIfh+vDH=t@#66B0Xn$T@_RE2UpZZ(*)ky3_BjlYJI8AV45QYJ)av|0uAYRzXpsTaJMrp -gyqtdkgNPn9GFXMw*eYTG7a06D+TMYAm+d!SAWr<+v5;r&?sK?mLzv?qNVv{CI}OMq -O5QZ-sLxBE~J9BW5-$@@oeu4_fy-=I*c`v~h!VG8;!xk_4ojFm#1hkm=lSMU9nl889?A6%rt7}jPrPvgsp|9a>}D?NP0*WOuFoTEt!UC0g7EOqx0~i(2m8ZhP29Ny -G4kVqb4|DV*lA|d1_}Wi;}2F5EVRUyYBD@|pyW3n_e^&=w_1b4`Hb`qJKNaP2nAl$Vp&1

^ME8S4Z9H&)n*NrY+{| -q14hl}l>@RmbZq|9FN7%gG;pNh}sF8Oq<}9bb?-v?#h|hZj+`Pj=X+G`=wA_@@7C7nS4j&P(i_bd98} -7nohim$$sUPceBK8Y?;ouM*UqMr?&k6M7gJPYCdHGi0fg0z58o-e}%k4SK`GE@#Ltvx3KDOU(KJ<3?c -XP6GI;38%uTGk3v`bF;&?A_hBedc{>~F}fp~-kd90a8;z@Ih2Jsnap!YTZo+^ElE#%tqyqeCFX` -0yr`Z!>B&{Qmb@>aBI%h*oVWBT2``fDp_2+3l+}>k$Ziee9v%({Htwbub9m&@CKtT3aGLXTbjXphZLO -Uk^b(SX!G=cYyO=xO*?`i0orZ@7S8lp_qY*8V?wcUYtMusY^Gsvz`;flERATPtG?MIcT|h-nllhquA(nL) -I^yt6gfXlyS*6PUghSHC^nxKrq-q?r*E|SZosH>b317AClh^6H?y4jK2!dBc*-ps1zq9u{c~G>1WjLU^;`4hkOt|uB@{?u&@E_zd{CSmuj-5lOj=PSkR?*9x5qCm4wsl -=Sp~Rht%{8_0#ZmS1crtPy4o?*s{|wlJp*G5OtCEsV{o)zH9|^P$HWLsqM>WbafyR~8y5_$mZ^0H5a> -B!5Xt}}X9kp2gBd#p$VSitryQZ-%wh~_T=#)*uG -xA-v^I0+ICp}V$5R{iQk%6h8fAwKKk#5D?vdooEzqxlzJ25RLMwpqo*lo>v$4QpYtTw}Fjm>6_<7l2& -!0>dor?b9NY|f^WO`a}IN7W@msy(?OtauY8s6%U1%b2p_*@T{vpy@L-mSj{Wo=LNJIfE~r`EYjM|1G` -!t~o_r0-1Em39!8dFj<98+l5L&MXXFOUKbC>Vi2p>_7_-HOV9Rak^EGEh}}qne|u -O1e?5-@WM -DbKh(rLCsCdKFE*dS&d(>p2`W?KZe*ILn4)BcB3~I=X(%TzJ$4B^u$yoYkj{mLZl52>J#FkUMCPb_3+ -SB_QQ39(vERFqoRL6Q@>8+VZPpBvr^A{_UQQb10OGyeB>xe?2Y_;0tX6 -F|8M4LWwX9i*zgfnIY``GU}>f~>()~E*i$*7>i1#dG#$*ytarCaeS85C^G^ef#;xpX+?JYi>-f}L6pL -lI-Mb(H-2mqAA4q#wEXZ_@$Bx&>hW|KqrD-yiC)68#@R -kRVWkA`ylG<{&VN5Y*QeS`qN9MAv=-Q3%A0_}a9GUP*OG0jG~30Sq5Ter0D#M{8+w08#+}$3kG#vHXv -Nt^x(7)O=zCUUTzF-* -(&UewzF#kQ=uXL@{1Z7JvP>zUU&t}}H#w#PA>YZ#aJNOX-QbNSNOr)v*T>AxPuf -AltZ6FeSOnoFD$txK&%}Y2_(;90H*r(o9n>FtsmU@S!_-G%Sof%FYu{p?YPN~HO-2#y_)PlSc^jvX=XKt9G1vA~v2GJXC?&tJ^E@J9k~$1 -uqN$sZk8zb+wW1yDkX6}*nckr>(rt}}4{kS2HmV^VEX0v^8^{r)=|a?b^o;KQ -zO|o5jo|IH-0o{|@AMNo^1)Hj?Y@1UdUBFuvLNUtI@F>uKW1WvHkE>Q_IbybVxuE|`@Cz1G!C&MLfUH ->k#gf7nLFq0cuzYBntrP!?PZ!{kaH7%gAM4=2F&@TTgByhJtE%xVmfjx%w2ukEBETDtJ$&k2+?Gu>ve -Y`}e$XfV6e1BcB{|#So6-f_9|;4G^Ee{{ul)EZo-c2t<^JE(L&>evb(GLWB!GY=+bH6>m_)NV!A>UjeB`B!f%eaX(1NWEYLf_m5o1Irt8&2wzmT -bI<7|sqkHL1(Pa0`xD6q(EE3iW;SMC*r;y*3tZLPF+` -QqF1H*4u1H;%vAF71@a1)$_fR^|F>Q*+NB1xY@5)b=b1hf_x_&J4MChI9KKlsqtoa)HNMfm5bPo-T_X -q8`1IGN=6HD?9qG0S-4*9QN_&qi=802AkX$~^5x9~L3$`J5=r<#SI}=7lNt_QSqomR~X_a&u8-9HNr)REcyjcBf2@jaCUtttAZJ3tpqw-ef1Bbk07EanRgp>xd)e(;mjAkfwEI+gSMQHMc(JJBke4 -9Pap78aO3~N2=%cK}T%7=R`rd-^%&nNC$8!So%?YLi)uiUA&&m>6y;-ek&d6D~ -_dahNaQ`JX6Zvy&L^92&>%l?h(Qjg6)IjA0AG%Dcy(S=caVaJT!N65v7CL*^)OJw7Z2dPK#EGG$eVV=UQsk%Vic>Ynl*Jb<(5wbENgQYelJcbP%-p%wh@UkP -oN;dcHe2(>Pl5KR5#h*JlM3-*@9B5VG7`(Q`{Iwk($5aVVU8TxA!#%EgTm -6IV#Rkr&J#;9~^hQ3%M9b90(V85s>X3PF&Eh-CD8@aT9dXV{ -Mf=Tml-Li!u`tI$7NIto)>LNWv}tq+e=Ug-PqM0dX+gMjNn|R>DM6~dIzbwZW*jKSVIy}B_>`p0oj8yRw2zT|KWS0#h%zSxuyo^@t89%+JlelFrxftuNeA6 -#B-#C}>I$3)p>HI1A8lSyr4LZCZG~JIoO?^!4H7%m_BISR(a_Q+^bzb!dgP*^xwmmNEBRF9|-^b|y?1wyTZe%vv9&T6_fx8f!t>?T-#(e)RtX`c@PIFAPU~Zvgn -V@Y}NT&qw@xKOdheh;s)!;*gGX3cjpxCi`kO{5{eHJmc}Vd9({jwG36C3tV -e&OC(*36ytQ#9(E*FKdof@5!#pN -|`A6w7=admT7rMIu^OU#ERJY!9$YVqDkW-Ga)=)j^VV3@rdSE)68ae7_rygLQ3h*v*@6`HufdRdsTmu -&BiV7vJYoE^rGwx<%5laFtd))4RKS%E4DoR~)hbTQV%7OUWK6BUWaya*?e -jo$9=*esKUB@wO0dip!`73sYm5fVb)rp!V&gm!cy#u6+l3H0uy++EO3q#dH0{~#UAbu6V{n1%w;q#o5#m+1Ikx9>+3uE$o>S-(hKb`u>lgKK} -a_@vnbY&x*tj`x=W;2nVr1oIBc?qF=$D|b3FksyBQ=2c<3-c)OqGns=Y+UKggHLK^oOiXmNAcT2&4{K -M#lPIVVISOrHtCW-r0Xkrv`y0XfIzUfTm4>M@tJ4*#VtN?<)`I;{Oyk~Hvg%hk@>Q6Fq!PAl3@yTIYJP?0G1&-{#qnPrE7N*zM1g2YIFloHN< -!>t&L1=ybR+7PKU(c)xUvy#kxj5f$cEjaF)Dh}t7RsOt0eFTd@A;DzgCMkVdKp{@Ri2=I%`lk1u57qBYn17V -&+)ylwMN)=J(v`5LC{R4`ZPe7GlvO$ZY22T(cIfwagWi$-KF-U_eKX$f5%;hqH!(4xweYmws8CAS6#_ -@f*5d;2XqJ}d?szWEq7L0uJt2wx()H*;a9+X7Tcb8DCK{lfK}6;5M4m3FyycHc#js*|#9m>DnMv2wMsu4q+ -E)eBJ*&ks-UjIfdGQ-YSjM^{?;-OfmT_AOPw7(7s7wqA0+KcyWr>hzgw4gftS>;f=rx{mu -2}?_>wlA^<+TQmiQ7uunU|g)>TLojHDN^`zQKIFZx~@55vQ~?ih3Fjb0bH6;wI%rY7hUE~Z`{P)Z71(W4ljo*Y=enV=rao-z=WX~|DEKt^>(dbGjUn8Z*HD)j!pkuDepzr^emZ!0p#B -M=&|5bSp}t$Pj~T+FyP~lqY?}jjFR9?hj&ns9QnWoCp4a6og#=xEoX2Q`&*XOCyAAKZHihbM`n_`A@`js*$n!>@wBgxV -R{X-vs5!;Uojqff9c=6p;*jf(n&SODNoi3H`$!6I`%Zj_Er4=P5g)SDfIxy}=$ -$H9kO(Yj^GOgDSCU(<41h+N_^aN?3U2Ul$5V$@R?~=J+U@;4%HOE9&3=kbsE$+xkiytp$nijTPuWe*W -Zdf9$T0KZ3W-=Oq5)xj@*pt*1{M>;H@6e?aX&b?l#OC21INJc?u(lqN_VLokNMVVXc;hM@?WCJ5$BU= -9h8p#wT;66l4HwIu^VgLzpB201r$^`4G#&;bel+HxAkfJQor*Hyy5C)DZ=9Rb-9@z!i11QWhmKV#lEI5sjpd>Sj!E1bNa7eET -t{4HNF~)xj%-gk9Veq|HG8msV*G!4qb4-ye!%=pW=g(l(y)}Jkgg-ZE|JjQ&u0!ObkiULjCVVs;7W=T -GgtYHn5O78O)5<`2D}|VnUA`3^-fExc5P<#*sCQ5Tv`~Wi;ZM_BsQ3ETph@1}!qaX_>|YmMLTlCKDma -hkcfl3D>~L5MEx#EXXxI`S450Ut?okC?lbcVEHBMs@LuBQFbIDb+0}y+nH|0-06WaUb!L*P_l6Z)-N&xDAwd -L0zN*iQsk?smxKjMO&_Nyea7?QN}9GE-6`(8$8d*Hskt|k`+)I^oe#xvdxG}O*j-q&c4}{vCYx<7mPtQiaiqYz^TJ`8?SZ -GB4{L$Z{!(VKmxVhPjxB|2>11RNkkF{lxIS&qSh6`*73a?AWpC=}es_&~{kbRGw(cJqb);^ZD(<%2eY -}^QY%-Y}r%S{IzBFFCH{bZs!r4_V!D#KlWDQ6j}LU-g|&chHJR=|XG8emJsYxwL;YwSk -KUD|lD1%467QNMeWCrvJ!kdpwuzsCGBHbJC*Mu-EIx9TJ~@sN)3vTU95lNmN3*mz^7gFnL+N(E4j7u} -5t9<-^|Yr3BB||MRg2^(D)=*1!X?C|0zr;zp)z%-dSOgAUeHZ0>hfhUIJ~~AUWi36C*JuZrl0)teF@W -1h2l)@d=^JPh!z?in6j8WBmrViIa9h)UUMb4_hyTY4D0$Wb|_EDUb?^f=SY?;=;b=y`LnQ#L2bW)2Vtz&yA5Np-T8hGcvOPATUFqZ_! -x;9r;*Jy%MTC968pa#7W8>A^vO41{P2ZafDri2y?y#$K4UQT$4?*p)ezu+_pv_U{vRCYM{uW*1;8;BC -MlFg2^@jp4XnM05U{;{~+A?;5R?reXTD^cU -(Bm*#jrM>&EA{s<{5wVi1y+iKV1fVN~#6Z2^eIK5gO!nm#Bk6R-Xn6;5%@pO~L> -4D%@JgHpup3;25AZD+Nsxfxe3dc?5z1+a_<;UE19sWEUd}P>+iwm&hf-Wn^YX#IU#g_tR!2Kch_VH|@))z~!#@FRdx0&!0h@$<9F1E-`MSTH_r -woP8L}0>%5U_3)C6~UZ`r=```Hkd*ucpqZFULWSw&ppr%Z$A&Z4Jr-`YKD;H;q^Px}Ye*Fl`Rp7G3EtSSAA8||gagTn)ZD1Gyt&Zhy@yVnuL!!uDWPs0e34>98r{bc>@``ZDSn=AQ -qO5&-ZMw4)CEXNxu!XoJOs~WKD440l?xbZn+}j6Rt<16&x7Y1`ilhx9V%rwGC-`GWH+QtnvwF@Mxow_ -03EhI*b3@(dNaNZ@5^`9!7--x&;Z;njL}bqTL%Pw_EG)$ZVOl0LcM-I1#!W! -$p0!@suM%hjR$d02L$*?N7PBbPZu?KwNiqdO~zZsW*xgw8&uJ|3{6x~9HIo>u}bBJmYpjuSDlbyAk3= ->Ah?rN15!u`dSRpUKfOTE2=X>HZLkSJ5p?w%>@@|6&mMKXWd>2Z^6+b5UxE6M=%4AW0fQ8D{PIw}1nQ -QVaK6a092PFDc+MvLYdHIbFtAIsk6p3vZ<1pI022T*3alJ2mlOTATfGV>;HZ8>i3=;8dpKyxhYdHqe>iLtbMq?-`1q_i{XJ5A>(aL%MGAevPJ -nn8UEbHx#EHS#zeVV>{0oQy2(t|G+5a@ed?=Lv8pOMbbv(WfUhyiB}e -S?_qef~=p$Yn*!KNReX@>rpq;~h9+r#k{qPtg|$X(dB -aRX&hw|U9alO`{1Q`%)9>_$V-3*xAce%p-q)2X~&US5f^{SiKB_okZ${&+Zua!lx38oKy%5m8D}TRTD -8ZN)xk&LMlzV95(Kl&!2lOC;BznP&9)VCp4}VdgP#;jEn$`4L{a111;U1 -kwW@O%~XD`emYGh4c9s$tYuJyY{(GbUvBPjvl -=*M&st<*_Wg)4+svfJjt?VTGs8xV88<4=Egx8WR-6nu@=?oTAm-Be6ff=0y8%u6>(;g1&2`1liqLN}v -8TxA#E!cyzfpzLIWt2{C*b1gZz8s{=LWT)rKse%_w}<-Jf_k126;Jp+XxGHOT05(^|DtU&+Ovgj>3Z2 -;@vl;HXy>oF}P`C`G`JQ>MKR@?lx@WOQAQyVCNva3k`A+C9&p2WHL!aHwTyfwyXQy -YYKa66?j_|_jiy0aIWgE`jwVBJI9nQkDj*cl3^-xEbT0kqi8+Z3t(k58OY4TaZSe!_rv4mhK-^GP8n% -3{hogKHy9KfjMSZJw)azVw=-jPkUoE{IV<>@AfcjIx#u+g*Lm~-AA3qDpJEO)a{ojHcoV|qLvelu7U> -CGsGXF60kGET#T( -u5-DiQ0S1f`gni&^^H~aS-*TF&8{J1xhH20UDz0)tigF+}hWNmZs`8D^1B>y(@YyhVyy$?>YUZ-J;y|eg0H9KMD;Q}Ru%g>Q#{b}U+4^qk?qYQmXDP#X8r3^C4(0?VR3 -^K}4`W~{qrIaz<7RR^h@<&Q}Wi#T6a6hM%zYc8qmnmg{{LmVf{_B+T2QB)yDP=&HAid>VEY|9ysmcD8jwzsrWab!K --Uqm(A-BFYsHn0^*o*<5U{=A%BwCuI;3UxcJi}*Ps8AP~*Ds90RQ(0Hsbz9HpNk;vfG%NI3p7nbZI5# -`EiRotTwSB@un2p>TQ*T7~?CR`YhIsz%wXaVWSTtsJ+2D2A^1Lv*^9iZ-!y~+?<){#fZ9dD{sz%=5cI -|m7BdwBc=xI>(Eg2_8+vkrdkJD694!JrWui;SN%$6B?0T)g-FKvpOzKd>f+$0mUxUVvHQuhyhqN2-P4zWa{NWkD3pM|X -ZS5U}{^7R1!y!<{Bq*H1aG1sk8evuxA~A|UNgRhUghEOBb1PT?ImyfV2>}5ho`6~LYqUtmuXN>WPCbG -H4HC#QelBpuUdq!oV#NrcHCn-fLP5vsy4d<}NPyJjdMEZa$DRUa4-xgsZ~jtugaN!-@dJ4!QP(BbNES -F#rC?*}%btOJrAU(u2#Qve0_9KwXq(mumwFjO(60et3=CQ=qL-$21u)>E_4l!D4+O5H{B@>1*N6cxuf -duq2+dH5MI$LcX_3CfwuAjiY`d`T2Nqp{>gQ^M33(qO+*dDi4HBoo1D&T1dcCce$oFgUF*uvpdZ}!5` -#v7ur9zANZP@mD9`yFaFOl;FKuR#g7p{1})~2;vsme=(G(`18eA}5S&Ws0rb9OhXuRt#Ka<=+MX0$&B -yuXNdnNQ-~Go*9Qd%$~iYj+Oa!y`cP_i%Jlz8Dn%@Rq)LL*FNnK&nVw6D3)<)K15e#XpeV`lfa){rsD -F2RrDm(C)oW`2}{!w`X_z@Vx&2cKB+1{~LC=pT@w@XF>3UirBdkxx=BmAd?quks>U4y+msMSS6`Zs|S -*}=D|^2B#7zVs)*L0DjT{Xx`m4avz?xIF?m+Pv+OEMMYolq+>ZHiQz83_SF*h)pY+SF$Xic=6qM96iS -|@U65%s98Y?h5dUI2+5hCk(l(N+~%&TTVAq-|o&=8$Il3mCbZxXZUBUYbNFVs6(R2gQp!( -jN0deGE(F1Y3h|5_Now-CmU*ZjLYg7%F9oucenA1dQ%|_gg3`#cN#sq>AdjHUdyl6^z-_u9NyJY&`$PSE$*VEZm;n4lp7U2hV< -!_%Z+1kc?+de)$6?Dsp!YB*ET|vLA$*^dCTkLTLV972942a2vE8Y&};p!O@rPY^ONo4VTozxOqx6PcS -XPX3LM0W{Mn`K%ak%}C~>G|Pqii|6tOcgpPqXAbY+Abi)XDu`t@_+Z|~Kno`q6OlOuXs>fws&56^7RD -c$Uz8>mfIR|EC_JLcwHGdvPxrXZAweasS5?lxE6B4KPd(=dxgP1ubc#=izd0@tJ}bMOaC>Y` -w>AeO5)QiW_-{a-DA6(T8<@oQ0<0&<(^*%7`RGLio#w*!wZx1ZVud*$z+ -nIV^C}B?eA*>39|%r?A|dMmbmm=;ncjhR3Li30+O&n8m1jINYofp?~WKm=_v1KB2HABbIm`(()b6N{||ET=GTP%aq6S%CH|d=75^6mhWamatN#V}`4 -iFl7km7cb&(i?V_+B!P9p?Hz$8f$7=kb~LBPn$!blv3iBEmBun@S^uhc4iCEP*uAHD|GWUs7$2G|es3 -Z_vo8uU}vwLX%(JnmNtL}$SA3PS*4qZ80s#3X=Y#VIpG5z2`C0V;4;U99T+qK^Tx+!a!PKCC#JC)1G{!HCYni^KZJZFjEWp@h4V@9co*w@EO?B -Zf$OHt~#EZWyA1za07##A(z`hKHTk=u2Mf`0joI`l!+ZzbpKj^>m27dbz{?!`@-aqKS@&U=a@Zz|#ZRM-syy29avn}7 -lw)BZ+q&IBXnX1fBlx*SS&h$$tR7^M)?t}J)jwFD>pI;RIZNv^1ERoFIqM5jb-VfOkP4}QG!}>=}Rh> -obN<6_=($x}?jDYmW_t}x0)n$j*Wxo)(y!NF#o+1Gc;rom}+)cBx*fyyU;S?K5!Jmxg5YPuBIxb$#R4 -f)nV!QW?JcJ~37laRLK-_v(T(zyw%3+#snLIy=6jMxWII)BQlh*UzJ(!EU@8Di!{ -{f`0P^R7Emy7mo{^Ngp10(r-(*N_ncj4_n|L^uc|Icpt&;R2FauDx_@3Za&jP`d2RF^RM&;P4h -m-)2N{@;Fp>Hpd1euv(Fa^CL~?F5daVDcM6BA-gRVglsOSIo;YkTOqRb4ZgnhiepQs8*O{QqOI%}H)v$k(;<)680j2Yh+?AMrn4Y$Q<@XZQ*WvL&j$1;)#oV3Rxx -?P*lld6$AG%8uh;91l7KI_kk_-`ZDwV7Z*-Q6_X*7jfW -~mQg#0MtbbR7hrY`{@MaoQgcg~$aL?@WN^m`x%B_@3A28Pn=br0@_dd?|Q>p4o$q0=NT_rlqq^=tl?o -{v@dYhQtpoanf5N2WPZVlYPd_>mLr#o(yhrjq_M8yX=QqA$4Gz-KAY`GQj@p-0-9eTP^oI@JKrV}1lcaKuz~CB$YMDDWZAqlsUic6}mS+3(GhVjJ3l_MI -iBo%-?Th(*e?IAKs!bVEwPeheCAAK{=TI?`Y>W_ZW3N!cvo~)j+4Q81TuV$)-=3KYKgb$=LR&T#;MqsVw<8Y}k`!+#*&6jb;0bc86DDv#{2;HFz*m75Xw$Rs0=2+;V!{sx6Q92mU -&8)Z%PW9dekBfDrz#3-SAIOBJK{3`W{9_*3@FxO0#2bbWJ~sm3|0IsyQ+HA!&~~hU<;=KL$=ExCpm@f%o@)h!?kdtd!b?d=3%=qu@Yn~MtLLu6>0oJDl^R!)}z>nHSsv&H+v}MT9(GIm1E^Lq;?mY^fOBvq}UCP{)%;dv -#I{JqyGWsO&SjT&4`aT@}r)<-~;4iINCN>k8)bDmHugGk5Vc7kVq|F-YuK!-cTaw;>lvsay&<_y*Us~ -uLum66L?=*HW`18^=|17nRK^h-^4OAtf*EosfWmyEjTq<$UQ@lpNI0|r*{=DG5LLg8)qoCZtAV9npQ$ -XJg2B}iO$0B~2Dy>l~0LTbHck+sZkzY;00OgAKWu}3@CX17xNV7UawtCOH$L1usJkvW5cH}xT+6WQIxD}Nf&1PTjW_@jL^WD8=dbK|=Qr0Za2 -;h&8>klh)izrpPq3ia!4uUqZ7H{Q)BclHX51yBiB!CB2$I9+ddt?v<4;Nd^YwC1lJPWpU!p=LfEkoy@ -DKzkFo(+2WseFC#vy&t4~b~R2)$;Ka+((VEFJqcE2a|QUz8Y6n<8eC3ARGZhR6dRqd-huw|S1$<&?V# -_)+`y#jSItQ3TQgD)nvvEwTXzEjU3uHHB($66;mr=$h|?{YpIuT?4k4=Axj~ftxDi=PZcb<~%ElxI&% -(HhPxwNzTxZbM1L}`Xc5%Af;7KaA#VlOrRU)chOAES8bvTy%=wj9>z7l7?)j1>IrH5_eHdpIZ9Me%(xYLvO{iLUbL4+Q9W-w=C^H`%;{0d6_lv2lLn`2`qlSo-I%#qpYLK^HYk_wEdAi|7p*p73taeF`NXmFUN(>Zf45!y&%5#$n -bo&zte9}5o}lySxP#X`G>yK|A=!tDwzvfzn$Vo{!+MV`xyh2tlBQ)IRAC>70@iUY&L*F9~mF{@%WlZ_ -?q{-N?V+^?_t0Lk?^*yES5jz&LwZ2Qa$=u?9_y{a6SARkj*8UlvjoHyK^N~lX4SFLxxSkI1beEBp(gi -8`Q-H;i=%crLb%PSWM`P@{FcG0`z#-#4^X)1LOw-Flc;7QrRczV?C;*%*oe}Ks6df~bV_C?(lG^bv=W --*Y^ZX3i>ca}jvP;`nf)%J8v>kRQ$Iu^<6K6OKyOj8y2P}YSVlq -_nb3OB|2>Tx%LlFC*Rz6xnCYD>JX9;M<)Fg)6Qdk^;K#6d3L517k!>N3cbTH)t_SG-4G=Y;xP}5??Qj -*s|JC4qYoAW`*x;#uG9M$)2xF6oml0-V4+k~5(Gt%;$LgG&HZj{?m1-PvsXtd?DcH;HI -#kdB=^Cq}TNi(@UKx_u*he1@1VPY55UN4<R2CYg8MXcIVyQRZp(#KcX%Q4#se_p6MP7pm}^;3o@w3*y8S3OnMxyQTL;EGbzh -?+PdOx##%-lJv!sD(Zqc=F?uc3B)QqM|D>|B@+jfbUhUFqV@Q -y%Pf?JDpeax;SjkJ40<|IhN|B&1Bk-Yl^GiFLW91Vq45?|DqQB5OuaG-R2k; -@e4g|;S`sfJ9jlOwU4kF9soiRmJx13a|pm&^mF_vwo-`(M^C7okwT8PSmTwEgG_2}p5_)8G<52JBI%`6u>vgiy^UE3>%lE?IoLu08D5v)iNGi_kPTeK@DO?Xep$W^67<>{%nezhl -AbnteMvr+m{@wtOGTQCJ9WwL0M~r-iIJ<*qiNUn-e^xTQ4e)qtmPtgz7<3-xMD$QZ7`HNX368KG8@zo -c`WWqHss0cotjrA<%4@ -i2A&@a8*Bp6SX@x?6O}mYk7uT%oTR -b@6%PJ|E?Ayq`{6i04+UL%4mqkmc#ZKQNy0a<=IVmmlZMUZF>?O_n`I1TGK!-gDMp?&?k|_1By3qDbKi-T=J4$jJE?3=D!b?yS{J!yR~0&khR{2d~|KPLSB7otF*29_6O+vy=S}w>6fFw#Z -iV~5E{W?`ZGKY! -X3JJ1fC1@@#}@EyRLtcco -7ZgOO6)+v0~j^B)%Xkvj4u-3dE|?bSUwKW1>=5VLQ9Uv)K+OIojpA6P}B~^3A8s2hRBf!yJtx_sf&xa -GjpjHaT*TH3Sz4>oVK8_prXv)D3ZBE0Q8dYCo7oNvdpIrea*$SiVKNjI8bJ8UbwUGp+M^wLmQQuphHh -9L2%Qr@m`Yo8OodS;~QGdwwRf&E?dz8Hajy5mjNU-NelZo6KZMHb -<#OdyujgAQ~*I_$&HWu26=pOX!d71(>CB!%v^%Hc8a_Q8`%QeRQIp6XInnfLP%GJBt4}*K(t4CtAyGJ -?;q5YUTJZcUl_GnwSx*&L~LFlx&-X(~HSa=eiVBEGif-UqHtCb_qhT}?&FfQ -p>q$ovJ!1r2T~=oj$hnLp+U`;ClH<<~EhA}Cp0M5;Y+`T~IAA@w&GIlm=8EcvKah_loQZ2-G0)dE0bQvAPk)!VfFyQ}39Gau)Ow -IxdT;Dw|AcM_@{DeD?(KK4j>dXz2-(k$g+31cq;y04RC#kgw3jI@`d8_Af%r92-qmU79BqNK=;m;~n6 -@(CMRN_k#cR;$C02NDY&I>mGI744ky96s=)RwlZNJH(Dz4EAW$0Y8+i5ID!KbLaAD_NbJPu`KPQkoFp -v%rX^O|gTXVvqm)m8(6ny-aRV%c}cjb4W>DBPm){qt;r# -QmLR~kGo<)w~z_NBHGvPKu0KioI*A&+S-Jr+0U;`DX5R|$z0q=yJ(j#+y-KQobS)MJywPOAfM24-P2* -k7S-5;zKeSP29X0!JHkjfg+5t^ -mD6_?kfu1M;$Rj06R+k0-zzCwftam5QvCVU4pR^hI+(spi)c$g)?@A(enId}TpvfDgaS&tn)Ad}0zPG -hpy00cLRO9oqw| -%=-cglx0~PWx`)6;s5#Mi*I#S4x|R3FE~?X&EwLMcHwdxK_Y8HhEXC4L^u7|Aj5?pjn}Y>1!af!QD&3 -}V -^IqAMip)Q)yTbt)laNe#w&UNm5!XbZS4fB&rnI@T5M<0t$RQI$MYONL{Y_RZ)xQvv>x?HhI|mqn=a>ZQluuGp|mhYkyg -HR=&11UATeTR>03G0!MW-;wE%Bg5QI^*&cyt$4SRo&9kq{9N(>Qt%g`&tPNv3)m2<$UW_;OTk>z+;4f -P7DcrbXBzE>emC_zfj%UP3pE?}a+=zWgZuRQ_R&WvAzx7Hw!e^GJ5>uwS=m!lF^GCx?pMn1yE?uh`(U -><=ap}Vsd3rHYCVVcb{9%^J;h1fIBC%-yOKq%2EO|{3W={Abie3aA{6dr+z?uwUR2h=(8s93Q(WNc=A -7E*P>4CderF@1G{qYsjf~2rg?Qrk$*zUO!mfNDZN(j*77|a#PM -Y-I~Fm*-^d*||e9HC5?rwy5TofQvPvg>gUyTInyDzJUoYNUA&77Dps?HCU;hbaW%ig`Z1n-rge=OMa4 -Y&`_tKYS&y{j-8ulOS`2T*Hk1GDpOZ@$)X9`HvLCA*!-PwBCrD2d=%}@| -erZ~V7U>Ab|wK)2Drg)7dWBgAh14{8V#>_CF%nRRUWu!na{-yyCx+w=^j_Fr!@$d%Avyk8OY}D?uMhK^7?_zuUqV8AjUUV8yt{r;Tfe%@3>ehqzgW%Kpz|&t4}yapg -amZn&EP)h+dFkB|H)|j8NRU>gn!E46Dh<#=W%9EqT*6713F&zAG0Y3hk1-Ij9iL?P=c6m${JaqYLTD8mw%!-hb4xzih -Txpg(i6KzmcqI+{2dckiV3aW4;hmycAbbIfBxiun-UW;=f{kh8}S&Ojs^4yLV@Z=7u_qC9pEOk^)_o7 -0hEr@D0eH58ZOkvHjwp$5)89ax`D@3#v?>#^X)>#OT8H@!L4waCj;4By22prDcIDv -Al<(Ey`fg;;7DP1%!fjHka>BU%ty4d)wk*Q~PVt86-SNW&Pwgd={KL27jedpr0Dx{(+bHTY_TG7e_9r -wP)ffvmA57J)xrNGTC{Jio}W9AGT>i$Z9An2~I2g?m0hnZxEKf&tp>!?V~WtEZ4G}V`KAp6WtzTGxJy{iF7Sa+QlWxLPC{O{=e$7!$rfJ1+G?8i8aZ;$)VrV~aPoWU3hjNhXWguy=r -p7mNxzsapo;3Kj|0|fE%ElI%->y;e@G_M*aivFrZnZf|D*60F#MJgEyv}Ys@QmQ~{h5^%*6_XGeOwuD ->Ds1`%7WA(=(t(BdOTMWeXpHB?_2q|JAvM}^3zUKmH#D{a(yIN{T&gPtcx6tgA$-NIdn?pZqO -ykn$c&u~gq!9s+x4AYOfzHN7FykY!f3zBNjw*%YIYF8pDs7k1eA5p<@QTdx`cVx -EVT?u|n!UFK-ypKy@XTRaSR27C(mW0OUdVqCY@J;zTJaogIK$CxM$>58o#kCYqj2WNXS*ly&a!dAVcJ -U!6;F}N8H`z+Rt;1UyI=(DKNPCDE6wBJq7!Em4~jpv@*&`2g72ai6pjX`?5VG6e^Q=2Bki}- -C26~a(v~K;4#Qn}?0XS8s}O2sC?@7n%iCO$9Rtmttx6$4W{&h9&ZcjhBH_Y`_iFDF0NJ0jAr8vU% -E^J!L~Jrjq{DZi0CBjC%EL8WBc#$@eW2Lf}jj>v-HlA=^aOsIN3k5x53n0XVk7PWE2FognkblNeArg* -nRCs8?zP9+ImpKqn7?O(}FbMiGW8V~mj*(3UUx|w^HJoi}G6ZEM)+mCt3eYoIO+gMR_j!F4>JvkhK9j -^9%GCmC`iytK9kLFbD=Wf?^$#M^$4r3^{7hWqw-Tb=`6*z&SqP(IwHgMBIvVR0Uh8B;p+wpw~l!uDojqf<)34jH-nNaF5UeeXLOsgym3YBg1?swD))+GuB`9^T^<2tRp$pK-K -3%J1)^8(jpmHXfJ+=M5V)lu9F<-<1l^uDIn_O}Qyw`*LEqK=MZc{93EM0i)bYr9PCHH6@8vyDbO-A)b -%jmu;&Qs--7V&*rdi2f$a*KQlDd>tSou9SKe}Mk6-{L%SJ!1M -g`~+1u?8NY8{K61sA&-iEzkLy3A=p&3-JmZC6>gZibmsB^4Q0=TM4#`1n6!&y2LPTo`xYBs?uC$q#Jr -Kj1Q2kR4YL>{PXp+KS)Yv8ckicDSue0`%ykVbbdUG>&NMac#3$fj4@|!p{eM0L2RtYvx6F#zbE13Rf> -h*ck?y%)-SW!sG)Iw^ut|fv8BS--|_Om?p@^{q8o8O)~PeLZFW&Agn7;Fo(Io&JKvVKESpt?Y7L5!e!ipo)&huWEPp%07ocd=_S)#-7rE$59m6Ax(m$ct`{Czhb59;E>huVL)#+e&qG>_&&2fygZ?-!QWY -l{3V3|lE$sEZX-uiUIY4$g2YWmMlo-A>uzlGI*|`oezP`R(ibotFpw)$9A6mk0gT>-(LT2PO7D7)_gQWX!wISqvfd4s; -Ncy{Cg}UQJg}LEdeKunnn$V-3DhyJ`*1jWQo_%7p+N1fxM1b_^Rn$?D2{F+b)xRseOqZ0IzJUpLAA6X -?BP^94;fbVAdC4zEM_-y8W`2nt8MFjVPDzgjIR_*WvyBg6vIpL`wIN-K4|S?o8JsvA>*3VH`!*CoV&* -E*~fH4Zr$WMd~bs_vsjlkucFid@6)|KT&zn6YVa8liXhnxX!m@k3gHo`JhF)z- -@k+e)!C*2Yi&jj%>3vfsQhxI_amlM=btW$hT*`Z(2AMew-`%^;qbWgQGWICW?~4TDi$RqBc7*TOKo@6i8!qP>Hwa;TXw*RWbOF`c;vf<%Gd;mm0@vR6&&NE- -!)0YUJa%}V`g?CEc-CCaB2C+#7qYXuJ?b|KPuf(jpd{&Y{AxIDbdUF;;WNdaycdRpFCsFF!nWJ6QgY2 -WMY_!!ZCg>6dpWE2nBDNKr6>aGX3<>MciO%2H*u$?@?vqy6OH9o7f(l+_Y*(mhC_C|XY+n5NyWxtnuD -UaxW$Ad6+`H$M&%XV`2|L2N1OF7A^kM6&;>iq``fN&mdF$BbF^HB3@hzB35UtL+tCi_aw0lN(GQ6E#1 -Srx(6ay+)HNDsU%hDOhx5b)A+vVZ{Kk8~?}B^1N8$CADL;PqALrZueeHiUu^(-0|IGp)KD3`7@!j-L8 -Y3teLs#e}7>ZzUlmrvZUQT+S^~5JH?>qcu&j%AP9~T1T&sR7GnTc0pI+lW=q3Gv5@ySbt_GZNg()ch) -XRN5Wu8k9*)*K_CRE(0~R7CVAO}yZp{4@O3?6Np|6_ekxBNQm~qA&Y;G6eX&?r=rhb)j`RkT`jjd(jj -iJxu-i)&U5noP~c@d?YAj-RXlLOJ;W)$oh^O0`6_86bXP>u0U!|m!{F(!53Y}WQGZvXzKFPPf8*OyzTF3X+*xwEZn -2~C;ryN`k<46tZ~7+X{?Q$0>0gxL(1-l{+m}#&m4>hQ{KI44Lmu>(n5U1|f%bn^)A7+&43_9f%Gcr(Xs?HR~|HG)c7{xP440~KbV{_s6Bw;_wyBFh20vQILA6p)NkjkU2HilO2a?;JT-mt7tF0V&E$dh -QFCEJZD9XGuy-sr|*UF#t7x+ezExw)KI5{-4J6{gJ^PMN2nMm0+CmfV#q?`fREsIP?zlhCQIrM&ol#u -xM{w*3MJzw5(Nx1)t^?vPeFRZ}6xrap!jhPq&8PycGj7i(~uaTr)<-)v@f85kmPAk(R-YtHVBa9%%{p -}c3~Wv+)h(Q1=JQCXV9PJxgd_=87`yIiXZ6BEB -hFL$#a^jutM|@eFVx+J4ZA*pY_4r(N{2n4RE-~-3i=7|-p1Ti9LLFEk;mQpnMoD7tcgcoEV^Jy2=gT8 -pgBe2Kk)|$Vhb0+|GgQZFmgZEc8JCVP#&)~Rjceei>bzc9>BHHIs6x9Sw66|=;fl6OU`J-zHCI-?*J< -2B`b3k*`D_)Ha6Hbloa)y?n=F#*IyYiAB9%KkJA&`1q8(v$X9OnIRF729lX~gmhXQ#5ll^|azVGhMB? -*&1lJslwHwMLD>C)8Uqq%1(n5Pn6|KL^W50*qp42Mbj$6Wq@VV#dW|G!=1I}0@!r%(z<843ksSTLIY) -TNffUX!?R&_%QcXi@ZMh=B?~Nd6h4pcoCNS<&==X1JV9otz1oL$yq*Jv?%jp|zCIsDZ&4&WsO -$^A9*gW(r6y+qjAul&+r9CVn2f!8!x3(3F_Yb=?(MAhp;>kkPH>Hy4Zd?+wdN5M$06-`$ZUiX1Tpg{_ -Q!OuWrjRU7NFlYw;GyU7PC0PR3-R=BQ6L2;oMGgIINuL(pM&}!2S_lY>OjnnpwN& -Gpr@2I1}erZ`3uD~fAn6XyA2l~OC{JR-2KKiPi%i(#~t>NMvp~3Yv|78WS6_ob -M*rKqNj-mJx%KYprr|99rxF|KaxX9(V>dq3ZtXFAUIvZKwTlI?3G-Mxe;yjInTwE>~{}bG#?MoO^43n -ctUMYM7KsX`O(t0j!DScS-*)mY8W~chb`lr@e{`|CFd@WfiQ>Bs`*i~Lnjirlh$_!_z};8q#bg+dQw} -VP%`Ez32>WcGNdtT=lKZ9SELuZD*r4)pE@n1r0V)%+U}|{*&?{u5wS(!0uFEM3_jJ732#kyL5K@@tDvMyY~RBRu~_TV?|L#0)HTR>-$&W9(5W>|EkxToyf`i -8)I_(Za&^0_C%dP5ZR8VPx``nU>ASnC=S=$<^)r?y8UsHSU!F+?4kDn?jvT&1g_92b)v@W@SqBdo%71 -8*vNOAh-^mu}}uy~k^q6j|}S<7X(?8ou<<$*q|A@cH8D=7!vlQ+5vO&d0Cej^|MHuw|YwS*y=ya_;@x -<%Zn%;puj1$`BHx_N)bUBF?7F(B})f+Z;1GJtZ;{wtX=(;nMFozSCAX+RkWb-XvwHw5%VMh*7pSv{&i -La|e4i_H`#|`{r;xZ3J?W&9lx(UpT(FGkRkr$c93+T5RNO_V!YdwBM&eM#IQ5 -%+D-TZY49oR^>02dKFWqO)q*|GqN}q1Q4>(*lg`uMYqu|l@3q@?@ -PNQo<%f&Pn^znVbT3@|UE6xe8A^q-4*RBU4-9UAfQUU{-&FE1Ejkdn=iI=`IFOw8tW2{>{&xR?Ww9@h -7eL$YkKFa8mVe3TWswEV@+-Y%b(Wo2A^wGuVZHEj33ETyFczQJUn7ZVJIyQD=a4-7V&CSBYt7xc6o#v -wV4F$MCIacIYw6GOalY!e?2N+}I&$JO*D3}IYl5zsW4F~$-hC5P`}1PrcAn;!VY@@UVKe#e#1m6-^pN -fLS(y}4>CJ%deCx7}vQc%8>*vqR5VRnB)bipFj)T*IJ#5cpNtRGLmmSN_@vsB2gd7H$PQ$FtoPK -VaJNKpi|TK2X;B<&bD}Ix^0*TiLp~WmIkF0MlbxoT~khv-SyAhzhb&d&WquZJmYP7uc=Sr0c!4XgVn2 -=Z#f-%EHmI(>E1+(WTaW46G!yKF`9Jrg-!cge5~lK6hxY92h1T#YyxI1XaZsf_fs@*^C<{O&t4X+^~y -_b_}+w>65SD$vbbCv8zKtERA`T<6BXm+QAnNVmddc_QJJoikXR`NKD-)dU)^dW*b_xO5dvCo^AWULN^ -keLsn;I1clsF^gK%@WgVYYM-%y%$o$kZ!hrz88Dwq%hHf#Kg -Q3n?45^pE&`R&_VldI))b&wqp0X0SnNUW9~VMndoJrJC9oOG;Gj~|FQnMfZX70?r|$HqCq*@J5^wnav -_swBrkv`M-X7`yi0}0u$Nx|R@o*CVu>J@8W9{KD4tpnzUk~}Ny$+!W5`#$sXJCwGP#hr`g1|_UCK(37 -;guJDDic|$Uj&0ZJxu@#mV}^20Hho^;D#6j_!OD}o<&lBQlC@8^>K;@d3=-rVn;A&l>!w8l6dLpkyoD -L)mhHOAlrcuKscB(AA=Bn9$Y{GHW$aBE0B3nzx6pV3UVz8^D_3008a#|m*{02_{zkfuOHx7Z$AD~xic -@7BKB&>TPbaVzbQ`sRwhD&^fXg{Sm?4Ek9q6o=1gsidFyG&bgQ#Z2N$q7^g}9Mau(6~f*ZhJNVD9BWM -4T+mGm{t{Vt86^VOAv0*@F2>s879>OWhG3&{5$1r>kupZ#(lXs%mS#4B0+#~Q~!KJ)vDA#i3u?S7Nl? -O?3g3lrHs@aQhp?h|TbH8SnpbWLs>Q5y7SlU`FpK3(F|p1!&hgQc5K9$>U$L{%S -c!Z`TudhcXu#RGP>hAlvL(AGchk3YH7^UlXw{++2F^?SQ**HIzsK%o7R&bJnn1#)E*aGx;oM$yPf$HFGzm$Fin -!ptKG4v#9^^->$SBjOR;gqNW;1Zd`6dOQo!ack@}*SJ~xalm7%J+-A)Rg-eDR?Hb3W$&4m&_Sl)j!?Qxrt>Ta!W9UdO?{B!Za@=-_KBAn;5Z3Y#HSOWp>NTug3SvLmsY?P3Um~dhMa0W>_$$D! -Osk*LZ-?;wqcx%2HBv(Ru%JZGQN2fn=dzIsO`7fyUTH$JU5$)ZE<~2kc7i8a&=YM6okbX-Q=FkzP0l* -i0WRPF`cI+*$I;SUB#>B<4y#RuUS>B5?6qgE0gsKj3w-+4qQRG; -B?-ZG=(g%ON;4S90Y4(H5JS67-rS46(97VdV!8uQnbJll7-+XJ`0evHZ1fmhl=o=v<(Fo8R)pOK~6() -C&)*~b1yZ5hJHOTOE1cnKhZ0~dIoV~ktMRVHNXPO^FO=Pv2n+nHv*7gri&Uc1UH!{)bu7me6o5w|TmX -_{Z98}uX_-C;R`utvOx`OF?ANtOncCmb10wL6}L>x^Z#oSl_3v@7?q$&r@~xpbkPTXt`f?Mb%+Dq5q2h&?KJun) -|M5>FGKD)-0!5HH;Bo*ys)>-89NYk$0m_hGDkN28MbXdO4J4cV_;mW~K8ySqF? -$)LCSfIv4=nU=gH>x_C?X6`fArT|zQa|#hQfog>4D2T1%mEGkZXYg6(?ix0@TPATUT_xo?I{?~UFaP) -BYqWwT`Cc%M+^d4vWLqv-U!k9q4*OKqB6R~S=aw{&HoavmrK?q+1nn}zux?;_W#WZe+kk0LJpF9pHaa -7bc`<|M!&iKyE+u~Wxa?I48b5PW28~yQ&ng(d$p}(px~E`{*7dS+aWKX<&~A?Z?(k?=t;zZN8zU|aix -gVEBP3`bd93;Jx{lC!u9w{_g2=q61w#!5d$iFfsj%JWRdXXSBXOk+-@bB`O9f>C3|TOT$l0Jl#o{(Ol!hqBm(L(QrwQdV^xY!By(PJLV=C}vsMt$*TIrr5Bao)P`mXE1N1NpGmQ8%OulaJ~ -){&0D1n@PuZRnN**rN3`>H>!hyPuSgE|pp5t%VoA9qfZ8ZXK@u*k;ot^SYi>0VgX+Q`pOT;1RzrGMkU -BvfaBc)YtOnmsi4K)Mxao{UWd}{(5l5TjIymrDwRtWWCK$m7X^<@a+{btzrwLDUN&%r7QZaq!n2o$T? -K?1_)OJoy)gmh*`$Iw}h8wiGArk2lC~-n_+7cB)ug@Woc^om0IJbXe|4cZYxhN{MdJt&a$|^&2I-Q+= -b+KwRBsdHTl@>D=e&BPg+NBjUvW^^>=mv7X_I6-Z#n(+RMqS_`a!5W^ro$9u-60j$+ASm6(6AFXhKEc#HwRzr1gpAsbx9x!LSq&rj@$5RkBT_lBcbbefyXKCL&PZ;Fb)RLNX`;9JM8Ob5A!Kmh}ff1hJHi9Qn?&kp2>Q(m9?02ZTq114aztTwZ_Q*TA^(hoCb`9DLm9e_P3ro6;jD4)(8Xs6Zh_H>y&RO{fOOg -84!gUd@h)XL20B{DyRwFsh~iItffBkvKkE5lE?Y7mABVP0%3F;PMLuX)EwNj8ud}IuK2CJu8rHFjjrv -lqG;>_<(7A3^rK&Vv>q)>1MIgZIDFOcv1Yp{~A6Xdy(Fvu-bC)MbH$+2_0*j$@6(CGU -L5M`^aNYfyXrN%lXjH7hK9BKAwwM`bP^4^y3Qirwa`9;|lYq3k>w*3iGE64D`0bRDxg$HFnnF+fEs=` -g0ePgY{8>9!#dlEg;zpy|T^1b=q>@SD>rPmkRVZ8B_eczGrz@vEcIBFM3Z|{6hO{#Ymkyc8i8icY|aE -I-}y#EmLRhQM0Ta>KT>ZXCVK+?Ax!6v1-7#shdCtjczdAPPTM>)Ves_DSMaksd|R9bL`swO}Xy}6e^miy;D -3B@y3(AArqr8?fz)h+ndOqg)_?B4!OBKDeR_x)ErwK+hs&Ho(pRjc4;CNlCK(NP(!2@HC}YxeK)RQd3 -QwZ$dLDJo9xaWw}-nVhYOlw)m|S8^d^e({yv}D&cvYGk*8ss-15H0->MP~^z1151nfAeD2AH1LUQ%=l -<+E?qk{}@joOS2BhzCm%$6x4Xz}TGf40HCpB-()uMAJ3QY;?ZcGK -7=i7(L|6(m)S58M+ff^KNngo=GTb;uXmy&q8mIlv8v9v8p2_yfYul>5tRE(G6S)t>6nM6OI(Mi(4`qo@^`pE_;6*4>R*rI6r}u5qo`Aenfq# -@M0RAsZvr!wf`R-b~9>a~NG(#T;zkM)&@?4GnkSlGWcvWy-g3%Mb+k`RIZ0e2# -nSF^GIunj&7b?YP+)m-@jf!TKc}f=#`>Xk{Ri?O#x|x|aE^+tf5tT0o~~O(u%_!XL$m -vt&yTSWTh!jqRaSZK(XG>;_K1Q(T8g`qxc%=rIU|a$d0wi{ehUC1=bC~-y?dIl9`@K&4`LT+s{~_4N*Tir}#>7%?971X^Cf0oSplyn -vbQ)%jcd*$jp7J>7=^ovA$n5`cfwk+{B7--(K(-8#l^PR-L6?tT-zVYzI+F9~}(8>X98?O=tKtmB$&| -Nhs&d55_)|hpH^#0+w#$RUItMIkGc*MXv01f0J0fgr-oTkL$WsBZ_fEo^fRFHm-h5G|dbX7-g>cZr-F -geoRf#U-?MDT-ihBqF&S9#Qs`9Phr@dm-FF^vTh;Yf%rsN|5A;|__i(r;`t*fB=0(VU?uhA3}O< -KK)TdP=Ofpz8ap2@tgeg=KX~4KEQd<+ll8{df6R<(OpMyfj?w|7*CBbpHgh_UxZ7JmJ8_o*TJCR$ -UD2UG#bZb(yb5q5AwYhdHRO?zY3gxNGMbSBv_*UOb<8ogw)SJ;QJqRANo)E{*O!ASH7MpvTZi;DB)0OW|n?4O!8fb -53D4)0UnAztX%YAL>kWp;X%f2XYu)-c(ElTy<)L~`s%BcR_Y-q8;FFPqe70xZ>cHR?;Fr#2=Sr)|eo> -h6+Qu*k>@||^Wki)Tfa2FI!ZmwRtWj*&n?U7sORG;`dKX2J(Nui*8V%_PP-fAfuI#tcD?ghTv_hCI^n -Cj#F%$BfXSw7F(0U3ivJaL$Fy-gvuxT29l$9ogq`m;#GI6_k8SatTP+>hr2D~|X3UG=fU^$hFX-8>RW -TXqVJkfgjuv(#NhU3L#~2Re*y3}aZ#l`CZSvai~HtDoDpC5{KpyC=`fB01d}j)hD+%!m3pU$(id@Y;s -#`YU80yPIv@m^j}m*>NKmWy1rwy{u!BmF_>UqeK)L^k9TzBU&-BWOy -}`59_gMDJP3v9vQBWvq1Ml;ET8Uq15%gS01WR#4#z{Wj ->D8dxXj{&dv_b?LyI>w=PG8DebJ=hX3bu|N560rRnnCWb#ErD*N8+82Cde*Vmb#Xya3Hs{i3oUjl%CI -Lx=9z^6KhE0e|}&~b`kfbM4$DEMVw;u}EN_*LBjiZWgXRLJLjrvwccbrgf966~!flmR;jE8hmyA}>P& -ka>TVYb4mq@i+yyV&Y%$eo#6>fNh8M*6Zk548n+nc{xZCprs~#$v)#RdFGscRSaXVeSi#vC}ae}hU}% -rwFU!gkPsz6W&~wHnMwLv-Y?FeD&&rRRjvBeVM^eOA9N6b4KO5$Z%guerNblg5}j_3_#%B%gONBwW7a -WlBMD}gvEt53%>^GxB1m%6hy6-Jo!@fVA9_*FODy0GFqTq{7hVF8lpVjc)4ZH|p?9m^Mw38!atU^Wk? -ebGD)QAb^WGWro~p+UAg;Ln2bs%QDgkp^)fv2{D<9fx}NjJ((0)Fl$!dy_h!e= -K~c|eb4J+tp)1=1UPw(q9k(JRQL(H^2EQkqnY?;>yX1HFYQ@f7Krmbg=BLK@mJ9O;J+g<}VTYb4ZCx= -GFYJgbr$U>RtEB-)x~E42J3f|5)U6IQhuj65yR{eL`xdTB@1C>7ZlZ%w=1yCx-(Pv%qV0BCATc+%uv% -%i-wft;vXx!#^yOR(Voh<^=N@5Eg=dNFVI0!jC>faMI+r>ggG*!UmRCZ``N-G8am_W7~>)bXQMGW-Pb3@h6Zt%wlzEUS(Q|r%(kpUVyHO;I=0#E)8I~*x&3(VX -TBE?T-zNTIf>lpNl0(KH_%7(%|ER___6!|lEr&eT8R}d6-I9&mUrb*GZYsqHrcOxZ-fv-Q4-BBC&>2` -G00_jKLvAo#7a#g4`GlYEEOdWv%c+EG4@CLkZSXD5K93oJR0V7bUa{9pJpS(4tXXqgCnWb)*h4-+$H0 -7R=V?!Yl}mBZPD&#l5j_wY~K^^sdeyBmss-bocz<|bWow~yGx-BgDoPME~>n0a`Z`SuG>pa>r@x<+t6 -d8gH&Jj+YXgBR^XY85|iS#rk(l7ghf&Wn&h2a^3Ah)*8hGJea4(My69S5ijR3ze8&ky-qIK=^+_Wp0-wEo -|MQ{Ca}FL0{LCK7;1#`pSA;N8$SuknTLV=TReC!^P-W(*brSQ$?r$)2tE^_n$g{2pKb6keep@%1abGX -Dr(p&#+}CwM)&lQw<8>kP~)!DdwxD^}qtUJXSG`2hCu>-+gCKMA}jxeq$ge0R~0a0~qoZ+{23&>P<3! -(<&8_PSw!ON&pk8Wy4?ao^$`Ci}`7_6$S5Jvi=Ixd;5}Ut;`vjW)JXK<&=f)2DXx9vDe$&s2Nd7Fhy7WA*`8XaorI$2`pyb6V@!C$U3N>A!`nV(e;cz_ -n2lH&=m*cdbl~cPZW6v~#`^JRSX{g8NJ%u+j$?0X88qB3Q^EE^3OH`iCOVYVwdos+;xT_L5Tqm$O(Pk -bxB5|Wr6H1$FmNo=3?JM8AUAFy(-`ot0#YJt-b@f2Vj14Q}{pdaIOX%3w(X9Qc!3Hm^2zNjrHH=+-ur -yw7tqVu&$$@4jc*@TQ>APOnU%@T(JG_Ugwoe=1`5inpBY=hw!W%txc2= -}QRt1KJpipr);t)}%8?C!?*NLRv$Iy+>hlJpb?ev(%Dv7Ej1!K2YlRM$EzfTA9>P|9wJ9W&#Po#5ND@Eo`zPuX1Dq0q&+1$B$u=^Gy=m_wSao#~N^hJ`!k&$D+wcC -@RF`_gZb{{U`3WR3q%;qL$5_&)>iKOOtGNQ}~8-v|_;+l -wCtfCVyEICBPK`A%Mow3gToAq!p+LY?%RN2IduUGN3U6mtW37Z>WdPBu1(%^g+!_(U8hORz -6@ys>P=q80|HkXDZygp0xEYEAVzDAI#sa`!5gm&_APs8q)+qk>NK6AMhavkd5?{psFOm37A@2=|*J$K -BB>qP1`Oi@q`n#z23o3`jFHsr#yQue{pfdC=EPsy5{j~qc3P1JlQ5o|VC|%#@`mp{uBK~J;6sBwJd+Q -Fnbj8L**r4#flJ~Rv-0YqtQ%D$cgZ6!K$ekx1;Qx7ly21bkNid1u_$Ib?hn=omZeyN7rCsW|67H|h+i;IhqbX37 -XzdAuWmSrnyZU$vFAT*auBRBca4K0s5YLTR#$lh(!x^G3GT#}k>zS~+W0I<`VxvVHH_|uEqq;t3*-cW -HFJ!_MJm<2@C-(<6ivKVomi0O3O!y9QX)t{OVQz}BqcOp1@58oBir -mGRtZ|Q1dAdLH^xhoFKGin|b>}RzrPO80FNZt&gznQAr`9~!akg)^3y^{?5RLKdiLNQ_kk%XgVOcW%6 -tY`zb)QU~Xf&=K(kads{+vRUBpz}%NH;UB5(&N0*oOjNQO~)b1rkYYyQYy%jMCdg&X^Mf^mO4U}zE2TDI)k8@4SU+;wP07rA)28fk|oe{2L3>W>xm&($b -$P|e|VV?TTpY_9+Yiaox*@T2g=KfC0M#>A(me+!ljg)lftvNX+71Pb&cKCJ_1-b@T4P^yu={jZ6X4<%)R8wW?Ce$M{lb3orAmQQ0H325_Oq`XHxO1tUP?$gm`z0g7Cs%YRMy8Y0um{nC@c5B^ -{44m6(n2yK|liFsQhkS0yOyqM83T006lw2PbOm6@7gm!!hsL@@U0@jOfz>|7P_QjmN50&@p_()S -afrPmA;1IIe2Gc}wLk!BUyi+I0{w{Se>xNBV=6z*zo3_j -{wXvEE(2|6f_5Scl2p1{Bnne*07>uH;EYahQ+uxMWD&%dLuD+yT-h1vgwJ7Lz$Pt@ -gMf?=Bnj185ch#>%}=W{5}Icj0$Zij|Yj=*{BQm4)P|W+H$C#o?v`y-p~g@*EmO##@`qewBbQtOz)vwM*{8&OMH$?d{%aVdC~FWgyh3%b@?;bY}TZ2RHfY=BTo#?Sk(^vxXk=lL!JKTEq9f)r!^qged -w6X|Fl?pm#>5n=6K+Zp4W_I*5-DMUiO}NKTcnM5}rXP{Ay4ywxaxQzfQEfLvXzYqeN -n>96IULwPfZFD^4`bi$%16G9IiHCct}PNu#*QezbL$nOwv98M@;&nh1_j>$fSC=`wL7m1=s$*-zq#Zy -i1~W@cM!vpG|pfQMq{8&ouR5`pwBfT#!{;2M3=FUdXN0*|~}kZ^zUMCNH}J90dX1Iw2$jy-4dN`O9Dd{k_=Uj}VhfjI_6(?&XO5%D3Qu7h?9|$;Q_JqKka8{T -X74<;{leTZnlb``?F{?`QHS5Ho0wVa&-Gk#-+p^6|PK>nDD3oxSfCdb{&{TM7fV4cFD0Kqvf|2agMJl -++4epg>0^r(dKp)B5L3Wt%$c3_L+?vp}2j%9>+Ii9_w#lzfkZN4VMbRM$2Unmc|Hj}deh%9I(#=^hnu;sqm`lFwOTdpdw1MY7fTvr|FnBl_QgBeJIIobv%==(bUkr+$!!i9My&LC*3d) -p!2DCpwxp#hW*%c6YEbZ1CR&Z9+1tQf#Dcp))N@uC5iK51)+&;#d>*~ -87=}I#2aEeOiMLiI0s;n4=W30aA%?BQv2YJaKh9P$tBL5A<@bl(KlFw+mP!(kDj#Jifma96_vP1qy5c -AbiTx(8!8B%v+$D+d}zC0bF-C2t)VeQS_^pAT6!_px>JNX_OL~V3sH!@ukvv8@PNfUQUZ`xgRak9|?H -%3%vt(7pyNB4P?koKU>d@qOItvcVHBV+~0jKK*)HRlqwWD3QOL@2iAl3@?p?Zx9ZD=wD8;fi0Ice3%_ -@=%-~TA0fxHSVq<`p;%YQE~chFhWmOL;XZ!kwjy#tDtLNk=dq59T+8HUzIxYz7@=! -3vfW&hXIV58xg~UL4ZxVJ;GZ`ZInHU!pQ*6(*i9%uicWzA{SeDiK40rxwL^k|W;}a@+seT=;jJL_f;q -1-%N~t5`6#P=fSrpuf$!1qVEg|JVh&w)sm9Ac#+P56{~5TvpZlF74#VO!z!ZeW37o`MRR2`Ql>qBA@{ -&$pnSYdk+Hf3sm50BIT|fr#RSeFoameSvOUk@r9UKHE83N?))+ixHL7V9s^iVjE8=>DcSSf&G*elFg? -}Pm+c%fhYr+5xpVR8E3R1BgIk2DVZ>WkYyNY@(iUX5!L414d=3t7zPD4Jmwz7-BecFej -X%OMR#tr7pdp<+q##=?a@}wH@D}d2kfY&M+$Q*~RnHnjJ;IX8Kk?)El8Ht;dcPnhkQd1G);$CAwcTvI -`-&t}|OsJTkA0#cVX9Kzw~hvPIj=dYn=B#^axyQ5_e|u2)E_BK9)F?Pd#~na=EvP|qC7NMCeVy>i|_B -VOqBFnv4Y{&t5?U`-sVrw~OKdb|f$(-^prGK}K(fDy$BGZIn_Yu>V6{*aBMOfI3fG?x^UO2bM<-MysB -VK*|%FxNYu+FPlfZk~_bZENuXGVJh_hG=BJF;|qt4$bqnJQqaH$E`$MP6TX4-1#vc>VzN9uO5-oH<3D*8+NE*4MVaAD`Bx*CQL@170-%T>u^lRxU6>w)3c^_1fJ|Fe1z -_5sI-#a9PU;&S&8VMgJIP3=fL`lcT^waFdLkn)3WZi2EDmjNU9)370MTmzO?iaB1eRv#)((!S@X{4^$ -#%NoK9(C&p!#Dy-+7zHAKQ9tzKNho4_egxlze-*19|=vJ#4#DV6`od%6lu5h(UQ^e2h3JXg|qFbKVu)UK=C0lj(NbkMstn?)NKfKC!;NY=_JD`nTVg)#&*8 -mkkVUIA2)T?`|vckX9CCM|1s8wBezz0=nO497XQKdq#MA2x0|if+Dxi`^Ya3qdI6k(nM}JW6)MZ$$OJ -3GJ8VZM}*t^LbG>FP@3AVFmPFVAe?7;Wr?WCa6D!Q!wHtdFtrs+G*@GdzXp;Kv9nWcJ=DAv+O3cC&Y) -}GjPS!iQ4HK8mRVIr?h%J&s6@+qK3|(~#6BDYPFkye#-aK6j&2aC={*{{Jodu1=MLEM& -yjTo%!|%j3=L!4KG3K&qs;l$FH-v;m^7Se(XQH@x{NQ`bD4m;eVAz`>LnDknsUvQuPJ1F3C&nZ)w+J{ -xc#C|X-gMNPmuw_`z6Tr`Oe)4HblKONt6XUFO@)H1-#qy*{`|rfhS6`s*h@VjyK3p?8@=3T*BoS9DtTIZs&AjhQ?MxijAz9uwLdo0VDVg`se9BpvrTyS -CgZAEP~F4@xSVohrkY++3fMzH{T}Y^nWOB(5aMm)#^G%IGh`oih*EhMuAt_iDLx5V+n@8MMs=Y~-4&V -o!nfCmJB15gSPm{+JAXcFo@qB5?k9;WmMw7)G!N%~0t21EW4AMZmKK0TOB?`EmnC0l{Ed5W21bIEsU` ->dJUl;*_mnc=~y15(8}eR_5}Gpg|TEa3~6Bt*sF}kd8}$?0rN4f&;8yVz567DvEwp=?gmZP*4t>zs%I -va2q(~=AdMFjoYc0fN}cLn_Hvub^a^mqFEq*pEEDXxeSEWK;bw7$z}4TTf#7aKB2&3@OPw$-vCk+_+K --+deT&VvdJ225{f_rr07{A>-uR-w)6d_DD+z@@>Okqr6TEfga|MZ=!1%Ak|$cvS3lish}>+{=h+199< ->en=@3AaKtJ8bF+*}+Ta?iEdz98&ixmec)oDE!m>E6Fe{F;OhYDOIYdMV*~+9DC)7dLsikz2r@wo)LYVMRyzvhA8ttbe^|0I%T -gG3u6vns^B{kD+aFq8L3$4vlF4AaaLKUEnz4eSmMA-T0c@@XT&%tsxwKA? -_OPaCg?qHA2tLW+=L>7bLhQJzjcSMYNw&WuUoCz37|CdW#ml~yR$!xA~TZVFS#bYA?P(xALa*j8qRXy -dB8_%s!_1L^zl7#?l`8T1)}?cpI%&3M`xne`_!MOinjE5>X6$kLB|9q`H6|F_##&uqAUSDEo@GzQ)q? --zdWm;I##<3}N*|6_OgB6akuJAAtkVl+z=pLRE|D_!;~%Ontxr$d?dnoNQM;T^*Q3+#2pL2}TIOn>g{ -u|{fZ9Jt1K>%Z7b?R>p-jpk4mEQMV1{pufq0IWTWKY9t8rh}WV<6R$c -_4Ag4IuY@IvfKV2!i7z`XA_A)BcjYF2;H(`a -}DGLG7$eW~bgy!g^k;mLliab^=>2UC!@P!Q#YZuS;=XD%9L=YJzZ-(Qnv_3|L4M7{KQtvQl+O~N -(63y;iOs-X@hvqyL|xW$G@9f+;gcLs-v%8;H!Ru@J=&>|>%rYk++Knp!{P!~keXWq&-xU3S*eIk8eMF -1F66Kir}-8FMZr@MOEY%92P1n-Ib -~8hL7Vt%Vy7oc>Zy+o|nNp)-qAvY1RG^i$-=$TZbM4QNFB}o0iSlxKIveUEs*5lBi4Esh)WJVUP^tmh?gkB?iIupf|w7kCRJ0Y^X<&x} -07g6&aq_F@BXMYFFH_^hwd&iW8Cx-mpzivnuN46bIbO-cuMCDTJ8mnkeDW^SHq75$Dg@JU|R_pj`+U1 -Nt=i^!3UCu^wOtz{eng`~o9HWM^P-J*gr@4){7lGp}%dp6g?pOcA -p}vL3Uk>w~9LT3;Avs8n(hS&kLvUaexl;Fp1-VkNuf>9Z0L=h20`b4-^NqL+5bpHlHiEreukzPLE281 -dYh&-#A-Dzv7zMWU*0EC@;Ma%w$WQ19d@ge^ftAh!Y^Fe?ANJ}=Bws}wV0-PQN(h1m6yP5Xz!qR60<; -N%VhA{a1TurjD~Ae#k(C6jVaso8J>(W_#1Y4@rIJ6XK)dqF3hN&bL~&R83Vm2Z0o$l|fLjp}y(LfSWb -j|mV*jc`tT@DY8sy$7NJBJcz2?pzsuPC#hIqFXch*S302O^7z8=U#fxbB4GD0?A75LW3;R&><>jG3Wz -B`P+9eh7F1wZsz*p2ps|CZT6-`sVUpPcvhV(*AGD4$&80MU5voinj_xTkRpCjG;CghyXk2CdV2tRck{ -2x9=`djYz!ziF}#+(*XH`CcgFN1Y7lJE066lUpKo2x_DDmtizuAsY}4@j{#|VzR~r6BBQ#`wSaHc}V0 -;)Bz-;oY7v?Xp%?|AJb@5Z~K%;Ofs%J_(rbyLrc%4L}v1U%0^IaIfpza`%cHryI7GX#4N;B#l_q^vU@ -s7=Xh$C-H3a}Ew>o2#1UU$oQ94sR|m5(S>A3!>(;XOMuIv(EK6U>KI6J -N{;lTqVlRU#7w=RLo_(gJ+F`nh_mFa?-$K%4y%i>-rfk+YDIJX#-?aBoQ@dcNyqNMiRV8 -zRQp%^;eL9Y$7*!U2@)%2qiP1SdB~3iyB}JYvdfXDDrXL(qQS&5|v5-tQ!?3;u^mf)p -Pk=B6<~i~ZuzSNI)E&mDLeCg7C9Z+a^J(Ag*mp?2jomV+`So4YZhh8$_?hk4inVeIYL1T3bi3)2cSFU^h(Z#pIK}UjMb0HVU4QvD|41T#H3$ol1+NtiQLwWj;C&agaDdxOkWqMz2Z_mtav@lG8xf{Tz*)_@b%n7(n}6tJK>m{t| -I~By8wO!Q^)4*2^N~Poo1JKqJ)P_6+28KH$erFiH#rH^XB*t42-Tg>3fW&*E$(Gk^7(%1`lS*qIN(B9 -%^2!1Jnr4RL_1E}v4)Rg?s;Zql&^!c;XK$Lx<2p?QN-Fl+|2&HO|_=*?po*cS+EF!CdOv65saJ|<0}` -(t-q1A6DZ>ZL*`EpAPVt2r>N~qVcJ%9u0PFXh&2?_ -iTQ79{WVt?wi$9cZE2YL{UlOK@JdfQ^WaGF#CX?56THj$z-Oq@`dn+54LF^uQ!B(@M{xe{dv!Wc^X-EP0GD%?S+#Ze -jJvxsAqfFM#d%IW&)J|ifcYCERPu}+CSeWxD~dkH6nM)P&ehSrdYbWVwm($0Zj!%8MDIBz(g7hbpz#F*-N4s0mA_oR2+24urGc_XP|v0B0#OwO2vT -BDgv?~E4f@r9-6#F+TN1A6xbYJspWe4`ai((6bD_dzoBkQ1>~AfUlJSA9yvA9Pn|9=`gZ+Ml?-y9Z|> -Ib^v$<1Z(R9I87mC}1VO!v+=o61>R!jdkq#hZ@=oWJHQ>@A8x8p%w0*w1ol0pFU%Vyo=UWP-%;az#*c -(?qRK}OL+y!Ka(zMbwUyjEsTmoPN$=$iG}JdtAe0 -IXt4BouAm-pizo`s0naSKG>(_LRL8DK;y -eQyOTbl;;#Wrnl#o)KP5ZvR&}@R6P+`$3n#t5h)(z+cu@qwtxkPM5$KXXFRg8kDa_{7zff%z-8kS;H0cS7k1PekJ+V56$w85!m|lt&1U8lU97yfHF8mkuP5~pdJBkPP>-xpy)`p= -|x&UM8+>QeL1j$Qd@6Qz1(r7^gWQY1_gTpq)qFZ4(`G!8_xp$X6wnNv7Vq_zqcn_kEhpcJ?^hWhD$9T -0q$c^Qt&rUv49=f`CGd(Yg&J*8kch5^?-{kz6LI2!LRX9BRJ9GwwyGr7p`}bEa0eC13$ENpyk_{K(DDN-~= -!wj!O!$IS&r`FC#(`t-f2uZQ%y;O!YDZ#MvMi32|a@cIaD00#K*bQ{2X48aTQdyJ=VW~RKq74 -^KKINIuClC@Xep)PyZ^=_ft2%PPJH1P~9(gO5WY(UcZ0iN(Y5t#C7Dir);bvi%;;D);|*t1`1vex -d3-Yz*|@s{|6^9Tsp0z0MHr4OA_;YJIKvB+aSmgwG~kV{8|(K8nTp`f{ptd6{?%X3(fd*leYV# -B$x;t}w$}g2QV+eY^@_lYgG~Cxuk)0GT{;gRT_jD|c4m@BxEv>43pFC#M7pm{p2JDXrIIQpI4quZSLZ -F&2RfG9=zJEV4i(ZvwyS>TZBy=6NkJ$+vIoN6sDC`gS`C#|;+G&-3-99%|th$@pHh5# -(S#ddYDcnykRExZSbgL%mEaWvlExzpQKGH3|(}j;OLZ -9YWQ-Cqf+;iF51Q0amKI?ae4DF6!1)rG&Acip*N69n3AAUvtCwgZhMjz4pug)mmfJ{0qTBk-^)cm3H? -3^;iC1e;uo&%(^nGhQ43=A-iOMRhPfACOZ$^fBhv!L)JdB?wpm~U&>I!zF8D9lsPhs4IUMH6<7|*}Mf< -*(8^;iB9%2!5grJH-tz22E|9dSy!1ZRkcx*>~ei!OxIN?!n|uz@oMbrnqi+O`K_gTC-OYMd#|+E`J@e -{zTEFXHuCF9;*TeNIp-~3ih_+o^QkT0=(I@R9eg>K1jrAD01t-r>34kR)k%FV`62Mn -(gF8bH4I>p6x2{o^ekIXv#Av~~I=E{)<#k&OZe-fbK-7$AO -B>SZo}61ljVq8mUNw3iY#*+#Q09f5xZ}Qb?UKgfcF8+lPZoKn7#rT_pGWId1S^l1q(=hVatJ+Jd1LA0 -_Fg%;fb1#^jhN%GZA5*u`misPJ&4RB|vd`3~vpb73QK^F3z)EmYSbbhqr+#HQ{4`CVZCF25nle*_UKgS2TsQC3G^v=%BnnQk-4wS4nZ{R)8U8VJFdT;#H%W -U}oHt0SnRrxup<@Rq~FkWiAh}USR-QFpq`zBa6=fWV6>(j0dk2s(&nP#ltk$8Hz{ASpv*2dWl2To>k1 -={W>W&N|B#e!;1F>LhZ-cQn}`z#1kxD-pgYk0%HbiI|*PFOpqooF-LRJiU=QhndR5aVnR8etVqjXUn~ -SOW>2Yx+#vcU3N(+GnBo*N44vGEFI5cfyBlnCd6d4*TW`9|;?hm3sE{LhFR+D6*8^t{C0p=MK4>B)VU -Aot8S0q#Ld$BWl^VyYYakOM^HW_F!S<2wej8`lvfqMn!C3V&*~%+lY-cj#LhOn^2{-OE0D;X5vOMc)! -13kDW(nx*PNqwSLAR(Hv)2;I_*7PDSONrTK?82gm67CnY#5FW`PuGx@H}WkpDb(|peS7buuI7nQr|I!g?;|AT{?5ns2q>+AALzn~HUT2k|piBmRH4m=9h`&4y&=}MT#@W9~3Mj%L87Kt;_Od7_ -cU&=cohnd1%RnzW`BHfyV=zBZ$hZzg{1!Gd0GpTn?{*6FA0X59p3)Ke5bs|hQ}-lU_Yir%b4~sm9k89 -qrC+$YaeVo;yn&k_yO4*hg#F!*>o>JDAEhxbZ&^c37O;m0y_w8%@3k>5x_4aCuRoveXS0R=WV)Zt7W& -n6{{*zAyxkHvZts%!JQJMCK9(|9QYqv6mYdP8Etxp{U{BnL##MPdA?8Ut!1BSeGFgJSWQ(YVFcIYXZKv(^fJL}%$ZLN`E(@gDu063yW#TZ81#4AV4SBV_P0w$=;2E}=8els&gH`K;cff^V! -)3mwu9Kw4A5iN6?O8Id8YE?nzgSTG1(r1t2dYD>chh}xJcu24lsmHy&mW%PMnwmYq;BRa`512>D0O)W -h5L%)t>pMRHSQMRE%oj3amBOav1xL&6*yF^28=-^LcKuKvM#OP!y(Pi8~iIp*@%n->JbseI6V^BHRQk -$6J$e|Gfe@}y;oe&Y&XZ)O_*aLLyT9G)ZvF4Bz*)ve_vLTyJ7nvBwz8lE1y%Mp30{#(lA$v<_M(hZ(58@&Fly{c@3;THph!^|p9hL4FxPi>xq -lvy-)TzBKJ$*}^z;E5@6y1v~`*<4hrd|qe(gEKx8Qt8G#c@O$*XS7TIOQ@G -FEblE??}?7^WA7y3Bf)OO_SUX?Tb@P1_zgI`QN@OYZ&Wt@#d=3^%(^$b0KBLCxN(YpXxn!i<~vz3Su{ -l(qD@8ojNn8Q-#ajq$S7@y8g*gz9qwD3oZgcgv)K)5_Eyd>&X{m=Yc9ZNujgByYX`u;DUkW0zQqK96= -<{HS;QRNxLJhV?$Yw-6RXb$Eqs|h($MkktSpb{#b3B;MNvGZU4lfSe5wXt>VP$Az^ny9(8CZ@W5}sE& -B6w}*AxKY=|+vbaC5e{=f{}t%t?(Y6F$YK9|cwV)#tPWjyj$P$!TatF=yiHD<6q;)Ei(O_g(%a*j++PDfN -lpj~76E)JmuJTCX5(W^;b%0qpandoNmN5yzbZPTqZevU6MDw&f=KERq_4OUs5h>lAQ{~!&p>rOyCnfB -yDlsLeG$)$0&1#Z6@h7*~m_>8iKJy(>i$QX~OJzyOLjpWx~1zo=wCitDI06)lY8P?MjI=vD5H1~?W)V -Zr(oUKPnHcijzPc=e1&27Uxe^BzAwIYx{)S16@?ka)zK@%?H;dH%IeROy%x3$+@gj&plEK|&KXA==xH -~2@+-l!&Xh2+O51s>P3kUY@3g0C5@igESH3(KkjJVsZv=s`AGYdIjLmRuqaDG55ij@;EkIyg&Dw~7LW -19GV!ZrMp1s7*K7JhD$F851oyZLEPo*Tw89pLh;;M-py}LteaqRG$tGx!{!-7XbFJ99`-S%^teKyoEu -0Kafg?-~8fW1`qC8dV1ZAa_ZikSL{N0iO{!K5GfNQD^7X<+{@iz{j1)RC$x-%?4mu^=pj4`Eu&SiXSx -T3{_Q|a&YLFRVB$As`#j{g?VI?X{pRF-9(QT=n{w}mdKh;M>A!%;ZwLg#8)W^HBlpni-+nnM@&DBtJ~ -8uu@cMrVrW>~H(kffHlVCcr=h9cGLC?EO -xv&#$@iEni}zLph~6O%LB0;F{V_01cL2qTf5G^UgH-Ys%BJ@~FMhj4$M4S3?2XDBq!Pv6ExIiNA>S4j -Z!e22{zS5!8{pU$48AAP;$3;0g#QXm1!M1UoNs>urv6)Pv-ts`99WqFhiAD@h_axnNu;^i5k?Wj%PDm -BX|0PQiQ}$LeU3IX+O@cL5y`s=ZKwnTZ_vCOA7K{v!}NjAOvmz5WinIF;k=iN+S9T<5qD$L5N?6(hbp -=4V$i{Q4#nk!l)F*G2ICEsy@m=|F+`73OEh?2cG8s67!Q+f7($g5_q;o -Fn_C9ety>+sX1{jHS=?1Q7Vx#<}Qq&c3Psu8Q>WkqycIIci; -x|x3N@g}WlwE*LCm%6BLV(#PV;zOpuHh8t_Mlh37rf+2yq!8v8oT2iP*kMN%DDnpFKq$^}C;}5Jvqa| -DHZ6WVaW9h~ZqO=|gL_i%-W-##ENV0uMtQ%;r5>lky$6zVRc@Euqz?je68f92XgMTbR^FyBttd=baqZ -MukrC@jOOkf##SLE)a-U#C9xT -Qt2I=T2Wa*u#0@G@!=y&XFJz}^z(W)7<49vxk2h#t=}X1*BBVpia%%BIEE9>-R{3{9E-3#@;I^zXE6+ -E(Naj#;_2m2;Zi0(e$>EO#7j*PomZEt{O(4m~~@%3wl{S@3&$VZ^dD#&hIP?DmP#Z^1S57^y7n5lB$= -zU|0<-O>KsQTTmF+a5pm`(w=o_@cC3kZ800jSb_G)QswqaqbmM&6zOI-3It+a}-Ldniwmp>R^a6qt@c -8f0W2B3Is{p$C{X_mA_q_!*e3H*mAAF(Mwkw0bh%^e>}RWL>0EhtxG68+FM8C#knS`k^;OZ>)jKDxLa -d$SA}r6A{@p{qN_DMovzq|!|HHkxVC^N21GO^4o3kytJ7+(uZ`&dnbJp&dp5*Y;t#isUVZp-K6EQHYh -k`ovpy__%LN4Q5XI4nq3Q?IsA%fB-~q*?OAna$=izZT2^u@F!}(c>x^^^h$|M&(J>N&+<@*tPfvBZxP -*XW*L{A+IkG#~D#kg*y01X*tx3)2Re#P99OU@=zSpLgV=^Xp}b=7o;o4S7kYKu -=HCv`#?dJpfoOvKu~_<_I@8w*=%1eDTkIwf5~eVWBoPeRzCt)e{WLVD_b~}5+o2PB=Vakzk6??BB1k=N5AY -H(ypNoIS>ks-lZeVYWo-w{?WgDyyx5ZbO=KIIGvx1Q@FUTU%sn@K?lKe -~+7)Y05X`g7#jG4V2#b)7{7X$FLcAXJbFnnZGW^Znt#V0Qplf^!sh!E&o03{-0U?zIxz4v;2Maz&}y! -X{khL-P={S4vl1lldqXC!1u<2YdUqhmKfJ7>4U8f*CdojpE;lo{E(-bhqpMS)_Cj!$H(M+<*qFg!U3f -u$~^+Gc%c+Sn=!?7>f3IVw1vXUfpKKw!jG=A2A2^%zGV3t$f%p+93vHNEo8JW3_7&ZfTp2k{euVonS-0O?MS;Shfv;Y@OlxchnRmQB1MJcw4)uC#;C~H~C;i>c#? -h(MB4jZkQjbX6-FdPamY&H_^q=aaN-k^hli*MoF?NmTIE!(8Dyd2}HG^F6o`>2l5iO#~-`r^S^5UpM4 -HR;G!DZSd+;SGbP^H3`q7+a588lWW5dDqp~C#4_MQn9hTViNK36q&ro^Yi<|toJxH33^LI-W47Bk!cd -=8c)--KoN!wXCdkJ-NCe5czz0yr`s5B28^!dgkTFH(5gr-WmxajCyIU?<6s>E+Z5fxtDBu^7Ah?C|(f -i1c$HOIfAgGXEN#JtS<|#n3mKh9tBPMr73l@k_F6i+Rj{~C9@=ecExwK`WN|0{3T6%jY10Rfr*Pu9 -VHA-m?tih@@+^1k>{#v6QUZRM3>N8;3@`rhxDw>7Upk=CEanfC$>y^VuT~^>RVvQ#=aDVhLaZRfYuc#(wE#s@&kFA)ST*ArYeGj#eK`Eo~_6I;W{JhVf2ljf$!+) -kc^?7Jh_|t)G%Eq9YDh(x6;82dbd^|F~iZx@-DCp&UJFIyFC4&! -~xm8ukK)>Gc=0F<*mNeE&2Cud&*>;rQtc_~kN2`FO^NY=wXLctJ@f?=~8OaA#^#Oh99d+^1T$r7>LWwO0RGAE;(WOUpI+I_qU%E>HonoUn}x|eDzzph7u5t5*UP%I7-tHilGp;Q8${RX -cET>l!PJt-JJFZ28X_N&GUCno=A3!5E$D_?F7D;IN-M@83OG(wFLPqu^%+q``b|J-Nc5z`QOg=c3&`x -*i$O|L?43F(@ICdj8=pZNJNyNM+h|wvmNum6x7rZCuVJs-zq|3EHzCB|lE4JDSN}h3O*ZPdyP~9 -fTN{Mz+jm1x;!Q8}w>!)GC^GxY;>LMraM!QJ4O^PQVW@drH4P*_6f$*M%JApKdlC4d;vw4?;bgF<&w| -JCi*CctVvIP1+g+OC8!bW|5N7kEA#UeJ!rAtJH!%4^%$VJWWEc8b4A1@|<*s~)|7@gfqkt#)EwuY%x( -HHciQi(&FWDj<{^)nx=Y>6~8RE!k&b*KU2K)G|wRqnwSMtT*?L_QfBr^6q2JnYOh7s5~j*(`@7qhaOm --aP0CT~)4qy2iAw^xuK<&vXe{Zfbes@E%0}F&ZbouLkByU@fRx2et}Yk -glgr51bP{oQv2}Q$`mIAuQ^z7#jgHBM!ReN0J?3o+ltT93l2TSz1l{N^_t(s5hKL?5d&aWSwNf -Q`)pb&!R`}kka)1Y**1Nog1Tu-$m9_@Z_jvEgkg1Li~CayERx`5GCd1G0ekQR?qLq_U%@$1HoDbWF+KifRP}Y<1_dCjVx2%DC{8BgRi|^KZ7k$Wnud3(2@pV}bar2$Am -Rpzi?J4~eyRZK#7x~un>(4ImEf8Y(Pi21J%D4NZBDUA-i}YRWCidb1g6@rKIQh1(gx)pypQkBebZ@HO -;0=9iO<>60ew`NkEMx}nQ;ZwFp!B|-rgm8+;-e^5{TZDC&h>B*t-Bhe<<6>`-J9 -h7E10_`}jTixEo#5d!~ZK_w{e+N+NrLB7F~d?$wIzF8;FUAXoeFWB7XoU;hK3y({?i4|t|}ybx{(VSf -Ue>2Gm-dUyB)fl5zyuKGpj4DR}gAoZ7GeWM6Yd*1#$PT^Dn-*=$-dk6GAxi#Z?6F4xRbd)HXt{^J!tir;EEmM0=1QhmrI=TS)V84HSC397A+ -t@Z36RE`HzuwLTDob4t@ofG(Fg!~3pMAAM}z&nc4)kjOu7&|HFYru&RqsV?vW1-~8n@Z1QLK?q20?2kVLj6vJ -dgB}-FUWDUh$RVwD-egfwI4q1Cn{4);<#Sg -;}UjKI75-S?FkerAKr@4mu93Uy_xO)!w(z7m)H6a)HjDBNqO-#aw2c7kkIMz7 -^5I#+QxSEt+DDErvqH}4VfSMT!v-Uu6SJt=2}IVG6A@zsws79$&cDFIx_NiHKvprC9~j@^lM^X)RF?S -7l>P4v7UJQY7z4E$b0{t2MIGtdHac^vMj>fpsu5(}JG6Xc~u#b_K#PXwk=cM4zAUZRCIjCnJ7aK@+S- -RIaqjc6pVY?GL5%eGD+qSPI`a6(=UE@^=X9HE8}XgyO+zkeJM=i=rtp7%c6wn -n~4r1gZRz3z(`VyDaWIT8`4kC-|DM=gdb4S;Q`C5vnzY(&8* -0P0;9ssNw2cfXO4^>oUN7!$o@?VhdC`89nvSuQ{@5r!-tzuEA$h?D6-oa)j)WBM7OGL8w)FWt!MgvVy -o>w-u9QhU0#Ej^s4IXXIM<=N@>IfBrhqelyanP!Y}9Ip8L4Jekdp{^T6)NB^H`~!Qbzl;jLy8eC*4E| -|c@zXQ>-JszIXJDNEX|ocw=c+bVPUm}tkV3ww(IwR0!9={PlY8Y5*;zb+{X97a?{Pzs?E%5Y)Cu(MJ( -cfWxm!%K1rzj#j&{5!v2gTXWV-*mH}C$+9%OevjQ0c`@s3*Px3uft{t!!4ZZB^9|EJ6QJF5f!YG>v`@M1xnkL}u53{#-CqdkJef~27mhB)IaS1pn4sloeZ9bpU_MwDAI+WzUWL`_v@B -uFLBBs9KG)SW-JEMG4VK}eG)SVauP_pMcOK#M7EUNVV>>RFKuoD_2LSxR&x@r%8(lz;Ng-d0qGs}yZj -^hqN~C8FCryfOBfPrYxu7Y#8xhnVZ>C1;rz3sQ43)KM=1u#*Mjg3c7@#JQl0 -^O1(OrG@WTA4D-AzydK90HE(!LsOff*KkQqWNTT?iel$ecN{7CwSiic&L(i5&+esMjk8wn*L3fE?41)hYMM5c1sf;OUQww?C6n|F%mS_*qYtONEdvjf1RPjVuMDbBWbz@(sTGgW#T_V@f06H@V9# -I4~$T{S;#iUDsy9@41rI3Y$g{;(zh!{3cQ!zmC7RtWe(x$cnx(0nA&H&IzeV>_we;9z2Qu`aiQ9$=Sqre0g+t=pAp6e -G1CTp7tYQzBa%9Jh?*>EsuK{cJjH0l%b*0yyDdaZ#8t8CiDhwfTBVwX%YA$^_L8=r8#5%A5f&c^Bfg( --Eyf!_nYDo9l(eDngdH&h>$bmD$o1%<#2nkN$lM*CvxYE)!|63DaNdJIBRAnTis!4cJ<_Ty$TD4a_?q -3~3ajZQ>qbVyMyP|p(7f?BcsJ}(yl%#IDJ!8>x{2W~Vy@c<<}x-N&_Ae=9WKr_EhORP7zdAuCQ6Pi@p -XCT-R+UIIEfxlLt{pIk=R(4DSw+!vE$Oleg7NR>|c>@I=?<&Vch>hQz%TJDDq?X*grn_lSTdF!0)v5f -2x{6yyfPRe|@xSZwzXqNqD?BTkRYJdUtM9I|JFiL4MxljlFe+Nqm>M&)##u+m9Ie?k%Hs22~Jys}}Lr -3f|ZhlzD_6rit;VXLyTJ+Yjl&6-Ma^`I%)R-#fB&CXzOO9)MDO?WS%3Ua@7FBk=vA;R?VuoJRP@n<< -J=pKQcfP&sldgnIgS}z!g>b~)m({wP0DtmT{L1s)i@)1v%EfHPLr)PD3gPWcoWY(P&bL1b($&iC2AvT -Oy}Yl06JzN#dy5aXQDeD?;dqV+)U}NvKsdx#doHrZt{5IB`*z-SoTyC0ycJ@%DpSubHcM2u*$E1H -VR)90SvGd=XWpO+pxUkLssbWmUig|}8TX`cc6metX{C?8GK4@1+6v`=)9H@JP6yhLx|@+>vE0yk!r3i5O=9LZXdGZ(PPqjxp7T?KIT|OXh)c5ldJ|O=+aJ4nPCDEgC8ZWPWQP~R!4lm> -Gq)XdsJ{9hjb{8rvNo_;%wure(=!CB_G6NA3IB3^zIR7hp8*H8Z2I@cyn&?nO6N+uL04i?(DTaz~KUW -iLqjm+(il5-WY0uXIMXSc+3Qv!xef#JkS$+WiQW4z8-o5y;qJ491mW-9@XkfZ-GDZiu@;Wh_!*S2{tk -V6QjfF4~E6Smrl}N=3}hW>0tu2yF2u?CUIAIH+#v^4A&>BpwYA38z2cMi8C6~C11?bt(S -v5Cv90#>ngiwm>14R;QLwWUvy=C+ty{=m<(j=>qCc~c3pw-l^GLFU=ISq|DNE-@5v`0rYe9=o|DxmxU -Onb!tg3dVI?^wOVg5VD?5k0N~qxwBH-*NM`FB9vYH9HIFsFc#0aDZ7z7$qmw3&VH9Sbj5aJvO8qk)7yF_pRRCimtfXq;wm0^OP -$=I;Y@pQyv)(+|NYUvLEEc!al8oFO$=|#FihchXUZOr3&c##!Obfv1*RoZ1Qq7RhoRTrqk&}nz?u{^H -lf&{emOhz1nFG!3GM3o4QBo1~1p6=Cjcs9?Ie#-d!)i|WxQ&rey!qWqF&pUfjv>6-`fLB~jSJ=)zJ(S -5Bo0taALO*J7Kx}WRT&iV>xJE=+tN9c+nkwc@fP5`8^<0uHq9o$`7|V>qARtk;^zlz40WO=W -|7&X#0@Swv1gbsPZ7DR;8cogIM|-4?Py$1{84g!+YDJHh8ebypQ3=Mq=xXg(P^fMO8 -1)xVU_QGjVpySwSmyx_aT+W-bG}XYo=(q|gGwuMVaXLwm+R}k~eJoZG -iZs774owHmatMhU)w`d~_p3L7*K7CJH(Ekis`WpxToGhV#n=Eat15I|TAv)jZxj&dvPVED56!q>>&)+ -_@mS-T)Gz5lqUp58(!H2U^LdQ&OvFI%ww|3a4j(0cU^S=vn!# -2?7g$F~3JWXbp+BuiQpbFClxmK9J9k!2S_GVafWa#&w}1~~#<^^SKUDa3 -?^p}Ts&{I%db4wYmhqrXElit(7u-S$t$wBWv@z<)u#7u+K`El=VZ=vboZk}t)7q<`O; -W)cvZ+T0w7*}!VTQ>M`=wi_c~sM(XyGlXkyr8P3QCD5Qkm(u{3JX}97gha7%aC`Jrl69JJ)>QtiK_``9i -EyKD -x`>@A-;32PIayd}R!#+}S{}xkZ9(O_Kkn7Ax!iAW>J-N+ -Tkn%4XRAQX*V@z4z_N9pl3<%lVTWA(8+bHJ}B`dDh3PNv>W~22ux8kL!&IYu?qAl`yUXa7`4)WCf5ISYC1{{)#ik0T$@s; -IPRk>kpmmmhfmM>3RkE_!+C)lO#Z*RItsV#9#om1S3ku(m#88Q6-(=f|u(xQao8czl_`2Z!$@T`QytA -TAGJ~wTv5`p+Nc}5sm)hfsv$}+|;DJpjB&r^8As{b8A(oD{M#u!D+>X4To+Z$J0F;s|}Q()Q_vj6Y?& -QGCTq7P?94(n=s-nenQ@!&mkr}d1SkJea&XgwFh}Tr~g29?!S0>le>BPo76m#xcQ(hUpS1lz5J=YBix -5KJLvPB&%Ek1@4w2f{=V0TUJsY=JO8Lm_doSYUt8t>omC?vsA2r+aPE@`-IH -uTp?0@5E#m}{V@ORTWveTe6*{g#a7fZ9fFCIbnY!>31b+s!m1#d&kJ8w~H~Kzea(9~a&Mt9e?kyGD_j@ZvuzsqV$}5urpR%Un_XmG!#`60V;5ky -hk?JuFS+s{>*G&z~W|t?B6N#=*;rvPFK55i99+3NJKC@V!k9?F2jV}hf?~>szbocZ7`#ZM>{Mr5eo!b -Nc?Ee1j_CAI2epYk*!E1q|v-5E})#^*t$TGjjeUYGN)eN!Wu$llR=p}*HO3)C=t+6xDUNm|*9@C@a4q -5H7mMK+tk#+0Wq}6l4&}U@=8QikaCE}+8;OL8Y45iNNA@lWON);|GN2UbnkvA}Y^39gF-R+T=6@46|) -(jd%%em{~(-AKUE(TQ32&tDIN1nzSn0F$rI#j9rv{uEgK$2!co=neqd!1u?0SI@-NCdll&hm+7?+Db -+t_c>z&?~bAb-t+HhI#JbC^!%-Sy8-m2+NuHs^$v(8G-q0cPAGHm_vqGh{VN}_LRh9$1IZnI*``#D+0 -5tyI#=O7W>o4-CBT9C&sIWoK(C3nM9iWlEb{*PL#+_ik=^%+x4l&bP88!4BhW4g{Lj1#4=hsXZ|3b0a -$WH7r(t!+s+q1ZX255`f8Z&_=Fr!f~w(jbUX)Duxx6orgjDC4|u`xP@Cx);Q~D -$ujC!dCjE@9`~cu@uEW%zG;ohtpRd6uaDg@Z~x#F@l)R+41Ig8Q@eH!neB>n*c*+CcQZbc?;w;X`#Ao#1Mzb}P3 -awullPd~2CfvepK`AOq3?zna<4TZ_5SFdhPUwT -{FE`g3`B~?M4W84`%$`+y4e&8s9IoooAy6`;c3*e?$JILV&>RfI5B!R69_3{2?EQKxSY;*V3hu6KT71R=P+3I-pv#ln(aRrNLS%uaHr3)`oh_w(VsN1ypbop`QM}(J_gy -o1rwEVPU`M-VasNx--5>AloP)yUwUwium2CL@@QO`bstg#3@=@b)yGY*i4FNjZJ$8=)-#%C8`fV~@3Y -l1lQ&4;erR{g$an+O7!6j#EhqJB#)K9f_h-GCX0svka&}o4_?pyl%1JE0O`Tj>v23OoX_yr0$8+faR9 -zPMg<+AR7<`Wzf&QE_tSL-^eDykiq@l}ABKuH(DsLE6oR%0uPP@*2`fd;h{z-@U*X#hRFX4rJ2Gw!^Ry*#gLTHvPPJ>zZYzS|;iE -M*6(BP+UI;aiq8y_N``QrWpy!*5#_V-_(yV(;ZDF9wHFKg`yKcdj1pIJr;JE0?4H=8e2GgM)eA2YMTvKD6jhv7eQ?P)Yh -d%SzmA$!^qAmGyW{D&BarvnI6C}{|imv}am8kcPBakV*g6}pm4+2^(Cu5pKDtaN*vc>8>uS9}CUw=sG -DrV6KY3Bj|@8kcKiouL$WGO;barXWd#6b0OCr;g7$yffCczNkH%mvpIwK+T-=;d#~-?&6~tMDM&*^id -B?`h~LRY>r-cab=(FDJRjaT-WsFEL}=3%;h9I)Zqn40jHjfwN+LBIQ4x--Du=$6h@Xx;e3YvK{Wi|{t -rj<9N+bR6W%@}k9&Rn1k-E$n_REAeE7|c|Kq6?2e>M3~|M&k|- -+p`L`?inorT_MSzw&!i|Hpr=znrYuQ1&B_BSaB<>un6}HIkJ2Fk0DrJrL@>&lcDI+^=v0X!5Pqmk@j -Oo25IZZ=@kc_tR`IZ6W;@PH($WZ$0>(rO>;^5Be*sX`0+MeD|G*w>Z&<^n9`tkT~Ai00ix?ZtP)WHd} -yCzD4i1(0p$hA>O`(@0~XeRJ`?w_Gq5?VpF|^d4I(lj(f6y3I9Sso?~CDa4>l26XB7-uu{Ok4!++6Uc -haU>DxXe)SXY1I|J$Vbj}50640ZU6l+f)50uGV||_=`Z3)S8g`uk4%O7#D~0J$88k=%u;Iq`P=Wnn97z&BvSht1jP!mye3{n(+B)Vdnnayh2)Jv40eh$En^S-xu8Gb_?lGTpuInfV&;kyy@1)_~Y095|gjE)1oH=`*{rYZDx6+==LCgMbPU3$Vu4C$wf1Yv_|OZSPi$7V+sKU4sf7BMe57 -21V-Q#>dYDTo%kST9yK->l!~f9JtNS+4RoG^uu2Q89^{vP>8_cUMOV7zX80#Qf*3_NEsTeR -e&HPzED@BHhm+|C!pjO;4Nj-YWf8LfRdPsnTNzfD5;|MCH*-zCU*h20%H=?%~MPouP3BLBb<{^_xw5d -P=KeXk9O63|b@WOn)acXuU0|BK4@=E?YN0Zh@mp~@Cz?+prX6BRVug6p4W&#ArlAx?ImGW4w;gT4pjs -rS)agbgQq?i!)@WOz~R*TIV&cZ*-m*<c@!A$M(hZWQu_^}Z^dGq+=c(szaj3sKUe3k!9LHOj_c3#vJBxz^DsvQ49A~@=?jiBv+!wA_L)KcU25J -i_GBvePGtNT_dl&pY)c;{A^i2U)4WR#x< -?Q?P9q^Gx1}cvzMH%@R0t~LKMQg*bBApcda+&L~wOR6PKD^L9TCRL?B!%Rvuiw~^Bvo -b)I&C)xoP_xMPk1oRS5YBB4&tV=KL%xFsx89Z%4(g@3D&}!s)URh4IpB-cfq|Yq%xu2!p02=WbG-hkx -Uh<)Jhno?5s-^LT?0^{Ol4}cAcmFvCGRR8Dp}tO38_%A^5JDZ_ai;|i)jdOGlPc3^h(5@nUls9uvLSp --=wRmH2#pUqxjp^UugbU_<2?4@DwgiFm@0OZgdLlJKY|m-LLXD!1*^ -!qp~#A%NX$bf%0f94iR4r%|9I($)26rJ0%Q`sn)fd7{t0|jOVv}Rj}Mu@7+(q@`! --|om+hl$vhN@2U0+h!l`MO2g};7$_tzW*u3c{CkG>-wk+017vXP1g*p`7L$d?S+#<5b=o@|0;A3P`>! -|w;R&m0VW)Nt@|qtmYws`h}6*hBUqs8po;GDZ6G{#$^lHQG)bt!nZ9TX6A9di$eX=r`E{FhW*dK?yf4 -T#+bc-V#~0tywoX2{zm5dTR(^;v4D>kqOh#gUQJ6H4vx8nwotno)VicIIv?xxH53?4k3pDVaWUOsL8p -S$7X%GpNqzpc-O3D5Yk;tq+1Ahpa&l%xfAo8Uoq16s_QI|FdEgMYw(MHrv|Wdi*CJ2*9$UvXB?hS2Py -I!k~9t+rU5JGKTX@iPO1=O9?TvVqma_7Cot7xwdCq?s(Cpr#M%riDLK9`q0U{5$T&5S30o-OsD!t8df -fP&^KKG17X9hkU~LuQl67tswOSLi#{1#;Iy+@dd^OoPKxeM1 -H&u$0F}R=E%-HP3c-I0tABa;U&Hm6L;rx-KeadAFfAqaEI5+xNp&Q97pk${e`X6}@efUWcvn-y_AvUV -h)ol_gxm(pd)XP^i_%*xy^G+y1LW*&IyDI~JwX|Qa?0e(}{?xlhz0VczOk)kAhQ?X1XcATuuV -3n&F5AM*%k~a5mr|uG)@odJy`45-anUk9{&$<@3H-;vUds2d -5UkYRV}VC8QvQS5awn2W%|Jyl;&jS$*g@lmSpGY^My`kX)dl2poI#d2$>@f&;nH%u1%mPu%_=i{Psd5@g?Pg&YrX!#x`hLf;SEhcR%aiL!u -$vyTWp+lr)zdym#)gk7=b!Efe?9lpD?xUnS3&~(hn?Fp=E -F|U{;hO5Rl66}Qt)f{kcaghbv>nyNdQRd(*EA8WEsTxzH>YK&TZ{Gx71xke*eehEj2tuVFt8}?->F#*TE{|9>9!`uc5Zw&OCbuTnB;Jv#f6Z%QlyX0<#ul(%y05Wk4r1%w^w9E -$G!;#}2TmCvRwb%%OF%ra8ey?M4x@&)n4}+5lMkL!SpC&Sd+YzsFb;QolEmHXr^CV2_QbY0mw%f_9F{$uk~BgEPKPu|0Qdcag6w~zYoB9F_e?YFA#@4w2uU*<2Ngv+Md74(T3#QbtC>FUR?l -Qno(fYo={n)`*!w;?e{)ZiHY*G=TOia{P=>9}u+pC4Qx9#T!@!gF83*ZV}V84%jdUSWR@`~v(98VCF0 -Fl&fB>W6E-qv9XWq6!fuy9-y{03FNu`fUOK?$`CF2m0Y%`OQSKm3``eWkk8%1(mx?*Z1e4azEFl{DKL -8E#h?%>ZaWlTo9j*lUoMd<%pV87BtU>AJl3`0R-c)bLuLurt?VuIi4@9gLf+5)g$ginBL=_Grh93V^j -JrD5o-PY3`Ldi24{#*L{6pSTD^kq>;xEB_k7!I1&9|bu=8BQ++?@ak6D;jXPT?z29xm!b&xOry&rFn> --?<2o%YEhZ-_HQHtC+CnLR}ql`pbRdu=fEPD2~EmY34@Z;{b9r&&%pG7>671%z2hMEGdk<6TLeVZrp@ -E!$H*Pn05CC+zHx>NQA9g=Ih=#9o}r1&&$*YinH!amIq3<)nW2LgJQ_n}(eF)l -n%2{xaRqOurw%Pmh0nK3Bf{X5eXN3IdqDvMUi@w}&gx=R4~obu~qpEi?~d3NM$E>9~EBtkUDk4i_9!L8Ka3Vw#GS+2O4>QB*iXj~OXS}n -Mp0laBL$GSQ9^?4uT($AM+H`tv?0fr+)T5Oc1TVo3`e&R__h8p0FU;E2eppDBuxc -Xsh{c;j6>&!IgC2s@cb%x*Po)b%qIv3cbY_-UVyR%bZh8;C60o?ZaU1;Z~7l@`?;-|}}a&#x}EJR%BE -Ni;q-M`@JCPt|&2qA>VPY6!ebF`JgDM{Wy_%skSi`wrgDVgFOdD8qwTR(a|I8myPJFTrWWAd!Ox1x47XJ1v#-xaHdWA -?_|EO(*(_u_$B)dRzmgtWEPam-{z4AO7;Bu?OT0{+MF%An*Nj!2V1K^D6Nrnu0TEQ6yr;{BkxKrP3-k -474juY%hfGz!WBlr~I`cpH$v%t9A!{o1$+0@Qb7p#nZj1T82uEnr!NUXEhYk6&g<3y^AH>1;bus3SNb -SepmyM(*xUEIy%DqTP{N?tANItLdCRYmeKdNwP=4XD{Ma|bMi_n^E%4)Tj{xt!T -(3ub*N6GV*cj&9f~(d0H+4juk3@49+iwZm*GE1B9v_0&eXZL&+8502PtW4l&c9@G|Da?2*)$rVjE;v^ -G23oje5{s-g|#7knc{t}@AmUQa)g0Tk}&^eF7W$?t?(Ahwfp|?%ZAf2cad>f(c2f>j-C~^W^7|UCel8 -u6z-OZvoH9UM?z<}-D;eTdsU0_y&IKn-$1terf$lpZVdcodXs#(G2>x30lzhz+TToViyD07^8V~fd@W -jcl`(m81n!k^bqmP0cz)A3lehu{b_wV-&!LY&d0b$Bs5@Gz@oyIae!7ZHBR^zc?a#-IR%N&p_n%9ovPRG<_LXEpeQ$w>cTuq -8VoZ9(q0eav8EQ!}2PCJB@{2UtMp{^MkXYEhA%#+otJq1N?@6HH=LutKZ73)F#rx{A6cuh|O0q2BRce4wS -qtk_RXN3jUMLE$?I`GNuBl9?y`=>^e|W!iGUnOcvNYNG*uwEW}l -)nn}o2;{bvcT*+GPAMH5upbRqY4T?$!LAh@ -QL-NA?fdWM5=vcXRjO?%)@j`TyvJznRbfdl&u5TF8gJnYGLs0F5n$NymH4?ev5$nhx=9+v;%*-t;rQRxws-);7zjE(r0b!*h&#~;5l-1F9 -zr2mvMSNu}j1sEW@9M0`;#a*|%+U*D+KLlPsgv#j%qGPC#ljbUmH)_v&BW4~wUr%HjNM9th7; -ahA&uhb6sPafE}=e3)i?#~#_+wcD?5yOE;|ETO|e3bo|kFuY^(+z@c0PzBegw -tZnOJ$`{SPvdq+Q(1Wsw#A)emywCpn*q76Orq5@9iyNK~ZzvdzA;J(?zzG%lc;m4)c9X?A6zS{bJsI3 -AjQ!wU5NeSMD;%xm4Aqdc0NY&?hI`~v)?#N;sDm?8z3=Q}HynMKv{EQnQ^VKflL1Rr?SmoH8SIieZoY -c%#UO#4=_c>WgJE8$=@R#g>%D_j5+R4i->`@=KJpGX10Ndl?h~Y|VN-v5!^myR8)moRx#-l$ZXs_S9P|{ORRI(S3G?Iml1Th@lgZ89YlwC)aEA@TvLe_>JljR^}bK{ -~|79l2J8a3(1B4)6pBe^8xjGE7ne9UHjkTh5VHk@}oqPN>^EnQW}PDwT(?rk+ZCBcyd|&&$RmQadP&> -Euew`B?XVYd{KyTzK1^9_1FBm&g=dJ_3;8fpfvLAiyLF0rwgN@A?|Hf9QG_axuPp^CjuCJCnT4IVKR$ -PDNHlNv(#PReO@6&Xc4-;2hcsS3yDgfOa+zYBT$G_wC$4~$I*=ek|7D=d=h-3+yodN+h^C)ooa<) -tOC27c@#j|ATae2;yX?~hfP!DUNc#?v;1MsM>(p+a%lSs8bIgot8IH(56gu%Pozw~!iGKJUcJ%<<$hq -k{lm|{)>GT7qlZM`nQ)=^XEKxZ1cgV+tenvR*gpJ>;I!#R=KS41c`E`?8P8oMt&KnUv;UaVHqYDF0Y9H?AZwIye|NN~K%-8(!Ux}grH~acd5&e&Teo~SFaf$@7kNPbI -A{0u&B#A&641q8LgD42&F!kG*Iu;(Xw>Wk9kb`}jLk@PqW7{Q<)Nc|$V%5oqH5xx;ZGZ0uCywm7eMdf -MqCd^S^zcCM2XXY+D!-Jkh2+tI!j3(Y3=YAV4$Xj$eNFi5^?C0;TC)j#-AAZezComeVdQ5qqr -_akKr9OJ|9Lz@^cFKNxX|bS^3a@U(nIuAO-rDF?9hxV#MTkJ|9Es(GKU!6-&-s%6EyI=k54=j-Tq+lD -9J8c=Kyy^YCh3?nvB6h+2j)_(PfWU`t<>S%BY5vwX4K_ic0$H~R6-@y^#O5_sx%2nT#otvB|~{EK}1m -wnczVWl*V6ggC}&ET`mevaMuZT8DCMKpf$ZUA(nc8d8`pzH8Z%()%I^%fnz;{BQmt9(Kfd-K -6b69Lj&FT3&*A+R>S*Ibg{i)IWvm~b!QT~o7QKGc!Ug^vG2Hro)$%K3Vxw}!rE743+c1$>O?dM!pqxQ -&>vcuW=u3hRTdCW@wmNwyWI=uk!Re>Hn;3RCo+i;t=MeW -w1lySl~1( -rX*#wkCC%Ucpzajdnw6;>~EaCCrXr*jv+W&OlS&a!y8`shyzU)_87;=~A>uNZit==<5~CHFNoZ3^EpR -lN1iNj%aJfE7QVEvqCm4N{W$Mqmo}rW4FCj10OU^y_H`U@77P9{T3CN%4L4Lu7>lE4$L?eUbp5@{J_&J;dxs095yIcC1dHxC?Mv*W+X2qn-M&M|M;meb~6;wn*P0ACkNmD$_bylVA}dqvVI(bBj -vUgGN|$U$5&Ua~$ZTtvAco9NsxVopifm|%mZPC;POJKGZrTWgZ4rs%K<6H%$g3|5FYNH6F5LA`VEBo! -T&veNo=&r9%rie?Krl>MS*Ac)qvz7=hLmTjx%A`iLELVHhE!tnk6Irm%hYz{mB0yp8+chFO~o^+A(o0 -V{qItMCTp{P*s3Xm*^>$|SS?v0>ZT04C2-+1|!n33ZY$Wvb*MB%75jc*{h>baKn+!-_ghrS7`-qthv* -0GuVmiEBp2T8;4N&x-uC4f1@_%k4Iho?5qa?fDy2iExEy~136OqsYSw10H8L(1rT+63^0W(aI9?Cok% -y3^CoJfw(%?<9e==joqHGe5Tlero(@_1u+x-71kBsdsnvim-mB=N&*=s0pND_R!k~dEW9>g@aI>;DO% -;?G4LwtDces7i^2hm}Ka(eu=`dEeGQ&ZN?KcW@vi@XP9}UBrJ63Kv~RO)8*2p>3zj5a>hKkPRi0CVgL -EaaaBRJTZk;vQ|O`QRqmhW7r^9`&enUyGWy$`UyvtHD>GT -|U+}9>xz>%wDMJSXaT46u+*lmoZzH(s!D-X^Mhm@RfQw>%*D!qJp-8xnrLfYgsgdFiL1+KuXju?NziH -*nL&hG9=$sv?{x~$HsPz|$Y5}HP$lVYCjY()m-sh1aa2uMEEvc)N)-_vvLjtXOPL5M -=HI##EJi}aiYKi9Ev6u^txLV&6ka#ZE*%o*X7fv_k@t%qXj|hV5PD2^6CzV}e+7qt(`Z}@iYK?No3ZT -dS0YIjw#-Lq+akd+?)X~0ybheP6=co5J<30m%to9#}ogp^+dS9bC567y+dA(J$LSA5iVmEy^jwq1G6Y -#-Xyw)DfZ8b`HV`H68u`kG1y%x;gzTPasZrO+M%oPvQQZ>;o7%*Fd)joqnB)x_ks|7ax16qWiQP|-H` -Tr0c{!FjlsrU4(&KsuLqqf_c>LT~2pyi9ef%MOphk6~ZVfUNyw=%2vZ8lmZw4OQ&kh_P&KCZwxp|1)vcli6?rUnmm%M{z;R84Bfg^_WQ} -o6&e!UdG__iU2C4caR3(Wbp=XW9H(CjtwSKF!_2d9(jZ(!xOW_sO>#~BAPp@A7jH|QM=^{P!5a#Vhx#_* -;!O12R)=qNvGRR4}R8K~ifR5#15$U~~RkZSpTCNXnGcRg?zu6UPj)nLzuH9MZ-r5;_CtJv4h~D0_>%B ->7b*)T5dcQ3`NCZ(bF^oS6f;L*}>9&}0sc?7o{K|DJSch{@%EeQJU>DTV_sfchH4wFhIs;fLcaI`-uQ -4OnThU?)c?Io|V+kVAK+`WnVrjGbrN5Q^4ku}umZa4K=@Ee2Ei&Ff5jWUMR$QBoLoJ9N&rBk_bfJFs* -|dG%7e}aWR|Ep_mCO9nj%D-FM$*f-)xeA;8PH-{3~Du#f-`)A8!{MYW+M=+q#DX4dE7&ZfCHz#!NBKD -90InT$>(~JF)tdUG++VM7pIdnG1&Avy>ugJ5HrFrWJT3*Y!1c%=IHCnCcJp-1`@E@KW>Mj>I@4{m964_16vU>V@@?GwT(NAK6lHxq%K>q<+Cb=WdEcUQfJmIhK{ZI1#1sWj=QPKe78Jd|c_jQf^O -fK5E{PxBrBA)e#V`(0pqbBn$-g)t?d)Lk$M@wQBmUg4L3&{~+nBI~FSZXYU==c9bx%mrqT7FZ&^~Q}K -Q#U$Rb|cf8`D#kwJ{N-;|t)i9tNS%eEv>b)f)-fmD5wNd9i`%RnJb0vrOxkqO?Kff2`#ov|mnNYp4B3-l2EC{&I396P8UHAr -H`>>kMjs2#q?x~q&7}pghXT6&h=es8I9OMw*oE75Pr5Y2xQvrPcOYB_KmpR(6cm%ngfkRBVp7k17eGVu+rK^9uKKi^Y8BYq&yUtn{26w`U&^13_ev;zyEq9dY -_nCiM6_rU;Dxyk+A`YtB@cLe})l7e;KCF#vo7c%ZWa!FoOGr)X@~Kz|}+Lw*`|Ww8D6FZm;%mjq@3`f -c8b|t8q7w_E`@0w(7Y1ydur0cwl-?=SL8E@nlov76ss3(n;Fj7KBiFTt9Ujb5!8X2-C$|9xB!v%xlA# -23&=w_XvVAN4S-i8pcG__zoyn(#0edz@l+Veb;&7t$hK~%bBwwsLt~9gY(-U_GE%M3({lJwpHaH1sgR@Ut{S)R5C{q#9K -;ie{yS5cXVV9fy)W5h6Qw^N|i5~@JI)_ds)G`d;bSil{*0AilNB9ATkp=%40r%2mjabt9;zQYWqDx_e -;B>~QO)=wCQ=l+7FkFX7<>#P9x_+T1O -Kb5&4eh6X*;0L}@2XEy;4jvyNK;l!1v*Wmc7na2RYG0 -E*SA~lf&89Jh^C7kVFnX`RF9+xQF9E_kI4t}|n_r0>Y___tM<^?TmppFx1^D~J9eh^D2X%W9`6*h_MY -L(}yT9>(jQrQnzXG!TVn_YP?`5BVqZjv_krF|!McG4r5--Jy7-j`X6?;abXc0ct^)EpiE`Ij%k^RZo -ky54twF5+D<;QL0ZU_ye#;Hy!`Qf(Q3#+kx-g-q)86MfdX+qAnr=>o?%MN}X$j8LPUyurFrD{ydg|y*A>wEl -sICVw_564;c~yc5or9BTvD51%LO=)XcXMctw>vMnvrOHJaufmcC?~32Zuhx}}NX3SO`9TTDZ&=v6SYq -YlReidf~ASzBnla{xk@vYdxEkEb_WOgVW<8dK{`)10A)1tzA;@4u;SG^Cti?hU0C3#=@hW;oRbM$jsb=WO8 -kKB|cps2IG(53%@G;+qsoo3h(Q+}b-jw4Qj%Zn6<(Zr#6K}XrcLn0s3 -x#Dng6I@79PE*BIy>|Ygpi(?`SgNJ8R2j&_v(NKtkfeF-3{%9V&rAzL -9~_7v18*2yl(KuOw7tjOo-PCM0HB$gF_LAZa-(zPLVBFAw{E -6$u%ks5%ajM}F#gM2Rwua|PsGZ9M}djrL5rXGaBV1;%h8hyL??C7E)%7(-;=uzeOf*8QQMkxpNXM -mweW5Mv7XN~EI=#F%l0%BG!;pop0IM?zmvWn(Odwrhc%SG<>;*VUgBO_RTA?XS#}{>hERcK9%~2?;S( -+vln*gK(-OE!tv0k2oJCZFElrSDoR?&snZb8tZxkdpb2k-Dr?!i4yMc9D0B(!@UcXwA8=bjJj%JtrXC -mLzs?kn8^1M0496WD9G&=C+Q?z}#Nw3P?jM9VO$o#GVfoR0GES8#H{8D3_XE~PP<9YSvyt?(WsGiM}0 -cuEeI-HFskE7BxSz1aQ6Mape0uUh2xa}IaFN#Nn<}|et>OIhJ_(WmmDnjQ{E*4TXLjtE`ovr5Wwc4jT -MV9JD?)VQd@OkNFy~WS74`pIcu1{Ufh}S5KtO)q+GK&*%xN+7f)%^;ORVP5$pfCHOypO8;p -42dMOuFAM@P0!A=!$Jz*mAP5QnzD5Yf4_{Ckeu{!W%$*Q%U?vnhXgCn^!w`xbZIJLU5cw@s3b6wZCE? -+z3O?d+33`Mn4;LDBba05_Vh2G7`jI?2ikXRnmy7rl!FB>4RYYj8U-9s65g+ogg!*73?C@ow@PWt(>d ->?cKk~Y9`d1hpzOV3u#dhR`f9i#TPb8L#j|%4WLsqum#eWQyt~az$r@0t}^7nKWb_zhA -$L}Kb&XmDS{r5w)US6O|fbLo$?iRr^%%M&^PeV6=bb4~kw5}|=cnV<=ic|?`*%22^413GA``5enrrIG5D+AVWW2T{ulLMBWA!_Gjwdy`<2@q) -;YhKCOe5^F0Y+)og0iOj&eO)0LCjgO0#HF9L$h}7NT4B6(X39PGk$}6;zYhOutIHAp3bHgL%;bNqp!+ -frupR+hYz4df5Qnh(w*(76H&7L!j@}&WWh&!+JEY>$@s!>Zz-BVI@v}(D~hPIO>4f-~x`b*B7H*CIj7 -4i91HUo@F+LeM}K%(eJJY@qD>N{ez{d!#T9ODIyH!cb(1(T82&<$VJ40%mufpeN0w$r3OM)!Fn+4$mlVrZNE)dY`NucyO0$PDxTLU@D0wf?f3{r?|9rSZ|CFM_>OiIPFtbKEM-?OALxCh -#9n>4%JTU-Vk8ec1XXYluh7M7W6b&5%iO!i~LGoS$cqz;24>-x@&yM%J*39lP#J9n#9)4STK06~z=WD -*CtgqM^;#Lf=`#XDN0K0-sv7Oo?Smj4W#XX9|d2Z}($Cz_#NIrX)NKZF9;A%dbk|wFE=uSF1R6!-LpP -YPCU=ayAFBu{28QxxNa4`W@#J_vUZJ0}W-P49^?|64xX)Pmek%25Nlei!Bf*N)QL?Mm`9$aOCbv!P;E -mw*WXLwIz3B#y{UAxFl4anUq_m(p1M2@_s2Q7umsCfWn{#Y++p1BwOB7hB8 -%=iX$NNu-^zCxI{RJrfuUmeDN#9 -%jM8QJBBnIxd2qjPwr*IMke+Q3Z@IVrW{#$aSSnt>Zq>l747&>BoMEp_xfRBhD3I7HDp7xYNj#G(G*b -l-!{4VK{xV`@gr-vJff(};>_Ju(86Sl*U!`%b@i7tJ|XLxcru^{-b1o}w(ejrYm9M}q?4pF!rYVEk^a -Cd=6t~d1IeA#i{4uGJ~u#YiAA5xd(@G61mm&CEZpnnOEByeL{Cj8FFa`6TY=_~czYxM6DKWC%lt$lI) -;17VF=c{p7Ij6odmeVai)R@T}9UB%{GX|)-iUH2-cXkx>>#<=5e&kxeZ{JhEpby`Qtsu%4e@Ox3c6hY -$-;XK$W#LQO0KR2Jvv+|rc8lG(*Magw4K_Q^(w!f_jI!iah5oKy#~;T4=U3ek=TP?ew-HYWJcH@%-=E -%VJ$aA52R({=)Ne#bC$k|IGBr1yHE(*_jA%OU7QEmsy=n^Y>CiuKIvSgo{p3O3)Nns*7ZqCU8a16*^a`o278&C3|(aowOpqF -028TPD}E3TzNTD)T`RlKrQL7EyFyW))=h^Z=Dl!L9lqC*BFWBLL>L~-~6iHZerWv2~JmN4O-8M2vF|w -%3r{RCg+=AIq8X;wyba3SfFJWEnXNH%*OozHfNgb&uBs@tnnU=rwNYLOqNeFWH?Md1K{oAP>ynq%iy*K1(951v7vmhsg>NOg`- -SsS!vw>~cB&4BU8MWOr1c+rnruurM00BR7vMXWy2102&Ab)<2a@)U{tbzXQ2K~JJv`<8XzBr -YxIz`W^(2GB>ce>-H`FHoNc47;)vwg2#?W`wGA{X}77;u$y8f>A+h``w)$dmdVRehxqs~-?<;K~54*l -6yPS_=~MQsInFyKs22D3w@WYF{bPYLnA=8vg2Y1H&oT@xE{(ua$8QmqcFB9$lVD0+y#FzIE?{ysRO_^ -y9c`W8%P9tDh_!U&i9@HC0Q-(>X{_V_?xaF#Y{+AtW1mJB+anUxkR`mx_7EgJNNW-3~|AFtf;3Pc$Le -i~#{r-_ybnDvnC2MCenNhXEHJq3=y>l)I7aX|M!L7#Z{f=a!Dr| -^Ez@5*vJ6K2+=4-aH0RR_O)oUZ)2czkG=HaW2K~9k;}#5BP>!DKuUF`fo@1JsX|AK?nSRq!M8`9O&@B -4(87Xv}?Hzm`;&xOQeJ@tszX87rBoq9Q9_~{&UjLp@GQ89exZXnX3paxY?}q!{cvOThf(6oSYWr_tww -}zT;{(wYf2Je+OyvTr_o>|kM?JD@jzHj|Z^OHmfmp*KqI@B4_?koV5cl837p9EQC=`dP9^Tiw+&|bF- -j}J&^9PYdW-Ol*4e|Rg@K5Ro`?h|K7P>#T=eyck`>uzvjYQn%?lH2u8uzLn#&o^I-agCY19~= -#>hF`$x~q4edKH`8H`txxlluJ#I{sy-&iQ2UyA`=zhqaWj6wFQMB=x$eWu;xijITRLj -0-F3zaOq>x;QIJK}9=re-E*`TSC3d*GCfVXqHNl%^$NXP9%CX*zi(abjEF{GZR@mL>! -FPe`nrf$tRw)ogW#(c?iMrwuX{H{>8_kF~;++VCNGx?nK60CAD+8RkQ!nunKtRCO+(uNCH0 -*EAcOdh`Mu;7$md-SD8np0I8R4O&pnTFBK8J~ksD8xy!Wtm=LvUlb}4TkcjjOD-u-}>IN6y9g=?(i##`W -N^gZ!9o}2mc>ZR-|wd?Z`_zSnwA^F8de*DVH9}}a|6LdK{wpfEE0xTrlz5~OVzekJ3qo$T>q( -xsY(%C)hdNjSdfjo2C3wiXRXHyMoJ1a3qpP1WQ&!^=>H0{|r=iJ#qoeZvvp{{~-m{*x2G%Q5`(q@VK^@!twjr_|y9B$A{2GQp4hJtR746u)RcA3+WNM@Av}Dfax7f8yX#LXu)1ZG`kM9Q%Xufl)_f`peZ*;?Q2gz9cw)XrD;p -FlvsvQ0&NnB(TGH!akLyD0Uc3;gKZl>UbQiz59Xu@osx_pPX7-|B_<@W*}?_?v&wV)|-`lteD}pdrg!8Fn(*@W4{8nJd% -K^E6X0K1Co>bjFF3mKVb|pZim9!{-z4Zd6E4iQ*Hi!?aM8?rD;xX`;!AT7qbt~RLYf;lk2vVHYu#!ut -2Poj2Cr`?c;B?gU{AN1m#Wd)0Fai`yZ&%qu&=ErM)FzmrIZzPFS{-~=Y%l${v^t-xw_`|rXPF-kN(-S -b;L<;DVqZdGe5g)(Fh?joy`g+wVP?6XM7!f;|kUaK_Lw3J99(6o=>6Y&LS*^h~BH9+wbeX1ZshrW?+|HFCK8=KMK`1^X_W6;rw{Tzfi!cIE{MJ=&m(IN)6v9?Fa=!HD{jq -ur#7D~<^W@kpuxaWC2S0b3(8qJt`-p|gXsJdRSZ_lCdy3wtW|qsh2-yLb7ct(dNaJ$uFSj@TG(ODD$CaP`!e -?$TDJ63=pK5m)*0&YH8#Rzf&mU+r`v-|LffNyaEaVUGM{wZn)9p1xHo&(lOSgI5r@<=kv}by+XZYkb* -;TwTwKxIz0pR<}jUF+@i34C^l%^PYvvcxeFC7R=O^Bko)M>#EEb`}dMl+!sLplROo0{vG+-*X%uSvzT -}~xl-)aV_dO5YPfTNXOSSb6u3xgl*s_JW>n!xk7ubecy5FtyJIhLVMPy5182yL@l=50i%+kyT!vh?&- -1BwE50$FR{tDYK+bzL3r{>G(aGzcp3e7Ytem)YicEoJ`5WBSeS}=pcWIyM%QVgl3PJddBTjd8!Ce86@ -VBmN^ZBNu3h5-GJ=6}hYB@ahn1}xpwMJ!yk1I!C-Q=&O{930wGt^0~*3ifpBThhD)Coljrq7%cIGa}4a}fh9Z0!8Nmd=bbq0VZ -3L7NLnR**CrS6V_~v%D&!78!01K$sB}oV;9Gqj){t24=3kEwUv&p)W!;%PJd_cR|4cuEE#Ws?$_e1Vuhxh{ZjX36h!sZPa_IGv+R$S?SPLh;5O@N}ie(7TvvGt7_kV@mp>xDY&fpE&-BGzDtAKs(5xEP*B6qF9hE017#JBs{u8jWdZ&(S;0Yv4rLh@1}ibz{7y1p{kd --f_cc99jF7H?;)^efa!@MXb`ICfGUbMhO>6IAmo$?*`{I>Z|VQPv3QHa8 -E0);>dgg}DA(ftri5->y%Bu;`5hLOKRM+k9vE71?rHAEgMcvNt3V|+m{5&Y4F`l20;{e}L%WCKMG%o8 -5y>2QhefQ7`53fu@f#3c{P4*0-J(Wkls`4kcF0EdMCLjL5MjpL&cf*;HpFmyzmQ}C$5*n5PL!)Lq?28 -KR=!bj@fj-bA@gsThP~L!#IQmi0f&4{M{}LT(hf85)|1R0Mx+meuA#xQi$=0yZ` -zdb^|F>>hK;8KJ*J``N*(o22#(^u?A-vnE4gZ5YvZ4sJS%+es-(hOEB(M1aH-N8rBa2J^tCDlH?C1wo -4zfn>K7XmWxcq#qq!GpZq;!8OE^Zt>-XHGL`;Ph!8)x-xKK<&yImENvUqXD -*ESxkLH#m?OD>jk^dLeHFWA(AXDdc0ng@E~|Zoc-r{B(RqRb0-MjyE%ElgTUmngDM7!P{*x6wFGTTqj -Yps8|i85;ww(nQooOMK#8(Z%C&6r)(c6pUUGX{>tc!eBpM{C9 -_>&LSWtp1j?iAkUFQ{DX9s|Ul#ygMh;V02^E6r*a%;CwRg@zl#9pLM9-v~n`N3s6VIj2=qm1_nXc6h^ -i?^x);&JY*h0Y{ZJqv9#67Gqnz=qhrZNhU)2~u)MC{I_ndm{pv-fdm73a>iZHtat|DFL*cuucNXp^9{ -sE}LE65xcg7O+6T$v0)Q*db~EHon9J68Lkt-fjP~!*Ul}A`$d_6Vc%-WEy!N%xe8&4y&Dy#ccVRpV+) -UqJ$a9Z+Cv;>aoN72X1R4WM>5(b3Up-LM1ekwiR|vf%q5Hy+mmW*CWf2hsfn{mgc-TXIlyzKPLP+8Vy -A6nJ48cRxq&9^Mx+BU)Vx(gO7D*L%;#KW&o_eaFK>S|&cR(Jxi}CO~a$QP=2GVL(coW(2CykL*G8 -9?w-*gm^m-h?Wp#p?m3eV1^K`mJE%&`0|dojChdj8p%;aj;@HYE+oAhyshbDq!V;l4#U?u5t_Eb_8O+D;he@ESy|k -}0S3jUmEv4E==1z!NWi2yp`lSSy6V5JG!A)zel+crV7SJmv~m0Bn -EsqOM=tJ*?xfX8~yg~zPdkGN5uOF_o}1PcwAndC3$2@D!WC=pJeG`;0em0DjG2aV4$wC_`Sb0_gf;Q> -ryB>9aot}q528C39;2j5ekjTZtScRCcLtTOz(UC;M4|OJ}-c_)|^@6-VL&~JA6{DDRECPYT>Gzoa$iL -smx{FA!c@8iLTY`>rlQp)o%)UH&&Dr0lb~}EB26{z2Ks(-e}MDnZqZmHeg@5hL(5Hy7boMew;aTdV3!3aL)k3zMK_Y!Py(h -ROE+*fAB*Fn0WVU^hz60gvv?v(Wmo*z&zxo+tA|A8X-|FEZQhblkIaMRP6cduexcbIdu_<#QIy$R2Vf -2(xbN!50BR~8>}&Uf-psZQ>5r!uX>V3LUw9WN_3*>{tv54md_oJFxq`9NCAzEFnJn-_!w<@Z@kPQ6G&FEIPvVV0qts|JuY2SnCL6ej$k!xsLKN47T^n(eTF+UL7zWATM5v -Hl&eRL#`Z;>6X@eXUQ3=0H1&{`f;A6m=~y2dGlmv_+A!y)MXMa}Y`ij{5g|MK=G%Z{RJwAh@dICJ7&( -Kj^%(f1%)&1gj<5JHGa_PY-n`=w;DuK@kfks15RhHKdO->QOZz5@f-&vJe<=KF*>|LeU-<$~oKIc^u$c -KSo-{?%7d0mQ+TL3Haw@o`y(reze)}?eG2KSGKvP!Q8B$W~NyB}8laS6z775_+at7s9kh+W6v`}Ij;+ -0EAIC)ilj1Nr7iPPFx+HS5PVu!i$EGDpxt7o-On(iV1Hf=J)QL%#QKY%9;-uSybr%P+r;Es3*jeA4GOq7)WG_m`pIwHuvDw5Pqn|!TA6Jtkp -Niyk>e4^Cy1;V-mFX?TEmaCw2;sR|s>CmR`j$H+r}_{tqhz{@6D!RtK&8J`x!RYcMC#>-F6<6yMPz0) -(1196J0c6+}l<<1gtzgtl6v^tGvl=f>F2ez -bc@IGv`(W1jU>5Y1YQa7UtmO&b+|htBVeE!&p9CuYK}8xCIz!s|dt -PdUWbU>h7pL+`IjqiR2wm|Cl01*yHE{a+6HJ8Xs0<5XoTeAsIKjLBfr?H)79WxtcE-l%0pdWYmT)G14 -O<2Jb*sJUf8oGTB=+sHYjhV==fnAIG5 -l5CBi*}>z4p)q!XWx|x85BH!d(_cjs@K>0gv#m^#GYTAzX*_V4GIr_Xei`Ng8K|f=J)d#TU~hr@%tY$ -4zYm8pKj8@sOV1(U^0V-c$gq)ya-flV{5sdIoQWK><)qq+DSb;X-JG&%@rFKVqbMgC0h)Yhs)kf8My} -lnuxoLeiFr;?|>;n~5%50S78DDPBBZZn(?R9MnrtdT-UYpHLQg$&l`xX7{IV!`!>B+11TXr%O?Y&!D& -_@qFfUnE-?;=mMtJ64+on{)RcoZG*Bxs%Zz3GnZqpEODZUpwf6>^`ZJnv$uz%?6JQcZmxC*);(UZ36$ -0zZthR_PpbuQk -@_)gehXH%WK)Zr__E!h^5RfPOUs>&OKb*k+J816|*6#s(GSCA@zM?V>F3DlpsrYyLgmhOr2CZnMBJ-Y -Vc@CZiwH>J@Ve4m~$?u3^244DB~##)g58k`$v{^6O%B91fH|8+l?S5L!^C873ObH#JK;(&F}doPk*jy -{z~)im{oEA%Vj+ja9;o54<_9mX1#u;J%8xt9WeBtdwtQ<`z}ZDfA&hhq$&U4D!)lqes_Q-ITD8SXXb@ -Wej?Hj{|fXIi5!zJX?kEslKhOS6h9H)pEbtm50hW^sgF`0=D&|(Jpmup-xxoDEQ%bHIXl!$KT`5;b_d^7e(>C+*)iyY;YUd}Ne}%%82e-;u}_lqqt -A;Rv^Iar^gACdV$WYXMA_f7{C-4s*xR?Al)Se9>c;e6F`0a~M-Tm?k&>>g@ZsL=*d6s4zafM9s+o7BD -1ooIOZe@2cq?Bg?ITfar3is=`Tz&-0fiZJ=#too#UY|c}@Mxwpxb) -)AjJ6{{H>_mD>aU=>Go7?E!yue}8m)ACZY~<;Z_76|-MT#r#n!W{)Z7kbWc~SZ+-o#-Vko@TRy^Na;t -6mjTT4vbAuxNss4bs3^sy+V#SA?>IUsvNe~_TY4oEHXX!J3PI>xNF${=Q}7;IW-hBUAUs^+IxzBbx9e -N(8gH*T(GC4JpxeV_PQvug9;pT4cL=>Kc;$HNz&n~=7w%mme5C^5Qgy>2UG9x6fN0!FPYa%>tl -q3b@fxv7JWmTv?k?yJ7PWa#`U^*KE+NO$6R11&W)W_#3)56K@bm7EoTJ`s!P)MNUkI%wwQN;6&x#X9S -Z_VgT+fT>2d2buXmSN+8K!Q9v8D<>oyw<)_!PVh%4A3@c;R<6Sa%_!z*yIVqIENJF&g>D7)oDre1Ki= -5AO#c=DWD$H+jCx2?#C>!bt571eBpR!ZpfNOw-@z%lEiFKcvk(quXW`*{>oq%M~zQ*AeuP9IVs_774* -dA!G+WhsJ5Jf#@m0L$~p18hvpw>E_b+;H|vJ5XkIMzOtAz@Sc`Z2BWRrSR&r8RZc*>j5*OY*Af*QI>p -z~=x!8~@nEB>t@ezRt8&eFi&HGs&43Xvz;#^~J?iRpaa+4>7_k8;sewG;@Kl`ja -`}iaTfV;o*yQ-63XDU-D4vu2(tqJqHXDwMl(1i733q>;xBWn`c8F+ZNR`t} -mxzQ5=d!%1(#@fS~h*)$z-5Td*ou!<}v%l<$?DxZuv+DIr8wdOk5#9aE -FWa@kn9)Dsj^r# -!q=n)4JKk6Mh;xOWReh>qR}=oh+AFBi%}dqOYKn`&cDVT08l<2CWF*WXtrj=_QX!u{4G*bVe6x`2nv3j&Uli&_r1N-BKzE0)2g8!?X{@KHN(=V)~Z@+Ze?k-7Q_K*cNxDVqy1bn+u=WpLgt}Di`s1;Ox -VVx|XgD7zN-RtT=9KikIcE2GO{q;^?!zEVc_?xrof2*_hHr*M!es^oapCMLRg|{oI=oxMJxZy2!^np3 -gb`uLVqi#)QM(CO;u~r72@dg|nEPIi{sLbBrzp>M+B$m^ye~Kgrajh@2P#xsJU<|WI-CN7JyqKF5?{` -c-8ATijJH<}r7DzEl-w2ya!g-@M0!*(uo|?P)eHz04bpZ$}EZ#Gb&N3ol*bOU#g0NosfU#!EI#smc7W -}%Z(CKWs8y)fr)ZMyzP0s3W0xypez^oa6-mSO@cVB+HyORJ3tPYl!2dHHa*?1K7Wmz0Fz!`+a#KgKKg -m=g5C61WWGwcDc953Tl#?zawL<6$>j_bQ0q5FI@TC~u4$m1&a@fK5owI3I%1H#&cM{Uq=N7Qoq|1#g!k22i-fEI5QAzQ$XvZ -AYbtxXr(`nyS`D~ittX6b%xh-pW8G*7$u1g|nQ101!0~giG5EkDQI_6)wY|_r5&+P#>8KVMC87JPAQ1 -P*n@;&Udlu%8;ZJ-8($%fVNRV1QL^X_pQ4q?bNw-E)sw|81NXUQvGPWYtgn(Un?mh(ihS!@||1h#WM^ --4B# -2Q&XU^MCQNCw%oA4NmJ^YV?f@H$aqE{#jvzgFr8o6#5f9?AN4@t=%Fd$2WzGc$qVsJGkoqP(@9nKJ96 -BFegcFk-Y|+k}hKfxr(1)<9Y33S47h>t4YhtG?S(HBs{#mLU!Sg|)=+98t5OaEiuvR7GMK@ks&DJIva*?{Eu32nxYS1pfh9LR=hCJ -oRbGNWSE-vm-IEW2imGhmm7GD2b0>4q)~DtN=tGLr%nJNQg>~pDaBx)i{2{zumQpHEzCpqxg9^9kx(a>?Uh>!;_!oIfrqlAZkU|>ofneP4C`+4^ZAB3UV(WVRKM`H>0nT+} -(#+XCsO~PfU2tBqpCy9RNs`}qN=YO|HG(C{@bXk9I9 -YtoI3Tf+sS!P_yB5yj7n8I3gxvD3gG#qIxqL%;*xk;2-`2gK&S9VM5fa0;UQJ_Qy@Nw@w8BWK?lB6Y8 -qZ%cvhDRW6%Sj!-jgz-q8-GB7I38&a`%G!CgkfH_oA=fQ2eyL>CvaK*yv^DDi#tn#&8+@Ha@WEs)}(9 -mNqRoVgXp^14!y@gZ-g{`R_iRoDqP$zRDEe6!cuJaHvfEOD3;Od6orC4PZ`Nj@$gLS=tVM%h! -hn8~MvnpMac-5nglk6%M5qAzor!`hDYx&{-!CaJ=v6(cue*bg6!%IhRj!_rwST5~^4o$^HgVxoy8L!5 -hA+Bv0`=#&z+yTk$b|?Lo9$nwSpbn^feL@P2c$TsY9$IKH~09}7eC5||9n5z!q_&h^c>5lGJ%JGiqC|BpgdWS&jg{dtPaMG2_*o{{4!bYik_&>x^GMJjF_&7DJhnWti -SVU>mX;yQ6K3|`7?)z)i^j@k3QzxVcl5}M|AG~DSSK>$oVNPDOC1LAMB*YORbJc!^S$`KEbWQj -pYvI7U8}~?A=iqsgL-9QAsSMU&(FDf~PC7+V21HNeWMjv|3sziiPx0cmVXlF}izAdsidUm%2wtgyuV% -q(zE7jCqw;VlW&ok?-_#@rlqo$t!bf<_BEwPet;9^c63KcqiwRhdAj{CU;!F>wZ4GZ)W&-pJ)obk?{_ -dH|y=QHOP_VR6Lx)293Ej#tg_3*$C`h5H30nPg3!`L>AoC(u;^H+SSpZ-3{7LH_>WOz+wxWSjs>K2z|rVGD=w-YtahQiz@S;>>i=V~~QavhL-b3KA4wy -q`3s&{fwK4J_AE(79h(6u_Fk1`i?QugF{5MGAfW -F&tZOtc=auZx)nEr)iX{;#7d^j||&KR)b-s0#fCRqcQYq9KxiVG`Qm)RCGZP@wZWxHU$Y&RULv3Cpb2Ha)7vpPF>>i_lEF)0;*bnhpJYclaX&x)h`?W!> -CIC+o)<=XUx80eJ_o91NrjhrU|gL%c}|cpF_;Jqf+j6sYo#cDbE$7D#7%zVjf9sSJY$67M@Ieoas3rv9_hM7!i2BxN -<)9G=GL)0Z(34RY&ryC3^W7jqEy#6BG9|@Yb+Cv0c2%ZLtO!06}8JdI1Yi&ia>4PBYeA3Gk6d#X= -#CUGN7VDVc2J)BMsxv-RH(s0dvwgu(j;8dB*r;`>Cf)3XT%aRGT) -Zyb$T&O!6(oMIaC+WmAYb3h{sHJ`h^;htl;do!ExsP|`HnvWG=6gDXv5f*!KyZZ;&r)p+Oa(E({_2stGv}*;B)VK7ESD!RB# -xc8M%v=?S{qWPVZ>X-)}@1rai_QO5#-H8WF~RKw@rVCDQnRJD|aJ*oARX{=S8<7;?%+p^XjDT_ZqRh& -~Iehl-E?9igB4ObA~ge|4RQbd9F=!=(T?7(##EJD4)CuRzoH9j;=`UOzM)=EL)(inq3)x&*6uTTHMlq -ih0g!{~KmqZ?IC#kpREb>JvODyd@bub7w_WkN)K=9ad?X)0Ox6629Qe2LDN++jSwCwS6R)s)}i$@%dd -UwvINq^T&u&GA5bhe8J(*!)0@X4Gow?fm{^akJthrrarx;$V*1 -@u7K;1YrgwVq7!lEaOhcq`zvaRtvq$=mWNm+a!Iht{P=UeL-W2F -1$~OQ*q0f=Yxvh{%+I09U3m1XUp7)GyYyI0N`-;H+;6%Ua+{6ztK#YPvYJfZBqEU*#PzXf{WCvdu4dK{F`T2L~>i|sDfxZqrgC4 -0M3OR~!#|Y4loX`U>75M>#sNyF&`!4$0!5a4A;MsxCj*Jc}(fG(HVbDRNgB1rP%b^eMf(|lJ8a)`y@$ -4tG_yPLb0pSjxj>Hx5nMOws#vLj>+HUqEj|>aI-_U#hj -^{;S{w&NfaIOx-^$4a+Q2s15)%Cf(%`e?BM{#uU>cg5G+w&=Ig_C%%N@@dH -*tMoSuXLqocXeOj@GTE5N0qmlG>wtMHHbu%4g_t%Me;MeQQLkL>fb6QXy>*Ftqwoz0M2FYM -9tnz`dPKM~rMo21hoZusmK{5xk*>0=sJ8p_KA9{>?z97ARwAVJ+W{W#dfN9Xvq?(|5VY(;kp7>S4&_28P4#ADS -ePcl4J@a&c)`dQ!;FCS)B!uQzG`_+@nIC{$8e5`MEge02eEu69nm=|w=>JHk_!m$1=S~s*ty9GCgMFN -)U_K1CxeJqWOOF9>}(j5rENaPo6U=OKf8)N -YPU%6|5vCPjSMUht!0mB63*4fwc6?8DuS9%+j|bBbr@aEdANPp3!-F<7F-L-Q3!JsFD1zDSNy+1qXZP -Ve8|-R3jf7taFxI}Y*iEWp3x5D(7+92tUt)gd0M2L2s~c&r-u*BqigXCG3(^a*@~0W)X8>p32A)TO(*1yV|Hllyqnj&!jo5CU+Dx8{k4u;Uub%d9skZ;*)MkM* -HoGRR(SERoalE}^tUJZwFSix8YQvaQbG_$(By7GKT!rr;}D8K2!ih~eK)6@V}~W8;-helV@FdvjvSgs -81bo6XGh-j$k~2c=rQcO1~mJ`4F`o3{UIxR`=_BvETiD_Yy}~Esn|{Iz?33r4h*wg)-c=4xEMg5 -5?;$I9XvkeRWOr2+ZCROpWNOniy-yI}l%l}kV!OYSx)+qb6HTsqTR626HUlP6>gMB*oYi2i=Uz+c~7* -y|zb1y$^`IJ;e^2=HGi`wVp$%X^v;jNhd~bze7z-Iw?HViR@zRASXLyw7TV>GB-CK*d3?L`-P1R4O -aTfxAzC^K|tl^A4!dufgT3@vt89ay*!~_SHnc(GVc^WPKOEHDeev`IzE5(!9$_Mbb@ -y2f_0vwtPX-8qbHQ&u!?tvo9h-{}S&crq4AsLwwZ5jc)H>52+&@SDnpT^f9(BK@R2CCMwYeqt6KkS-n -{LaW%c{)|rOXPJ!y}V_9-JSM?0Y)w$NCvj{Fi8$miN$t>6k&)H2J!+opsA+I#`2`RW>`{d>iG0)w&d1`h{4PIe@_@DDR%k$hxt@?%hI$Kp_S_$rz>vTOJr5PW}Nn*7Wp?=Fa<4=1w+ -9lP%%vQJMKaZohwo(lbF>tysHTzK$YXGdQ=o&Ui1?anU#%&+ZkxA;s;AC2_X(Ga%(dk_re$C^>(BZY_ -_#nYo>EIw+U`+}j52G9}h&_9ufrt}fYX!7sj#_5EP7l)UFWcqVKMjz=M_e-} -KI|Eb{Nulv2D&I$ZGe(!L5z`x`74z~yV4ZpYPQgm=_ONTw&YUrQ`7629FdFCv0(T(BJxVXNtCw4x)v| -^PmltzH*N#8=}Ng3vlFa4W>ln|lx%|4TT8Nh-F6g@U}AYn%H9Fj~n(<#Vn&3T8_x04+}K -%5O?HjgYT`XuC@5QfpSOyEk-^=a1A2PlJow}y520^rxq;6;yeZ?)x;Rd1t_bUrZpsA{PT70u^cB>#Nx -{PA33hMBLnya0@fP*gjg_-1f4x8u&YHcU?!*?6q7LQZWZ!*&^GHF(8Vw2fWvBYA0p?jQgWL!Rk5J#qi`ngAT|MQsM{ZeZ9QOtmU$_M19O}F?+k;TPfD -~a^5#N@|Z9(k1FXh4aRgYWi;ZJ@)jQu!xdiI1L;{qlR5@^fd%CoZDtpD5~2`#_qb4+O3LX*aL0#K#F! -cD2g1bGk<}IOM*K8RjEqsQyzi1OJ6y=wmy}Ut6fod$T^PBJc4UV2@9B(mBikNLI+-~%icVn{2;J@N9aggEFb?aJ67hE_6th- -z|ST2C2$NWAdz>c_w!u{^2Px{fI-1#;1a2xcS-n$OZ?{B~eo~b$K@y;fbP^vV0PRNS$f%U><(b|mLd& -%8b<$`CSY&Umc(`!x&uXr*iWuuUn!L!9H>|3%cDCt -n||j$JMMcE*^U^vK1De(OMb@i64VUWHP!9)LHErg#BhiM5ZGdGH>3Fp!^F4R!I -xXg#K>(~&wcZqfAd;-o!7GGlbj6d7kl|~wcTuXOrVcx%m^y;L}gKlU_=qo9XvKqdIo`RHE8^&QTQ|7q ->_;PI9&Q;OomE$vB5AVbyarI*M7>zPq<6Ht|?0a6kjNq(XAgC2aVzaJGMr$m?=|JJE{*0YaIZ;TTW#c<&+eyP1q6eR<9_N^VXf2ODR!2b-!X_dZu?5q)fxwLA -$b9h7*GT8Wz=35X3wDeY*1kH|S}o0%;jl=ZvOY$vZ{hT!xf=^MY}D#c#2KPG`9Bi|)cv4~Ge{dqP`8t^nW&zXk&?lNS|m}xF~!w -6Jo{4XnhrLmj9yBq2ut=wO8AuQ*vQ94Z^9xOS|`PF9f*wFdu>Eu~VYxTE1$|g>fQ)2l(z{P`$A>bbd= -d2Yb_Mo8&ucgZr>hV)qoQMxoaX05ADlmjj$`GsCQpk)@{cAa_^=h+E&Vop*30+fQbdj24O+ArelHKDl -%QQKN|)q(C7&Rro1mZDC;5$uXd6cknm&T`Qw>3uR+w?UmfvtM(*sX`uwT_zV+Vv6E|T#L_bcUUlT2agtM9l^%l!FJ1d+{bgx7em(}8iF59jiFD#Zz8$fzCMUEfRTc+uHf8ja%rrejsn -oWF=C3}Y63~sPzL3TX$Y?i)0;>#s#9sjCoskyQ2&@S(88%*QOLu-q -i=#K6lB$gJOLFPevbxi94q$Ist;jh!d6~*(ba(BDHGm}B1YO8H+4~dfrul9&UVK*7S7+w><`a*U7d6W -qpjHNklX+>Ac*9%Xf*u}&!%U2`Z@+We@_k*GmgE`feX==-J|}vkPA1NQb-(m!X(&$*22RmEn`FO|TUZ<`jHmV{H^I|0ci*pOyR4$MPlj3ID%`WYeC=C=h2>o^uM| -NQ)K1L+VE}SgcEDE`Te;Ks>h}8B`{3Cc=s#~{XuAhH7Ge79n{a4TN6{vlA?Kj0R1jqIWmP8O5BXNwvF -$_lu1flT#Hxz-99r+MA`Q5Q6_yf%7V_JJhFFSN0m9 -(%{}z~WZq3IBwZzXT$MF%`-9gIx)7|>3=3KRA9zV0v?x@N8W71Svq}zR+lZV{9$VWu4>Jt85K;2mBp7Vliw3P=$hGT1|0UWX7%%aeL3(*^wDTgJ;X49c>oYu!09fi`+T7i|tv^TM3ZdxXEEGHbR$0;g_ -y7GrGdG)ygp&lJ9Zm?Ok?!>HrpSClO}&BOqg5>kS#d7R>Q*lrxIHKnG!quEvcXdN8>)||S=!L~yq;v|vCpl{rjrZ-lwht --)(@d{%;s&2~jQokK{KF&To3g-ZJtf6$pnoBbtaNP*5G5_2C4pueNiyqffD37;-9Ky>svY3OPMXHvl2 -_IAwWU3Mg3O9P`KVt=K&P0`vd@x512A~~Rhnt3LW=K}lHnIy@GG)m`e)+@-z+?2;`gXZwrFy-qtG5s- -=E_8MlFeNW&>Q4Fwa3e4Se#dp5GaIC|0aKqlYdT#XkbZo3M53|JNVj1ZgV~-%BQ1DX(k=;yp-51Duv; -X#EQ8!Ut@nrf?o_B2AfyD_c5!ESqDk91C+|Htc&FA7c1ixiQ=bXmNtj5l*9^xA;82jZO^;+eOW}yb7= -8BU3Z9L3-gmFb8HX__yXF*iVACi_w;b -u5^(Y+rM`&d%%2(yX~gPEAk?H!;nDMPyt*^Y_p#qu#w|MP5U!T|lk&)3uP)X47g>pGvE_anckNgL2DJPA7r?~~;DictBPfZLgc9xKvo?WE1J?8r&r0(p+}mfc|b>vaTrsxB$A@GssqCdSx6 -n+e^~#eW%Ki5qkM3b1s(*C;K3(>3n~t5DCsl+98BXH)-I=HsRJ%|+pWe%_}znJxh&*`x+ -%Gl@YCWM-cyaA4>5cOcpsRViG=5S1_Lw`CW-yR-?ngiPDy~YTg&F>#`QG{p;>5LL_Nn~>xAXpFi`@t7 -@@}%V69v)JiipcggZ-|wSje8xZTpsvqIq+Pl@~UtKoZu$4sG@5N&6;u^^r;zji1e3=Gv?g&}#B^L)`exSx$QoHLtO?0H2z)BB$Sgak&jxBu0PrT?#X%k_4ByIN}4(m9<8aLrZvVAEu3(dX{3FXK5-@-&F0T-BD%(5sgi?HbqFJ%0d`wVv -}g$T3iSfSi?QASGEDq{JQku?b2)({U6QWN-7VhsH9kpwesAj}Bn1JCy5&tJ2y&u04-x?;vCVM*R -{6nsGDVMAo17TVo9>k0>h#fx0-^n};^-sjhoXYVKv+~*#=EyA#jqEvfO2Xa>UBS#8Zt*0I$hL@ST51_ -h|^G1Jp*ULyGqH!nNa;k>*$DE%*rLZ&curM%%6cD0jIx;n}9FCi8UsAO>7@>3DKQ2;-2re_0Vc(mR-Si_2HrTMJpka9C4)qKcj^{puyRR-cEdFSk01E(T)uO8V^cohv4nju%5sW|>&K4 -0%Md46$cm&mI)q86p(EhiIB8HjaN=$ooeoLd5@S!SIwXp6T>Eb4~lZpPZfFkVA}8pL=a2iW3_{)V#j67ohmKzJzeRQoTP$Eql -6DK8w1yp__+BH^btg|-#rudR^!cv{z%DT+{Cs86t)i&IqB7ze!R+w)~TJS9<_7`t-i1(W0@W@>LEM^P -s?-GE5vT}u-N@8*w^oIRShN$y_o(*Q#|yn&?_Mz5@C2pY4;4821*>FCf;Jlz&U2QjflRH?n4BPfLmno -XOVcv2p3eC6<#&~l*4k+;RP(|XVM!|$_g+LKEu^9|xc<-82|+!PQUN_%t#Au~owOsegEiI=0KzZnh;A -nLR*t;W7W+>cVqh1Yn7@l8{s7k0V5%tl6thy8bOlc}4W`48v9{DC0T&pZ7XNByfO`HHLl`~<&6TQCjL -5VeCU5~q+oU`3BPCm1DA9Koog(;J21@20chSgFgu_6goNvk8D%%VOgXOsFWl}Ig*Tz0jA`Kbw=dF%m^P>-%ow;)qgk_k&pi8mu|&_YKS -~~DEE~oA6fE)J)b&yN_G^szl4w<5l{4}lVZsM2ovnUYv`Y&E#yF3)IZUds}A@&j&%Yu1r3o=Fq>lcJG -3=_NbZ618!2)Jl7B%```KK+$<}Ki9GvtwHxS+tQr-^+-n9OrN)CJuJY>lc;dO24b;mxxfonEI8}1$y{ -VpH&Gu8Z;d{|>xu1E9k%3i!3Ed5dTDU+I?>ype-gT#TO0QpT}5<8ggzY^X5*)0Npc8`B{i@=}Vw{N38;XGn8{~AE(hSCKi)EBy(qwT_$|clNwJs=S`vX#>l8Q#*Nx95a2=p6 -TzZj<^C(5}~1yt6U3f6cEUai)UhN~b>U8kACLsp3FNiU62yiPVsbmB&!gY;Af6lVlTkfzMQ)JB7^0!{fm1%Plei+)NjK~3(6q?Xw?&CvI97mTwB+1Hh^ -%?N-}!D53vR6lBnEqNq0-w!;|t)L;qqB~Htw@qZ%~Am=JeS1Z;Op&W?DUyP=j@Gt<>Qz`b8KXV5XcM-nEDWOi)(km?R-B#WuriON -)6z@cgKzliYZxwm!$2b1I7^m~lJPc4yi=^P^=`z95a4S3(E!ms*VvMArUrn427p@1XHVAKkby`Cqet< -Yj(MCpO2;oXl{kcG^H`3)s;XFP14mXeTK2>DEb0?nx>34eyCg&NtGP@?pEOW%CYrYQoW4_(8^=&iaYI -|bE)+EtKLkB|gI4NqGsM;5(bR4YKjLAOt!X;S;R{70*!*ZjI_4$UTx^eX_$*o=eoemBE2k#vjN^&XjMVQX8%k9Lj+fJ6agw$%4<7%GdgGVgB(OCn-#B%k -O9X!oJb9Dxo+yheq2Fh%|B>a7k) -IRn{qsyA`^yPS4Y;O;@}$cM%wv;&jtPHSC$I6}qhkph!o=>}SwLLNztgI@NQBbmE3&wUZi04YL! -KX2-Uky|M&$br%T2#DAs|e}d$30(yd3y?SzIXZI21}#;gXp+c$9~0T=qnu7)UMnr`PMAzi-p~_GtOh> -&!L)*!RsoDeDOu_iJKIRVxiAnTW%&D1>nujv(6?1a1;+AYKD>h1@apVCrWnuT|h0xKto|Na8J1pHCKP -DAl?1e9Fxo0+xPt?Tj$Orbi10AOm40MHsp|4_t(+cjhvW_689d8Xlw^oy}n&c$9)Na6SPR7?j7H1`Ua0MxwqUVKAb(wHVRz%oV8(lnF;InQPZwMk7 -|)@GPzSTB4T2lF*9)<9xHbB2BDULnpJSAJJ?^Ub99_S=NL16PPJ)jqXD3j=_W}kyKiFT>~E{7*y}*Tm -hiQ!cv6d_R}gL80%P4RkO<@CrSH`#h~75Z}a{V$Xhx?!kP=X$EjIjLOz9onavWjmau1mxj&NzhY4|oO -+O6`9cz7jw-owwSZCkTU-svkj%EL%r~UYN-M_(y|LVRzVB$~h=WB@~6kFjTfujToffz}U1V%w50j=A& -jwiW(`y-r~rJG>Sitbim7!e!BLy*mPlYn>FmF@mQ*oN04>_;9DWVQjgPh9#HsI6cygnv!I4G-e+#%9l -mjVqt-7D-9?SqO>z(Hw6|Z1nk<+5jw^ZSBf1+#06ouAYtTi)-ZW%d|d$+;J(suOt%a)&eD|eWV!ONGt -0WJlWu6jBI3$o#r9DH-Crn@%O&eZh%}>)NKPe -Ao&7Ev)!S*yt~?;-6h6@H5x>XO{{5%ys_RWdeV8oj-#YfzN=E=4;2-bAp4%Vgc33=lrN&tjxAm$jD)- -XbVPXE$GfB%ee@S+9hl{syi|Oa*^yV2?%}8O%=?-a~zWATMhbnNM!+BUO^R32K9NK-{#@04B&b6WQA5 -P@6#jE?pL5!Bf*UEhBQ8Q~QDsOmKxHC<*)H@qDmhV3cYAEia~LPDQe8MoFt>kI2xT -tCMRzL|KH~l)VQ;MUFn--FbH{)s*MkFCH&q{arT55dmnsa0JC&@l8Wq)#fd!omHxA=(p)Ltg60PNclj -PrWuk~!LtlLrxtbV>K5ktD3l#QUQFRws0%(H&R|~&SHov2`0z*?^-`cC{31x5iLt3n!#bL%?%4>%l$1 -t}$QtRzCQul2%!AXc>jx6@rL1kF1dXb$(w3n%FjiNZB -xf*}K(aFuN-y)-3`FEg^&h_)}0Hx$do`*Le823uZ_T4B5)IFJX|!m3h`(0nouxGW`^2$bc-g*Uhnb&R -^0(R3Xe^{$rGKYRR06h$bCzz7&Z(C;$8!f -o7a)70K)<%n!Ua#^yWHE0tA-`bz>PN#xbxCMQm!LXBw<8aq-*yr0eld){Wp(}{PiH*3h;>`@)n8({*< -4x6pSdRn#XizTO`mZ>+?F)@|5Qy(UGuuuz+jX8Gc;i8%yS4l$;}v9o6x})1QL=%|^;PS0@Ni>7B=BZE -7H+GWf64qRH|E#vH|Ccwzk|tboheZ!wN0^JUFU}|#Se5T!k1u%ZB{)1`H^MyJk -rAknh}^b{s;;&O`+s(L!97A#;kR$$G8mr$obVNloM~vP^|CJM^Cv(+ej0_RwT(JAa^sZ)*6eS8%KpxZgV%(=e -Djta^~WavMVB-j;JnA`%e@*IMsn~2>S+wHEbrF13HOHrJ7+^SQ+Fby?0J8usAs&+$D)&!>Ac)K2V^ya -d#be{m4c#LAbO22kq4N1@TRV3JaP+4(JRk7zlKRw#Qb_|&-ch|gI=FKDFj`}4{!O;H-J8oU_Xx)|NaN -(_djj%^Fs&t-+nRCZvyr|y_1hf{U^`*Dn6qKPCzILQz!+)6po@KNMaNS!_aQrKoJB<5h(H92n~Z<Z}A-4r+|{(ZW#=BcSvdv+r-AfT@%52+90+IV#Hey3XZo%6TbI*vR&VTKz8L~_ -*XLis|d}Y_8OtD=|_Y%)v~xt%2j1_jO~h|Vh=f3)ABd8N_liR{tjrY{HcdAX^{zU`eQ=3PaGGYg2-nY -EoWGCzaAOXOl{Igi0zMn{ewqRP?LlsP@TSI<=oYGe8sWo3O?2`L-W7YArWn&d6#0tpyl}Qk`Pp*{Qq-5`Dg}Z-DA-#0nsE&vM11;z54;qnn -+L;ki7rq%sCF9SwGl8tr@};~Qoc_qMO53tl?!7}uoFGZDZ)GZeAULGTQzQs& -#xxX4Y+7aLcf%I7Bgyq+5A95RDBYI$HdQZTTf{@iZJ}=y0)ux~^Y6E;H$E-8HI)%?Ye$FZ#_UhCP27A -9XW4#)73(F*Mub4|o%$bb4~5|m!WOi*xud;#zMc<~Y|ENpwztL8EgT}~7W`JYyJ@#)+u&h_?f4VgWO_ -E6T4Ym5fRJA^e53hp{dRB{T%*=={YzjeZA+1%^(Aawg-Su$>-!D)7~7<3oK-3;j^DDu|EaJA{r3%9e> -k*7TsqbAvddj#Z++%?eNxOzduCp0g8H~SdqJKp7GCNRW3I -#vp7LzR$Mf-mFph`q!P&A|v9G$ZdRa61oXbFZx%W=R~^3RWFvja%DxkldI@pnhJb`CF*$D}v%By*@2v -Xc_WxGx^}Lfdc)*(dgm2`1{r!d!Xes#)|^6QpkGdm<(=HV&H)U<5<7o -$jnKntNGnPqwAWl7<3xo@Xlc&}VSQ72k0g+ZSy(9ROzH`%KG?JJo}A_kOyf*GVc|(;F@ajI)4>{+;7K -Dt9S5yG4EtY_^tG11>;F~QBx&=zK(vdaeD_J;#KM1RM<4OyOR~PH# -}B+MaG+(H3sfAEwnjYtpE)9zA;2XZh-}lHnkHB+NPl~YAa(mXhv*f(IB|7#MaogDGl%AI-9Uby4J-%5 ->koxk{H|?HQ4Th8X?_l8@VAj4+{+0;%EwOy`o)ncngkMvJFbFIBpG#o0Ma=4IQmvcv~shg&X0Y -;GJ!QI?((P^>|t-iu3o!pu=ybpwEE)J4bFdA6*AAZ&%;Q1JRwg%5eJ{iNbsR`qr#&R6mCfj#cbL6mGy -8c<%dzOYN>-mG0_c_rOEH4wst($)~8dY*A0&HB)j>tM;fTqn|>dynGIYqgQ@i0o(WAJ9j@c!PUX5CxE -hORzO@Z=fE?oxSnoNZB|jqzclvCm~`=Hm-Iy(d7Gs^?S8B4%`E=lUe0`8vAxjS&CVa}C)zSPKPuw_#F -wt~h2|A$*4;@9L492`tF-4%I4)cl#}`*i3;OneMUr~OYP!fu!_27^N<7{mo(IVHtI(a{euCbq+T-z&d -x=$;K>{3Kv6)g>dWx4rr0Hh}JwWV)h}}%8r$b*ZcS2}=U|PsDrt|TkFG2@H11Kvq9}ZUT-Gcmh?u2b!v-90@_iW!`nE%?HnMZ*QaEc~ -#vMl@PA|9JT)mepZnmhaLO^N@(g(=Z)X_!<)OtAaX8r#wyRCkKO?cZVNz62KjTOq`QUxP{Bn0sD9^2q -v)$f+&MmCUlV$!c)&CmclqOF;$P9wW6%k0ktI3yb^$(FHexL)rhGWC$Dc(#Iyr?8e)OJY5gc9kL5(c_ -ML{-_y9D`$*&QfCcfbY~x>%C6 -F!>}QD&Jmq@Z|a`0@5?TH7&s(}KCIHer_c%fSjUM5YpT -k#wv6C}=VaK^(#^?A1PDNA9n#PdjrB&NC6c{BEc$v5)N4qcH%@8jp*D%?ey_hp5oY}#a)3+a7mxzJ%B -MHdQ47mzOt6J2xdqLOX3VZg%te>hIadAcIVNamkok#{b(8wATkM)a*Id;Ua*b^(7B}dRZaaZp8(DO&>0 -pMlBvMULVt8d<774h)7ipLsAp0H^f{SV+6qPVH!1I%cIP*WKbaqWF=I~goOGy)>uYlH&-~K_Ah#|G-?ke9O)V-Dq -bQXXk;?C(#opZBGor;X)xPBk!aNBD!k7BBE0YpoE>OjQN5Bvy$UZta5tjl^%Bf_AZrUXWqY>@s-nsq4 -W$juT+?=A5yjODenmg)O=+L3UCK~`yC_~6z+^og?&)1;eM?1Xx1@QbU$m^$e^XIthw_SXTx>nGIe7A` -aZ=H&8*E~weO}!|CHs0B$#6)hj9kFXOt)OWw=As|S*Wued3XyKeXR9frt=?O|UthH19AX=n*_N-ObOT -;6weLIGp7z5fGIVz-rJxPRq474DLt%S21mCL0O_OzpP%ENZvDXT@(tQeMtt{c)k^Ci~(ov{kITza89G6o#N&FPMC55w2p+_f*3IPH{yG)%MOHTaO)hT!O; -xAj9#Lz=d4yaBJcF6`|tWF+5{MY6Vkjt;w@P^q@}8NCE`MBXhk%r9B>^HOG!?xO{;((s*1NA)A6{Zs- -zWVvo{SZ+0Zt)9cy!Fyg%^apF<@3A{2r4cDA4au}n|!jX9N2{VR?K^jlC7)Z6m#d(>z^Ux?<^4dm($& -}}pAjrXxq?H%r~WzSF7J+EX@?CgmOq7fW&y__a={1_8>h)xJj~@gih>$XYNu%|=LuSLPACP -QxU7YEzy`N0YPjG^It?os?Ev~foS!d4@Yb*jvzu3)5U5I^(Ne^BA(8{zHto~_6H+<5NF(EID82O8_*;Xx?1H8#GiG{hY4%l4u5l>yFKonL0=h2jY?XI4#}dXB{wwFy$_OeY3HJAA{Egcuo{O5` -*UDPuE&HG^s@NioW@ZiE4MyzGnH4JFxm{jRH6>-(S)AYGL+*zi5TMHib>L@fa3E*St^U7_~X^2XmC)* -lOG*nWwk=!pRTX?0e?*2%#AxbaTwXIQ`y)#;Az25GTvU -gv^E^c0tU3GGSDYqr=sd@P0Kr*V2(kHSsw3?kvd+l7@zdl#gB=Trf3|5hv<=gU1~mZSepgLi#>@MZK_ -dNiL@F)u&%}OBt1r*^G{V`wzM7{si1rjNrRj?ZyvHz7|xh6iIJkt0Nd)C%ga~6l&?ebBHxYqK9VB*5b -bO0Qts@($zY!e8EMo53kC*jLnYYKfvzhdkHPn>#c%@(jzSaqb6;2nCgYiOZA3Z -lmkV^TD=WDp*5{#S|F##DM@lzp8%fU(c*NDirKK@^{ctQQy_~PLGB6n{Q6onNW(hxD#|lqWD2_*z#?M -m&-#A0kOhmyy8$VRXMY`pHR?@(ZGYlfDHxl;J+TyB|IO3`h`Q-K{y^R58IpqZ4p7)IHxBd6)7XXo5ls -cibdOVHz82DneJ~n0fGHsrH|)n3H^sX2&qyyjAxzmtUtk7A^6aol7QeQ+LMnMRMyh9%-1E56vgy}wHE -`=TX9Yu9fMkENQ=~B|$o!B -~Img&(Nu&tLQvrb1wfBw+|dPzuEn6huNTf9rLZ3HGv|E>Y5!R7*+)9W9 -{DQDh-wr}b0IS6r#`aR)GYprxY$X1HALX@a7=fu}R&016W#}fI3DH{p{TuFiAZJ`KW3mDIxuWa>CVQd2(ux78|s+5;cLmtLP|Nq^_l2QNe83Es(($AdH-_fmX9HvPx*qwqnT_ioqyk6D0bq<#Rt& -*Ew*#KgUk2TJZM;vzzL=6s+n`H8fG2Z;-NVb_95V-{1tpzW|ONG)~O)Tv7k$I&`J>G8zIHb3jeJ{saS -BE${gAPWWZ-;|5oRH35)*>Ax|*kYia?plS7L! -6gB4~U6tcgN2*Sh>$H32dg}zj;8XqK@VmT8pgMC3v{Mo5G!A@U7|RYoG#&w7A|iV=o}8`N9djWQmmzQv1Lc% -Ce16z35EAJ#6A#rbq0a_{8gkGsOIMnSA6V8;B47Ftl8+jr|Jj=zJ2Bj-+!gtvosdgLBnppi~k!s?QbF -*6%8O@wE!i4l%RI77Rc|%yvE0G5=)H^ubC(lFZxiS0WYD`z^d<+QMyoMT44792aSf7m%vhc49&P{J3B -`P>Aj;wa?%;w(>u03w;l9eXU#R7{RDtM&>cRpVIqayyM!{inHEa|HJ^5hTnJY1V8URX-e1NYNCy*L8* -HttKqE4_sjhW;n;&ciw-T6!>Pay4y0;*HA(>fDDe8)8-sZ!<}YrlRyRCOqH={$P|nI{14J>0bf9_^Adm*xl6bg>@ -kSv)Lr0G>jIX|{x3Xq4@|wozy)35AL4d<}H4cp%1f?LU -$rG+`%QJ6@p?*~+h2~{l*Y34kD)&mm5M;2&qlm5O&5_~b)x4T2;KBY-7)v6kaXwE)r9_5)Fr@L(pFmo ->fo03LmW);Kbm7s$@w4=OxIvvS3#NGopEf{`L+8hjh44<48OMW1P1lD^ui*3LA8(rTe6MdD{>dh~%_U -H^ELTb#DhDNwugBd!wLk^syxgPcNX}#w-)wjk270khDfH=03&*kFN>{T=w(UZNz?Q}52-c_43 -VaDR35rgR;m;%3vxb#l|tIIp8ig-mfasPPzO>FgZul*yu`oZh|8eu^cv4UHiL=g%>aEkmM!a|6BO$*! -FcG*5&K<#F3+gxn4^%J3R<3z>DHY59kHLdmO+gcK|L75e+QRHsGfbN~dY!i~))Q+$X0Kw^|Z@E#clFf -ZI`;o=`3WzoswYL#Bhq!%;0>-zA3<}y={MasW32iHF>qZFN*3C$KU%?8u-Xa|D>QLFX7`6#4p?eFG+{ -QBCzqBI~f^B{R#J`KM7BC?H1Yzla6Jb3@^~1w`7QRMUj^q}RSe3G%S69Rw;^NOxRt#y2Hg6~k*kIOI8 -%sw2J7)!a@3emQtTv?eBahN=krpgJZSu(%1I{NecCF)+w>@9b2LYt_N2gh>Rz~b|T!dFECGd?6;g>A1 -oKe19w;xqdoU@{Si_Z%)2R0@;*VuUzJpBPmI+4*(m35%5bCbe$$VGPe -VVq{Mj17i({!47e+RTdB>oislVkauJ`5qFl!hFP+gaCC#zmZC%s$Vb#hM1dr?Cm5JjTuSE_SMu2xZYZB!N)mnR>U3LVSH87 -4LUJqB$HqRc=KxKG@5NLl2|SJSvQMR?7@f^J%)%unpr+-ti(W^RFBZw-2?q6H?Jh>F=E -QSAj~Z&+m9=961cE+9_x?ey{`;T@rEIa_Ncgy~p>ZckX@#ty9n`+@g!da*pxnWG*GJFM+o+7_%=_p$> -3&PqS-yLb>`DsYcO!8vVE9N*MSuSo8!XUdHUo$JMzrsv`LhCoqhm-u;to!KzgJpo=QNG^MS=kqO-R*> -8xvQN|D*-YmXT542AJpyyZvvGIHs+7xX#4o0xYF{BO;5W~vH5&05ju`F82R!<6poBU&=L#}{{9Z_|Jhy?;T)hF8>DawKgL;x7q -Xc^Rk;p<;(VoHxl%~Lle~K#0NHR2RP+lE-#HYr_@DSO>7*q*Ma3Nzb+xNXafj&a13X2Q@!s>xVdr?qMrSUVKZJ$mjdG*!Se)K&OZJr;!%l^q&~)JIe -V1wdvnJ89IJ`?*Fm5v;6xum1?EwFPlY6`ssmxZ_@r)rFmHWTc5t_?)bmO|NAd62vWa%{mOs)C0lV-_- -Xe`n*8$a(qA4;HGhNT{=1#q2U7g$^ZJ_0h2onICkB5H6>i$&@Gcwy018ZZM22kBV%wa3x -V0N2_`9f(++P6`yQk&u#*2g-V#D#B@0IK=)O4d*ttc&uw!N;9j#yFMkA$F5WM?4klf5fk-Bt#(?Zw+P -8M^Va)?KV{mDpKY8MN(m1)ST28@w$bt|x@18~EH*&!G)7ZpL8YK76~mdi@nDK_b -dx}DHJd^5NOY}_TGq@B-`TQ=vo@8lDt>SVrpUuSe$cUFJ6T}hB$1=eTR77qh~PxK?~9r(r&iS8Qa$3E -HGqrSEc$|4^x)Yx08TJ)@OG$!-E9R&D%lx@1Jju1J&ei1}}RsqXEG3?|KjCJWYlFQK_Qsqx`J^FU=t= -Ia2^!{93odtKd`W>Hg_fcPHWQC$LpxX}kX&I2-Y{9mT(7vtw;|=@X{Q~~Z4g22x0{+eo``-Nm{>}~i- -u(jp?1tqz|09V3<`Z(}cZq>@Ba7co!3<-l79P|9w`t~yM6LEJT*G`Ra4noY>2x^`A)7|jay72%)5@+L -bhH7}LF2d&&f1{NMSeu`sWHL_OVhlQc3RfdFVb@7IDWwSz&=tcth=*c`->!HE+l)K$%Y7a%c&{wle<| -3cTs7XBypdehCkz3dVoyIC5RChiS_6*&O(0SD7mNI^!S9_JYbAfr#mVk#-?F2+y3-kjC%H1lw8<}_k~ -hPaHj5BJ~%wt8k5iPF@hLAFoyKP5Jw1bphJ8a4F7Zvo-ZDqZBaq+SN6c74jh>J^m#?vuTD>skkBmCM9t&&ZBEUh07_u}bLRWGk8 -P4vEQnw4O1K%ABTMv*1WKwk@oZ4hjUNiePYW!7vzUl%C3STo{{Kcfq%MmE;`)j|@$UT<(gn=Zq?;~PS -T`Z66(p%cQorTAS-y*Q|YMR)+9Oo!Lfh$y(cXuR33JA)cfISTAU%|UQ`JbSYcx6msN$xFiW`1v8mFGf -Gx3OX#P2{iQk-UN>ud1NDYVNjTq;v0B54B|PQJD-)bMk+Is;HzB5URR2c9-TeBxvE38i&#r*rF_1}q- -q}_-gq&tLnkH4M56NWSLlvC6^NS7Df*Nnv=^wq(@OivoXWG%KcDO&_FHBl -0^Yrq@xRH^$A*GJ=Gt`Lv97`eb7I)~tL~@(&|E$__jyPgb0As=wUacT}(7;sj4s01Ms8-^RqCMYC7%^ -x@#Q(2whU&v%ujg89Qiw#xxF^-Os!c@yzA#C3%So{_Ils$`LRN78*`Sxq0bcJ=<{=)V!CHrv$D3!Yb4 -Sie$lng6mgFx?3ZE~nWHysaEE^C$k}HWAHOZ)Q!J1wg|x&B%s5CoCR00)f_xfkSQ<4 -;R0#YjpU=~7?)qhnG`;?T}^h(O~EpekOKf@{yNv^x?KWP>kH$jcT`q1nH%0gSgr!>oz|igVK=iXe_2yWOZY~sCNlk5RsI?@8;$8bB-E@ -(`orATW(>9BX?d9{vkNV4|MY$yakuuFc_SoC8Mw=v+`GphOAtk-btVJlu1x< -8Akz$b|fXXHO7bm@sRHg!Gp759Ha5gmH~CuUK&}v -S_*O5=LxXor|mQ^jwpzLVx0EmU0(V_IgZcX -xeiAFBc>j)(K@@D>)Oa;WUNR ->QJ5T!4^5ATG1mwVs)S0yWf2=zGoE6CNZZT2L1?B<9hA=+avV%N`BB}HlN6X}Uv^Q -}chN0FAG~Zj&*VZM8ih?bmY9kX3)>YZNL}Uv7xmItq?YQJ2YN8_GfFavie5~&ekpx>bDViC{Og#?8wv -z3!>!NV{Oxm5nGYBb-3B1`);H~q#YTr$2t>|;ML>_(RVa@7ohUjA6@MXZtY!K@sjtqXVE>UwqfY>qZV~Ue?|vW -B4ch=Hu~_HdcRm7tC>xih04(QN)%7Lhf(MDPVBu{NfzhKw2JqXSx_@LA7bz(ehl@Qn|vRYt4^MaM8K2 -56H{eFhY6CvNz1MD3td{=W?*y^;ZUz=cRXQc7D=PYrh9DoEi#0Iw8tBZQU=DVMoX;8N!XL-E(q1opGZ -%|ifXSf%qRWig?5&BFZ3Yg?h=S|9rXz|T678MM=Ls-J4-Aid<(3w2UYc`#)YN>l={1wlN(_a_pG3jgsRRXG3SKjVM=XZ -Mf){6)exWw+m6%M?{!{PK_gr%Lid70&J7*_%4c!cdd~1 -73eM(y!qH?JLNsu9Vd3f*3I-D*+k(Wc$YESX0H!C;fJCPf+FaKYCm!0S9qAj+e*SFcehi%Ha}K -u+Y-UQg5>7Y8j{8_9X -;Cp17U&y6doM%w2egurOb3LD%^1Gk%+RS3Aefy?30ogmzhm4ZW!WLW^6UGF6G#2n}5IvJ_P7IX$C~pZ -Sa%)@@NeJtu{F%KYt_*3kO=X|0&=Ibc|X&=lyjE#7{xu+FZ{HG*<-1;uqBBR9RN3K4UTVC)&rjQvsch -A^K4mRRAD=Yq1J-_|2^SHIyq@4|Qf`p;ry{ew)l(BLLovSL}}v%}fEZCi0{{$-;ki_cx!^2)zU%6=xI9lW9Wdx32fhsmx7WAP_x7~x}DQngki7TFsy)5*=JgJ@BsH^VdXOldWKvKK!^@i~ -ce9u2FXMNEeS`2gQy!2EGTXp=Q3xIv|n-GBY$*)9GhN!My+nCR1ME$z1*B|=NKJbm)fu;2T(=Cmy=}K -Bty#AByB+oQoxA(=`HuN+?&wq~9Oer9zmXChHMps=@A1qUUe>{}rd$BRXgV|7Rl -vxtx60$`4Qp#&3(tU6c|RcKJhldrvG%r2*_?4ZUtf|J|cmW!6>tZlo8(S0{gRUdA5i_k#eb|(4vB~l{ -w!Cqd0i6fY0nVNw -`(I-&V^>HePg>i4nRS}-Y?kTjhSSlnqo%iVKGanHKrjCc}EbLMnVv?i`=0y?=XOIHMlm{&Yy#$x}r+-eQ95Pf{1PVtUWVt-{pI30mlqSz -eCz!G-8kApsUWj47S-l&!a|daVZ>+MYq3H>?{2T~E%>H!=R%M2i3Sf*AjWr}M8u4E*b%TM|Mx2L+0Nap= -3#wb|aCNYhO}5sx-j_co52Z0R~9wnoF6pVQrR9Z6UG@clW8a2H*SclOf?N04nD3EYJcqi_QOD^}RXTk -+4<2kEYY3unKkKa#!OW-o|cYX{rc(e^nCVz+`P!A&%X+GWS6o!Yc@B=GIMT2Ztua -z#712=Pr_5d{AlVr+91m^NKB&X)IWfL?ytIBp)CUw0%-OEdP^yxj+hqXQyMcX|H*6W>4Oc(g -(a|)$`Y}X7%(LU8hlHAvFTXrr84DviTHWqTsLK;}0L1=N?t4uY*u-Mk}%M((2&YPlMo0Yp7MQhRo -he*$HJdT(EX9X&(i?j>*c({BPDp?`pXQVY3h`#J(RM# -;f(=yGy;>&~vqvP^y+*VD|aOP;;n2{z)@IYNxV|R}FoIW8A#FuSeRpHA$@j>Jck04#CeGRMrF -H2k{Nf=N>xaZuF+1BqPxBRRwM(l!Jtdn)*8F*t0L2TR~!8CjveFzFPAweWADO{I`t7x0KmD>Z03ZJ-; -Zj{BHBv`9DDrgnvO1WHbH-k#9Rl|I{A7w2!{?>aTM2ckPSNeR46`GVxk6!{pZ2N66jKXm7_8>96>17q -n%&?7LZ++PgAq`U9aYD{toE;Is8i0D`xL3ozZ9VDPTlNABvPo50^+tLNRftc^b -{=k*_^+3|GV^P;u@-)#JYa-LbqztfI?*zEnQ15JRi(C)`{0#|OLmvXLi+H$t<=q^NRf}=QoFpo{{)2a -bBL|(P*&G+BrX8wk9o^ETb@#rqt2?nysN?B|U!y80v;u=4D@+{>+)Ih9&cd*`qdV{<;JbSQS^Dxo-z4 -Xwfc`yH8dtbKXsFG#-&R68)ZPV=%^Ki`W14xV#NFWBm8#9POBLeaD*PRGOMn*=Q%u`)eU2WE8HDn-g9 -LL#v?P=}K3Op^Vk#(8+^l5WUsN0I&aA>|DfPR``c%1UBthSYZ=y%69A=E=7ZCjUr%;$B|aqXr^ka5qc6GCv8!PF!1~2&%#izrGxGyCHSS*)YdKAcG-vtU -+MRWY11)KF_rEYdUin7A|>WNNgtj?mdCfaLtw7WIGy&Q_R6=r -n}T5h?+ubO+-QEsr--mO=vvD_@?Ot{IV^)5yh5x*kw#cZ}Mno`Fib{|XR+8v{-#91Z1**(%`w4K#Fb& -V`Mo{x`K@tRzuH+!$5trF?4`-T(e@+@G(gRg0_e_45sPSZU4=rcM+vcW~PB2rYOF*HQI_#KTU`fkoilje+PwS -Hi5CU@~f1M&}-z#SXLPGE91q+m*F{tO=E>sE>!G8`;eR(qeiKIfFG<(?rbMR6F0x6$uFthD07yYr(!> -~KfX4d-R)-31fOiovMqg^@5ZD~u&51HGtavvqtU&`UpIOmp6dIY&Bp_BF4&k@mwENbQh#R -5)qtg`fT2mjI45c6?d25^i&X0{p*9m#?SXSUuQwh=y>J)C&Oi#%cNNO1@RfCMu5{G~w;OuVaN!6Hoct -iseH$8AHS$n-=I<_tcoUMRdx?A39j9j5968ssB2s)RxJ74M+86wC6NyxDczAP@-|?D(t=px@&clE5@xD#gRW2FpB#V=Jf*sXR%c|=9UZ5kiV9qledIiMH^QpzO<=ZRohX0c@=?6GH}>Y@x?$gjqS -a2I4=D)<(gFO4nH>ha^U%<7GWH4MpRTLqOQ<4|?AH}eB&yn9G)wy}B}yThdw57@Q{nCsZ7xochLx6t0 -t6BCnJmKw;|y%d!ycaHdH0PM3oRhYyH3xvB%3Zc(Ez5GdEp!e4RZ1pi0bg|uiE8F;ZIsNTh^!Rp3v%h -`&KQ(Mhe*IRLVf45Em{LSBj(aOsnTGnj`T4))1V7}fem1!uP$Y*lG=&n(=V7F&#F<55iAfy8x|5WK@D -co(#SqVtIBa1FFdzecIG>|Q4th@@oeVwE3Z@Xe!NQfP=Mam-kP^5Wb~Y#BsKi8QgHf<*<#RL%@FJKMI -d$}4r}R^D>RFVq5D_v7bl8v+B_KgVzYf5`Fa92Us;2??GMV~ehTKF&A44ud@KcsS{}T0U9}jGr4t{=1 -Ml#UL&iu)qbEXhdKYms#^VZ4-U^Vo|L`mJg_8aq=7peVJ@`1qYQ(&swDuAIr&OPD#v$2j^@{^2fDC -}kKTWWF2`zz}@))8YoB{fI3;?-dya^tBIi^p?bX8e?Qq%3|!V5<}0-*T%!_Whe*NKAdLB@T0% -9elkx(reWWH+V)-I?kG>szWnO5aKzaqKPr=;vnDox~r&0xgY|CI__HLF~-S;2Pj<$+DQ>1v9i;J=+imaXaTavG$48GR+V5C67c;_GHRkM~w-;8+x-q_$2 -Q0T(O~rweS}5j+f<`2z^@<)ZzsgueGL+CsFzx`1j=h(!u^nkE#+O;D7v+s<`05?$0*NSNjF|-3{~Aen -EbB!+f=0kl)=fU+ov9*f*Y!jHN{QA`)_MPnT#TRxEguIuIyu%7f -iHMlPUFYnIm+y?D?)K~qD^cP2ff7f0YI=KFYtX -2-kRth%nqEh5H$YHj747USZT6jeUYz>zUS|!D$!T%;_R|2u@N(Zl^8>gZ}dL2t258`|nx~$vIjD6x;w3u?TJseP!Dw(qyKe~2}Z#OE}a&dwH@SU6F;4jOSx5ih8wJ -@VE=Kg*jHL{=uCmtGX&+jScK;71kv*!A0bmo-UnN(wa4d|0c!rB%z$=MiUP~C -kzC6)XrW{ToFb;)z+pv|-r)l+xbK#C*)mW1!UV}-tP8|fN4PHDZa$X)%tF{(>7cV -rHTT7&v{qaU-gcjAj^nH*Tfk3z1UEo`Cr-zZcL)D;vA$X>#Maw-WE%hv(MvKvH{^rX-;ruvuTwT -72Z`Z&|HkZ`$)nw6mx5F*GYoFi8q3~k7+8$x6)wm6TKhD!}adS8nbg3i=A~@)byAc*lz8N4Q2!!;?>P -5NGwf!pp|V6X2k4;!Zn$Bt5hg9EN1>PudL(B8!J4R>vMPCM_b0q0!2qj#)RawNi{Q$yZIQmePk@V8OK -B=b>d!GQ9eGF@bXv!zr4-C!ru``+XXvMB{u@Mo~`!k_M9&A%)bn?gEV$IEvoCa%r7=}gdaU+R;1t ->B{L6u-R+8v_$VsKn)+G=ZyhdOed!j%=jUWe;3d4!GFFA#6O- -E;w6$2qmSjll9Qg5zw>+!FD@dYoG-=r;LnZDFLh^)ar#iNooV0+sgd|GbFW#eaOW!?wGYw|m$=sxnND -K(Ig0R>Jx?hPS93Z^!l;=H<6lcAEtK$BRBT|NU3o@2NdL-0R(~V5H?o*8ESKvjGD$jV8%HRWrtunGD& -x5?E#fGc2c|1rb9tGbTT;)0xO7C=6PkXb3r0vxy-en%y*HR7L42eSv~)gHjA~RRCcRGz_>ro0B@Wak*jsvp8y8h9fUM$ZAmqw$e?kEirbgdOP -O5lok_9=)2i|{*OwqN92*7_J0de1>s1UDkM=&^A0vZDHplg5zTfBRQe77HS*`=+=FHEDxg8tYAOep#F7GZeZu)WQ?3?J~dO4l&OL4`ny24r@x -T`keZmpo@QqRx3@SM?Pz7Lb|lI@JvnO%Kf;~MIgRWy-yk1qCYTv+O=X0I>Fs`n7E11eL~lZ-}RdEGY) -{=@>~a}~YXl+T1gRR?_!^)en@^ -;`6~B!!n++iZTsBx1{47sqlg-AR3(Fs@Sz2Nqu@g+V!X<&#=QdL6AfUpu&2Rx4cEY{AHd$MBl%RsjWm -Lf>6*hqVsHSQZ~^;(PQW0*3;DVQtBe|z)swo)2xt>)0#@9J@*oTyMsI5!G~1`4{ -Z8$BLWU2jXq4u@vIZP38Kot%mv#O%;E&TJn*ecCpa|~-nrqxBf-a!I^VnI!MjINWr>F8fJb;f(@=x8c -%%*Z{&(M>QI)YeAgp?NlpRD##~ciBhWS?DhA@+-egJ4yyq*YWa1CV%jlgWCmfGTQ_rW^t@Bprh28{PQ -elgsSu#g+zGU4ol*9M+z!ddZnyswGL*U-VdBDg-UzJ?0?8_dW!fi>z>@OlHH%8?M5{ULZRc*nH-@li~ -fg2~}<29xow%FYeEfm5^)4xxvE*GlHd&cRF@(;U;GfN3pEt-(X^NhcmyINZr$YY$bij=}C(f9``fRlo -(GKp^t;=z$a9L2#jV4m<+|7u{bke76G)(*ZLD0_FtPs*GSCa=0Nv#X@66r`Hc|@@T^8Mw5_vDW?aL_y -7ingp1I}Qm;L%2t6P&z_o}^2tPgR`ll>xa}qSU$(xt`e7{_-_{FW1*?r?9w~%ngNXG{)PG`tNg)6&VdRwclKsJ@lhqte1YGeS(HFU>Q(s%i4wk-RFpix%Hb# -(=A2yw&p{1WPkmCZ9z^3p72Kyt-zG8Zv?#@K8?KnPFZ3ChGgVEDh7O6qc6Er;J(80G9)wT_01+904HUdqpw-=z-vG4_w0j7B -IslM_jRdjB9pLZ+0i*Qb065_WcHs$&6th!BF%RW>f;@00!CnV2h`Xkqbc09_fRMLYA@8OIPSkFJcWdB -<;FBF=+-=w1%eI9BO^8Pc>Oiihn*?&*LN6P13;~29UHu6b=@2V55uSig7a%u}D!^2m?~eqq#S5Oghm) -faJQH}%E+RA<;>PdKsOJFkv`_Q~7w!e=JVDyDCIzr;#Mcwy42C0k#=9zu`8$JUvU;GyY{HroBr5Jv^8 -#WK@Y(>9E6_v{#_ti354Zxi?ml>1K3?A@yxmAGc)bAqYDb7@?@w^~?os#;XeJMK*bS`F^@-*nG_+k|i -#W9<*tJ=x(uA8%00Lu>-vRs^fOQiBuUb{y5&V4*z?%xE1^ -AlE0Wkk=Lf|;Tw2a$>M%dHg}Vap++6jG{c*F}Ax3#ST%=qGNB!ie;gU5a^NQ5Hgwl5miOl!Tb;Q?vz;S(NzY=Srd0AUP-N -^v;#5DF)<{TRX^;!lc;qZr0^vWd}*z^3eC4mLjm;4O-##FB6#c+FtlF^x^^HyjKB!Q_#NS0DbG!sL+* -gCnF<6C{R!xz5}#dqw_RM^W^D$0*7{9d+zu6y@dMe`^%w*NgRU6Gb^RlI$s)gL^eYA*{0{7{Ig7>?}R -q$58@i^xDBQ!`U6zxn&4wv)U|LEE%)*M&A;cUMp$Mme5x_Hcj&ClwA>AJj8caT&TKVoxm#U(g}|uDP} -n$Z3_XY-Gky$HzA78eW#Kxb4JG%%PE31V6l1j5lzcaC$tVu2|?aaqx7?_P2L1|`>QC56f%5Wib&AsS7 -Mk)DO!zOVzh0xLl?zje8kR`JZa~6pSNtgSeiF|Hqy1jZ56*NLLD{j^^U%4NQ-*iRz07tia74$gXD6v` -+4b&8QCawn%C8(m>2tVhh6&A!18mzHrDt!3)_y6H_4nwwnY=mac|qcCfxinC6`zZKJKlxr!L2Kb~p(8 --zUTPuLyGNU!*X8b)PR{?%40d+-VA0(UAX=+IYt4LX>E!c_wnwyBT+4Ku-kxkqa0onniz*Z0uF^5jGQWCQ1XAYwh;f_w1uq#n_WjYKg8U>AN+6C7QbGse;aM#d%i}OQR#JO{| -;v6BkObGD16Xyo3pW7_d_jYJ&V%p^^#*%8}{hRtNA46`9UpZ^FYoA@zAiO!bRLkE~6P@cdeCC=+zXkA -e8B5CY60_oww*X9|)n6^Dy6#%S-Z*3~6CT&5~FyXVs!vcA6)ctdH1PpXWDeboVj8yH*xRt|#8Xg%=Br9OCd9ActT?5kbR+@JWn9gIwyF2IT&~V~ykF@(8aklCFl+Ey+soQwD)|t -J$SW)eEbIGvve*OFAg?R4DEF7LG+v%H6bix1UM0xzOp+n0Ra(=>z@1u!Al2yzO;JZ)ks{0Ax{g;8~mfx`Kvw>u15Gmx5yg))>)+<5NW0)?UIwFDTM?d7(RxI;;<3qc6;73^#dy9`Zafa5U(!>R>z2=6h*-_#w#_^cb -lbkJ(xm=c;{tcc4Vjqd&;Q>*Sk8~jmXKfh9K96y{GKQ3wdAkiN93O*5-OY9`2ilzU&`I($7~tBDVM0f -%f03%YuVYeH|G7xxfs0sW5xts!@B>*M*Rm#1pC4s)b_}PzOxbTPAMGCnXaf#RtOv6_PHsotWGL=@6s@ -D>_CcoGoI|kDo;arbwEAc|cw8gVNA$pd(|}oNS#pn)_A{ytdKyk?uvA=jwVg2F+DYaI{GF`8FF$b(M- -ctf<*^P1QSb&L!3i@dQdKFmvXuWbBsZXNFMPyS~M*PA!wSG!JL4m!6JoqYScN*zF@5I)2coe3>UZb!8 -xPA)BpE{^jPan$rR2GD-_Z_jt}PDv_Fhg#!OaDJR#*D6Ibh!uGEX^o7j*WRPEiY$#(B*q{*LQJk63F$ -=*mHN~8m$uW~m@gxXjKc|gn(upV~grX6PVQLUPQL{lG3I=^5u(u(=##8!GJawA}Fc?Td^2=t@8Dw0ca -hSCP1uOxbMzBdD^#?v>GeQ1%7P@@^s0Tk$9CQZ(M31v@Xh`rDLE(;)uagHe1EM&4!P(3&1#bnbk+bGc -%D&Cd%D(@S;JxXK>r=8-p9)?JAMQSYY^v-oV9+OO+$oXB+@ZsJn5)}7I@OTB(4j!V`@_MX%D!PKk+S5 -&XE;hk-68Sn#^2{teGW1e=)6MNwf&OG4?|0u_~VH$=Oxz&*I;w$+JvDwj{*P_hS^Z?jP^@z&#zK@x-Q -;VyjEXr%iW6Y0&$XkcH%_hz$tCMHw33yL~SVUwYIzsjB?aY=Hr%-(kh*jF`^4RXp5cgk -m@ITpo>cW_v4kNz8)g?9;2k>eVf_v8+v@vXOcUUDfJwR*@>5dsuOI^x~zVQ|KHyQqiTb^+H*a>9!HN< -TSKJyjOc%d`;~!8eKNO3=Sb*a;kr*WVoNpJW(_=M0dnvxAkZAVSdGu3lhrxkChB-`gl~Vha=y_8(SUl -ZFihA#y-T0>u26{HT_FH`rP9h1#oEwfp-tUPaj7(r?>K+LT@My+RSvV!iydIJVcWz5%iFEsmk<;>X%bct -shf*Kf=6US7eGkHlR6j2paBZGAELFU2*G(p&}|AIy}vK+}`^0Ecu}?8Ju^;;@ncQe9?$5TAFZ1!9%SV -CDc2+Cbz3X-X3?|3AT@qy#@fo_7*f4ZuET#MB2Yh2D8C{6btqW_c7=L=aP3kCz;UaB6@X;}diIq-979 -;&L!;B7*;mhmcJKr=a5lBcOjLp`fY&lLwF+x_DxkRLcA^Utu}XbDkEz5Z4P~{2&LtN*oZ$=Is~xmfK4 -ysbd1BVgV~0m-@i|-Vat7bZt7e6P$=hx+nF;ONWcWaza@m64?P4a$QWotTKQ~AHu;O#iPFPpE3N*d}D -Sp3h@7T7+!>QoA~6$cnD&+4^9Bm-lKz9ydpQKBz#vciUiC~m0&Fyc1k>Ss_sHw%e#cvo*M6bLX{vDbs -snGE!?}e_5QWyvVsx9X||Ia0D09WxW3GLpg8*X+C+aL*cuJH!Zt7iH6}lBYn=q-9fiCq_QM_~e1J#lz -^sZ`c7XNIn{P=W&U7qyWsU%knCQawC1V08Oy}(xMFVC+V37jZNaEbb^&fkzpr2H;y_C8^e~6x2a*c;j -Y2xIdzVaqO6(&p8(cdR9E6sea3#>&&p2eu9{qnlrNmGrt`~ar(MqZ6YJ8Xn+3;dT)US|uPFG%#8x(+? -~czC?;`?^cBVDu`+^P-#p;pq}*Da(|^!YhY4N -lYy3SA`1GJe%{I;^$)B_S2^A)~I@Eqbr74>T>JLg#XY8qop4bYocA|s7R5G8 -gC!wcEmiSN?TIQEwt`;VJO6mbzCPu#WB!#+Sg-Q?pQd&GDca`uwG1rFpGw8TseKbQ83JhhN6bP -X4gO@YssDL2m7jg?jll`%|KLdY9}0sMkFfL}`-5a*5*n}tps=5c>%+-^o>4H3jfHybt2iwQuSciIYKl -WvfOKkXPEHM@z-;jhI*39NDik(+?QM=FQ!lI-DzK1+ijA7um|#=20}720NCA-kveV^Kgj%M3{8Gyd%l -~OwCeVfWsb$RXTIPF!K9GqG0zEMwgyKGaEiPV99(Zdsm8W#24iS#N`iXqV{X9(mpm&aI -{Akwm)d96IB`hK*Pqdw^6SbEk;hUAB)6pNy9k?sk1=JkXS5@y&IG1*N*fj^GhqJy-~29Q#f7Wbs -*7EzD1P=#ESs4>H0Wy`{Yq`@{7ToMldvyC`&0Bah{W=v#6jUki0x*UQYAx!X`)-}+nGC?~AE;i_HHDP -kJkaa?L>=ds4nZAOc~2W2x0S3wuZi=07IgI{Tj%ST4 -|&^Y_tkb@mUo=On2q%6+*hLHfLIJ@7Zx%)bAkkn3W6zKf80esM>i#^j8H4 -FjQ^qzqu>phANPMcPj-cKNC)XGZT2oMa(i0O6m=>7=h5Jjd)E0WBLT4c=3z`y86W>+n?QmG7{Z{WTKY -cq37!KZl+RX5c)$eF8Yapqb+77*>d+W_-$G!2Sl6g>5;xG_>{0J-$%x{L1|A64RzWIBN`Xvv^t8Ib6u -7;Z*jSKR{;P`G_?8i?U9>2~O`76`?Y_`a6P4`biqR6|LsJ54ylr6dyZ|zXgU`5loA6%L{Th3xz@ckOK -w*hV`TPD|h<*_GL=R!n;@N(q`j?5_9?t&wKE)5PlKcr)0NCTm^? -F+$QPKLqS~l9=c*F?LbKb#R1x&ayEoZE{VNq=}{=xUmFy0Txhb!w;v&yK`oX?`<(drexX}ZilALap%R -sSnn|M+)gg@ipUae6&2BNmSg;#S8 -gqrp^BZ(R%n_(fZ#bTHn%51mWSFD@hWn2TH@l?XfK7U37mb;)FJzMK#Jcr49-m1zC;pWkHsNl3_k`!L*pev>}$sy%<6_|2YLY8&aTKVGVT -tf2mf&+We`_NV#$fU9W+XDNaO=ou$44zk))7)FsK!?Gv~;5Ex)H2SIV2lx~Af{ma*1R!08g}#)8go%{ -^+{PG`x@-cWDHTJP$fxCw6R89SX^EhR2Z&)HWr0WrC>Wo7J0vr8-$-HJJdn^S3;Vai2=iCvjw}H$CgQ -2jI0rE{%%zz+dXX%I`at+|6YV#eL4CtfFw~1pIXz%1K;}~^3_XB@Xgb*i40QECwn*?ezoao*@-WS&xq -{D=2-RUV4@|_LK*kKR9^ZMrHqu`m*}+_;fXlupn^?mFhr@C -Z2W;Jw106?G$;^Bi;Ri7;)-69Y5X^!L7wn{LUV?5PqIP2`>s@xpa~2tz$%JCnRm$>RCJ#H3OwvuZ^E4 -_J`ozAcFJSOdQr%65!&?le_|`8wd8~_)~QMT(pmChWyn<`?zMvUtP41Yli&QMfY -V%L|zPaneRL^w)UBV%M4cUD%RQLD1s4rc3z)uCMCNE)ZOn}Mw+qbO4!`b^_V!iJY#qVEFMe5E_JIyPa -&HDjZ24%gZ{O**4KHLk}*b9{Gjrn6}$&;*W|iFw*evaP3)WAX;&c*iaF7)yDwdgFLB$(W=!ffz^0%hQ -L%)=xan7I9FHrrp+wI@$F)1Q!=Rp{q2N=dg!IxniSp{EXIdJy#?|REgu(hATlR7j4#SBOwPoMGWu}-?pSy5r|bX -yEKBGgKKN4?eLe6Gsharb;eil}=7fNf8rTm&~C&lB^xsSP=h)znnJ4%7 -)SH(=0VYU2QoKx{ubd0-2342pLqg)V)D{K|x&;CVPTWq>9yPY6R{5E_gN)9vvoJ)ePjrBH25Z7hJ*f= -(2_FjF&vaqBFD|Db8;S4ya{X+`HlN=Ul;sm>eKf1j`z3~farY@#qT%(fRLnK9+#*~AS@l^RZ7|sK46p -Mz183H>D7@h-Xgf4t$~<8)|M#?X=;t}dztMEcS$dBR$9cga#4^{ClaYAyQ^lK@`!G@lg*bWTM@3MS+g -Y>2q!vgde)hV#7Z&pj9KlSKFyqYee*PXhg?YPR>~<)b@-!>xrZK~YeiYndw$&p;<}xM)fGD&jf;vc&Z -Q}A(lFlkvgG!dU!Z#uK}qji;Z;-Y1vOu?J1I`6o4`uz@m4lpWwt*HXJz7o$bEa--)alT0A&II0384T0B~t=FJE?LZe(wAFLGsZb!BsOb1!3Ma&&VpaCvQzKX2SH48? -bT3ifWeyWq~Z76CF8MTP*Kg6x9MqHMM{4oaR>$)O`}bC>Yv51jPuxweV$mc?~c0 -X%seLq@AbWa~e+DVOn?{$`3Po{_4iI-?JY7JsqicXS++@+~J0f^)yOVjFT$e6R-TNz!>*8%@4#~09*$LPPe)@scVx`zjEC -Azr>lYb!wG;r8Z*rpgAWgQFs8RG#vB$0gR-RKYU%`^P)h>@6aWAK2mly|Wk^)s#oRCg006xO0015U00 -3}la4%nWWo~3|axZdaadl;LbaO9bZ*6d4bS`jtom9b&+At8k`zuCpShfmL1(ym5_0aaPhiWUKo~kM`j -uXrp+iQVqVOJ(X6M6Bi?+6rTn$%MHEch(4da2!*CRo)&TcU(O2 -S_V)DXFY=0abHSgtj|Le7@NYpdno#rh{9Qo!cAk4A0fn=`&tUn-v3khGFT{av!LX2WU?wk3u?x*+W8n@!tBZA2%iU9l`Xxx3s -hmV~H=i#CsZMk%2h^j(hs1{xw6MXrdY8y -;UX!!O_R2sFaoG^~-@516ArRc8IRfD&YBMgtbpf5_oL>lu85OtEFB&-fqy7?;jqA`yn3@C$Hz-7|XF> -$@5;8MX3Z11dmmPFqu -FP-)ANTuxi)wO_u{KL2lN1_QjwHP`jby?l=JRX1e)7n8N%OX>IAw?E=5K4`IdR}Z(`pzLiQ2&=UeNPk -P=japw7!_m-m0sx>xxRP)h>@6aWAK2mly|Wk^~>h7J!C008Sl0018V003}la4%nWWo~3|ax -Zdaadl;LbaO9bZ*Oa9WpgfYdEHxWkK4wz{=UCry2gT(t7Y2WFZZfgv`N}DZsR0y*9`*KkSKCiOD{#TG -vvxb(f@wW%e<15cHO2xQ33W!oS8Fo&YYL$oEg3l*DtR{(Uw)STZy5+yZ&E%a&d8ad3o_=yYKS8+E#Vd -AF-1ZeJi$7|%jk~wdtdFPd!E;_DRbrf)wL}8s%`WIO_LSGj&SW}*-J5~y33W8^lfO&_t! -1_sV^>ibzEHt_}EGBmCR9I^zen)DYX>c@vhDvq+UUD1$r52Y;mh!G@DqMwV@5Io6#7dDk63HbI@g -(KPLUz4)MN|D=sa8dEm&1O4T)XZf*TL9}i=xi8hJ_aoZ&==GyZrz{STGR~;}0tDw8+KAZHVc)!TsD<` ->McJkON5OSFRCu5DDDgOICxbIkE5ZV+ZqTmO|C^Qr+bR^>0-v6;4vLNKjD6lxwwa6P(7%lsy*j10RfjK3SEZcovH5vsDkE|Aoyea87j}JI%Rkf>{ymmc -VXaYDu(O$Preax8mx{{4)&>d)2g@$m~DyMBmD{!7h9zeBo;985v3WU0o{Td#w&BJuDp*4||Iwf&14%~ -~p>r~sRD(@xE{9`*1QFyoo%R7og4vQdi%!{G8L3x3?frf2m-5xT#>C7s$iS-(_alKAKv+tIcVWS@S!R -Ogpi5uHCY6;d4eg6KBxVb-e5*}N7N)5FP9xO6l0B_Q-1=o)Qs8_HAki?)GPcR;F--1@pkC>mfk$v43F -%jPJHL!S&9+7c#E@Bekr0O+HFdq9tdno(w{N}?6>B9p0wwPAvPpW>VS4s;8_4^GQ6BdI -1~zSPcPKNvfD23$9GL{G7<0kG2xz~eRUAcGl{9_Tqc-%al8<1mY%E}|bxcPi -iQ!3(&y_km_qdOb9cQdM^q#>f+jb*qLztM7AdOv+eglkeqp5I#jeIubK@uTfKAq9fB8PeT48AL{<*+q -RjRa!%EF++?Pp&Cpx~V#&)gp8Y`{m*O$6gE_%Zxrl-?0c97p+(GQzgC8WQfsU2}%N}AuTmBm9qOxxD; -sMR$*bWeRL34qd4E>(tC$C`mw`~t`wU)ShvZUaeUUKYmj#}ThLtPTaDNE2+!Xdc)p@CptDKY@d%i?mx -<6Ngu+`#c5C*C#?<2&s49&Chq#@iQ$jx!Exy8pk$Xd+dAbmX3)qeN@0&Wlq7KhdF%yPu@{Dc(6kKkRZ -(!=L-U?-06teS7=;YpFG0kU0dD!LC-#T?@Y~2FkMRaG&?T5ntrG0?=85CWBvULBY^j883%)ol+6}#76 -M>AWat$98@^u#{f?SD5`}aM0fvmFgLTsCiNOXDXz*@zHW@3RU%=iEZWn0U}=SQhlAexNx>M1Jg5URX$_*L}PGkgF8_Ih?d~JLfg(_ -dF~>mcf48?uhYPkfH<1M%G5oFvm*Fz-f_Uz;9%M0#_Blid+~(u3e`^NXaa8B|u|EduYTM<{NAqkKskG -1nd0&&?Y-=2?p40G)N=`FP--2jC7wXi!<8@)QmN4JVw`4G2jv0AtG@$ga#Z+D&8p(HYLOz25Qspbptc8*w -11q1Dz2_>UCTtLOKsEeA-zAXy5s3K2y;0a%MHx8;Nik_+O~=7^A2fT@2Q232)Vez<;&hVlB0q8dw9*( -4icB+DyHET2m5+ec|hZI5d-*_3}0#|Co{A|)7u5U#%u5e+Q*EeN>RK6%_8%|Q5K@QP^BHG@BQ`6KNu( -9*_QIe2&*E@}x5VzEU1`z~!3S!5*Cicu4l;2pzY&J<|{pe>FIuk&D8COsug_p=*XW{m3r97C!&$Z!xZ -85e?viM-S5&0`>AsPcoS=^@(_YV^?4!kd6*83@pXWQ;JVm9C7Du^dflxX^l%J$(uqCWzQsuqafSYAV?5Sy -SK#}NEQHGnZ(igTDyrhG8x+LIRdYtzBw=r*jF`Jd|r{{K*YH^2J& -V*L*r{&wI^1-OMkhbT?Jd1`dt$pPo}v~1Pepzcd(XgVhd?4%qnW_^a8}!(Q4~cHtWba+XeM&>qm);!g -z~jSg}eIVjMI+MQ`hU21O-DX%9?Pkipsz6+re#l1_X!G%ZO*QE*XS>`L4A6X9%3rPocnE(d^91(>H!! -)VL0^MxBzy8e?q26gKZVY3k)y6e$SQQo^0FJC?!P>iQGMgJ#nN+{tM&W{A<%nKhFlUSDnt`DrF+BI07 -K-I!-E|%uPSl6ryW2}3*ThaFenFO*nW`5Nl7JAH`mGN9FxKDK}~rNu?lt!*F|pC?5Q -K>5<)});?I&P5(7w+006>=UhWYHhYA!!M(j0SDSnQW2L<@aGx#KPvy2Haf_{!C$BQH8cpCFF;l)TFNp -tPj%XJa&h#~dVp12v$?Fx5#{31?i|czbJl3^B$k-YBQa3j;M0Xb!%m5S8~-H%5EL@(6S*n#T=9rmkAa?HoH}~8EetxN0dI_X;Q5 -z12Q`WH7(<03a1`wDuK@B%(cZn+MO{- -PG?Z?Wr4QxAf-5u**(3wN)_eJ=x=@KAt*}0or6mGv3DW^)84=cnBE9q>*;`QQ?6QXaM?UcP2DC2#F$B -*n455IH^WNMTpyb1+i$9@KKf-*)o==U~ii+Ya(K}noPWEc;|~;OMk*{%O^Bie#DHk>P_-(oF(QM@j2B -eCB0(_o=tKWp;YyZRHuizBjLJ1Kaps$_EzIL-O-n^son#kv^7x&nT(8mVKk@PZL>1gb25$uW)rF~@rn -dg%K9m_leO$+P=V(wTe;B*F0F57@04v4FPF_`KEnm5GeRuq^Wa5Pfv|}pxAPB3EHSH`Ek#sxX@icwLK -9tz1d?FYYusKw_KHk6^Q~!eIoAP#f^Qzz)_uQ`Ysi!YKMc8d^4Gs!Ca{WR5=-;|#xX^Re34@jCCewc_ -W~Qb63oO1i=yxd;!wM}0*KWT*7h(*$r~CMlz_3!Z;dD1$hf#bqwEevS~8MW9202R)bUR01Z{WB@INDj -#ks1)6B0E-V(|o~g6?Ln9&Y;P_g*ipx0vc`fl6i&ZZPhD2y)N!sIv~K4M9g>`r5P`5@AJ@IM~EffBlhSj>a@ypY<7b5>R~g4yLJeO; -bb$1izgDE7!2oq!7wSJW|s~oG`7>In%^=95FR4L`roGXC`ij{kfW_IS9*?+G2`fRt@(`Bp4^@V_-smZ -N5;wR>FFHkU5dcOM&jK*bSqum_2J)c34&#OP`nQg%0yDHUF?IiiR6iy0j9E$qgV5^ug~i-+*OlcSk+ZI>qQYFG{ZW-u -1^9Hk8*uS@Rn6SEw#YM~j;6K?uw?Y0CIjUM3}Y6?ezVBNbW8ObWV6Hih;Rq`R1+8sGxsb=meL_FcGpy -C5eH@(iV7@<3VTvUG -oU=Q3?Zn(yH7ns0*aq*1xob6aX%SxwvZvo)xH}_DQ$wxjcwvK68FWGOZPi7TCqg5K@1O?+YP=llg~EeqZh4pBX04s}CRo<8(G&&Si3FYP|D3z!`G@5Ac>blVJHGwA82I<~l1@WOl -3biWWdu(!++J3f5&NCxi -GV!HhJ^(m?$K{0MGl%U8yBlJL}YD(0~cSTf6vSc2uQA-(Eq21LnK4iL1wzS>@2{V1-!UR}LjUHw`7;q -`4Y_5&C(%$u~t^=_X=9FRD4uR0RRWDU!BZZDW9K*Z)baW9st`3+QOeGTd5r0-(t#6}*^I7>Uyn#I&5n -!h6t&EqnBg(-HnmLQwRS${osjCL?yUdMBuxeTCuxb -yT-W8Crt)W;rFHQV5$IkP3nN@6@?LR?LN`==!NTo=$Sv`oW~z<#4k$(Gp@w%{3-Tl<~{^C3_wEYOvrh -7h&d%f?_H!eyIzRT{c|LPZ!dBNv!#jYLGieDz15hnOb-#b?O3peAMc0Xa_qrE;Tsep`H;av^hzfjvrp4L9|m`%XmnpLxu?8}T3eIkkb>iUG^Q -x$fOVm`*D$&c+oFXYn+HL*RizQn8GWJbLtV_(c`^PJa-bixtw?kC{H`yevQAb!rR<2>IJq~KN2AV8}r -V?6LV5HP)h>@6aWAK2mly|Wk{qmL88h7007tu001HY003} -la4%nWWo~3|axZdaadl;LbaO9dcw=R7bZKvHb1rasl~zr2+cprr^H=OR2TLBAmtHcS=^=HTsb}IQV=L -*=f{3Mvngm#USXK}D?b!thN|aQo)rBOqc<=2e@D8r8uAwke>D3JcUtYiGl`OlsxX3L29ng7~<6D8PS4 -O++$Kfx_=9O~L7#V6LEbWET4t#~+(1R)Au?M<@EmGcbl-4wmx1x9vD|C6Vb*+}a>@b3AT4O!@KA(TRv -({J;4t^63hlMQ5ids0AXztS9$^DmTFulouendB~&?2k}L1+-oQmujsZmWt!3sPJZ!CI8Ct`vDx2`y`i -4{S`Z5vdPM&JM%{i6uNgE3N$V^Tc6Y&frSem7~Yi%bMSM4ZV)x{hqccQLBIOKs~ILuVB%0El9ECFV|8 -~Z~ED43JbG+W2S@9pR4=CM^ -y-)2|Xvcy9-k84P68op>J6H}WT~Z=DG(+R1*rXK~jwAM&`L!J*EE$~(Z=R@w(0%>ZM`ri$t+riZyTT2 -l&X+gY$3+f#^Nt$EvYV0+-ZSS?$ff_ZxSd=5mj(J73%YS09K!YS&-)JWjTS_0YUT|eClUsT+3o+nF3v -sQHt1^)p{9C)Q2vgOlcQ2QN{}iwDRryR8<4^aP&%p;b#1&SZqb; -a=B3i#=!>`T)@kLd1oboYPJJt6Y>B0^2t+YOY&(gm#(;EpknzW -)dh$sH=FS0(w>qvO$;1}n3sgh#l1+yYh5FDUPHcW*d1y>_b#34!xlbP#|&1}sCYH9iOuX=k)QGCdi6# -8b?(@%3OUu|EfSJ=%6+$RX`cN>#*LzpTcU2klVi;x&Dty|J;Id&`4ffjl$E8Jl!l>!Wr;B5!(X=G2!ju&)_4 -r)Ng{7A8^Iz2Pk<4azspvnli)V1>ZbhCF!u|k{D*5M31&n(TYBoN(;Mz8uiLo`3N?u{0UG?0|XQR000 -O87>8v@UIOF1dI10c{{jF29RL6TaA|NaUv_0~WN&gWa%FLKWpi|MFKBOXYjZAed2La_YJ)%!z3*2H$w -3W?=OW}#$e~cE(B>9`+32WC%&y&CQ*!CIcXrj7v>njZH@-J-o^x0&R*>FWoqVHb>yV9!{ACD6sY#%6v_81>Q`ZKov^BDZ0P7nF7 -hyM5$s`&SSWyk^AM9Jc8uO(rj*Us%cJN;hP~AYoc=^V&}ePk3GSyY)v_aLbplSe(ANuFT+rtZxPyg~t -7t-jz_2Yo|_NCCCx90&|*4b4Vz5J3E0s*7b0bd%sfjw-;AvovFSh$}x8s$|2lvYjrrd4zeugO_OCoG< -|ljyYmnGThE2(7ohBZsk_QeZL3fKpZ|i6ss! -B%k!CNoAu#Se6qK{%UZlQKxv4*e6y|U26`6-w9KngnMYaL$R^*&y}fl^ZA4xP{o;q4MyivyQ}MA>?G_)&?Cn<8DR -$?^$wgIND$^m8>$EMJ1wGUaR8zOBrmdxI^&(aBq*St0d6Qqsm-(vELnmq5T$pA*o}Rw@AyxV6IXu+wR -{%KgAo0&%(|Uog7a2gBigVd4_{HKvrkSj#0)B6A%tL6dp>5OEWqK}E($+(?^!D#d5I$Eacm?ZWimR-T;ErF#v6o7p60GP;vop#v02nE4ih;GhO060iTx=M}1Y-6)g5WEcz6 --vuYdtPlz`BSx`9ejtw(X)vja11_fh3@=wt`v~)ht-ALq*HNHUZq8zeSH7ZRJ^!o%S)NPDw) -c;1NDCaBrD5?rfJ{c(~?P1KlsqpGTp#q)ouWeJ2OO6FsXCc9JRm=DASGPeMkCXS^$(y1@cwZAWCzdh@ -ue|8R2UIG`WSr;C-3ngQ;k$#ihJiz?v3Cu7H?$OPG$}QF*BtP~O0ARpcwFrg|p7s;XQ7V^0Mz3$~Ue0 -W4Fb@&ybHOP^Rm-fYvxUl7wLZ1hyX?AwyYrtfN7@{FHhdTdU3k=aQgi8M9eLo04RvcE16A&_c;(xjvhO0)>T -^$wK;a$R3Jv{fi_Q^HjTvff}CBL$xW%vADuR^<(sqwDi7K`-J9&~y?pih`NyA57j{KIo&0=q3M+bYwD -;oeo6}csP8TnJ{P^atiw`IN^$MOpJ`xY%f5%_%ot(aU@4x%Q@d@ahVp_kn>oaWJ^$Qv|Cw_90eF}`+y0}4O|B6RxZf~N;QYzI#Cr)V%ljT_Jzs5Pb5Ug$$qTIsfQi$MictmlsHQDLJi~hl(gc_a7(;Axxsu36*dytN|0OR95G*TXlXV -Mhqg9Oy;QB(A@LMUFArGel6{4tW_=eI9pjvRphfNC%t`hMUq(lu;0~ZKp1$51MaC~Mv+72L28j;p#d4 -v4mqS~2AT1ymyKn(6o5_kD~Zr2=C3ShBZNkY_A#GQPbO#JQB`#?I?JOiP*78j{Xo2J$fA`tXh^uO%8S -de^Q^!qDN>`eOZb5Qc)aw@K94+9+%ZvF7Guaqa7anq19%)IdR=&Z~KhKHG5sn5N)IKL~L+m#yb3X{3Q -{l+ohRHYnl)uF&J$!IhD#Fq)vXdIEf8%=^mUy>f1z!oS8_}d9gT-^ff$^~{cnG9{bRiH61V=A&r$-mD -}>sC%2icoT#ZtlSIXp^Xz{y!Cc&hLcmS(V+;=A^<}jE~S_JR}CxZoyro01fu9F919V;B<|V@MsMo$E7 -)J=YOU}%a&@q|C(01X<=4p=>Jq#c_y=Df8uj=Poy~9b=wdxaq%<)P76j!1^2{JY~NB|FSNtp2?YCWND -$y={Pz3AX^gAsx8>)$an~9i5O|O^X@LyQgZT>z_#!QKixzypQ?}qxC|J^>FZEceq{X0oyMstM5HG7T0 -!uPQ!iMR<{7M%5G+i~r0p8s_0gGE;oAV0kysls~#v~bz=nDo`B;!Ku3f6~%UpcsTKiyW}D)?e*m#~L1 -Cad%w^0ZiD@uP7OcyICN5e$(FB3A-TXb8$SpU2AZv)@W^B^of -t{H;_$$tx!FS3#&6!|y?X6T4prz>xDa5XqfwZ+gVs0g`=CNM>qm@h4ft57{A+m*WDPPDgJ0wVO@+JyN -Pr1=;+p{P$R`8!ZF+%*}0d -}*|Kddukxx@&PD>lX(Mf)_e5MuppA*ML%WY=D!*U{*pJsK%4}9#BR*0SIDUQ=r -cwnL&G(CKmEQ+~Wm}AeiuT3>DOeE07>ac{A!bTk0O-JN@O^Qpc;cLHH5v)uuvpZqaf~ZO?c7B(o5L2h -Wy3&KLRW0xb=N6b)b+ibd5|vexitWSI)K)@K}`;vXiu;Wh&&VP+y905bDlYN0kGdlqPlV()A7c(4m=+ -@Qm6MpVw|J)`4yZ4LqyE4l=z%qsOEq9mJj4g@p@7D@pLdhy0@5GQMGdFz1zNycEF3i30g4jh3{0OP6w -xh%Fty7DoCQNS6829eor9azLb%+`W6t^^7(D}^TB030LP}W&rGn@B#E3i@CdY>%^n#JjsE=VGy-9I(KOrHqeuAXEH9I)K7VvB8yoV{5E9LHv3VXcnf -L_oK0n(tT;BP+i@eO6#X`$WW~n*affTFO-;5o=zlbdOyA?TPLhx&*1pNayRDac5U_c@g^>-vo`Z}3)@ -)Fz%PDKg1*)}({8H$ij9x;!ScoQHe&UJkeCY#eQ -Pp=C<>xQ8;oE?Oda^4V>lC$kQ{Q;f4v|a2tr=I6PeknCcKu2$&;gF8@l_}{av=Z^x+;y+0u@=H};RI6 -bOAaz)Hq4*2MQDgMdTqAH-vxT5ciG!bdO?zHPL$-ou=@~Tn_g0`a!y3*zcAurZm4$8;TT}8T62tpJ`- -I2&a&JC_4yeYWe%botHtnaKZK9zGfKItd+AY@>~&hI4QBW-3j~t3*2YWTByPk89hCrrLpED0Q9Sa{;= -x&u@9v_+FdOf`F%rbxf4>Dwm5u=Xw*-)Ujwkb?i7V)u3u( -#6&qVIkIs@;}qQyV99xn5g5(;ihdU-*yigVKap%s>=}2c0gq;zoZ~o$t~r49~l&9a@;EWvn;Wc08Bqh -Fv=VgaSqLnT8*fmH)+{o^6>vjYjXelX-@8{+@U{6Vzt%Swd^nzpkccc~Wd)!6J6r*FLF`_djy -(0Wn`T-CQQ9D+8IgM>j1jHvvT<|YZzT-PYDHr_+g_S#wX@t>V@Muyc-jeGC9j~kU62tJR))ab_Y#Lz1OEi*Fj# -0XOe~}Rg!j}0M81t;i%M2KjvSqqx?GeBMJ}q@kr_(!C9HbyEn3t=f%~(lx=`~Z8g^u1?wH)3!!KKg{Vaem*#vL5*9$7OcLc+<~GQU^$jUxcy)?p^hyCGzX}#5};10=ah@IKquDe`3Aj(c~ -=R`8)kQ%!`A%*R;2Z?W`mLEes4)o=OW(M5QsQ_JNmc!_Zi3??vvUYheW)S^ -+mc>;-+drc%`rk?MM7}^!uOoCxgK0Z-Q~yw26DdikL&weVC1-wRxquvE*{uaB+z3TM*+d@j*@N!)|tU -`=CHW#(4`=DXvrY?NVrWBTinvd2Z}p?C&LydqhDa%;}F!ZRc5;Mw2jk9&Z0fro*AUMYyyGmvn&zC?&8 -Aud+zbWRXPPYF>aaXIrO+v2_5276MX@Sh>(NXFI_Qc%}nD=#oZ}hkJVMPfvIGYA&MhqY%(nj0+bA^2j -Iz5CUJ3NW)g%Vt&9J3NtBH;-G}H8?2I$Lhe?5X=UlIS`)+QSc%Y2G4orrG3ZNJ(69q8FMABG-3k7354 -bFThM}5@Rrqrch+32lgEV!j^!FY>jbI#u>j+NswFiAf1M9HXu7XP&1no&E<2ZE;TCNthIa{HA8EoikV -TRlG=~31|@ve);=z;q82a2qFjrI;!AaDmu9t@nWS6JcfoF9n0lKl9Sp>so*Oh@eD2FmR7B=GM#&D%3P -jjzeZr|~%NWOy2HBCH=U`O3Zp(77W6=W*h`Iznkap}MnHPS`Y}V;A&*((if-CONRfR=dLl2JCWjpBXn -jC;GOSf`ck@f~dOMLl!833%R;9NgWKh;F%JNQR3kgtV_UejwOuwRgj=lQMV<9?1v~(CUYHXNN#M -6Pc^RQ)4Q4_IXeb^ORJ3^S3R#{vw&W_5=al9^vy9RjqpnggK!iG6#|R!_&&X@7r|29DNwRjcRWU~}x; -dx?pRqy3SQPOmU+0gNS^JUkpny-8gm*QJTtPM!x||H-W)8W1cBa#_IxOn!86B?A4sDG16wsoNB|V2J= -VxtWeh;07n~V(8+aZgf*${ra3S+0sK*=6{*w_J@I>9|bS)Gdh?WEKGnP&qQtbq -;Sr<^K+9=7sy( -sQ_*vvrUl3YlU`$H1I{~pCi04^m20f%f5i5`#BoWS2N{0Y-iaUQaW7Gw(46$` -T@AzqZipqzQncay`u#;DB`?VPfXW7PHw*!I6^kGMdn8f;WvUijWNORf1jOqp3Ka^f%F|+11tm9^_Ql`Brn#6V;D=ilV -ya4p{n!MR<9m@C0v(r@#FWhmJUK)*^)Taf^rfiLekJB0K^NqUenp=wgl^m_i&$)-=$6 -T9#DZLo%4pZ!Ez+V#(OTV!_%hD#@QHKE-)l}7yVK#^M1r>?e*|3pXlW~BFTW@1Ft^xGYT)N^ANf8g_*%OGAzSJRMQ{AmqYHErkayJ*PlXK#fs= -?`u0z2MF|3C?~j -s)b^D!mprj=}C^oG&fP_u}7wcJYoj}V+Zg}6EuF6MY;uo;P*5!_TWC>Ucy91UuN<%AD#FK0oW-p&15?=|FULg%UmEN~ej3fRBo%yyxO#srK@)&^o+@A2hgDm0%w%eVI88M7+mgNqYyWjT#ql -Ld*zG-QAxVr~R0f(|QstwxKm1wAM97htqkxSciXhDdFKr~yT2!`~SFNagEKccKEH?@?kVc~>CW# -iV&7Gyw&rAPr^VKCefm&i`mA>x*%nVded&A(O9%8{>^DjmTIsmLlqv>8JTueE;Uz_PzOdhzisw(>6z@ -{|^Ya4yAc7}7CEgd*ZB>w2_Un9?1(KDezt4nvNvMT=}>+1h#G|qhu3`OA}t|a2)sSX1RUCUyUkz+#(q&9$~?lg7Yc~~s#u4PeB -g25-abck$ev@rB;G)@2q7o)%G5t4L-ML;NQ#Dt4~!>*ifhx^4Ily~*gx!8*sr`O4ZP0iIPO^XsFmJk9fT2ak3 -av}vy0ob!%64-m$>8M!VL9mT3*JeirAU-} -n%%ZOSqGTMArW^rQwVBhaG^g7nEM-yhHS2uM(B|?Ymq;Wg@Ea=n1Mz`V4T4e)t=3fZ~pBE!ApN@x^K5iQpT&X@vpz+Gwre-Ay}C#%n%Y?J# -u3Y%KfPaShzL7x#A119#N!03F`4dl&fbZMPxl;f(qLdZJ|p!iUNg+z?9t`YW*iff#X1hUdW>Jn#0GF8 -b+qceKa=Ig*-<@AMhK>hCdrTpH+`6+q8SOPH -hA2ER-lZ!G#-WpE%7koLN}Vb_XCF$a}nN|)_gIs?cYfTCzW7c`h$%l$eeoUAbV)_YJNgIn`eChH3t^39Obh9x#+_iCgH&JjD&5tBbai(M!_ENci4#(u}LfZ&cuPcOua)v=8X -H-b;QrYj@5`_e-h*8xSojMjYC*5-=D|d|^yH4oa$jZPylV*lq=4dXCh9?+iy9O)u#;f$g?zA28gs-;l -3yA&|sxaCBOPDl^L6C=X`9; -9{V1}p@j8o(w8iV$2o{%=m}YLA#Y}OGJ1`k@Pu9=^*7sR#uE_cty1Z&C{42b`Z=vfZjo)H{ESK7eT`s --q7{`4HL;emUqT^d{=yqUsa#U4FaLy`DI9qt;b{1d+jA9{FJji(b&Es!O-lmWZz@p2^FvMj&HA?w4>A -7^TDYnNiFU#s0w(VTn5~&nZ2zcC_){FYKaTYTr;DOhP`h2;pEfSYY3=hDb)j1drR6;{Z0ymoNBsNC0? -g0?x4TT60K4q)i+R>h65OAxLob!@({d0;=)L}5jhGmSAf3WSfxkUnt1KOrK>%n^e}QT1Mx9ZzOTLxdegE~HuW{##KNuG3F$+Hk`2 -&o&yn{x;a0r&u-&uyU4e)q5Ko9`bN}JP%WFD=I~-aEEMwzCT@~J?;nq$Be5wmT9U-PK-uv6bziIe0@s -`e8r-e<(+42B7yFw=dfcp{sYJ{y_uCsVHg`!>rP&^AS8^*If-R6K;NW@=0ynREm@au1m{|Jw`9`On1d -lc>r3oER>g_i-ikEgE>pH9Ugh*6+}!zafH@D_5iCO}jr~_y=Otwz*0);g?xJFDY -X>F*umUww0MbmVEREypKT1s&-7Q>TdOj!8;NRE%d(CT){dfb&5jb>R&><%l@hL78D?rhBQV>5|KN4JC -hM?*yVoU#*y8lLWf_yzAX$33Pv_(~Wd{Vi-L7ptDhRZ3MTXvZK*h#*G5;SK$&+JGXZE47{;p5^>+^{k -vQD~J-L#;d$4D2hLP_xORW3((~XS)QTAb;h7krAxev0{`uSR1YE^f{1)L0uCb=!#B5M)nH$kgj;Bd(~ -%})#uxp2O}lb=^zYo%imryD>PqS{V3o1N@x+6z=(q)_r@@-#OLV%NP!91U6CTdM;D+U=xW; -!aOCZR;#wIWl;Q!H!LhjBP})xWlZEBSpWLyNPIW%yH($bKO6;qRUgy?C0-Ocsvl-u@j#hCJboWcTutV`6(P3VYzsAGtYV -5lsGRZ@CX@mB|Enx@Oqr|{GqpfdSIgxw9AA-Z%1OEq5O9KQH0000802qg5NLc?uv*;QC0N`H$03ZMW0 -B~t=FJE?LZe(wAFLGsZb!BsOb1!pcb8~5LZgVbhdF?!HbK5w2-}NhSo5@H@W!l-@xl1*1X1tDf<7?h( -JJ())9*H6$i))J15~LmNPWoH=Tl&lTyZ|5oQjW8G*Ut1#lZhn~cmOY(`BNiRts*6r%F1H!pTTD^xL9RMtjn~?rPx&EZI((UVsXwLM7j7;Cbg(bv5t%QT8g+x#YR ->tQ?*dCu3=bcpH!t%b~rIg%JL?YY9ivMUQH12NBB98cv{(dQ&zPo)nHkbYnXhV!yqu668hDfc(Vb>{6 -39qS!Zi$UYg$nejVW$=G|-&t1Ow}qnWBJ-6AR1o47V@cyf8Rn22{(Dl3`3%#wN{nkxUX%nJIli7O>-Z -(5A$J{k!4@WUVDYKDzwDM64Q>ncv_neH*G%Newoh%=3tV|xBae6*I;wKNUKdL*EO%^K7Dg~p@FObriGEpW!&WTj2=!znkhg;vZCL*eN$)cm@wlTxt-m{`7F< -HC<{QwfGPk1EsD6#ZYAQ^(kPN26S?td$auOa%QtbcJ(Fox0g(Z~X#vo)^gLO~bvzNrtERY-=}Eynd{R -{DkJe@7Jn^-B^j -DTTN;%H(uSoL5$uuF;SMgaoSb}{s0FnA=EWd&@OZ;^arw0cIgTVj>)|{?&#YpQ2^BdO9nx=h#qBKi#z`~&;p5rN -@0@VlpeJKI|YgiO0pA8^uQ)QqPj7Fk0#o`)BS%}I=O&#ChyQ~0h7AHyB6u|E*N$>z8d6r~WrwPz~^$K -)`?Tv~LTH6L$p?}i!BXPPEF@drTP>ozULZnSal!Z1iK8vVX0Z?ZI>`+|ZkZ_D{CgOICOdPBRx~|n|j0C( -j9CAfaup0ouJ{Dk$a!?Tgs7q*YH5fSb@ZWptf%YehGre0G^xMtr0gB;0=mOGt1Xqbg3C4jnOi+k5R6$ -T9o*-F4@UMVfLn*}MLMCyeq_|tPDugE@4iCjIL)bhvu-(YvR9s$tnpj0b!-_>EV`zEq4kchef;}+bM& -5)S!PYxBBV7Qxy%!N^icvf680T?$wUgX~{&VC6(HPJdgD_()yvaUh#fWWCO -=n~om|&TM0a6*G0t7W*;>M;n`2SBZRt*$bAATR_3U)lS7R#nS#6?d$?c#q}gB@N}4RlImrF$6rRsVYO -MB4;2+%vD0qj-R*Xn4TBgUbI*BQk<-h#q`)Een)QzaBRAjmep&4uFPlVB~JLqw&C3y-S$N3vn>*pzM+OKw8gCfdtE+oFz`w>5Z -KtP~EafM>yGWr$r&}W_sPEO!0fS6)n`v;LvvR7g1N+?)<4|!Zs$Z1}sF?fQO;V}QHp)hK_E!X$i3t_6v+J1I{w(oLl5vc17#K(57c0I+{p+tv*5Ez{gDL`+?sW|)oSUi1p@C>vMxB7M*;u$7}rgl{;gfJTJ?+<$ -@NKPXN7B9nik;LNYt8^Kd8x9|<>0^aBA2|>ro+Ejq9n4>#<;iPczX2IFFYmyROn@R$pt#^F9V9IO4h+()(EREZo4wD -rAHc|`6|K^{c{eB|uB!(`~FND1q#fumeaUIedZC8y8@ -X=s}U1D1zW6Z$Pc;b3~gk^tn;7G|`u)>~$d-Zn2okDlKOIg5246I_-3Jg``5)Jvfm3Xu-vzkOcM1|Q+ -0CVfO0Nh!HKuRCQ4`=eb!`>No*GhvJ5VHiQcBz3}iGCm<8)}xQL9?hRll3wxtLyz$z0UWmWrD2sNU

F&$@&4WU1z -MJDKBnZ{vH->~*Qa{cz;*0^Y2dPL -$_l&|E1DC%=&sQAuqzBO)E3L$Ygt^^E7ZWygRX;EgcF(BCa$PhT;DJ&Hl+pkX4?`OmGIb&+?a<@^ibJz~QFjxi?F`&zJUl2~U -mUupBl|#1nJztb%A+`R??)^~8d%bt@Co?Wi@mALq)L-Evmw9RCmQu~1u*>ISHgBtx -qpce7t4)*)MVZ>@7F%D=D0@?uteI0Mb9a_bpeb89k5c^|PiVb){;eHdy!jB0N*ytnpdlW-Y05Wz2G)K -H^>s=7`cA4kdHRWEQKkm9Mn%Q6k&p$_D3P!)9Y{aHiDqG~T9kU$^JRId1I_{#sU{lbZ&b@XfZiz=QJ& -?7ZwQ~bHxpxD6gpq&1F8E9?zRAQv1->egW_Q+6W6Yxy3cG|4>A(LgK#3mEmvAHhv#wWXI3scvH4a2y8 -{Ruy!j7pNj27W?iWvOP-yMd)?7JQt!`pCu%sR%vBN0(bOsuo(RZTmSSeL*Rq^B_e-6-5kao01rZIOfV -A8L3QB-dzZ9B2jMt*b^+K8~C*HZYtOTPPR~1~bb~k>F?NK15@g{thlVINFtRvwiFmbhC-{%egCdmE4o -Jao)fNsyOOG`x#j0%j~-0TmZV_@Ib*nLP-Kw&~HdOj02BA784zK0+ohpgT~r$ydg$ZqU -WF-3TW#$8ua21^jHavz{f~50^j47W+=S!4x8wBWgoZ9X@-B60ck!NJuF9{gQJu~tE8f{(Q>(y -IKqQ4FR#1?*{=7u(_IS58I~fJdFp;<~QXY=&7YOWmPnmf=JUwf&4=ku~tIrZ|43{2)Dxt9VV{*k>5F7 -X1(%1@0%8vQ7wwDb>O6?4{s0tH9juUSdPPxN3Lk*(@uvdN!l2c|3J2l&LtPy=O0L%Z1zwJf`Ke%7=|| -A+7%}<7DN6nG$DD=bB{^r}X8;oV#daJX3w0Ok(-D{_d4*qyiHB1$T$W|Dh^y -&0PrrUf{H2*2jYm{QZkulpo_;<2G!B*tWNgq{)?|G6vfHiC-C6X(g!d0@2c -M)_>^4mttOtMyEBY?uv4p7MP_W_;17JowZVR*YA0&5t6fXV+30Ms@NLGI{}E&3SzP5d!iH)~Ne>xHbm -#JI_kk)6%qL8gSHq^#;lf1a%~tVFY<3-h^iVaqTn5|2sWG?=KT`C!I7YDwL3FHnPeK7y_MU9ymd*1fG -8b-@JAE!E<{it@M$rr4UCJ>)nNwu+XRA_h$jEC;w0NmErEcmVgBvM3a*uz^?|hxL(a3;4dGhq=Wn+urZ0PX6O$BxFkTlY40Ci<=bDq>-Wq*78pQ;F^!av|4DX25p^=FQe}j -ny#Tkba^O7Smf7JnAW89lO>5bdgnS4wxZqJygE*T7R^*!$dp@N`#nbZuJ9pG9d)bOSYhie)$0atW}`` -X60ytphR3e4<3M3;VSI9iVH>Vwid)n7bs;lEB_sE=VBErEBzLnE@n0H!~1L-Q_QLBX#ov-AGg2)8)xp -g>{Hq;3|cxu=|E>L!Kt(P>q$M6fuIlD)%5%Ct#}!K+&lrz=j~bIyRrnWQ^lKBL$xqts$TG-x^1V1O>zv}3>(Rku1qp?{OmG-r>I+xxy0>~%-Fx(k5u1rlfY6 -9t~J#9-Gdf?9oL@jKphe8;}CiU^$A3gMfscmoy8V1tgXvd8S3Wj*ZaMPimI*yvn1{I*6uYl1Goss>Q% -Pg15zNTM(12q5ad;H@DYvY?+ViX?W$iJ;y16)v?*PV*q|RaSgAFLV;?8BaVKamJ$JN076$3kTd4*gX}JvS% -fp_42`V0W8eu7JHZJc3-0CFjdUUiNCZD_%afu?5ADo;a=a`6GUE~P{sYBqTpr=78Bn{|iA -SW_EeNTaMn<7H?my_*7v(u=W{W;V1L!A;~g=)4mEiT@y5Pe@o_n}Ms$l|=$PwO) -*d7p>)*JPbuHlV{KE{!Sf#xy27qnA0FUA*xbhWPOJ#Sedb>V4=VULVEq0-aw -+v!jdSSO1{eUzKWzQbV`T5H6Lu)CWu+w9&mhd3|zm^8aG1>%|?f@1Bi|EMhMcc^}H`g*!o3B=Vg|3{I -rh;WY;#*)>A<7%x-Ge%9e2)l|C5H_q?kt)h}}2N)lss^~OoTVjHJu^y#Nn?&mP0v7~#9g`j-z?P0Vr6 -uG0I+*G-x02-E4%+3mg(LSiZY$&5k|NLVI>l64dvXGi+R(95uA`5WB*P;VVBKt4mSf|ocbM;y_@Tjzp -yKNoGnEWXZDfMwFJc*IIg*49ncF_y+@@uhfER+iCf`c%CoB}AV%5>`&t*egP!?vY0R$Bdouwx$ncT<} -t76&Hb#rj$HjMVTbY?}stqKTotq#a2PE$Yq=eEQ`RSsM90Hr$0on7@g13f2q$ZicY-6a!56x|cx8c>* -LZ8mKsKeVAs9+q_BOK^D#B}mN0aCG(6lEu*i46I@pQ?u!&+MbJz5PWKH -2-_oa(eP~ICS-e(?b!cYOVt}+R-s($nWFLUo;AizTA@dir~X`24D!dbTiMW$EJRK*@I!yU>d!`5+;ZJJykfAE;1qN}r<>ja&Pg -C;t}C8`~3d9QDf3~Lhbth2e%#i_dGPP$4?ZxtMiG`GKC{Qw>#;qz@&R$_v2by<3Vv=D4i@|?j$@OjYr -KR)eu&)L5RLNNe>-be|3*8+0|~n{hG%GCB -Y(h4*F{l>_#gE$s)9Nd3$L3W5QK3Nbiv=Uh$eir)8bXr?oH)|qa<`*@E -uI6$+=h1TSgX^wEABycS#!Z-7`Lm||w3Sg~8BN8mbN1{#OlcaQyflQha;)%&SJSZr8=6twYGC -WIvPXy9<_bB93ZA^lK2A)Rv9+#kMX{BekW=x>CAzI@G;wPu_)h-SFtEw`E`PGan-5i*ayq!^dj)m3Zvjy@e>|s@hliqe}7YLIncoYBo#Jf|<3VH^X)xun#E>f_R%7hY3ptS3tZ+{odDR|F0 -chIjDDZXGy1)>p}nMtJ&-?svv7nm+uDD%!?;J25=43$!QZ#y-f)q3=}EnMxX@$K&iKDG-Fa0Oc=W!Y# -ad_Mlpx8nBxe%w!_)Xn~7%ULtWXEsVPMZk?(JHPmk$@Xl|k-gl?gUW?QNf7Ej!IdAJ*LC!c!Jc4cm4Z*nhkWpQ<7b98erb97;Jb#q^1Z)9b2E^v8$SIurCHw?b}DFp1nc9CQg|75cP_SE)Jpr^LS -0D?S2+k$7LS&hbyTl6V<=%r7$Pg081NV1bQmyD=SB*mXk^apYK-L0r>t~sudPwd9SI-q@1MnNT3O?@8DM%dzlrjPAn# -AwS9`QBJ7>MEY||HzMS$Q&1$~d0VrNN8_fQ==tBub)3pz76Exom}vEh`MI`}$3@8yL&9$B!5_7H&fpw -8jHY8uHB{>svwwRQM@d9feE|8M{Nl{L|wDhJ9#oqSsGJycaecPXb^7;B^&{N6h4F?2_e8p9Y=zrPa$o -Q%U -pQVFPUS)6=vVNf^_4J52{DVOu+ -xy3j(|>H(zndvH`O^pOBD5ni^~P+JYl#dWVt`()k3qTxEE+_r&eX)a7*xcHrE{&jkD&DR$fFD);~Df#Scjrf~Ct -Lf(Mc`F-J7+K!Y9sIAuI{Z{E_^gWA{@rPyUWw?evGsh~oD-EwoTq5Et5VwixhmQF`wpg-C`g2EB`DTZ -DI75=2$HEw5oiaP19QPRGeycfm;u=DtcdTGP+@k>ipY>kKFvt{CA7fi --j^0fzM7b~_>wvrXXq)t5+*$(10FG9ly&4W*}x9L_%inBUQlo;7!QBuz9-_9RbzW7 -m^MF{zk)l0E17xC%#4E*IGogEPY4MC5zJk$KiZcUo^}2njjx%zL!0REY6M0u%5cr}9k_po!_Y~ci>>& -Ox+6Z*vbZ~!TwbOFl(48L6Jgq>8s2-~N%Xo8l}6n=GDT2Z?q#elZ2nAs1{@&*fT7Q6w7@A-dGBU)E^4|Vw -ey70TY&-KJ^Z@6aWAK2mly|Wk_t338QTU005y2001HY -003}la4%nWWo~3|axZdaadl;LbaO9rbaHiLbairNb1rasl~&ts<2Dd|*H;h;7Lo&7Mc;~`DG+40=mNW -oqDaw)Y+%vS*ye^76_Sc;EZT4H%upA~j<>BJ;#fR$J9B2JOR>0Gh|*N5-du~;uNObilO(ygxJWEKx8S -_Xowsf2TMI7v`}C6}Z>5)F1PQ;DUNmM8R{Ylc2rd(~YmD_6QEMo@GPO&Rq}0+maVH(TuN~A*dG!KsRq -3+`mw#-%Txs|yn+CI;T_*xRyeyT6okK{-=N9ds;D8xe5b$S% -!Jrb}h!qg87|7+~ehVYX!eair6WTsb+YD*)vMym(6!We|3ZK#MfZ4@ -A?07gj89?k{udsyD)LyDOv|?-oS?A&xtosG*T%aKA0ZU?Eorz;79+g4|%7tR0ZeCvprnpd5iJMpDGK( -<}>1|m;>6+jm2h4(i#!D1z -BOg9#X0qwYPve3|{((GGYQPU~lW8qc2jo+)|0=>0M{`t~Wy~oVEs42_iV3YMPQTQV3&Klna6{mJVF-uGU#tEB!9DFX6j_v3oc-oB`zY;Br04uTeE%5xsz86k7oMJV6n -mkC)44d!BxB}!;`$$RvBNorDh!dDB%Jd@++LHiHPmNg;8_+qi0q<$er$`1{Xln5v@SzG_sVbi<{26vm -SivSxVVWRGdAGr@DEmjY|kbLqTmEIg&c9wWi~)2oWS$YIfLS|QFS;u%Jnx%#;0>Jxhf|&b^l@Dnf)C? -3~aL4;-a&zdzF{Qi#n1Q%=Gv4LZkjBwrAcZ0{2U-^Na-?h*oVcPp|BQHEEA?O -1XrVbTd0freSVo`YtbdjIz{`W<5Ndo^qDR;pX%F3k>EDa^1b*9PXEaOx0Z>Z=1QY-O00;mWhh<2hoxrPz8~^}SRR91T0001RX ->c!Jc4cm4Z*nhkWpQ<7b98erb#!TLb1ras)ja)o+cuKF`>(*to03W-+LF^;szm9!iL-S#O}dHQ?w#vp -C=!wwQ>03ew$;A&Z{PUe)I83Rr4ozUR_>Qk -!a!%EBvIRNsoc46AQ9L7oEP)ZX4a&sM?{`nm64%Ur?ozTbKi${aS$&%$iAhRqvff?P>|SrltOi51X93 -7?V=rXPb4P6s=dLyy6Zxfr2y6fkOQX@$3p98O#kAJg3Ls1h5>B_z>hi&hR5ZThC?Emw7$?Ilw< -nl}uG!#MkmgJyza^a?R|g$DXFwK@vyjZ(j|>%Q%tW0>k{k$G5lVFH5xqg3OcP3cyc=yNy|YZ}0WV+1u -y-%hOjU?{JLcrLxFFXCNG(Kf#z6CohlBU!D2SkNvOCU%z;D;(vF1_B9NrllN|xa2PI(+l%B};O_h)^A -)a^hSH7t6Cx#W`CAEl1^k0l#HkhqGma+0?t*+H%5@YJ(j&RRsqtJa0~HiS?nZ!FT*#GjG+6u>0WOh8j -}Z@il+#io(paf-EptDSshjC}MOFj}K3Nn?n0`nP!!plhTKMY>$)Bb4qK!g^#KD=qZzItH$ZPRrnY5#}6Id4xy_EL!eIdPtC%!@nIei2C_Kq0z^o?ge+=--TV7mQX_|Ad5(yW1UzL_*6gohCJ{1Hy -Vykph{edV2l-diK{%IFQ8NPt9c!a+H#CnicZU$;r6ytyiZ!sRvlAW^=Gf0lcVN&DjK;2H?Q>Y641AQ4 -I6!Bw0}(C3miAQ;P@uJ-c{iZivb9WMO1TE+7PFW*8BB@v8X$nO&dd>MR^ -YJA!x57$U1PMchaX(GQy2A&K;u!>n~7*QfM)@mJk*pz>qFS~xXF|kewGmzh~*afFa -6b}?=rLM)1cQ_E5IRURE0o*~0tg>rBeHUIpRotp-?9J=-yNyR%N4MYiJ5@~~lf;(14jq+wIH;xiR>7? -cTWSc*aBH?v+n@tj;rl^ZEM2gJ1Ca+Ym};5lS+1rp!I_cUsvM7`qG3_4;oK1bFhIr+C_wKfRu(WeL%^fo(OH -4+8H34L#^F*#nWWtqO;n~a!VwUFfVqV8uE3}@JOc+3u%0;+$KHAjS{#fVdS4{SFsem;YqI(1L9_uYxl -W+MICg3dUuIw!ra&K0Z0J`V8i-5?@ -Xi8+{eYZ@n=IcJ8yeu_3A=|iSiqcI3^D5O;A`RPks83|z)T%qZuvKEOH~cT=^OgcBL2QOHLQz%LqKs7 -+Feq$f;8tEjk!|n?xXktLP2DVGa8*i3+9b2eIlEuNKyY?Ft)KW&jfbT!20esz`zZjkCh02WEjkKqd#L(ts(dP&Ti#4`5*NAp|_AA^OGuyEGCMuq{jYY+y%#GytC>BmHSAoT -tu~(x%tL5x;+C8*D==<02^o -!MtNsH&lnu$I@ZnBh}bAvtz%?a&-G)n`tO2~)M!bFbT#OGNJqG`rwjstIBM3n!0 -0yt8OfWe}i@G -M#4diB19|G*fdLrJYLx2!JuDj*h&ZC1Y#9OQ35f%_&dMC<2CUzcpVu8O5oWg=xofvcQsCqp+bR%R1W} -`?MlIZKw(TU$v%0{q*u~V{yjT3#7A`8i~?*Pcmu)#u_}`y#^|$%SBMN~n-C1DFqu@$--8&Cn#lo0jmF -th8jh(%B@oDpQ2?&|Buu~|nY>3*zt;){1$dh?oFaqRX&`!>SpA(ieb2I_>s&DBw#LYv#NFy52w-p1w -{)0M!lH4EH31FE(&krnk<|t0y? -3EH;K3RXqvVquCY5bp*n%+>zp_Z|E1&2glyffpDu5>(*KRTA>_N|jn?j#w_kq!H|5QlRyk*!*iMBY_z -Qe-X>w`yd$bmDTGkIa5;9g8=jo&XEHkj9Y2$(zQRgo^W!*sR6n385DeV`C(4}z*{ujl4HzHJTG(l%0J -DS6(ou}GhRvgBzioh})7E*%*wS#7+?KR%_J#C**Br~b#@H8J{Dj?CQA!9)x4Xy%M##x@-C`?UJ@i{C? -w5|k)^ZLj(dQvB=B5WI7j)@4n9iAoHSw5G;#`$g^?>^TbV&2L2E(WxXd3?(`4j890Q)Q7B6x5fP`j=W -+XwkruOSw>dj9grV5t#9SKre4Z<@;Fkodq*+JXD4W1Jp?t?O2t;QJtRXs(r9R&!U;a1Hhb*;Q|1y+j~ -Cw5tQGLpcK90w&)=&!y?_jE3>nmXr!zr6Dp{a8WcSN&A>Cs>CD3=99&o+!bz50k@I9h3B>v9=K}KzRH -sSVebO?4fEWu?e_>{YEuWkEj|B^Yvm^**U8$YPB)bt=YOO-W9&%LYbBy*VCTmtX_StFGS*$24XFyX08 -gFTA8?IeF(>qSB`5q?tqN?CGr8xXBsb8768z?Jp3*;4HIu#H -~EzjE3z*l5n*~(OAhTouk0r-kC*7ytl>=_lTzv^9Ebg<@VY6MZ_n|=YC9s_%97jf!cYKnAG>n`E|RUq -P&c}*1LL9a2PMqwf_`=Q-AUPoCVh<&XyX;SvXxFSlU1;n-YsE2yo`2ZvsH&`hAV3p}8w#Eq0FcK9tUH -x~PT-A6c7AHpaXziP{NX73a9Qw2KVzE&ZUehq_s#`m!Xo~Gp>+YSjtFNAT34B-=I2_8|#VHr#;H)8t2 -DL)L9^1z#R$)nI9JLgu&-cgUM~^(=qD8e}8H|NmQm~t)NflFOpzOT9V^wosV4f6zi-gmtRU-t3ivrEtzSl?Dtn0CsizRrWiD!2<6Q%VDSQUSjk?CmP -)fcej-KYj^s0IE5T4jh}w6}R55H;*AyPLhiK#Dj+VthVy8fsEYNsD20IGq|vBM3Rc+D6)s{=H`%f0Z;z`Na` -bsQsK$Bn)>{1dq8fUM&1v!4?Kwa8dKM^bb09c(V(y&2doz6e`13D@BgeF072xoJ>Z2+n9JN|!pgw(F; -B=|JSzvJ}uaEK_%@I5Y6~7tP=nc!g>R`4qopz*TCN~ZICYB)gIHt7;hRr -k0ZM?J;hUP8S7lz~9yt|7>x3AxP_xHE -&&d$I8;UE9~Kfx>nSzj#U4_C=5&DKBVswl5-ZZ|)(6g&{U!_oNY;pdP3>+u&u&;PAw{0WoTsWEiRJgz -mvc{-FT4AvNWZ!j+uBB$s_CK9H4NI+YwZv<2TE60$xtb2q&Lax!2aq5Bw4g?6ATm%XC6J+V|_}%l{P#5v##UTX1wwb{h%Rc6CC>t%7UaW#7E -mv;8&5a#xblB|^E^`;+B6m3uqetECa#oFV3j`My>jrnXr$6sHiivRUDW@myiKW%IrMD9zIht&?y`&)q -A`gsH(PlikY+}FFliZpOCt=Os+dc2@ImVN6yboDy4p4J1YH;G-JZ`e>TgfFns|5zQvnqCByRyzzQ>%? -s&4KVO&&ma6UyOttFuZURVsS0gdQz3#K7rLZK~SmCmy+>Ru|h2X54hUZWL*`^C1vM*eUz~M-kI?caYeIG8+{e)*S+%imhVF`3_-(LJHTlKVnuV%eY+O`T*J&w9ccrpv53MmBBb6=meS^QcOU?et(;3p -X0gdrrTtEorj5Lk|sT-_tExH8gQ89`=TRkX*OLw3vq8JmdjpRugdX71GKigyA451>4Nm!6{>rbtVm{2 -=`*@z{Fd)pzf82u~#6H%o}n8_M@3}g-ujU!RcMO5t^1^fZ1*CG$mP~;V%WU6OFH;G8z>;*TA) --FUH>J(PMAqjab*qqVe?bVEnMDS_~K}FnlVU!^0X*PAqSI$6R2=iiq<3m`I0-g -HQ*NHRXAx;JEh{@)_cVlx7G+V=?RiLgu6?&8vzyZFVMRo#V_xp`SiL{>xQR7JS#b*YeM$SSLpn6H>%{ -A-Pk4>hwi6gyga?Nut&*C)%*gAQX2e-F^-C<;q+uK{u`mf0!aFjF;IB)#0W- -p=(u@lWB7#M7tZXe@?;w>A_bd-FEE(jp67O;jf7vr~r;x5~D}AT;aihrX5|9i+jteup=?br|(%TU+U2 -veR`W_O>ym>T`t*={Y})quc=3=T;xd_mXa_P^E@tRyTs;EORI^b~XBR9mBg^paxZJ$BY(wDn@8j=<_o;^-r0*h4QXb>p=W)8}aVL>^#t>-}4FQ%@L -r!lIiPTEtDw1F|ivn>q!CqXMUdMTsQsNE-3Hz~I54 -{pXuDrS|2ccFBVTWcqgG|L{Sij}_9HxsgRs%iQlsCbSzS6L6Bv?(vj%_j(x3saXKw_z%cT+u@)*Esi^ -R!mZe6gi*xj{GcTggbC)N&<^WpD282Hk=pTne($_Bh?zX$CaC32+=EBbK~8_7xUO-5QZbjSFGeFJ9Y! -Ypc!caCAKZgC6=|NqAkC6IhjV`i7?mAkcWp3dUEEte}A`Y~!kfGjS@zq{-FRkd_l9X3C>mF(eWJ)61WcfwT5*g$%z9H2yS~|Hp4nS-mzcU2$3 -HPj22~70S?k)2SD`I)@$wlS4tt}Iy&J0L@jb{88_^_M9SbTG -Ecn0NGBv((+<}%jOTHv0a;hiK|ftnf-l9!0*nxKZi0=Xw(VSA%OKJ4AfSqic!k8mR*Xch -bmwxzN@)^I8x(Pwu^OI*TcxBb&u|gMvURnj0%uu9;9!2dWuWR~Z`#wk7DX1J+dREB{ppb|8|%?nGB6D -2*?z}IES`miZUE~vs`bwrU^6tCQi)B}+Ah#lFg?I3vCt6gEF^a3i^5XSVAM2gyfcJ;QZxMx?PdVbA&^Mc`pZKVvAdQ(xew!9IhS2e2749z;L7a0~dU%XWJCm+GS_ysUI7Cq% -p6DDj-lzbZ&8aWfS^ONY@jC!XKx;Vt?Ws89{=t36dI14i%1yQS@=D6}5RB}#)MI`%o3tyaKIpG2)o$w -D5@?!&ZTjhzDfLzduT=})Z+`{vC*mnz&-Fa-0HQ=Vh!1PIc=iOSm`v%x6KcSn_CO^reS4>G7h8qA+dW~MtkH -kD%V9;l)y)q(o-I`<`UG!jH?~;Yxl&qgV6IRvQY>-t^RROQD{FI!w)yF+$0A~KQ_q(&(t!vHIUSJWug -~nm^tIpQWw@rdn6(Ri6z7qqlsW$Vv#NrT^7oK1vwsGEbsr#ZqfnZq^{R;31jH|#mGCwF#5M?pn^HYR^ -U30gGhmOARTwNa~zrobgU^169>`TuK0K20Y(tR=JM@QD9qsDDev$h)rMWByuUwrfK&1-ON$k(6-N!^Y -Kj&uVyvLB->6YUgKd&&5s!1csvdBvkP_ad_B{7nzbIn1?}_82S{0PYS8u -)@upuFh&90B)Cex|FtzAqWURKV5?moq~8}2}W`TqIY%cHUX>SQ45hw$L5lWmv5!Joi78askcBrM|s3|;NVqP%0zEzIFn8XS -jGjLMa$-T=7cb(7GVhu%$ASxNKyw%?NQ{S6Lu}Vc+LSozq{LB;?a^c>R!8HV`~B4UckU{%c6BuF-j%H -*x#f!80#LJ{NPYI*{CMu8o}>azf>neY{YEpV<2Zk%5=%gFA4P#!JE2V7hH#>rbbT>RA-p -hM|5e+EQ5brdR&q;J)YY%m@%>pqB43mkZg_we>leMK$-8iGIGe(O0RJ)v12@NRJua}>zH?)@CKxsYOs -9GphgVoB3RE#5uB4!yZsUPG(+}yHd{_0G9f5|WQ&dJFiese!dsAR^l#Vd4&uAD1CFtH#=|zADV4`#~i -=p9j{@ssxjV0p{S--nLhL5robq=i1k7|aq=|!d=$!6!yG~?929Ot^fi`I=6NBp@4ZZ`pV&7jgS7Y1CM -FROn(18os@bOIx!1>S?!4U6{8G5&ELeSZ|aaB}+1FJ^dFX90S)oj--r@Qmn}C8okuFkPPL8OZ(Wm-); -avsAwsyf=RfVo>*&^tab?IFZ*NJie=Mq^AK!B0fh&Rr^}i*=oF#W+vH?R>xA@&V|>^7eOA;PaoWXHz8($C8>cOfl+ZXAc0WYozcJNQDoRCg$#WYtJ -V!2OrlPAUhp<_v3?cNqBhUvF&)_k&R4ECt-6Cf%Q%A_{Y_iB#>PC|wb<%rGQX5jnu`O#lJExv$oyEW%ypWU2w|+=fb&O`W~C@GC9ZEZ@31KE|BwQz^7~qw@6aWAK2mly|Wk{YlC&jq}003wO001cf003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQUtei -%X>?y-E^v8;R9$P^KoEStU$F#&TX1AZ2n9nQ3T+{8g%54%ZQYHX~Nc4u -dH#$K&f<~>%<2C$(4k`5y@jwQM(iKD=9kwTS1T%3}`)C!K;g27OCs1p*0B@W&H1a -Kh-@^W=^g&2yHjEThEcYOqm`P*a0IN!aD@cBDD(fK+KsK`*>Ypu(8Dpp$H!Hw@S< -b!yAtZH_S>Xh&mT -$MlfWH1%elZ6x=P5^NJ{t2igU=b&JYE;K#dTS(&oN`s=xF}s>~xME&>=wfPxa8aD>LQciU+J+>uRVpK -zTSEDES5|RM5Po5ld6xg6mVi26ZRo_@@0N^U#q;xjGNIY&wKgQl-X<3$!Pc;q#cjMg;F;bkWUzFurzXjsk>hfgg3NDpW&|K_x-{I}q)_+W%|8D*_c`5 -tb@6aWAK2mly|Wk_*sO8kui000jK001!n003}la4%nWWo~3|axZdaadl;LbaO9oVP -k7yXJvCQV`yP=WMy?y-E^v93Q^9VVKoGt26{GkNBT;H6>ZNWfVFxFuj%^vckx$kFL)mt{Y -wa#}`TGtSl2l2gc1|!e@A2l%(CfV&Fb?mcIn1I-IFCaZ&lZz!@J?^f>A||-1_Vzu(-qKTt`Whg+@6@m -5mc&#lu6K-abq=4N{hfPFqwT+n(%v-@w}>2rOZ%+Ef6f~y#bXsFL(0|%+MIB-JwLy1T4!$@RYzao}x4 -e%)p_}nF2FNss=Z$g~Y88m@1-YmMb{|MCt{jPGi -fUlP6Q9(YI0AAU>wb&^|n?&jn?xpj$yi3K>*8OwT>p6S+IiTX0=?zp$`xvb{5xWzGd>0#W_*#6*-xqW -n7r^=v!K^A%P-;f*q08oFYGxJO-&cRR6U@q2!NdUV=OuIs%@9RMvx&=5#w&z1}XdJnz?oNT$9ApN7=L -=(vHhqIgXQIUOSAb?Xz*2yPqAPAPf5am!0qh`qv%|>YJn^Gh7Zn6APTTs@6aWAK2mly|Wk_+Hz -cF`_005#w0RS-o003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy{)=D#?iY`r{pvSA|34r9i_iY(r$y_(!0tMTDe%g_Gi6#U1Ve)D+y& -TjwIbKgI7hy9{|X|a0z;mzZR@8A6Khi_kX5B=H4fBoXC|MHvPfA#qM*Z=bPub+SU<>$Zt>R_#J-z+&cP)b7Jo>FZyngrbuRZYY;Gci-<LU;OHeul|M6|MH8ke*KHz{qFJ0-+cM_{PEk -*zx?Wppa1?>pMUxI?eD+*?Qeefi_ad9-@SSBX^NT5zctDK&lLZ$yZrO}p2=_Ce0=@(+wb?Q|4T3L_dU -RGzj^%W^}oOAW&Zlj+m@)u>)s;Y{q_IX8h-o!-M{(1-Q+(e^zR>U|M+$l(b2mg;hef;>{fBNZ9|MHi=eD>y>A3po|m;Y49PygIaKK%4QJO9U7kH=4b@}D2i&*pi?(Z4! -ZeEQYOTKsAq=cixg+`l@W=e&Pa*QiOu=dAlzSFRd9;EQkGpgulHG~@e{X@2~}r_O%*r1qfRzw2%K{YN&_pM -UuJPme#fQd5Dny7}@SzkGT5*%$w`{`ISW`Rx}!|NK|~`j21y?yE1q_}TBj`r{ --@U;5Udue|Nff~Y9W_uckuboe)Wrg{oNP;*Do0Q$mouHU1{^x`~PV1504+ew8A`I9rf&g)-zu9aM!D2 -j^kCUM{7wb@ez#Kk& -GM$2^Mh^3$qU-Q$xNx%#i;-2Kk#d5F=!_HRc&eH=ZaaFp5oo@amb=&|(!$E$Pl*I9nJN3wd-#`8M(++ -;HDeq+A$965P|*L^bb(JlA;U9Zmc#mDMK5j~>WBd`9*qqjCQ*NbtSgW;S!>z;>tzVvJK%sdyb`(!=%i -D&hvdeFx_yEkU!IGO#YGo7r+WV}7Kdq$X`;3xXM;-#DSU>5Rl^*Z-ItT;k(@qRHE|MXwui98&xQ9pVN -I!_j3|AGIkEf4()FFn2eALl3@yb!hvq39R7MlZ$lI$7r)NA}o`+4Io9n{?hEnG-3Dp0BIttJn9~{hqy -UNA|CuSBs9bdNBNFPv}w8_j|JJPg?RK{q{YoenNF`kL>^K5Bm;G+Nbf(`SQ4~#Y-k{-<|g^>OT0-=#K -jRibd&>kA9m+GKhn@Sv~vbNM7zz*qED^8~cstOHVEG&=PuPPu?+lMjjE0M-TsEY|pEETfBHLTnpRey% -3|-0(;P`pYQOuob|?eIeM2PoU^;^j(Yu(A9i$$Mvn-Aeexm)LNxjbk5S0TVp&>RdJ6h;lfAmmd%V~A( -lY4Oq7_H#Y1gB9m@J+(blwlL!jwqMIQ&5*r*)=_9y`d#tT%3{!O4=X -Q3H~PVBwcc~RmWzG7eDRaV%z>uppNgR#Y#ryYTFxi3XTRC|ZL%Q~?R2rGMMOOFQYkv1z_0k`}O1O{EKv%i_)XCjZuRDTr&=MuVZX9f@2o@BL5=gwe<~;X2jAyS~^ -Z@$}SXka~9?mqMijyZ?%mb4=c=4xwAi7U9ML!i2jVSOodG~uhTc4e5m!7;6`!ZOqexV+girFq}A|w5n -7IxRNdYARGv;g{E#d4$EkGs*CnSP{yTGM@@g=yh=-ut-Um-%(YdjTqY%2q$2MWF|Mw9aVNyBI -nBBBlrSb@hz2Lc(bE3if{v{_$CNzwLuD%l$ZpRj0}ag~x+up$h`bj1f=NMHbIuue2yLBNt{|b=3pQ+W -pSwv-P_Ani0*O)t(XkjX?kLQA~WXP`pwL5aNG2bB+?>K?C)a_Pd^2uTT7v=wbB>>ENJik>pH2??1h6R -}cRvR`7Ibk+^wlPA5b|bGuID9f%fP6r+XgSFKGc~Y>3WcwHQ`U!G*?R_cDZSic8g?;Xx~)6X!r`%Ptpx*!y -_4U(&`F(ZQBwvB6NzWoeDoDH+k9!cj%pmlo?t&lYKTh11K$%zNH&|L>Bo?++xYSDJ6bu -F5(7q5H6TK7T?CFAOKYc~*~n3y(=_t^f~YQJPKgo(yzt2fvPxXL}~$&T*FU~Kgta2B(tYBJvL4X3LG_ -FxWs5bpeYyc0d?ZkUb|PYH)}GRX+V2po%G1xG*iK(O1X>_N|PA7ig{0G_V?-EaFP2M%X%VSH#z1seXq -@$Mx?MHS{e+&WkZY_DdYLHCkjo~Ql`FZ|qgbK#fZIy^4b1{SOzk86fIf$4rOj7Bk`xO_aiUj+X_Hgp_D4r -i*pVcu}UJk1k2xU -C)e(_7#1O0g=e-e%HR>n^U4lEAj;FXA(dqL4L+9uqe)y;aj2k(HU8e1Mxza;oIkyjfGKCc~Om*EIK2q -=oDMtf8}sVoNOqn@Z&ECC81A03Od_gm}QUbkMvfm=M#p$q?a=xD8{x{J8*WU(14_TnV$S{T#^o@Hy+l -O@9*f0~E(OOCy9dI&uaY9}`2#s)mWNI6)oOTnI~Z$hSWH(=tK_|`T2wB@YuOO{5ag?Jhs%*#E%`yHwh -6rJJclP73APaQIXjKmitOwEalG4?)ggVOPFOTnSHr5}~4II&Q!JxWlEo0sBY?1Ki>%wR(Z&srh%NaFV -UYOroN0*G$^#9JjHFG|>Gn;^NJEF&YLS3^4 -y@iRlKr7h%69A=XpDBE(WFv4ZUw?S9)Y!N{R-ap+GK9;yNNGWOo`9g?+&3^6Si(=sMP$=)8*?Z;ck-p -AE`$+*XRv0B|xxsQ3eS`RGKpX*tD>KaZ*&f2T2OZubr(;>a*q5YD18sM?p3$JWVx*Rxl`lQ?T7OSP+; -hhtoI9TL~V24jVnWY^c_vU+PL7Mnoih?#ruyi4-0tYNJR0q3#JncgZih&}uIC2=q;MbpfycfI#Uu=jf -9ys1i_bC9v1MGKXr~|Y_6xN;a4p>WJm%%^wbs6u*)(;oFB(^@mUZ7(p%eW2Y!k;Tbln%ocC{ipw`$40Qkz7qS$0+;YK7X7JI0jT-DSH71(_0(#&abeG7~#YULSh;%?lS{WrQ!J -4&)p*~{=wm;e99r$jCdL93_RnA3ZjZqfk;Fv}_d}I}CJ%@T-%BN5=BiD2H+kQz8hCO?sm@c-EAuNra% -E+UaorvI64Gpmf;(@k#5b3l&b@Pi3-gK&WBr-^{lZ*nJ`bU?x%=rSEXoXozu)&$zoq@&9y>6{gUJlIT2^H+Uh(SZwvfBp_oEn~m>V -=OkVB!Jj9E2qY`uD_79Vr9ccD_H<_U%C|tO6@Za0urJCi&>|Dd+MD%nhys;mLvb)^42m2EsDf{%h}x4 -(!{`pZJNG?!>dq9Z;~3wLZjNm;8ZX5~bQ}gkOTfC72%9_MI^dx#jF=zho}w;qWCLmfF;)f-P|n1-0l7 -!oLS0>B)aCg2*0@vIh=1wKexVv|rMV_)TxHh5c^r&F&$E@_dQ|Z~zEYI(T9R(axAL5(OvZ@nM<%>G{}euEdqy-S?Z41z3lSmQ=w#5w_AmX<09A22jK0&A1 -04eJF#w))xuLdQN7N7!o~^tA86_1 -ha}OiB=j$x$uPZ9QXXTUvf-;ZOFod6D)!vAH?|UFuW$R_jk*M!o=^uYJUU|8+LGw<{!lf=T*>(lk;AkK`=1J^6&<}^vt699ry>6rZl0(Ce)E@Y-%b*$+)w)ZZhQ&t>iQwx=W{ba)QJNs -cbtwCLI9vbr)N~w+0fetl7~_J7FHrTl^N46-Lr10S>;#e@+1 -zxnSqy_hLxn!4zmS^dmEfayxRd=(51eIz&Urnfqk320Z!Vwv?UAy4~3Ok%xVvn^c^Zv8>x;X7&$>2C! -zcj;NCoy6b}8Os!!c8!at-IJ$YOXdbtts)C8@9AC&dsHc`>xNA38y_P!YSC6L&j2fQ+TCg9C6Hj^cEJ -B76jf4TE;y+g1j$vb!|%VIii^Bt&f>q$Hy-R;Mv20$bUs`16nbXCnM6F&mqphEqvmRWVb$4o%Tx{viC(l_+>En3yOy+@Oy4>aG4kqbxsv;4^z^)|Pe5aFn!*$OCykxnB%=3m)U<_y|#0FcwE{SE5DI_-9A#?4WRpWDl20 -U9l8T#$tFb`^xz;wG%6Ndo=4dFOK8=`viMUZZTy%2cdxS?pvjOg5Qw#efguDE~#T2F@J0EC@pP#=dm7 -?gx^j;CaXnh55^h732+Q!{$|sU4cOze8w>rXNoy6#Ei-?!-eWrX#e#RP5Z3L;<9=%~D!*_?^F#;}os5)Ida5`xAl=`VGB-#DOlXWX656G=PBS; -OXs}46%zO@)ayhCVG9K*RZB0O~E-_o0NMoyo_ZhkzW51lk|!WL(L95pp#IjL+ll7hWP6C#kgrVMM^a?D*mMX -Qqx3fOSYaQDrCoEcGqNbsB<*R0NTGzqKT_|4WdXTYk}#z!=g{%LLkhjhHQ6#MlpWXUk2*6Nc(dQ-%4c -yI<=|Pza=VhP(IDD?^p>`G@EgkbRoq5eEcuKa6T@4nH(TxHGZ+hiJd#Ahp@?8YXKe%Wy;r%TcmL!UBR -xT*)?!cxteN-UNIE4o@&LObXxjBF*CcrhN7Q7cG;2SlY`!5D2=^B0iq&0YP!*u5qsyd-)3LOi4Qc7$VhH{;HYL~mWB -7VF$TCM;>!iS@?rIAAa{hdN(k$_ojsW{+rDRT8GZLyDl=DEB;halBq!vkOi9+;t%RsfoQnR$*{!zbWw -OQg;z{q54X+bEzHi#loaJzAIkr#4AkKch`Zm!Xl5eBUK7CgbSxxq+-xa@^tNeG%=k_R#zQA4w^z41<$ -g@-fx9pLczmH{lhdZ$cnF1g#d*jAv77_!&#l;Klrai9x3SGkjp-~=;221O87odqjM`n&(x< -Gsk^3~T8ig?Tw%`?U}H#a#k}o(%fI20vn#*kFP%$`e-r{kuNthpYXP6Z_JeLmPF$<9!0&-jwiSWN-r! -u|eW ->KGd9QibH1=CJVY+(Coux*#$LY8Bg(Q?g8Q>q=JoW_-P)fFF|yA&lm_M%fmt;NmFKt4>U|_m6p-Yod* -cOQ`Uj(FL&dX-;4T^gl}6Hbm-|bn8#i%$BVJ0$OP^2AV3(VwTEdwEdFU7Db|eIrUymC?U&4_3&7*h3P -U{=a?;CTvG#V91EcylmpKXxNbcr4?2lmrgWGUl* -6=iFw0uN!r*^E5+iAsbEv)a+!?hdY&+WJ6 -M?2e+Ku>w!`mfQ)5=g=l3`D7a4R$?%#a%=Am$D`gxru|M1&}=J`&HNRHf?K0O#SX30m40dywwzJ-I!>cvqJ=osSWF8+kvR>Vn -a3^>mO`=p#KG9N`BZI(}48vyvK2Et`G4@pIy+HP2ggo@U6aVPOt>p_gOXmP5X~4C6`0Rn@JDUyBnIcJVd@;5?rtY;pB^SU(cFF -sp@Xq}^Vw9y2`xstaC5%BU&A8eQ*^Be;xBZf(G9F~}P-T;}!vhsHsSrBcU{ex+nl+A{vYEq(fE+RTYg -O5~^Fya~uf$I$T5nPdy3k?+|LhbbTfmiOd_B|v#y3%GbVotU0I3Xu|8;I2MXH(cLR}Hq$Dw3cD6O4E2 -`ajjEjSoTA5LlshbI+Ik7z8EwFE~m5xR-J>af;=vIPJFp5kM<-{w6J?UzjP0nHEj8M1O(h>0I_Vn!c -;;e+{Fr!gJi`{|ajdeuSj5nM?7ZNFqvoAH2d6OF6#;IeyMx)>;Qanj -jbY$idltXYYzWl>Vor5rC%Z;#2*-LVaOxvi<@M>@)D-N3Mie9r^#s|0&DPV&zlQXj1pYP*c>z>vxb>V -qWx?0GY}JUMqQ@mj7XB|g-y52%sxevmt{}{FS~PMY_l6Dt|2+Azz148|8`RMdWLm))@I_y4Qn%_{2p#fkF>`V~Bsdh={^dq(LE)XSRlwpLXto0tek{>FD --6Q1^Nm<5Em|QmQf1ArxM{2!@4vDkEum6`vcAVm3U{v5w$`m)^8k^04^Ou;x|K0;p^q|?|v72l4Z9_& -18yVs{MwWkF-Wm10i!Kdt*He(aJ%*aN=RFA=kLqjk?#h1C`5BB4R{Rq>jfBma0Gzf8K#qEBQ3f$-yh{f_f|}b_Ye2gIIMwZ~uY1*U3gW1*mx62is& -2WKf+(ORV3tbI`Lju2c$GRrnw&L5gfVeb>#;$h-GGrhv;zSc-mz3i`R9j$ch&W=T9!@SwWqJgo2_^~b -r;31;rV1FE#S=V{$5H2Fbp<&l?zeE1VVCwqlKpFBKxr4C@VEc$WK|`XkR@wddcdHFE$4Ux3rzdUq{g_RM}#Urt -UB`R&#jdi}Z`C)Pgf=T-Q!?iL{r~jfn5O|4H5J06iRXJkMbd*Le}|VYM&ICv>&U(c&0;{D_*glaklRb -gDf3u+LFZbl&!u{b?=!rI>p3Lir)uCUhIt8IsnXo;C~F-l@P^;15Fd(DvT0C*Y7!Y}5iu)i2S6qzcor3?6Rm|+tysu`k#z}DIfEy*K1 -((Nh}KM}Jjg(l#V^5USzzHM_J80rg^+#Mep^3@`w@=Z(45HAm3B{|HZ5Dbx#zI%wJbeQBn@;) -b;}Ms9!a2ohP&aAQwWOu9M94{7$zK%_+8I6sCzw$f+qUmA;c0X5C9zl6JeHbg -2*_hHm)$SL+gnhB_*w#L!fm0{pHED=yI6Vw#E_2&b;`6aH)C_++mGrVXB2>HOjF?q3!l{7ic?JyjQPx -lxUVBp2LvZZ!)PbBHP+@J#f55w2pAG6>`%MvRHr>Pe@l)}~XW9>RwQR}O0W*8}A)GH0zy{dXcs=j&!b -=ErPf+T)j1gRHY7D3+7|WnU-Kk#$RvaD*!cf-^VTnx)5bkxO?)BW62YpTR$5fLs$!q8V5)tf>-*c=(x -N$MRxESOxpBChn^R?H_x>sK4#5@fs@z_;9@)BiQ2LLbtiCyHBdw!8~ZRvKAfq3Sf_I(P2y4OW~{O}Bi -6|*}s!4a6EGj#?=(eigN&Z+XD27f+1j%SUyGk_P=T@Yv4gtT1HiVd39~#;5N8y9?rqIXYeVqy;8rIt##9t=$PUk1!! -g&~`Pf0->xptZEWI@}4Nnz5cH9aEc5q?UI4 -2b#s1Bf&)FR=W)zIho|AKE%MOmG}^_tvH(&Duc)y&7X)e~yT{gWy#%l#MK|50J-Mi1#As!rKSyc27(jGImWHP^>`fb1D6!y=<(h_zwd6cP&Jp9Q -nF-9J_O`lIdK9yQ`8TnXTxaa28!@5^b06h1FI}XO2E>RN!=e4i(SL&KvmPlwB6AtVt5`6SXh3;NzIT>?E(gp -DYS%*2t-A+90J|G3XdFj2iPjBzP$c$(ZZ6)+WgXRt+=*k|D34wiiq`>bhZV0tdf8-@s!_IZ&(-Rm%jS -*Aaufx+amcJ|8P<0ORoP-vK>YLKv;SEZZ2oJBk -DXCIf;pTM(_)7OYSodntWtj>^O!kUT{}cZn8v?UDh=9+hreYr{(JZnAInrqMd6UrYr;ls1TctdNBdw$L15Up6qW{Ix)mztBclbq6IRMXn!G0v)fpHkqo*a2 -og?)feDn-?V+ZIfH(?7%qOU?g)Wl^EqwQ};32(aKL$$l?U|7*3y@X=MMr`=!1F_v&%!7!q%KSZtahCOq0zB6z|aAe$EG301@R!eU|EMMZ{$s -{Ibv&B4I8pLS$Sg*Sk8pE~(&1k-%PmP;;o8ilbAIKdtrz4rYugSyvA*gQPmN}a;eeWr@`;ly7XhEQ$? -52Fa(hZ-1S-*ELPFdo1HHnc|U5+-H;Nm-i{)=vlEfJ@}1TLXTL~(N4X4}fB%JJot0>#0%?g;b$h)ayh-i{v{%9iCRW!Ncx;!~07(pY>F_FfzGO5R!cQEKcsBB$gJ}4My -(Qggm9GgV=g{GIDKxhV6YhtU+JiF(BSFvOZil=YgF^@fFZwZvKH^^6;dC? -kZrqCl)F@j|d&SzuKv=0;@=gtjt%Tn)V)5ucFtRZF5w)7m}J5Z1~@{C5YC{|f$M!LDKGa{C@^rzr4*zLnO))I3q{??P!y)Mgwv5|)-p9l$ul@L~;4A+ -C`j%4ICP2sg+9_*YJ2E&An74z1=)V-clrD93(uq9^K%7W9~i$_IB$Cuysp`lL4Q^e`SC%{& -a0_B-HZVXu&aGxkL+?03%|nf<)|DC%B^X9*$FgW|n4Ed#(;uu2#=`y7@L;vu-1%`Ok?`|uy|8JESk>y -ERidwrCJrl%7n?k3Ppt2zKshMzzavNuzVj}(l<3pLCfmR(XNG`3$&-Rnt8c$7S7U*d;1vb?SZ(IuHfj -K8o$)4RCLTI>=rGT+n>Eu!3c+~cM0^|6i2l2>avbQW1HqBd%n7a`uHR%i}O#+UEqMNL#9pMIqHXYUK@ -UeEM=ix)ls=PYiR`{_QAKpQ8*BGqMWCl-mDGv<&mlg?O{Bj0{1b+6Nt*`ydKTi^rUI)-y1>y-~8KbKd -5oD2(}iDf)2^|yisi@&T!rC;h^@uAPtli^btM5UijvWf;HXC0GZ7Eftdb@8+c#*qa7I92z$k;fTo&h3 -#C3ZX8@d`~JbXG;0JL6opydcV6zJt>wX4l@&VC+Y^=Z5H5!w@s&_Whq`^OG>%}KeDVeum`|z2-~`;VwKx?!JkvzE)V%BPrgg7R^Vmka5)Ram2U_ql=3%hqRW=-bBStzoclHp| -0mIkQaWqT$yyurZ&ho@J?39AOlcc~Q;yPWPQytE=0#C2O<Z -X7*TYQeUPn5=%!9QN79M*v$}*i&?~vBPsx3D2u%IQZcHPtGb!_-0xBOc7N<=q@XR)-sr>E0M?_=k^bc -GVNgQ~1Kbmq*4(@~i6LgE3_GdDk*)V-dRbFZa>p2`&a(+TxPS9T4|@;LTm{IHU#@?llIw#PjFwilDS* -Tv-u%X>b^rY#GoOhn`5Ov${(3PKAV`D6d%Lg9lM;}eMH&YMo}dtIJMqd9g7^^CF*z2_f{U -`M>2wi+EH^r-074(OCEVT3_N(7^4MtkN3`^zR$G&7e9)^nn_0Gocp3C93gyCYhkVCpiuIhJ3r5jm>p+ -%Nx*esV7l-eM!iTn!UtQk7pucV<1|a7MvmnRa@5w#ATHK|B1F-VSx>O1fY*h7Qcj*)WNL& -Z=;+`0$Qr5Kd+V!d28Srx8myjvqqokyVRrZ>FI#MyJyC`+?Zp?jp)g_>w_uQ9vV5oQ8&H2gJ>w9eQv! -Rq@c-U`bn;zDpURq{qi!3*RAT2P*gvKqx&t=IMNdfz%zQo!s%bToeu`DA^jvW4{(})(GVm(3tVhO8ju -6+qHhU`$PwwzJ-dd~eRqCt0v;-BhpX3%*c=?o1#r#8|{p$H;bjKyix#X;1Wx8AYt70vR%S}jg_PH7Fq -b`ne;ms6_-f+*rqEl)x(?+fxuV{c%!*jpFWrAK$>8&m7IP%9m59Dz?ci^qyhoZxQRL`bZ5DV -}a+i0jPO$|9zPm6o=mB&w0oxJS@b+0Gc>uDMO*mFj&yy7t?;~R#0Vh7~t*uK)g}{?^qu1{kY)cNxpJrK6nrV0sga|V~st~opehj&tUAHo+dp#*bdc?k!#SdVIq%ZJS@hV4wPWav0=MGM -%rKOf;k1#bvk~4OGb5i$ukQ|;C*6{g7ar#N6^|Ew4y%!LhLt1cI-;F~?%`Cs;G!(W5XX?!3UN_DOAG& -hV!)a}gbwpmj&9>f`ia8j7$EKYK7T5r*wmi-EG=H+sUrg#=2laKs3plu^<^fq<7CBpQNfvhAzjM58S3 -=i?>y|A#@`XXJD{gt9?sbq`nefTe8?v$0i?qW?ucAY`o|cwC*V04*F(}4xNgloE2BNuQ-)F-$rLEWf( -gCsngp23t&7@LE)aLf4IhAlP9)asn6d43gC6Mw#lsm`m?@;%O)5F-vyQgR9Or~?gj-|hr@S9=Nz4O*H -;;axLaLRP-=j*R;Ia{8`xfBIYnR*#LQsWs=4&{JISLpaF(w~opZHSe9d1f8e1*RGxK=Bz`Hmw3qNQ6chPs< -nS|v3WnkIV>nLeLmeLxQ3z<&({#kP;a&Sg_oG(#W9>f)sv**AlG+#Wxc1$3F=Rwrs|ClV0urF{;wCl? -W50g>vig0AHoPmwD8oHB^8{mnVzY)%x!5wy@o(QLaGv8{EoI@IS7_lO?eE$^ncV)Qb+4Bj-D7c(6Tv8IcIn`FogUg_dg0IIWuW^Ys^OV-g|c1 -3zWe?&(Cf?aVn$Ci5uVKSE;;;?X^!7}IB`x~9!kcC-{Dc76u2A{c@b}k+x&qriFI=@-VcK>=CQ!=s~H -?1E{Pz;8$XO$*%!xtFPG6;Fx;72pHcUEa8hY`=7W%BdS!=Uu#Nm0h<(hMH$`I}$5#D5kd;;gE>9oceL$_Aq3YeoesXALwWwnZ&wV)Bt%Ty|yT68=je+x4%Q(>j}vk!x|?}nTCqS&9R?=)?b=zS -KOO}%pUogcxrZzw)0jR`YrCmQymc$?JMhnb0mr8G`3~)16dx_QH{$BzOh4>#kas$Hu&;e2w)=gOWo`6 -D5N)>vAw7FgHO$k-_&q;aV!Ya^W_|C5>Egy9nIVdvXkl)MDz@>1 -`p$Stas9T0LK-gCMyU&I>`K$#&oT|fQu63Ep4I*x7Mtb2|9_Ur)@Le5rNN$!`OwS5R -em*Aj#wWv~Jw1MkT@uy^LuUARt5Y>;ucMB)~QEdp*e6`|UZQLeh6tfiv-$_dM|ZIMgt#*q^8BYhNCUs -yT43ok)$7&_K`J`fE=a@s_==M%hQ -HTHdYle*U-*7LCNl&4<0&%U!?XrheZ;5c7^dTSTFl5lOd>3uKWLx1-RFL~;{cuB3t{ahXs01{g>I$F? -OlZ*dikqge0!GPe0Wb-+0y-wZhVBQ{@EbkF;ql--~{|P-R4}(nSf#6?xbPN0aCZs*cX0EvP19h*1Xs|%NrRl -y~yY3+-!%C`QaepuGM=9cY?Un2lyNH`!jyDSI=2Xb-){)7PauaC>jqAvX*ZY@*j!ZoSU8S-hM|m&6B0cKIzuepSciojkH}-^L3Dge$`XxopYu -`S=uh~NS}b6(m`6xGU@E!x+cCTnkl1p;)RrjBd;TuJKIyvqucoj~neA0XN(f;XTd+q@BVvy{O<=*=4 -@x;rQ^VE>%iA|ksp2|&R?e}2}pjH#PJPrSZy9QR?k7${h)DD>%oF83y%+uSmB)hv3dGkK5Y2qB9U-;v -GLx{x@UgII#yZi@nG5``wyNs3}O`nPe#AIJvpK&Kn$D7NiGiB2+Tw{recGJanZ*va&*iYy|m&KvhM5q -!1ELq4@ri{AF-Nm(n*kX0W|Jf^iaGt0@KW$;AQHo}`p@}39I<2b*yzd8%Zqk`7|ef -muwj+_@K&LXf{sp_>31e>rAV?)<+-@C}o0&O>$yi#`<;1LSduGY3U?_tAumc8-Run*i@mbSd8l5hQrxPUXBiq7rvL}Bbp^*#oU~hL&Lp~okqn}W0X#wUrMDEcwq{w -y1y?EL$Z`OJ`9SIF<76Po5OI3@#S&(9atWKkt=vlq{`h(p*?-*i~9{R(=XO?T7Y81$?)cSpRmU}JSR* -Bk@M@*%Q%(B%M*Djweksm(WIUGHcUMJLwS1)vADYBx%~%@OSOIDH#*mJzP2hMu<{xW9IT}egv)n&c*i -dr3@*qIo$_ufVi&jj+-(EGv&7GIQ=(a>Y#xYOpzT%@(QB5HBke$_5Eqe8g*#jH? -QxvX7OiJP+|A+U^!xDIe}jm@IsVsV4n;%b}4%?!)yTToT2ZsSYw?Bb;B^4=~ESuO7*@hlEyw2M01MS0 -`0P?mz2MMS&|T2*a+mk=@cs@s@(X{?3x9+uYvq7fE2Y(cRWOkuI-7Zt3_udLt@)Zz%j;!M)=L<=G9s- -3K$IIR0c1t9f{gDcB>mpJONIDN3ZtH&E6sNP-{WP`kxWkVFzEOya?)~-AGN^Mg2%Lc)`E;Q~^-3UF1> -KF0&BKlrL3JWiEVOy408>kA|9gCdlx^Ab`OJm7`Ez(}gWS2*b{i+WF(|qSwZ(Lm5lcod#j> -bh^pEJ30oA0EH0i|l-@7z9` -iT8SSEkds(t}?IOWt@hthJg|oYK-Y~gKq9ZlYFZX(Btvxnqsf8)*UAT;k3XvspyXsyyl0kZg8YI4KVs -Q_^@;el~&D*aC@K$^d{4Y*pFG_p^&=g$j5fsk`bs_QGhkD3iyabmm1Sj!>OU#0c#ez$5f_k@r&IJ%5I -4~Eabb^|d07M6fH}H;va66tG!U5~M-;>?{TZ}&TB -IcPa(IcYg*IcYg*IcYg*IeFHk#iYfg#iGTc#qB&&JR6*e59+>yqO9P|cyJIqIG7Y1JP67x10oreI0SG -p@PvZm^Pn_5s8WsRhEEW#?l)+ev`ktiEt8f>%cNz}GHIDSD``=*C|VRPiWYURTd{6M%c5n`vS?Yfyl8 -pR@}lKM%Zq1Sw76(-(c+>7GIBicb!+D)sCNsvWdMYNnzEoCEGU@@igkh#mY}90K+Hj;68PDH{~Xj81j -PVx>|_PeZ}*F=kYf!pr2()FfOU}73X)d=!wVor;JOE1Xn?u{6dWL#0HVZw={)gN_lvB6ZUcfD5WxW71 -=KCTYymb4uvx&T0{jw0>p_qo1o=VG8aJdTNa%i%6@t4U^a`S#AU+Aa{J?b&yw$*i41B)8$qPKJz+(v< -fWTVEaXY&d&jx4WgRE~*+!>tx4US|4#a=;*ILL$qL1>T&43gDBk!?__8kDLA1S;+?QN|zbevuVQNP={ -Kz;S{|A@IHeG!E&?_m{fjv%CZmVQ_(7a7r~eI~iOO8eEqV)V~KMV}Y+8+*=ctiUMzgYa9Yp8I<|Qxs -y!zEZr{xL)k%)?hlgYK?W&E>IT3$IyqUxXoG|1owi -Qyo@7~syJW;-a34(cz1I>I1}8x)%b)n7qbR-F4%9DJ7U7l9!KALRRjgieqc2{IM|xDQ}!KrMn;FHWZb -LE)l?;DC2<89|(PkV5fnP|zQgwgyF`K{04h3>sA41*z{KaT-vx;Q0P5*8ypvYB>NPL2-YayZ8{FrTax -{D076Z^Amj_I#fIact60H0S^c~`QSdD;HY81hXT|Su$mbA&g9)gmApgbT*?FZ@IAZZ_@<%67ikSY(7;6cJWNOQ;7mr&u -ebic?CDdHdv8)RUEd}@%)3{s6jnlMQA1zuUy>9p=oC^sKcmxURfUpHs(Lr5sP}dohe*~2mLEp^XIP^lf%nZ?@v4n9lwi%dcG1Bw+erhuaa03qN6LBcMm?hOhZgZj -0g04=C43yQIV%z3Qs@8Gj^zlakl_#jIiB*lUpRgmNfvNS=OB}kS82t6Q|0f!8jQNUD!8ldlvD;Kl|dC{tWA$-j -$l#z7=D#XIX6fbB~3v+M6A5$7r%rYc91>^fOwEG4srnj01m&CpqyIATmA&)_R8n|pLBpMt^H*T_$=Ko -GDYQGP$n0Yx&?)8L5)k0t_)~@P%Rb|g$1QxK>=96L4zW%pi(Ac+c(^mm!S44sHzIesDd)8po}W0pb9I -fAT0q}3aX@n5~vAy396rhq+F0ti#YD_;+Ie=6x0RUK8Z&gX) -=xZQpQLNVf6$;SgwqoI$dJdX}JYB`918s!}4X1+;j9>84gBs1OM%M8bnU^ow5-_KQN;Ws?Yn$MMTyA$ -wjcUk(rfq(&hqMF{fhK_)y%B}c4#9`H-ZUIsb9uoR(uHDoWG0w06yK~MuC8j#D-pY-MneezKD?)_%*E -FdAsoClfnAYUGG|8mNZLUQBb*O2Ja!)*gH8>G31zBB2gh#kM-jr4etL6X}+ayv*u2N~NS7aHW1gA{R) -=nZnWK|(f2zJ}*d;cO9hzLT$bBQHT_HOQ<6`P3kR8l+2uTxgI64KkoX1~kZb1_{m}qZyOOT=nk}E-SB}lFWIg}tB5~M?d{6~<~2v -QkADkE|qhQ$%-ele7!G=dyPkevu}5C}Bo%_BLXcDlQVBsSAxI%a?#r$4LAqarh>SsyA -_&vL28W~wVl#f)yCNC!kZAkqPm4v2I>qyr)y5a}ofJ>BXwLzc>3<~?5g60m -fDr2{M-VCg8o>~jA^9|#s~I$+ZQn-17?z@!5v9Wd#r%?a==x?cnfHXX3(fK3N%I$+ZQn-17?z@`H>9k -A(uO~<~8$^}KLYG3AwZ_)iCT>$FvdK~>pD0M)o14VQ%Qlscf)0i_NobwH^DIvsa%8{eY)MW&$C0 -i_NobwH^DN*z$@fKmsPI!y5iJV4a}st!$t3#oPL;&_UHId1PWUk*XENr4Bao{1j`OscEGX&h8-~MfMEv=J7CxW!wwjBz_0^`9Wd -;GVaK)kByK|Yi%eZE1u_N84j6X8umgr2FzkR~2Mjx4*a5>17EJD;9wD*l=@OPVFk?zoWR*@Sc_q&p$q3F%HqcX~Xbe7~Z3`4i -2Hr`m7PtY}s=yZv>FXA|6=;O+!>C%8Mo-3jhaaCd^c6WpEP?gV!ym^;DT3Fb~Pcivx$i4lKEaCd^c6W -pEP?gV!yxI4k!3GPmCcY?bU+?`R?tT#>n0Law6Xu;T?}T|L%sXM;3G+^vcf!0A= -AE$agmovZJMY8iSnci?`GI*S%sXM;3G+^vcY3r7)0Tkm1bippI|1Jb_)dU#9u9gRs>D`zzsL{ZI|1Jb -_)frg0=^UQoq+EId?(;L0pAJuPNI**!%v*+Ja*A2X1e=DesE3_JCK0y1bippI|1Jb_)frg0=^UQoq+E -Id?&y=0p5A+OA9g1-7iuD_)frg0>1OGP)jG@Clq}kHz4o?fhPz&LEs4jPY`$>H;tPC-gg^-wFLr=yyWD6Z)Od?}UCQ)H~1Hucml5q2CGpPUv?+zZ3eM(C>tPC-gg^-wFLr;88-q6Z)Od -?}U2idHdBA&nEOcq2CGpPUv?+zZ3eM(C>tPC*V5)-wF6mz;^<^6X2Zy?>zUH>0xKPUt|U5oqmx(IZ-G -Yz;^<^6Y!mY?*x1&;5z}|3HVNccLKa~?EE}-_9X$|3HVOHcY?bU+@0X=1b64qhhF}b=12>eJHgxu=1w -qo-j^0)XS-jd2Hc(C?gV!yxI4k!3GPmCcY?bU+@0X=1al{tJHgxu;m*4V9>d!GA~lfigmfpQJ0aZ(=} -tVQgmfpQJ0aZ(;m+akk(Pal1rPmAj$LGhIqiOtADDM~Z$bIu2oc~r0pAJu -PQZ5pz7z1BfbRr+C%`)a-Z?j)#FD-w;5z}|3HVOHcX~bsRFTl{gnlRVJE7kR{Z7Dl0=^UAodEB=4~Jk -xyI-^o;5z}|3HVOHcLKf>@ST9~1bippJ7L}l^G;ZI!nzaIopZ7n-fC(0q8{*^wE8^Cu0z7yb`0Pnmnx59pQzs -L{ZI|1Jb_)frg0=^UQoq+EId?(;LrxTCd0KO-2VM4ugZa#_0?0${GoChQc@ST9~1bipBJHg!v?oM!bL -bwycoe=JXa3_R2=XH-4zXZ~qknV(ZC!{+e-3jDQAa??}6Ud#w?F4QQ_aKBjA>0Y!&il)!Fqtn2=}t&@ -Lb?;uosjN?bSI=cA>9e-PDpn`xD&#i5blI=ko25ATJB_4t%2c1bFA#{tkw+`$duf-wF -6mz;^<^6Xu=Z?gV!yxI4k!3GPlXcY?VS%$;EFTyc*VzXaT!;%s1Z^j1J3-nB(oT?eg0vH)ognQ5X(u#0V;^q9P`)HoJN-5~$Vo!A(~CpPCm=~6?F4BjNIOB=3DQoG -c0#ignw_!h5wVfoFG2*>PN;T5wI^66LE8!1PH1)lvJ;SQo;XC*kTrm3LZlNCosj5E56Lhs;OM+BmBt>vBv?AZ(g~JMuylf@6D*xz=>$vX{zBjMCptomVA2VbPMCD&= -7^ZW?iV3~O($$RVbcklPS|w9rV}=uu<3+NCu};?n?f8aY6vi$x$`2J!tNI#f=wrEI$_fZn@-qt!ln~8 -ov`VIO($$R!O;nhPH=RBqcisRVOR&7POs1eCMJM7)4}Ks?f;}BWC}{1Q0nw*JVPc>b>_~$VE}u -)2o|V1LDdPWPEd7%suNV5py~uwXF9*gRN5P|1gcI@b>_aL4g=WzB2%F11XU-LI-%4FpiTgF0;m%}odD -_tP$z&o0n`bTPMCD&bY_?o?C2rV}=uu;~eMO#pQQrV}uo_oYr)yq5$}Cx -AL((+QhS*mQ!W6D*w&>4ZopL^>hS35iZfbV8yN5}mar4uZjV -Ce)YC*(1qhY8}!+I6oOyB;r+Bo;AjH@?sv*0U_r>OLBA(w3=DRe1`J(z2&z-Q&eC0Y)b4ZopL^>hT36V~SbONIj7@a`q1VSeeI^oX=e@^& -w*1oJ7tJeJ@KXB-TLnjr4uZj-a4gI37bxsbi$+)CY|>sC|I@b_s|LTZ+srX0#GM_Isw!Ppw3HAi6wwiCzLv&)Cr|d=yYDYz6 -4v={hof5<_H#mIsw!PpiTgF0;m%}ov`VIO(!@y!O;nhPDpfKw|%4TH6hXokxqzo0;3Zcoxtb>Mkg>jf -zb(sPPL=52ZT-_bY8y5ZQrPSO<;5aqZ1gNz~}@b -W1%xgjbitns{#?(^5wTk@1&1y;bittu4qb5QfJU6AMkLKhIafY1emuKUYDu~yyh@O3rV^fBmT(8r(;PN1V23M^e<=>kg^IJ -&^m1&%IobUp7lfV$TLOBYzWz|sYlF0gcgr3)-wPan8wxp?5BMbN^R3tIT-ohQRGb-zK&pk>f9Xc@FjS -|%-%mPyOxSxF1$H))ZyNLtiAzZdIPv@BW{EsK^#%c5n`vS?Yfym;0{i;ET)EiPJIwAl5u7^Uv_qUA+P -PxOVEu>Kuby1>!}mM*Y#fu##9UEt^fM;AD{z|nQ=%YiUTF9nt^uylc?3oKn==>kg^Sh~Q{1(7a@^n|A -tIJ&^m1&%IobRD;Sqwcl9(gl_-uylc?3oKn==>kg^QEuVC6j-{z(glt#aCCvA3mjd?KIDZ7>V6R&uyj -GB%k$P=PlKf?*mS|B3pQP_>4HrcY`S351(PnAbRD;Sqwcj}(*>I@*mS|B3pQP_>4HrcY`S36b$IVb`V -pETL4fH3OxJPSH|kysd|cq;f>jmXR$+q5ow`@h3AMN4fheTuNDw0^bwQ`=xVaH^uLY$pD0M-p3pQP_> -4HrcY`S361)DC|bit+zHeE33f=L(Dqt1P40QTjjVABPgF4%OzrVBP*u<3$L7i_v<(*>I@*mS|B3npDK ->4HfYOxk|iFDck`!KMo~U9jnbO&4ssVABPgF4%OzrVBP*u<3$H7fiZf(sh5SAf~1JMSft@1)DC|bit+ -zHeImkf=w4}x?s}KZ)05chc7FDWQ>L8%K$T~O+RQWuoEpwtDWu3h42m&gsMx3$I+cy+<63tnCD>Vj7nyt -?4k1+Ol6b-}9(UOlISzWj$~`#c#2rTayQfYt@HE}(V6s|#LT@alqB7reUQ)djCEcy+<63tnCD>REA*x -BZd=UKh~1fYt@HE^jx3subk9AlC)CF35EOtqW*fKfJAlEf~FJx)#FCoB -gbic?H(7NE&1+Ol6b-}9(US06&f>)Pc@rDmQ=l&gSktN7=jZgF2en}1A8JY4rOMLDEUl;hgz}E%7F7S -1MuM2!#;Ohck7x=os*EKdb!b)_%h!YsQz}N-8F7S1MuM2Wrkn4h67v#Di*9EyQ$aO)k3vyj!b0Z8y_l -qP!unTfskn4h67v#Di*9EjLpmhPQ3us+H>q!hzkn4h6*Zt)^7>JjGU>5|tAlC)CF35F3t_yNqkn8g6Z -h*Q4zAo@}fv*dEUEu4Q=RIEh5-@gwu?vh{;OhckpLmP~!!8(h!LSR4T`=r|VHXU$VAuu2uDfmhFvi1f?*d7yCByE)hPf;5oDB}mzG1lGq+!jG3b7gB&BA*B(@B4U6AX7To>fJAlC)CF35F -FrxuZcTo>fJ=Ju;G2Hh{B1GKK`C81>_n)?&-1GFxnbpfplXkF7;LT-T81+=dFvTBS$_lx`htqW*fKw;Goyt?4kb@OVB!Arrb3tnCD>Vj7nyt?4k1+Ol6b-}9(US06&f>#&3y5 -Q9Xuda3L9qL{SR9&Fz0#z5NxLx?T^EE2=zb9xP<4T-3sha8> -H<|4sJcMa1*$Gkb%Cl&1EDGfOc!9f*3Ofm_`6>O20&c^>H<&~fVu$G1)wefbpfbrc@IMRpEN^SpwtDO -uC?=I==|;%sR2;e^7CBONMv77>Vi@il)9kQ1*I-1bwQ~MN?lOuf=<`kbwOzQ?iZVj7nyt?4k1+Ol6b=|xgmEQd#N$~1|R~NjxK-C4RE>LxWst -Z(Ipz0IvxZu?VuP%6X!K>@$)oAnX7jc4D7reUQ)djCEcy+<63tnCD>Vj7nyt?4kweKwpe@C-@UIcC4{ -c?o`KxYDU3VdDQ>jGa_*a$TgjxziReIQui>jGaFyCByUc8*Lzt_yNqkn0M)X`g^# -fv*dEUEu2iUzgv>MZfqZVC({87Z|(1*9E>V@O6Q&3w&MR>jGaF&op{qq)0ZBne(!@a -h6p7pS^G)di(4D0M-p3rby3>PjaFkx6|ZR{FjS2F>05B0o^-f>IZhx}ekrr7kFSL8%K$T~O+RQWuoE( -%z68=yctezM;9hUvv_bx}ekrr7kFSL8%K$UHj@2n|t+%eIPvW>hjVfz-8sG13`0lzep0$x`5UNv@W1^ -0j&#YT|3>#^C8!zLPEzB?1+=c(bs%W&mjYfF(7J%u1+*@pbpfplXgwfr1-UNBbwRERa$S(?f?OB -my6U{gi(dl4E(mr3tqW*fKjGLA(7NurO!Rg4izET93us+H>jGLA(7J -%u1+<<3-vU|}(7J%u1+*@pbpfrbZu>^vYXPqdnp@@TB2G}g0?C!w3tCk|DQHTmC#47kDrL}suU$8#&3x -}ekrr7kFSL8t4!1Q50SQUK}#P#1u@0MrGbE&z1_s0%h-u<3$L7i_v<(*=_*aCCvA>)P==dbs;VYQWM3 -mM*Y#L8J>JU0zwTU9jnbO&4ssVABPgF4%OzrVBP*u<3$ -LpYjL5bOEO8+UJK*y**yk5rDb?)CHg}0CfSV3qV}}>H<)gM%irwpe_J)0j3KuUDwW&p>?}oIZBx~@B(r|z|&)CHw3D0M-p3qW0$6Xau1Lxvjw>H<&~fVu$G1( ->dDE1xZA)V&szx}ekrr7kFS0jLW=T>$C}Kz#wIF97ugpuPaq7l8T#OkaTM>v`)P>RvAZ^#!250Mr+N` -T|g20O|`seF3O1*z^UPzF^ZAZ2E#pPhi&tn7*F3-l6XGf=yqr=?gY}!KN?R^aY!~VAB_D`hrbgu;~jn -eZi(LnDhmSz97-p^VU1my)j-qAJvi|Ry?j6~YCECrf-=nw@*OJEtWIdqS$dDWE~Y4GCD1z(=s|Oqth}vEu+&iIxVBqGCD1x( --Jx@q0MyF+TT1KZmblO9wJ#^Ybr#*DqKd1e3+CQiLbJ{`(;hnQq0=5Z?V-~iI(&m}Cxo(uwqM+(xGrbXq~5PVQ+Bo%Yaa51sb -VY5$z|&l!%;+_ZmA`{%TO&TNm^IrMMbMyEY=T0y53bXq~D6?9rbr`2;>J*U-kT0Lht6My-UUagUt=ex -b9&s(F@ck$`zVECBh%_Rt)A2BIp3E}cINyWx6x?@omS9k1)WyVX$75D(C -Ld}T0Nh>@TL88+CQiLbJ{-+4(|MyF+TT1Ka3bXrEIWpr9br)6|nMyDloT0*BKbXr2EC3IRsN9*&QFaO5vl+!XgEu+&iIs!4m -KlJJiQ5)&BkxrZFw24le=(LGWo9JlgolcQ|<2FC+6xDNuw2@95>9mnf-(MWQqP6D_X*ZqL(rF!?*3l7 -B+jolm8@F*1Ul7}Hww6 -w7>9m$kYw3s-@UU=~nkjMfo253br;T*lNT-c-+DNA*bXr2EC3IRsrz -LdSKgav|vrdfv(jGePq0=5Z?V-~iI_;s;9y)9a-&3=+gicH7w1iGe=(K-M`{#H+Z{LaWZ`{VIJ#^Ybr -#*DqL#I7-+C!&3blN|s{d3wsr~PxPRr=Dj84nww1iGe -=(L1ROX#$OPD|)`zYnhyBk{C?y1yrgAxT1Ka3bXrEIWpvCF#f{+zose -2WrzLb+LZ>BkT0*BKbk6U4e0yH9n*}`lXur7l;^*I9^c%$4Sg4P03+l9+PP^%}n@&6Fw3ALd>70Fi>r -D7JZiCfsI_;)&__#-JTMO#6n@+pww3|-5>9m_pyXml5Voz4nVmjaFt2+t)joWm!piaB#w3|-5>C=WcE -vVCiIxVQvf;ugz(}FrJsMBIPEv9q!yt?z>-?*KuT2QA2b=U)%upJ%|9)d=!Ep^&br!95bQl~9-4!5_H -)c5)7PJMslHe&6R(()#KFA=6{<{dKK{A8a3?gMM^c?LV9w5m?4>U^KC?p*gbZois7JjIPztLn6>POIv -)s!prww5m?4>a?m(tLm^7GQULatMm2Mo#y_=ZN%DFr+szWSEqe-+E=H2bq-HsZ(#fCw69M4>a?#;`|7 -l>&evCWhWnTH)oEXy_SI=$o%YpfU!C^VX$JH}o9nc>PMhmoyPm9*+uyiNQXA~FxlWtww7E{3>$JH}o9nc>PMhnrxlWtww -7E{3>$JJfx9MFcx4&^4qBhuRbDcKVX>*-6*J*Q|HrHu$oi^8LbDcKVX=$C7)@f;->wA6c=U&tDIxVf! -(mE}z)6zQatJA(Z?W@ziI_;~|zB=uz)4n?GtJA(ZUtir>?4Mtr$(M^>z4>WrotD;VX`PnVX=$C7)@f; --mey%$otD;VX`O37f7S`?Us_(LrFB|bpT4uty%(n!S1;~fkaPa-?Zw|w+gzv3b=q9#+jObZ*59~YZy& -BcTy%^ksik#VTBoITT3V-lb=p^_eRbMbr+szWSLf@iJ5T+M+x)bzPW$Tc`RZ^38={uhX=$C7)@f;-me -y%so%YpfU!C^V`TFY4PXA8pi?p;(OY5|>PD|^wv`$Oww6so3>$J2^OY5|>PD|^wv`+i#e0_B%roY?UB -(PFw1a?X!Tk5o>PFw2mRUn)(_r!*AdtTC4gtV$otLof44fW~|7agOSqOT6m -R)cjjQ0Jod)oEXyudnWO^fzuZ)xJ9It50}}8?2VrX=&{}YSY!`I&H2M9<)ZR^>tcb=UEr&O!P0UvC|q -mt+CVkI<2oyJEFA1PCM+h!%jQww7$;6zcyX1uhaTE-}ixZCi)wm6*J* -v7*4Jr$oz~ZBeVx|V`MwXN^UvS7otRo+7_>KH&y7UG1>b4m<6z^L*z~{o -HHXWv5+s+GVF*cG_WwSNwL^X@{M5*lCBIcGzi$9rlq54?$D)eMx@jpTBXNt`^#9p`CWwX@{M5*lCBIc -Gzi$op#u1hn;rVX@{M5*lCBI=X-tY=U&q;JMFU5E<5eA(+)fBuusdpw8&12?6k;Ei|n+>PK)fc!%pk# -eBTGsDd%t8#))s|56@uZ)cQKDuhaTEt*_JiI<2qM`Z{f{)8;yDuEVVOeIH2Yn15-5oi^BMbDcKVX>*- -6*J*Q|HrHu$otD;VX`PnVX=$C7)@f;-cRjXq%-^^TQA_Kzv`$Oww6so3>$J2^OY5|>PD|^wv`$Oww6s -o3>$J4a_dN}rS^matep*_mrFB|br=@jTTBoITT3V;2b=p^_eRbMbr+szWSEqe-zP`FM%ip+-PW$S#uT -J~ww69M4>a?#;`|7l>PW$S#uTJ~ww69M4>JuKD@4UL7driyhw6so3>$J2^OY5|>PD|^wv`$Oww6so3> -$J2^OY5|dO>513Jz^)2zj2$IHdLX~piJjs;DtiUT4bk1cG_X -59d_DbC*SK^Klhq;*=d)ZcG+o{oz~ZBeVx|VX?>m6*J*v7*4Jr$oz~ZBeVx|V$$k$}=ZU{@no!m=vh#g^b*F~EaruD;tA%!2Xs3ntIs13LPI%0@(M?sp*SCJ|HEp%iRy%FA( -^flewbLRyEwa-hJ1w%)B0DXz(;|C*ZL)hgT#CzmP3~u3`=`U-U0kyNdG~qvwb||^W3T}@+s*yjYWLDy -a^2y&!*z%24%gi^MV%>n95F`o6?VRE&wbe9?DzXF8BL5PMiZln(b&_jWp^*RCG3>?ZL83Eqen4gunFD -IyPG>>m@&*4hcOOg9L6{t|8U*Ibr07)T(|oXeqY$$8Kj3f40JKj#XuJWT?}+F5F1Oj3-2WZUEGL^>)u -@V=DO^!(@MOT-*LKo;IF-SFB#)zjGHlT#&{UxVT^|{*z&lYcrUr`!*$spx4n2Tx$XjQ=anAkWsH|GUd -DJCgN@MIkN1)!?_V&`%RuZ)-=@5m3=jqg17zpRey+BEVxTb4{x1U&)FG%tP=}zlI`1W09TLzXplo~p8 -v;56^!qq>rGy69TL|TWm4ry(l-aX&A5#mM=Q}%}a4cYuY-Q5YNhasawMu&{< -zKcUfhm6kd=4-|vn`{>N+uiquY!2BRve}Z|Js;pOe;}OMNo42jzcM6sS0|me)BQ7 -plh+}y>{(MlO5F0A -T=(AW*gERRuAo1WV3A;vV -3A;vV3A;vV3A;vV3A;vU=bwor0sWqGv)L+Bv>Sv-M)V!!6LyT!R`1DJ1_o3j75y){Tl`##$t*UdDZ!z -o9=h`6S-yQx}V6c$gP-aMOxW+_$SgT(kf3;TfVk255Z6_FK@)rMl9eIhF&D<)gLyGK3OC8{E-diS~M=Ysnu#vr -mHvLdo#z7=T|X%%S|X)O%&lIxOEGsT*Knt+;f0Va}v{nO#^E-u+0l#H5;nv9x^nsWh@O_NQNO_NQNO_ -NQNO>-{5%+ceJQIk=VQIk=VQIk=VQ4=;c0R5TA*<{mX(`3_R(`3`%lk+qKCZpbsPRXdrsL80wsL5y*; -bg}opeCRupeCRupeCUEt>a7?Jq$rLK{Y`&K{Y`wDR^Hnyw -9Cp4`7fc+c$LKU^{Z6YC+g>_7QC`kouk1}hRLyYVpzKHC3hAQH@OrN2Xh3lk@#@({Vh)4>*TzrzD@b} -;X&r#KfpB>0fvLxR~7>~~1;;jExaak4*1f)5G)?kcAS@BTESFwdTAe`m~@;339`7$0KH9*)1me0!L05 -8*w8_YmI0JbTFPVV*tY^^n&?Ue9+fU`_=Oxjp3eklRCU54k<$_K@2{Zui7<&l(RC>%Q9V%jzMj-+lYe -1>;v;$Q~kli0mPy0@XJOuS3s24%K2_&cQbI2){C@Wq?KLC4|CHsd&y`-_9C(uk!AC>-^Dz8kL|nS>;%Vyobo5P=ADrv7&m)SIBPULboIrc*W&|CL_!JvT)4Ca -U*$E^u)=coW&1$g)@Ij}1rn{rJtqdXv_hw6baY?`C4XnOJWEdK1u_fZhc3{>}w1&IWISdJ|N(D9=6~r -1gIH{~2hX4$^uvyWXVqCZ+e@EK+)t(mUV1;bf5QxPLcky-Dj$TKT5-ow9Etd-Je7M3()pe-BgaLsY9X -CujeckJNnkg;T+Y@IHk1A-oUaeF!gmQvXUe-8G#5Wt`dnzqn)o68td5J|y?i=Ir8B@FBr$uKasQ@I!* -x`1tpb;D-}}hj$KV|C)hF@I!(h=GlkPK7{t+dj|N$>tP7*LwFy;`w-rT@IIvK;mLaT0ogmXMI1pYL4Y=4`OibK35n>< -zm2k|7E2LwFy;%O118hwwgxmrZ4V53}n-XdgoRd~elmP6i)x`;gm*+_Gu+?;&D)TK&xu=5>U3-{jnkL?_=iVXn!&m)sIk8d4fk8d92X{(Lzn3~3E%4QUN&4QUN&4QUN&4QXXZ^Iu45m^DK{LqJ -17^SzgPIVTK34M7dFX2@vv>D-5bjAri%*$mkX*$mkX*$mkX+06G|>gAjexEkPUfU5zn+PiXjxV*URzC -`!_;KSw1B?An9KMXJoF#GPKpLXw`7-$%%K^^ppJ?neP7-9@Dh8RQK2ytC;U2$D;U2$D)Kh#gZ&tO)AS -@qp-GrIrFKxQB_kQvAfWCk)f;xNGBT8A5Pxb8Q9_QUY~6Js34*thz7Wd=I@|HVN45I)hI-*?Gq7q{eM -l#5X=ZpqiH_M`D9-;XldKJGU+>t>*vf4v##W}usaZU(x!B{$c7xbDL(;m4m%T0bb?KQYj=|H42I13e7 -%Fwnz5F9W^Yh?g7ja^08fzFha^x-Zv#zkBl#fqjU=7-5VsMi?WE5yl8(gfYVLNhxYIsMVlWgIe_w-{b -VWaL}tkuLivu^lH$n{)bD(AgF-V09I45^1DAyN>Qspt!B1YZSH#*(u!UUdNt_PpjR`b6}1}FYEY{|tp ->Fk)M`+xL9J%!Rr#1bvF~9>D}*%=)<9STVa8gOdBsR5@3oEmUy -hWATbLp0kx3>h6Vick$gH3-!#^X=Urs4&$~2oEm|15?cqP=sm_s+oNE<|P9JbqFd#H3-#IsLWfsL8u0 -y8iZ=ZoC;1&aq8@`5UN3_W>=gL?0=0|Z;^;BLNy50AXI};4MH^t)gV-ZPz^#g2-P4|gHR1ZHPYUjeuB -S$VhqxXPz^#g2-P4|gHR1ZHDWpirv{uFaB9G*0jCC>8u5)<#JI;HsNmFqQv*&7I5ptZfKvlb4LCL0nR -Nex0Z1uAH3-#+jnheS4?|kT)2zK7X+@|8p&Ep05UN3_2B8{+Y7nYHs0N`Lgle?&X9L{h5L8HNAgO_*2 -9g?W3J5ABHIURmQUgg1BsGxKKvE-OshvNY-5!T{LQ`Y9OhBqy~~2NNOOdfushK8k0!x -{Mp$4>z^+E?&k90^5v2-2qPplkkmj@Q<5_M#o$tdOARhHY8gOdBsR5@3oSNPKmZJCqXvx{G-}YOL8AtZ8Z>Iqs6nF!jT#eB0BQiJ@i%{pvqB7NYEWmsA7voY3PcSMH -9*t=Q3FH`5HɖwBE4FEM~Hf!djun=`Py`M9C$w17XK-2(H14IoFH9*t=Q3FKHA)tqVVo!6v`O};f -Vo-xY4F)wB)Et5eKMnje@YBFg13%3nps>@xP6Im)>@??_Kf5xae_{-RiaZVSG|1E76GJ&Zpwk?(2{sM -bG+@(!O#?O!*fi&R5j7`D~*{3PcSMH9*t=QF90=05 -t&AT)(|HhqFQ;YJjK#qUIu~7}Q`;gFy`jH5bz<{4^KYgq;R<8rW$rvU$xuH;2hYR8j!)D27?+5YA~q5pa -z2)3~FwoiaibXG}zPJ`tA)Ug&5ReP=i4Y1~oU4g`Wm~8u)48r@5I<5vM_%25}n1X%MHmz0b|XSt0T?$ -kQNCgFFrLG|1B+PlG%S@-#QoDdIGU(;!ZRI1S=7_q9p8I4eY+<|eJ6(|}HMlUBTG@TR#*E6y}H)8I^V -lg)b{ZL$e5&He0a+r?QS)HG1jKuuF>I@v1_RJ>_!g1T=?_B`z-pb*nQOan0u#5DK2uWdIcg;3K#O#?L -z)HL^|he`D2C7dNd@5Y5};!J}x%}q8Trh%Ad7Xgu&fBn<&cjD4q9xg8~Z!RA$UoIJgNfcO`heTpZgDD -NBG|%j-!OdAAwluXR?-Tt0wAK$}}j`piF}@%|lur(uye!rZmqUlayjggDDNBG?>!tf+8}~;}BGAX|Sbv2r8yDn9^WMgDDNB -G?>z0N`om4rZf+$)b{Y-dB?*^;j@o0lPJJ60Mk4KwXk%ux0ke{OM@;Ax-{t0pi6@;4Z1YD+=y`e>z^L -}?&Xp(m_&IHpr(0=EW|Vr(?CoEF%85t5Ys?R12N63@7{1u2sI7VG*Ht(O#?Lz)HE+?g_s6n8i;8irh% -BI#NY2N*e^su^QK&Jtn26P(GX+Wm| -od$Fo&}qVS3N{VcG+@(!O#?O!*fhIy1W)n5{)sWBXX!j5$kQNCgFHz@`D425g# -L=)^PJ!w^)^X+Wppqm+X;4c;_()A0Ge!I=hU8k}ixroov8XBwPocHtAxaF0VW@utC>25%a?3Ah=&X_g -YU%-?Q)>vWuHw*SPLX1m4RXM*RphasN(o_kNb_l?oM(b@MNVG`XFtbGNCm}V#LoyB%i+WBLD6?fkWp4 -%R0Z!z(Nng(hbsA-_436m(qG!WB3Oan1ZiRo;$i8Bq(H2L})mX{3PG<@IJcPugrIt}PF*&(w3%0SJgz -$SrB0-FRj32f5tO2L!*uYbDvyNAn{OU4++7{(aJ7{(aJ==>?1NjQ^mCgDuNne_YSPu7>}1ThI>($D)l+65;46JwY$4r3g~IE--^<1ofyjKdg*;~%bjxbDSuFRpu -W-D`hGS!5!BNdS`oCIL)hS(Gp(VM_W@1wH)r%|JId;%0!G0d5Ak8GtV{;7ROZZU%Z7=;6jZjPWqW!x# -@^JdE)$#vbkAx-Zv#x$et#U#`oS8}KCdI4@(ojPWwY%NQ?XgfYSxVT>?FIDWWpxNf*^xGrIwJ^vKTOo -S~7TN1V;RzwL?5~d_fNtlu_C1Fa!l!Pe>Qxc{m7C~wD{8KD65v(LwNi2pEoFq6&aFT{kIzRi53`8O^C -1FacDINGhz>>z_z3GXe044!U0+__IC}B#%l!Pe>Qxc{mOi7rMFePD1!jyz5Y0h_VdS)nWN!XIGC9(QR -n36ChVM?kg9cCf{O9GYzED2Z=u%yD$Ir%cs^FmdbddX;Hw7}$dA9jL@E(u){x}@RD>q!8_q}c|FpChW -nw?X87If0=Q?hr0+~Q2anS?V5XA&!+1Tks0%NL7G1T_h268t0jPeDzhq0sED-kZ -A5F}a19BqmXGN$8T$C26~SvB*RylUM>JfJp$8SOO(rNx+hTB>_u(uVpW}E|Vy*Bw$IvlC+=26N^kFmY -VWlpiDxUgffYBP(qi4E(u){x+HW-=#tPSp-V!Sq&@%abMf!+vL&cVP?Ml0K}~|1#5yQJOoEsMF^TUy; -}uUhlW->COxm3|coKRPex3xgB1(9Z@Fw9+!kfg3DB(=PnS?V5XW~1Sd&zaltG0{kbI`*uL>E9)Eed%O -@+9O*$diyKAx~m$ln^H&PC}f7I0E!!gZUj+uL5XEif}aFG34R -jGqEvA*=UjrF1Um_K((Xac1Kh)qTlh)vli(-8PlBHWKM8)4`J@TI=NObsc(Es8Pr{yLfA^-Rgu?GJpu# -`B;@cE@TS&mQJ4sLuWm=Aou#;dX!A?5+xqPwEMDUZ!PlvZB!k -~mf34;;_B^Ephei93w*qwF02?}u%3!Vfv>Fj6O#Yz)_P6C~D_?!v(LY{;?33(FoB;-ka1XYNW&c=dmk -BHjY0k(@03r$4ZC%OS4Puf$KJx|$}hyQ==-V)?VEPWEsWl -F%jH-+L*Rn+RnR$|RIYD3eeovBF8{lF%igOG1}~E(u){x+HW-=#uWWP_f=bD3eeop-e)VgffYBO+uH1 -E(u){x+HW-=#tPSRhQ2GHkX)s7}lEzY7*2Ws7X+hpe8|0VsVooCP7Ssm;^BiViLq8h)EEW?t6bnS#Kh -!Nl=rZCP7Vtnglh8)l7nz1ThI>604bnE(u){x+HW-_kNZ{EI1L$B$P=glTaq1OkyFE&?TWuLYIUt30) -GpBy>qEU=pyTdp}DemYfJ+62PPa)5&**4#Ywxp-V!Sgf0nP61pUGN$8T$C80}t_Om2n#feZRp-e)Vgf -a}&0_-w`2 -}n+RYMz$6weiA77ol!Pe>Qxc{mOi3(O5}YJBNpOG7A*-$5|Sh&Nl20^Nv92j>8FJ$2~!fLBuq(|k}xH``$Ys|y@^=9B$ -h9UPXC -X;Kl7u8_TaA4uzi+_z#BYC7kt88WLXw0eDSMo+{|qOGh>~DEAxT1av_PM$>k*TM%kE>F(qS4-tXJ -vruHyqATy8|$P8o#GGiRZIE--^<8UJm*F9YKaNWao57*uK(=0hLwq$I{*pgYaWK7AJk})M?O2(9oDH& -5T>y!*98BQ{s*HlV}von@xyh)b;EVTb;EVn-_4>E!%Bvg%u*%8NrsaQCmBvMoMbr3aFXF9!%2pd3?~^*^ -30cmo*EifGOT1)DH%?3Im!I8yQXybEi$lVV9CIeS*K)7$(WKcCC`2@NpE9+clI9`P24Qvi76RVGN$BR -v=mO27>0nNOGcNBF1fmN@;zd#I5E^@sL4>1p(aC3hMLUsB|}Vxm<%x)Vlu>Jh{+I>S+?ZeYn_|XM$#9Zq(?D+FB+uS`ZV{a1-I<-E{u?%poBftPvuMe1lF -Lcg@4Ldo<;x`l5nfEmn36FiV@k%9tbPA()|(hxGPY!F$t+qji6U)|(h$GQebj$pDjCzGO_vn36FiV@j?mnO9B&O9qw4BpFF^CCR -++F`Q&L$^L!!rl*94l?*GHB~6Bt3?~^*GMr@ZNJJJ+GMr>M$#9Zcz+@!J-p`Vl4Bp -FF^CFxvyQ%EbEWH`yJW-^jwB*{pUz3b}DauXv;W;K(MBqK?#BwfFK6i6$aWH`xilHnx7No<}n>@w1|Q -Vei?_oiorhLsE}nbk~&lME*rPBNTiRx=q%GLmE@$w-osBqK>ilDzxJbB%x3o~6M`W=WG-(qtsbNRp8x -BS}V*j3gOJGLmE@$w-osB=2_e{A}+I8D$}pkt8EYMv{yq8A&pdWF*N*l941MNk)>4BpFHaZZFTz_Bdp -eHB3g5j3gOJGLmE@$w-osB(sLeAd*2OgGdIE3?likO%y--uYY0;;t3)dL^6nE5Xm5tK_r7n29XRR8AL -LOWDv<9lFv7Pvf#ulUvd%YTzeylCz9k!($U^aW>PrGEMzj0WF*N*l941MNj~ShH=Gh8O8aer`$cQ}En -nXW@=jw&l941MNk)>4BpFF^CF$BzemKdyH$DH{!;n!}$*g8FoMcur8A&pdWF*N*l941=lFqfy5ZQ#23 -@7il8hvowM+iiQkwN9W(|`;B!fr>kqjakL^6nE -5Xscerr0v2fq8#r-dcIzHSPJ|ZVklW{p;n!-@RNi1{npB3?dmsGKgdl$sm$JB!fr>kqjakMDlKJ&)@Y -pWE4a)h-47SAd*2OgGdIE3?dmsGKgdl$sm$JB!fuat?v1|9*1OtNCuG%A{j(7h-47SAd*2OgGdIE3?i -8oOKiV2yPUwEADJ~L#*d6289y?9Wcu7=e|}`voESeceq{W}_>u7=<4 -4Ai%=h9MIx=)*=*ZBKS*K*yDH$#D_q}*#&52PXqee!Jj2am=GHPVh$f%J~Bcnz}jf@%@H8QJ{j28L(U -Ocnt#Hf)`Bcnz}jf@%@H8N^s)X1ojQ6r;9MvaUbnKem9i~M~ro>_Hb)X1ojQ6r;9MvaUb88tF$WYoy0 -kx?U~Mn;Xynk1t|zIQFMS#@I6$f%J~Bcnz}jf@%@HFDMH+Oh&VGIV6<$Sh1U3zLi%`TpL&v+TsEk*h} -6HXYEBp(8^_hK>v!89Fj_Wa!Ayk)b0)M`m@B@B6zqoE$<&hK>v!89Fj_Wa!Ayk)b2@T^x3*8@31v!89Fj_Wa!Ayk)b0)M~03J9T_?@bY$qrtV!~{-z8?&oftYYYmW&Nzx79E@rh9*qee!Jj2am=GHPVh$f%J~Bcnz}jZCp>)W}t%@14){z2{-^iJ>D -yM`jU{0V4xO28;|C889+nWWdNQLNbexj1d_lGDhTY{gGLGVywtmk+C9UMaGJZ6&WisR%EQmSdp?daTgRS3tCalO*-3 ->g{n`*lVvKQTgNgvbby5h5c*W|@(}A%jC^nUO&ugF*&{3BHPJji&E@gUU845BKWGKi`kf9)F&C)DDF(hP2$dHgBAwxojgv{b1LqUdu3AZI^wY*wHc5;7!YNXU?oAt6ITW^s|BAVWchf(!*23NjRAD9BKdzwfUzD^Lsx84@xiWJt)6kX -cv6X5hn)tFzz4#us_aF5}8v@{(u<-8v_6Ulm-9*GXMYpaA|NaUv_0~WN&gWa%FLKWpi|M -FK}UFYhh<)b1!3PVRB?;bT49QXEktgZ(?O~E^v93R84Q&I1s)2R}AWl1Gw6_ivrsXdXN=6HP*5X$wrZ -rK}#c>C`B41l|cRVee_qdjRWlJWDe&&&U-VSZuj~}!_mWJM$<_$n&l%JPUp!#^iFQK)1~E(X>R3KYN0 -4GC6jcNnx9fBfpc9qS{A|^ONElGJr&4_I%nuaOD}=E=O)a)z-?IJ`yVQOLstut= -myi>x_H1i+_V8i0qA5F6N)N2rLd~MtC|LkJl?7|ZBpmHS%*9zQ?`q0bmA+AuYat48&~fL!`MS$>3f`I@of2chg>*~TlDY9Bs7oGjT3soYWeBiidwpeku6o)~Q|SYo0`;4iXo|5p!{(c6PwKwb>JB -UzC)zE96qVsfhlZTu -1cyoq;<}!C-rcZ#O+s_xIn*xghV^Pt1EwOLb>z- -p(o#NrU4vhYte#s7}XkDMAru6lS>fWnT-yq-U{R!W{zH3GAHv)^T=n=fg+Ol7tnz+`Y=HKO6_Knr_Aw -2M*^9xW*0|XQR000O87>8v@d9qhoGXwwt$O!-dGXMYpaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b -1!3PVRB?;bT4CQVRB?;bY)|7E^v93R@;u-HV}Q+S4Iy7&#(H)EzeZ)DLXV}#DnVVEO6iqLl0SdSwreoUk_71U7Rt4>(Gi?!F<|n8Aq>lJ!EZ -EWuvNM;TerBle0NDNehsD3wW{b0g!A;>-hB^{fpn!(Pn<8(^CbCLgEnIllv8KThV0cGlpj)7l#|k3@1 -eG)1?MT1GzkPHVWlYQ-uD)Z0#wT!fLuD$^d5`&J||eEJ-O-9E2HPoYeke`(I}5rHbl(0)2=#c9ggpA= -i(vCjNqn2J)As{H(H8fgVxajMR8(J={UB?CNfP09Cmyg4OfF9NsP|BLCY3%5eN{o -EHKS1@b6b71~f>bhf-Y)084(PIxwwExunvLtpKJb=TL1rlo1-`Jg-Iom&OLkhz;372q1%Nf(nn|4Xh| -}#y)~f;~u~3aNP2d3Featdb#uHj$t{W3_jC0n}=wqN16shp?Edm9O1@26y-_&J>*ZxoX&!+CQL@5E*h -c=%VAquBUXXm=-a1dV;l}UExNw(1l_0cF|4Y3Fx-eScX4~bw6PT8;rua!hl|H2jsI}@C{hZlm43?E%% -6dq@8SN}X9PN&CT?RY*f@M4MjF7l?0$3~eWb2)`dlcp7Sl(zGpWP@tKd`xdp2v -P5k{?Xu>cqN+(tf^^zjjGe)!q5bxo=Oi0Uwd=4{wZdu;w(+b5pOrqPi4=tcox|N74R6ZmHy7FM-5n?g -y(1i@okv+6H*1XHXar8Od;@<`uRot-a0rrNsZU1vNPi#fUxH=25A$fS^U3vpmGo0Rhab;3$6n?qhwh0 -5r5P;BMmm>23GfPQNu_rmu<729&WHWnNR29eu@05Cxu8}t%?8EnDDzZSRWVXB6+x}V%PqX7vvTpCbKS -0K+71%3r4`dRO!7Ul$Z*cKd)huglNCWMM8K>E)Q{||jyXh=Di -7Q-()5&+!hfCM?S~L4c{{_fSx=qP#u-T>Xg{tmg0>3?z-sF$#ub&oI;J1pzO~y+hOnSUzmW|Z+acZcb -POY?Tg3k+ZNR3Y$cc!=T-cNa^=@qt8`_Mdd4*59T7oZjk_Wq$K^DtTYP+qilws((Q{OiK4X+s)RkY$j -VEDC<3e;9cVq9|Ati$X+JgF?=de*jQR0|XQR000O87>8v@_>jT?4haAN3M2pkIRF3vaA|NaUv_0~WN& -gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4CQVRB??b98cPVs&(BZ*DGddF@zjZyHGw{?4zci*-7<#T -XptHg?vL0Aq*5#uhJ%e5(izGYxbyOp~4_g#GpVR1YsR%-EP@rS(aNEW`9vS5;R%_0|Um*&nJku9_{nZ -q^&EPJ?RKxAhP70PR8UfVyKrw_1*5;P_Nmo*)_0jrv3SzJuqnq0hVu0SRZdS|gGDB -JiK@O&ZKe&WEkdEc2`@THT?IzV!3@XDhlMk&5ZyTX3*AY7q~(MT2^tzQitZL>tb*nFVJcq^w4)bnwV@ -V*LNzpVDE5XHYDTUTkQSZ{Bla+aAt&&Tl*ayv^5k&AOS8bkaae03^dwx_@T%3LYY{?Ix)OoVIL+N`;7 -b>quca%3P>>VSl-I)X#!{E8NN1@CT`I7*)_p@l;yug$CUl4uIxbaK(v}>H1YhSJ!}`OUQ{GsB(hKt84 -ZYe~$~3{|q#nt@vK_$UH0JB<=iM~Ib)*_c^ -IUYmFbmJ$1f|#3DH+IUP{T1mY*biO}kV4K#pD|gx9c;5?yOvy?Tn^ -h;FCQ&oWHNeBbP(L}Kj;5-rtEd9IpFIh{n}=GvL|mG*MEM(ehB^{#(W?N!-R<=Sol~WJe3I+ -*+Kv8wql4#i>GU*uZr$3)awT`uY&CCsH|fnUbGe-BJ0Z#x&UCTLZ-}-ks@o!z;h8;*Cx+DTOB9`cKOY -KzSU6iB5=qLLP9D`%jP$gGYrkv7NUkMmj(8f0Gj=J+!~^RaZt_^qV~BjtT37|ooKJ>lwiF~$mM- -_`}!@!c8937Z5=hth)?M3k0eToM|qA%S>KM8D}v?SK`OL+~&-hX-kb3!sH9Nwfm!mE#JR*@xrd7f-+n<7F=Jz$6QfiO(Q@ -N)3P~?YQWHUMr?K73Tt-t43*hioz2!-_8yuEwxH|JY&jo0VT{*6pmI(eF9m`N-g-`#jvuB0qRi5k92* -t@ykddrxaa^vg0oO_@KbmRoCpeLlR?x2#>nSm#Z`0*?Of+j7r8YEP_d4;^KiTqLJ9OE2+q&HA@g<~U1b -ZxF+S^!9;N^tBV#*(h~%yvQy@Nz*N5E=ySMabf>BPs!Qh22ABssvJ ->1pJsNL;#EjH_20YFX`!$UHHO)$LZ@M}Jg?5h>>emR0ccUUVxG^lPZhg#&SeUD6SGF8T>dA8Gc7h!;| -7Gp7}}KSi$Kjr<33|1UC5h;!v#$_JnkZW@!SJ3YXo7u;f=iJX=fhG&7~NZOCG1Aa<$3nC=5cQEFo)5I -K6TfwY(%}T-Iq@|FnPJQf3pB45ujFY;tJgfFId{_JGLC9-5U{B3}mvZon{^9dC+pj80k7=0y-%jn3^Gr404=%Ag7+AO}mST -nFn^=+emz8*hp`To89sSwY`F-oq`-|HutiTv~gIWr$4KD4w2`eloKt(7qp-AD!ZV0L&lSKL_SP`F|D6wer)!Tr2-&V2@$Nl7<;OD#3pUdU -W9*xP#GEROKRDAZaupJ83tv<#Mo~fXCaFDCK`ZXgkswdfjfyMv4Ot4Bv$PY3WppoNXkl`5Wprn9Z*_2Ra&KZ~axQRr%~s!U+DH(7=dTz -EMFExgpr?~AP4fU0QdA%ah^nY}LW?~PtD0SFcb&-h*WavdFkriZdpbYi*_m&@`R0eI*UKO14IV~Q7?1 -jc>0$uA@nioRRQT3wby#i?9u6tz@55-Z?q#Z+u{^A#d!>69U!N3d-p3UO&0j*bauC8h%KOMH<+>fshpt=Y!c}h0xE~vMedv6Sc5!O54c -EhH>5uOAVkXajXDd~2$4oV3R;9fzVLmt{QCH`gzoeizIW&I?sWNlN4_LAol*r;B)$!)3q7E;6cZ*U<^ -(Vq%==#{qT3sdN6TkRe>hrB2a5#^ALr18S$Do1^`FMwIn18sv&Y520azgJG`4D%sl3Vfh2)Cf0rx>j(}Fj}se7pZ3iTzFyuvN&d)H~fA02XH -;%7iLSWx(o`vg46bGXOTAX}mE3}3D`w}$rFk|~Fdy@|jA3wLLjS%g6kh*@4S>v}}v+G!!Q!^GobQ9h -EvBo9z=GUf!%aW(fhheW@ZWI2BlaxQEWA3ca9NWsR{CY>TI2rBhLXcAQt@li*3=a}R}!C#hWkW*>znQ -X(Z>ugJcb-*H8LwDYFP@X8~df)dSKB_GjClxYF$MTh!a)D1TIs*WJh3>PP5K8nfO;mlBNNQ*2mp`Q>h -Er&UujYYlBmyCnSLL8MKRMscuR8{PK6VV0cw0|%tqcD|bQliS~3=LVJ&lsYc!y!7%{M7L?ErsH(2bc? -+0Tij0#=Mt))%o!t=Yw}L#z{9+f}dU3j78hIz1`c-F3LW*oeR9C_MpFnzTL1+Vf9xmubtQbVfM%?&wA -zII(n&SidZ|^(4SA`B>ylPVddH>3okq$;5y-#68lSB7voCPa{YWSytl01&#aKu^_7MklCHYa#0{?APp -Vpw7Ljfi$n9I_GaVP19dwrK|L5&=!51rU@6(~(QOQ4INUP}6@i*;)?^(U%+FyOt4lDdGP)h>@6aWAK2 -mly|Wk~oL=!07W003?W001@s003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy0jYs{#Bpy(2JnjEQ7ksa3SF~6&O|4u>Efn=l&LkZ@n4eN9K}#*4*gD -)~YhfLG%Gq<~jin5(QSFE@($Y(~-SHDEvt>)BAb$oL{uA~t6I+I9vnjRAgqKD;9PQTF-2uQfGj?y!ED -O&$rPU7cZ7N{{*0w^bwWoT&K{xGo2M}l7BkC$e^Wf`f&W`O9=le?fMtdvM67|(x<0C9Y55j(@nN%w)H -`F39^yCfAD!(>*=Mk?f{Dd;$-n`ViQ|KTNBvmrC!d5}u!ZMRHg?|R~TdGV+nb2f8mu{aQN_i4G|J_)` -Z%i(WDws;0qux-KXS;Dnk?$u<8nZ(wbkCYC($C6N$qd0`nXz_E!qGO^-1;QtFc66kL}+LLzmxx#SmHk -94(*49ZlcaH+X1nOTB$C4;E#51Ltq1>D^h5eJw`@nB{)@@lhixdHJXDz07RkBcz%>Jm5z&2HGu$M`Z8 -L4o-P;EojlT)?tI>zEFN#+i&o$k4c;Tk%~nYOqbe)3uRt6C4}*FCGa|aZ(Rj3Y4D5%a#bgl2G@Q<}0xx7+{ihx(q -gT6n5pojrmSjQN_$EDDiMNC2`KG%-IrX=RsmP)xedxeR={bP?OW>2%ty -&L8a#(K3eR(no{I-8+}hRjpQ73{4ao7RD;Ww0ktn)eZ*GuGKOn9F_z2Pt1O1P0IFT4PytD3rZ4+`A(9 -$V^uWv_V8^HQ+x5Um~#KxB^;w<6MMW?`gBSj@!f96NxeEDtk@@q6_{wT;n!f6eQ7mKjOJ5N!JO|Ln!+ -#pTT{kJcrt3r0>y*LxCg)*B6V|J+f)d3ZX7}Fg-PTvp`uYD;zb`M=)bwt87W7|GO9KQH0000802qg5N -Wj?;G~fdO01gQN06PEx0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>WppoNZ)9n1XLEF6 -bY*Q}V`yn^WiD`el~vo08aWVs=POF$VPHepA<;^k8RY>8Oi;Lqiy{<_km;@lD{dRvW?1&?_qe;cWQIw -yexmDgeCkw{DVMJwYIPoZL+bb1o#C`Yt^TO}gwhU96?zD`Amqt5$+O4tK2Y_j8Y;KP%3(qmF4jJOhtAq_$+XJrJc& -fCk)cEk>Z$MlPuTxVgnuJ(K6LxH$IL<3+8||%J?YyxSSG|LUXdvu&8cVgdGNrc!hMv5k@xgD5F6xIgO -0KN1hoDkonaP>N??LRI4rWJ@&}2E5E{BH@o`jD7G#2@(iDhyKrk##aPGIfXrtYZW!*EW0c3?^mtl6TP -@ouGLgyg=ASUV=+Xd7&9b5^Pwh)fG2)HQ(5)V(K`@Q1sf*g9C~UA=LzZ^-XTO| -REs;tod0O9}`bSCZ3$Y{2D{oZU5*mrxgVP`s}?r1_y8aF4iUVGkePG~%zj7QT>ooLE@lsJfaP2`ir2_ -RDgC1dtNDwmhOKzkhss+cz7iP7eWB_@glR%CnlUp-Vtud{lQau)QSWJ22b8ttvb>|pv{bQdV+{?;(`Q -N2cgH4r59cZC*Ak=~UFpu5T#Tceh7K7nkkE1|vE0jL#j#3iWyIfBez~XHi -Ua0a4CIk5N808|izdHHSUI+ZaT;2*jjdy}~0Tt@BxUEyN4uOqP@ZDAsGgxkwu?-ha6x(b^>;SL9JxVH -PQLH56rLja|kVVHAsy6GHfCtDxeg?%!@QdNZ+7eDnD2*xO1}eLQ7?@KuG55}j4}z25z-f6ut(_9MHZg -vU$O!+-|+O(Ar6vnlIRmQ}m#MCf4dJ0V0MkT(^VSFV-$$2GmWBElcg&EpR>ozI)2+SOmo*pJX>+9>wS -?C}?S@#Z5nP9c{4&gyH|ohKQ3ETi1B3=(73p0C7Jn4(S3fU@n+nJ&|O|9_5FRI5dbI!pfnP)h>@6aWA -K2mly|Wk?|V-8oSL004yq001ul003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy;$g+#rS_F$+_3^ARc~soK5RJ2UU?o3 -CE){ex+EA1$$nX5lgkFDIz=n2w6sN$10jNj=(mThO+tAY$%MQ~(x7P>rM?w1=Z$XfQ@lOiK( -toK@Ai1+Ghri#Z#0%tTrivFK1~v&>J~{}O-~Vx+dY;Ff>ej`!*5gar -{S41@^DI@V@fH0$A0u8h-ZW&w(lZYZzM;<_pZ7uLx}bqawWYT)S?kF9;M;)t3o5xlqIu?J?Ta(z-yZE -YkcU+&>x9awFZ@^bGrBv5+}8v@{8hluwgLbEHw6FyG5`PoaA| -NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4CXZgX^DZgg`laCwzf!EWL(5WVviBXMaZ$^ -xq%cF`VE8roc^2H#6_qX}2#wbQeD)DNT}5oaQmzO=hFt^ -aZxvYE!XgnpwG#S|}Qsib*<}nm -e9GRHV$TTeb_Xp>;ib_IOS?C=S%8@4#vbFPW#PG^RoyUtE+uT>+Se$x^>j75ra`aQ2gOBrmm;Mo3s^@ -PJGQS}9VZzX?X6rLFyEEC{)q_DRM;1qOSQ3bM>m8HJ$Xa(+HZ|+5c#&oE0hWQ=6&8qp_4$6RLPYUwhr -p9Ei*Z@_HXEXNVPdoDKuHGq&wD!Ql5maz8Q=BovGxy4xxjtFmD*kv)#F~$Pej)ChRZ@J+Nkr^7F$=$r -8yES+aIa!qGm&-1bAtacCqzH9}(p`bPdCvBZ9;9qdPh2HpO-+KI7)S?Nmn&_C?nhs0|%U6X=c_M924y -r99ys1-@w@NVG_KEQ}c-*EnD#dL7IKBx{DfTgFTc$_T?iqa)LMOhZ5#c~KNyudCTJ~GMOUP%yREGx9H -(K!gFaW;BHMs$};H058NF!+M#_hxLy~LK#&_t=N}L`X&`Y+Hj|Ht1?yVZKGcy-v&FY2{geBbX* -cm$^jn-7KV^IlfXltG{)maKh3E|;@$QVsA7@!HS`^R4V-{mExew^>kK5K?P)h>@6aWAK2mly|Wk{}I09!sub2&V7W;pH -kUiq!jhx#Fnl4~n$RjZU+W^yL?F -CcbDl_@C|nk;A1HSiF^lhD~`W07B&Oy*T>Ds_f(YSx}@;f@+UOqVof2d4DEnk}lCmun?cB#&jv+A#@7 -yV~ZqXQhTUk>o&xh6eC^b?=BJ{Po?>Y#l80CFmV*J0Nz+Sn3F0^G7##b>bCBSESGld&~?jN^q_;BdPb -WTWF6D0HV+rj1jGwO2>Jrx+c)CL@l!h1mYmj8IQxd?a$_ni(5C_0y5J% -sU5%x#p(c-CQKO8NlgCwEhET)j=VZ0bc%W)Xfd>PMY$siy~n2!?IVqOyYBykSN!a&K4y^zW^r9Yv)4g -^(3Tk*_jbIKAEMFLM`SN%~BmC>7hy+}C=dPg!RZG4wrtikb~_a^~QOOS|QS{)O<{f_MW8nOd8KI1u&<%t7rOvstfCV==ZX{j{Pyq -LWHQ0FQms}iRl>Q(*zoIIrj2ERU4hBSX|3^qXXdL0zZKK6FRi9e`x-yB{sT};0|XQR000O87>8v@rY+JraRdMWa|{3gG5`PoaA|NaUv_0~W -N&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4IdV{meBVr6nJaCwzhU2p0*5Pj!YjMNv9Xcl_4(rvqa -pcGoR5D=ld>aJGEBonZ1Vn?=VFZ=6voG%E3Zks1EK65;C<|ABO)IV@PcpSMf9`y&VH-P)`wEqleuosO -BSZolcim$m~5%gt%z%|73Umiuwk!lS-*_=iJ9SLqYAs_G!BblHA8G@+eKS3wNLwMKHHm4Rh3}-Z02A=}Tc0Uu}(4P2 -IX73&4b_pD^RmTJabzfg&_uB+PbZBSoG+NfYsvVrpkq3Kb$~8Jc$r>=t%%3qBK|FyK0)Cm{?l5&R|<` -NuNg;m(@2BA|3^)CN_o3k@IL7Z{_aNZ}C$D#~WIT}9j{=P~zDXauGqu{OQgD>bx%czGZ<iDZdke_v -+}{0$N|ObyoNjSj&e#U2pvjDlpQ@Qy#});>gNUl33Qwn2f_bDUA*2BmXxMbs$a&$<0RrlOj#)=%#H#lg2`aseB&KiDh5kHZ{HPwBh$IftdIiQ3*V9AM%i>hmn-34fj$T3!U`Hw -{z9)b}qZ$I$eMlk1XdN7`P9gTp|~ZMyO;AjxCs-8lQ&QP{DL=NA2slIBY)s>iDFGb8jP90;Q~u^dg9rWNp@1Y#EI?c4EC; -AvXm-lG#Y-yw1$E9D;G5@Z?s};+yRELTyk1e%Vkqz6y|pORPas}Rf6oBvyAf~CvXRUR|GnZelK^c{eR -Hoa(vpOEZ3(!4oX*m|EXrFXknXXt5FJ<89r3)08%3CF#CmGG#^J2-c$n4=C8!xoOBX}wFLeV@5@&x{X27TJO{jA~1%azorrB-V+%X}D?g2i|&n?kcy;tbmnjT9jdQ -1~Y#Bvw5^Rkr-Dj&pro+A-322G!~8?++1!^wu`8`|aV~X;p1Gt-Z=be<)tRJ>R4!_7lbED@n`LenWO` -`dnK-vI_F1RcXteAu1$a>Y7}Pk2@#pf|~BB@y;9RizNPj+NdE)E`h(yGF^4eVrw=1R;wWppoPb7OODE^vA6m~C(3Mi9r}^ -C?D`7newwVw=Z{`hswfkRt>E(JA+)TI_YO&e(Kqa-vngduRLrm<$BkYkR5!i5zz3H#@uQnSadL8UKQ- -&UJ49{a(8>7kALsfi1xeVIKVEdY&i+>Ip0yD-|-=p6^C3dVZA8(hC+ -+LEu2d#6bUnj6zE+Bo58zOW(Cb#FtYEMX?quoN5-qdHVwNX0w -4ISoadNJP$_X8^TD2QawvMSxGzaBjrw)QJ~Jliw`&ga3|DP7`on*a_67{W*9*fz;G4K0zX|pZjI+ol~ -60%B}YjKVcFZ%`&p_GN5ClQ*=%XVA_NM~MMU}IX)Xx7 -z@%5;;c2LhEU{a#S*Z=KABUac-s2w_5EuFlZzGsWSccMBdPR(YzVNv>xw(IsKx^;_pIf6*YcP4dL|@! -#wBq?Ck=*&hb1^WkN(nz&Vd5Czt}|-iAkn(&^?Q>?%D&s13_9a6bni#df?;bk>9rsFtq}|#M#KAYrvx -yTGTUO>Om36cTkK%Sd4M}<%SgChm@NGfk2k~wJsW1?S>iEU(#1Op1eVBRb||UV`g@A4 -Em?rd+&t@a}tYx)^11zD2z0tx^%HIywpetA|H0M!f4f7`p2PfhxjP5Ju#B*Fv^z=z3Yx&9V;gFvgL6F -X#+D9@#|B3I#_6b0}F@#1S4;NGC_-Bgc_Yn2+ozMXAVM6yC`~;pWRPmKVa$a9+@h<=h(plPqKXmFtBv -XKRqnVQo5bddBH_8*w_%8N?a!KhCYPnV*_ky%3L9bNSbDpQ^dbHs8>&IpjyvPx(vUw&snpX4Kbwnm@a7+=beVW~7JFo(a{L{zObYN -vDGKmivjF#&>u4ZmVl#0V^=~F3E+LW8F>jfs9gv1-;#jpj`ke2{zL-)`ut{DGhdNlHd=OmW)V57QihC1kk%e-Y1!@YWG%~T@Ox=lozXIriTQW6td=TSgVnMoJ{i1e* -_)RA2wH|k`JJ`wR7u;TWhXoi9n!Mb1#U;nQb{^O1b4J-KT+GAIQQwWmc70khqde|EsVA7WD#7Sf2FKp -U)#Q?j@H7Q9NrFJlfy(pj*>&Ye1aavRdA9X=GSBCxAuKa1Cq9Rlm_g6b$3Vt;)6d)0`ki`Rsu -eaNCNQI^Tb!#_yB(Jt0?>lP)h>@6aWAK2mly|Wk{@iyhRHI003SN001-q003}la4%nWWo~3|axZdaad -l;LbaO9oVPk7yXJvCQV`yP=WMyJj~{lAaVs@l|Z3a&+ -^YBmg6(zQKftYsDjY1C*fwBVm%5jTCwPL`}$7imAl16evW{GB8h+*aajq29F6)=yRRH6A=cO@c)#G{3 -Ge}AhDuY_*9O~+MtSb!Qsd80*0t5Q}~Di6;(5dS0VSvdB{B!8i8rpTAAMLnYyuw%<@FwMiP9=?gbzxy -^Z~dN|OZMx}73gj@SaJz+uMbPu<*F$0jA6KuFzCg{NCf!M;)-xJY3)#6A5WKq~Z!#t2pjvBn?{TZBNi -@MSQ+y<5zoJHCf6-RZPDp5I@PEip}6>R*P$qix6un5t4tn1m81fKh+iyCskA)nGW7-`nhO2J>-$HiMh -HDRg1doz4fn#jrbt$znRWoAq6Q8Dc@PC9{s?9mN4bMv|EH(J&tBtn_lZGOvqpnDWv`1)bR$qQ0vB?LNB7!L?+SUe@-|FqG+QNCL{V-wibKP%5 -`#U`&d^L!ZSF%0h>JDB7>t;=;Q|Y{UaFZ{%_!3pE(Eh1O*{6vLx0ixG`T)dbZKKHniuK)yY+;5KC-r} -0-sIEkm}GnIx?J%Mzs0DMx)_{OzTXT72?#_M~jE{4oz7A%O$7vwOl$H!yuR2Q}5^=oA0dcKX)1Bw2a` -gh9T!L*@|n0oA1=5%hS9$HNp3M9XOY0Ec+>1&xV9MLNdY2*f_BLNjB9uYc$JzB$t9kSPrSs#7e*`D&o -zj$*a9%fas^jEnrvip$i+*{Qc@i!zVj)JmOifHb=4?sP -oiCI=n+(Iy6aLV-AXf>wa%6E2fT)Sp={{UL7N=V1C%oNn$$G74u3h=KjL-OIOngR^MH)8G$X9oujp&$ -+mRH*A96Y{dW2QFPRC6jf`R${$Rtyy8rilYo8|KCq>YM|LXkDh&U%$Gipg!H_K6wDK!;2ld!N5{9w<^ -eitUFVIh618?j|09wTk5HoBo4s_p5Li>YAqgEZywJ(Pw7ooYTa09)36BD&Whey?l0AlsDKq(nU -uRANI!7^4(3&c1yD~Yu9MdGc-o$buVJoSh@J4G6;J!-S@A -5cpJ1QY-O00;mWhh<1Ac!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7 -a%5$6FJ*OOYjS3CWpOTWd6eC4uWU(mAn5P+SER%jTPo9MN355~v3ewoad)XPpaRtLXe6sBF6OF>y3DP -@Zsym|epZ4#lHJk_lGAlgX2jk*V#V7Naq@oed;jtO9)I(r|MK}y9zXv4hd=ts&wupzn;-x5hyU&Izxe -F;-hJ=!i{HL~{Pfd@fBNvvmtQ}A`0>~8A0NJb{QbxO`@`2?e|i7>yWjocn-5=o`Q67)kFUP|@cx_c9$ -(#``Tn~HU}Az5n&&SAU!w|NW)D{3Ng6|M9-mZ@+u|#Sj1TaXp{s&f-7(`1c<_`1T%!UcaQHrK7RJ -c?|%F7n?F7OT~uFx_|>N`KmAdu{pQpA_m6Ks{^q-Xxo_;RAAkJ#hsRf6e)IVB{?{MAePw?a#CMM`zxn -ll`uOQi|GSUB{_vYW`qdx4`Stxc?)tm;pMLl4pUd#S{N$I9AHVD(PxsUO><_>C`omXu;v -awb>isw0zJGlA?c?`;_w8^0t)%|Wjr{yiH}d$qk9YP{x_|w6|KWb$wg373r*H4yJl6NGKVR)nIezxI> --sOg{LcN~wfTDq|MI@+KR&*`Yva?OQ>v-|pbY-)QGfmT@Xh4=+q>}Z|G0C!pY&huxO)8R{o@bczW>c1 -zW(ga;r{I7zkmLV|N7Hk{^Ie2pZvq)zyIKiFMjZoU;M*g-=DpU`hNGWz*kNme)s!327mpyAM4YX-+cG -S`_AtS{{BZ_{P4ftDSq%bpa1ytU;KmH|GUqB@sl6@{O6Cq`{@^tA3T2cgD-yZ`44~j;~#wS_}MSN_}N -c?{-f_d9zTEo{?BEsH2-XP%`Qhtt|Mcwta98lR_Z@!y>&I`u{O9*~MSu1F!y -QwPFYiY3`#=8ws>QEAe)CWBz;}{=E78AxeE7}dn~&dp_V|}iAMWV6eQf=K&^_x#0fA{$ES88&-{rw%`|MR=Y?}XCbI={Vx|M@Sz`225v`HRng`jdbB;s^Kty -ECirzy0gye(*Ox{?R}F{PX|gNB8gV3ohmL!{2`SNz~jC^MC#H({wUl{_5-ZkIx>jRNnu^$A5eGKR^EB -@qfO1Jg)uO{eN%w%X<6l+Rysz5$F2s>g##G+`oJ5bv}F4+w-%>x|ZKvpFR4m@6Wy6@2=;3ZhiK6oc`? -HokROsu5WRN=dRDL>$-op-nKX5{;#?d*Gd0%XPs}y8|kIIOa{lzoXK!5;N`x}`{lm;757WIkuAwv-Wl -$a&AxAB@{e<8{aBM?yV7J3pS^oL=uoYJyh4i|k=b -hno-9JC?r+=LL?K^8XckT~9+A}JK%QZXX9||`VWcY}*$1T*9%3YA}?$UM92rrXP{#CB*K_|nxymWWf+ -&QeN!8^lh_djH4B7utDq>u>29WmvMFin{CLu8C4UIgjj8D&V@j-Iua=(NQcsnM4IL`ciJC+*W -CAE7(-ct89dA`=cG*~^;eK0>Ld`tNL1|NUV1eVwNiE!QKkuUCe5>F(DrLnL_QJBaTLpDq$={{D1Mp$U -=NauxRLO@XVtzTVt=Plor6JcZ!7=5C~WzWKBdD$Udk+THYXd1HQ57EdXjZs95Grwn6S*mFMf)bzIJe! -8!@x%<=CU3>R`DKs~C?kBn4?vlEWw!D?pd7GCz4)53PKf{aLe7#d%+RIT}Pyh9l^*r8PG|_W8AGxN75 -C~5#y{; -582$$J$=!9WZqEBJgKeyqw_i2MYfM@`D-)N_f3IZ(!SMv?!H`2r5qLfRJ!>pAQ#o -g#6*HFAuc^?PFd(>=dPb~3-=pOA+9DKn#Wzs*IZr^=$0N^^a+R1H|;4+zuTJ7bDr_O$RkqcP_88IQa7 -rbes|x4AXtJ$gE(5|L(Cn$?7l1I5e5jcd3QF;l{}sX_ojV>4kJ8R+;wz6vfl0J-9kg%O7m^a9V&r4Y9 -Ft9yf6IFJ3l6Jbr|>W=7z;sOkejl9js>*Hb$Vk!FhQ7$!gvGM4Ha-gnPbCZ|M%FkMzbgp{-w-9$Wu$&pKpy*Y8~^TEz^C55aQP^g9#m9z`6M=zHqdR!=tba^L@TO}{)p9v -7Kf)~%LYr$Ijji#f&p+?uR9nre+D>!*S=K;&HW)!c>U>$&e*zUInL)%w%|-i*sL6)|L9Mx|@&*27`%SMQ{TFJdKS5A#uL*9`n!|t -XnETwXF-2>gE>4@dG#B08JuxyWNjI~}Fz!F6sMf@o{^OcX#2p#1wZHq3^-|L>Tz9*DoY#%aZK?%>!BZ -{hMTTpGl4-klsI)*zlSeVzLks?Sg=XPU>Ewyaqjkd -~n@RLWCTJay$Wzn!VoGgP&7m!2`N`LJTv9-g|)=gbeE2L7l1@u^dM<^-&&p6W3_jCrWJNY2b*QYd;+^ -fc_e>EYf=Ew43it1eIW_BI2HHZ#4fW)^yg=S&x$6Hj7Ou9rUYsUUTNcUn`CHC@nzhz|CtEkA31)V_5z -+XQW`3Fn!)D{D{kRMyio)>PEoq51Tk>vS}FDW8ewRIBl6&!^qjL|ROzE{ePx`Z`Z?zgQWV&fP6XXC#N -E8Kkg$g;q~3yDtwqNmfjyn19aneP*4W_L|t74z`5Dn=$gzOI?zzY44N7+#qXu4VSA~7R?RM&8b_%$+M -?FcZZir#F`b}o>{tdZ!=L<70>y!BAv&ZcBGfBi6`CTj4974!gk_miqEIcG^Wrb(}HKIH1m(=bDG|BRti%o6JLfVR -dM&s`Z@i%dYgw@bEQ{oy0CHJp)=Te#-*h^jBpQK=>5q0eDjgDrkC}*GySM;XrjZF*7WiB$gy4>r6J(V -H-`T8bQQG&z;JGxC`{f?>?$_|2Gj5tJ&BT7D<@cxWH%uv -B|E2LN!=4UlO*V?g%qK*f)#n|DdhEK(=6+k4&Id2|a81m~OEElVEDfK#$C>*@-)$sc%NSH+nsNq78k`R8cUHNP_}-1A?0R4>Z$ -UOdApyF+rB|VhUD@@AWbZCJ{e9yQR$1eO+^-XNZ#(LTjka2Q3yUfa*k5HN`1zQiKofnHH=P@;U6Q=tZI7RR@kisrXJ^R44 -&pr%c^Wy=`0J8Wy$b3uSz*GJQ7cXp1$MN<#=PR@LX6B7jblj)+b* -YZ;*KXE?sJL{RFZl7-9=@y<-W+u^LOo{tG*EQ>9J@KXFbS0OO{eDA{<~*osopq3iyz~{dCBAmet8)(6whxVoaH_E~Y=6h4frR%$X`IgXg-YXr>spLF%D?EVkFQ{QV~D8E1 -}k(@(3OTr(@2clC7>mOZ18rYEhX*jbiwTd&`F#<``IL(8G34ClWT(iqtcw`O1WFAd4=uqT#g@~WX -uZ(vclu9=bV`BBzqoXdSoWSN5PiOn;2_?oTC>x@zvV!+l+2KH2dIh}s^8O5M@(hT6ihFr1n!Ygmo|$u>!6ZU$2+YI&yMJy)IufZr0JSxznU;x#igqy3r@M<;(LCeC|)lvqgU(w2T+Gy6}76sbKJxKN3h)8>xgL!LA^_b -<%gX%xFQEBNlbmhxzVy(GhE!HN){i5kDgTup=*!cH&#l%cNWbteGK)GB9W#N7x -k$^FKVo`=;a*94EJqT96r{WtXj~OdHm<32aQ|yMhm2{y+jdc(=n!px^E9+&08nksv&SzriXizf4fdj>m=#@tq -o-d*ho-fRu1;I~;$Su)P#Rfn-rnC|ylC)GSqp0<%NE|f0QY$N% -;nvt5PzVgfsGm7i$08w&(?U1bBGJeqP1VU6Dr}p`hABwSY64|H0~E4)WI4V@6RX@z3iSr${gj9vyZFh -xz^jnz%=sN@tN-Ou_wd%w6d9DIwpUQqJk%$N*}Oa+u%C9bS0DTE#i&t5Mw?wcRUSMkAwrJk(0zVJEwb -o9AX^Jn$1H!*)^M)D4D*)%NsSz$mx})51 -ToPPNXe-_B={WGoH3E5M(fzT$S96k$Yxjv$Pz6oGyWh$E-od$zPRXK$_DB@w&Wsu93v~abDc3J)C&Xf -6emNMGPl4{MMe8=FDJ?w-4BP2d5{~Gwx}gCOEria5EK|aDEPR-nWF8uD5BEy2sh^-m|%&rL0#=nbK4k -vjhGL!D$d@a&Su%=X2}I@Hu56c5-@>Gi{lBZ2jS$?d^G#sTn&Q&c ->2Vxg~*->1!p4Ke6xWvPOh-LG84Sm}GyNhOOK0MVu#}bxJ;WgV#YnJQR3@4_UHKVFGu6gA0B2H -#@dQJ5Y%*;NRB`8e05YaU!UW^Nu&gddW*WkP!Y1t0Ga-L*vN#)IstBL77^vM4?XGO*jXT?27VdlACGw -YckSa(?w=PMf*e&y9(j+fzS1@c;Rnq%82T($?-%Q|O&NX$+5Xx@Fz+~=E7rkux4EsvznW{UZ)#o^H>f>uQ?Vp2Te*ZJ5xw~*n6|Q>IZBT4UbAU -4CxYex)kM5Id_3olXQTW0^dy-a%1e8hH)cVmXPnLNS$o;No3*wr^_8=~JmfK*-Skq^L1}*v2gH_!b0? -AYH7{;N_c+LqvtH`S)rDtyE40?c#8l>#h>vt -{EnrbHZt+lsQX5^O1U;b@K2rU@@O|Uj{(~*TrP6*Tc^Wb#_>1y*yxFZ$pmrw!|qfu``?BhJeSMN~zho -A$_wIl;&91Sg_WwBffhZ-73;vj!Z2lq3)N_x!JUvtX)axHd~pG*%O{^tJ$6%shmfg)u@fnl~Y~XBidU -|)vbK#_54nTTCgK0Yffi4E_h8<7aef!>#<)MiqJjV2Xl&RhFVjRoVzwg+J{*ragEN|0l4NEl?DD9*5* -gch;U~(GohIp&Q{0F4-NEt0&ni{Ox@}~Mt)+>olG{OVfIGfW^X#@c#Pp?jxj!FY2!wWk+}K+sUK&sV2 -+8bnM_W*ozJOiL(Ft62IIR@W;HicvQf#|ZgJ(wutoX!M;(VCQ@E#qbJwPsS{6 -xW&~crgc9bC#`uRWz(Qt2JdYiZ>^QCz)QH{oiHyoVVv0b*6L5#;nQ-z%~7$*~(=*ZOulfvw6#!g(`%0zM$7!rB4OFcTQ)P+M7)*MLlCc-);RaP9EG`?t>;!zCa=rZ+?}CL3rMax1_8v@G`&B -a51cbgf@&OFoMWX*EK)T*YDyWuIc*%g=`+oIsinF(!c4hNLYNjsGH>}w9`J}1L@+XLwKYCy-ZP_x_l+BzGZlQ -<`9&!khV*MEt?@BL{PJ?q`~e)P}pKmGB$-}+;MU%mhO4s+lW41^F!fCO;iYf5Gn_EdAvra<96(x* -0xm+kYKlg;qSOj~4R&HB~x(=|44)^W`MRrk1VgWfSYpPz*aDyBsxL_S)B+^f!(z)BH7EfCE}N)H_M=dO$)=X#|;aw(sYuc%E?aQ9T8dNnrtZvo)VcxZz5?PpQS$Z!A&952Q9WP(b_O3ZeK3kWL>@H_Cm!n6^fOcmnPMpGBj>@i?7E|G6e`ayO<)rs -=I(s>Iyk=w5CgyVbdpYd9Na1o^_T6&Sa5;~(%z3ZT0LQqOW8BNB>*XZnnqJ!xpXIQ@a%_7!H@F=ATh7 -_7ITN$&3M>=m&#je`}8a<|Gy!bMiMQ8zG`YUDKgDBD!puubD$T` -Fr1?z?j_}ld7fPTu!Q7*SNquw-9lRZ8?yzxcHg{@U&_N@|GiH>&*PaN!{h7?sBAdIVHKA9$a%?$V_25 -G`k#{UCzrc$7Pp;o6GsUW$C?Uraou)mm{ysfz>tVat4H*Rb9@iE@xGjv#QHk)#a?}a@cY?BC{NVSq^` -!S%h0}-x-PttC;01&~mn7!Nx^1mTA~BD_DlC*5jZ=vXG_JauBavE_tQMeorTyx;aIhJLge%T3Jv#T^yET{6;a}?5vyyfuPvJJXyCM<5fp -xy!}%c5`1;>XNwIa9Zssawv}EobVM0~^aZgF8c!fZ7(?SsY?nk1uE0mSb$&F}Cge*>>t|I~BB@O4xS# -xBZ!o4>!Jew@qla<6_%!vF)(YWi7ECX4y`5Y`ekRe%yYWc~Q>qEMm9(FBvQu;H2%`*LLn}JNC65``V6 -uZ706A6JOgIrEQmb+xgvZlg*Z5OR=TcQrsbNXSgNVl7u87Nk|fsgd`zJND?j*QiK#aZQi>nnG+}5^6A -Q9kM!wzp4VsO@cNw`QV!*sC2_fCxn@aGQrvNIXE-CtB&m6kuBB_~TDq35rEBSCp)>in%e7?f$2&sq43 -Bh2x>@y2eH@98#K&zO>5g6@K+qSJq_-$JYZ_D6q4YjS8wpG%8J*Wb -ARAeKdZOh=!P+-`#-cE$z+>AnhYg^&U778e! -*0Qc+W_D6a$|yR$9voH-gdk8D8*dxN+lKMB?Y8Z@ZhLRr9@M69@3yU)ZKGw|KiPIow%v(slV -Ahw&8Rnj-oSaY=FLPmMA-On6SGZY?hFNidFwXQ+NKkG2A^pz6Ww-Bw>{HsYjoS}-1c?0&CYGdW!voBH -aqVO)xV9-ZKHGB=-f6sw~fwiqjTHh+%_?{P0WpDH*Mb_W`oylTXNfxyfak)d}{NmZGUky_Um42dfSfPwxf5w%0djxX5O} -$w|V!rnYZoZZTooJKHj#Ex9#I?^1bceZM%2d?%gKj+m_w7Ww&kFz1zrU+o{`lY}<<4w&FGc-uA|}y|H -a??3zlk`L%6yZF^kX9@n0IVYPK(&%eo=Hy)0RYF*xA}PwKj3wwt2R#tZgf6+r--Qhc?qr?PT2w6;8r*E_0-=|F`Oa|GBzEs`=(caP?Hldcv7n>dzR`{;hPh1v6qBOo!;;PCdNLG7bNVV@|0S_dvU9WT -*=&0@+n&v~XS40pZ2L3Y{>-*Nv+c_4*_FAb`Ph}&_F}fZm~Ag++i=*kS^qXwCZ6oOY+EcFzHi$id$v1 -B?zY<@!~4_rJhuIfZEIuO)!4{=+pO3&E4IywZL?zAtk||AcFx{{>OoT5Zp6Dyp|_ogowMI$Ri>41+Y# -F)#GaikQoLtp3m0#D5ZfNawg<88L2TO(cZO@;mRNit_zk5uP2S9S!`}^TH=Etyb@Rf_0ypy8m~PKOrJ -Zxfb4vn>Uu*=i>BFW2+k$mloNi0fZKbnk8a}h_nZcT|ZZp<9L$Nf|$C-G|Xg`DCj1?Nro`y4lE!?&k+ -Y@6GR1-TBA!_E%P@KF9Xv#*Mp!AK>_lwf!cY+tE_GZ)@J#X~9sq^N_ -8zgUzygBk_#+wvxNW6LQW}xpj3%=%Vfba&w8whUuid=%ngXfmZ3ykAu^YT@@Vde42CtjBZnV15>PD-ZiEbvkndoMsn~C -n4sR$I^P;^7l4MjH;-B5Hx(Ot8OIrD~e;sT6rFuK9$2BRB{-WduEGP=p=CZn5-ZZf*b=q96^jBYZz$> -;{78;ou+y20qKnTpgD#uUNagK!|Dn~ZKUy2l6 -x~pCL($#yr65`b7~NoWgV81L#xk#0u18R=%Eo00B0A2yK;N;fFopmc-M%}F;WeYZjB -2BjO6ZbrHp>1L#xk#0u18R=%E8;@>0y6NbqqnnQIo>32vZalj2=*FWPk8V7==L<&m>%XKE9gK7{(#=R -WBHf5|BhouV(Sb+zYis|^e@Q1g80luDn~`otx*6$aq??iM7m=zwM0#f^MDXawqZ^NIJi77d#-kgLZal -j2=$`NSnCTTG-H3D}(v3*(48=)LVb9IYrNjvy-FS49(M?8oPtA)HFuLbUHTG<#PYN4Rn~`4A<$%%+N; -fFopmc-M4N5mC-Jo>O*JRAJkCtv)x@qY?v%H<}z?kj}nQmITY3Zh=o0e``x@qaArJI)S`77(`@Q~@AK -TX#48TEWn*uc{bPd7Z>@N~n|J>Ot4eI!BM^EDRJTVm9WQ8z~281=qG5yVk9N8KECW7LgNH%8rOj!nh9 -E1dv3^T=~E^M(Losdt7V2&QhBx?$>usT-qijCxJC%~Cf@-57Oa)J;$~LEQv(pHUHnx)JK0FHe~69;0r -Ix-sgW?@5>;fTeDNx(VtgsGFc}g1XPB2twTmbtBYGP&Yx{^Q8nch+x!>QTLh3o2n2+XUcU-a~?u(bIp -)*=JJzxW-2ygW(2~j*DM9l>NV>DzPjfN0cK?YSvO?e&zTZa)=gPAW!;o@Q`SvcH)Y+FbyL<&SvO?eka -a`W4Ous2-Os@^%DO4*e$Jo1Xu+s;qt=aDH)`Feb)(h|S+7~;lh$j7QQW%cZ{0U<-Mn@4*3DZtZ{56g^ -VZE<_jv0Jzr1zx){R>?Zr$_u>927ay6)$c(iG4XiztGw8@g`jx}ocS&ixA@=(?fnX0DsL?&qwRdFxeE -rUohKx}ocat{b{;=(?fnhOQgBZs@w9>z==Yo;4eIy)zU+SAz^yUg4r6f|%=ON2&3{U_q+8N0>9%xRx-H#yrR_>viY>*KVoR~5fN59Fy@zxmT}T(w#k?V1NE -gzDbm>YdMM{xUq!j+3B2a6f)1~Px{|J>vl6T+v~szckIawK)|6qpUQ5@~wRG)zEeTu0U -cp;~w_Y_B&@OzWJ<=X&%^ZTa25$}C8uLvHrkMd<1G)xu4d@!sHK1#}%|)J)Jtcd(ob$c`T?4uXbPebl -&^4fIK-Yk-0bOIzl`I8x4d@yyHJEEK*LT5NgSQ564c;24HBf7y)gFhh=j9OESSiUIV-ac)c?eCv$qY=H3MhehvH@_%-lr;Mc&ffnNi^27V -3v8rNLJ!YA-+TvIG1i8#TpfnNi^27V3v8u&HvYv9+wuYq3!zXpB1GZZ5DHSlYY*C4M!UW2>_c@6R!{n~ -UW2>_c@6R!TqVr-W6d3khG``DHSlZT*TAoVUxU8 -h843sd8u&HvYpmBf8PmYYMqq&00I>mL1H=Z14Go -QO#0H2B5PN4RIzViI*Z{EsVgtkmhz$@MAT~g3fY<=B0b&Eh28fOMV}KyB0b&Eh28az18z447Y=GDRu> -oQO#0H2B5E~#iKx}~6J3}K0hz$@MAT~g3fY<=B0b;M3{DlO84Dc6xFBn|lq(DG-hGGKLg!N5WE`*7Fn -3jhLbC|q_DOi|&g*jIk*~5?=GvG}Rw5M~Jdc?7@XC#?t6cb1`kZd5?m_Oa9d3bSwWCO_tk_{vqNcPT9 -I562@vVmj+$p(@QBpXOJkZd5?K(c{k1IY%G4I~>#_Rdfw;MkzCL1Tl)28|6G8#Fd(Y|z-Cu|Z>l#s-Z -I8XGkBUEtWju|Z>l#s-ZI8XGh=Xl&5fps_(?1H{Js#Q_Bg8XGkB&TxJf4j?u_Y~a_xuYq5$8G#sVFxX -(Q!C+&)@u()PxEO;C1{(}E@N3}Lz^{Q{1HT4&4e}b~^_stXoVLthgTV%ajWO6@u)$!1!3Kj31{(}E7; -G@uz^~Ut1A`3)8w@sLey790t#^i^gSQ51jhN_|hZh~ZHF#^F)Sc-)kTvF1cTm;%=F~_XU_yed0Pz|6OKsA7B0M!7h0aWh{MF&w0q8dash-whk0IC60Bj+ -AOA5aaT8bCFGY5>)kuLljH8bmdSY7o_{=HUefRt>5eIk##4SyL*41D*!V44fDc@y<|mP^*};$u-3%L{ -Lo7jzAoNB?2-8S_ongoFFXEW4^A8S>7265zFGR&&$ -ssU63s7B3%lc)w!4Wb%EHHc~u)c~s3j0jXUsA^EvpsGPtgQ^BqjhZoqs0L9Dq8dc?n&G5gFVcw)sv1- -^sA^EvpsGPtgQ^Bqy)zUTST(R}VAa5?fmH*m22~BJ8dUXq6^*z+tASPntwzlkI=>1{U;x$ttWom~FY{ -2MgR;haFXAs^V(311Dq8hkbQYVg&VF -K`xPcZPxmum)faz#8-Q%rl#$tU+0WvIb?1Ifyz_3ep;+H6Uw1);mKHLs^5eMo%{LX~9BSgR%x-jrr@n -iF3#rkToD{K-TD4r%=|QtU+0Wvc@RuG!4oclr<=8P}YE~0a*jG24oG$8j$tQ&_FXy3bh7m4b&Q_HBf7 -y)&Q*0Q@7#-tp-{Rv>IqN&P+wvYOvK9Tb+i=R)ej^d!zAxG^2)&Q -&lSc9zwTMeKZKsA7B0M!7h0aOF121|`IQ;{=Yk*MTgYQWThsd46B=jP^ef`z39OAVG9EHyA{VAQ~Flu1bz^H*y1EU5;4T2g3H3(`D)F7yLhN1(Z20{&j8U!^6YQWQgrvXm`o(4A!ZW`P)& -ODu-iX>ilvrol~vmIf#dMjDJX -@Mz%Cz@tG&gN_Crjpx`G9*y~)Ha&PS(qN?VoMQtD1e69S4Nw}OG(c&9(g3BwN#i+F5l|Y8G#F_x(qN> -)NQ05a80icZj5P3Q;L)I?K}Um*1|7XK6cczf@Mt{eHYc%|AfrJ>gNz0l4JH~)G?-{G(O{y%MDGm61QZ -P@8c;N#Xh6|`q5(w%ipFzFKuF-wz@dRd1BV68YnbKXv~*F&Jc-11BV6)b|gEJnTjMCO){EfG|6 -a^(IlfuMw5&t8BH>pWHiBOg3$z{2}To)zDqKiWHiZWlF=li2}N^`-kZNA8BOcbG{H$QnqV}+XoAtau2 -(J3e@ZMm@Mz-E#G{EvlZ+-AO){EHH0NJ)FiA`>nqV}+=zWKKekq;kAfrh}6N)AjO(>dBG@)oR(PW~DL -lcK44ow_-XZV_l-E4%+{o((kPS4n~@cG#P0!(qyE`NRyE!BTYt{h%^ytBGNlU(ZNWQktUN%v&oz<^s0G -AAp%Mhl;(Vkmsxt!(xjzHOB0sfwcPU=km*slX>!x#ra9l!MRgLM=6nMH-${a+1T_h264WH9?-HRVJxz -F;@HF9R!qbGO2~QKACO1uFn#eSfX(H1^ra5Gq$TX2@BGW{siA)ojCNfQAn#eS1Y0}c9rAbRKtCK{gIb -@p1G?D3L#c~1Agr^Bl6P_kK&H0WPkd+132GA5TvOIl%u}ROk -dwcN;ikE!#|Ed#O_Q4@H_iE*=9!x#rpZl{n+1U*EOt{xND2)Fh}$P;*X5CPGbwng}%sYVJ|>njfVTCm1y`YGTyHsEJV%qb5d8jCyA%FgR -**)a0m%Q4^fyp5mLVbt0BKX-1YaSk8zx9qM#f`lmZXF){6Ln%*?CX;NT_V&(sEJUMpe8|0f|>+132GA5B&bPHb -N*Qmi~hu@iBS`y-WiUg*8Eapfq_vIqb5d8jG7oVF=}GeB&bPHlb|L+O@ex7C?@D>!qbGO2~QKACOpmg -%4zVB1T_h264WH9$xV}+CO5q^6b{@pxoL9KUapG?8f{(?q6;OcR;rkZH!0!#wibgK(g -wNlTNKCM```nzS@&Y0}c9rAbSZmS)VD67vmxb9pg=OcR+VGEHQf$TVYCEOR+=!A+B!CO1uFnnR|EOcR -+VGEHQf$TX2@BGW{siA)ojCNfQAn#eSfX(H1cGEHQf$TX2@BGW{siA)ojCNfQAn#eSfY0}c9rAbSZlV -(om07?^-CMZo%nxHg6X@b%Or3p$ClqM)mP@0T15osdQM5K3yf`E}GBTYt{cr@{7;?cyTiAS$#gp4#9X -)@A8q=`rqk=_}K1V);SG#P0!(qyE`NRyE!BTYt{j5HZ(GSWn(iAXc&i_&IZ21=8WCL>Konv66VY3AIQ -&H;}m9!)%&bTsK`($UQ69LQ*r(IlfuMw5&t8BH>JO-F-A6OSeyO)#2ZG{IOOuvny*iBfohwf3VN`7yny6k3|JG-+wl(xjzHOOuu+EzS93{V6JBn)7e-DYR?K44x)Dy)zU+^fcjV!qbG -O2~YForak}V!UBk!CO1uPn%p$GX>!xMmQ@8eO>UapG`VSV(?q5@|FGMP=iD^8X>!v^J>@V@_WJ6o`JHsHcO@1(+%$V?VQyILaMNqQ{% -a~l1kuxkr^!u|nUapG`VSV)8wYfO_Q4@H%)Gu+%(VhHsCb5X(H1^rin}wnImR=!kLRquCF)d!V-U%)a0 -niQ4^ykMoo;G7&S3!V${T_iBS`yCPqz+nsdrDIcm6RRdxO{|(& -HK}S+)ugIPRgx7f~&sT0pgcY5~;(ss&Vwr4~jlj9PQbv^Z*U)Z(Z`P>Y}zK`nw>1hoii5!526MNo^N7C|k7S_J -jZa7i-%7Gud_NjFmVlw?c1=f5Q05^ssO4{vF=%WcW_++j!(l7u87Nk|fsgd`zJND}TarAR4Kij*Ry$h -pImBqd2nQj(ISBq>Qsl9FT|qNFG(N{W)ArEnssz-e*Q;-rryro~N*n-(`MZd%;5kZB>)LZ)SE -UPhrZ3KcS~A=9FzMN5m87A-AWTC}uiY0=W6rA14NmKG?j`RC}&tcsQvEWI-n1hllSnRYzqM~OuOGA(3 -U^M&muW5rF2n-(`MZd%;5xapmtaNwqeOpBHlC@oN0R+VL@SfI2(X@SxLr3Fe0lolr~PI_l396)J-(gL -LgN(+=0C@oN0ptL|~fzkq{1xgE)7AGx4dS@sc7-=!mVx+}L>zehfY6YbQN(+=0C@oN0ptL|~T~m;b3) -VHoGUtk4=o;AJhXUd@zCO-#Y2mS77r~PS~#?DXyMSpp@l;Whu#?q0v=jCw0LOo(Bh$mLJNf!2`v&@ -{Ilj?aWaQ52wD)dAn2W;NC2P(Kns8t04)Go{PTL1rf8TR4uTd0EeKi=v><5B7vVp?=U-91^A!~^v|wn -#(1M`_Lkor$3oRB}EVNi?vCzVxg+U9076!dD6c8M=IB0Ru;-JMr3xgI0Eeu*~>Smr-JYdiwptZ-$?Zy -=cEe?A1>+SSXUfRp?BC(Ewg%%4f7FsN{SZJ}(ntyn+U}(Y6f}sUNi-Q&iEeu*1v@mF4(88dFK?{Qx1} -zL)d-@0jS_rfdXd%!-pfz9B-19s_1A`U@Eeu-o&tAjA!k~pgYfm4+L5qVH2Q3a-9JDxSVbH>$g+U907 -6vU0S{Sr2XkpOW(?@X7;-JMri-Q&iEe={3v@mF4(88dFK?{Qx1}y?pbfWh35mxeL^<5T&W!+R3A7wsK -rV{mfGSPrR3xgI0Eeu*1v@mGRceED=Ee={7v=C?^&?2BkK#PDD0WAVr1hfcf5zr!_ML>&yzAFS;^z-U -TED{iC5zvC41wRXZ7W^#uS@XyIJy%-uBH8p22(%DrA<#mgg+Pmd76B~+S_HHRXc5pNpfv*8bL%<^0(x -gC91v(B&_bYvK#PDD0WAVr1hfcf5zr!_ML>&y76Gl8J_3Oj0xbku2(%Dr5zr!_ML>&y76B~+S_HHRXc -5pNphZCM3`GJ0Ed*K!v=C?!(1M=@KMQ^q{4Drc@U!4&anItO#Xau~MFRLN@LAlmxMy+C;-1Ali+dLLE -bdv{v$$t*&zi3l*1f$e`dRd|=x5Q-f}aIH3w{>-EcjXQv*2gJ&w`%?KMQ`=eB*D?&!V42KZ|}A{4Drc -@U!4&!Ow!91wRXZ7W^#uS@5&q=bfP-pr1uQi+&dTEcjXQv*2gJ&w`%?KMQ^q{4DNS+_UCv=rwZF^Ryx -XfffQS0$K#L2xt+|BA`V;i+~mZEdp8uvZhav%i76L5EaX|pvyf*Y&#Doto~IBF;Ip`AanItOMLUaj7VRwBS+uigXVK20okcr~cHVa=2ta3n&YFLRyr+~z -0_`l?S&XyrW?{raghc`i+!d~?`3BN!=4}|R=vwiuqFBX@3W*ftD5_ClqL@VShe82m^<6aua^?<&!>q< -;-7z!qnQzaudgh%omz(*Kxk1f@V8++!5SS-`S@WdX|qmIW+pj(qma9I$0!%fgn0Eel%~wk&E{)Uv2$QOly1MJWI9YJA;AEXCAUy<}EI3(kvfyOF$%2ywCu{x=e%d^td{>CF5M>d{B9sLu3 -r^Pj)9%wb5XyQ^y4R1A2?Rn}gtDGP4~Vi5Wf96Eltn0uP!^#qLRo~e2xSq7oF5Lxpte@_QXBa22BjVu~jG_qi1J!j?sB8x#5gDeJF46+zx%|G5fYX%TmAhJ -MYfye@p^_+J_1Bbjb6b>M=KxBi+OHbc4vT0<~$fl7^Bb!Dxjcgj(G_ql2!^n3{Bb!DxjqJC{>yqS>aX_POm!+m*HyAw@{xC})Gn29XUS8$>pUY -!KNXvO#2n$Oe%OA{#_Dh-?no9I`p&oncB+l9VJRNl8+Ylq4ldNm4FSQnVB;MN83Am^n0vY!KNXvO#2n -$Oe%OA{#_Dh-?tq46+$yBgjUOjUXFA-Wfh6c}nt>9~( -S2c)T-I_2jY1V?)P=jtw0fIyQ7{=-AA$nPW4@W{%Aqn>jXfyfakw(6OOoL&t`W4ILXgHgs(0*vzq+V> -8EQj?EmKIrjWRa}6CEIyQ7{=-AM)p<_eGhK>y#n>jXfZ06X^v6*8t$Ht9!hN_-7Hg9a+*u1fMWAn!5j -g1={H#Tl;+}OCW0a=^2w)w}q=8er8n>RLZ?AJle=lpoQNT>RlV>8E|Gngic4IUdjHh8=!^a -IyQ7{=-AM)p<^@0W{%Aqn>jXfZ06X^@y<{XK*xrT4ILXgHgs(0*wC?|V>8EQj?EmKIW}`_=GgOvp$#1 -yIyQ7{=-AM)d1Ld&=8cUT8#gv?Y~0wmv2kPL#-1+>ZPeJ-;*A;`H8yH&)Yz!85o069MvRRZ8!fhVyA1^juY`oa}^@en+pDs3CY`WNVvFT#d#hxz=ZM@icvGHQ##m0+`7n?3NU -2MA8bg}7T)5Qjh4Hg?L_IzPzlf{OL4HX+IHdAb-*i5mRVl%~Nip><8DK=AVrr1oe=LexKU8d}*if;VVl%~Nip><8DK=AVrr1oet)bhpt1X|}66nr -Q{hKkj2|`Z`W{S-en<+L^Y^K;uv6*5s#b%1l6q_kFQ+(G@v7ur^#fFN_6q_kFPHddmII(eJ0SgsOlkNL&S!N4G|k6HbiWQ*buQHVnf7+hz$`NA~r3zpu_0nZ#5+S(4-p$8HbiWQ*buQHVnf7+hz$`NA~r;9h}aOZA!0+scg+u*A2vU1 -e%So5`C;?J=7-G>n;$kmY<}4Mu=!#0!yZ3ue%So5`C;?J=7-G>n;$kmY<}4Mu=!#0!{&$051SwM_+j( -I=7-G>n;$kmY<}4Mu=!#0!{&$051Su0KWu*3{IJIln;JGXY--rlu&H5F!={E!4VxM^HEe3w)Uc^xQ^S -UaJ%3x?)Uc^xQ^Tf)O%0nGHZ^Q&*wnD8VN=7VhD{Bd8a6cS`5MHghD{Bd8a6d-YS`4UsbN#YriM)on; -JGXY--rlu%Tf?!*@*$n;JGXY--rlu&H5F!={E!4VxM^HEe3w)Uc^xL&Khfl1&Yp8a6d-YS`4UsbN#Yr -iM)on;JGXY--rlu&H4~!=8hZO%0nGHZ^Q&*wnD8VFSYkh7AlG7&b6$VA#O0QDLLPMum5Vs-6`#D{NNS -tgu;Ov%+SD%?g_pHY;pa*r>2kVWYyHuj6b?*qE>}VPnF^gpCOs6E-GnOxT#PF<}G3280a=8xZz<9cL5 -5CWK80n-Df3Y(m(CunA!k!X|_b2pbSKAZ$R`fUxK5IGYeQA#6g}gs=%=6T&8hO$eJ1HX&?4*nqGBVS~ -6vZ9Ndww4fnCTfp|L8)v09lfRh(5W*&eO$ZwhHXv+3*nqIvVD}94aIoQE!@-7w4F?+zHXLj?*l@6kU= -zV6f=vXQ2sRPyo`IeQHVteV*dVY$U~|CcfXxA$12zY24%i&9Ibd_ZMu6Qj&@;ehfXx7#0X70`1lS0$5 -nv<0Mu3d~8v!-~Yy{W{uzLo22G|U+8DKNOMu1KKn*KHYYx>vpujyaYzovgp|C;`F&p?m=8vix^Yx>vp -ujyaYzovgp|C;_a{cHNy^snh()4x75(Br?xe+~W`{5AM%@Ymq4!C!;F27e9y8vHf*Yw*|Lug?thP -5PSjHR)^8*QBpWUq5G{Cw)!&n)Eg4Ytq-GuSs8%z9xN5`kM4L>1)#0q_0U|KWCsPeNFnB^fl>g($}P~ -Nnew`CVfr%n)Eg4Ytq-GuSs9eF~lZ)P5PSjHR)^8*QBpWUz5HjeNFnB^fl>g($}P~Nnbx_peKDz`kM4 -L>1)#0q_0U|L%xQ54fz`KHRNl^*O0FnUq5G{hkOnB8uB&dYslApulZi{z2dWY&As&}Z~p?Zhv9jbSz-hp}t>K&+epx%LccZMNJNRpDIBq>Qsl9Hq(DM`9WN -|92O6eUGTVW2-$?@+x%^$yiLRPRu|L-h{TJ5=vby+id5)H_h`K)nO?4%E9dJdzwqjwDBtBgv8ENOB}O -k{lO#O7WE9DaBKYrxeerl&2(5NuHA2Rez}7p?Zhv9jbSz-l2Mj>K&+epx%Lc2kITD_wG==L-h{TJ5=v -by+id5)jL%0P`yL-4%ItQ??Al+^$yfKQ18xA)ldWY&As&}Z~p -?Zhv9jbSz-l2L2>K&+epx%Lc2kN~$K<@y(1N08iJ3#LMy~FYj%R4OZpuB_f4$3TG2X#=2jd-#cQD?;cn9Mhigzg9p?HVl9g24--l2Gh; -@ugldW?54-obbW;~k85Fy6sHL-7v9I~4EC-$fsccQD -?;cn9MhjCU~J!FUJb9g24--l2Gh;vI^2DBhuXXZ}I#gYgc=I~ea^yo2!$#yc4AV7x=|4#hhZ?@+u$@e -aj16z{|g^ce48yo2!$#yc4AV7!Cz4#qna?@+u$@eaj16z@>HL-9_`K#%bb#yc4AV7!Cz4#qne?_j(`@ -eaj16z@>HL-7v9I~4E44D=Z9V7!Cz4#qne?_j)x@eam26z@>HL-7v9I~4CwyhHI$%s`Lv4#qne?_j)x -@eam281G=bL-7v9I~4CwyhHI0#XA)5A -&@eam281G=bgYgc=I~ea^yhHI0#XA)5P`pF&4#hhZ@8k^h81G=bgYgc=I~ea^yo2!$#yb@6P`pF&4#h -hZ?@+u$@lMV_kMR!1I~ea^yo2!$#yc4AV7x=|4#hhZ?@+u$@eaj16z}8=^ce48yo2!$#yc4AV7!Cz4# -qna?@+u$@eaj16z@>HL-Ef1YZC|K9gKG{-obbW;~k85Fy6sHL-9_{K#%bb#yc4AV7!Cz4#qne?_j(`@eaj -16z@>HL-7v9I~4EK4D=Z9V7!Cz4#qne?_j)x@eam26z@>HL-7v9I~4CwyhHKM{FU9ocn9PCKONh#3By -1D1i}2P5GZo!NQ1gjU?&z5?mw~jlWSpN;KX=hJTaaaPmCwVlj2G7qqu@6aWAK2mly|Wk?0H#^f0T004^y001-q003}la4%nWWo~3|axZdaadl;L -baO9oVPk7yXJvCQV`yP=WMyf&e5WM?WEb5B`IBML72edEfL00V4h`%7 -&C~`7rX=M|oNQ0yjD8If#f2D04;8iDSxjUShU2VI4`Jw*cel(@=C>cz%0rkhTJmR=(Bj-ObW9a}Ob`3o4xpR#wk*fBy?Rco0GFO7Eiv^!(>9{{Ggv4 -?nWS$HmJ-Ru$HUL|b6+BLXp>#37mQSZx_&w#j?9#O0mr6Jc*%8u<57Y8MY(ca2+?TziYtN#!onh5)Y= -2C5}tmvA+(33YbZ~WHiqkdSUF*W5vljTCX13ZlIBy{o7SkzahkYy7>wJsnh -u=Z>#ck=Llx}-5XFr|CeY@udeua(SEJeE0Y$0Qu>Ldlt&@Cu|GQs{;~RYn&rcvV`E)O*-1yvGXwA@mtzL~EwnaapS_5RgmXM~iQ><$~hrBYl -t4G@dRVZ;=b9af<#OkYu$}62MSph4u}I17I>plW(Yq`=jw_@fg?-M~mqo%V;=DDW-XxE=I|69H%s2rt -?`gh=?-glf}WzOD3N!E&*8?I9ae4Qn`clN3_?0pekr9o)~S;Sz@9{;1}67f7L@}^k%OYC9j6wk}OFZ- -=$|OF*}&PH{AuwYk#{K`aJ5=zda-g{X?Mz8On#U1oTiDW4qKh&WF@VjM|$|z241r@1L6<(K18iy@v+V -yZ^aF+O1Y;O+`_)&bPuk_B(6V(2xFwpJ-AmFZ+$>1g9DM`Tq1+gY6>q#(9idCGxG*d@4)!Cs{a|98OL -pfH}`ynCWA|V3|CmcfTCOSVxV+Dmv41uKF!In@q5+RIAm>m2mD5Jn-vWrj2ERU4h9cX|jmMRU&+2HeIxk=-hX}Hj?Nzl7OT-Ccow!{zdCkurA5Vm3bgENtLX!7xP -C}Kw0;6mO9KQH0000802qg5NF`6B6yHDq062&M05Sjo0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&W -pgiMXkl_>WppoPbz^jQW^!e5E^v93oo}xt#gXRU-%nAJwOU(hwJ`#$k@t<&z{Rwt ->1J*>M(n5G`2C`KywZ52U0xjeo^vWID>E|Ui6@@S^OK+a|g|$EO#6eevq~%k6{LUp;Ow-fVw({l8zleEB^8{`TANU%mM9`PzI^@N+S77Xfwb6@#EV!Uq1it@&9V}#p_pZ^3SL5UO)LqHoSfE`l -;Uk^6ByU+sCiAFMhPe|9q?GPqg*jk9n(a-flnr;AdOAuE-|-;n&~W-hcUW`;_m#**<-I^Z4|)$FJ&1K -6w4=?bC}dzR$<}>6@SZGT+&@KRkc>x9yV`FaP@V;@j=#TlVnn_V)Gm$&YWpdHw2s{v4{8FTQws{`4cG -_Vv@_iK)l_azxwJIub=YzZ(o1);_DxI>-$$<<>S!nw~tT -XzIiu>|NQZvw%*1F_bXBoMSjk`|MvLwCg0imd+pub?iQbIIoCgZ{+9mdY<|auKg&-4xV_BTczSo`d;f3<{{Ez2Z7*Kg>^ -C{~`HyTcAM}SbtL=-&?fW;6Uw{AdS+uc>gya{r;oR|4i?H_tEDcfB4yF+wcDP>GuBi$@`yv{?P}2 -`u+Q#ZlC<=(@*~R*@y3K+h>oD?}nJc{6~ZQA4B|gHu>%A9LcX9-#&lw@=ZPZpL2TOWCveE{^9++cJHn2KW}4i+xDl=@}2+czwN`1fBXOFRDSZ14S -)Ej4?h1_R>Z%5|M=?5G|T5-ynL*cmIj1*eEszL+wF@Ne?7JzUO)XCmhj{D`STY)Jb(4y(*flodH$5$| -0KWlnrF)^zkl`RinZyhTR-a1-;_tu*AR@?i$-->I*D;G`|Mu&5ZT;e>?dSJDf42R -)zo%3BG<%qj{GYFTl^14je(}>|?(OIKub=&Tf3NLFt*{@zOGo+gAKmWx7c7r{{5NckKE2#tzsmLd<}I -u2+wZ^pX8R_+ot`EA(5Jus^x?sI`OD{j{^X+%-v9kye*4j9pMUz%Z~pZ8M}PeIFQ2}j|Ep&>7{(92dH -#gWrM><8AD?tkOyc(JT_f-R=Jy}|<+G3e*N1sgoHO{W$DUtqe3~<}W`7uK9C^_g&qklwTF!R-Yx0ugbY0vJ==V?jqKKTT=`yh?peD -|ZnFjbo%ydLp3Q#wAbiSW((al$4*Au5yYrG4jkcb(vvJYN2kAZAZP~ZHVfJ_C_u7n|FVAP=$l)71o$h -@y7+1A>?GM=vo_X_e^INfUq~~HV`t^)k=Y!`^^*A~`$}3~#mviMaa`|5CcE^q#wj2BTkdLvR_5H|(j- -C0)%eOQ9R^G62kmInTMfnBSVW -IC9?F%D49`yWps{9HYMHL#9djd^^*g=5Z}dH$TW;bDr|ni%vAUf8?0=u^979W6Rr&qwFE#e8{0?UF2Bym5sNNBeI>EDfWBOxBbf24n{gBE+= -C!p{&6pXRxit`1VHkyX{`N!rRE%%E8JnXxlTd%#pT@91-j%9TyF0-Fb&?1UACZVrMx7k@q_DfsbDOy; -?k^4GxADuP_%&esOnJLu1D2$4R^CXVMp7l>Op{*+$wK_K5wqZtrRxUnA#jIkEQWz_3`%oo;P8jjg3&< -;-{yMx29@3rw5JM+$t)j1xbSGo8Jzc8~obADnhOv)glg(@65cSvdH(T!Q!*SJXcEw9!EvQhvFQBkwhM -%*B}ZUUqh!=R-bAx-vXmexKFAl4yq>h(V6QjOXO#I?1kTY!^$C<(D?jWsFjeT#Fnf+l~Iv8Mkbg>15n -|+$#+{*Br})b$rlJx)Xfoz*FSC0-I_q9mg=z&ga<6eu3Kg&HVn#X-}8Kx;U}RY$)$Vx3EGyW&#ZS$i+ -P5d->&#*$0!Sr#P7NpxylHd>DGC?mEiLbHW>!FRsix(;0S55DCyCdiA%n -s%gWLLGpnOEkJ;n~ur^A5o{cxxVfw+%cBk!#N|L!ibKc$&4U-Fc=z>n>N;=VAu43;96#=gEWEWK3)%Z -e-_a059#6I)H^GwHt1M?-0Vd29Z)966lNbX^Q0Od8K#>v*IiWqh6Sh3m^XI=C^uAWhm3d(d- -NH0v$d`hSiC}e=j1v{(V#3k7zmQN(d4dmlt9(kLzDVH^O$aT^2)A_H?WaNXd8qkG*Vp#BO1 -P0p$m*AbU)2_-!STMQ+dJ2uut!a<4wQ=elap{EOEAQX?k)wYsJ$%_iOI(t+9DIM!I0l1HJaJ4-Fe5@K ->%y;tl!#mdhufR}j8)Tf2=ZTLzxk98X`HSGqE#@y(`AK=!fgQ-x67#wT|-VD=&hXu%1PYb!zTTpRlA8 -xn7uR|{0Xy_Lza)mvg$0M?EoBf;`cc)aggP~dRzJQtq~TP^9C9l^IpdtT$0f~ErewSbV!66G@f&L;@J -{nU``CY!5-sT!*y{1104q!{RNU4)o$XF{FXBVI7ugw3n2#s6gkDvD<>*1XUd4BEASbOV3dv~fg83&r_ -UwqsP&d>DA%#8A#vX{rcZ(wzJAdCLAdGQ-vJD4!pkeIWzf|TIY86c)OrTZsL;3ipq)7Az&Fo2SWg$V@~k~s4R^rI|wk|HMWaelb#(!0u1 -|{_c$ajNsz&G0v*=Wxu$`y={)D;iIg%$%hs@b98zZu%ps0D@JfeQXnS|NK^}0}OS}^xAGDX&1?ZuJ$H -WOAE>b)!1J5uve9Qm@CxY6+#6+Y~etlfb+r_d<7n$>>_h%RZ<5%7=rw1D$S7bHh@;i8>#nWKPm^gqx| -0F1{YB#4i{R0SWf%F#+7o0l58>FKkX8|uB3pX$)15bw7Kv|e)e#-Z_b+0@&-74`7*m0-R`Ib|T;@&XcXF~Ha^Wz~*};5S%qhcNShunKh^3+c#>UhbSha~2uHA=Qq;5> -cb+-#HS(Tz|`QWI+1w^o#M%?DbM1ecc4j?;;NT+dA%eZ(3+~6HIr?})Kg#kgO!w@;&KkZcee{JId}|0@f3#5%Qb*|X}S_TmOgH0MX?6E@ip7%W -WLMfa7`e_sJNLRKl9^s+6f>Xj04ZDo5i}Edmzc4qm)SMdK$|NH5&|OE`2MFwPFc=y0b|#AO*vSwlDPca -QP=EyYy4y`$lD5iP1S=ffuaXZD>`(3$MnB>_5upR*(?%G0*AH$Rl+~m|jm*NzC`W9M -EPc92oh)ii8-&J&78nDd6W<15>{{w69*ziwp01_M-fEA>(U}*nNz?(2*OMc2 -3aVuo8K(Qfz)O>@D7?Q8ZdVj{ypJKPW{OMhm&jxngwKd*u2Q;p$+VO>5l0j#xn?2xd6;<8KoQdo!}B2 -f7%M9)WknrAfMsCm{82FGw_!vOE{V@{W4?@?dHOC_0u&D=_i;sfD>7p{K{oK29xdwOBhfo0vz%+#vIJ -U+_uOz5}M=0cNcGz4kPDnmc6qoNl#9=s_G@-W?eSPFkYZolfC+rcziKR7t=@*ciw&{VPxXTsbd84G3* -B!0XCAEwPDQs6mmm!JVbjw9GRToGt+?aI-|xnqvb>Q#xqANUm7oV9}WZ5ux)-Y7%HaBqzJ~A2nr*BKe -b)Z{JU{TT(S&23C^>%OUu9~Hr55Kj>(q9;dY5rPkg5fq4R-Ev|jYN_21SWu|k)3rUls<}DJJblBN$01mO^z`#Ksfc$M%$m}c -~Xl6lyQFs@Xk&x$Q0~Fn)4QMxUNi$==ijgKm1ZA+=^XYL>EYc&FYlj$KfSv}&nS+{7NJQ*_jr5l -UOF4@S1m(lgG9w0d*4nL*B{0c -Fu76zz~ZC5@S6@%db41Q&j~>>(1DOq2D?C1Hq#4qi}rOz{#^KnLeC0rTfl>%h@9MNduJ8@OZgx8i@(b -?px{&KEFA#vamQLI-4jv60;YV$BjXz~7mWV0K~LCaUf7h@h4mKV0Gx%!I%?p#u^t_@uOpU&kG0ibe9< -F)Y@KD}>TTJMFpDByk8T!FItVP2R|O2nkDi-RawAUE&NEI748Hhbe9lFX4Dy(f~{W(2Ve9y9f1vf}WB -$Od0lbvZ4=|RS1t92hHxJZ~lw1JQ*}O3Br;!ji`D+db0V@33hoq8D7l^Gqnc7JYUPzKr-8jHs1O(YLZ`!i-RW# -KW}qEPgV>K(4nbK7-s{`c-(jWzn!nr%8Mh#JlJpKZ3DQa+;&Au&WHqSk-Rgl5|!)n!xE`fepS2m}&;# -rc2keu+)MP>2UwRA9CAeWTtx5+q-dC%v-Ll=@jEWJJEI2Xu+j-95eh*vs%zx+o1nhg)UO#L+qhJ-Te* -X+w`7bA$hUzPlnrvJdAm!1-OC3B_XRrCds<2xwL@S^beCtZN6ia5Qf1aW3@dj$JuT!gu*sbHLz=>_r1 -AHFv=AcL%E{jafY(QC8KV~V!lNGMqFwHQmK|dPM%&m;BX$q&@SP|UGas7Ic1G+LhbaE2g4{+i2H`kXb -f`p!|#JZh{q&gb5z(}fOfm%tyy_#DEa(%yVP;Ir7ZB=q_)v^As6ctOyxw?UpVF -ZgKcXP~fCzg^2-)lz+%Gt6g!)DL-jZ>EwmAC0v84WZW_+0yJ4G0XGd&o11w)2SP%H9xkj~19cNnQIDMM{!Ky5Rq)9mA9}Y)VT7)SBAzdL}$2A#2L(27TH*raNT_#)9$^+Ee2GNSfG<~cZwLB9uwwdoRv^O;C7O@ZrPc6Gq-K#k -7d6dz65)3fR0IVWX)QdIAKeGRIx6X2_w>pmKT1H30)9XO*Rm~G;h5IjP%2#1UW1}AA(ggN;p~SII|g% -iXdSex;LbR)QPWw*@b0&|2(SQ#3lWdmvf?M4fx)$#vDs5pSqJJ=>TsX25OT?*4q`<@m;3kv$Eg(M~*L -OwgdzrtZ^{#XHcG`s~c;(Vg4IwgY=BFM-`PP`2ZS*o-%>wwiJ_gIi#GJ;`Yzfqc%HI+L=JLxG$gi59cPvOV(+9U6< -tj_r^P*zR0~p;&8!;`g1V{)CiK)3U%=tppJn>>uu<6=P%vF)p6q;Mg>08Y3;VKL=Vel8qg(1ZC85EtP -)6OPcfaHD?D|QbkJWD8fYxA2CJr+KvX$dty#cNd68&jhIVB4QV@{fAb~$StCb(ps&If2(|Hl|Xur*3E -gMhl>LKao5PTjPF0x}Pw69cz$L6B<{{q8Tgq?^BiXij1o^|#r?8+PCHdN_hk9t2~T6TlQg>xNro9M0R -Hjp@sr7ZwDlGf&a-YEanKd3T&PZSB8xIW}4#deBf88!4R+dd5;5p$#pOUa^5aT0Nk+BIptF)mwO -xjJjFJ?H+W&B2{TtylToxXfT^H*v|rV` -Qf>&QoJEizcI-q^mp#kf>pM6NX_ZErFw2hh^uk2n|nM{8?fo*PBWjxO8U$uZxv0l}Dh+oJ9x(83VFs) -2<8d2{Jx`7Y(rd5I;Ld{6iiR%PB8SzK4NW(RpR9~|S<&374mmLDpZi -9Jj+2gr?u10PrE1`|o9pWNj&2$UTF*OXd<$U&?NZ{94HT~t2s&KW8Ii3+_8azpDz?0EZ!6MYHzXgjO0 -ixV8iO?p{nLF-voatV((P@Oxjw<8GXLYJm;uUCH;`jTaO%M&z(#I>qA+Ww;f6By`X^ -2TmX8Yj;_`;=jaHFP(FQKZ8YWP36@NBM_u5s*LT*e~lzqEcNBA8^mDunyCw++kF|FDOklQrYaonFJ`-(Zp`_rIE3zXp+y{ZLRfEYVBI3IL_0;#5MfOWj&>UNCBXtG? -}K%(7D2XW?|XQ6v-82B8;osvbLtUOGN)PE9WC>-SD!j&Nh?o}R0=QB;4(re;L*+ALC}ccv$Q?>>lu#TNVdy(gv;DF& -bgz?QN}~wZz$23fSHdTdAZ(ze6Er!LSTgUZ154I?%liM2urN_9_7M* -Tk1jp$i^*K;8@%_@wyNz?};v+=4n=hfp2b;^=p~r|V5uhA&Y0gM(H%!ETBrQ^?vL5V(W-Ta#i_>NSRb6_K44HYAx --ToT2Nz+~;pB(+-0e5R>Zd>sdnMCILwy4NO+ZY(j_2if2uGeq$R!fZ)Wviz2;&Z+bbzsIH#$n8N+xu} -rXZYRE7x|1+)${-^W_m;c;FJu9Dq&wxqw#Gis&&1=C@PSP(sRj=mOZ -F@HSEF(EZKUtNHo}*#uA>y=mk5AT8#{%Jy?Ih`~q2?BakW;}HgwcgOZb`r;CnBHZ -w=-?&#M{D7>AIr05p1|^692^jXy(UcpOkwaS{p5k}b&xBqv&6mPWh}1ATtJzNOJMwk-p#VZFf+m(TXu -E<7!&Swf$p`zY2~C46yf7bO$IYnJ4X~KSEfOfG8JPTFAV^TBO)y|wl{BopnF}XO#Zafm>AxXQb0b4WA -5$&S02a*#&pAvxC@8eYhy>s1ObkA+XK4S7Lu@9Bi^x@Ub_pXi_~1Rj5IXTWezyqHbEyyKZnq8*G9@7( -7n!9)f=QGeE+o08w!a#S5kU|8A3fetWYzSo2tTmWWJ*D@WA)@qI+#v*R1_nivLY^Xb61Pn2L28r7lSm -T%-z*O5j~;O6^(drT0A4y^@1%r}Y^zzePqI2Nf)Qpc*okoq#Zds2@nX+5X5hB5Jtp7u{?@}b?_O-eaPYjQ@;c{BH(;6F582qqKGr5aVrEaU1=BoP8f$p`tc@V|RLUyvyfh -<|nO5)fR%lcdty_aP^&)o%oQiVY}n8#=AtOZUd$3Yq^3#KR70%Zoqk -LUwfKLv4xpjV8rvB)

^r6mt-P%vMK69rUO);rc+CIjKjrXEwAnRX@UG5=h3}3kaH)&8z!Wzx8>fz -rQcEIZ!yP;gA~y61Cm*{=3>9KDYEJ~2fEieE&LfqPE{q$xHT?vR#P0^EUEd{i`O8hQy6wBF-N*{I9|0 -b)V%_v7yTGZq26U@cn_*iQof?^er|@TeeQ?4SM~OkA4Y8s0;a$ZHp@$0hR(hEwcd -zo5c(=}qP(NI7l6BX&ja1-U>EN?h0pH3X0|Y~Z7c0JbdM73Nyf!sQ0(Z!pPiJ{O{ZN1y6*8s_exEnhp -v~n2{vBn_6F6lMbWvR>PuAq03hAyK(S*8gxHeQ303RXD<8}KIP3{)sye7QcZb&PPYSy0Po9C;u$wX+D -$c2=fm+abjUfwnuQzqC?jJh!JE2q#y-~XI-76w?=m2$0x)OjBHf33x&Es?GKw&Q2kIn|(e!6}xS3Y& -f-QBRYj?uji3OAjo-6^KDn(Up(ad;8-0Lw`6XVzTeBKfn`{tqcn6NGiQyU5>8EMK4nwZ02Fv^`THko# -%c-c$#3SIIU$K(alkh79P{`5)te?sY0fwD^Al=oNgq%8cZi9R)P^Auge>+Q-ul0+CsMXtBn1`$=@K%i -wXU!Eqq~*Pxk^|K$xaCxaql(uQKTyLW;650ten)QGFMulZF8Dik}#@hQhC3N{sk<86Sut^##i}ubgvD#ojvjeuC?yK+}E-e2+2;c|5*S@;@VvPtatZ -XgA~`g{W`kW4hPqEsS24tNi28*b}27*buafYBamCm<)_4v8&V_a)4jD$WAW=Jy4S&=41&P|IX4S#Rg1 -zK6Z;F6%Nb}b5pXgrawA5noTp~N|*K8MT#lPgi;dh!z9J8bdk_AE&Gok<`!$U*&&Kb+0C*dmXq#lO#6gKA!r3@_uv$Lx>!{Lev)%%)-oG)Ptkt2OC84qzP6jNtWeS{P9y0YDba@e>X>Y#Et|D?_m -Du9Oa+tf58H-yk*Ic0bacWt>E2;RH7 -l$|a;;)(9H$+Rf#b4!q?5>TFTf5y*NJ?0xHoQ@@_PEka-wi -KL)xEbfKd$9UyRy*R%iRu>#B2QTJHpC^^kMu1C&9#IMR#3?SwU&x`*y^culW3QGt_~NHwDyC$-HY -|6mzI2F0_Q3A4)+C_3$wsr=9{wQc(=^4xV@<~fE6|6#Tav09CI4l0`E<{MWVn%|^1>6{>w-m{GScde+ -;8vr;!j4Xy&=St5U(ZtFhB`8&0Ch~4*7i5P0v}lgyB&=@3B_$p4epFneiEMYW43tUbGK>U}r9o?l}A@1{A~T>fm;50YOI3gKD -T;9hco#fA#KUCITF7w$-`b30CSuLH`wORBo_9w5@)(O-%poU5|9SuaLV*#U%0V$tQb#+sTpbgxvW@fh --AkT;|ZsPflx?kO5VK8|7;FGq7+-RnktYo7Tg+gQh+T;4&1pmAakm(o<9nWRjLV!4Tlj6~PJ?(y|pHSAX}29B9sD0c}W* -pPMUB>aF}*b;{_O*g(MuXUSq*Mce@qkHA>I;O{Szd#u(qSb!GtcG<~y6-s+ror7!ZC1eQ^fwp1c(VM1 -x>w^Ny9GDWj$svxoUSkJ0ozk{;{g@8$ZeI!B&x}RO>a1Psqsbky25Rps!grgc#Q#^8VNP_f -jl88H4(f-yaKHu2qAX-nlb|u|I#h7e-Dl?3A|C|%9ZGrJ=dGORUc2xEu3^>YAiA%_95wF>)vG!yNE3_ -c>}DP$=~HY)-IlEegvIg?=w7>9&(t_Y9fIaTiy-VeDh(jt9V)k_cr+qjtU7JGKmm3i9{b%c`VtGP6ul -b(t+~q|Al+fwJ7m{Xw&2+fhsGWj-Px)_dSn~~BKLXYUv#gVf<@&PQm<|=fGlmIQ?yWvy6HTs=Qt$AS5 -;^NrFUgj3|gHhcA|UTSegrpf5AR4&yE2#bo1WYnIk%NvxG`N7On%!*~E=U#Uo`d@CbZ+7 -gpia+S#3+ruu(Ono31_T)uBbd~p?mFatPD55`ry7O-ox^{pN4&d`w~RB@lO_2iw|G$zZEx^x>p(AL&s -0z2i9Z~#QSExJ8X>bZ+SH{hj0s6=YngamdH0Golq^qodl@ -Ru!&Z=w6q#TDLogv4L+vqt7}}qs|9F$W}-EF=z!bSRwA*rj%COPm8x->Rv-cBRxcl&Wvx$kE9-eON(< -4rW>cI7Ic+IRvb9Pc)8nFI1{zMWubdrZsxZL$1bjhJxovH?E=kRkR&*fk-?HFvZBTuYI-gm9R_&qce~ -&c#W`@YL)L-Rc)9VqYHh>uWg$X~Q~qwJX^g?sUFczxy3M;?_6heo*3VN}{DOys6E|xbQM2tHxiI+@pV -&1I?=QE>GqCyV*6%EIuMMHz>~)-8<1_dj+rSrj5;_v-0h_(x8#hW99Z)%Tcfzp)!9x;m9mqoWIz7X;M -9Wic53_Z7{5E0C^M#hTf%2s2-b|Db#jv|_d8p&A5!ZPu3*GDR=xPGh@KgXO&vm(jg)KHDJCmCEx}5vw -zgXoIjICr`H7pnZy4wYpxNqhVw&Bu=)yq)mj($L8M_W1>-@fWz_$RVPR!#Gu8+Qy}rDIy?UZ-c=dUT0 -ZA!K~r4TVr*3(jYjzRXQL$icgyyxof$Eox4Jgd~h=y~Q!8CZzJVFd6ZYwC`Y+yC*R6z;p!nZMq!R~%aEUYD!KUH*qhuO -n9QY|Q$AG!gqnO1nY)G(zzN=C@j9H7tJlZkK(+S6$9z4{yw5Bg~~{v@TCX*2LTzwj@bOD2B78w{feA> -!;Qmy4NXd=Hb-pw7O|p2=e6)tWA1U5!9##>j9l*-5znRlK@tgu7Wdsv%lb4PdRauq4fi}|IR+ZdB5-= -)uY5c7X`+ -2p(9%EMuL%Ebf1FYilAj= -~)509zR*hz3}KMrT3<+>m&cW%#bZ3d~mf^*Bi>^8h!zDK -W3QG_9TD0vi$-d#nJaz@K6^WzJz`0K9W~Xnnie1(!7W2g>{1<8V&h8~I+jUUCVzbfr%9(+r0L&pezqD -V*>;sJmUxFCpn_h@96YPKAm;{F#6{!HBTy@onzar2gi?kj=12zN^INss`?M;Y;9m7FF@)$sjO={W1zm -;1cr;wrZZCvzljRBgr32XKDCi@ -hQZRZ{`=PAV=et@L8p*ZjCu3r>L*R6kGYi#-l&$18`y5xlf%(#Me6HyqM?os?43pD+e1EZy^oq|}8uN -bA*VB2BF`8pAZC9L?#lQun}PY&+X&WJ;GUL56&rYZQI!$KI>&DVY>c9=bk-Jd7HZMg&MlFjw7NPne+; -;@P>W6t3|Y?iAhy11Leg@r1%?d={1bgxvDm$g^&={{Xn{?yE&nL_sv0>jYi9KjPG;sIQln@%C@MlXFe -`%4~L0f9^V(YYIT@wLW7fwVtv;ZcrtSDU2T<$iP#m8i7xLgexf0jU}AnE@9$DD(^u2E*?RgT{RZtiR` -j#GEMMiTLD&9q>8aTC?WeU-Tu*@^2jy?#ZNsJFsvM@8z$)Nc|!?O7ZFP;cC61d+qKKv{noTneximba#(9yC`D!7!3uqP!Cg~N@H3Yt;r6bj7af_Xt#14Or;5E9 -)93&noLh>QMT(Qtu&$l%*5`wmgOI8S04uuGSjL3yr_0)*DiP^gQu8(S^b01sqTBFBccYBQKDc0*%i{D -6cS_acYg=-CSwUVg4jF?27)4y8Baus;n9<=z5?jU*$N;@GkXn5$~q)FU9aPA=iM(J%T>IyYF?mH;`1)dAD%@ -nQ+e_9TCI!YaFBg*6X)G7=c+;L1<7xi?S`gnl4@9TEX#6X>Yn5pOg_i-R#KnyZm7`27}%BZgKZqdBs=n{298}CWley>^m4H3iZ1swoBZC2HhQ6dE$BoQXy9eU= -_7^kJ^7s%96O+NvUqJ!RTb%POh|Di%*FmWJdF-7dP#hNit|la~4J4?Se}Li;`|8vs0y%#dpv3b$T?cI -+qI9aM?IBs2A)0auYSg(%FCn`JF42{fceG)r-k44#jhQ?Ju+H{07h9diUIM4=w8chMgbWJ7n1MqIvSR_=OqaOuwi>1HZK5!Tn{12a-#>l#`psizs_Nh@6NLTt=Tw{&fBlHtbv*zBN`6Y^d+riNILkWLo27oA7)H>j#4 -o?2Lz}qyg`6VBt}ffXZM~WUwhFpwWZFse;|B!M2}h#monaSzdUD%LjcMTsgQB)QONRs5=FWe}Gwn3TC -j^B9Q9`YT|&Wh*mpQ1H~*a`3`QYIf(~Xf@mP1f(lnq#fs)Q9Ng>V3PN!`16aFhEmipl6tleW4p(ZDI6 --|gt}pNpK~N5e(SW=UbpL^PFOahEN@Vhs`$ -;d&eIoNa>h8u(dK~W$O<_6NfK$8&25#rtn94Kaa;T^8TR@jFZIM;xT2nw*lacjZGoAtVEaTs&j)AB1joPx3Z39QkU%dN9Cs136i|U;mKWaPN^Hf10^w+owg=*Zpg0{=4T5r9P|*nhVZfmTD -ORv8JMNR_1H~*ayu+2)3cG%T{kOsX+h8ASu;DFG$pt$!gFT1AuEIb|64Zoi@ -l}%9332JBoff;0I0TmSTJ8&3b224B-RL6n5HD)Q21H~*a`3|;%jxwMW0y$%#Jq#3df&3?s!VWgrp1*CV&DVxk26=q%2|b0MHA7P#{8!<3@}qW_jVA!OP_3;N| -3H@e;gnB{l-X5JGAJub>WK6XKVn81z6f%M0&tCALCEJ19B_RpWqY42rEmjWno-1_i>P1Q-ZgFwR@)Wm|yb3jl8Ksr#91u~1E{uc0vLFymucM4?vao)HD#Vjwp!`O8v@EuuJ783O({w=n@hthD-pFmY+O*s;&8*x?EfggtXOfOe -^Ft~nB4f9dAr`exsUt1DMCKhou`(4~G6ney7|5TpcbTXdp|ad-nF%kAcKEcFv4;-;)7;o&JhLo3=d`I -0h_9~_HegjXuG)F(Bv;h?{P`0gE~ZBmD@BWt>uAA_?GxumC5h4A%FW&z+i_Q45Fwff`<>=eZLKWnn!w -PLH#D#P&ge!ztWk1fg{^~2g=HpZ3jYjZw^W-wWkQqXT)HDXjPN9M{@z&BmnN4*9YTAZLr!4r*_Q6);r -(<)V|HLl_pI4M&3wO6GDGoLW~?2Pa8!ku+r2n73`Ej~2n`M35ANO)OPq(hpV&G?=tI;y-PRy>$yn+LA -NZr2RVcgy>6#R}VNaEzQx`mVk(!g#2izUZ#|sdl^cizR8>YSEVz0VDKrTIumfvQpCB@T6dWaW`c)EPN -MJ}wyDH?c0l4YeNfFa8Y?Q1Xxz+|vUzM&%SkH(|rW1v4AEvJJtrQvKrG0o$}a+Ivb@q*^7#e9|yBBGS -}(}V -eY)nA~zbhwML&!R5<*F%!f-xYe0qI@U|Ko6BMwo85Ed`O+dsJ;2n>)l-UKHl_*Rw*KHJv5l!{pA{Iw_ -1fYB}L^X+X?H~SJrHx9}Nt@(PXc@?AM+XoTlvOhtsA8`$g)l^BA^LWIL((RF)i0(r_|4o}5Yma~`}f) -lI=*l`J3bemRJ-kZOlzbf)KA`CE26nP6Y3Rx4A&xnt}|uXC9-wgr|2CZnMBT4Lv{*B!^IXyW0yTp0bH -e$EbG(YkEJPU!0e-JM^#zAAmC_y?^2`nDULKM*W-qet*8Y|DP#G;*y)$$tv8?DtmFJKS)+e0bOT2~bM -|1QY-O00;mWhh<1Pjr0xcLI41(i~s;L0001RX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5 -$6FK1#hGcht|a%FKYaCw!TU5_2dk*4p@uP8uRzy{iKWoATH)flsYB+DLRWJ#c847@i6MYgQQP;5<;vZ -49)8_$!}dV%o(ySkvrKIhcQkN9}wjW@GD_~0M^w|)HS@1K3XefI2=Pe1?a)9vHWzWC%{xBuq7AKX6Jz -W(lh`{L>0FAr~Cyxu-}e09G)yxsor_@582U%#k-fB*fDZysL0c=!0Uy?p&}|K{EHvflII?dOj^d-L$_ -p+5QB`@i15e*EG7X?y&3d-o5XS?~Y+{@vS`FMhcHzaIPW_~vc>^XZ4jC;w3o-oAW%T0ejJbpPVr{j2S -ppM2tf{i+vFJnM&_>Z`tcxBcRiUvBNX&U(Z@eEWynN3UOR&w20L?fL!N`=`I&zgjc-JF|Ki0v#$UVn10Vcl4f?0;b?wH}&!7D8A1=Y)XZmV;c;jQgtF^Dc)B|gxe=D= -vzPaCie0%@xkFW3Q3H9FX-=2N_FJJue>+Pe@|Fr$vN6(*s^!e9+`c1vJR=QrT{aa6Z`2L47!B<<&>gm -OscR$s@>j8iG^!X?MQlI$f<7b~e`}$9e{&&y5{`}LgzS@5G#q;f>?aPmzfBo!}KYsSn^XNN__0F -A>z~;7SKG_)UOc^ci7mZ*@y+Y|<$?7xCRmgEk1w8H{ruW?SD$|Te*5k}=P?Hl&U)(ht6%-kZU6AN_?v -&J7uw$A{R>xr|3Vw}Lf_;3`GxcS3;V1W>XGY(_Tj$&{6cp)&tBaVI)1ihJmon{y#2FZ*GB&O7wuQ?|N -6`N=~#n!Ud~;^{jVQPW!ZQoT~GlrC-J^$_V+imT@pTGXom(M===(9in_SsioKY#Y|AHRO~#pi#1{!#tUFTei&#*aS!?9)Gg -_3VFr%5;vo|NIJD^Y!E3H|C$WU)<#?|YOb3f{zW9Ogkt{r>*EM~oBUiHsx{*h& -)N531be%AY*cei>%J+Rjkdgghz#Ziww>fPH_A0BP?`dRFEvCsXkUG-qfcs;7)IdOf -hV{cbzwbr-#cv|R`9InKMB-OtrGT{&y~^-WPGg4@(s^I~x4dDm*#cm3=&li07j+t!0O^2$-KwDYJ*?e -(SYID(JV%+5CMwi?b|tM~A%8tYz9ZCT@rlke7LH)~gKjR_j&E68&T`dOcyU5gwz~d9M%mnm8|h+zqu=P2XhDEBm*JEa_Ywgiq15P%*zVlX-&3a=!H97` -_#YDY{F(kV@?;?2qXw18osx!T9tMwZ7*P2ehYH)+aJv5sd_UQZ(_2I_%)o5yod#mNcCdvi^i>cA(meDu9p20Ep6Pv)qTU<50EITa6s3){CyfM=iz<{vQwF%A5Yqf@1kJ -$!WUE!uaS08ByE3nrDYQJlG9s8}Xx{KOKHkf^@WhfKqe0H!%AX^qiPZ-%W&25m2KA7 -wT@G?dSGXNYLzhJi>XWpR}+Y<)?&kmgKu(!5B^sB&-;?c)Uy(Q*vo*n=?F2&3JEoagT+2C{;7ZCx06j -d$~wv-F(o{`vx9rDq2sI-0cr(y9G{<+*P^jLB(CD1=>b!K-6RJa>vA3zCm!PxLMnhhDBcZ%wE=3zKny{hT!xEBdk9HuTD&B5KPzpymj{p2m|uTfWnX0u%#i`(>5WKVF^ngiZc4?Eoo&>YE58jkfT;{DtsYn&hC?vX -qZYCZ#>X^bWbk!(KhHlfgd=@4!z{NdJ0vV66HWXv%e2~6yHJ+aNK*B)Xhsd=70X;pRf|+lZEX5vAun? -lc3AGg7%nW0!7=Yn5*&Q<(2W~;>}0JOaMO(=u}Itzk1Z!0Y-2re5cA7y3=S85%T#tE*MTSU&9T=CZJUB=(hw)-?-P7l{wW~Pr}^bo9c(y$t3UdJ~Czlij0F0I+G1x -&&V$2#pHb=5-VJO#gy4h+`mE^k%V>KfjSr9g>Y3-178MqY{bXyD9>r`KLDzePDaNePWPLz+V~RuuQR8 -(%1RsArI`YzQ|{&uoFo_*VYLD{x%+ax>W$8<6R}Bxl_ipxPlO|LTxHlz&;yLA5hm!Xk!ma3H1@l8fhR -B|wy-8j>cAMcZO8S5EacZl0I$$gMnsS*le?8Qnhgw+BnLcr5IrZ`bDg3#c7aI|#mn05y!9flO}!cuKMjf}=!k{HA!_9ac!bO{Q+x&lFbTx<>a8L%Sp!4o~%<>}Z5 -h2QHYNBNgP0Okr!>ZKJ$PE=HjLY4uN9rINfMq)-UnT)m2sabMUi;WEON{Xp+Yof?i`#`|by88qCVZJ- -N2ILxU|QXHRa3c4zcr9X3@fJ-+G1Wth~}07`^x0X#MrkQ|7~No4G*iBB2qDt5y%)#yIe!;#JFZW+{JH -zF+UbYjY(px2bLM_#3ptbTs8=Q}1K(>7+9?P&-dI?PwB2-1Lamcx7qsq4qBotKlh8y8yQtK -5(g$Wzc^Sh4!jvN&7CvRAK4C}?v6PFM^`$*N_T5?R=5%Y^ -%Ew+Ft;g~tPuuCuA)W<$55Xg1aK0)n5+H@X4yd;3~qO;2k8*dATFL_Kk{-n>s^$s5(66XLJ7DTF}a{3 -IEIu=WviWA^K@zuQrI9L6=G%Q0ZW5LD-Xs52~{youwsM8x{6E;LsYFD6G?YoG{O3l`YzT2bXW0^ihGc -`#tM5$VS@dhn3iAz*FNzzP;DOYZ+eL7gwYOy#)J0TRumd$wESV5}kfDJ?jrH -kNkd1_zd~s(7Yl@9cskIr#(P1eILhCs12J8SAS2o0`c)mFDz=%)YJPsl|0q^9^0nM^?ogO|WMMsMj0V -$p)8`co39WL`FG8xcB#CRMUh*a~WmJDh=HYrH&BP1R|IztYH=uG6Q4QE^VVhXmLrU90m0d12ow%@lERG -tic7js1ZS%&@-?Y3eR#s^73SVvb&JfaE2H1Qv>1y`NnQKA>9Tge9ec_X$P%L>T%pLutU%d4Ljo+t=?23?5PY;g5_acfOsAC(k2e;gqOp}<;qd(0Z-#3lwSikhL&Q{Dqj$M^bFyHi5^5SaO2TM? -z`>}xk)%4S|s@+WpIdBZMT{xLkCQvVQ9gO@|3tF&a^)8VKCcCE)vLJkX<`AeGsQ-#pyDj3DAaDnQ}}Z -6GB=fDd1|lryvLqkk}&HbgPyvEoB-voyaNxhLj#vz83ssdsVE2{JkV4xX?1sN6qRvx3i4q!K})F5n+fE -77#b%>`8UZOrn&3*}jD1pQB95^Qn7EE+;Yu0)LL%?1`*Q27xmVy$yQ-N);@eQckDZq1d3u~BN{q_CAsK;SSCpYoOJ^~S9m{(J2F>hX!8KUfoFAH5$d~v-`Zl& -M8i;~@LUBhEK~XCN(SH&P!427IX>RsL=JKgQ=D(5PML(SNwrc=RkbLdDnkIWw7$TFz>EA1MY_t{1Kvi -atU=xwJLIh4Tg&DST#8U7X-s&$q~KGcj3kS1QODInilU}wL*^y^rgaLY%kx_oU3958o=SjaQwC72wi- -(|hB1VrUQGdKhlAe;B^?ynB~us80#u+qQHj!Ok%q(Nw_`X&*`;B!5%yu}y&WcxdFK=BYxC5nuTl=uu -)j)w;ve2}xPPr~qU0~R@cw~`?J2iRJG?AX@XXMsj0b-}oJ{K!aLj;Ad6d67w%nE@_ljO$9qMkR$-%k)ZoN?hHy9IUsV+Zb1g7!=Y&?mM_~IrJymJorW9&6 -dTp2hGDFbo*`dcIyBhk0?X30DcA&{86sG@RYr)Jw$t>>STFqhBu)olj6^xWbOB%<@Ek+%+A-`=qt0e< -pc?IDh%PBJx;UJ`iRUK|JRnXe=Ets2%0LaTZ4PLVxnpBQg7_ZDYkoXZ=DMa#N!;%oQ*09;>Wb1=v*{HMAUaxdt$LZ}_ukPAG{ -IH2;T97{kV>OyPZB5w`O641!VSfQu3dk4WB3C2iw9~-@cMMh -xd@V`{oG%Y$vC=hXa-{fEiXWtFPZ*MC;8p`Brfh#z0m(o}&X+k}Gqn`AM@S|lVbiS$5eGcVu!-GGHZ% -^>2GgKUVdW|@)>q*RW#$BMMHuw5=pHqq;9xZ?GyyIr4HNbwNDzRY2Jbt-pSLa;*qu)k<0od^UwppHVM6=WQ!$sUUn -xBRsDwMHN=>&s)e{b7kh2t62~#2*RI&SqHulO3U*W3jO?Q0O}vK%z##-}aRPFziF{r?ddCJLMShTa_` -q`>>O(MkeeB;RU8Xs$JKN${gfC;}8V98tK!LNH9Y(a32X(q}C1S*3VOO7Ra-F(k!%LD256w$tdY}E~_y9#H=64BPS?H;2<1;+a)_ -WybHpMowRRD}dJ7Ja8@UA%i2ENw*NPYhZ^B0x}FSw0fyicG>{YMy{Od9JmnKM(f!LGFj%#_ -7jMkEj@8XUoamYg3e=Ou&pURf;kCYcJ)*2!sE~?f)2SUoUmIhU^Z6C*xj*9_IG6oBp;CbQpPZ|^OUNh -cBSUQ6Pttu%vhB>rbD7XEwEkUk~Yb{qD?4gKs%s*LFP)4fo*q)SSqOvwzNZBcd*lDK}q|u>NpBrn{+4 -f@PN9qb=nAu+o=`Fir^_-F$Jt1m=x{624&MP85|I$!=!u)($g)54cU!+ZU_aVHf18v6hZZl**Cjp5KU -c>OLY~%hXNBjMU)w*%O3*tsBx#1JR3*0EQqJ#0agcNw|Rm6c3lsWZ^&)ikUQZ0pVS1wJvtF%Zq+MA=Zi1sgivkzC3xLws2!Ghc6n{}vu_e{1OpI$JC_ -*Gir7jD%0H!A)h39;yOf84pS>mLcBM#$ZOgpF%Eq)q8!n_(F%ABTklbEN9;bYD_WRBY7jT0_|YvKp~9hC!;^dBl%)8(3Wt6iG`*FFWR8h86e}AvPDiA93!8}~;(9);n0gXO6OeD%0tJ9<5=c;!8=POpqQ~Sl)rVP^uCjuGxC<7Y -b`C7k(L^bZJZ;Tg*4daPfKYc)s(0A~IRrhlKWH&DD%Jo)j?f&;RF@(jm9+M^>`4(`E>(?M77=^x8zGs -}Y}o1-Hp512Q+1J|7%V|TPpmwZhJig0{xrLHHZI&8f}2rGIZOul1i{CB;sMIdD`VyuZb?C-W>KN)&ClwkT^6~g&cxB2K1tU&RlAWQIj(@Ml -uzlTn(J0@dj^76QUif&Cpa7h@*a>))zq;W|*Uus2YXk9kCcj*n$HrbeYpK20Awf2R ->f&Hk6;#a7@MZ<`7#3kQ4Q$i^GZn7-R<@X@k{%w)+6C<3WsHi*(qU(RVW8qgj~s)Csc)0#3A`w^rGjJO;N -10^9*<%dNLNpO>~Gb#Zq@tLYqw2AH;0W+>W0YlKU4j%xKsQNb9Wcs~ZS7yehH%TQ(9`Rw5Q5}kbHoDd -_gk9MKc}J+DTHjs<{y`S1Wk594r6mX=)3iBM>N-cD3Ff@-Qe5)nIE30D*Fw{U>h3ljAyaJB -WsAU0Jt_#pa=^>L9>=d9VSmHf>`&9fEbc6!uB=q{>!$2!=1!MN2`-Tc{XGt(Bxq9ZE3C*q8E9DIr^RE -VeQWs*H_3OlQm5siKgHD$2Fk+LgLlu>z$x@KvYV9k5SbmD_4ohD-@MBg2a|;bCP?*Lt@GvA0VBu8F9b -kWhevibJvI>cElG4!IUPt`_BnCea=F3Gatd*+9*=?grbs$Ga{eCNe5>8{1 -n1UFUvO(lkh9SHZt)^1K&npe{t5nG;|ObFE$DA}804a&6vDr~1I67H)}DyPIOF>QmXZZ>TWJKNZykU~ -CJ3NVmUGTV`gFOAS_f=6;1>VN`lR0X>X8|HwC9}37%$#O=cG$L*FtB29hSZj#~8OIZ|37L(ww+MOKEg -V$ysuxrCCm9G0g5G92B!-TJa9iyaX2;p4dY~QZ^!$Zn0cd;m-l>QWmfe;Gc>sQ=pLziyVS_g3SWBoyq -4>QG<@qSJhpG#rZX>fdQqx8|wb`+R9ExeXPMsXFRqB@XG3=|ZO);WE#SBb-Lj$le!a=igtn)k&#Fk-d -oiNpQh`UJ;B(rXdYGLd!6t2@USg2Z&!?r7db2mBzkvwFh#qgbd>9?#C$&TRECi+68w5d&r1s_;LkF*f -tFp+kyh8Z?TzA*nOmJ@t?-eTE+_)vxKrE8#S#pK)2D`@QCJws)-GzcH=pu*Ud%X{cTJpsivE>z|1QW| -(Z2}EOylHg#$L}RK_LvXyu6h?AZB&HJB^FkrxPwVm!ERkLS)f?@Po~!=%=!J49lAGY~&}VcA)sVI%3H -3Vc_5IK)Xn;td$JUGmJs!*P;6icUSv! -j~Kp$pjIV9z%VV#E-C*c-R-dvhu`|SM!)5v4(2k5Xx+D*4=}HYCTi%vYH+MOhwE~e<(157_upknKnea -g^LOD9;i|lYpq0ghfF(^`SLx&7J3iNypfjDVOMA#;FDWak6i^ggGdfrdNeTE7f22{xOiKJnO%QC&0tTIJr>k*mP5U*oQ;uwkRlz%8<& -IFhE8X+dOJ7m~*3bg!vXM%gVHZ}SSsIJ0fG+E9iY;7=G$?YvL^SEjCNQVo`7tl{RR{nzAAyB -DNG7cDP~A&DPwKxMM{MWgH=BxSdKychA^ahMmf+ -as|k3rQQmjel?}l4-GfM3EDP%tA$w$k+ -C!W#qZqUDkTo)6xkm10$Re3{KA$hN}Ces#3%tdu^h3QW7c3+DN~0uy)1mM6s@(Lc)9ptVjcg-8!}SrW -VlCAD6s5csD%>1)%j9G}*C(e70Olc8?X3_tkJxVY9+DLPMOivzk6@hSl!E5@gyvxU!U+z>GtAwjM=Xv -tbFLbL_iG3V3>|mZFaE(5WR?mz1#w3HY=1p3=tzPy0FwYC4+Q?j$acLU1~NlFP!OC~=xc!pMJv2ARBi% -U%_P%9T()J5qvUGX6B#aQy$IVkmzfYX3m@L34%wtmcX$k+WgpjLa>|scL|~4GMGh^=_MU`c!1hz5?hj -e%?wNTRZo;;BL9P8E_TZH1A!Lz&_BfhkIp~Xq+_{jInk9uGhf-k5Vhr|DkJhYE3CwoRDVmWR$F5S$(^ -skuuv`TrXjM6~&59g~xjo}0Z$mzZx7pW`I}2UXA^SEx@S-jI@_V(xO-&LA* -g1!1c9kS-07OtfJ+!w#yC(!lQk<6Bhzq@`uC;Y7n_|hRmJhj88wB8jL_r8ppdl!dWu&LoSv{=*Bp?B?3_=D8lY_|C!!pb8j%UVJQd^-6SA+ -}^LZ$oXb_oj5CkYY0KnwnpB*Br8F%R1ZKucTCD;IC+w{bVECN=lVjR&*Quoo)%_WNX~CdmfE#Xeo&ROOJ!-!jWufR6ujN?>!k5A#PDM0`pt -hQ=-f*z^9x|b4Z*A`Rm?mN2T>+BID5cX=N1j6BOFEM*gm&I3`bv)m4+3EoCuYLz -O;}FoAuvPAXXjt`AI&`-eYWs%baje?d`0nW?+PofDF|w@dD&(d>Q67x3$lT3iDiY}M&}y0<4NK3|Qhc -@5lu)Hz=@MS4uxLJq0T*cFv1CFn*Vs${|{rBiF3K{X!{C}Ds()RUa^W3vP_Z1HFSU -sM}h?es*5YwdLy9$)a*j!WkvKOHJct!Hs)YdyJR{f9PsY!&rp=hVnq%RKapIMe -n(F(lgGsuf`rHE+o^h32Y4k-1Ur9-=_{2(@R@&A)JUo?Yu>$BeJheP*LWvoL*L*RNH`c*1?Um9JxL9z -Gq)zW-BMxmux6KCTxq-4`UGiX$j -R{D5HPvcv57I`oPUBc-RVftJKjGxNKpPiblFrM{`z -bJ|Kxds8|j3BJ0w%obrzcxua2nWr~-oZ=BrfLSVG;-3Ao9cZ^=m;zKlcFt9n&i4k8)g!hr%jISgYET;x9@1d-#+;CulG+sz5DLr&0n@}?q5ItjXUNZxf -E@Cc=PW5>7Lug9-p|F?f#|rW&QBu(+`ht?{7&cpP=%w02G>-%chw{jVI5BlGlSPR!$Tl2-C{nrnM=nJ -ndnlZsmc7fLboPd!R#JQ->toN<^u>B>pjU>gDULg`A+KKXTIQD=+jryp6E-H;xI^mw_xXkL^Au -@C_l`;mG?}F{6LzXyZ0n8Z-jy~6zWc{QmDZMDJ5vq+>+MABxePQF*rPt^xl$WpH#A;UX~gs(eNdO$6% -~M(=TWW3^pZb^4?PJpA?)N)0p`DBnhV4LTHbGsY>u{!V+dIohb6agDIDVq^I7&ptqoXI;Bv^!wIjI9; -t;(3pzYuPXfurfE)X -{Wc71TCaVcW{Cu6a1N|Q71_9SY=bUw8z -0cnv+0642SshKC4V!%BH06Ei>w}WHO+sx -VuS*1Bp?VCAk`(j<4l7_<1MWO*n2>lA#Y{lfB>JYQ63GHDJgWm6H|cw(N4vd~i}q7;_Bp!#jbqvYg+H -3^04gG$^#0$m)$wQ%UrlaY?WHS`I50wyohQz2br1P4=Rr4<+aRKOoiSi(tPDm -?~E)Sf`DJl7L~S1~Wn10m9z)PO;Qr|fVlboD)~eY4Ew`~bG|UU1}>U|HU;F#NjYpEx0Q0Z5M}!e+z)Y -a6a1751P28xZ?x510~n;yB3;4tu7Ef06<_;0ls1OoEzj3FggRbjOs@jRup7)XkV4XNHnaN8qhVaTU4D -9#2VK0ZvKbCUIy&FC~aI)33LU+gt%Bw?F}-3;m7BRC(crVLCm0OTutXRWF_+Tk)Ko$1#K2dIDP~bIPzE4qOO|j=KDwIOgpb=(Q#VSgyFxoNdPrNoJn%yo_ru2# -e&Ba`(s7YK6cYlS|6Rq|K9;K+(Pw|AfR15MI)0oKV&ioIOFKiBu$5(%U4BCe}ThDbuS0rx}4PPJ{1UY -k%@#5*{|UJ2CVLu9QK_rg8#3lWuClPi2@l)7}!91c@+iHq2T6%qt$un|w)5&k{B_0Z*ChfnX1Il*1W8 -iI@sjX{OJWXD$;LDRad-_N{B;VoBZVaLNSJ;hK8-A6M6L=q+(jccRuEmv?!Yn##te^yKMf1|(GJJUTj -6bt6!Sl{c|STbOo<>xP>pTTjf|qiuNs^tQO3Yxc9mZXWmk` -t^yKp2V8Dp`xUytBFK7;-l#lB5v$PK0B@jwT)HJRHq#j}wd6dl&<9v`OJSXpkicGlwZg-$lqi%oS7%g -RWwDxQ}d{TyK*oo`Vv44xLMI3FZnTHqWBpJ|WQ)su*tF;S&O?Q7bS-D1{_uU7NA>2DruwQx|S29~KyKSZbH#~hKA;?=zWHi}P#1n%@2Rv57i -RIyeUI!Qa^CHiH!>BvU^)BOtkdn-9VAhhk2=fqd(w#u_9ZKe5Uc#n^m8*muC)OsKw2C$)BKG1dtktKnYA$^GcGt+96Fh -sshq$m?UApoXHe?O+k;c_ooP0l~0y+q_Nxtr+0T%3Oxh`kbOHHfzfNDOGSq%=#S|CEE-y){v>=l^J$(O$5HUNcD0I?TaDe<+$59u2ZPKWzQ5QEW;QLZ`-Wv)e1pIIfV=w1mB -2Z~+|Lv%iggdsgFn$WZX>6VC}XVbn8HQ9`n6|g|wrHe^iR;_mV5SS$l-CTu;V2Ok=gkb -bz!}QJ^6Ud#1f9qax3g_Tzn4z=AW0=`P=d|VrV|&WPD+{7tI=alk1CTab6 -CJwSP{k1Qq(R>SR8okqym%mYivn6TQnM1dKH_=>IPbWBpcPb7+I?M?eE=&4Ma2?Oe7HtPXT^|JI-3jy&^X2U>M -k_rnNZ;av~26=}5Mk3-$6B6~`T;vl$9kx1@h<4wyt}gPd^~Hxx<;{Uqfi1zDQX*yY^a56jeaqKJJ -iGY8NEJDlXT%q8e1o_Lo6Zg8y_27a@iC9JM0hTPCr_@WSD=&q&*%o1tlq-8W&rLaRn;U46*WS3*wB(< -2Jp(JXSFeImhV$pQX&`eK!J5LXuLf*XBkC+TdNC)`S00rr$)M7}#t>mduiO9S^ii)1c=C!=ZQv*OS?| -8890>MTm8Fa}qr30ksNOK|X)^_abiX7fg1#?)>!hr0tr2hmqQ7BBkM}Usw$1+9?l%kx|pvjqw3J%f_K -mh4iX`K*D`M1M*-(~kPejUM-Eu#ZErq}auIBH2)4o^y>E4ow@0Y&Tw;ope_YkC^d#ROL*C@{TA+M9W&8C29W3mdm>B&Ci@mlWH$`0XhZC#qJuO}qF -&GpfyD555>eNST!|1~@+{)-g1^woflYwdV;%A}g?R=PXOdN=lA4~8@0LkQ$vwt&sH+C_xGBWLOU{y-O -|hE9IK^*rm?2D&bVWJ7#hElO|F_V>9AvM7HG#He!tzWD6X>6yPnh(^f`b>+O>b+tsOVvNVj7O8m$n%A -Nxx(uIGVXz)_Xyfkkx1Dtd^E6gA6KX_9{u?DJkBi0^220JeS`@fLJYlWd%m{KA}+3P$+FKGo~ONxPId -og7Brp4#p)q0?`aqxZ$OZr)<#SopQz)Fv_~74QN0b#oZGcryeM@BB)F`8UlY>giyec_h+g_0K{>%!c2 -KzJkea2q7IMZDVRfl7E}R2qaqK9Um^rWdh*mjvyzD8CWXRuwjOg9k4&%G*+rt$H6)QqjRBFMtAmi1)< -MM4SSG@zt3Tqhqd_1G1OmxXm-LdB(}Ujxz_go+hIT^hT#i{PhYZ)I*fOyDfL -pbN?P0v;^{ctS)HNdp;S+@F`cxO))_E3hw0Q_9qZba5r;0sF)FL2g*9LVwP0AO4~R{@p}9-bi)W -;2<)d#(W$##Ad$F-6Fg+e@hAuO`S;o4uEg1p|sRV%r}%!gPtZ4%8@BE2K^c`IHzqAl~vGv-gE42#wY1 -ev+1hzmaDUc@{_0d4OoCL%LLFkn!Ax#JCt^oRI*}`69s)u+bzgYk_s=pseo4|Nqq`9jS#4=)2oqpGY) -g)_*|-&q_H-qXN=GFdxBIbdwBxrg%H}k;E>`D)kE&qbtEJUH*8>PVQ2JtGlM2ir_*2&qX0lE)(x; -f>^J;LWLtZ-}V9zGZ~8qY}+DsjC8G$i%FZOAJ4dtB(8C#*tx4Km`pajg51>=qITyp5C{4Ec3{ml&lzlc=3Aq)YTdP$9WZxk!n* -!<0$!Ac0O93_ee&Vq8!1j`B10N3%HiGi;=5MR*-ML*FxZTZWi~y#@iZ!3W2zG%8_i*an|WiGJbAp`Gg -*{Bha|25V9ebW0%F69(tWXyOi402|zWpe_Z_!)k{*2f_F5Hxa(P6K^rXK7m@#`dDn%i8kHjW7RKJvVy -7kPwv29mt?4Nm>y{-XJIy{7Kw?|37+&RZiBTMC>;YnG=@fIwPkAUUIpX5Y=k|DCTkZlsl77%8{#z?5r -&PiNTC_8yq43R3sJ;22DR(hjlB3DUHupm6y19K#4|45OxC;B+TibijZ2u>{1R4;lo2T#*YGF5!@xUq2){lrlHjZSj#_}EpWBbD!gHKnm=6{%@X&p?3)XynBdt4VRNhUB#*y^p4HS@T- -IC(FA;DR0ANNedy|XLwJErLu;N=!U{)rSdf7P%_$MVR30jPdOhFN%?a63#puxh@6^aZLkBj1*6>C>3P -wuCqQ`WDdXX1fdL_Cr}j1)=;d%rZ{s@t-zoJR#X%A%SKu9c12gajDIQ3RWW2-+*BFOE`MF -gmBt%tp)0Ln&Z>M2fH%cV_1B{l7#hrB!t2u?V0&#=_A9O3rt1e>R8FPg2tq-uk{ENY4REQ*Hj6q`egmtZyizJG91_ -97=r3lpWwF4KB?*#C+vlf=M))f?OlYTnmT2H64bp%jnEv+Bv|H$TEw7ZvyjD-j-#upkbSnNLvxirRFb -1UX$=xy&MsRemQFGrgCA;F7((p(Z63Bp+qJ@OlK`b9;4aK&4olzWCRnNohTsaisRI*NQ_T)!$N$@{Au~jKB^vFVw&# -dw0JP)1*Z|J?Q~2ec%jD5yMuH1i)gH|$cJ6MBmOu2^kQ -YKU1$h&qe7YxH4}YR;>)S1fa+urqVqDV9u`QXQPhy>d{l$m2vdBC*lYqRL*5^l9H*{l;byzme(dR3mB -q>|X=F~Iy9>ljzJx}_^f!Tse6%$2r}+t?T7sCu8lApTQs6T}-5(^cW?K-XDJe{~5q&sO12LdP6_4X0< -VeOZdCX~WU^xLBVcUK2j^36Qt|7S&U$_+8cK3rvD<3l3t6|sYkqhR&dNmjswSFZAEx&Z{;Lf&hF(ga#O$<1JR2OUewD0Iy#XXRkdQ><>75HHvR18uoG6L9Az84rJ8bJ|L -c%MQj)W)y|FdqF!jv)hS2Rs?#qP~zYA3_lVz4Beb1feOCdyv60fy}ee_br7?py8FVbe(wWlEd7gBbP_liFrt*oQ4p56>HNSkZw-W$Y5ANgU2$G!E8 -S=F-Zo#ekwYD{dp-r=GLI$V!#EWv7~Q-P2jvZ!WYR{Q4(?Rs-&E=0E+RQQrwVwQp2FDyWiRTbFN*^8z -nVxFG}zn4Nu>6e#ARGwZ*l#hOb=_A)$*+GWJvz!J*>WO|t%a%S*@MWSIL#K^Z0pId^G -0l^7mDKms&Qf5VkoLvljxoTUxJ+&%G-#sz;fE^VLgXZ?2FdutJB%83!0m5(;p-HuiO>{5+1B4(Qbt3BAmW9`9;W%i<5l*E#6>h0;!;DeIe;9Ap7SvMPEE@JQv -#V{{Dmc#@JFNKE#?auPUO>hs4i1?Yuq7g&>Njl-_wS`a7Ue^N1fbxBB*r=={yO^^H7O#=w>V0$V1T{v -?Rsr6cgGhvLP@%X~Tl3H}$}!FovG_>|R}z3QP}w`M0+Pd31|`@Wyq+`8s$?-h@rD>F@p2X{-PR8sWHp -t>WSEhb)!8tPXbF7R#mx#u=PKxG9#Ej`K#X%U|zApUPt`rcoR4$chJnGn;Dt(^Vp!OsC8Og$I!At%8gVEnKe<@wMJb1_hkkzA0J)8klDemOfdX89J>}}X8wW?-Rb9 -xpnsG=XQ&c#u3E4b%5Qp}boFq?AgaL^XD$Z5y;&74?=y^niK>8_>)z{fVpvamnr3^lq>b?Npo!uL7aw -$dBZS|l2PE89%^Q%uKyvT$rvYM`@KV!L+7rG-qJu=;u@}Z>z={4oY?&i;*JRn@0Vul6owl -QxoN9$)Mq{F_^ohfv2iGIAL-)9Eg_e;5_9-z%WyZA@E@P#FxTa`Ii(dt^4(;KUmn^-}F0lW0jQ}9+N< -Pi3iV6Evj;CJsRX@<>48hOZaGeenmGpWyEEn9>%M3Pi5(T#3N*cAyU9@DZ`t+kq<0eP-Gc_udsXaLWh -_O!#2hEs|HyEUBJoo3A$Em$ -%yo+qbV@{QLd+PurWv|9Elx-%v{f1QY-O00;mWhh<0~wcGX@0{{S#1^@sx0001RX>c!Jc4cm4Z*nhkW -pQ<7b98eraA9L>VP|D?FJow7a%5$6FK1#hGchu7a&KZ~axQRrl~he{+c*%t`&SI=ivzgYIK=|n4SJ9j -J2lo{kZcq=8MHLAiBhCNQVG;wzmK-;Y#Mj5tCKXGd3^89Xu93&4-H3;lQ~T%$!MO9XgFOYf6*^;yPYn -r_Dl;acTx*Qi7A+*qssh{N{PtW9p#8copb6$OD~c6z|XAAfi0Pm{1pu3&)K_N92lXhs+4#NDt$tXe)F82dcx)9(W$3H@E62N}x8vIO*48Dsl2G|q?AS&Z7-yTRc0g$+Te*{?>ccT4D1UGM@gH -F#M;?xdX81u#=~>HFnXJQ$qrx9rfT)h=*t!9y*F&%)xHz`1NMLU7e{(OL0BwEkKjexj{T8v@AWpXZXdA(b0bKAxd{;pqfl|P7_lC0#^ZR*s`P_krmBFY*`u -E*ngG)Le_qQVP<14OgGzR&L70SA(D>~yA@Ng@Gvd;7l6?jg^f?f+4y=Wixg>T+^+esz1UPA}h`{i2>& -`|RMEx?39cu1pq5u2XeZ#6~4nO^g2|X{y!xJb5&w#hYlUOKU0>o61C0QK|?_M+ct5Bu}aY%U+vDlNKv -es$#CHJ*2U|`MF(g@Q+T53{S6%vPz2Fs;D%&GO?O%n7}S&T^dkZZ9sWhsoq&%JwHBv0TO$ckJLC#)eW -t+>c&`8KAKnDm5EW^Jm=2X&pKi!#a7P_4j~stR?zsg_0V#D#*=WLD~OL%N!m#wc6Nt2 -G$=No|T+MLJidi4!a2A$O&89{;H*u|F%~WWJ$Cbsj?;;<_?rX1&VZT)kJ9#)4Dz#^k1iG}rYkO(JZ*O -d^w8qqJ2kdS;icrY?w)TgQmHC_rA5N`F!&!FxKJjEN`e`RK%-<|qynY}(Tm@ekvzXk#BtZ&V6vlz!)E -uL@d8W0mAgb_w0%7Z8M?YdDjd8C6>|uhRi2V6FNvxqJKW{hb0vzzp))+ZHPtmg^a~N~6)bA~B6C$#Q+T3z%}FN77tP9SMV=(f)}>zE7iE0l -J<%N%7slpCh_tAxb((I-HpL=O{sUFRi}OXT7ls@~`)mf2s!2s2tku$_lw2GwGCpY?9_vl|*33$?rtme -XTV@YPb&Z9S*l4@8+~-Zl3)xF?;uRCZ#Wc+2R#|1fAe1PEWIN@g!^!Qt;g3JQ_}lQuLu90XCKl4U_w; -(5 -lcVp%1DRNTmsE4X;Kjf}VC+M~2+ygc4b`E|6g6bL`41QaqW-*7!G{&e;G82NS_DBNfs{2+xz;z|3*XC -U6A}o&qX9Wg&JJ6j{e#emou?fBD-5Wm#KfMrDbOkZ*HxEVf{VDq-pM+90#}WumH~U70AEbEwCOju6C2 -WLdi4_@j>r{;B9s~^nI5K5!;sJ75ikPXX#x#CG#%o=HNtA|^!xgn-tf|pCxRHY4Vvm_@A^| -6?mU?EY_AHWtHDxuF|ESX%+5lyG|NO6hpVKO+^^!bF>zK}&GbC}Z`Ynu!iwtkzW?og1LEWm>MrslHn3 -oRn<4J>iQW>PQBBK=jFPwXj%5YRn>Q5wh;9z=G3SL4jP?~ExLP@LB$?J(*L -89n%QNSe2Y{U{YTdgs+JqF%K?pW%zrGm_9H8pd#1mh^#Ei6^mq;rbXyx}b;7v-c1h7rC*ZKk6Kuoc68 -W4IY9j5zEhD7BE>h>fU24^-qH2rY49`;=-VyEV?BeeqsQpp&vy%o0AM*z(qj5`N7BVX&~*fI~OL1Je+$7zA0mZ^Hh`OB$R&x%I? -q5w}2@F06-2AL>NY^EM^lsQ_7M2}h-WMB%iZZrcELb{i7kT6P2O$M$U>ZJ}rNJ)t9Aw#Ks8_5iJAyZzInN9 -~m1o~iUJ!uO?F=n*&%psf(+92(DFq#|j>X}oa2M3Z%xjN1hZb-MW51l}Hpk2g(BRGiS$h&>G*XtK9h? -G@P5#?i{gIy3)lv`EZg)nto2u;T|!5eHXa)1Ns@&fdPZfMPg$f{pafm_m}7M;Ux08vN*&W{MIl@t&lR -a@STDhFWLf!A#MP_U7DT{jI8c}E~=C%`KXdkS+X5KWblX6ng<60%j}OWcql#^-ZtH^`a*n{)CJiuh$B -9zn>TYNqrw>X}FN^sFHZZBnL)TND8_dwbexha5I^{wdF>M|ODtp%yJ!7Mu@YhBV_wB!~@AMiZ!!dGcT -Dgi!=JpCKgf2Lm956K~&?MZFU0uGsHm#d+Ii7p5c>Y@~GYhKEE-rEooHq6{XJoFj(_(u(1IJa~{a#Ug -=Pk@K`kicO{g2;C?nR0TalbAhbaAh)3)H)Y{PoK9R5ZQK)|KKDvUC8jLmI-;&c*h8sYLo1E>w$vr1(3 -QSaCId9^U;)O$i$S7OjWl+q21}6Pw{uyFx+q7$+x4OSqrwG7MgGh -R!$0*9bpyP{Jy86duQ@SW%p%M#znvqz<;tB}o~LZQggK3X&?+f^-o$^@`dRmw7;FiO3ciJlVa9?{!+| -J`xAFcOJku9Xc?YWB4y2H`awip>7*PMbLYVzL$Daib?WXaA%xhROR;K2Hcen=^98>^M)ZF*&FI -)eJgV#rDCdy*cSRrt8TLD=Mhui`Sc$Qqft@xF;4jJ#r$o}moSBF;=MY8XZ!sWx# -O;F#2;p+yLv=LwoHSy6j3`dfDSfOO)Ra1*-fNg;m{9G)jvz$`BgaMjS6M5y%L}DrlzYwHpB$gC6iCP%2UqWIrs0#BTQ -WVL%7fT1X`UFj{>l881?gFf4ILIPw7?YUk8`(-b~JRmvo%+;NT! -ik+V8c*XvZq{>uY`+Oe6d`+I1my1(yHYIM)(r?Y<4UDYh&bb6mTHM)H8N?jqVZ?6%he`N_G!BoBeLBp -9m*ka33yC6p(7TwXeY&z)i$%S(M?z$E)@vtfEtEgRYxnfnKau&69cx@1D&#v)~b5V9`kW-BOO|w_I3I -(d+>cO~=2uh<&PaJXxsA=h?;W|l{ke9nAU}k`C8GyTtMP0|Xuzd0>b)=5A-vpd>-?RAEv;~c(oa?;3) -C{5Ls5K5ubI%@r?{>1G1|B*1RR0)QebA$(W>P3CB#iu6s4pft4 -{jbQJ{@3gC*5*(A1iABbhoij6pX5s1Pw=w3vpP;Z*3)WH6fp(L@FGlmv2@^To)ghDH=uQtnrK;6raDq -R8u6or?E4XfAV;MEI;yf*QE8l4{`#8^7Kj`AV!Ko>8{F(cni=oS7eL{r{2;f+uG6}7Gyj%Wq+1Z&ip1 -wKz17NSc6PAnX232Kc*@igqGJ}W8*rKud}56c)=o1x+ETTL6W4}g_7^?p`2^Cpo4ep#{3%~Rn59P8^H -iMtb~@P)F#cbhbL;LyxBQ3~A;e3_}8T=qPB!1~Looo=6ey>Zm>Any*m7H -*j*XCcG<%swFiuxtS-G%M`Yi8EAEzR{)W^S{pQw)}@sUT}41lptaBupQ9Asz4GswcXKqaF0!E+K^G?ik0z%{WjZFj_yA(2XZCHaugUy$&H)@-qX4KMrOyJt`r#hm5TGJc(&S=Z -u=0%#ye2!dHv==Q+^kCvpqLaQFi@(!H2^^M)gcB#@fLtN*A1mk%4)UF87;X5`RJ&r|QS1zpQk}$P`p# -`cW9u%d4(ck6H1-^@)|V%+H&TB*2~8l-CsfDt?~-vhswR1tFPAm_08G3yB!VCrsHw@|V0Aulk;6%L(5 -2))D4`f{Vy>I1+Gc_7ARI&IUn0XF>TKzr`xojs>eP3)IW41}g)S5*E)omkOuN@_ObDxWDSeX(gCT`8f -$P@0GBOfvAN}wf)&O2Dt0#SqIqx3OG-|Cf+qAxeVI;f%|0E9_aVtVy_zL=`^xSb^xvPqzTA&)L-?F3% -#J4@2Nb&c)yo0rcIB}t_^a3Rq_>w1lG607YyfKx-*)%(VBLw8G3fas^E}(j0*r%xNrrns#mYvr%i3fl_U9UnyuM^ra -943gDkQUS%N;8Ax|tAO(l69_!IrO+9#*9%uwM1R%M1nH}^EFy@y)vi#*-*xU#kD$qrG?u0e0Q>_Ren_ -lg00_f=yKYBKo()dRGlhf)8oEhje<@9gv{>$C}fc;Xm2Du+)Dt76#nOf-{quvHF*6~nH?yWJD?eA;1N -_pb3wb`ZX!-rvurkY=y%p}u|^K7UC(Fn^yv4NqQ%A@%tal>1WNJxT4W-RB=)sgu1s(aW{WcbkE47vT9 -m0k&)8=TBduqke~gVb{DPpGmFR=Z9CsBRL|c`o+>fwPx0A%+821E>QShO5BHzYuGj -1jqS>#xMXU9_l)g)A`#SP~|9xxSDsXZTYM28 -dISQ`$j~&u{IAp$$%_MC%5o_cTV`p4dE7Q$HQ{X#AzSblj;mKL@+qKH`CeK+7DIJ9Q+TpJ3#$Cx^4*mT~z%I6u++JTMoXw;F-XHp{e_ -#DVW_j&mE{tZ)S93b%Z^ZzGLrV$yjej`#pn3#lM5QUhA^zxcvmV&;Qqf%&_S@Ci%vSz9TF86y3T`>Ca -C0?7o@$26cCGgn+S~@6aWAK2mly|Wk{E=nhw5J007gt001xm003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yX -JvCQV`yP=WMy_RsntK@3c_zRMZhWnd(*D@7}%omsf9IzP>(v`_1$9>izYbxBt(p*RNmhzrXwLhc~aje)-|;yX))Mub$t0xW3 --M`S|IlqMyHc_2Jbn`47*3d4B!&`{#Gp+i$NAf1}R*``ndw@1OsF)xLWB=KcQj-S=iYi2eXBozxPJEO&#$u9+*SO|pMQLP^7{4l1;2fNeewMM`Q2Zhz -d29x>DxCS-o5(M5Bo8H_WtMpd4FZzwd_q{q^J9AFi)ozPa8#fAi}7seX6FhwJ5=Z~oKUcf0&|Z@+ou)!<%p -Rg!$j*RQ^Qe)InMdinnPp3mO@`L8|oD;o -0U-H_{7Z+G?f?%!X}ulD!c`!CP$-tSMY`f>T`Zg-6@u07U2d-;L(@6r68fD+dp3a{gW@g_~f@={ -qgVjZ+B1cZ}<3}nqGbP{f6K-*M6#ZFW-FlaW{Nd@SD%R`1HT+5}*9?^Iw1d)gNj7uRj0kx1W9a<@KxI -eQ|wq{r;0LzWV&rKm7WWFRtJJ;fvq@?#s_UzOFBypMUCNI`eOJ^8eSx-|i~Ed%Jt`o97QNU%h^R9{rD -dc;D{^zW(O=^UJ?H?_vJ>`PD|$^>R;GLnG>$5+6`ip=1{V`WRYBJvMseJJH_T8R$AJg7a=XL$!7ytEoe4P1MeScI({PfWrSsweNQR+`0tr~4 -m@KO6uALZ^y3O@S$>7&(KvFV?G^Ncb2X?`&GzF^R<|MF91*MGWxR(^5+@$>z8sUHul*%$jUb|?PFvTy -c}XwoiNSB$@l`ggPa-Hc!C4}WgOWu3c!_2c*3-2Scmzu3?De*xCd|KW?Lr`>7){M8?S|M{n%{Q95&;q -x!Q`r`9n{^6_7fA`ye{^FDUzg=bf_(6Al`sbJL=|^2=X;_Rn8_{y#q3C3=f## -gzH#?O#j$m+NOw*VP{{wy`Z-;>A^F-Iu{-?l0;ZFKVf||24;ptIhJF@mX(={-Ql%zUc9Y7p=s2(MH)N -u-qk{_8;ZNuB^VOxl8U}<%?2Qdr@nR7vqr+KAZbCSzgq&;zg-_-)7&I&lW$~ZJJ|$5m8RPtv;0+mG5= -~qpbbY-L|%en7h~Aw)WU}UF${32hDH$ceT!4`RZII57N2pmOT1hZ=X;5kNToK_MP@+7uuh3SzJ1o{q( -&xF1w<~?zCcYT7KnBo?$;>9rn#Q^+tcHVcYBY6%g2nH6uE}#vo@?@4Gpx6iN_%aQzhZP3kGAi%FAV6S1HcB>1HcEC%c1m_icJ9?F(J9d>(J29p3h<=fj9u@b=3{2rh%NL*Rf!{-Z@URQ)dV_i~FMUI7|7z_yur2Njcp1+2%e*EydW_FgV~@lBu}jL}v(CS;r_@V-QSV+$p0L9EV=FKI3nRSod$nb~4WmtCE-sB50@I*vSEbmVKY -N96Av-v0_3?z2ph`X-{)-k})aL5bXzJw>XqEe4qp}Wy45m}Tyj@^#Kha%p+jer;)IIDMbA01{uo&%0AaEdfHBQ2~6tU6Ku~CdW2SDq%Q7Yy>pLSWtr9zT?{Ad{DtYeGH7MDN_v{ -H4Ek$-vpa~&vF7^$X}LWz56=37abs0$3^As6TQ}<1eYxS|K(UeoHVos&x(hWuvU|Ecz}}T~T5!X5*7^ -My_&ba}LxCr;$Dbcid;l>f!Y)Lk_q^CowEv|UC!rV#7c5Y_Iev>h>vunFzhvKu77YPzZ9e_>25_M+tWCwjZrc^xKAqVxLGAvtG3g(}fln?64}JS3` -wu>5Wvr}>mGz>sDAKHb0S`rU;lC)(idb0@D=T7UMXaodwVw5@&d5qgPoO@xbzav39q2`z8AiinBaowD*PLERS_Bj}?byqq)#tyq5i3&DErCcr;we{VyuVSnFthM5or -L{rkji)PbZ4dtvS1vNMdGu5A++j!D{!vj(e=+tC@VT_p`;^{wOJY~YvF!B1I$D+@4=;Q$o-!lSVOS4V -~gn6V#vwiVM~gAy#fm4&y)-Cq0>jO)Hok_EW>Xe_{$1-P;RW2g3ok}RW@Wwc^A!05hEVp3wBaU1w0xW -xkpa$hLP;#G0iu(|{Ha$hKEud-<1QV)WGfg3s)f>G_?b^MZnU(zr+4U^O8Je<%$@G$U_2R`Ot?H@XjE -P;c;8}$C5&-aBAfTEQ%-v)k3!((gUvVqG6E*rRH3D_4(;;A+8*uY~0j}1IF@Yuj(1CQ;t4?7;&^@7U= -E*rRP;IiSVH6YpW)EZE1K(PVE1{51mY)2~FEE(0-KK3$aU$BWXMta`m};x}F^!ozt2A|^v4* -Qb)eXRVh4&HD0ZOOfno=W1gE3l{(9ny30eC-ZKIvD9-q=S(@?{S --q8(8UJ1&4lLC<#V780lc7gOLtKIvByF9|MdGFfzc%03+k}*XQ<2_8)^!f=hClTqp@fU_Ay*#K4gsu4 -S$(1B?tXGQh|HBjfg~=k`nXACv?u1FQ_p)&L{8qx(WhFfzc%03!p83^0Q0vM-bXBcq*ho7*qhe^3&v4 -6rgVTLX*?Ffzc%03!p83^0O&wJ($aBLj>KFfzs&x4He2{Rbt%$^a_^vo*lT03!p83@}2pvM<~Vj0`Y> -fRO=4bRAsm<8hB0eo26p04o7j0(%l*1YT_54i9*)0R=Q9bYN44@j2nyFdoy(!gZ -nE1z&<+GH^eJ)=|S4U?jjufRO+rM1K3iy}(F-kpLqBMgojPJL9(SO9HF}SP8Ha*pmPw0Y(Ch1Q-c0g4 -N#_N`R37BLPMNjKF1CFMdgYl>jRNRswqxU?ji@ZtlKN5{v{G2{0011cGm0C;>*|<{uV*Nr06AD*;vld -lFzIz(|0R03!iL0*nM02{001B)~|3k+|cFgK15@0333N+-t6fTuZtRz@T>`8)=1S1JX5{x7mNiYI?xF;hez(|6T1S8q|sTaQ_!AgRa1S^ -R>fiKxJn37;5!AOFU1S1JX5{x7mNidRNB;#@F#V<*)l3*plN@7n4H3u}uzEBd3Bp69Bl3*mkNP>|BBM -C+_LfjWhVoxR*nP6mskqJiN@AidzfsqMDCK#DuWP*`-{E|}eOD0%>r5G -@Z`$9=Dg8#cOlmsIaj7%^x!N>$76O2qSGQr54_qgGgOt3P+$^w~Ak~z{&zE3#=^c$pRw_j4Uv+z{mn43ydrl?7H7_5>DnU -nmJi$V&{E!hxqh@bCv7{?Nl;U<77$tmBuIieIw8$^t73tSs!w0waVB`$9=DvcSj!BMXcyFtWhN0wW8I -tmBuIieIw8$^t73tSs!w0wW8IEHDD$yDyXkBMXcyFtWhN0wW8ItmBuIieIw8$^t73tSsyal=8k%5{xV -`vcSj!BMXcyFtWhN0wW8Ita*yWT>{LdinOO36yeO36yeO36ye%DpNjDk -Ul<8YLPfj$cxCIKJy`lx&o2lx&o2lx&o2lfMYN+uUnykVp#>X_cX<^jpVIy-q$9ik1+s6mt)--rYJ5GMOSVk(wrxLSX&<2&bp%TDf#7M-uFP -NPB)*CmK@bfZnYKFvU8+WY`s#x##OD_D>N-$Y)dJ0@wA(Cs(N>XZt0O4Y#D>6<*W`$n52w^WG$-*;KS -On|ceo4XPR9MuDg{MHhUJz`Bd30fwE`05ZM+MWgTe<6PFmBZh`9*3T?yWLStatk*1wX2=WH$1f1s~OA7hhC-gbEc_?tFI{X;C@gJ-P$9}eGPygc^23Ag+tu9Pr)IquH` -@J8if_J~{KmUklBY7(ybGy!gIyR$L%RLKxmtZ9KRyof?73F890Rmescd$pQydzgc9_L`?-t}>GdI%_| -9vfU!`IOIObla;R40T%ONwrRJc4s%B5Hi0IBPR2eE%wBl^*O1Yyrjw6-*iQxT3H%HACG`bqQcVdHFcy -~40=-#=30MY?Ag2(-6?s?kCl~Z%E%&_Gaq_kgm^qlxHk@TnD~(FUyi{jY0fnG;kX;rn$6R4O_%S^A#j -LU3?U!6Mp`r=6@Uh^M01+mKVWcnH!;Cr=k&0+eX5#aT-K&`BQnhUD%(p;a0>gUQRAToF*Q`PZRC@I?) -d4|L$zV2p1q2rtd6{Zr0oSY$H`R8>0Xt5{t}|q$YhXtU{(2$bQu-0V04%%~vZVn43h^d=h# -_Zhc6NuH{iT(kWs5w1Ak#6Hk+k_ffB`t7CoBG1ssXOUyuomS|jhl?Quc|Z8waq?4z&%_gEdXocYT9MS -c;7$upL@g5UOqdkxi-c5jSps5v{!|Eoi)4rOZoi}g#DX_l2!k(fQxPE)mb=P0S3;%+iB<6$8opSCs_t -=Ud<-qNd-l5@wqH`AzdGbmaV;UKRT%67xltjx8;@IwLz+g7ajSS`9e1J-t`}CA4*O)i+b=1ht;1|VRT -S9gi+JQBLaXE<3u)gd$*Yp!$;maP5s9k-@JJ2kQ!g-lI>o --ZhK`tf%#V34GRbX;-fQ7`%uls9$Q7&T$o2V&uh8ED=0;!F;JMedg{yy0dv^)t1K-Sxx`C73`<2FPA04vgvSN --a6xHbQ-eh22g13cLn&naE=Vcs#V;Y0E)YuvT~Gp=4ez!;QRIuyKzFfykr$vZj;Jh!gLWR^x -fqyPV$S}*vR1x@K3$dMeicsIyyZw^Fdfec_3&OBYDz=gjy;w#svRw_JbXezBPL+JCpBffJiin`))-mm -l>C9uY?aX7cLJeDRkuC~Eb_Isxf}^i6Q)OyEc@P}w@=(8{{PUq=!v5VOxz{)Ps1kr&aH19VuAD%{&${ -D=2gb?>5m!jHbh68pc=u}BX+~roox=7{hyzf08V{jT>)7V{aBb50^76p4@wfu8>(3_6r?EkbJ=~7Sfv^Wj5-nm;)L8||q(3V0%>`w2vHI08>^u2xRMJn?IqTY>+DXxz3$R`%Tyr5c- -&i+`$}YfONP)I`d>R_vHLC5@i|&#6aPmg`80(@~kjDrK{cTEE)*m$Yf1LPQA}UJQ0{Fe-YDqC09 -#1=Uzdm=t`x!VJAM?$Cb~0P7e3pkNAQbWsjV4!Xc56nzDx4;8++7kvdRVLuiH0BD_|G+3L;I6N -(3#P)Z<8L@sNabB1b)hHAC;vxbrQqiORQQjr5755Yo7I@qI+%3-ph#2HIjQ)bcUrVwR -HA^)uH+Bm@)>B1m(57ux_X7sd?8M*vVAt$vmE}G!MFwsaf#XPmqCy1zm9>BU~uv?1kh2MM|M;sb{^Y= -w2_l-eQ;pUsCc!HytMl^TGugKoL`9-6{%?3(6Hi0zMdPBddw3FzVfYNo~rjcwFNP)E~7P@z{+7XYDWn -3)Hgu04G-Wf~v5R3chp{^6<)4-76`l%3@Se?bIP@f?qHV-5z>&fe|DaAUr{Z)5H7Y6d+qDiq&+bsdxJ -&h;4uq9ct(4Tq7q(NO6_(H2B01g8^h?8!fFk!LXQ!zeqjO=Lf&&Ufc0MjU%xlVaS5+iGN+LZ~a(Z))0 --49S;g%q6==yckxlMU1Oas|O>%}k83B+w{ -B!3FsSi>+_vF^g8(-}BUZ5Ze)q*L%F9rDn3)dxB_L-%?Cr@ExSq~w~cnL8WYe_;8YDl)#<>pJ&bahr10xk8nWO-VEXIB01SE;eQK#^zX{+sU@BFG)5LI4e!x*;_tM>+z -?&4h5%>kMfX}=6d?f$*{0^o2nSaT8PEk4GCBoi$`z6o%Qb}Ih8{yOR375-_)*ArpDGHz!unQlmpZgAAce(qd=NCZ>ipQ -1(&?K+q?aef)?q*D{Bv*bm>%fE(325ML&XhcNUQKYh*PsF7!Tbim_jp9y{|5-D^dLbn(-I>sv{o*L2< -+XQjOl383|0rNghT@IaN0uaLEcjPGR=saL+lsA9Poxn$u=vK}LnDsEF!7)+pj8T3MLmWT6)+C(xsri) -a&^)A*oTn+vEqOMpYNQMOWvQ}eTE={UNxjbTNqSkIkB1D6lcT2Zat&%QJ3 -)q%wab2a;^z1uG-codE9>8MQ$j8`H3a!qZ9K`K@sC#ejJAKr*5$rC~}TqqW$tpwY@l9#Tm*hyNH4cHtS-QY -KGO+yZw?2p-P=6>EccZ9T%b%9<84Z7ubR )UXl5>E4Y0aGyg81MJ-D_P!tjSWip${K=nq|Sgq**9R -E0>byk+jpxAxpdw_fq1d-F`K?*Q-j$o?hiybeD=ABQ3%{Ot-cWSr2(y489vENQ6}6>=$u&skfa(_gWz -V8svE+m46u(yDMrLvo<@F2x7QD3o(Dw?_`}{?%r}#nYFiHjqbI%>rAUK+?2y8ROuFXMmnJ6xXR(zaY= -U9HfBvpeOzK0tX)A7V!hihsl$NDKoFxnjj<&)q4kOYz@zFWVgh+zcewe!n))U1Fp>~1lm -y8aO3{lYwqW5eNX^>M&=Q<>y%ClJ)*)%wJUi^}aoc`Lus!g?^}J600EZJ^p&>0Rk{O3TsOdk -BrM9#(P0#HD-{lj=oKf}0UJ*v3q@myv*6uWDDj@o>I1| -D<=NL!(Y;>Xoy+u0bEdLO&y>@Y!3n1yiwr}+)G0bAEqWC3v|FVY<2g+zWG?C -J#*f>_}drG)k${5IS6FuMY8@ksPhB(LhA_Ra-nQq2bZ{H<+@suz~XP}HS^|fg#;<^I&RvtHRgzmMe`^ -S)$YLvO8Tl9gjJMN*Q&qe-K8(Y+S4Naixxzf&--)sO{O8P1A@2RfE2wXkHPuW2Yo5NG!0I2+^GUv#fUF9WxtRC7`f -n}8=l*UB5Qsus3ORU!f&6Do@5B5vWOTV8IivVL*KH*VtcHB=T({0IiWqGn^q923C+lQ4!%itJCGZH -bRlFMJ7k)8Yo@=3Z3rgF37dt&}+R;y@-2)`_-Rt$`T3XPYSlk_}a>M7{7O#EiuuK3jS1hoKw+Q*AcHW -FQj($G_o*HJr3Er9rhq4#JD|a?*`xB236 -*S(nK#XEdhC*!Cj=I^zJ=H#6|KMrPFIC-FfpR4r9K6@nI&dvJB0-JlMHDa_0>Q;$Hzdf6vj^)Lz8Fk9 -tpI>j_J69w3==F_-PYf5P-3;Y*3?4w5{EA!MPI$adaznMXGq3ULh; -RmaXOw&arqa)e&Q0c-mq#0_$&vmyKzJ!GBMOv`3{j#fB!iR5~H^dkPd(u4af)&5vD-2~Vfeo2_WWfg2_=`Kd53TKTrY-?jrT4fonPHTLzQl((Jea<3FPuJsDa~cY9x7>uB(xQ+fuC3J*# -Rwl}Q*j%D!lRLK@uuU?X9PYc4j{PSarZRGuT$yl0qWJ|oDIY>PLlV-#G@iKyeF3+a7JeoLopRM)Qi4^ -og1~8-XBC)W^(aUIisWE_;=S{bRE4(0%ICr`s)&a>YbeLM^D?(y*9FyRo+oQk&vumj)QueNE&(w#Fpk -S;1P=gp#4L<))1YsNVjg+pa%uS~ -ORpB#;z8m5|lfamY!&_l4eq1ww;m=Z3x>dA)I;O+rND>hYr*y4OOKJPH7Aner>evu^abnd!|^pAfN4P5UGtvD=w2`LZ|u7*OQ*+(9jt=)<`G0 -l87@?3rMo5HybSA=En!zeDW)B6yN>R)nmk09P!-FXrDONqrApX-<1B=tw2++w&0N&O=oPCV(O{mppE# -?jdp#^_VkA^mhm+LPJ>>gz^b{#Et+_a7Jqlzg^lH-Aw)gY{y4T)hEK?cv0=igTc~A1kF!L3{xsrtlSn -AHhb=>@be685^=88Dac}&3MsTNP8N=F>Wg?d9LNlYF50!~aisnxcWd_in^_7hxP^%SbO*Ee*pk}4QjU -LksY$W?0l-jgL%UK;*pO0$>3jRm89%F-_X4lwuRJbfNA!|EMaJHvGqq_uksQfUU`A%ouImI8Hj~hz066if9*h2@g#ZbER1ud|#Eq -#PpjRwfpY)Lfz|<#=&gC9#(@W#6)BZfP#5<32G#nY{C%JiE?SDznqnE7iVKHTc9nA=n5Iq@n<+;R)6l&Z($(!4CIfirN`)#o=v^I1*O -c0Wk~}9>=m|eg6BkeNpeL`_(7j&MF_p?hx#5XrdtnD9i#L+icI^#tQ_LSsp`fY6-tY3eP~f*Gqi@=7m2%RS%FyG9W -{xcZoG4S>j)o~i_w-WV7k88d|U^6nx}!@EXw2`2RFC^(?pYDjIssC;mq -SHUow@_>gdtbWm;RX5qn`D@K~#?9RYkj+JU82a7q%W>4_g*MML*mAF62i&b-q}g__fy3Su!n!$EP4c8 -`IcFaR*$vW#0i0rJe_rC*|JuZB$xyr;bo`w$G&hvOD{n}#;BmpE=LACMbO1+M2^jgxm`iv&Cjk+`8hi -NCI7P&rLC*GJUmCJ6KB&7lR(CxMev{|g&C4<)T>`|mM17Vex+wT%&EbNXUDtLc|~)8k8Uzl_}G&g{k0mw->(zzOTT{H`gJfN4Y&i7Plh -m1vJ;8B7di%RS%Rz3<@k}S4b#*g!GVoR2V_^Mrts_pxzV}tfc8Xlm!>0RKYnZ?sd~yUM6Cyxm20TcIw -41p^j?CQ*8u}KszideG{tW%zUu$D2KpOG$Pa<_Boye@#ZzwUGMfws+-80)?B5BYr0Z<*E~_6rw?<&2B -%sjmxY~*JR>tIxOFyN-78pao-KG---jy)M}!fQk&i7$YNR)BLdt2RYVGg@9o=g|l2eGOLUowgYe?hEy*E|vOD_K*^6^@xuCFwQ -S}Y-^o4roG3tm|^p_-LZpQ?)1EsOe5*!rdF%m=Z7jHcZ~vm4XwJuzN$61-0j}*^r@w>Zuo+^x|3Rh -m<8Zz1tMkNb-5Ow2IW_0%sTbXAQF~~P5|dMoJHgZIa;a7KR8OU;cy>hvBSc)4Y9v}I0IJ>R*|6ti$1N;?rmO3Tp -VUH02jJN$@T?9a2^OA8@jnQ0?f4Z$)>lv-k8_*(d(D*KX}CDN1y%W`Q48 -n{>;k&{`CC%?Z5M$h_}4h;Cl7u!}Gi6{X5>U@Xhmw=dV4N{`()^egF3T^HWvG4zJeIje}$z2po#4OV3 -^?+82%>fG7xDwZMyrVkotUS=0lsdyo{Va0CIKK;W_kI1L0Ffdvcl<()Ms2uOmAUyyPLdLtk)f;B#vMN -w}fgLpPbs0UG3F!+H!JIK%lgix3)#_10-20TYVYB=rieo=U|MsyZHXiS8&*2{@Ti*XBf{+G(Aeqk=|P`g<&el7@ob>U2^N#qg -cI`zEt;_JyRl0y|;|f_y&yYFdb4J9STk8a^yu@PHi|v3e3D*|t3+Lp4x=q6K@ -S}k?%Rdz=;>QNe2{Lv+;v#_-QK~tN#~~DRldXXc3U;jmDD1=(8e!P6TxKFU}1a^ -j;K$y5K7(8`>CzZ6`}3H>on0XHW?3gAbnO9E$)GSMPdpZmfoK!XD9FoA)C --yo?vx|ilj7VVHD^xN(SP7lIwZ1}~2=M;Doq4~x-cM~II0JQe$!KM9v{j;L^(mFoyaRW^V@tjg72F!H9?(ELKC&5#7$?=rjpU=_lV`gkmIC_nlE8y+^v`|uzELhv9M55mUK(KL90Chytr{@s2_cg~&zWo*KvGlRU!#NTGNK!-!|23tg3Zb`&ucN{WDdk1jkMaqqu0y;kIBv;@NeWf1)wzCR -GphP$knXD@W2upbgSF4)}JNwEbxU;_5DyU`qeC|ILI-T(qRIGHR=RZ%Z~397(AIuS_60^%_sVuDCF&} -0TO`(W2ojI$15-03tjO#yEk-rgRxt{Fjc6BpoC -GL9hOU5Zl13Bd<l(&sUJ;3H1 -v68>mlG<3~%h?Vih3&Ti9Nv@bE1vxNEK#9)dE|*qHvrgxu>8Z^zp0#li8=6gE}%e*6c>^kIm#_{@ -rE5tCy$w+E;_H^lR?AMSBOJloL@k~ZGI@})gOUP$>bBJ_ut;2I8Td8+d;>F=@xsweS3NESRrUaX_+etK3|77t5UG6!MF^I=quZOc?W86f{ewxaThd>n)N?z -P`-?iMQB-@Vhzag6&)I$|}sf{~_9Pkd8r{*+Ne0>5HPz!k(P&R>Xs*%KturnBT)~kN*@9Q)aNG(V3%n -QV`U6)Hu0W@keh$DsAkE9-FmE2oJoDneZ0c#ylu^msXle7;g(DwMLUJi{7?Iicqc7)c^O*M+my0ziTP -P3Y(#ZWyaySJT^-nR*yv^~Y9;4|dkg;i0ji -bHMBsY|?KYDk6XDp+j(sO>U8{3Xf4>vrvR7HViG>fFch6Fci=7t3a#Ja1NRRC4Nr|l`ou#bdW4J%e1CXoENS#0I~!!dA{@7{FRbS!X3^gabB* -n>xCx6D+?m~?pA#crwntk3Vy=Xnjo17EeMDsb-GB%VxQvl!%5okauw>OO57`Rp{dYN2?^A;F&&XfoLI -tW#W9+e;5O$cbjwMC48^ksf{<7tEyC*BJ^I=(f?%L~2sSQfqz)O55NmHYr=d%to1I4l&&ySIdmXnkz5 -N%Y6tnmAjsIdEw&+{$6bD)@}she~fCV1{~o33v-E^x?3)*60Kf6>BXu{x>bNF5v; -0NhJB5k8}YBRCz)GcOR&1~avCh)ytFgX!7{c{|$b?rG&`f-$X)=Rslj1aq(#fGq$@Nt(wwoO9XJ{N#?a5 -YGnM!61j+kP^0sqPyX*5DpUlsak`P4%B;n>ZB1M(0$D}uy!Bh@SeO}g?jBq9=!NdZ=;BTmD!PL{)+~J -v#A&0X{XE(c-pR!nnwiB%T;#0m4x9+=WIrGZKjZlXVEQ!N(AjW4qe|ic5EtJ6-gM2&Rd)YkXh2@CA|GqR(LFJJfi -~fCHO?A-OE=oxgfUZ&%o5n0#~mLdp6&V%Y-nwUC)3&Pteg&`_0mA>4vFYSP9snyHZvInxUJg1xBrWHw -uW8nJM(ZIV6~gY(+0Aj;bC_ez0h}M>IA`IXa-RW2V#<5@3;{0Y)s7y77K|^XMzQL7GsGx;Jh?e%(vx% -5$Oya{AvGA{?)nM?S(IKC$XDR!`Blttq_S+AlZoB!=3ns?Y>F?9d{s?iRE3fJ8lD7<*s-8CEfQyEKd# -?jv#vO;Xo1p55MKE!;0u`voW~C03{GEc4z+ml9J(}u`Eh~m?M-^q^}KN-E!y0^1w`P -PC;TTeiGvv>T(vgLi*VP9wZO9q$df4K!%@qC=3``#F@1m#aKg&pFeVXCGGha) -ae4A2DJOk^b)p9{hFvHOQ(gY^cvRjxJOJra! -JS99umt&6^a&J~u7A3^*_zm%FzyLNka>^Dh`;8?CawC>@3i{ZnTrLT9Z$nq#vB@pHjW+A})uDJ6^Fk( -riwmHYt4(#e!O+;h;#W{0Oh^YAL#G5~`K%`%;^iv4z3C5VAZC`7d)JVYWe!rpL3S)i`-VAEQk6}w&Lj`t74+~87TozJKeCkgI9VCw@8FyQZEId1qgMjDVSeIC2c%T;!JS;VC!IH5g>it -}Ndv&SfjJd>caBeqGxzcj>?u;UbwBywo5c6o2|3J;Bi5$V361LiI9&C?B^4i9l}3+FLgiSRa?q`7t-O -;48~!mjuGaR(F8do1&zZxxQ9bPt`TC04PpJ2~+W^HJ`8*nUZR@Sy2j7e)BV -#K%iKF!rVJFK@#{^q}(dCpWBot8L+|HdQ;2G3 -nuG58Xf!*BKOjyhgWl`m)pAooDD&-YEu=GQ7jFzo_3f9z@chj=`7?iVuoQnI}~a>W~`=SGq+%*h;^k$ -{||LxFMcR&h*WPaAJ7k{0p8*NW$3ap*+rkj($sfgDQje2K5H_kGtOOm*hjh3O~V?G}U!8Y@q#1N!wx0%8+p`5iO!ao9~d};fUv}%~^t&k%qj#b%Q9uo_cp1YtX<&ixw?fv}nuvEunZ8=|MUN*r1ff(8;7Tla#YHlY>q>t6vM@PdEqSnq$sMU&+ZAdB1cCq1CqQKFg%Iu? -OSE*<8u_aOufpY3!4JmR!ITGaBDHeQAyvk6Qd-ZJsMtO!YOc+-`;`HvT2VCxmSvyk5E -OJ9eqf>F{IQ4QkTlKIrmR)T#<~Wg+COU;AYn>+duulnTU11E8Z6X~@Bvp`GtQ{!QnL!-8>~RRg$Sr@}^1BCCMN3Y9V{X7FoGf>I2!5PRlU_#X#- -w)ksUW|7)n{xQUEmZ$Z%_z*%Q4^IN=LW-ro`{y4SyC-=fgl(WeIjmU1VSP4PzgM+E%-TeZ2dDE^tIqY -A*;JJj|qB#UFk0XLd__6DC8A;JslrSo*dn8NN8qLZ5&sXPpTHJuY)_?BlDiXPn_&JZBZ@ -^#0B$PsqEeJ=Ji6YTuy>gUm! -*1KD|v_)7Y=!LB3&`s0<8)v}8%m*U0B)m_8dQ`ELEL`?9v;@UX~XbqOc)#4^6_=N>n8ufVS=e?hAlKK-c^q}K3dG|Z!Adp)0nDeqwm#H@)6mH01-*MgRY3+&ny?B4Z^8@kuTapCwves5zK1( -qAEe`0161DY7dP8i%#VsxYzsR>~YCX=Yf?XRPIP2snEaAP$b@8Ljoi&N#@;%M;=~?wqOiFgV1+zIEm#{&xRjQV}L3iR&a -0oaAr&P^pS}@v!yj<7&3>tKkItkgvh`NV(Rd3nCBq)RDrpDh7qF>O!aHsnSBM{j_(8uIgt!)|9hg}pHVM=dNVc%$cu!SkN+^J{0|2*s(Z9pVT^!MJ7c3{O&k~ -b25iGn;)kaygw?lN-@5x((&?zJyLkA{MDZXZHY){2D^FFmW6E>`Kc^gfdrNl=Fg$tHPqHt&*u+U*4<$ -j=k`c{aCp(OXTI3rs_wq_T53ltgi!D9#g|dA2jY{CN()3w5vPh!S;qqApK_<=yjT(08N~a}-6*{Dsp5 ->)4Qaqz7QqhuJ&<>gZlc`8;KFHZ;GZ5I{s?p3MjGu0(CV<`7yYn-IeSq18;ZqN@%@2M| -J@x;}Lxu;((TV`aTUo5sP6T_MwIY_4PQ1ppasCst>!}a7tW?ax*p1iU@X6)3j_p#suAVqT5c@+P?W|x -W3;04!YN7B3cd%T~PXl0j64q4eOZM?tWx_JXGC%xn7;egqj3U$RVsq5hS0A -$o&%HHQoJb195O{zAt8Ukk7sYICAQxkDLQ?bQ!oBe?CK{TAIq?gvl+3S>06}$Z1u{-Ksv45# -#8VWDbrzQ&2Y+4p|{5l=p5a(z(tuZ0WlvJ7wC-}Y2qkHYpdZ}d4z4lT&YN?kGi6!>H6_sF!sXA;%BB{ -$GpMyLio^R_MP64m`)R0%D@=`LAiRP`s%Owi8M9P)jRSKFRJ@%I@fWl`(O*DV$OAziu7U5N?7pj7V)l -K5)0n-x{mn~wY(LW;*ouu>{>CQ&1SUq%J|BRGZ-gzA8P!0pfeTE0YHYZ7pg!D^knR60J`na9X7&2FmZ -cF@(rI^K~*SjC^OYC-5{-mRl=gU(^^)shy@-MtSUtk3(hKw;0OEgccJ}11~yb-$BMB$K%y$x*8BqSn) -rmNG9K{brsZw*fn&86R>?m0cpTs=H3oZ+_X?q>i1)kM1w)chClXW{dmslF-7)G;nexBJ}uMG`s5viF; -(g@XgXJae<(Y&jP!ad!l3ye2R -V)LbTE-DE)~)9;aUTl5RH7-75GX{XC?_t=w<1lY~dZvyk*yeU}>!3o6Kt=au*29}q>mUO!=yc9rDJfv -TruiTZJ%laVY&L+t0>UicEk5M8g -=e1PE*W-Ggg{SJ@3Gg`7ZXyNo3C80%`rUP#ZstRGAKtT0(^VKh!y^^)c@9A#Qr&hZte4IUD;Ek*Fbb(eK!nRrlAs|C2C7Vm$7bt0W3flM> -xD1DwNHHc9Ot{_f{m*_sSgRxy7g}G9G4nuhg?nKT&G_2sCT{aB||54*=5{8TZ`Xy-clwv&cr2QoO+h{ -hp8`!A2rPU;6mlCiznWC8V|J&a{7)Di;>>TI5s>e36hy6#ZF_LADZZCZNBT#V#iLRd(ELIiUDEWOxI0 -Tuw(^GS1{I`LGmd*6rId$vNq(fZ!p`~*t`!bAt>4PvZgb=lUbQeuw?cOb5~4+6tKW9t9}~o4B2QI59ftZqrK#m#1Qxj;yUks*nY_{BZsg&(UcEza}7Q-5ta|KbE&3$oqzcS -0vHxKdWqe}T#_%2eL(k`h{F>Bcp?B#72izxL2|F#jiHth`+xM^9#-PXQf>()NGEOA$I+4E1a802Dw*; -In7*pU*B_xZ8zc=gY${Vd&Yza>hCC*!ZZ`tdlNZ4a+wH|aGoymedzf=X?43xw6IFMjnBLcH -CG@iUyv-lpC4!k0iAC;V9&&zr8nLrCA^s<($gF41A966|4*>^VD>hyk8q>=*6s2XwFUQHZSDbfEO=%_ -R5?OlxJ-`i^c=z^-SU!$){HD0ok38)u&-9>Q)f$|{{}-Di|*87F>Y4pct_anm}O2tE=90nJIGH0i!nu --@xDy4NJRH8lMA_o@G2A{tB&UuQhMA(n#NVd)-^(+`-J^oBOowsB#;AMv&%8V<;<+r~EU_09a --q>kp`(Ie@F8{#{?7ZQt{B1XpJ`H^2+IrmP)wWQ**&ZXvx -O);5rq$v?%^y~zq1&OW-Z4~qI(_WZxhvcq8d-M;%toL0>MZxop%+5KfzeK+kg&oQx0rvH*cD^Jq!I3G -7kvfhUZL5Il>(z>hkV+5f59$3Yumqm-naTXk>)5I@Yo7_M&^OCi+bXdC&n+Kwa3i$%uo72{)uW2~RP| -fQ^ScARc}}3DBD5ZtwO>h6yjk>4`2qQKKhn^nT1d(-mEMqDRlvk>4^b7?W -_!``uprlJtaN2OAeg_b%-l!$b^H`r!C4$Y2wNMY_MymvhJypsUMUXU-_>dU0k@52ZWoWY7_Q4AM0niZ -&5wB#Ml2A>E!bGRuE?Yi0hz -PR7;F0YfQvsrsmXP31^LOhI{ZCoA|3G&OAo(2yPISMVi9V90Dsg -|36)ER=Nj3l9^@4}#?T8ytnqM3gfc8t45`{cdBME>YILtjK0l`e3lL4x -lmANdNfPR&pQX_y$YmA`Eu%NA|A3$DWhqE$+%^m{6(UBO;bMRTga%S85J`m=0(VSqzMZ%6eb -|x{u63HX1Pkrlg-V9SmK<#-+p>Bw9+MNs7sHt=r#0_sWG&C<$O7Z&H~M+)OH9fR;(=7AX}u6atEr`hf6A8 -H085-4op_mlulq{4il&qAj -l&qAjl&qAj+^bTeQle6#Qle4f@DCH+D;GYYWTRxGWTRxGWTRxGWT#~3UY!!15}gvA5}gu5m5 -F0I@jE98o=GNSk`$Qb>}QzhUb*lICBX{o>|~9cXxJtiwuy3WB2k+N&L%3diHdBZBAcklCIYfq&N)vL- -76P9p(I#A9X8Q~O*CN>IoL!FHj#r(`vI$d7qOr(Yj6bN`e*Gfk|{ViKHei)~f -E5%O6EVg_TrtrF&DOj0P5l*lB{ -F-cfV@)47Kz=ZCf&?ggGd|%4Xt#|t+Paj?1zJB?y&+|WDZ{Ggf%cuVrP)h>@6aWAK2mly|Wk?%^{->*M_z=Lj(bT--9t-6U~SpxCe{q -)m~CgEq#rvHhT$FEP%#o6i6@%isbRE!XQpf8T6&R -XH!Zx)Jkrsj5!Qb)AD5nx0l=SL&9p)MvG>x2h5KQgjo2>iXxZYv;vQv2M${YW3GXjd)ta?Gqd)ofbG@b&pSai&^5Q}J3=s?n>t+}*9qxo&(`&Q;YaQM6)f%Gwo4>c -mXs+D$~9)Vlj#x<3h3>g#6g&#Gy4iAWA4MsqzzLhE|3=*;}JHn*nXzMk}>SZi%GXn8Ox0$S-plvUsDO -3Pk<&>iZ9K4@5pJ0*6lTJF}7?m^cTzntE@xp;RY4$t32)(MN -sJcfzaoxe&b_sEZbAb6Nt8=b)m{L-3#eA(x9IoL?V~EQc-n0K}>$Wph-yZ6=aUx~>T&A%U;@!0_dFr1yK7 -aLDZ9L(#@)~J2c+s;E%Y;WskF`J#gdwY9va(i@mbNu?^>b-a&{(JW7;{4|Jujd!P=x@*E? -D+iR-Rn2ESI1|!H*chR_;xSGv?RQ-QdHpP8W6(V^)UlomL -)!%ojnrrD6cWVuPBjIYdxzqa6(o@BJCFV+lwo%(gb4I1PqwIQH@7q0U6Ytc0-8|0zwoQ(7kA_Rsubm! -uZ?+dl=9NV8zvLxu9ztjGjbk}8+_n+{S_dTV3Y0`g^{d? --)WnYr9lFSLqw6ZTdE88+N6a6i6_^opM+E-#HYUZB1AG?mcm#CFZ-b)Pfg47^8)XTjr@GWz{WiNlLr` -XBZyxU%rzCKCR_L%oSw$!|PiT=NT*@+T1@i})=8%vI4nz>ibB%M21x;|B+b}MDCv=u^lOq{!gjsV$pR}k@6ZFu-6Bo)=(F#M`yBW6?FblOdWk|83E1IZkI{dV -Yzd=edD3&Jbicu;X++4Cj8kn3*}pQ!vaxGr?cM52`ei^5N#94vdHF!k5q!{EDNh8PNS4vOPvwG&O^DT5$P@UW5<3Z?wL|IpVWqVD1xFnsY|C?85kI*k$WtgA@hi{0uUE_ -co6LaIJ87g_y}hJl#EN1WE6XT1V$juBqC~&u8YLu4ATW`NZ+4i809Vj+{>j(5F-!fF)AYp^{fOkZ}0Y -<+h^v`MhL?cB@6Imlm@m7rDtveWOSiKQzlcC>TgeBH1*xaPHjLBY(9-rRxgR*nKMsBm>^j?$&#oFjFI -?y;1(iGk5Mj7k|@WdWEPK51JVG21I1v~kVh$Grftd4>ai8EIw3%KiIVK_gUE@SS;9WYz9vuvX=VN!+H -;EzLk;+P$*#$W{1RL!^Yzjrf^!+LnR!`}6wwN@9ER82>d8@r8$IF(=pYY1f!MMkkCI*TohG^mULZwMI -pR?}pme3uO%OWZ@&)aH_Gw4KqX5LCDR-gV*C*b3`OrWWh0RHXtp@$h9Kx+$;FE}kq{nyYnJ9Wfv=IB! -l!6kPsPqGKk3%ewM&xhEt}};mit)*9fTyL$zomUFHl!!%K_gwgzGldyG+7GD!bw3{AY-CLaATY}oRepw&0Xq0r@l7#Oo_Mpm`gN_jfl#*c}Z`-SbM@$iTEgtSfK&F;Y)(goVKVZ;t>EZ4O -2RtM|01k#HR`40(dFn2lyT1AQsRwIc=hk-MQH*u4b{x{0P4MgI?XM`^pg!mK@R -?Or_8@(YtVVOX8I>abo3<&`}wA+N|hXEl>A^zmMDhl^_N=HU;9B7>~TEoZ?ogIA;k5Uvq^wWti4KYU; -B+11dTQYhIom4CFqTwxv^O3}(Lxzi;M+)6RxU-TD4w$mtb`K4XGb!y2q?7IRYjlMY5IhMmOhEAL8sGA -9<&q)6uW%18Jr(gh7D66wOngg@8D&GyMC&1mQUEKXG0|X8#ltiL2)RuzCxR5QDc1qHRYrrzl*j=^Sot -7>8Ba);Z=H#BAPr7kaKt1}3Tg5PkwKWH$$h_8WNDbv8AdQXzTv5IOe+p13Tnxi4bw!C%%KDa0VkX&Z6 -L|l4+x*<@bf@=`Cy`%mog_JN*?46>BO%bp|o`g88Zobh4J=IASC=eioIfZR3AIik0VkZkEp`HnNo&%l -tqN!Fv2HynMSnVj3aUFeyV8*d`P8=Wc;X#B=y$Iz;>Ei%%5V#L^uL5k~n=Fuc7* -(@L1?rI#I~$1w`ZLOy`P91uK@@X;ealWXvvjp%6{WkU%(tVN_BN61it!r{9xAYv)ni-EQ{01$zO=_Kl -55IxaGSbAhoIDznv0yuJ<8CwodmJ<>)KOBN&$tHk}dB~$^&%(h_^|`ma+On -xE2@msRG{vJitAQ{tAMz+s1ji>uH5`NBSspLJkMTJ2IRq$iD+3wAFo^JBf-&nkgn?}vCO0WkBV!5(pPNpYAEk^l0yfGfajTb -+^uXYO-B7-vuS$Yt@QHH1U{)W-@8ew*zROXW?P{F<{FeQvzSz{mJrl)+;XMj|BMf_*#4DQL#^DbX -@LfOm>2XBH7v}J>lgI6JY#JxBc|=c$-NXByJk+3Fk7P?3ZlcMNNcP!U0Jeq%sKmGAOU#B7C7S1KNSll -#LP8cu=AH@iknA&@X&8!-T*)j!RiQ4ts~z8B42Kwodz3YeovKrREs1Dbl1G%Eo=T15vCQ#}OiFan4lC -tP>4bYG9&C`$;ZHr6ew+C&ocuwg`$FbbT9Int*irD;A5ne!>{{ -w=Qn*ungpnTikgBUk_AMRBQXA+u)=Lm>4l;?AaoVh;QXEkxNDuCTh3~g^T0 -lkH<1GM9zNao-?KsJA6vGvuE2{P1*8%oGAg}6PNN7BzP3r6Mk1DiRgwSj{t~<5HLd8^o%p^6Ow4a6pn -Al615#8`wlwdPc?EWlf;m^%~>fP`{#Rq+?#^C9bba~_n0cbGH3TAsTgBjbr5+V~NLTjR+hegp&l9dSI}&G8ykRA-^DKVr2QN*kl{z}Ht6E_KZj&ppAk)F>7<;MXtq!DKc84>>upJg8 -23k`YHE|l?TxM$+@6y9&5X^;!R-3?F^xDaei%n>HghJiC~G4hz#0?ep>e>R)1i?$U%7u%vzt#aqw4y$ -7Q*p}^&Gok-2)Kc8umQ~r^-tM((z1+9u=AZd`V9wBWd%82!wgppFZ#Oq}SFEqpTD%ak5YI$szSD_4oV -fJOp83OgVqWj`^qMxZNBBzf5Ah9Vpa;J$Hrut*Q}_^n6n_wd!;5U}aw)WcI&t^d_ -5IFwO=AvB-|d!5bDFRnb}RVQ;|6>tLEldYJV`eu9P|XtQAnr*Sr4g@v3N9Uw2AXdW8!$oZlSxcCrRVx6T5StP2mSHjZl%BI!T -6&3KHCFvs_{^?iqvlwRX3u0s6AjTEI3*?vTFmwPRsS7N50lgh>0F97BHx5k6rs2v);^Fdi?15=vi4zt -yH59g!o1E$JXbox^DYJ$8NN4Rr{)e<{r%X*j_q47uZFToa)}rdWq5^%(3sbG*=3~NZ+VGC~oX!Q+!fl -v(q-bD2-%{)kfx^duxt^KlZEGmeoD$rHAC8D_7ebX>1d8D1R))e~IMde$Z5xy+Fh8!ct%R%fw*JUZ#w`6wmucPqi -6yKu}X@(;JVcC$ncM;y+A_{a&VflRerB<1W|~JlX4|%gKsl(!TCr#OOHq^dM{?$0vrQo~(M;sN&O?X6 -qfphw#|J)wiX4H2FurCU|iyZhDcqUegD%e0&}A)u+F^sHA6M@`Aga$yW>*eM;G| -J%-S)`Mm}LovmAzB7LiLQreXa-Ddnn@_E!g#Pa)aEqGM-{h(NDZcj5@~xkbwGn^Z7k?ZM<2xJU*mdpy -{6HufYpCR9#`1nHBdt*v8eRW_MQlr_P;V-G9;kA>TOkQ3~0Z*esg+qbF0VsZlFU}?@Q3&^}c$ -o_RmoCrIXULLLksAOHBmkPj?^$GNb;dbEs=o-FGWrds#NEoxWZk{zio5#+b39;hMep*YBnH^9$Zae~W -+p;bg!6qb2dL$MMwllcW6s_Yf&3Lw?=kGcv8e{}XyYIeen`C%Aa1yrat>(&W1@ppm8}d355$Yywrwx5 -f>0{I6MXIcWj+^4r(eCByz(?a8afw6x{W2aQCL#EIj0BXYBN-k+7l6v=!1QRU~o*1U6^O_`OyW{vCk- -O#M4``1tcMQxOP9px~_$vUd*1GFa&nHhcOn@6aWAK2mly|Wk^U;35*O2008hY0027x003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvC -QV`yP=WMyeIRy6WH39!6j_B&gDDbxpYJ6)vNv=@&53`@u@gDJ~%wRJQVLw&JO-524 -r7(uf)}z7H4^U8>dks4zh(7aUnitf5u4?;rsplDvjq+ndM@h#5yg-95OqeQ9VxMGKS`_`m0W|rOrimB -g!6|kU!OBF^`s-nT@lwz}I}4<<%D$7Ux-Bug`NGm3kqrA1aGoS5Z#3mJe`sSBlpMZ-ku9rZCYfz9aUN -L|l+;Aue>G^RIeQM{!FH1^*gpB!I(CiF+gSEq-Um*VK`LhOt4{fn#PgHI>>7vlWW -#rfIg;f@fOS{sUq%s!GEif>@@J_C~rT}E+I)S-VydJ6zd7UC}Ys*&coj!{t}LW?XPo|MBROK+=qp<)S -rC*m8CW~DE_(4`Pw&f&vu!JL-lChRUqe-^xxux_ASd7m7>}c0Xlg`_znyt^!aibL-H!RPV`JVgLgiSG*ECy*WCyQ<8L0uQARkqK^3af?(&zLYbFKx~F}3Dqgg$?+hX@fytpo+eP;1(FFoO=x -WjRYHZ&GrYsQ49GO0f(%THU0Io#5E^)_ZsD4d3V9B(M(g-IgE<7N5bO||U6>&+Fw?6BUKF@cK%{^~1q -dvd;M&DC#WkzUOc4~g!EC?zuJl9WW4K>R{d>_Cd31-L`=;DT8}>UfuRrWRx -do&=eJ3YDFr3!fFK!z4Hpid3jhuuhFZ0gcFPjYh-FgqG<7jTI-Lc|ZLtepCPW#j$*Rt!+^t7SYYMeTPcxOK)fD0rNK)5LDN -_??8a0!sno6^J9;~O*F5;UaTaplrs^;XKSal(~v+l@lCpJ^*`NM7g#NkHwX}F=P`EXmj%y82`INS&+8 -*cSf!U-RLK_={cmX$a}7y6HtPUpJV1B&2pSigyL93sb3@q4q$&McfMdmmjlVglh#Fp*Qboon3Xs`L}= -s9CL@>RTMzaL(rv!A=k;IB!PgNtE8!aA{fwe)z~sZCvplQ#Y@mQC92Uvic1Ja==epwY!SK_7Ot&C96y -J-04bg5rXQ)M+nG;7xZ|NYYc(kcejH;t%(_W$w34`ji8@{$9$WYXRHrn_}?QKW?rpp#1xRK=v7Uk2N9 -?(*Lv!}c&=lY9e&grfg0jKJ{N)PpmD#~618{rG!FW6($foWZ%UstqZ;$de=>qCIpF=T7!ET=B+J6Oko -sa%ENa>K+PV(Hb^hC3T7NSL1o_??r?UqqTY -EXo06lxv4bcXIr_JWNxY&=VLS2d_?FAVHV&+5i@HWaG~w-;Wn4uUo3?({SDM~WsZs_BwPvarc#`{mhe -_rHQQ0^S+wF%J4WZvcT}zIgnSF%vZ1QP&C&?`7&>(+NQ~{pikxjZ=9${?{S2rx2gNlKHoXF8BSJmDoX -0@49C1H9Jv_pzHkwIAGmqqgL#2Kf9ewCf;f_GIPTW2ZZXc%UzPJ}|qEJtYOc*_mx68eBC~p7 -PB>-cn@`^;XG!w`4l=!1kVvS~BA{m>-x28&7HW?}Kn%`HJ~#XMy8VW^1#<#2zE~VW!*8=9?F?%Cc3cd -`oWNw`Y=Kt~~3PljRE8VaEUHT9at|Fz^HB(UNE9GpAUs-FS+5<4tUEa7Nt9P=*703N -vY?pHzC5%?YLf+*gOC-e;XpA=ZeguC;Qn~(c+vL#KcT4uJCn`n$K9n&YF(2Jf^H?c;^yxs$rL*rm}$` -0F3J(N@Es1?qf<2k-Au?9BIlH583>pduY=G*#_B+AK)`*w#(NOBM7i@|pbp&nHwXgXt-tn2ZPkJ41$W -wUs9^_z*+Ezx@TqLKdY%Q2pm!qKd1Oq6%v*b`)bAcYMkyxd&Iq2=)>yX-WPn?=kB`HetD4X=>Kv@wrvsw>!I)utvbEzD;NUaP6rBmo7Jb{98{Jx>dg2% -b4*vT|DQm=$2XNQ?QiwA!tqIIx%*PHZSE#BK4xMvp51nWjOTIsvN3?gtRPbbs!yI?I<7Rpig1@YW(4ysWI?8b=Hl;{|4<3rpbc4|JYmYQW`Sn@!{D&T6 -vEu$3c|^UV&|&eM#L&;!{%SoDX>Uav{Fu4t0q)%>I^Xe4aY1gW(t7WV##Q^Z%Ro#yn}-JfC2mP58Gb# -Lsi*`L3V$#OB9RebQOdW6leD{^4U(=J9uEbsv7HB=4^tmhn7F&a)z>2PZD*Fr*?lsnbI=(!6JDrYaZ^u7*|M9%vTaT-4j_r}hcK_e80p23}R=g#QRdn> -gN)mn1(>;-9e?;DY0Z>Z=1QY-O00;mWhh<2dp0{KD3jhE;MF0Rg0001RX>c!Jc4cm4Z*nhkWpQ<7b98 -eraA9L>VP|D?FJow7a%5$6FKl6MXJdJCX>4q1V{LC_Wo#~RdF@+mPuw~d{+?f9U9@T|rL66cd1 -MHBdDB2u_0uZM1c*8$bCb|scD4U~pOeI~op=VO%vGxjB17UFAD@@!Ja&f3)|UH+z1x3(c*2ej_x4ZD_ -u0Fn)4dODiR_lQ#V&4QcA6*GNgB=BUN(zaQn2IfKgoO^;rjOWE={IUndNLcPvW#>Q^;(4YUp8_lnF-u -9)FGJ*&@zacEw7EPRO6cWigEwv7t7}(gK(HBFn1_C}w9_UiVM)I4a{AyS%S7_NR(+(zUq9R5vAix%Y~ -R@puFkPVsHFJD;-&$rfx97jgbIp4Fc0Woelwmv?aUW%23_dKf#7@-OTxnP2D0Eql#iVadvjo!yr=S=# -uIP|cIeJj(AWYFBw2vm(1HzrnD#>^{3=(Hi_Beq;IQk8+VwW+yE8?rWxepDHWuFc&{y6=3 -!FEqRvroH|$?nO;=eLkWq;VJdt2HIJ3qWwj;8h-_!p*~iJ*>G}RPW9M| -IurBznAyv0MtlUyFbX#y*~-XW0+iKl%NUq_X8f@rTpH(d!8ylV0CN*D+x|o>QgWM0qqN934lCMFO`kq -RV;w#-jm~f79)Y>@-I?;6D2od+EtA^dHxDoacU2cOx}|)CB%h8mW<_M*5Fy%hd$6HDX7i(cc}k -5N&`qL>r-veGzgrfhK)6gbWQILJlz^H9OoBAzflvh+!dyg%~!#umOe*pnd@L0}LI6ORWiOYuFG&hL~v -pbwgZZh`_9DNY@A>sj1F~=%WVp!x8$&m6`$f*Z@`xaY@}!Ax5xdBybL^Mi3uSAJmMNTQh`HV^}hV_!# -12h>u~(7&DKdeheqaV_$NPCeRRSj^P}*;ZVy5m6{Qp;zL}H&>$`#E+8%-E+8%-E+8%jXpj?-6OfaTla -Q0LQZt4c32_N=Nil$!#1M%glFl(Cz>ok#0;mok8$dRI>Hu=Vuu>C2O#tx#;yV!EfjE*ck$j2dOC(<+` -4Y*ONWMh!C6X_Ze1#=abBPd3(uhkK$zTB!bPL8pHP%q{k^jgVtY&bSF_Ix?s1QV@W(qa%oe2`9j05?WH!U+cWF*wHH03(Cp5To+jAxvR7;=y)E9`_M17Sw8Ds0m1 -nhR{q9M~oPO9>`_^K@s8uy2js-rb7d=0bO8=S8A@f_sa9WC8B%+DL#Uox-@5H= -R^TfVX_r$=gVPW#WLpIeXCKQt$+JVtWC&UlL)Wi|Q*2ELU*u)iKLo`JBL_o9@P%sCgVl0m+t0E1eZ(< -`_7l=^8hD2yaA{V0h>Jy;;_Jzo0FnA`HXL9*)%zTk4l6p-6NFdEbSJjQMFR`tWT1>!;X)A%|nldrKY!nZl+7(clyi -X=*pqV&49m*|iC!2<^{Vw~51Wz&GdhlZO6uZF3E$bPkZsFqf6J4OnEkZywU;YaXoyE*q9xAe)G6 -#sVGDWuaS^Db%mq0z}LFtyeh5x$8$l_%cbujgMg3$ECBeV@%4rQvN2UtxfGZ6vzxKC+m -k6$5S4qb(4Rvp^wS_F|y3pw2{fY_1x+(eLL)McusGBp>u^KstJA9B5QHVz9an*T8{nxR^K57EO<;1<_ -curUehiKw(VHqN8Eff@rSRLgJcPa8;wqyE+TpNc^rWXykW~ItnD_LguDcralYRRWE#QWTAFXlfK5qCU -C%;Is1O9-19b08W+m0O;{jX3uJ?Sg-G7cV^^bSt~SL;q7wtNB7-&t(uxpoqvNeRqr-we4)pq02Uc@}d -HwhBK{pcI*DRs(A{r~47OXxVxiabMR~Zjl9@2bJBU6cC|lJQ|LN06l@?1vRws~|H5EB -jYQGbg}F{_YNu*OK(T&cL|?B98jYJmqRok_(reVI---uCJNr@#zH?==5vZb}GHzpEW#PMGuw>W3yJOcZF&aef4xsRG16e6zbTkI#{V-0~YA~DkRu7!F}GwKpN-unPo1%1!$fY}i+VxT(BU9z<1rdj -e-3{V9sc(=p(Vsg0yCI;muZJEv`Y^Wg@};xJhL}MM<`bdRcHmcllM71^ryot#w`ffg=i54BW-i&AX0{ -smANC%_h#eAh*N3*(Vm0PNXqI9fNN2bjTYVm{s0I<}&XaBnktGyhVeZ1ye!b>u4BRu(4(iksSqHrtlQ -o*leh)pIde*{Am|GHwS9p?d?P#9iCnNE?9Bj72?){ZoRu8V@9X@*Ap!(Yj3r$3K@MeX|rH2RCX4+*0| -IdjASQKMMdvU&AIiI1`>72n3=J0(B%ehv$BeTLi+NLXblUxKD)2nYItpdy4_!$&9EvNVe}d}uo=&tbC -(m%+Gwwj=F@@Bhgn(*UvAM5W4xwvB&>1%1r95!oNy#6UlZb~({>z}!8ViSF$a&G~Kq>-P3=pwOHzc@B+0ny+JMM1tD+RAS??aZ1a`lab|9XtQm|9=cdhv$2UgD)Mf4 -o@G+tD-ns>5+q=E+}>?S9RnlDoaQUnJ9LewGypeM4eG?=?f1Z;yxc?Tbq2OB^ru(`$O?{U|QWIETPNG -%sRb8CAsPNpu^d|F`-@ks-vRu=&cLWy_N{nLmQ^ktRqql#&Lii0M?cu1!}HVE -qft119aiof>$ewH`?CBshac>a;Q>5)>v@Nlx1+&ewKvXhJ^tv-R5n#vAs52%VHbMe3jYaQ2y0+%v8#F -XB_6$DY4&I2{U18v@5g|hzWeETPR51VmHvj+taA|NaUv_0~WN&gWa%FLKWpi|MFK} -UFYhh<)b1!3PVRB?;bT4dSZf9q5Wo2t^Z)9a`E^vA6T5WIJI1>J@UoqHVu??VRMaqut6!#&`Cf#7uBy -ds`{d7=lIuWW!UP*51JKTT2hmt9hdU2gbi+e#MFp|mPGc(T&hqR%+z4kwPd;0GDk}l3qPA_jx>FveU$ -sg1qyXWlD?Sr7JG`tVvAfl6GE-1|Cee#bmiUNFod|buhEXb3TW>F~OoMw% -B@4=1Fd=^_@@y6?g#jBTafXlSGD(XM0H*6CE&FGw2y!u}yLExsR28HWYq^H1hn!xW>=T!#1EL#wn20Wv3?cg(tbp9DJSm3= -ru)%Myh5Wv{Ilwol_VWun*uK4*-OtPe?1e6ST_2VimbSfGmAHzy1B{!z~?Oexi@Z)9LZ$?WZ@81=AQs -{7R(oafuY16TC`;IA6nX0K7k)p8Sp($8XOs&Tl_S>(9<_FHdi7=!>y*)qqaB)1P>krfGtDDm -Uq8lL;#gfc6k}HZAfP75AeAaJ%cbc1W6bq9W8Q1#dyMGA4SKr%== -Acp9gE7I17JQJ9+k@~iD>o^q`?fyla-0E%2q)Xt!bW!Ogc#7EAmDrj7+m!e9a4l0T2-CFt``LPDQ>-< -D%O?qHO(m2i#Z7RXodAL7AcjbOm`Q3R1z@QtmKKb??jn|b-rANkh~4K3Bx3e@4lo-7%pMByIbpq@k?uH%w6VrXakofvW@Y~T*eM@#~8! -&c!c{1V;u4rXL4TY)WCCSG(ne~hORM&4yE`2ZGtK2n_%2S!_+Qoh8t@}&@l1imLs0F5s{z^w&!f$>VF|%JNqY%%=Q*f85NEPvVG6nja=OeN`59wKJpwch&?jMhs2Ip7q4|>nW`;=l@ -NXboOxfR@%1?!LSa -3K#)vNE!xoCcB`C6mGsX_lMiNe{0}LNk6fZny-*KM8n4#xKHTqGF{?Drs%0}PSNQwNYMm<# -{oVD>?E>b5Ye9cO79A`7}=SsvgksZMT?`QNc!W753A>-!QWPHHKL#D^E3I~P&I~aLhF`NpFnp5SUci{ -QMi5!l_Jt} -M_xz3M-&J(X&-4SuD>d5*R=+Z?aZ(Z+#iJn>qOwyUz>1sre-vw2AgR@1Hi8+p(}6ug?=W}i8xSSdJQJ -6piESx};?0i}s`SX2SDMd3MDBNOYN`6fp^D^9$!qL0<+TgYZknjADUh1+@1iv@hcf{KEv({okyJu6!M -Sm4$4Xf1lob9Fu7^(fTy>$@uwcRkDVX0_t3xf1*1z(lsIkg8UuX2k;6tFeAPlLI^UW}f@j7`J-(ru9* -gUQFWiwYEfITTRsbm|4kG6#N#{7iFTkSKDLEHS00oEP%ew2lblQmIqqD`rLUwXyfHOqR^g++JY@>;h7 -ecwYPpkFl&;@fpV4?v-(+$c1<*EPg`!9RoQp@uF58XrqA`L_^xjPyx4+zMY65o#@N=1WbO^$y&BC(o0 -(^4AOFiZsPDzf8dhoN#{SJWj`o`eX65%Q{eOKP=#%%h)yOx;QEqV1c}Duqy^&W;sLJnKV7_Y}idk{>e -M)1lxi0MUijG3*J<|X^SkP7v)OzzPoxI|N8_e{{fgL|*?ddBI6w|!skZpk-q4LR5q^J)4EsnWfV`}{y=EemjtoS!0uQQZaAPR< -;7SyBAWZ&e!yjxMw`>`E&4WOK46<7xcCL1OUYgo{ciQgH<7XxsE0ZpRs+7(ebRL>E|WZx`STBr%ru)s -Y3*?V8xLbAaeE!>{8ZCV>7>QlOQq&d|Ns8N+|YM5oV=B3LGD~!#bSn3m6e}t*81#e@A8I5ih9K}4Z)f -zL5rthu$LR&rPX1^s5xRr@YXLBEFmXp5Lf_XA*X8*S7tj+?o*v`>Iq=jf%u;n -wl=y$<#uk{gvxm2QH`XZ(Xm-N#2_1wM6o(MPsi)eK*f9{r#54s|;(F^(Gsv*4I)KZ77`y-1yuQg*rv` -bAV}w$xzGg%rC(}8mqCnY}hUwbniaxdxXV-iMfsO&b82 -7?{#fWt7;asN5QT`U6;W@J;LgL_m*3;puT0)OKdxp*6#e -@N}E57pl&jai`o0m$9cN5fm3IBA6ewkVCb^UTgt#6;YQgkWbFP_WS9o`3d8h(Ya{jNhH^?18phO;2LP -O?zGBQcfl#zNFPn2hDy7lqJgA(p4{y?kfkH^Ew>(NinzZ~ad -`9|6fuC%|q_8;{HCe4$l?6tJ3@Mez|QSe!ek0?(544i)hP)h>@6aWAK2mly|Wk?^QdkUTj001^D001} -u003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy?Y(AbB|Mb`lass};k68MNFKK%;qDnWO=0jGbdU!(><;CgQ_1zhLzPvg82er`dS$lN{^bXOM-QR@Q^g5pMlW8Rs -FcPkRiiYDX#~bW{>4HTg`5~PI0S%cpp`l7t{8No{?57YO`;Sv-w4WRtXXviH_y^tk!E@}t(np# -A6{Qi~&eE4C%qZ?&X6RlJ@qDz=AWB}mF!WyNJc=JxQm3Q+zaPxa2ZIOxW9-E_5$TXc~@T8#Y0z;@6~b!I$d?x>O0&rY|Z~F(BMd9|M1c#h -3m_g^40Bp$U&AFWT(q4CHPO1bTqI$Nu?5%Evs*=HS2x$wN^{n{%|GFYS9N<3}(|c;Nv+&nSRv<6`Auq -Y1R8WAej{?F$@<8!&{TZwO9$R5VT0(=@Qb0krh(;{NN+!#$l`f2VIJ!{N#G{r69xh1)QTV8}W7uam$B -V`vq7VLAhFFt|D!o_@uOlg}5I7x&*8{rScH_1WDWo!<=Uglh``eKPT=qK`@D2!5@-eMoIgHW -z{7hJS|ZDi2s(59{ckr)05;DS3WKlh)&`aFoD*rv}>k}~Pl3D!DN2VOVzJ5xDW0HdT#CO-Jwdyj!S -w%+`ZSNxL+?xUL+X$uPa8|_;dw%6;O#(N^T!`XmMb7wY!*|_$5IB3 -0C9XVngpTK4-xflk^Emnu)(oIfKUb>bSabvu2^Rexrg7~Z*}{?NVA1bjDZ1n0+HFPCoulbO;!y`ISTb -03SR%9KXN-eJIFLMGT8#H_4{uRtkg@3jO|J(@4yPN2LDpe*xb|5|R;rI{7i{{iXpS69NW=h?kN_?nzK -5g^v&Ax}hkk_4_XAr(95NOi^aAuy*ph;c#4w>k2XZ^C2JT(-0T>F#$;50eW&&mcR|#gYrbRBB4w58Hi -Amw`1Qtjh$0865AVcJ1j5`GMSwUuUWLp9-1uxDPL2HghiAmSNLcn#o^E?>4Y$?#VFk~01K{ybwz8L7R -b9oX(E+n{+gvA7A*~z?~AIU=gu -j!2l2mwjg2$u_QPZRwk%N#7KaE4Ng3cX96wMwur+Ph9X#+*g%4rK?ROi7?B8_qlGv&MV2*!Z8hj+(O? -~LS-Ro^R1_N=ilP+zMWX(is&J%|V=!=j$M};Uqh5A}H0K8W!;a&0^gL3dQ(Ao39rOjy#|qQtRQBf_I{ -hP_4%IViD)zJz1*RR*{oVSi9%J=(iJFKeqZbOreqxm?rM@M|1!~U+#Eb<4DP4hJ;ky#V-CKp#{K -F5?+v!vBc&YLCXkThp!(Zm9%IZ}Sw8i89z;AYyH2*er)ob3?gY~@_ARajS+tXsE19f{b+2D0P<*YrXm -S=h@o-oguV@hca=JdMqS`6XHoM+<{T|iu84^R0I)xh*dvF(WFY+M!xW$Ua!Am(1LMnR!&OEy*!)Mc(j(3}%;fhR}j+W$K*ls#@*U)N(@6_?h;HF -IH^{=%?9fl2QPB`=5-2&zfch(K24mB*SgL6}}xwG*y^pqc|s6)B(7;WxK|MzG25EZY#6Slorx3tZ+#A -j>sznY{7|O1H*1a7uCD8YuKppjMIUCxf=wTM-A<94Pid*TA3;y@nS=gBRApvvOZ3?+|rGtZLm`epaqG -t7-vRvU%|=7TU`>no=B?6{#s6R(L@#6BQQAS>+^@ke(2u~g0dCb4z(=|Hbc<(#!|1 -e+T-SHU-|W~c;C!kJat%@QdV0divt~T%bJPEd;t}88*JLFWfJSH{8DCkVR;6uK;X8@Ye$#oqcG!OYXr ->|sZh(zhGr2Pd7+7gUWdmWiMZPIhXP`ci3H7Y97t`O)WX5E#`moE0kcc0dm$!j|ml#KYkW>xEcb*HY%Z -p?+UX&k$nIMq$pF`gC&P4m@D+UDWUjDdPe?ZO7DiIzn~?UZJom~XR4;TezJ6hUnl! -Fu)*41*nU%MK~N1Tv#jV0HEhsS``X_QHb5XtOEm&r4@wAjY=d$(nw#vBIN0{M+Y*W8owB~wXlw1>26Y -6*)}n1wjMm~{XI?1BL0Ni(7c|7CwAE~zk+x*x`YP>yXKLOTbltAy!p;%=e=v|`24XQ-J_S2=HFe4(^M -mHQRjAj*&9Y<7T&OE$oDVjU*Q!Dtfo5wSh8rr9c~UjAvTbi(Yj=K^3L7(M8myj}n$L;(9e+IWE&ntt| -2m=gGU4AA(T7p#W33Oz)ch3}e+2Sj!k_Vt`J0C;FOB_QpssxHkdeLK&nEuJ3vQ#t=PxCO{0$mtox?$& -Kf=g}eke6L3!nMh%uAJ~Dh9#17bMCqP$eq8_Ffgne|s}NNc+wD4}T2u6nH;W|Cqw)FVFffP)h>@6aWA -K2mly|Wk^uBntsO$008(f0027x003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMyWQY`xEpx8_zRFS-r+| -+lt|NR~F|HpU7g;imtGu9Tn#io8DQxxwJ@hn7sstl{liy^T&t*aR9aSEmus@eoIgRJi -a+~B?iOXW17pn_6rpvsj?@tRES8_(T%azCeRB@qv&6k+!p`usEuZd445nOo9_vt82=}Os_bR|n!e3P> -}lH)w9isW{IFkh9g4{?X+Q(Szd%Ot%ol1F+&Kv+?g)8(>y$g?JXP?aXPMO-WuYj=f|ROWZp6M(&?Wxk -+koKYcXNm;uG5f#PR?AN@&_{V&f+%46uMK(h?3Uwum$5M0n;ruh5$r4EEgUn=sXf7AGX)?v&vt%l>Qc -_&fT-_@l3{&qFkn08rz0cu(#qL{@37)IDzsaJ+9TNLo&(?Sx5OV!0t`vS`bFK!y2IwWF$VQ<@?t3XPk -G9{Yqbs`ne06zoeX>t(=G1T#*XSp7LV08Ym8lp^_&G-?4^-I-k!ISl-09sXPqM;4KsX__)aKV@#bX!<+n~ -K!L)N{zk6?#5UOO7<$i%Ksr)aOEZ({K%o)2<1i$wma6l32QVYq5%yVbU4H}fQAC+xE@Y$j%Oj -hm0dW(J&teKg|Hir2LTOnbBvoqWuT5RjByMxa0J5PoJ;#q|R2ONV&xE?9zY7qA$;0KcdA2A#phrkiy7~(jcRmCor7Aa00^#3?~RnIiJ9IG8qWQa -3Bl@G8y1AsFWRmgRy!z!ZF4X;lMTc5GrPnC_tFtFy_!AxDX&&fN%kh1PBg7pJQXxQ%0&*5g)}aa&gM_-~ZIuMCgWxI2IBf -pkP5#nuBL1hl$)D^Zx`_ncE}}|NbrMxIJLo2(ExL(n0=wxZVckts9sk$5iE0%6ZZd4T$wAXi6dKWVlh -EoWs!?Ee5LtH<)mg^AZZdpvH^F8XKc-T>Rl)C^yuyC6lz%K_HkIWeM8Ve(Hb=``Ki;KMqHcd -Cr*_S_TLEVkAQKxR7{i4^{uzeiTiH>TxFTu}Q*~*S28$jIqhjLEGxi;_SYLtGXN;gd0EnndgD;rtsVm -jXST{`E -O;I9Q{^?kvwBa3i+W&z(BanRrnxY{Igp7P{Smcb@5M*3`?3t8t?e{hILA$1~NqNr_?;0(&;Q8we)@VI -#ZHR^s*uo>8I?LtiCzHHsha2NnVoWjEh-+drW0`s!!CDeMhAxJx)x0S#lTgfy4qD1T*Zf -jt|sjLncWk2p@aH{$%4Mp-G{Yx$eKRjZisf%6MZ`rqj_^>N57NZdYc8k5_MQ#Mt`hTgr3!4tGTeTSP1$vGV6ct1hWMKJIB0p<*Kn++C*peb?loAbr9&? -zp1^jjn>VNT?eLLdO2{D)n~-!BoJ+!M(6fyrhNztfj0}b4%}|!=c7TutyNI}7BD(62;6X3J6&Ldv>CR -;az)h3g7GID@7-#2d!l}7b!Edtf3ALE>GOa)-B0g;jVcWXp)=IULcd&H9Ty`{>p}=*@z=yam1*Sl^-Tz}i|cP`izc_2%qzv&HJ{e%u7Je -oyQw(auk|$#trcx2dw#X0+gD!Qwy++pIfLYGj^Sg@Zu%LSbEbbig|QciWn0ZPd~0zmfQ ->&MhG$?7(HYzaMlFxTu^gxLkMB#d}ZN{eiC*w%Z?cipkxF*!=gw+h@D@)z&A?#n8`$jVAEz0lSKOrWX -8Cakc4g`y2$mHKDtp`Vq7tbW`L0-PoP4ua&?TD|62JvR;e8p27ELQQy6w3*SayyIzZ+A5~wA8-?VqG= -2^?B3SoRf;ne)Hr)HW`U{4yeK7CU#w>JoU^wZ@+j8(aXx{8>-%rr*Cj2WP`eo|8m-WjbZGH`OpvKJA8 -^1I4{==uZDw6LQ)qnY6W%+osoF~&bz0Au*y*P2D-dKe>+n)^8D;X=JuTsuWvU~L=_nE9JS-{|?Sjd6K -SIX)u$~Ow)D!4tmOXIIHI;1TBBM$xpP)h> -@6aWAK2mly|Wk^dse_iDY000Xt001@s003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy -z=K+X)&1UcfWsJ> -7r5r-7!cF;1M!X>Sk)`lafrr>d)q4Da6c|DzA5AJ4Do;{4?F>h6?2TwI_0i?)n?=e?upqoV6Peu&d3p -_6Q`C@$zS`zub82+vPXt2CZPWtP({iB($C3}XhKnR=eaWsI4>sjn)@mMW)gL1m9jjK5N4F^iUpiH)cBx>Wg7VL1GF^@%Q20i^U%r7DLtH>>+3o?-GuJX2|*C@N^F+l -oia)R_iy*8rij4B{(x-;;{buXTS_d4U#^17Wl^5`BWN_o7nchc}mc;yysHDS5t>v+vBV0=l2*3rqK)k)so_;B|>mcuquzzat+`R -aCv%r@;hc6e>lH5pMKWp&(5b;r+0UBc7037baQ+=JwN$$aePZRpKfoi?@k9qcS;$GHJN=Rw-hfR@+kw -8b5%xhQq-n@hIsR?O$dkxk~PgURcHfAeMj8rfYD)As_$hbVF(FubsRmYUvE{p%G0VjJfdR#bPu_E%T+onS -5Y0JmFQOFy{bqBXWz1XUQLuo6ksJ!ye4-ovLwmA#Tb>VVwIGYqah_mgdzK&SIu-FFY@dOUQObsSU(mK -;!x3^xZEFTQ76Vaq>SZKi1i~J -T=+x#&tqtc8ZQy`Htxaxk?pmwevqXlXXMw$ -#wl(r2MNBWA%V;!sd3c(0%2`(h~k`OFGrdd7SzD_%rsH+l_YEbfICAOAA{kc-rg?;gOX_QMCunZ@@+i -jle?QJfw4&k&^X!VWTl<8;#wf(H^$Cw<3@XfpE~cy7^T$cb=Uj+99GMNi?L2_Fi=K2bP1-?;?>sI%Q* -`XvJ0nB-1Eb&N=gf>FgdJ*XLRow(hk0xnF7wo&^=QD2(@QgmT>`ZdwzpzHTH+My_bY$VR+r^NjVfk=p -gSXs2!-!f>#Oz{I<7EDm->z?tX#Z}ozWEF(E&zcwO*_TD1%4CetuU~m{D;T9G)IB>nqmzMEIHy%P0i6 -PTzTxJpU>#uQpX36mUV_Nvy;6W8KAzmN5Hb;f6Ci~p+f@He+WH-HTW@U5lxpGZ3SfGN5-ONaV%?I7AC ->nBIznj%*qO@I&eAR98zAY;^k!Ym3+lSF&5g21^0+O{CxVC<;_pe2^7ECe;eY2qit-WDGsljEQMbqjT -t_^LHc}4Vk-o@pGOq1cCuSK4Xxp&NVtoOSE1c4MgeW^0yltlJiH6NGP*Ip39^vwVXC%e*DiG99**TAIz_nZb7Jwu| -Ov{SAq-)#z3R?%rrTV%2rLU>8Z(3p>Su%|wP!6NNRUy!3EPu(_s>_m_F_c-0yBw%oNb$kBCoXW3t=at -@wflfobfnFn~S%qu3dZ`gR$bDVvF*G9rIEZBmkabwup?C!&&X&0^CHb*!lS|bde-PS=Pz)fg&Em)B@# -CWwPOdJ?(ZXLq?;aVTIPo2GyXJ4Z)@cFvrap|naZbk62{}~6yfxJF-hlI}x?5$8`HQ6u0bXj^ESuArsrRS5X&j(sQNu_;> -d^(Y&Qo>>~U+4alz7^&xY)?S{G?E8`3FE+QlYF}`*ndM{mtG+jvjW}?lo!82o7wPmHBX(R1qMfiM4tS -ktQmcIpwZprMKCpG-_P3%JgZo)U(d6M(`wTgD)P6*6OFut`~>Zfte)GTa))6R-EYhMm2@Ux00~XNz9924!OR|9fN;tcz&NE6@K3!5am?=*x|MBb^3Y)?0d~eS^Coi^I5{bJ))my&R1GLA5q`OTfQE-)F1lI^|ua} -QJKfz^;rI)LpA7Wx?aY!D7ndsSbraJtG`jh$Y3y$`pb)I&=;kar|E~7XbRn`;Y$kW06E()Zr1O*`Ja3Urc!Jc4c -m4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6FKl6SX>Kuaa&KZ~axQRrl~`MI+By(^_pjJFypWbs3` -t0v(+6;H%Nzn64DDnx9mcUm(8O3&7Rhja{n?eTTmzoLu~+-;w>K%0OQro6v^(Fsedu+qPT%Q3yEm}@f -djfFqXgqQgMsAFJahxFgwKFSa4-JjLEyq9Cx#FmH$6i?=mP~?} -f!bpl_4`akp1WiyS}ax}_guwA7~!j23aMYvFbsu^<(_1&Vm>^rwZ&a3SCXye8mZ{YK -qD+pjjx)F?6#N;dU<47zKD#@Q#7+*QM_=*yMdsn`|kO$9_FW?iPhYFN{;abf_s5PAV8h>seXk29R>I@ -|b!juO5NxE`P*_}zoAQCh6hMwQTTCBixLy*kpQOu8-Qs9REXCV>45I&!+Nwf-mjF-YvOfI4{$=~}E=& -=Yxf$uD266uGlr+|A1?s1QW5d$}ZCCQ?Bo=Te>aS}(s7HD3l{}vd>apI=(qXgz;ekHS>2n0JW?NcsLjr?IQW1DG0-<`KX_uWir8!wRL}zI!msZ5$6zvsR{sHhwML^>fBbNZ -x>yPv#RA8c_+lAwG>lP6H&knM91YxeM%E8RwA$TXclZDXgNq%qT%T3LK!ndaUWDWcy#+plP^b -#LNshgP?YUE3w364ig0sS_R^ZA+B{zHtumldmZ9YTOb|8eTKwCtLRPS4et<_Dl_Q`D2On`}ll0z4C`g -bQTJ|)ALNt`s*WNleW@r2Q1^=OV+D0G+_YGJB~%Ynj*MyV3(3w6)3(Z!OwCmwo}?s6)mZy5Hd^Y7GZj -i>#|J$!>p!IjJH>9Hk8TJAf`ztbx9(F-!PASD>VUZL -S%LcAxfLW-VawDs?8ZLOX=J~iA!r5ozOyV=o=QD}VJfE*leD(AB>cm$+pRYlDjq~{$#MdbBVduVgBit -qNUB-MTKyn=MUD{{ku88kyFW=Glu88kyBR=Q78{w{r?>gms7n0+M@7g{a*Cf8?UcRI8HHoiTh!5ANN* -d|lFz$xc18Sfi|tvQWr{R%oO -O$#DFyRsT=D$Nhhh{bMcB%phNB!J|ci(oH!gx5yW*RjVBTX)Bp#TqkOxM|(BRD) -BI>%1E15I&4nk?Qm4quna{wkOiP5{{z_P9 -NB$@kS?7 -WppoXVq<7wa&u*LaB^>AWpXZXd8JlcZ{kK2e&<&lDGv);mcmwjNt8;2kOUP7;-XcQMrg2)VH7iiX2wa -qzrNp?J2q+BT8b2(^PSswF8Jc2_@SHjeWy>|POIG?wdtn&*!qLcU@uA+G+r=08abCr#MIIuleE;+KV= -*Xnkf0k#zGS;gt6=>WXFNih5|rqrLBjKl1>74%WoX(6&unKxdPX~_u1J%tQex@GEp)RPHJWGYF650FK -|o)ZPxO@uy7pGEX_RTLkZ)3tx|+qI4ZX)^u1nhz(pavM$I^;p~qSpvSste;kuHRR?f&-g8Is~atR4hP -ncI4$arq#lD-i#bmTM*Qn%15LyQ(F{zR<3UhnG77|H+xqjj{>Qn@P93AU_(=UC2+FsY|JGR$OkasLDDJ3=%LwK7E`dVG$g4qw+n -O(|G*&%eaB4Fj481kC2LHkTrM@3#O4SY+jZ$rX -s<|kbd-G!HZf7i2FrQaF=3H2JI1tXZ> -$q26RVy#V;ZnSkC+H1nL{=Lzte{1|&ZxBsJkeqv{-M{_hMY<@J&?&!UHnpY5V5w@fkp#E~4!zsTH#cc -z^b7=TKrIMI>;{>zR4N6rusCQ*?Bq@A_()wodc(qFS<$D6rc;TtF`bqz$I(XapCf$j7_|fWx7u`hyPa -lm+r&jkeTAWPP~1kT=A+&jbyryDBSZaMuTuS`v`f_Bez}jcJb>&F7>$$Ad+UO{Ha?2%i>fU2RR -qlW{pW(`?53N(PnB5mY3(a?dykHo!wgX+ag!Y4L>G7AnqpSvOtVw?eU<+FUxTIO%`=9W(nqSFnStXqdTzWRQZ|ZMr4I}WcXYQy2Og{23i7FOYzwIVRFL -(u;3KMWDKRXKm+I%{>h#&=cJ8i#hAko49YDmFQtr6?0TryFA_N@<&C^wVSfi$$T-zPp -&BY52LWK#Dk^PDv}|qz-RIMK@y{f$Kkj&8BfO#!!};Jx0T9&8S$NoVV4+{EB)m3?$bFryocuJ>OZNc^ -fypT0|XQR000O87>8v@&Ww*cSpxt7uLl4CH~;_uaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3P -VRB?;bT4gUV{>P6Z*_2Ra&KZ~axQRrl~rMH+DH)n&aW7WFGQjo(5uv5bRUchl$L;H8%5FSgcf@Z+bmv -dcb&-h>+h|B5CfI&EJccE-n^Z8Z@jeIw;vjfr?UmkXa0B*jcGJr`akFw*mkQ;@s86{i>=6+BwwYRgrN -uZkC2klDi<$Y8)T9l(}r70x#fvfni4>JtrNp67ghlFmA`PQ3a&|IWN%Odzu?v+tl$Xk_hl{;W`)WPUU -i{#{Q_bNls?20&6(vXZK|5_`Y5IyS5e`pou!U{N5g)90E(ON9(hty=&*)DZn%Eo>0uIIB>o-Y0k;0 -%)Vlrd#Y4P3Cl_IRG33kkk1m!)8CXv{a&Rat6ZqeQ;6VZwOsGbN%`qwMRRU2A5D4AHF^rljT8xr=OW> -`;lvCTE@pOTeJ8Gw=-3EN6zFSD?uiTJ-Pvv@5TRjja60YC*o6SpVb=AJ9VlGk9h$C4!b5$kye+|b(v( -DA)9&CH{sAEdeZ@}FhEr*HR?03IfThP-{B604$y+?pqZfwWB7V9D7XBa$*AGl$Uq}IBOr=?FD>M#*hj -HkCLxne*&1dnG>wYqe7vm_R$ucC50xyhb{%Y=p6s*Ev8I5~zob!{!PRv^(ze${da<8Byz8of`IgI`Ud -kq?tDec$`hs_Dcr -CVVy+42q{YihlrQCCNsB_nCWX{H1X5QIxnR@TRjk1&Sm#KGpU#8A7-{3om+d0qscXM)$9~g`Dzfem91 -QY-O00;mWhh<3dc!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6 -FKuFDb8~GjaCz-q(Q?{I7JcU{x{8M+Rfv@UftXYtY!A*j!Hx@XrSdc-L}P15mb{X%!*1=r?`^dNx24- -cHkr-tB*3JNPM_v>OFD{IOD`k@=JvuMScpRo -JrpMDUy-6+0g9vthoimDf(pZZYqSN_O@=z+&9TCR`0TtAYe05H(@iL@c5ChWKYhbFpd4?CXM3P$ -(vu`2xFIS{0F=CgWK4@X9j}^Q$W!0rVZxc4um^E0x!aG -_xd>#Q2g1%HXg(X=w~YIMi_d=KaRNtJuWJ>bmbchpQOsVusfZuW=7S)(d52`(eH*3s9GRzF7SB>1M%BuD-C(C$rhf)#A%rs0$+vD4ajpl7IgY_|P!)Dt -5zk3xPuemy_AqZ@@S?y_jAszKH10FBVsm`JA19nz0jheKK2IoZU=MX6*WAcKvBS=`uFwycl9JnA9Neh -IkE4-bXN!E1tT3kjSoofzz8nfWeC0xsM!9v&Vh7qL>S}$iwzo=P-!E+pNDp*$?Pj=C4^8r5*MZ`*7{R -)mJ&Y(8~V&cHpA-x*cX)P{|Gd2;daVfqw3`+{oM-^_Kv-7!7a$Fj{0N_LN? -)UG)Dq%yFm~=G4m@NgZm)hw=lYoZm{7Mwh7OS?FjZ-+qxxMw?+szRb22bJ-;XTy^$Ul{GOiQ7yQ1S-x -vJ8p9?&dhRTx-%LjI1D+v{xB1m;D2qWdm_J2Crb1$coFwzt913Ue!6 -2`(`dLScZ!;yliF(k^Nq(VM}ASJO8!9*IEiJZJca+Pbto$@X_Ir7G`WYLpFUls#d*s>VPVk8Sk7Gv=t -4>5oAqfNrHo1VQR)Jrzo(F^N97IslZeq=}VIjcp9>I)0eX7-nCvw<`pE<~I18z>Q{D$-&JK51DQ#N9v59n3aD6#u*C5#~BKVqYT9+>Uj`khoK0$fXR0# -8HM~cGPD%Lm_dLp-?!^P*l_|Whhb)P>LcSeZ@TcI}fd~8kD2d`)Q5ap(Lg3pXw*q#UaxaDvlaf-p^ed -j*-g~Q<27&+3uM}Z?qfYvh(x`tLbO9V;1$aZnG2agKjfCWH!Y)KWsLY&}+;_I?Uc}KF+c(eViRgo=SG -}SDz50aBMduj_iiQvE7h3vKtD=c0=OGZYUhv4T&SWq2vnN4T&SWp>S+BB#!LHY-~65gxH^NH$?vT4ed -ss{ptDGZYUhvjf#paub{_?w~ji8i;c?K#jy%4f=pMf1 -K^bdGR^x(&Dp{3Q*@3YqN4@32mnMSepx`f+KCF_*k1s9BDI!V{In!A9k^#ca$~{{}gTRS5g(M&FXZZR -ncb3d$2Z>IMQZute^e&gDh55v>60}&}K@aiDGbgxuS7ifR`&8uW_hf@KGYrWb%m4CDc5?u1^D~q#(}& -a1_s+CS&cMWAyfCi0~;B&0UF{qNrMDSW^2B8rGT^BXqI_8~$wx&4pz#3s3c*lT&Z$+qSBiP&exLhkV-5nK7vNNnZvV*lO`# -q!{ir%ycJhL=7?Eb&86>^+m*O~O~%F1dBg|FPkrw`FeV2HOO%C=9k8vFDLJ@odB9qZ{}uKfDDtcfGr8 -KPijd@HX3!7iT*fpLh^-**wbIPuz93`z_&Fw5jMa(0feO@8;L3cGJY})!SXuTJPSQVAVgH*mv2$cefD -Toa*7#evquf#$|PB;`2Lyo&NseV%}=a;orZMi&OZ|ZE`vnug(hYzyAolUvY8jTM~A03db%^;>g9@=2t -LnxQ+cl?$Go@mi!&wgHS|52|C#(;3T@|?7_X|30p-xkyI}VQ&-gAfKn{CDetn+9CQlwCQ|{qFP97b6a -sYXKk~4v9km~6>8p9ZyPpquS-zXiH@v~$9dZ%+Q#a?mWZkRr^kvy^cf0dMziM~q$r(*iP3tj*Kp)zmS -6ypTy(J&xgB$e9G3z(z?Oa5f%FWrlXPi!2E%C?cL@^a=x(S<#nHhqaO-NFvLh-Sw$fJD~N2WsYv8k9H -^#U!M3dP5!LgL6&C>)y#i6c{?aBM0hj!cEZv8j+aG8GEPrb6QXQ%g-%9Lqb2BYCHAEbkPsp_Mm!0FL$$HRYfb3=A$NEiCe4Rx%7OtoLx+)&qsW~@+xsEn -yYM^Cn&XNU*ms9nPAn?`@MKik@E7N-1TS5fPHdmkYZo5MRTO8&RI2Fb=$53KfpA{9P(vp6@b;pqg~;5 -4_v=}#N{X@fs)@TU##Y;ZcMZt+uPcBH)felBR23VM37?1CB=@lNl8>IgVWp=n>|f|cm0Y1q7|xL~8c) -VbhP?}9zW1*i2cIQ^zBIDLi-PM_(5)4~PeodbPaoIA~bqFefbuKUCNM3=&Uq#cRH?36OMi2RY-K>sa& -qDyU%FYkJ(;K&~-9Qz}Qm;CX2t{@h_rE}14>aaiVk?|f!{%FCiWf8BkPg=c>)h}Lyj%ACxSpceC3h~g --3f8zD%RfYhguPNc#yM^&#-f^zjor6T{bcvK1>OVm;wJXXi7)&S8(vO)aT7b6taDSfD0M`-opV~iT#sd -N;MX*fql;W&n)8w)8+)uPnI*fhpRMlp7b@w3LF8ii{b?vYWrj^Q>Q3%ZG=X)KS7V(A!5ax5DEsI~2xh -IdpHtnE35w=*|-X~;w``Ku)P%Kpy)Ec-FOaH(P5h(2-1zk45AII<3{9OXWIaOJ4nCH=}#mK{K%@E=f1 -0|XQR000O87>8v@`Wh9udIkUhxe@>XH~;_uaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB? -;bT4yaV`yP=b7gdJa&KZ~axQRry;uEj+c*;aU4O*@L1BCAx^{N6K)0I$wxcA*`RgS67P+1wFfttzl|_ -}LlluPpeU#)c$bQ5kA@zG*cj&v+>5sbqpfk1?&IL`DoJLAKiI_#ym2 -*x)(?I@LL=mHDES|a2*a??RX>MrF4G)b}6vDLOY!P~~Fal;b{Fz5G;fmyf%n4}N54q7HOE{KRt92|wW -`vA29#tY$`T$@WOO>sMiZjFK^tee8%SSO~v62mrS{kZ$YjoLcwg7R`yg}_KBG1}t^0?;ena{IKx-vFO -Jg$*nRo8yQ3ekY6FEkd>lM*X>M~Kjnku=_zrHoSxf4RXQi=^F`yL?lIa)LlE0e41;oKJ?k2N -QzDY0vMxu^2h2IA*|(5(b~O8-aV0{ixCTYjw2Wuvt{Ek~>)SE@2&<43s@>sUk38;Vdawe@VAq+p-voJ -5?$E>Rr(13)DDg7%|FPHW8<>!=O{*wP=p$^B?Lq4w~B{%Ct%dpLRc2wRi_yC`smBvwfz0F0~@i_Hds1 -7P5K-FtYnJH38y@?fLC?M;TR@6+wbqc)A(-lW%^_S+tfr`~wvyA2eV^DV`e%u^)qC|&?^CBYQOMtz#;}MxCx&FkPw@cUb>o1Pq3EbgmdUqdqZafd0q)x7-0LHT==-oM~+Kk_Es>Ey_y+4ykf4Bfl-YbMPr$A1`fM*X1Mo&+QB#`Um%dDV1=(?Q4eg -3)c@yZ^MiSU)&2TPPd7BZ%7W)Xb{sI8GQbt;rYhQ^Y$P!;g5W9z8+Spjc?o1qFeCGzLLc^Jr1GBEzFP -y3@`rpI_gy$j*4oRy=dP9?F>K{`Pl-+c-k2?TqYGBUlYAIHFAt*=<4=ys2xO_k~l;F%sz9JODe*kcxx -}wH1ienb4eH)=BXIFAp1MY%@4GW|WE(CW@`UiAY!t$~(!9tW6P&gu=>I5qP-EAJBU;j?EO_BBok%cHC -wQDHZ1kJ1{wj)b=@)X`a16QCE{LvCtIH3|4*Yj#5+GRaeDrpMULQ;^C-a@VPmopUOQ|bec%RB6#YwwN -7o?ERHrW_7Un#zZVSRjiTB2f@W5Xp<2+HCtgH^M19=>?$~?cCddaqu#^{xM8lM-v){Ep#m!p9WvfOBd -QUz_zrTi(_VDLQ?rkwEvwh49v~NC1tg|je3vDi-eBnHCF|T+X_qhRemD%yx76h!MUJDVyBFNu!0nhw` -D0S4;^t_!j>*FnWO%aa^8+jad|9qO39|*5GB*xB2XzOY5j$LKmer1X#GHuNPODeO^TN$o~wb_wBV#*+ -Xr=nCIA9S({-$JX;!tM3`EYhy~NW^wVKTHF}#da28~hEAe -r~5_>6`^El)KhI`pJ)J)7yVx&`IhkmQmR+)E#Wgqgk+^Tj#uTLf=PiIoh^)E}RteBru$XU5~C$u*syr -fPCqeR!&`x{Mz-=yDA5XIk$-CJs&Pju#xYiI**R3p8YRx58b?c+kUZ*<+h`qOJMtDFTY{VFda#ufQLafRM|zgP7YA+LjwTA8Rcjl4N*De0(= ->4myyN?tt52>zX3xg!2;1V7@l_XXFhCtUrX`H!%SuNGxOd|8<#ZY#^B#3#%f{eNb?{{2v|(fPy08%i_ -a`>8$m5@)s;x#7+IK>O4nGKHHF{q{ET{awpn8v@q}=7dBm@8e+YA5zH~ -;_uaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4yaV{>P6Z*_2Ra&KZ~axQRrrB-ck; -y4ig&aW7$UqE7WgcZHjRrdi{plE?20;;Oj3Yo+qR*j8pr`_DIzi|>uQoM`# -GnkC~gPAvg-sGYG1J2mq)NWuIQFu^n%LL)jmjMN)VJiP&oD*0H_Cl3LjUN%EseynR>Km!RM^mSEU>FN -#7@B>jFOEPlcbENu86W+`vhG@2I~ -C=v|B9~DJsn!=yDDVYpY>FHB~P($TI0mVHDfMm|tBt{z3`Bl&EKzGrm=92VVdqG+-pm6B9|1T6j@{Kf -f3grdDl5)0DL?yPmp`sMD49zP>eg}z+!6yO~4VcdSW2Fob5&R();&(D&VPZwG2ryrpmZ55=4 -@Dl$RxqI&Q-Ki`RH6HMyy478@PzqPXbMEb-pX`zEalJ!;^l$BP$GR={yU%y_13p-KT4qOxCd`JV-s_Q -Ix#kWZ1>&*8yIu~9NVQ1NT;#{$4moYBBhP6Irs+&G0|5XKiE)+H4S6lL;OuZrR9;CP;N7g>(|ZQ@q=P7^vV8WHi#u>~H&hb%msBmeV{BGGQ8SnQ>ZmSea|26pFOt7$`^NwIj{IcXjn(=1$?e;c@+lNjkeP|R?*kWIwOYx`9{~TF -)4$Tt!dK4A#o0xBjVnm#Bbb1F}iN`IA3tUkw&T+Nx54sXpt1K>X;qJ2%&2sGBwp&XSpSYzPMPEzC8GG -kG)fDYB+wz=bR{3z$YCb1ggV*bOg-NTQ4j;Y}Y?A^S!g|f{#j;-4HRWM0x91e(luh3(?US?KWkwqQhM -%SBI5WzXquOX*1U=64g@B5oz7Y9-@-KuH#{UY$VMne=s<12^jU -OL3iDce-9t#b!ko=WZp3{x(zu^`B9Rpo;aQq)wgwYShYZ6@H8x9D#oUP)h>@6aWAK2mly|Wk{D{@K*f -<006WO001)p003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMyAWpXZXd8J -luZ`w!@{?4x$DIaQ7>VsA%T^r>jLP)|L5QK@Us28Ed9>ZSjU9`JS;{N)ZwGBA7Nz>kbf_Zsn=9$@9pP -V!wI3He(rZ652hEs0{=i{5f-*CkCq;&#|4Z@A$Yc5y>0~sK24YB;rqlmdut-&Xm+uE6q1UH;4FYpl~x -kUvs1k->)_ER+4XImsu94CVN%y20*{Z(73vOfe2GpTa9uaFrGV3lTw^-xS%tZho6HU^wQ8+xBV|3MIq -<}UQ32OQq_)RMEk7dBa)QS=jP&qbhgDTcL4zH%SFh)(8!W9ZsRLv||McgOnG51 -ku1g2qYWqNa9>e43S6^X#5B>0ToQ$S978~YKJCJB6Wd%I`_Vh5xGhZ&ncb#rSS+mv()5p_fDJl#eL4w -VMLMFzVe?&${sQlU>YMzBIiG=@pkAq298ucO8F&FupE(>wUupU?Z##oZ~{64Rum{^dwK-bS2&sVc>UN -hxsxm<;EGYx3xykH(|Loz4Dow3rS(4=!)!(1%%nz8DQ|$Nf3XZs)TbZ|DMeh&zfcnN1|`DGmuTmc(R$ -hVe+}rQcC|H6<7Yuwjo#ZT6AVL;<5KvQ2-ghmjQPtX^_C5PAwc1R+fao)o9qq3Nr2mrxGcF|w#+pw0vcc$K6QHlw;oB39T-k8-nc|gS}p37y)HV{tx(Ar+<4 -`0n9^vrO0I~C!eEj_hM%Vf2c-4T%rb4^LyLxswKg<$G4nTE;LdH3X*{c$dDVV=vzb|K)isn}5!TE}HOZ!)3v78UxP#;5INXhL2uLn<_h635_b-X-J^G04z5z|NC0CL7acJA(*?lWFz}yK%T(pmdAZ=yW@oY01+ -b|IkkYsr1_wl-@GCJQ?;ke&2rKO{?Vvz}aDHT^uSU)yrsmpPkV%z*VxY8)c-Y+SP{MH05TiD=WW=q$c -Ouc@OWP*c%&qL(nD%^q#C}07&&7Xc`^2Kb-Iqn&G -!1p{Qt-#Q>Gr-wjv+E>g;5VN|Zv8H_Q%boiU2ZZKUpz8OTA$OdESkzxD1ZQZ|a+`_~+*S8IQL%3vSqi ->%21%YElYFMrGW!6%ET7&lP@kQB -7S%PqZ*X@%E!mQ!i1!C$n!No&$yZ#vGV&K_&^I*AU#d&bIkv&2S)!21db6aDDdyNu-k@4tX^%+}@&JW -zWB*wS?Oor@7lYfya&bK$(x34~yZtM-zVYEH68v@t(-PGR|fz9xEcTeIsgCwaA|NaUv_0~WN&gWa%FLKWpi|MFK -}UFYhh<)b1!3PVRB?;bT4&oX?A6Db75>`Wprg@bZ>GlaCx;@eQ)A86aSx2F}KxGM?9+H}s451%VNs;V=d?Nvi2nV(Rsewl -NA(6s!BZ6D2RUA^!m6U}7zw(Ij4Bi1Bpa0+(6;?PHZm0FVUmB@^IIlP4d5vU4Wl -8|t`eiY9G>LK%hdL$GCTmg~V6U&_`9VN(QBM3MQ;S=+J0z&az-L2e@JJ2-k?6wVJ9d!l6l}dheccd6= -DD)ZvbQjM!O|1!bn)*P)1a^te!4C+CiaufZ!JI%W$RZBvh=8>4TYq|Wb329h@C*Fb9*^6@>6cGPi>|; -^ba0A9S5ZI_7`5VVDAy=B0$lgT-7Dm1clv|=^ox?-=}(8f$poC6F|=XS9#8w-+d+E_qucT5X3{g!xrA -gUR>ssBd28YYLarEE5|hNGfk;*Vg6J{g!NmxP)<5LKfQ65VzR0pO=qI2H2$`(I6Q ->wE7@j5XBFfIcb<7IStivxh61m~m09`Oac84wy+6frrb?7i573p;wd99{xTMfHq*#Nf_+&Q$MH@tXZ@ -j0nf795ktR1Q;V*yov)!8}l_9Qx!hfQvr?TFB3rMktSs%LBq#@9DyRj%Y*cU*G0k!kvo+B6w{L(5sc}{;+?2dp(<8jeC=;o52N~gJm|Y3K_+mw1r2h+NltP&A!8+cDl;|&<;3*ZDxv`~=GD9V?;owk{ddQCsMZRssQw`~!+!R^HRj>tq-_RM2(B4Zv_wtMaXd%-ybBs3C?eF&_S$@y`F4Po9 -Bc^1sew08T-NkTbNq}91|bB)EAz}JvHsdk37`gZE%m=Rw|c6Zi#fb;WBeKV19hLj)k7p0f=9O!hQt&FD{8C%WSpr@15alq+;WvRbJ)E3sQfJ%vVkc2+PQRg*Mx0 --|Azaex-D|9L@%I!Eiq387RZ4!50Bk{b7rLw2W<^6ZdQvX%0nG;zvGj}JA(>l(Fv50YUVAy3OPB>~>M -9INuo!ZUXa|)Es)yir0|Ay*Bw)NrkLnFDep@84j^_)e$OH?l~L~4N47*0=Xh>h!#sC(8BP5ZR` -|NDk$7GwBW$|Z4Yf5?&z-VnO>haXI~lA!uD+ehhdXJm&t?rJz2k0o!Gn5zfss4hyX65 -st2STLbjnSy&t0RIroU*~_$N0n3@kY()wV|<9Tpu3MtD`}!d_+Vq4S=mohl*sj=A5`+cP|1jV1g81WzRka_6VPwM+XYIT4*QCTum%$^g0mihjVw2`?JmndXec!Jc4cm4Z*nhkW -pQ<7b98eraA9L>VP|D?FJow7a%5$6FLiWgIB;@rVr6nJaCxOx+iv4F5PjEI3^p%z>!@}XTO^xi0o$hi*BnYYjn=cnKMHsS67!G=nbAmQy7o>gQ-7&-gwsk6F$IRIad& -@5oU_5n20#^WrDyoWb$vudE`p9f|z2ivxANVGmMar_=a3=P=QRrT*yKADHCQ{<$}eLVNz&%RU4 -^_7fK9ssmge)Ffy3Ha$96PFBK`9YqOhVcOv+CBQW(XSxsj@&n_ -npcFp1PF%o$%PmcccULIXy^d~4QHRQ;1RoGn$Pw$`OoA%d2vc_(4_u$4K)kpP7W(`9-x!ayXF-=rdbC -KHx!Z73HBwPRHqRGHp+_%vO>7&R${Clsitn|Z$EEGF`p#V9mFq+w%YdVO$eXodKd5Ex2IU$}b@$mqMR -{f0_Y1a92Uu3D9`1yVsHGFCrLb7P5361@dZ(@;C6d(nbJrwK4oq+OHu^n(&oqwlmvutdl;ra5m>2ExM -kQSdxl1kjzn!1u1_b*I70Jz>dd`bzUFGqG&L86~E!R3yxngj0ga!0SH~quU#eN5P9#KO6nj -x2BRrE`ggQ54pQV`O#;9W7=9ooLL=~9-%d|R~ivD<<#9Rfw-mD31(q7PY0sfS!j)q2rv!Q|MDju&RrO|AZT#_pls82y3&AbTifOJ5_ins=| -C|)yxdvx)|w_hgZmx_`tW396tY!M7^{mF!GAnrJ`=^&&{@xr+`>%jxG-7k*g#5~fv0G6d2Cqn2hpsRo -epF$Wix>3Tgp^>Lm+&(9G_Y}1U**7=LD%lgNe%?XjNNtU&=(aMArn47Q27ajI2=bAPecsylX`ao}Nzd -l9J{`)g(1g|WLJ?(HSD?`3O2DVwq}R8f4!jgGDX=YwdigQKo~_tk6*hhd+b2FaL@Mgi1^#8iVl^6Z^M -pr-=7joiSU?vCN=bEE=cw^ru_MA%nhBoam-MIU^(2b?hjm9rPw;2z7N>e-{(ohPDRW+#ICV>l>E=qID -5uO(G@9`30dCIeN8*pS<<|%3I8MEWO$_VQTwDHwnA+;+g0ojFu9dsHGY3vx`U8GHm1<7`v!{S=?G-lKtqZK?d3XDo)sJ7`SRSM7D -&HvKbG;`iT?>)LKs=nD4_P)h>@6aWAK2mly|Wk`ntOMDar003AD001!n003}la4%nWWo~3|axZdaadl -;LbaO9oVPk7yXJvCQb#iQMX<{=kUtei%X>?y-E^v93R!wi?HW0n*R}35kvQS4rx`zS*0yN!jn*f`@*m -%(+K}#c>4Ml26%3k-^cSwqsoH$)n2LtoMaRnHlxt2F}elDn%?yW83WOF+Bo2At7Y$yML8SPdu|lP+99qSbNCb~c%A`$_p8A9r>#3Ws7lw!+Fm-*apPE{Og?IDc+yx%b*)vdM70Led}?otmQArwlp6!cIrmJ$z_9%ZRs#mvlA`q{)n=Q;%Odd4$1 -z-3t5(3z%x3ivEkGSkb1N64A>F{B%R!8;5#j9m_6yEveCV&XU8FzY3f68M!+`!7qC3IEwSq7@|T~&;6 -f=W!w~b-OL-r0vp6$&2$oOAXmb+>F1fW+&-nmS4ssFw{6_}?Y=xU!l+A)zTxnBJ?EdQrF3T9>J=JH3% -(sx=soLNHmedu!gEP=6UeufOCmEl(z}<_fNll{d3@kUvgpVpzH$04SefG^&$@*CM- -pGcCDy8(?(u0S-99svR%O|1>quoncVXzgnPFmykv7a~@yC!UWO{2!ADj -_FjR=SjcjCtjsR_XkmgJ2$z`qb9$cb6fe9R#>=yuLg-_eB|0ZhON%uqt7?azr?q!{lYgHlm+Yp|bchV -2#y~=+M-zR>c-9)9~QgY{4#F)C)DtEBs^g^xC?UC3zyo+`LR>L0Q1+k-h^23gtE$N-ii<6gO{9ZTg>E -TgW|9@zRd)q786ux2quc|k=X?ML|wDXEP+-P&SokaVMpW=SF#v)?aWKtg2!1#zy!{0fA*eKc;bb?e&k -^Ku$O9KQH0000802qg5NC`T2%hn7404^y205t#r0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgie -a%^mAVlyvaV{dG1Wn*+{Z*FrgaCxO#TW{mG5`NFGAQBXkTSwTm4+{kF;_TUW&sntHF1Fi;A`Ki`q7yc -6NGx9ERA-wY{@{fXMAsv$`c&9-GF-mb)~}umaJICEO1Y;8n -Q!rEYniH*Oz$VDvB=E+!QPdH_&Ry{Ri}Xo)%{dws#e*zI<-@p==LTR&Xl*xB&FN3%&=#Q%wW0E`(~Af -F7DOA%=w80!t;k;zX9XRrOb1fej%=+k1B=U^20AiWpB#DN?oWrnv3YapFVx~FASh+Iw`uQeebtFe)LtQ@TOu@X~(C0?3l^{XSbA7L%Ku<^P?sR%5Huyq67_qH{Et$y8%|` -Bx{!`uD2ojc-=86cGJ90j`lLvfLB3IJb^V+j{z6GCd!%7{>A{!D*oUdF~vX&d+f9`zXN4eLV?M4*a%? -kXLYvCvhm8%W@8t{joh}%A;Oq;4S3c**T9l7rwR1G%uMhRJvxi&%@p&pUF= -mOhQm{?_r5O456KJ3-F(=g$!vVK8p6MkrI_a-;6bLeR&UEt^<2pq4R#l?$xPrSwZvQRc0;OU`_b-WI(BNkA?ds#=-a@$T8Xo3{piUX$zm_CDNpTp%80*XvPOCd5mmol3rGtbic*@O+g7>botWfc=Qy|sfyS6Fb3y+oq74$il&oiBybnz6KV;k>5f$vd -7)f*{UO|13$63QJ@3DnB?PP9uA5Ea}V9gPtNB8}&90j~wa(R*IQd=H*!`VNxdp_TH_~Zi(xOAT?a!OHJXMxMH1$v37x3d>-naYpE+cD=lBAbhoqXIp}@t9NCV~iz^ -G3aA%%8tG4ISEo~sf_~au|Nc_h^UNVK^(oq2FW?4M4^BeQFiCDi-&DN#S+d3H(jX)Gog|uQE+MsPpv; -kmNS%s>L9986Q<&NhH@bq7y(Z%u)k95(;E)~N7kYmk|Va_4uXtSb2OL}$M|R27C_SbiY`1G%0Hv@a(hx87I@hvD72B4neSG^yy!`r`7m4TsuWG<5x%(OdSx2xXNA3d8 -fc}VAB~wRhcq(hdbN!1Uac*vcstn1^0YdLN(swm}HBOmyvb2pjLjAFo`Q!;sE^Zr0(+y~DRU1J?l}^N -4uo+Nxv_k6W8wz8gXOSYMSU^)zs2%9^Jr^vKo>BVnDv?=sbHlW7q!f8sR$f|E-oEd7iQ4m~B%zn&Hn5 -*VTAocXR&P^S8G_QWjU&x{%|+eTc*E%tu?!z_-eAg&asYVcNadxmp21hIUNsShtThPF*>DL-8b?1Xd; -X>@@3o?aX7mQ~$wJcRM}(wiB2@`kn#<`?>h?OP2sbh`+7S_4QRZ=X^O!T3Q2S-ox>K{T=4{#u^@Y(6IyHyv957 -QVQVMFP{%=H+v-c{lWMko3q_wTA+;@iy=?Op3!jUT4q2c2M?=oFi_xba28 -|qhlMN-vxp47(ae4h27!Yrt&9`4+9wFtq33gLn8W0cZsXtmuB~nQp(1LNv3j> -Ga74v2wHscIKOYWqR*4E8?>UK;tn?wr?#h*Tc22&1;49M_%*rr?Hwu~GhrtBZb_l+LlE;BAw_jqcRLT -F;6>RXk?vzb^dM(+O<6+XOArf2y=J*r>NC@H`h$Qi?na}wz1!>`};#%`VdJnVu(`gkg7-t?BX5Ord)d -*_Z_qWi+`x;OahWSZ;~{;yu(O#D=!`gzd7H4WGYjU*uWf$QWDxp~ZMCfqymxO?C^2gXda=jufeQq)WZ -X$U__RP9Bhf`CXAMc=yK>uR1|&&2sF@i2$n8$cJ~=eTaj#)07#;#%OkRzTiG*7n?CGyIORca!zkB}zC -Lq3@0c_1J513xrU3FWOP-^u0Q8zr+ivnaYxo*Fd=rco8Rk!wc#I817{nJw0{wYpJ2sY^$0{PgUy&0z* -z6$%ra|7tB;2I8sPGVpndVXtk$K!_APWqau{-K+G1p2!fR|q((2-YhKjTa&|Kx+vUb?se8T3KCm -`K4#|@er^^7X0H!-Qr$DNaHT>Dt7Q(4TD=#Ctf&L*e#Sqn%6(;9L^bY`inv&p1Bl6{)^QMtS806LA}* -`u`=hzOkxjj3abxn^9(NFIJ!DnqlR7O%6leEYek5uwbfBR>Rup+jss7y29po&@Nk4pa`-J7D>?EIp!} -UUfyVFzPfDH3%^Zg>0XpgC*y8a1&I{_C30QCFhz&tFFL(zgMdAQNGzAlqg#Xd5`9ybfd8f{!U&k(b>@ -S41X&(H0*rKlOJX>ASLf=am(bq<)??kS|)uOq37X*qg}I;Zjl@GDx@39ogTUmdL@ZxG!$DS06~u+P$D -+x3BUyCd#BvoQ8J|w9XcdI!Xh-;gM921V_&qVuNm1Tu`_*gPPNkVZzTTj@r8WtoW} -Nf4iDHJ5A`SR@%5_CYa$hfRuiR1qVDYa5}lD7)u;1T$E^~Yktq)Pqqx -D*?Ws?l#EZBv4GtVM38#ckPoFp};hEx-(oLT^ObnqC-K_Lk0)=DJX)-PhlSxxNPK&}CT_}%Fr>Z^z;U -hx@;}au=4UMGRdTW>$`bu>=b5o{7w7a?_q&Ag3I3n8J%8-_uQqBcB2?K8mZB5jt>5lj)4)N&c>iUSGr -fLkQ+zCf$==r?ahg#Fcp#xxs8g*xlGx2Ykk|6~ecJz3lyNQSk%EoI)ceNw -K9l-|QmgIWHu)30&&1kthoev!$fG0*p?soJm25qe}NEbs@r#XRv-eh6cf0+z)IGQ=|s_v97KM#&VsJT -hIau-|_)y`k|5}p2orAYHFcZ(S`qElXqH@zqgEJg?X^N4MT`RdX{#@KAV9(vx_Y2bLz78jDzma&}h36 -8V+vjqMed0SCfl4Ik$NfU;bUv1p^3gGwti|@TQ5TndmV&ptapHAX5X9XA|Z;{(3Cp=sQ0GXO}!uRa5U -Dsr$hDrnFLam*NidTP~yyTS6$ltf;g!Ib_HhKNwox8}{6Ko`d@a2U+HlU@}Yn^Jo{!hdwx|wYuX06WM -|F6|@=N!5Fw*b#yK*(|GuHE@6aWAK2mly|Wk^uQw)9-$}c#r@o<#CTWO(v#5V6oW8Zy(US;wuuz^IFywi(`_lN?u7q -XM*RoWU>A}FvBU8OlB))zjI-q#HM}Z$vu;UIa1comMW_%UJ)wDts0)-*N`AVUdwiTss}8+HLZ%QSO`r -qDrs_07fe{9B6}PRWVIO+vPZ5ra>)yFG$8O%^OsVVQIcn@XxRAMbb5Wti-ILGgUPUO><@i*_2e;OrCJ -Po7W^X}le(w{OG#D`xy%Gv@wCp_fT@Y(TrE}S9s9)dZ1!)m)miz)EH$z^<*PEwVZu6-OLBXA@mIBh6| -2bA*Pp0h@J{hgj&V4$U+w7v=XfDGk+RBWU_0vjNUJ5Ma?Y#OGf}c6n`g#;Q;$Yh5nVryBeV`^@vUP`6=<;eD$$4>G&whs&C%Hr}-)YY>uQo! -m6)tIpM;(`oKvD6E%48WYxsWI}%a1>5`L1P_t3^KB6$5m6U~$4K8CUG12)$uk@W3hoX9}%-#OsQdv4# -^p2=c+@bKsO!Wt9~Y8FqKJnts=^@S9;m^BC;|wRY&<4YL@oTcD0l_O#N`%YqL&dlm- -v8!Y;=b9SxMpmbfp92V{^Q1x`)d##d!(x}XCcYvWW#F+mSTwnSpkeN+=jeitRyFS_P{XKQe)UQxL6^ -C4Ot|!$`(th9C|!%H^k2uNrrq&-g~R?6BxDJUR}huSEp|#(~vYn@teuy`sCvL*9qcO=q>)xa&T2?_UH -!kF0e<0C?#{|9fr&{ig`2!go#y&?w<)jcor?95H$S$!`EyyOGKgmAAKLgZNWy6__@SFY1 -ks1cL~Q1zz`$#o3)V;tQuM6IWE_KW=kt&}rPV^fpQrb0CuaZ?g={#w{_Uur4BOBBIx2Y?3^f7{LzQ-o -S_6P(6?-NS2RsZz(@r3IKo&)o6il63gg4vd**#Mr)R00}qYjm0WMIK0FyOryIxOHZHX<@9033YI$&Vy -B49TmJms3SAy`$}Q6J|uNb}qPXo7b;u#M>DXm>5fORERC*{R5`z$h`QjQtk0c -d?*n$4gq2ky9uu?CUXh$eU&;zc1O6L%#o;HFN@L!5AsOV7IBG72Na(&tMm5h;u+3L(%y$Sp&x|afcNU -#jXH*)~VT`G17Cf7Gr&-c9#-&_LC%EFXgMmmn -%x(Yle5x;zv1dJY<@>Jve89r5zk0BRx~rt!)ICKz)ZOuxIu>^xDHH)LawMUgkHzgsuq8U;9tgh1vM~C -L%(Ll1ppq@0j)|GV&g)KKea?KgDw=@@Y^lBa%xJ@U;y@Jy+HZvFg+P8@Lv9tA!`S}F@i$`hgnaY)bD8 -<0T?d%hF3eW;`z7U0?qBPY1(PAi+`%mz&5Yqxz%_BvM;o=8r$bb;W$=fBu4iR1R(6hvW -8uARfM*s0;Dklb28lsDcfB6hy)KAb!cGrG2Y)_E+RcE;?-h5(a-x8d)PmifA?y3bU>efcQ_v*hU<3%$ -pr_`4o6T#IP@@jC_o41R*~x~5dV-rbIPI1xW#H;HG5=wb#^r-ud_#@C!0=1%9$vJ@RyaWP=#G9B+!XX -Ej$UD?SfHfi(hDU-^*M;czBMA)hZY3Q=_%y;Ia%I@}O0xA<@M~%Jy;jDNnk`sw5%KAXHgd3!O9r{}*+uHH -_YT&|(^>6p~=7~1XhpDpZkK9(T|^E|XuIQoj -YeYt|2t##CK$lN#;Mc8QZqwe)?r7ot=0c(>Vj?7f7>(vPd0m;>8jgvUh+{lN|!t&u -g4C@U_alFDVo9!vO&)k`DW(N+q^B;9YHrla)oS=`ve=B8WUoBCgS(P!bs$&*w+JdIts_j>1nubNSRx$ -kLPy6l>SGvF}cXtGH(|HE9yOVqsd%xPsFbrvDKkH0cU^BZeB(?`9*Da~_JHpYlB$Q{PlV5I8x=J4Q%+ -sbn9P&1bE_5UOFYDr#!Dk~_>o0gNl=EHBG6LS}4;wg45QYxux&iZV3)$bnDfiV$dHWBmryn8zmNuJV8 -Fd{z^=MKfSAZM{}DfZ$w!iS(=h79$Ul}cz39@kJxBL~5ZL8`&X9|wPqBHc1qtsh7MW+2H88TLu#jH*f -ogP!ZH`p#MmM-kp%QW-dh81=!vcZufB@VXZ7An@9lTdPK@w{N4Zih~hokjuQ1r>bGEG{*NDgTULcZXdpIbqkiq+kke|Z;tfCns4s?_d%mbY?+pfRV -wY7Wd*H3bzqRk)2e1$?67y%t^2ny#B$(rOT77uPFz)Had~w)A^+%YrJGq>LVu?5OEt3lS&Q5B@lNMsW -2SGI@QrC3dpFn+VuDd8z~JX|(1D-fsS}~GHUYeVf*rXgcm6haA(=yXKqtk!PjxG~WblB?=oUOt^zRbB -TJb^=P%{e%3L1~*jN%V&^z9Rpt=Nh`=+^6s{tk0z6<-T=6$n;56+`ybx|`Rhe5zAk!%X_$(LEXlNnOYIwB6@V5TQpf_{WjacXAg24tXYec -jl=hqJ}?UkvncS~Kee5~Duw5BtwC?cKESB)7)V-*fG$Y$zJ>w)H(icZaq>WrzHvqOt|X`I634m+5(;1 -vMJHjM7yf08L$@ftKLmsDC35xJ#lz6m=&qA)a5t}=rTJ=9Ds#<(@4sUU4#!<9J&epI-JqEyFxRV5c2b -CZ=1QY-O00;mWhh<3Jru{14ApijNc>n-60001RX>c!Jc4cm4Z*n -hkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJo_RZe?S1X>V?DZ*OcaaCzlDYj4}wmEZj<2pfTeN1^HuM)`SZ4DYj~rNj@CH5ZkC@_m$@;THY;@YkshDp*J;+3 -Th;WPdw$6e?CA9H=%{P=XIXZVz2A4&O`W|s%HTt@{IyrTGLyV1*UcaC!8AMH?z;Un`>wC5BlTsec3pN -sjcDdGH^3hsFaS0_F)g(;R;z3cThir9Woo@v%T8yowznU%AIo~x+-mA319M;4=De?atyc4Gv+64~=jS -kZ-TbvU%lPGyKrrWpmrZTv%*Hh6tEz!!@0+G#IMT!SZPPW&;1TU1YW90otW^8HXf>Rb|Gd%NtLviPsF -nBhO|vX;0e>p0UU|>o7GFLnIF)ezFaGxrFp(Um_w?h(H{Q4O{Y9(8)^MHhKp*WC4K#e~@P -e)%vci1~y#kxqCQYwasn^_~~_t$1b!6Fwd_E%jP>qv#P9Rb&Z=fb;k==cWyRPA^^M+}+ICr3x~*J@q#RR`^wtN(%>kKy4F@Eo8Yd7 -_V&6>KnbCxtJ^{Bpuf<@orR{*T9%adTu7MO9@Ucz4iGPcz*wuK{G#;fxxeE%Fd+aE3)z)T^+;$EZbaC -#OFcHDN11G_|59#A#kt8lG7$$8A??4YHuD%Wgg&YgMhM8ABC;Ji`HGYsAmiz^XcG;2z?qElSvM$}h&p -KQz=l(>t{!Y|oD;N476cnA7_iH49(RInQV%{$h34%38gp0cQ1WosAypF%C3))XEkfwUbfyC>wYCo#IZ -pIrnL>hdV!Zp;NcCiN7dSck1x6@M+NciwAFD^mAMt|8-MT^61JNhbCPj=Lf_z4^NPUaQN?v3c%krFx? -YAilW}b_B&WrSyY)5CGo+%}Ksm -{9Mv#MDhUw7T^?DX_{lkZ5_=S{mgUGm?j>#|a(-xS4F@y+vR>T3D)^i6qn%31=nKdGMo+w-IORRs@#& -gZ1KAjWIejsHyF^7EJ9zr6Tin#s2x-+g-X`aj;hd-aFEAh|QarW801zfL1Y8`1vdbqoCOi7wVECVFB9 -KvIP3IfMNS*krMdXbR=<@iRM`1=G9}PLpR(>6JCtD@2->Q=iWtlyTm_= -FTg7{%zk61J90Cs!AW{PA0>U94{`RR|jbk3Ls;^rO5PG@v*7K@>#b{uztB+mL_4-X&6V_iTW6 -h*US}1)Ul#SK!zL(>yybv8R%Ec^az&dueU?AXV3*JGXMXoz0#p3tIdV*O7O=~Ab+ylKoAxuHe{Iy-=Q -$GuZCwE~A!Tz@H!aO>wcij#A~N0ZtVvtlx0 -00Vv<}|y#E@2QLDowC9Gck%N#t>0V9D_w62zo^gpsklGYt|NqAkqPZFIc{d$Y01Ln-au$wr~1u3t+H= -t>IvvY~aXiWl+Lr4YXl`O3>pGL^~(;{a$RU|o4|b-Ii`%{@Ly5g7JI~y1M6d{AD+d -w_@c|dDR&k>^xG?#95(B7N%Q_RDBFULQg*~)VDGx@dXl -)X(TjDqSP=7DEU>RuM2W=Ury>0O)!dfA&tU~i7!s|TD>zDk)}CLWw1=0t-W4#eCGqzC=thOmqs0fXpo -KU{z~W>*mo3^?bwexStu7Vzq;J -qXAOM)Vjp;0EuiRh25}Sd649JRUo)nIV5PkZQ9+4L1}lq>O7%w(tY&S=P&{hn?KQLP@X-{eW`X*OdZc*Z}ZaGX?<`Y0~is^#vH -lt*>%ohfkT-0_VeQ0ZB|>-!XYC0jCduYHZ0Yta#9e?3ob?+!1?0Y(F+@!Ua6kw -wa2KF=IBF+5;j;X2SUM*XQ58{PgB>etGfs{N1O^sUgnUvng|ygCohI%d|ot{4H(F7$t2(^g7hV9A_2g4LF7&YTERNdn@*0sm;lb1)fu5+)qL~(kK%%^FBwjU`|Si={pI=7eFF+M!-a -7sl1#q`!S~%E#FNle}%8zAo5q#ofP{S|84B-v7p5yY)TjcZCM6I!w_qj$!QO~GYDYY*ax7{{P+L3;ZC -yeP|%^+V+U!!%XGu~#;O4UfxeUR1pSp|=LtpRP9Rq*H#zEbb7aHpNwK>Mp=_a$@A{4mf{DMF%WH*ZV~ -e6q17~AR5s(a5LT017>d7^e?fP~HHm1(Q?brQw?hC(J_B3r^rAG2r=mK8IUjYI+0EfFe$XzUhJ2iZ3UOW&*Bq9=vXsGY -t;r?otJ+m#UW|Noox)~pX#n%$Xz@pg3@Km<(qt)ard!&!EM=4_*4^1<59-WDYt=PiQav)*YspNTqNQ7 -8d&_g^)MwIwG$B4tZ;foI#M* -7AKh;Dx{)T!e}o$z&NlG*}Y3)IVS`~1`l|KqnMQ--tl|sMQ+E?Gc5BF1jDTuo*kK(;e;XNdA^5vdwEe -UdP)GJLE@F$ew&d2h8SQh*W(9{mf(0*HMhK1za4s+5SZ_-fyY -}P)fOs_bG#eFt*MQJcHAt*ylWYbOKgEL+&uV93y$T@++K@H%w|&RnkNv+SFdgxw(=7i7@Ic%LbA -ZJQ+kf2J1R^7lrum>c0a#`-_#EBcA?lDQ4`(!=D&r$0mP&zT~%WhD`d>$X2n$JJp_aSf3t_s8x^wv5@ -e#}C#c?fgQDa_6>>k(Y`ZHvfA{t5)0#0o8kj=_EghUhhLK)bUAZq`0E;%gBK6rZye75ZI3)$Ea$#QW# -T24*-WXt6BXl}7_tYKe|!RJM={sTK19WC7%6i%Hs!fMN)g&^j>RNm3aXju`xaG=M8DdzTJwf)^>WF4} -{2+YM}FSWF}$AynI$v^*0w#7IFr)QWFZy9Z_iG!AQA!OZ(9m=(3k?~xUenwp4>JYk6OfS8UE4?{$5{3 -z;%6lq{Qbtoo~N1zN%*p{TNj1%%Z_VC%PlCeqPi~vZOTQZEt>^yeujm>LDehjBDKzdU?VqhzI%%oEZZ -~3R=0~Fx^-u6Id@CxJ;;8R;}uB{4GShqUBU<)SaG%KuE-6Z;G<3YYF=x0~81M>w)hwa$mRhKB1*45v^WxNTxd__lom!Bcu@#fi^%NUN+ku*s@n;Rd)MnG@cD#V8X>Js{jGop*7 -y?&7o5x)h#+V)wa_NPQacZVsszbDHkkBCCzNv^?=2Di4#DV--)%tk#Rpa3FQic!@3&NL!bo&`DyVn(6 -DjBlPsTBevB_^F{=!rkd0gO0F~`WdV+dUxIdp=d{i7$FRlQ}cp&Isz;Cbtj%Wt~!tBBY6;X&?7hb&Pg -yK(}jIkszEiqpT)fN;Y-F4gC25~(Cst+4bnn)QVz>Fx8K4O&!{?6$;E+-De|FePiing_Dv@X%2^gC=0-6+6mT4Qt>=d1%%?E0es2tA3;X%!X3yUOjvI{F~= -KfJb)IBZ}WLtS5-8qgBb_0;I}HfoVWcnFI~}WXb3GZ~kd;CmP-spOx{&d5nFUZONq^((=r(%0sr<@sF;&=~KdIPgFy~O?MZEVO#FBfUUnl~!ai;(G?j$+@AQxaf^<3<>knf5v7Y1N{daa$c$ ->m#*~RI6B<9tT1#Ofj7xYfQ9<_xtC}Ks56@N}&ldQ=!QV)1)LDPY38c;Fpf1J}|JEA}%pYrT)x^>m+4 -tJ}{}NVMG&30MOQye{9}N4aXP$1UT%CVG!0*+y$zMx{=Y4*I`l`vdHBP1ymem}^XkiGevAb- -kWnhnl4j;8e71jTATjDG$rf2NnaU3Ok?N=Zw=oGH{B;DB*#bX^2WK;)6ZHlt}4d60Qzr;WVGY_cA6B3 -3;_g(3ectBaIWc8g{&5O_2krV(({V&zOFI)+_??5=gWEPFa|(2Pq^n1sH5T&a2#0NKr6+eEie#Bc0KIKOOV`k%&U+;ZOG!sqD!i<(%*Oi-xFFz3Hy~P-=qNgT*1^WTdU$Bp8yGE -g>dc@0fhwS4D@a{}W@@o`t1iK6nrM`$pdtWyR_h<$~Irw=tp-FfsJ-jZzpf{@L0&Xp9{uQ(PVA@nd8} -h%ENw$D8-;s9;3qwz~o#oRk)P!uOOQB_Uz1OUmvESkq`T8WO8&;u3&6BL07;GJuopwX`diOH4KNoSwE -3aG5!KL6cM)ejJ8J5}X5x4-+8phh+zr`^XetM0CLajh{ict3O)ke#Ot>D5eScy(Z7Mg?_2$XqHiBle5 -YXorH<#A>7!|U7(T03BU!Yv?l-9ntL3BVf{T&P(4F+_`F~m7@92}jmeDT(dlT|BT1fi->!N0`rR2O8% -i!q0XAsAqbLv+QCfM8tGn7$*&eApl+0?z@e!NRnRzV33ZdlB`AI9AoN6;_?3CoxY)g%H6=xB1u0ojHY -O{kVURAT_B`lGkqjY6Tt}u|$iU0InvXT)O4$A}FHNxP8gQ3B33MPt>jsxa!PdO6+6T_>5fPv7tYE~vM -@oO>~Tg8d3GtU1OSzu9J9^da4Li{ws+fGbr+GOdic?ygibh<9&c(Qt>Y%E@dCGlHA%^v -Vwkma_Do!^?L=(;2I;AL!3yc~J?i+KS#nv8Ks^es+JQlZb8Xd?bJy=oV1BVI+&@5tB3wXwq!T?5AW5Bu4CIi2e -QYwPFg8&A{`_*9y?^%+|HTYr)h+W$GAz30wYP0r-i#Dd9nW}!cPkss^AKSBk%62L> -rY#3c(cB9(?k24;;%xXSV^T-HgZNIn=`e-z^>@%96WbA}#Bzn)gJc`gUNmn#yD ->);m`*P+6`+JSw2vZlSnlbc}Y=JQ$E3fu>_OjBAtzsdt81xA5WOo}FU -_q}}+k0Yn!^Hm_Z*5(K76_xFb*02u?v41-DFO}e>Q$w@Y8@;awHEJ`k5bFGvH;t7$u-gs$2t~M%M7Zx -{4&L6t9ctCdO#dmv1;hY=}lDJ>=i64}xc1H4Kvc%#N1$Mr&TtVJcZfaWR0P7nJaA7T=xeQ=Rgx;1br2 -~N+db>-!TO~>>^*r+1V(-_gj$O!X@=F3!>2`eOGu@MNu`!b{BsGt0Rf>ws$y}I8!*U-_Dr7&Du!K8bE -c_XPJ)20NNkxwHT)E&TMhq|S&1G1lZX-$pj*q%-*$m`L`zl+qXTc$xpeA;MS8d6q3N%EN!WQm1$$R2z -Qzvvdqnkt#=t{_<8O3GnP+b(kRq$!xB?zpUAbn^<{$%+JH^V -CKjlc9)r5TgKVjJnHZcvBMT8uSMVtFaGM)fhG-;$L^e=||#pTO9Io7_D!xqRiD`u?fvz@2YqcDKg~}wA -5h|6sXYx;W%6`YdAY$BTRDUSEt2AaSlTcRmOQOX4ykeF-Wi>?^AGV3rrmgJ?l52)dudZ(6L3U8WdQ5c -1pV4!93}c?5%1y%H>yE^_(wXFxfgKOE${nArlm7uG#IQ93XS*SkCLpurj>j0~4rdtz1h-24jYfEG5q| -CCKLmC$NWgB}OjF);;>3P-qiZ*zJQnPtR+EUK}ofJ%o~Gk2p${L~ma8JIo3gr&Wc)(Y^&6Ec>j~E(&R -0fx1L|sepwcS$4=5+L9y+UN3>FN8)=wVFL=3eJx3ApK2Nj?+aqV<>d~yZE~84Tld7&sUGm)P}yT>BYb -t5hlg9PHL+ade)5I$vZ(gmCaiX({nj{Bsze%sI -2-f1`$HSUDH&n@Hhqng*4CVPNRwzVFr6b) -+zJvwUQhp<+rlDOj_ARRZ*>#A7->)OL+u28bC|Jt+hTTqD>yYcwOj@;DD;zXUM{6qNZNcS5r -OQa@v;(8nEvb13;6KbOO0B*V1_sYc9$IIS<$MJXeZY6!;V*4zye7x)YeQ>?K2gF2we177AX@bYIbfV!{tNbFQD7O61_;9Oe$^oJPiECDh054mz*^EsqI^J-mDD3@Xk*jbh!6xRRZ -Lw5{-~Y({rAQN8a2ns}8C-=MT;Dp3O|QA^C>9}-52*cwGV+QDX-A`wZ;xEqt8CS`CTolk$Z0s7F8Ox^ -R;r1(`iiMZ01{ZF7rjonyx`DH4FqzS$=yLa9=+n~Op~-sr~z+tP>tebr|O>d1m`xH9Gu%7AWZKL2*fj -97c4U$u>Oo&o2mk_x+Bc=A=;eLRWM&KeCDvR -z`V!?$G(442$OAQWZ^D^EI!zHN$iBUM|x2`Ma)G48WsQ+n(y{4a6Jfp1Lz -J`7v@?#PYCPv(`}n@fy9A`|Ro7xf77Cu!}mU80-l^#ElB>3--=c=7Z_N@L=eo@Jx&&o4(I4@fL=-p2# -km|201bf$_mk_Xgb -n;GvbV{3x3=t`<)e2=s+@mF$3jAS4k!uYW=_2#DAjIagZL>OCXk9Hb -{$&f#k!7bk@M{MPiKDHIzV&!n)Q>U29#i6$&OntD_mUnCU5RgRk-m3ox=vauHWv#yd(ccbs@;L+)-B{ -BOZ(3Js>FgXcT_u)UOW#sd0=#3xr3j<@X0iCJ_H!(FLXY8MvE) -SuUt9H8EnkZ{KGAL~@?;J)flYWH_`?}eN`QN#3u?j0?&VT#?}SB>4)5y14r{q3I}XKBP#wod-`$*(eJ -MKG?=~&aG7T3=V{@wn4p-Dvr0rML9k>}(AHEvYJN5_P4Vnt#;Ih!Kxf?Vc%D*i1AAUEeK^W;|F~!i(sY>bC6M>`f>ssYN~RaI&hwR{ -B`qKJ(M^5yIPQhZGEJX@@291w<)^2jBR=(Yg3j4l5N(C97iHBJ#Lq -|31sc&!^dMrrED2VPZ^pgM7H_-{ikOP;%?$9S>LZ*nKDAz<;JUG2aAmOnV=AFGKQzc -Mx;yye;N`(7@jm*h%Y*&eu22UxS2c!DfpZ-%BZ)#hU>A2NPGKqTqBI4%7sb9h#6@x}OKbkDoLXfSYe{_U02+-y8Q|?H1>lt% -u1ualqhT>AFAN1JfT5_$ZikCc_uhcr2e>NEX*g>bRPip6X-F2yuixrpLRkDhZ4Hw*&_Ig*!}s$rg0!! -y-7%A8VSnEmi>LRkPG|aha-5@vcAJozf3>V&U6$x2;B#6!nLH(-VJgjjGMn0N9yO -17Hs9FL9Fko!nv@Cpd>_U=jMqVS;4POm0*r|Kf30w2G-X=_)94@kfWERExrf9Bv*xXwWe&BMw4JD*2D -=7new^^tuOp(8yLm=ffl(1fS5e835wTa?Gk2anxF4+%ggyU+t3pwmDb_lKOtECpUu&9>CIrSwIl*9y` -RmJje3QpcBn(U%&}n4R2@pCEAIjcA*)KSS~IcBow1GOe7uwMF9lZP`K6`be*3MTo)F{HNN(TTG|dL2+ -_Kr8dY5is=hkR)Ch?F$U%)I(LxX}3s6iKI5asM@FKJg$cgTb5$tBjphzzJ_sKR+;JZy;n15ir?1QY-O -00;mWhh<1>ayr7}1^@s+6#xJ;0001RX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJ*X -RWpH$9Z*FrgaCyC0ZEqqs5dO}uusB5_5=X1vmkM1@x^kqAlp_%&?Z=JT9l*`5y|uj|sA_+EXKe2qED8 -Biq~o${dtRQId1lN?tBgg_s<4HIC}JYZm9~s8jY)eV83t!Z2A&IZiSMj*XzY8H-`Ooe}6aCTI -oTxcrorV{Pf)sDBrTeK*E+gW;epHOeIAMqrTgjqo9_+Ke(1ct_R`&mcXrlcdEEMrBZKSW$6+7R!TFVb -;Dxrq;CZZmyELQ*wqh -k>go>@WNDZURlP*^lELKuNOi*{`xK=N_6ZF^(G$b*rD7ERV*VrZc8^kJs6J+zks*GC^BhYw+KwNTKMM -V=OElB(EewV{>Rp^CP@zH8+Dl;krDotbo2`@SNgDV@J=OB>_MDkJl~E4rU12QS-#NDaFIzG{)C_w+k|nS<3KWX(b=o$VjQE#ogD -D>5dFYzdmF6=kFrfO%qEAwmMl$fx=^=bC4(3dZcd9rTKb#+IVt_l%ugZ4%P{!-a -JHWe8(gkqtCx_~~_e5ls2u_6pJs!+k*+$iZ!7BT^y?noMV6ayT+6L`Z5x^xw|#~pqNW90wnRjYv`aHW -q?sW2@8buGXQtc7W -6J_x&E05fe&x3k%NdiAiFe( -UPe(=4~UdZ>O?wI7_BF0d;*6jlwD#)m`U?5;6V^e;`-7Moay)=7%fSaP!3lJRip0BuS;_zIs~Gj)#}Z -rBoVf9!;Jsl8EHb!hAFJS_s3)PD}yTf-Lmgb7*?D_gDTn<$a9wRpx|l%GQ8g?&x^9oHtPKeVVzHxhs3 -=oc%(h)kqhhoKU)ASY=KhNkjkeIN}AySe`gg?jRLQcIRE550ZkY0u~tyO#n`MrwAiKg@U?HnyRw+geB%UvbN8)DAg_Qlu=*KTzW^3Roeo+QW+(d2?v?G-ndrgYpf%w7RB;UtiZ3So#6yrs@hZCrE -Z_L>i*7Kx0g(WZps#4XI*K1EuFm(`oud-$oNy}(}hIJM2)!OZ%Co!yYww~IE!XPneR|#+#yHcu80d0t -{Le25_~DY;@e%yEoNQz9()l)Ojmy!RB|QOh4w`td`sicSZzq)KzAzSw4kEX<=ORjABGcisYJGorVB4I -H_EHkcPT83a;9)*E#wmfm4mO -wP+o@K*=2$|^qotjpvLS#)Ih(25l2=uVVr2M7)OuWZ?^;c@#lDRF=h*^@^YRa6x~SCR!;sr|1|n>bd+ -el{BD|^)kdKu{>tw62g3CaHHQ|+4TD2O{0|iuH3=VHIuL5m)Ns&|*Kgu@3*yelXyXn=xj!{@cj&E~bF -fG)Ma~gMs@9|O*E=^~XV{;Q|00C1u;L8f0;Fp$Nkju-CqU4FxdP$xVOD4g$$+{5hc!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D? -FLiQkY-wUMFJ@_FY-DpTaCxm*ZExE)5dQ98L3juxZ;rYq{ZP0Nu%Jt~0BcwD<-?E!GA&UK8(CCEDvdX -6zkPQkC6SVxbj9=~5ygA%?cv?gc`bG%N%BTDHA@o0ca^9Wp&KcRMzO@a&+IbWQC>ui;)QINc6`UwzG8 -AV%MmN8XnISxOtw&x-_K^VjOAq8Fe#H%l!}!qL7_0`1uN-}EeUL(O`+m|R#m}Ms<QG*4gi@5pXYXHLTo@bWk`(;bhsMG -+m*#b)y%gAPLzMejRPTn)))U)WBPoFV$G;M$yNSMyxCAzL^Bn!K*01WUUK#*}`tSF1U#~N+D53H9akg -HnU@eBZ|1T9U;Q;{*NZA6(;U_SOC1do>%5+iKhgZb6fRee>qhssbxA`xQ}9LacoBWC2_+_a`^V{uknhOby}E(?o%Eu{ -_t^aHOl(ws{M?$@?r_SR-IvsPimVCsmL-`KgLr)(AIa%(S)=s|9JjOpAIUrR87Qk>aaLrZ*n9$>3=Y{ -D$*7_&ls)ETdE#%UKX&llKRx{1B?h`_^l#M#5S3pO0Pj?` -+TFtx^2DuQyiUb>42g|`1ddtuT_#EOkJ;$hf6+rE=laoX@?WbTdkcUaB=;Q(4krx#!!+ygef`7AcF+@ -O*G6&*M4YA>62VvJb1Tc;7VB~x9sbbraMbM}ro)*yaq5opY2(jp+hYO~W>#?HEizK@SDDI6 -NFC5UFUHvbK2Z1TOs31RDX^>{yUECa3;)e1@EGC)l>HctYdWHHMiK#Aks1z|idU*O5Y}7-1QkJ)mLq- -80V_5A*>v;~{*8A`&_^WKc=RHhS{W4tSpIppSESSD|ZC?7l2q18nwQ?FR(o -qealZtt9*n$_1R(4crVpz6l-~$AGr+1p416p#K3 -3Gag9?BOmwcB*`!yb$~35+6oeUf&|o{ji(f&2cOFh7a}*It}kObl{|KZ>$DG)7wn71C~9l2;U($6}lT -?LeCWxZs=8d>ik==8v@o%ygKPXhn|5D5SPF#rGnaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!vr -Y;0*_GcRUoY-Mn7b963nd6iX7Z{s!)z3W#DQUsO?n?vh&UxEK3mmpp64wTP?lAa{SKHa-&;pY?gJw%6rjH`0)P2xGeOv`2O -I~?mi_r8+h};@6k@%Ry?2WFj|^+>$KMilwmCuHXrjlBeWwNcL@5C(i;&hi#vb#Ar!)b6xM<6Ab(iHV*$<+UkhBd&Hu|)h3x3p&q+lDN{dTuz|D} -j6KWe5KqwF==*#fyg$7Pogy#@_MBKc-jDJL(x?w?UqtMf1nP#y|B=E)db_{O^36J4jJsIQpl-9-Xd%RXj@%Mu3E#>UNrk8Q~P#tK=SaFT4 -Y}@sdR$!rEA1&ZXNC`E@6aWAK2mly|Wk@?i0cc4J0 -090Z001-q003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kaBpvHZDDRkUdB4< -EUG7Mlt{U0YEKLdDRb?#+U5GMoBun-C$SmjOdd-XVXe8^yl#oZ{(?M>^f}D>C{8ososq>sKl8V-XS!} -TOTgi(NyU$o9$y>F0S=VJf4FCs$Mrt=nvog4Hf+btQO%l9+`BTPxbRP!H%u`*A@Aw^6$9Cs*H3kMf4Ej?J{Q}f+ -!qF0&3S#hDMUnSj -;d_b*5p(nfpivZQmQx{^aAE*tUzIMSjg(b19g?)$7v{iq_rxEg5!e_YGnSUDnS2M2td7&PpgSIq&znnJzM6yM=lQR4q1A@cN&)dDE-j;2dqM{wat@k=~m#i3 -5d+K^99PSoF5d0y@XS(P=R^@7Wqf){7KoZ5jN~>=vvBgnbmUBpC(ekp%;6j4ZjxXuVWaz-rE@kS(6txEg7|*A#;A>J6hy5 -TCpFjb6Z8c#-AJl94@^8v>@+N@7BqA_oph2^ZED2Z)T3HNRs;i;Ue@InOxUAKv<(Cr=z(iuH86cQm|O -E;;VdoT&3mTJ(ww6hO_WWDtOu*tB3%rS0ilhqCid7)U~!odbJ9(gHMPXuG_pyDDeX>8Qo{^5sji(}n} -~>YGukvf0@7l-v(I5B$I%s}e%`swuKrPIrqXCDoKvNtVNMJRbWJgIif`IP(Mm@xQ4HMPqRK-V+0)u2dc>rl0O;Z -k84v9xWLN33cqm^$0TxqJK?u$lKsfH9M^-hi6(GAV^4{ii7jHYQHj^1 -$p%~eU1&e15Hk0z|8^a9LDB0W{yB6&=+fMhICs6-a6xv;Xb3#nU_mq4K=?Y?sDV+k7gaZa5pQW+>|7k -k6tPX`o*CJkI#(os8&$MGEIVVXai6vTc~z?&l?cTG@P%fvQ)v$K3%Y98vi2M{ixP})wL$=1oUU@d- -bl}yAU7@=h3nwXFPO*JsWMBt0>4-0le6Q>cQLAUV#jmGCQ(E-K(^8{;a018rgeNoM87g%?Atzw&f{xi -&=KFR$b63bb4atIh@`FyCWK2Z6>tRz;jp&UO$1_{&ov%^X#{pF7Tf$-Mh`3c`8*V&Z|A55q&l=i9rOA -He|++xRL6;^SjJbJEt3<^I@{9OJb=k-(RyABF>0p&z-~w=7RRv-k-t0}hrSLqG1kTdA=D9|Dl*SF@-o -|otUW>Mj -e{o(=FxaTd6YIUZ4F4+qr9n4D$FNIk>SJ(z&a;Jl(HCzNj`_V!gwuP*l3?i!+1CAH*64X~Xzqhv~UVS}p(FDkcgw>!PY!4ly -P=~6ho~Tr~Y!WY2Hi?dCCRjF!e(*vrAV&`{`qLR->2U&A=`1q4sT-zq)G_6PCc+vi~ -v&?N+vHl+}!DrQ+yuovf<3eaT%)sKLJL@0@y&fv7Ec@VL%jxqkDVlGBhF9n+J&h9z2k{u9%G2M=zK!^ ->*6}h2UsS#R1!%)>6imxBGF+c+sWWb@Row)pNd%!yes+k|M2mEdKw-Huj5ZndMI1o6(xS-2p|Cm+Ez& -1p{$h0((N>org`Q5NZVb6TAESQBL(dj^?+B2wF5F1ti$_Ok2SnZu)x;p7(Lk4|IA(&~Kti8p7oh!q3{ -XOvUok(n33fkcoo#(JxN@;F#GN5VA8F{eiF`=Zr(_SfRa(?efms%AdELKOzBXZqTDpS@h2f#ZFMM`d)`g_95vaxUXXhb5ruTtr9((>EGu51 -7B@abA=Hke#L_LXdVdo;TIWy4%$Z=0}2Bq3floLVM3Iy|4$@dZL|}dh_Ji%gdd4>EGCf-htMB!}y=`txM*9F4rxQPCxz_5%NP_Hfue=(?#2g7 -dWQG+b6mUP#YH?KYX~nxUu$)UZ}><$|=LqqMm#M!%l~ox!|BpZ8|#YejIzTA^6@{^c8f8$}!DU7d!-s -v!hRy>1<~MoJvztX>dVeYDI(S9Xf?bW&IL -7Ie{X%$(NR4B|k-2i;Fe@T1cLmNklLZ4W@*4^k?&SU@3OSh-Q_me(U`C*)P2*=W|TaN-*C)AwByREdE -*WA}Q>!Efoe}K7z_m5~2WPMSQD$KB4-aVlY~f`J@8 -*8t+DRPkSxi%pQ5^8kV;*5pYxfe{khv9Anf>WGtb+@iIcvVILTpd=QlZsD-N^@DbKhy|HN;SBAKa}@L -9(fGio(NUv(w;XSM1@kWnrxAwu!Fa#7Q?Nt9eHM>tsNeKgV#z;;a)8+?F*Ad!&7bAoBGOhpcZew^6Df -T*VsRaU3G;)w;p5$3atJg7RX}M4cbIZw7&c+(h3x#Sv5gxpTH)(rR=!Scx9zXON7ZJNT2ip1Oa+9Zna -fQ}Pzft!|;$Kc@n!_CS+v6cR?MW4$wjs=?Y{!5B{L7YAjRP+?8&xGuwK|3u~ASDC5?w?N*JwtR -I>YQZ{tje%uK&+#T8a~uT4z~#TtvC4pg-)mn=VYu%Ou@EL=;pJ_%JH49FdWtJUZGWJrfz_h!T6Bp3vQ -*=BHLFyMi{?|o0O7#t7udOp`!piPDUEhTmn1+6!8XHZhM>3QbOAN~hWO9KQH0000802qg5NPRPOu9yb -^0Av>c05AXm0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvtWpQ<7b963ndBs>uZ`( -)`zUx;Mya!4ihB@p_fUqze@7lmPHYOVEAqb=<*|d60vboKsWliAu?OW9^lA@eRkYGj#BDq;z@A|5Wg_ -0F)Hbtj9#Wx!eRU?%KmbS9&G~d|2XIdSWGr&(l^qne8ktPjOE%!I@jMtgW`HVkjywPxD&aae`YI1dYX -=a`#g(!I=TkQ$#(@oWtTA(<+DWs}$rrB&Z+iX}_qRK1y82!Tk*YQ^0a=nvz8-2pZS;^WK#t-H){f%dO -VHG+%J2T&xvVP*VKv^6r@hoaUDPZpN7WO+y+LXNFH97{C74lkcQFYnydJC0d9Yfn?JHw8(ur!yK+dr( -i|4cGj*C<%Xx{uGj1P%W!m$M_XWZZmv)6(v6DM?18I#IfbMd_)`5 -8o&LoLHQbU+6FIo315BAxi0P0RG4a;fXhWiKX(1gWk)K9vEqqYncs|JJ7Bg2A1>&PXD>s{Y%gZA6s;u6l+ZMf1^f2*iu7biCw$ -Cp;FNYhBl?M+zje%0jFdd7CgM?u55h||Ms>4e?N~W@_bEXcj+&@2m+%J{@|A33M)rw_0kSHcFAKpuS; -~|gd^Rr8bh5T8&q8Z~Vinf^TQ1Wada`ZXFhzbRAU&A&4sYlY}Y~Xp945z$h>-3br&_2g>Lv{vTJl@mi -7Bd>bd|bfC&~9?l6ve3;l11RQ-CAwXh?KR67Sf`{c579}b1bvml?;8>J1lAZ#}J|i-N#VlRjf@?O(=9 -aF)BGjEm?(e)l{k24?0%%B~V~N>jZ8zWUR(cV8jt-Rn$ydp*u#M28#;8z2wkpf(q&v1mzAX=p!4Vqr% -M5V#p~*1OO7tu5~CMkYT^$HDD9ND#c{8EfAqCFmwJq(E{REo*97d6BcWSqjRha@^7{&-L;1EQY>UwsoDh5P*6tMPi -Kn9TlRsq)>8_;1cT^2XOE-tn;N@-A@vFLi_M_M}fxhdG9W;o%}C+4XUg@Y{~x0Fzf&zpYa?r^e*g$Dv -gXJCu^;r@J?HTTK%RZ8r(r(P+8HiRZL7Nl3?Drso=NM93cJPt#xDKEF9J_eW!?y~l3D!VrcEGf4UVZT -c%Y!;I|c80_CPvI`xx7$eronx*BTpFml`-C{=T05&x{J8MzV93f=_*Vsj$xH9WP#SV}nn@1n-o{@N{Q -D*4q*6O>9PVWZlf53){V+SpeZf;=ZFzdoPKoiR&KfHZnWk<7zLf$Q0ynh#1(Ni)|cQ`^OZ~@^DJ0#8N -?;5$qKq(Nz?al3#apwXo8Tv|%^B|BA3$I@S|E8hhnbEAeT+T)%=~h1kyGf-+g_MYVQOZ;_t>dV`$|Mz -$WKN{?JJHXYHVPMfvwyos(c%OTj6@lDpqv2@19m;Z0vk5V=ZjNZ$l!dy=6Ay;6?I_a&H|S|>&TI)*aL -t2Tf-kt>Rn3;j1xJswtr$>d*r#7BkaI9V%?er*Tg@D#=P6;Plz4NDY%7DSSX_m8y2O>iN!GRkajdv(+ -d$?VJvv02IE8}vd8}I335u6+_%^P2Dy$K5w~+Wwz2KjwFpdd+DF@tAb{rv*&w>KE_k!XP(-eqnB6>Z! -ve=GTkVz5NQr-D%++kpF-Dk!Zh^=TXfjUR3d>$r;M<9-p}^0+u^GT3n%U6H43F|hFDwZ3`7zVJ`VDix -N%ZSAt!O3aYVj|2pIcbyPRaS-GM23=I!PDWPP0Z9{C_O7+|xnXi|h+!~-K -gGH7ju#>StrCO4Wr>OgN@!m>M1^0@I$6(okSZz>xDajVx~mAX+X4uM`x1GgB-m@W&iN}UXB@z*rOvCT -fFAFZc8AzS+K3XKo?CzArBT3e2^dzog<;Ic!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFLGsbaBpsNWiD`e%{%>b< -F=K*`>(*s(~-0*GhVw*d+zztjn~__(`0vS-=&>2BS9i4p{7Wc1g)rlecyWkNC1SmPA=Dpe%Q50;NgAY -eJiWVEt}7?Mm3d~&zaor%1SZ5tjoMnVy?fRWVmcq7E1h}a=En2%#$rI_*zsa=IK9Sh5cKWdT(8>o`ka -dl-!E4fn#d+KrbHkgVjQ&GK>D+4hRKdi*gTQ)gi@cC -Gksstbeb;jO_RhbI&?wD@k6+RazF6a}#jzapFUHjqQ1+2C^F))GBqU&7H34}aFo(|Z~v^Q1Bq* -=@3(XLw9mi*$zH?1H%}=Nn+i$;rto=XDLN%5tuR=2$f;mwyKYrdKD#)>LGmRI-q2KA+Sg&)@|!AN-ok -Gg;MYuBrpOW{*`Pg4Zcm9NsuwnoNTwa3ZWgT}x6xV!H!WYj_9?9URs|aiuDQ#*7twD;#7h0eW9Bvue^ -pHVZpQ-4O;5V(0*p87oRfPrHZC3Hah9Tph3?y0Jycp>jfppTv0f`dS}lU;PBIcBQ^En!|JneL#IVzUO -%@B5RIH>WrR!{q`^OZ@;{Ie3*KBdzgpV2O`l7@L^=2KY1R3<7wG9Wg%L<{^LL8?x!mCQ9|E*&R(;S*k -7uazFbpIP6&O#Yn~dGZQ);YARLC@*!49VuYs%M9ziaz4Hf21U|puIc+jp{Gdz)Ha`ypEn>leb8qxo_s -5RE)q_`|ZmFOeB1RJ~ORa&1RIb6wQE|@BkX!~M;JcC`ks@#c69bh}-s}*olqzPk>8v{<$i7)fIWDbgZsWt@a<3Vk}($@+O06G{TU4fY -d#IJziR|{D#2oij0a&Rw*$BGvWP+0*zKx`#|xheV)4s`L9BYdsxOlo|MYiDH!U%4WHSq190FRLfR$=e -@P#o3PIVtNCLRqhWujU9i-(na1PuvvE8W}^+GRZ^-ip0CRfbtIsuvnVJa0WQg6l{dg9l#oS<^dS|?X3cg@wS%CdPS_pn?Eu2^YA*q)ra)4Y_8!w8Hy^% -vy#3~J{_ghX!@Jx22e7W+X=ctkPJT~I?{DAT-QV6k&L1A%KK{?cEv>yge=}p3=NI_TCI0hU{O5PyA2H -}NQmE@OKsSQCM&B^QtEQUz`h3Qo;UTCK{s|YShQDY<&%sdew^Fz+(CTZgVZI9LhkYnL;TnW?Wtp2lyV -#E&SZ`%4u&hyOI9&XRA!!yw$kT{hH%ozqp+bw?Rj|kgAGkd<2;MY5VYU9le`#0@Z_Qu+Yt43etN#cAB -i9Kq{^7s$Sq_W*2fcVoJ0BY$_%F%wMBik)`1779VOj5457vBnPy)knp2GTbPqu^uS`A0Y_-w`|M^cSx -OxTtd{4eAI>KVMk|AIG8%5m-Jpoidw*A%d5q{c|ZIEn%B%s>B>y{*7hp#?-+PA*C3TxYd9pt}OcRPmz -DKt$FQf8ir(HuOT%?&UlenF{3vffxWpC2~|dfhcD(wkLMRALtrSlf2whn9^bYt}Mpr_Ez8s5&4|}IfK -^+v{&eUkthzXyH+GZg{C@{^$Hw1zLK5p+N1EkOyCPC`ED{L>xzh?$0n4M8cesEjAoY>%! -}#1rhWL5dlue$ZYT0efL~0erfnRiFU+HMHzCw1W&7s?1&oMbZk`3_IKqc=M1;!0O6iMJ|&pelD3nqc^ -5LFO^$}9w680f)qHLklyXESdE?B)GXE)uoW7p2v~0tXHQmMa!dx -OtehvEt%+e3GhpyhT`+W29IbC;CJ^P#KrVTqX(UHhWhE`c5}XXK$Ez{_)tfx&0f`# -q>2g9IkyRl>gJ2QY3Knre(QJ4*`-`L=8d$}_CN&^QCI=qn{Bgwq!V7x_RB -9irt+2mEE)A()h`0Ke|Ctjp*(Pq0tRz|3^W=A-*kpwzJ3HhWT9T%AU@hXnXDQKS(SG{Bd4Mziv)iw+g -gcV$)GjRW+sHKw67#qS#_wR4mZ!a#-9e58$uP>8}_NjqBZ=uJ0UN7H?yS!EfEwzwd;0@C0XLyh`HDQD -wa}8Y}=9L;km~KTyuW^bXVZc3OS>Dte3+#vxb^U>aNLksAqxvB7NYt)GND?bd8k%0HA;!hpD{9FsK#U --JboZ-Qrf%h}`eYq%a`(ZMvAc@i6iWa>JpXg*1b!-x>77$qpY-S%-NRh~-A%h93mMJulq>ctNMEoZGw -Q?0xI_cgCG1iqBlfEvJu@bzp5gQq!71bP%L6U%Ut8vTdnbLj*Oh%qE&b&K*5f@c1gq*Qlnq?U0`!e;K -3QCuxvO+lNj0dHEwrUTId=JVcv^UMeTtV&Hl%Dz5PZz#;I`MnHljbmCV*iaZO~C_IY;U`hwuWt*C6L^ -a0$dm`?Fj;i=6Frv8-zV&1@M34nITXB;IJZ+(LQgmE?tj#o)rQ{^dY)dP(STFfxH)=1JQH5Jd}QBi9? -Vhwo(4QT+k&k!ohcAX{*=p$CQ#sK}HzU;y(nTN169AEe0B6YsGx2wvx{zY(IDyT5#Wd&TZjK_+f5##n -275@ERqm}jeoVb2UNYXq{QSQ}LJAQUi=t$+;$+e4H9puxa^r-bXPYf@CdGHQ|ZU}@_T9y{rRzZuhIk} -M+G^ltqlA&zRZ}!r_J~1((lIphF$<4GIgcYA+zWvta8ldLZEfQg+NR%G8NPi|Lp&O -j4>b5z{M5E=7j^{yBo3#t^H?CP=_Ebb|%If-L9Xo0_0u+<+nl^tZC6O8_De=~^5Ci5q61=~(*X&n09p -^&a%1+~ZcDO*d-mzKB{eo&3K`ZxXaUotAOT)j7N}`dqWS0xcVmDekG%8b{gd8&AeJ&fTrbCsBZ>s>a} -agzs$(nk|MtI*5wXXV`)=q3F@nQ$v>Ir$vU(*CiE3N;K?vTX=EUmX!!VVh;g#*YXqo-upz>nx4tDFOFk-XvZXw`YF4C$F@AwW1RJaRAg_mOs;IPnxF-sz)n}-D= -0z$rs<4+!=LebZ%XvLS;#HT*tR{=!BS;;gj+tZ5ld9?V|?gV#~GlnmHig&c78%mKUqdz20wK(dq^^M+ -|9Sj)To16jwxJwnJ5(qj^$EvD|23<r&{nP{)V5SGF%1Pg9Bf<6Zr->sMW2SI$EXvP$I2V&NgT -SUAqxv|WHWpEF;<<=Qq7dvtoDjJhFW36{`I4+%?XcmO3rwQT_TL6=x@bFb4<)M&)F5MwaR^T+`cf=CM -{AnjpXhm08{<3UVjc+{g~DAsM;VbqN>JuxT3w&lOB|K)~1_~|DKrlZhD#N+r79yQ)cec?@=fMW6*dEe -T!8bpj#H@UKX+Kv}?8!$yGVFiXou=Yz+0FwCuC6vjwIhu5BTKfU{ft-Nhc+(DL12I6MOvg6~TfQhaAW -#;N!ebv5zb#RBxPYf0NrEmSg%UMYR=y<&!!$_GiQ$ ->oDoDD0<1x-8KWRUBMc!1MF6u!ir=ZrF!(?e!^s!Z7U_Mc`K)4g5f~^FW2&yASR4qsZ5qwBeLI+ZoCA -mW4EVqfhwh_t)^_-Hqs^%~2cx<31XO{{OgU=dHJMLX!dweGlU>h5}pKoWd35+P?1b)*F}b8UCM`LAkjOf#*kn@f=xn01+o-zcB=ud1RXGv5S -dB7KT_IxOZYDs3`j8H#2r|{^o*B5SgpbK3g{HI?=F_vRZ!z0e2(be17qtznjA0N5JXv3A7v#!mMH(#X -1xiM2yU9G)+3GItmU9OSQ#KE%OjCK%ij>JRmO5R-tSR5?d72Y|o_GJQYJ6f;T=Z -!W@j$yI%&-U#Y3wGf<>qNg&(xuSi?XYq&v!8JAjT!Is<^XW2AGi-95zAgI3@|Oe?Gn`vt{SF%7f6*Yt -ONY*5v}1c_6rYDd*ox^-S -R)sNS?le?X}0tG&eqHVpEWtyq+=8pjWTjx#_3_@v^-25z?&m$=j*Vk!1K}xW+@O`Ec)Y173NYgc61X&RkE$u -y;N`tU-qQ^sj;O5?9R5HOnsL}s_ekMcro0>)0%cRs@;F;GgwQB`tp_p2p4ogkLnV -3-l=UYcJaL(gt9}!AfmB;IG(Zdjv$l_EORuN#&xc%$TV7yz-udtp|xgst};^wgKO=0U1g`h23MO2yvo -dkeF6)b$J74}u8b({;ClbEhrt5A(*iz?OdiWzf}7E%MUT%Ooxe1*E{5Y_3nVg=anj%BjF+R$%s8aSVP -tY_y2INvwIrD|6oL(kF4~Fg_G}$94vctcvmcKP{i;`g3d3<3KJ=Glntt*na%W)8ZihRgj|j)sf>8lO# -3zB-s@iyZ+`9k{!ZQ}EP1&0!|nIxi*L5xpQ7pVBd}d1_0 -oIJw`SL!xk4TNRxw|D%x=4taxt!Jh}1p9V0`3?-on@U;m~lZ7hnr1Z~uAO^r*V23&2(jQ{PN4Rc^KPW -kmvg&+5TbAElwGh6Cv+$1?jCPUJwr_S242rp(7%kSSC-77D9)Afn!~P7!^qE{^iv@q{kl%o%)a|5#eL!qM23rpP46Jj(-m^!<*@M=ihe{3wzD|n -Oabm2||IbpZBIw{ujrXZu`F~J7sr;o{hA#s5{eYYirmLYc9(w=mmGKBszX#y{g~cBlKKBZ$LD;5pzN?$UvIF{OK1*198bbN}b}%+$t3(KWLWR*aKJhoi} -?BbEOZuJq~Eb82yVdo{`ancYbYdUR>mIQGE294{v+nYy0TrU>DaRPy;WCO$h2dob`Xiab>rS5sVp!Nk(2`u7A(5q#vxASZLh|guMG#w{PFsU -r9s;y5kM_$ZzZ?N7hGwI&*wf!sBSrTYG7vOxNg8DUTJ(W3f@R;nchFy(rFpmcZVp-W%U;d7e7jSP4<2c>%8-+;~VR+4!di -T(M47l~J?oBykQ@?kbl$7Oc!Jc -4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJo_RbaH88FJE72ZfSI1UoLQY0{~D<0|XQR000O8 -7>8v@VJexA&IkYiYaIXpIsgCwaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!vrY;0*_GcRLrZgg^ -KVlQEEaAj_1X>MgMaCxm*-EZ4C5`WiU!E>>&T)1+##l8gaL$-B#0h$jaZueoa5NL^Zbdf}zNG0(d?ti -}-en_GyxjFRm5KGi>_?zz;?rUCy#iNNQA;&VY1CYLu-z4qk!) -{+%fq6ybsML(U>vRjdSE<5OlgGI#ya3PH}RYNJlP&8gh;x-fPLdp26=V=ADZ-ly<{t)WluxuPXSIIiZ+mf38ken0WDWC=PGDKtHSbI2 -E^Dda^oGa_i2?)VD<<>gsImHS13_LzrEw7VO@1d$Jne7pO4q4XtK{gfq)q*)CoXmCO2rH@^zf%VX$n^A9pr)4>`bG_K -Ri#ItyW3Sn=~?RlJH{Hy2=Xni&b?59hR%sR}_P5F6j=%f>6!!oYXV}hoiqYY%?g$pd1R9J0d` -&2r$!q8`!Zo*a2YsFbXzlo~+IAR+7lVZNMio<5LJ+dC!Suf$@BD%m%HM)2g3$(9!ZT~^BBVeiygl-S@ -E*}oW47QDvuK&W5elY$-S4ugGUn+a!s-^UMnpFDc|P_#y?tSJpVki%jql;Z+!P!dRWbGyB~xxP2P_d7 -ds@DZQ@1_=e{TMi6Sg5qh8;R_!1kubD@L4ZM^9#Xc4M4gqHjN?$Se7^(_p((4y8e@G)Ft=pPzCF=AOT -kg&gw-qz&wr<#>vuw6k=CrPI*0TUH$R?uOp;Ry@eFS`6djTt-vYMCZ;L&Um^igCYu4oi55EQd~Nk$l*RD~_05k9yg&8%}|-pLqVley4} -=|Y)-)H>=!M7BCq&HHE$>?KMzWMvbRRdNk`AhBUGigHy7vMzHa+*36^BF!P0YE*xJ;kV(3u6^ngP#kL9sq`U# -MkrCpED8$1~~UC`ToDAbRvzu{hra -ebxnI$pZ(Au;?R%!9}@@jMQZR>M;`_`uB`0;tst9+d$lwo4%Vq&uvn&0m9p1-t>RmaGr{&k!4BK6+jg4Ob{sj -MJ?gq*5v&UsU@@%`NBt)f^0d&@VYZpJ4qOS2+i-v?$MGp8O84YF6>Qj+#2|^!L)&Oma4hNlxnnn`2QBvJTmxv~>VpTi)=mE4sE1X-``A%LC+czY-}1rjrqt0o0 -ZXv&*mVi=Dy>JrZPU)PWDx8O*{{YWmEdON^XTO4AHNbqv~leq1e2iU%%R! -{rR|K2OL32D-9)<7ejgxzdy_VI&fvq`~VT>jLB(rK~mXnj&sCJ=OO{(q|}RsU*)AeM0%%m4?vk|cX_%n2d54_1!qAxFSX2s#Yl``s!X|X~_4=PDmsw(v{v^Ekj+%_Ukc*@q -UM)a4V6vHMg%F3i>*f{$R<1{gruY1ap#1+f{s;aoDA?F9+z31WVLgCiZS{DPBi!or4$Qi3b@0)GtKqh^z#fDp2LFkt&r8xM5-em*P)M`F6WoYdmtOae@Kgi(8_#D_UgCgz+8D4 -fQ)qJ@0AONb56PlerOO?pM8(<>Lqb=^yr&XU6``WjN5nWeFW=YOdB&X3#)cdFJ)w60<T@Q -lsOR+{eTxG`z&Yg^6Zfju5QI#Eh_gmSFxchj^R1z4s8&Oitdu?G%}#XZ9GKe9mD7I!p;%&4o%xmQ71u__-UeC_1ZZriGG2i(gdu#xjx6s}05CV2{3`WB5_74!7=aL* ->$(9H7p2mw_PKG+9b*r_E8+b7_I7jqGh%W&k&Z-%3$ZU8W8_Ezu(+_-`vI -bHy7JK&hIw!zj{!{i}_C=@$Ncbfyd)=N{y4?om*)mL3gaK`OuKW->>FNec!fF^<35sn+N{|P)h>@6aW -AK2mly|Wk?Tdy^pH}006=f0021v003}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kV{ -dMBa%o~OZggyIaBpvHE^v9xS5a@Ao$&xIm0n%ZZUTjIn@9w_4`$!!f9gVK$m+u%a^#*dC#$2cQniWE1K8r=JWx6aiBy9P>K&E#wBp{WR -JRglNlq!L}n2dd<0H$oCbum3TiA4gzT8MQ9K`gc>#Yri%EM2}nNoj#PNkH>7`z1}n$#`^x5v-)xvM5@ -Wx|9$_EZr7DYQ~pJWavDy_oI=P57QCD%Pb&wL7b%+cC+H`eE#A3OyoJlIu-c{zQ#~!c46Mmq!e;*J%; -tzT4R%m^_uvoZPuVK@d-%#HrLs=jB~R|LSnNA=6cxvlPQ-*jx$H`03*%!*DQoY%hsn<6o}Hq>w^mu(m5H$6yCU5s(S3 -`kW$8rJAUuSn_4|z*39(>-%@77gsko^p>b+DID(sf$pGfOQqQo*d2|!C? -IFmCe46J_yg6320>y8txHIPe%N`rjam?&?~vNdJ>=GQN3an}`@IJ6ywMJ01Dv!MY5wc){f7&uZ8ztZCiQoK;?-%E-h;CJu`C%$3NWfZ0plM)d6^3D?~CT? -9dJTdyK-i8~pOy3=2}dqPt@xV7oMz07d7KGll)r;!Vd^l%@(Xq%b1PsK$ -Fl*V-r+EGpp8oO*bY_&vQ6dcx2M^5)JBw9QaeakmFf-dQt^go8RTmiG%c0C?$+p@XQipOC2-N1uG>bP -edc(NFK*&$3}_Ehw22MfOnY;Yu9#X(-{8Q6^^7>vfgl}nrxtIft-2%3&zm -xC7`^dB9+*?dh(Qm!3NVH+HA42xicsx*}js;56&*RBZjs_A^OR$75R!8bOOj#ubp2nOfo66ap?@>3Kg -M3GC4|@GNzFQ3uSYhV12t$*Qw)>Eua;D8pc}OW{8Y|OZ?z|G>_ytx>Tj_vR$3%JGab4V -$wSy{)A|m9WC|*LJseL7Nh&dla4t)8v@#GYa3S`7dIZYKZ$IsgCwaA|NaUv_0~WN&gWa%FLKWpi -|MFK}UFYhh<)b1!vrY;0*_GcRLrZgg^KVlQxcZ*XO9b8~DiaCxm-ZFAc;68_F#ftyT6lp`~-)9c*nst -=AWPjs~EmH>XAzx$9zZRh6+kpB4n0_lJP_kwjz6;lO50F$cfoKC6bc|nz7ZMAm@-~lC4-6ND9P> -#dFFf9Uw~*(N&PglEiG81sMavh&x9Kmq+l$=0L&?4ewd9{)ZBpP#U2)APS<0p+Z3ns$};{i6CYXEOKf -pz`9J(c;z|HV#9>uA(R#?wMSr1(jdGK)=FO?gE*$q&~c7Px}T*q!A-fqNrG%;w%6Xtn?5{FqV@4c%2b -3gqls)1z41=_ib)`nlNJZ>hrZ+d40{TLq!5c*Z8b$*zmRXYL4ryV_@pc?SEPFmDhd&|w9#^5(_kxb-G -j1bhh+z#K^!~7Ri1>yyZ_}`3|@KTg*=fNTY9Ack0rm-Q2H#9+&5HrWR>x)GI8POkVn*6Qa}?`E?d$yf -M6S-DDrNxxFH$+EeBf&k0^iO{+9p=o*>XjK<@4YlQhhQ}UFrWq5Lt{{x?T6|Me%Mxp!$CWggn!6VZeOjVk0DJZnJ5|KEaO=vI1Pk -woGC=z-oPS<*c-y;ho$x;5IPWewgs>4br=r7CJ|v0paYO70ve***==Z|u!Kfu8Q@RbQjj2{71|*-&NN -2QSZEqUM}k;zvI%s+q#QiM9w-qkqJ*wiuuH;MBw`|1#<&z><<;V9?f_uZj0%B{uyO!#1*PQf{C09Z23 -JZ5yF0%R(kCXV1OQx!WH9N9t@DgFvV~CM%m!gIAxjYlA_u^TL18yXP^lJ{UhnYH#rX2<{nf$;js5e<& -Aaife>Ise?(QIsWDK@LL9J_Tgb(J9aE==v#~A`SIlycP2&Wxt8!Oe-2XUT9e37srQE@zc^@=9mj@`4A -MlA4nwtj_IujVv^eCtnxE%hf!WB{*LhZ%;1w(N6klox#PNSJ3#?tS{0#+*q=OTAvtQGxCIt6YK`e7^{ -bU@3TvtU!b@P29gGUv0NJzEbS->F7tcY|en82_%`QL;8+9EoudYI)xF9PQoLb&9334 -Gy?Z!T+Vn4QnAFq$7u-wbUKc>11yJzl(PytEa>eg}trcce2-3`jwP4cvYX79yR|&l!vX} -v}p)m}5eU0MQmOi!B@=rd)M%$_tyq?$W^eZ$w{WF@Kc58N8YF4J17CSFbvFUTaw0zg$y25k6_lu-(Cb -2L;MzgB1uEe$^Y;J9goR4n-k+bQzF5<)Zc5?Z%dN8Qd&TbZy+4StHlm2ErzHQLoj{o(3ay!1L+Jgh@$ -jh!LmuHhJe{u<$C?X$uD(oGoRIoWr04iUXrBOehW3Uxrr5X$s<2$Gsl$`wVlWNISesYlM$J@zb?7zL5 -o&PkS{4xf{CqKOT+xL#cGq(0oh+hS_l4&kAL6x9<)Mb|isc+Ka$jV+`S~x<7` -j5N^txp?d8yYaB13Z0h=4j745gw}^*DgI-N#RCj1?4M>VjYCXGhXsPJ?1jd`Kr7bpA%RMn2dRH6%Ra! -n{-!83NK64NZIL8fj^A7(4B4W1QNWjE0Umvg7BUsP`r)qmJ!50PHo=`j|j3Lk(CERV-@4_&>-r0QWon -Xa^=@vNXZUP6_dOZRazW)zyzq+G6Dyrv1wf>_d)$3?;h|0PI3Sxk})bRr7fo3$q&2MXZ0- -2>IF0vHVEo@|ntftn}e_g|2P}B6u54ORG-%-t1{c2a}9^Kt1w*0y=3`Uqx<10jli%SNiGih{M(Y}cy0 -n*vNLPbmy#j@YY1KQ;T&sRoDQ?%T9aqsQ^Moq|o2*tABS`NCG9B`?IrUF1Top*TtN!{^N1c4W%0Efth -xOZzJO6>s8#?-C&F`4*dhD;(oS-5)iu#)f1gwhaZsh{4f=3jXS~Yal+^~iW8my@)b}`O4@C0&Ix=;6Bp{bUHIm(AR$)t}{ZoC>;^>J -W^t$`{PcvRtM!FmmNqkJVsi!8Shq|Ohk#kE$XPE_>-RZ*=Tc8EgKj@$Kxb#uXz*VPvBcEZ&L`>hmd>E -s#0ezmmVYOJ$`cpE?|SYe$R9iKD}(9q%~w02E-*38<)6g&`fLA7}+Ju(_T81kbaTLT`Bj_!B(%~GLuN -T*g}k{dOB<0)oMHdfkGU4LyZ*o$)*SDWVoR>&T99^w^34@8jBh`7g4ZJ~dokC5c>pl3Y0>~$m&W2_}0 -)xJ$*K|6@m`Ghs?`oA3%uyJ#=0_m}<%w*9amlW%I3w6F4L<9!LO!u -Vt46bw8>wk?stYCcxzOD5|E{9;K0!mc7(qUz;o=yuv+_PcXdeTD -F3>se(PZU>Q?<>!$05$w!<+J8*&@AE1J#uTWoFU{PRb;p6Lj8A84iNBA*VQp$AK_d2B79O@mQ_HhzH+ -UoX$q$F9pDw0s6p1mNKlYd{2 -5i>AL?CkSB4GeXR2c32vM|j$(hBJJBf&<#2I_^~|uhKwOsHXTLWj0FSkL2Xf<6H}R7UT!8t(iWN7`pU -5#*$s`6flbe4bC$LMN5_h+1|%NVRRV)OUne0e|`P;j&h5Yqa2IOyzE*ny@5Wi?o3?xphj)!(7B+nO_= -Rd$-OAwrZw3uOEnf;POip_pKr!K3`ZyJfJc2S^^MRkHM(M0>kUa;oa^@RbuCX+H$PAey<%3z%+vSIr~ -N^{F1m5q8VAXzwxHg{bPfX4sdhVKV9Tr?;b7CA)%8B@1)s~S->tN-oAE|C9Bc=xb*iENlToBG)(DS3f -^c;{Va{!Re7%hadv2IKYImr&D1O5A-v}aZGcca^ap*KE8{cbM?1^3*&W)2Z7yk^%J>8=oqba1wfjT~F -T!Mzb34Pr?;{^)Hh6J{!Uh{3Zkm#`|7Fx&Qg*eU1k65Pem>q@kv2o50Q<{W9-t9c4FD^{}W~*{nl_Si -qfo<^(a95CF-o91`u1mTJ9vHr(hhYsTIsFD3<~B7p;tv?~k3sg;!IpgV0DE?DtMS4uxR+WP$-~ytG*i -EOfaiLpA*^jjc?Bo%`ni6M7j#AjO>X0@z^9T0G!RY1RdS4Hm2E25JTcs$)R+fR!7}Fh-KlU3UNt8_IT -0M119Z=U+XgB$u#6w?kcQz~6xr;i=_1+C&P!J3a!#ko$+jli!{o6_qGsS7PW>*~7Ohyn%?_ct)IiVfsU_esn=GlBV -?Ej8}Vp0nWcn+#Z*|G}ZEbvE`5VxTlr5d>C+UBa+;Upq;wADpQ=qy7AUP)h>@6aWAK2mly|Wk`=*pnO -mO003VP|D?FJE72Zf -SI1UoLQYWsE@zfG`XM?|DUTRS;^3%@6aWAK2mly|Wk^QBrf2X9008GA002G!003}la4%nWWo~3| -axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kaA9L>VP|D?FK=>WWpZU?Uu0=xbS`jtwOVa&+c*;bo?k& -UC~PkjC+Xd`*xG>4-kr)F{CP3|YXrZ(#a}P+*DJ>JgcYaaG(S6M?_ixUju -<8VJr`l=)WZh-r}uZ -bZVW97HEJzutXhzsOkRMj($}Sr#{!m$6F3oX4d+z#hPV9;@t9i&z%ITz>oN<;(B>a(Pzj)6E%!pN&=- -Fw=sAtQnbSE6SBjL|!s(*s6Xz6Q5&IlR5{lt=WdaSY%R0_M;W1UjwxI_A^LOnLk=@Lp~0UCLryH?g<* -RTl9h@bNW{4#{s>O#P#0PqKx@z+4FKcWY8AmtDsuY}tGwi}Ilf`Q09D8i1cX_L0hS2}vIK%i3oGIUKf -<#o{!wl(?lF9cxRe>}xlx*8E?kO%V|XUeCpfRgwo17sX~{AY^=TsHRjtz#jespuX&~%U9N>_EEe%Ux4Bas; -chWEeQI^ULC`p#!=~jWacG=9cyXEnY#9>HXDu^O96}Pehbb#l!AP|PSxM6Jh?)~kjh`qi2*T;zceEaV -QXS_sgz`NkOa}S{UqlV-UKc^u%yY&*$ZgKc3S1~n*NZi)A#X --(U6tsfBk+Y7*j8vF8x6U_GXMwyLy1UgK5G&4%22dO-8~y90M~0-YLx6O)T4fuWT+`>JY(Op>qc7=lS -^R!@l*&pz|)ENT!k8%IC?1J#Too2w!_bC!o)+x)Kh8SiHakq)K@-N7@p$6G3p^ZIK#Iu!o?wA5 -^MF1KJ(YwrQyH<-jQwq~_@1d`DIXT6#--=%gH1|gch|bM%C*cTf6^@n^sSsOuawzr -DK@WK1v9icl_R!No41}9CMq;r532`@2_>%Mm?3a2T#vV*jYy%Xbac+7Z0+SEL3I0F=@e#1Rdgx6puOF -J(h7GuJ3A>Wtk<9xY$7&v-TY6q=ITui??_65$X1rHx!k$+f7T%>Wf^I!Mgid{p_()#O;h2Co@R@vDVQ -mvcDFFONI!6VbmL}i^b2%n0bpUUzPu6jik=!Cm5BO35!iiWIgGmawnksg0c%;m{Z8WD$V~qv?kn0M#9 -U}Xfi_g~XE$7S&RZNH1S&!Ch+XOgb9bMD603MQ~b5hR%Zq6AW-=->=HMV~w4&zE|A7$4~{D8qzVJG2G -5O@PcJGxI&qzTb1U$mpwF{LSB8fc+0T|#j=rb#gGwDBWHd^&E%*F+&hZh%N=wVwBPG{`}RxXmd*VM%kOz?1 -rb<8jTew7;OXDfoc=zLR@LmPDCOr%F|@_H4*Q`YjY>u97!%7UmVtq&NmsTA1NKAsv`glF)qmUz($CBU -Xbg4#ExNRYrU1GC8&jE;u@S~JR6-nTF==Gzpz1xAU==+Tx2z|MLuS7vxPKFX@ZAOmv4gZ1B+ -y%yJdd$wfsWxOkhoUn_6DogJz~i%+Ox%JN6ooEgZ6lLc{Q9I-3Y^b${JQW=K$0}xf9c{C5GUNj&u#)- -a#!5tqM3U<_2`5y9a3;5K(D-(}?Ccz47NS0o5Oy9SUjv&$#CRc@k -auixOsNbZKUNyTxKMe5(vi0F5dX-bwr|>~Do5nQ_oiUp*pm}kmmI^=mI(1QCgUY?kiI*|SQyaORle;2 -u?9*KQAy*6J`5}Wiw*Y`#C&3z77{Qk#4OJC@)oi93<6ESqeD_gD0#MUcO@DZntkTo&r+1h -zf;mLd+bk{BVrRRjIUypa-qupD$&2xYb%26(!2nALvc*=PuR;y?>C><<&t{-1$Bh;Z&!(}5OlcpU}@h -FVw*!Esh#bV$B=Dmv&{FVpy1*FV4-Nhqj7Bb#LGqnu@j_Se%TP~YY@1b2723ZOc)QZY5gi`J5(s`_kQ -{dowF_E-BopzwP=4a8)UQ@IprKT8+jE=!0HnoKpgkHU{cizX=1lK{R&(LiU=}GlAGdqr@8PQ_JH_%(O -HwwFqztfm4KI>hv8m1f@r%*cjYSN{+9_&me_pj&mk`GIuv$*qC90xuK*_&``gsAHNP;y5- -c!napnekX0(_7Jp+c;&Bds79TyLs^>$ypE}@UkIh4|svDa|*|3|!PU6CB5I1V-T&+sn8&-oK4%8OJ&v -2JTC_o>`|UqV{Em4Q=ezb-%P>frxPpm&!7W5dn9mm$1U!H=z9*xi!%_HX_@6aWAK2mly|Wk}Ol9VP|D?FLP;lE^v9R8f$OkxbeGx1*@Q_e5Z9c$>9ndHbtGp*|?Y3sqMYp?R6 -p0GHr7si>^q=@fH2=of%S3%TBtj5kw^CJsggPq&(02U}P+f6q?Od%C;%AlLd>^Ofa+LhNYWCGnKH1om -s-;jPYc~&OOg-wN9?odY8(@(l9@2v$MCSZ{NVbQ+6qmuY4teNEnf7l>qL8NLNy82|T1`OOcA{jxAE27 -%>ajJQaeeIg6G&U5J1g1#)(5P1Hnwu?81!={#R*~eukj+%IS&77Og97I= -2K#)o6wDht3a?F4W`f5|Cdf{(492z+jNO3oLYq`Z$Rq%)DBjFaiwqpgm2@JIoLr%SYwKjA!3b0}U@J9 -~b9@T2Y`vMrQZEBGlgN9zF)*lcFcJw;fhB*0nb0DRkps?*HMh`@kWl+II@LHgp=7pQsuio)0$!SL(gb -7*qM0f1TF^QHnO{X@a0v0|Dvs6G&V4eIXq5g5=4}EnK2=YG%(1hbCDxA|mGD -HImwN5gVQZ!l{n12Z~uvRHa#26@R-$4y-du%+snSAVydaOTY52N8P{p;Q}^SWc`djb2{pWF^VOc-E{x -`WAQHoRfo!Dse!e{dbJ-lvCAZ#-thQR}3C|8Upu!C-%Ib@$=AKX}hBfp9RKu)F?!e*#=5LxvI^uYPZg --0pj$t6S)HFZ*}>$>)IG^d|#g+B&%zj#!sHbVrl^)rY(8h&_B5Jq*V^P~aN)4*G+e5s2yC_Xd*=h=no -M`vp2|eA~UdBU#-Kp#R8fcr|?ZJnFx{ov_>C-E|K}FMFU{_wugCNEX<2b=U3R2kg3g-+hnzjo1*_j0n -x?_wlxeLtya$NLcr3(jN}clB?lhGJ}4`nzrOv#E-u(Pm^EjB@>7e!4;Oc`2+0m4GHF2L5)$4*q&Wck77)ddzw%*x+HoVE4Gmi^G&Yq -9Pj}hqbZ5|42)OTl8u$O+b6T$WvzEV!mWZ`z)N0DLutUwt`KTR>RkCO|=;)J@cB(p82%12%y{Gjik@! -Ao&>Zctg6E|0I_3smL#qxXhSX@xv%dG0KXI>Z^~c`{`>UN19&haL^lYP-XB%7wx;r^7`?I -SMmDsyvED=A!eUo#gRYwY!R~e;k(@+bm+JYM_2$(BV%?&UBtNsVd+LDB6nE0TWMh%(^6|UDA`hXQk#YyNAMKc5i2}aNi4l12@D_Q -zl}2F;GdxjT?C5_$8)Rm3AlRQdO#)&`V>$8U29F_g66j-6WNV$z+HNgo*8u^`a;sqlY3(heHwVMhut7s7|9Q7 -YEhnQmZQCe)S%j%yv@C6o2WAX*qY`Y^0BOZhg;@oeJ&w-sL>SIZ&b;{c0Ppqt&)HYf&DPdX9WVJ5Rp) -I?1c0BLrBvOF8LnK?l5obqJRB&%$Afn*y8c>N|{@Sj7#2e2r3xVKydxvWlEUWN->^|@TEd8%!oTv7kQ -_L~V{4=)xJwa^T)-GsL^W*tUKJxcMFrxqPMv7BND!JPv90tanX!)A?GA0S=DA(VY|EM+NHG4*$lTk?@ -kj&hJm;hEdX{oO;}Bl7iOH(BLl2@*1mE0$YG6XhlBsBR~dhp|YXtgQVJ;-PJ~iO1FZewAQv;3W=YSYm -NaYSY+De4+;#J=!%~YOZaoiQu0bu_2tlQKJbZ73ssj} -@(g>%8S)9S1u`j^~L5iyP?evnk;mV+Hk>iUAY%%VeqRPh3A^SHU~VkfyoN&QANjeigi942^syx7tTWj --$t@AT!b7p0-m`>1I5N!J~7P%q$Pr$mc1Uy`Vv?(rBraTl41e3P-N8k-ciCq*saZxQNvX4@(Q*UeQSA -?PDTJlt;b)SPeGw_&q`soN^I>6JFj}x=#JW46UNofB70kt=F8SppzzHX4$54p~weXfh>hH=0xQ4w#fWvs$e@Quyq -NCy^jjym_F#^s{NXz!<>1mLkJ0s49go&HmwwG#zB)w_ErCrgf@Xq#$EFw%a6AwMk}ScB<*Sfqe-MV13 -jtw72`$V&fh0IObExTmabT!jqkk@dZc_JD9v4aR;3*7rFzcA%x*xn)m>Gm8vjomm=hwP3X92vO3!xd} -dJXTr`tIzHK-(cZ^BT>{JPHvmnU1nL10#ukAk -T)(CRj|exbC+IpiUBBKx?e1}cA3g8vcnYCN~FjHWQ(tnCSQ;>n$ns)OqU^LZ-H!W`m=KWcVWiz1qNx; -9-`BlCcn+>jXF?EpX(XTlcMVs$-8}um-d(WMpDHU7DzP)XcJyWQlB6q1-;;`6hQI1=?ba0`AA3dW&QUwaQQ#^=~3+D)T%`6r5vS -T8=zEYj6N1#h2&mgW-OM}fbaNOVSPSn>}irRK4bi~&pQ?RftTBc>4E9#XrFEq^t3rq5RHw`dk5tYRht -}!L69KInq?mjEd1^ayV# -2%1DeMxHhmZXoW<5&7T%eCEy}6zfVCiFvdk?yJJd&UyvhDltPIHVpjlc)9>--+lME#a4CR@KdNnY~N_JgK -B~(_V1%oaUlzG9tQ_yl*$aOLhQikfqB88*2LCXFL@g -3dEXgh$UFv^bH1u3f>&K{U2(&+Gv9xv!SZ^T~GqulfLgvz&Ym3*Elr$O;f=i%Ze -UW64FI1lhQ{}zW0yBt1dK3{ACR09sB&IV2P|6IRS@;#kL=AcDWK&Lx}F<+aPxe6xj|YLI&Afhy-R8N- -^@d-p^99A&@D}fHGCT*kRyON@16zyRxPvOvZs}cW&NeMn8bFE2Y2WE%%jU?7%12v3a -RrkM?`Td?w@pQA7|H4DwL1cQZy>gK=L3u1N7|qruz&r#-TDtuO9KQH0000802qg5ND#1S<=p@P0L%dZ -08Ib@0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb -=b98QDZDlWCUukY>bYEXCaCwc6!Ait15Qgu4iXZk+#nOY`1TV@Syr^#wvdPp2lT1h_yY}tvrYckrHpj -`#_kEJ*`v%L~;cb?YBkua!n>raX2TYTP3yHJfiL$uC87;KWnRADr#f)m?Numza(5Im!c&_VpWkt)6WA -!|0^HG63Kt{q)q7k>4t574&$sw>Uo_??D*Wvqc>X$0Dm0uirru`SX_Pv&jBSp)ZEDWc4ThXgUD+9Wo>0{bYXO9Z*DGdd97FdZxcrl{oQ}X$Wi1w<6H=7DYcYJQGg -bSCJ_dT3e;NP?ZsQpyQkfCjQiL3&F-CjjssL}gG8MDn2$Ga-j4hI{_DA4idJ)}Y3Y={p~>WB*6;Uwy#u;5UXJPYTsbOLB?-Tmx{#JUJY7$oQ>AjLo -h0GHQf>yX8C -N@p}U&A!VXW{(EFUS0|3=t5erN|g&QpIK{c@>W#K_%R*#;E`$^>-BntENNP3$Jk5*R7s&n)WS6ae4nQ -)RTa6gg*!WGZUc^A`7pyH^4!>hKY2s1o?p=8lSki=$jONEV}y3v3%8myy)l)`l=NlDl5;z@WzN?Qnp; -QlR^zG(*gK^9{$uz{m86ZQLdSUc$4bAnnx2cw$>{HZu7Q^5WziLtr6^oQ|}+fD7zNp#>x0&jBSG{w%Y4iZkF07gJEM9Np6q*v|Ea8-Usx;x -uv9olOKM3@^DPz)~JFCsX=2$rbH#|EqFw;H3BLlrxyk++FI~)p~+~u@kcd+F0Hs(pdUcdb8<_2E7xpK -Ixl9Ga&uX9OwK -3KT5n(I68yQGDK8gvk~Jq;!SYH0MVkC__2%VMy1aT#!sAdKfDAoLf@&*Sx)|VsC9G5^ATU_XRX*>vRq -hlxp9?E;k5fpwwgB0|OeJQrB7>7Ql7jCrKaT>x!*K_=>2>LMAu|0DbOplKLcb)w2ZQ?%Sl`FS$xE%q3Jq~TtX5{y<)W5 -J3uoFw>|__GE2hc{>=}h}Z%V2+rNldDAvt(lvzv@otJUzIdd$$KmB=NO_QP-h2V8529Q3SA}&a$ -qxWN{$ajfTk4jw*zErHRtU?Xgkl@i&6v3$0_EfNEopI|pL`$9FzpSs8rD2HBrR>4dGMap`x2j>9j0zP -x(%?Baa#EbVm^0|&GC@V3lMn|Jxn>Y#r`Gq -$ei-Bz<-xJ@n7=iF_~q>RR`XssaWlb5zUsKUR-mLEYDSOo8=P| -gRFgwt?MA7dsph2_u&yJh=(?zswL2*D~(o!A%{|PpzT=K!Pc=io>8)$Num*P&bFi9;hEWvNl8EI5&Ub -@eM$PUddFyx2*<)O=)x -O+2C`R~pbGrw}PZ$P(oGVNhaRAtfY-?(<#90h4OVBaf+wYVv&Jm&V`yu_?N -`9rH?a$XMTS=X?CsK@*CwCiQZ_ets;n1O_{_fOO80qw&dB;hrFg4)GIdr=R4=LGxpc!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-w -UMFLiWjY%gD5X>MtBUtcb8d2LcnPs1<_z3*2<;xLfV{s9Msgg`P%(LrRf=Z(9#Axgc6N`@pskQ)iV#n|;H+Bhp=?Q}A|mZH+3m`NuPEN0 -s8{^a`!jxdETDLQ9f1`=R;XRSL-y?Y6cjp9dq60o8jQ)3j$IDHR~TXxB5`EvL8#ngC~}wlsOyjhU0Ny -!X6zNBIOczjVKHx-Rr!?DJ|alM!0ocy4iy@bDsuFm;am>o1rv&~J^^2+PD$X`%P0|XQR00 -0O87>8v@*v|{0I0XOzGz|a%H~;_uaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!vrY;0*_GcR>?X ->2cJZ*Fd7V{~b6ZZ2?njaE@l<2Vq0@2?oaLzAi-p}mKbD)GSWl{<7SprtFFgizupPV2+9Bin)Pzuy_# -Nt#ktR|(q0<8NlZnfdJ6=pE#F-MZHBJO{CBv~j@JR?F7$JUmb0Ypcr_?k3Lc7ZbowO}j0ZymZ~pMoT$ -|hx^;xo4X1BQS!#YjbBpev9E`rIV;x!1Hms%-Y5L)Gx(M`jvgkGiI!$VBrlWlk@YEgQ)v$ -He*{f#5(%EHRD97GM6H4JbF4iV9lVz;`L6|B)lYRju^;z1S-Gi>Mg4RkC@$*f%zTjv@n*4f{y)x(z_x -d6TX4}V64JMH)auJr-qAjJzGSeRpI7|ol&5-|D3_7;qf9g4!!Q54{`2b0|SJUS@YV(fC1m*M5UltOK= -giKkCpDQQ#facKJY(xx9`~Q)@i@skOjmCjl@Q^%Fe$R7wihmc`Mf!B{-0z -6v5{5nFeqK(&4QZXjiW=mV8yh=!&7H#;#)qM)+?F9~+xYPmF3{9qd5MlB6tc+--{2H-laT*TNx~v^NK -z|32mYFZKQ?o;O -5F$q4jL6mZT1vCIe6H+Et-AKeh2GtR+y;}1KY?jvj@j5Q6?PE;Hs(+#c8C@f)yLZNVCa-RyaOIfX;P8 -er=I?g(|UIQO1Wf5>{1)TZ=O-c3ijaGKQ4@b$kEE)ouRg^7`ug?J8g0d|lpuU(H9k4j-#qQ}R5bUDBw -`VH|>g8F|NnjzdHl%;L70SGGtXu@L~q`h@#lxe{EJyr(q-I0=_9`25MW4JuAiBwKCWJf0rLrA3kc+8@ -9J%BEK)Tnkw+=>cYutr>W|<<#6>4{a2@xK15^04x+0wG2bM=Ged-P|5fXvB{0@8Vr5kkPI9+!ot$9Nb -DUF;DE7_dd(zYX;~{>?-ir)fFU((6O^L`k+mpLJEL_pE`kzS6kBX%?{_w?X7EknJJ_|@fq3XJ2(;=N0 -f(cO2~4OGoq2nqjQ26c9c+noem9y(A4WC~6@tQCs(UBQT9}93ncX(3w!RK41wYwwsOY@&3y -{`Qz%_^6Kk>Q&tB9w{LKa8lxSGT*(x2gUyC(4E>_3;W`oy#y%AKyp#QC6g`Kph;T`;Gbl`NL|uL(v@L+mcI`=+;NB)f -6qA`w!h{i>B+^e|GK#%PbU!#6F98698AOGVc5yp!QjMXQ9KloQ10@6aWAK2mly|Wk}4yygu3j002-3001=r003}la4%nWWo~3 -|axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kb#!TLFLGsZb!BsOE^v9JR$WiqFcf{yuej<@2pn%E?G+ose>Zm36MXSM?r#DB+jQs;vfrmZh+>~qgO_cJX7uOLZ^Ml}MH1ZY)rp@5{4myN=tef -A0iONqpz!wn=oIz*Q79KH57ifWSGkZY8Yr1uWxd7l=Gv+q~Q*~Q}9=ktp%a18$S1FeG`i;^hxJ)HWEdmT{6`kcrg8F%`{6$qgI_sE%^O9d2yUmNzxLN -jAX0U=RCG-v)RmkU#@gGS)(KrT!40of+(#71paAID(Iv~o`uIX5u}1Xj@W5CFJQU65)CewI<7pI;Afm -M%oOD;wkDx9@jK2jDIqB2C6`X^#0gjJ|a7lt5rDQ0g1=pjMVigcb{H-oPDUIxN{_D^2f=XZ -2dGI13w5mNfmae-ItbMjrc`4|2v!Z6eVil$L8eF;by6k@Aj&_iU9n+AIx1(;6LrwFkK)<$k1PVA8zgq@n{;JM%$oc)w}Gmy`aVJ!PIwDQYIB)8BT=+ro8ibw7E> ->-2s+w*qbSz5KJ&;d+Il;2tWFB=Xm!naPGH^`QM9}^1mWw$2REmIN -SlP0=EZR5`p@k1KU`5&rnMP1QY-O00;mWhh<39@YeyN0{{Sb2mk;!0001RX>c!Jc4cm4Z*nhkWpQ<7b -98eraA9L>VP|D?FLiQkY-wUMFLiWjY%g+Ub8v5Nb7d}Yd6iX7Z{s!;y!%%?BnMl7BP@`cfS?GrF0zMh -gCZ?@Q)ubQ;)W)*BxT1a`rkXGC|kC(1?r1Tk@G(0%}|XsTPn+@_r2w^B)RR3^(5BLsNQoqtY=L)mgk+ -Q-o%Do?&N#EH9K}Ap!L2hDBwg8rP{wh+Fza -twF(dqLZf@f9bIaA6qkYT1WlMrKa>tq)srXLGHzq*1@(4pijC>YxMJKG-hTS1v_`S4z4fKYpkRv~)je -op7oPf7O8MKpvPzIT%k(Ctc6zwiW+lxbfgjoj5IwZ=6+TzZ@4Ly2q@)@pX%m^cZnPDC5^lrZp+dz1g^|&ys};QqL!$-fo1C6rUY>tv*I|mBeb`mgg$jN(n*U#CR!GP6 -r%?THW^wa}7Hh>}7&UvgTeNeW`H&?|_a;laK`^$yY@@EqZV=XDSh@=lJ;t*W6)8pOB(-A4v}S0^^;N~ -rHN8>?3IT(d0(|xEt*C%!U722^J}}0ia7R(_VIr1YuRNqdsg^NA669|hQTN6|%wYKW;9ETI&()d`MRJ -P^eC*iDCW)%>hmKFDUX$(eFsZL79bSDMBSwxL^;Y4;{lt;^wG~e@`f@5iH7G1hgRbifgWSSS7|ULQ%S -K2=4uaDjO+uo3j?*ToBR6|%5b#avI`Jd6VaoX!$k0|ms5P(q%_im+PKG(vHS`Q!q0378sZo&xLWeMzu0((6uw5){}fF4Z@cXQU4;|b&bSiOm6%+l9?OX<&1X-k -<2tD?Pi{=b)$?vts776;0p3Cz94`-|F^l`B11}eRYW?zgiKKxKDA-(jW7)<%SXBS;5XavIYZg9Myr`6 --ncj?r?#govrf^h=;doQD38+z=zm4-C=+_1QMbuqDKTvJggh`%_q=XjC6o)vvdzkK=Y>#Cs7kU30K8{ -~znoY`_~_HY-*)Z-HyZ>Hdk&dQzeJc^Y{IH@)DPchu^AHy44&C~WczKLCijU_DVvmdDhU32XE^70=OJ -<$E^>UG`&W*1r5o(F>Gvws0lO9KQH0000802qg5NFCO7s$>iR0R1Ka05bpp0B~t=FJE?LZe(wAFLGsZ -b!BsOb1!gVV{2h&Wpgiea%^mAVlyvwbZKlaa%FUKc`k5ywOVbD+r|<8e!pU2!w`AZ(K@?9TOq1@w&nO -x+h?2=q(G2TD{7@#_4x7VUpNAK2qC2 -N_k9;c}3sru5a&iK!vYBN#*^2rNTnZJXX~}mWbqs5=f>#wIh9bH88 -90uobku_f5AScKezg@}gjw61*0Svr-()c1;x?*Eg)$ui^2Ued0wU -sxhl1QLAqR9K%bt60G7IQ5=DGQ;D@X4GHBSomsLLo4f=nyn=(^2XBg+ZKQ%$tc%|50s57&JMTeUlV3nVQQu!LV23!L9@Ha{rha!Ic`C|*rOv7?MiQ{3Y^NOzuz;zcEv%ioU+B4{`u+pe@Z0S5=g_j_Of=B -&X5NI53UCX1pQs98t%D;(2R}(>+&w2m2u`$eI^Z2f+e=va;E&gko~z4z4BRMIjFfTkwzYdk@wo)}qV= -shUYRU4fT1WnCpLq8|jF0BFCHC4i0BO6*_fapJa|212@ef8t6DtNVy --BZ=!|U@Vr3gQ5TzBfj`i`p&iEl@Q2YT9Sgw^BFAz``buS)I@dNE`YJT>pjF~)d0;zU9_Ns6GzR(i}WZUh(-8kt$9tp@A|u+kQ0{T$#1m`N6Wdnao|UV$}?9YJlL2cTJ14QS>D!d)q0m&!4U1 -8Ll>cOj|0k{Q^%f6~{ZjdrW;;kXxlxG-i8V1@ZHz=akXAbJ=Xd=M&w5#Q881rQy0h>-yYY!rWbQAaP| -@gv{^u2F$upNY)|H1`{9?H8rj<}`qSeMBfgOb4Cs680wF<*k49yh3kr#JUc0;g+DqaQ1q!c;CiZ%BP% -FHLn|t25Z5dPR_^h^#s43;@4l8Y{S6Q$zOjTME(d@1pU+Aov3f*ItAOWMUht?)OaE=(?Bv%7-`lT550 -kbajekvd5s=Z-N>d`4|(bHqfMw|TqsK_HH^_}T9xrUv`-AOU{U035?tBQ$;&kqJ>a%r7SRsK<&f2^Ia -XRZWVcXgF_KY$h!qonjeY2d(IQl&h!c=guqo73YPzI^mb{$5y7>OhB7HkwyuNyw{_y&8F@JOUVR0XX+ -9Fc0z^nt|1%-P6k&yE&r-vME2J89-1j#bspQ{o`G@G$P{x*e#BQQhB%_iOOtd`1W%{*cH3Hi$&ZD=Lx -gAk=HGa-+e?61>vRS2<1e*IzuKX)O)&+L>%PuRa%bk}BRO5iq6e`aUw+?GM476IyCW8&P9E<@f??n!c -vAOLsMO;cvmWkT#MJ=PpG%$PsI2B>-`$rJkiGkhmW@{^O%aDu5?lPCoScPK#$S#G_Q4(cjHD|vqL;-6 -QqUZrm@{jevROju*E#P*v4faA9^pdSF#y2g -J0tLH0(KWOG4;>sOc*>vA2cp5ER9S->W-O^X)eKBs~56k4YLLXjit=toC;^m=~r63bLmt-k-B{(CupG -hfW{>gxUC^6K3Oyj)ygyqJ%EGKTVSisIzEX6Nw5Y^d4slmZ8zKbhz~pb_XjF$<(AD4^JkwI$BZCu3W8 -%+QBSlVUGT1f`#Wt<$F)I%oG`Fz$tj`$&csKLhUi@qD2gU@ny_O3~|7XZP78p8B3tY!dBkGc9WP$jA& -2%~klAiisIv(bCK}L3*ULqD={pv3I6{P;SGFO~MCVv0w%5Kk0Jf9{18WE$AW(3wyVKNwmH=+}Twh?Cj -nOH*~_$tfQ?C;{yWr?uA>rDuo@rTkIiQT)n)SGHBLW0@(l#K7wz8%k%ZK*4j&PFou45`tcCATA^rzq} -O>?M}bbXOSGmF+NX6B-+qj{IGgc&CeU6;spmI?>InDr9B5Ga;hv5&4d`(EWt*H0Yckx`bHqViwtM1^D -iS@Si9k#K5CuasmVl(iI66Jmt4$F62Q}szGN*GKO+k>WqpYZ0r|+pm2M&{+C<1wQfzzkw0M3D{b#<1> --JaL^DlhW-=q4f3lz7-P8`%UETNphPS%p4{(j0HDR% -aE_YYDab3N+OeoV18~ZE764&;X`U3hQY@p~Yyb(DbIdg!GB|iW -rVr-$L3QRK%nuZ#mY(j@e*zrQ-4QltshG=*qEk#4a!Oa<2>+ho{9Yc2q460rr6^T)#wgGuMjgkrfn!; -ueY{KriQoJ>uS?U380WEM>{wsGv736&0XblosdR4h#(9Lvc(76=4Y*6nh2{`zo`GK}nM#CAH-)R#X}w -v&RE2&8xH>)@Y5GWCPb7Fzs*=YxaW}L5l~CuYxwCYG_EqYk#V;hJGEx01oVO8oc#J4>Xd1Cf_tpW`ON -xA;Tyc1My-9a5^4(Wzdlmv)ZqdkVKIg_|(931xHCMv?e%RQFRVNxbCkV_UXY>7nPORg29<_6PjOMf0d -X~@7+fD0wnf?&P_U9Q?NN2skMc=Iu;v(4hol*Z_AJUDG3q6?&lecp3@+~wf>KFGwhPSH{~CSHocM!T} -afT!!phJwuncmd8un%bLSKkI5$9aBA220xFy3XO%;m -{*7e^;di!E#x)YYsFEb@}5K9AVGhJAt3fGV6e$@OVue`63 -Vky41F)LG0Pxi4^JQG7@&$VrYhhL#N?t~q1kq{ynw3;s7vN)w#qG_KsF>1RtKcyz55UV!?%9uhYQ~RcuD0c9H<`&ctwVm?p!(wP}jNf4M -N#LLyWM{8G=zaD=mwAZ$9xFSQ&2Zx7i^@BI^WJou`VGR2u5jUdysFA3qNhgE(HVA6F&r8upOjp -YSq&IBLp|r970Qp -fK0gLlKhL8>$jCf?Xw-O5J3FtX?7VFn`C@c&f5Y&sCzQ>MWc%Z)`;{;xX7D#I$#?U2^XtnO>HP -Zo>bj5Ykk&NI1MV_-z=!wq7niT>35gygM7_=U@k*^(=QJv5*KLjRzxb(^wUEJUxAi}b0^r#xeQ#*#B100(^wax_M= -nvJE*ef$s_hd0UqR}0WaqOQzeU7vu?%Le=xt+h?`PmBYk9sCCBJJF{%=Ot*(p`|UKF6c@i@6aWAK2mly|Wk^P -ig%C{-004B*<;!iQVk -BmIdV0E_e!7Q6maf=jvMBN*6O##(t96>?jL%dW7rB_2=azjfvMfp6!-i*xOdgecUaol5YAs0MOeS1KQ -aW)<@hO&zZNQ4eel+8jTrFccbHi5iZvv*4{GI;)**D)>abYMvMPi-HG*NEsc>H0MWoZ_$t3-(i+TX=I -U!>XUC!<%u#y3~jQYn14uUf4ZrVZ017oT!=&Ti5~v@QqZ2}a`aJ&%=WMVa8(_*^V_5$BVrKDu?xuK8N -Ad?{EglE-|>(goveE+2);S)StaI>n;1Sm^DsMUh0LIa_DxyojI%CQD^!(DIk>Ki&){*MmDO@>`4HU+= -81jiDdDe%m)j(NEug-M7XGTA#5Y=*dV)MXIjW9H(d`Pi8!NB2#sEz-HaPo_%z5r2h`hP?m2}tB9g~h+ -hgGyM4)oX}LVXzT`@TEj{%CJ9kr71@=4>G&SF@1ygx;I@{(#g?i|{fLGzj9VLg5%-8MtHznss=Wosf#1S+}c4;LKSZu#6P`Rjxav0M9bM#Zc_Le8PBR1W)Ym=PYLb -W|>`z)Gb#$XK!KOM%zN2N0IY9&pPDw0c+R4ZwKWHM< -d309kq$qcMXMFL)eNfU(U|M!+}a9AH);h~ji(rp%?{gx)tFKYU=k04yc2t#*NcU0-~|t_MS#%h(dO!I1;bAhoy1N|RkKuS7?UHS>!yD`ZyXse;+*_b}zxw3fzB~JyeQRD_Uj2C4ZyR~;Q -GY=j?YqDCk5$$DB0K|1V7r;%tJd64j~PbM8D;zdA-K2M`Gp4cf~+R-{=Q0T@$+1=r_!N=^QDiMl3^SB^%qT} -G?ZLw#<{Y9dsh@K|PROJa@iIU;$jl;sAWRn%n5rc5fVVNx&gz`xwU~5vL(1kvv)j#LIEz4A5kY=(3jsO+Z^4T7W5y8&VDeV -}0WoLccLA^nKtueKQ!2v7k#t!iU{THS9rx!kvO~BpsQZ$C`xdUBhDYq|X*iZ=QhgcnO)lojwvayTTe> -}9Ini|w;te!AZ{yEg#*`pK@CUp@x_(B6R1J1&7g>z*u4e!Xgm)nyktlIAL}mFJ$hXcUOc!r;K3(xGT$ -@sMgDeUO1ht+DQHh8Q%;2CF&?sc@8#o(Tq*0n9P*%ebSUN|hpQn-166(L#O>!+YZs($Py77hFa_?huY@@QYV@Cr5x*}k%r+&D=x#i -2`=;1M}OW0lfyvr3Rwuhd$SKUrpT*>kPRLy_1urcfPM9qIfMGS2qe;1m;d(9tX{2SihA%PGVlWaAu@z -Xl6T1B0rTGMR`rzc6`AZ&A}0Njrrs8&aY43>B|wS()uJA1?g#IjVy=w -A%PYFy`xds5h_fmX9)hpp!2>OKD2vmb)uw$m6+YDHc#+psLJsK|R$vgL-LLGvU=(PK7ROIb$2ud%qjy -X76VB6ltx$YqNmehl&VMCaW@BO|^h+@A}6JUnhMCWFC-GzGYt+_$lHweri6Rsr -l8Hp$LuFzGK@$fL|Uf{Ljv@u4q0z|Xa8WEG=mdR!)be_VRhZ>g}bJkm_t0TI&%JEp_6SF)uCRtKHPq_ -KXya&zGH`4n>4mJI-ZeAp-+jBwYKi!M=fOQu<1 -EyDl0tq0;zSOiIw}z-OTdFWHX^!h18)L>dCRSW#WA8-ocxXEG|=YJsgpt@~3Sg_uLC<(E_@7NKN~$>)W*`ST5t}tT<5V*w+OWaPEt}qt{`K+desnQ~OqC-K(^^xlZuwQIvdr6{Awp8KkWGo+O -yLVzwTX?eJ8;{HDYb9SMvVQ9OS_F!M_}kNxQKri{u$IelbVNHAE1@$G~6P6RYC_U2AGW -SL5!7oQQh;K{)ACYfI@Q9bQw#Q}a*_XInE+yE ->9gq8Iiu&RRoLj;BB6k+^Rm%Z*6>gc#h$GrHxjPs8eKLA(!ZU+w|K2bCjz;i>PM~JQa&z;pp?t#cJ(a -Hm!&Li7IWC-BGe(*Nr%U=ZSgPI?OG&MW7>kFMsUmEt2X%4Qs`WSz2J*NW=0v0Sui*rN?>(V6()pe{5l0a0P>_VC<^^h0x)*=-#>4 -9(xV8Omtm_wIqdS*Q-MYf{CV!oYH7Z@dJ!o5^RBJlFta>l>9Rg)55)&bAx=Cu77yyH#xR_H=E&M`{9E -HcZ$P$cu7M?Jt6_9S6mN$0N1md2(UbqKwbj;#}A!9pSN4IFo+XIh>?L59l%UmzbPHF1ZHcxC|fgYI)^ -!IiLuP=|%HrpYA`fr^H+Iu{6yIIq-ww$F}m1K>4W&OHKBk^uaUa(mLatiuo4(sdAiB2^>z1*P|brova -H=!RV=*wJpn-S=>g=-6x=k7|_DmCxY7rU!U{(x}hWJMPOeSKxwpH@j@>Uv;A788PK-40KmfuK-=UJy+ -wg`uo8Y;RuQdou-mCCv`JPMX7jjj()`b{R|CcW^|;Fb@;HrI_jpsAAcG9D7)P{@S}{kvrU|asppVcjv -U#F~+5Kh2g+I1x5UFg)}PHG0_8(2iio@r6{R3C_CFaSZ^zKG92jZutm%t2?zD|%?+8?f2l}Qd4OT_ja -|21=hq2)|4!xy|nY-w!2YfJZ{__VY|(fs<-PunvIp!@-U0a1s+{Sr{BL!yj -y&fNbjAbb?$7EKcD?WC)zOf*?e)w6#oog=ZqodHS|oF`tSJjn*XS_CklV5T!V_RPG5zd~+?dGvGmptR -iUgjf!$)Td10VELN8%BgoE5p)-K~}WM|Rr_dm1HYVw)!O+IDeRA~Y+VpPMDa(ar7X=Hb=*T+!V(T;%~ -aIqkV)xO+vue>v`C*k6Dh+MHumnunVbb52OS+xTz`@8{Fq{I~OL{rSLkWqXjY%ZG=%v97wP>?5i<6G# -Vy)`9*_51k_=ht9Y#MZ*oHfvPLqzDnkngLHAQxkwzFq~Ce%-ggp0>JEpy=nH*O8eFRj%zB|iM3o#3#W -|=>nh_-GH9P?xAo_h_BTiotm=cvPJLwmI-i4(_50001RX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQk -Y-wUMFLiWjY%g?aZDntDbS`jt)mm+n+qMz@u3v$<8_!8I3hx@%P0Qvwac#%Vj1zn6bNW$&NJv6W5efj -FtbXWk@9yG-fJmLsX*<*27fT|rSnNLg>ow$8PAwE@NZ>4HY*nQ^?eQ!aNS6VAQfwP}8BU&q4(Xe~ob -mDXBjw=gWKvt(4xbV}-;-xpuEmNz%i^@)ClgR|^#i~wgIBtOgr?Yax;P2w%g8u!huv&_HK|=jPI=KLI -OT*S(Tj6~eeBPm1YKA(=mUXR!mBPR^Hbo{=3-Dy?q_{Idk*eXw1Zzy4hP^J~0jpHImYYs(sh4 -r<|LxamVfNr@0qb#Fp~Wv9*kgM2`qkaloL%9M<@L4r#CQ8f6qVXtU(K9uZ#CNs{gvOrriZPpwoKg%tz -|6?c_Y0Ue#g7b(#!pt^XI`Nx4|I4xtYz**jXCBY8>W_u;^&Uo5m@_HQxy$B+M(P5UeLMu?Ec#M+`u8IePFjJ0C7w*~v5#owbO2t*7&B!HE&TL5&}5lo2+a(5rbwdjdq&L{v-!2$Xo*o0b{Ef5u -`841VVhwp((MP2kh^%O%sN5A<;r9JM2H!>PbK-kRLx>4McTDUaxhrdp{HTz^`@-Jg4;Lx^kbd*Rm1Uzq@(5`fdH}4^O{&_WSEMa&;Z0TsF_1KKs*hQ -ABZQU3VJeEd59?mbjV%eTzepUB8b7` -T-^IJ^Uec)O=C91r*meM@PFvk%z5OGhdoymz{pml3=IkH$kx2FN;}Ty>ooJOm9 -Os37MAeA=|3y>K=hxx!JN8?>;cF8*n0hH3z*J($m2Mkv4*X~` -wy3y)gf7=HaWG@Sk~_4YaDu_58lpq4E%FwSmTGHIo)5A=80hPpu)u{Z8B)s)1taEkI8=22s?>nA((;e -J)eN2qmfy)1`W{NM!LliU*6gS{%{B^_O1G**Kf!Gvo`ZP4Ml$4%KB6lRSie;bpd%|1=0wa;d1{7;MUe -X&v5Nz@6@d8yly^*;oW~5rLE?2fx^D*P!)2MaJFf7DrF*~#{kX}B-2e<~i)jBz+XaQ;xrS`wsT`U~q{ -_u&YDBZ1J3RZ6GZdhqTk=I{_&O^SZ#3&-5sHFa{~h#_HFRmA&@tgV$nh_UM%Lh -4d;nH=E)6x_wgNdh>*j#s_yk1+Pi@$By`Lf9+PN~edLR+s6M(J!29~wLNu;Aqzl7HViHfjq#=-{;+k&5nDwFN~sfmb4O_}j%dVw`7(by4LP2j8m_}>_4LQP*7l+H))*dWy<5MVX@2qa>#%=7 -AOBoYwaC6$7#YuL~DGmRxmBoSchMIXz@5@&caYePx7|)?S$WenpgsDcW&aRb6{(}*XA -tBT!~PJ3WhU5z5*%NQM*OK1DxE;Z_`}G~(Gwi!^u(x-`7*lwlvef8<|aODdIx4BL(UVU&Q8ujqbHB27 -k3%do){YTJOPBSFgaSb>oCy?bi_p_YVdaRydvbezbL{R!z2`&EUy`u5HWjE#El%dkkUH{-A3;6}~ -PU`ERK7#2pN|WMYE7WX=xxSI+DU2Q^388i~cKNWxdn-Dtg#6?SQug$J&{sobj?yIdNDBu~;>R!rIpIc -NUSK1LQgMg_H)^k-o)sHj9dqhSNODMvCj(b#V$f}FnqrU*!*Z36ID9gg&gOpDQr%Byp -Yt>DJ}aYrX8NFIP_x;m7lFlzK1gE-j~hvK4+QxFW6{1EUtf6e&4x|MXC?;C!muN5M0qh+lc2fr^;;|&UhNJdz%$!ZP -qLm-K%CEBdCHot89qpCHz#;KPEySMA>02M}+XCJg<c`F$!gJ=v$#J}ZXBsyykC02ITbJN -p8ybIlvEcDooWo2&$w_@jP)drtb1dk}$Uy~&D9g&$F|@tM1({!{o$+sg>Q1}vCDKfocBO81wU3`?IwwR0dMZbdyKKEu+iL1P@xmOo?ly05>G1jb5;fu4PKa+;tL{@hHN;^K3CLj -JPo6aXW8df#nC{BcqHS2$xXd>we21`DF$dvu+VZRKA>(Mtx!O3ZTQGCOTK7O$_o27<8!kK$A5T3vfPaQmgJ3OexUbHoiKK -*PrPV=%69*q*)@ZSSY-enb`xj(r_4DwYvQ-*qruNrB#_<|5>Z=lF>`On7?<}!p^?`)dxj)lh0X;_at> -OUDQU&i}juF>q1VLTY`C_Kw!;==zR(YhAG)of=uf0_dz@6aWAK2mly|Wk_e4Yc>xE008$E001!n003}la4%nWWo~3|axZdaadl;LbaO9oVPk -7yXJvCQb#iQMX<{=kb#!TLFLiQkE^v9JSZ$BnHWL2sUqP%9NJboa(*Q-z=w>%flU{+K*vlp8hsDB6OO -#E75>=Agjqlgr%#e~Ozr-ynVAm2g@0@vNsM6?)tyX30S|e5~CaXpp$N1W6)jF~A&yy0#a;=oeoz%7M= -xSbx!nKVO6Hk+6;#)475z$ATb6mLP#)|jGXfv5iTBBCnIb+!syGv}o6_rTlEa9!&;_p^lhrcL+ez{-& -vlV8CH>Kg5O4Kg72Wh$D*0L`~rGrIj0?$C%YnsiL69(7C#RdKT!X3BHv^o0DN^O`CjS&`v%6h|i&Hnx -Q^DX=G>0`#eSWz%;83!65uh}D4EisX(W!cvG@}WTwSD^4A8)>piJ9IRiP3XB0B?P?|Ppeg$D?4Z2|Eu -41E#?eE5N=- --BD*tQWSg?P^y_%q9nIOqNHE_rXfBZoemxX87^s}RbY#%R#(c70$`0cJsJg(@RQU^70z97^GSE$uQU!FX3u%yZb)Yuwd=VY2Tc(4BIw7EbB>h+n@8_oU5 -Nrb6vUu3@k!_b>-yw>q5&8Nk+GI@dCE6@d*}yB8!j$q=-Gs0c+qDtN4z#ZY`Mj#&f53lou9~8?gxQkx -bT2>m2Yn-OD-DGiU48G3h!pQRtto1RR56Vrwt@(}#B~71<^u*tESkvYoTF;`LYBH&{`MZTX{MOE9|9k -D{1mLuB56;}u}=k}a2$(N?~B^M-wa?ClLpldW^jav|#1s& -YfpP32!iNj?B-j%Nez%2Eg%s1U$4w(@p8=w3T!%=OrI6&8Y+QL*!({6BU`HYB1hVHWfhZ-h0QB)JI}r -i%&4`t-(h0-Y3LvudY`B0^0!m~6qliN!7^W)#&tP95% --APy544xS(%|4i(vh>`4ic+~IJ}3yl@uuwIY-J^5sJWA(n_lWq;{>>3+NCn!ZU=GI1ovx^Z~)PfrorklF0*246Gw;6DN--Y0c#$UzFMjTR -&Hno!VN*3P$O;B6EM5OT|$}1p_K}a-x60^-pSr=(BDkOaM6T2K|a;|Y4Z^dZ5>8yY~Lyg#OJ#%<57BS -ci{}0}DrWanrap~RC$h@vJhUj|{-TK`!u-!v?-*qCMvzJR)=v9Y`8X6+}ov5T12kao7dyaxF>F@)+9y -;7ZbRx^x&oFns5M}A{jU>VR1CJFA3tj+DW*Rfw(KG=`@j(hzcwm8v=@Igy@a-N(26QqM1RUpZA~~F=p -|<_3Q|V&)(2dj&->;*;H4k7f4@ai1rPE0%8F-Sz6H^l8%(0!%PH7)RK!ZN6Gdi0c(Fn6cbVB>Ct_9RE -)UP?K=J*ki_><85uMk&a-!OUT23q}ADm=nKLg%^=(9ly1ce5avXET6XykxA(~XlAS3wYTg?G -0*x7Bsl7b&JESUXhXl)cCCWWPEwpaB%a$Pu9wO%c&E%@5HtjRx^EAJa$kR=?&u`&EPoQ-EXC+?h@j4` -SoU{%*wcvl{%*s2ik2J!daxov;rw^g4ug2{q2%f*ac4DfY8404&^v?-Em%hH7$JLv2gp%1oQhV(TikF -lGMgU$1|hTK`q&#jETGpE`sy=<6!9c0}x`HPQRKWI;_1|4ozoS-&&H5%0?<0RSbcHRYffk3u1$UW@;@ -u<9H|xB8gf97b%)K63CLUljuK0a8goXml7Mv8(sAesCUQYeH~uMxp)f_VLF~Bhq4O!|&bYJ-dQw>Mj| -S`x*RTv+ljTn2H9m`=rxxtAkmT35Q;-hUtDU4{l1`ON#ScI415<1J_hd=6VBkJHLXxkgCg|cGb)}Q8v@`JN`VlL7z$VF&;K7ytkOaA|NaV{dJ3VQyq|FJE72ZfSI1UoLQYeU(j*+At7 -?@A(xLy%33_?R6zibXTp^L#2MHLMZaa6R_5?k?ja*)&Jh{SH80xz#g8Nc;0s~tJNy|O3PPz0#YhzDVJ -QH2o-`{_yt3CO(iqg3b{(FniNIV*hWK95ME!DwuHVIB^nDw*k^&m>}|?+i;Doul}t(dHnI=UOcqmDDH -LTX=VRP2XH**C4auKC5fX;(D3n(pYC-g -g=MvGT+Ei#*Y+O&bgieTK<>YBsR+uR4`szvLk;pg0G3+TS;qpoW?gEA6OUBK`dZB*CN!5M5MDTAzdUO -1MMR;h()ZEJwaV0AVaYuH4FxDtZ_ug;Q){3KC+`|Kzkz>v0fw~eY+yl4{fr4eG`>tpwY#{On<@<*SAm)~y=f*E3P;DM2K)Q33zHtC -imp~yWaw!w#+t-CaR0bY?uZBX38l`WFy>sEaC3u>!JFLGZ1gn`*ogWRo;uJdq+O6lQwi& -9Wy2$yYvuH@bIOV%^|Hg(xokJnsK-L>HII`Q^BwFqsjdS5E70T1XD@@I2b4C7TL_)EH1T;WnE14T`3c -?J{O@I`k1V=L4Zbm}j#=?I8S3Jlwc8ok0DCCXc)hGjZMG~nN!9DRLwIL`PH(PxU$sM6Ttngw%%ry`(dG;SrO9KQH0000802qg5NSGvFLGc0r0IvrC02%-Q0B~t=FJo_QZDDR?b1! -3WZE$R5bZKvHE^v9RRZ(x+Fc5zCuQ)L(Cmo5_SEM{J#-wW9K+tKb+A0c(L$fAvWIL;%;=lK72ST8&v} -t*O`0l&&-FKgzRPX}AFs)TBSQtW9R9q;CmXhbSVxe6V(%9u%l2nIOS>-Py$zomq?OZsz7A4?~Vr$ -+T;_?7dLpa;*0wOHe!{7_w6kPwuF#4kEGO+YkZCPoaWyhnvD@W0# -s4O1nktRI4l{|k|mJx*4%F6YhrbD85ImwMOzm&c3ESHSZmiw_IL79Vxn=vc<8b+Q(@>xmZt+akHkvi- -}&QHQ@Gs*6FVm@4nPOF{yxWHG}%YTMH2E#Nvu-i{oVQ$;YW#i(dVNxF6w#aD52uKB^`8pk&#_MrQ&#Z -#JrU12w`l2J&avX!nDJj?jw&TGoX_qQ-_B><_R;Lnq2N*HLz*w-~4O+oBvn-##Gq@6aWAK2mly|Wk_F+fSYLr006`i000;O003}la4%zTZEaz0WOFZMZ+C8NZ((FEaCx0q -Td&(T41UkA5Hb|8i^SV=QTJi(!wM8bu@-wdC=A7>6ZP1VtINrU^3?rRX0s{DLL-_)!;en=FZkhvh8m>(0(O8Lxq)2OHJ8N-^nE -Mo;Tpf{Th@d28p9WluX>?i*$$}-7oq%=lYUU6gC2h^$*FG8HrGW7V(hT+<7xBk0RU=7b7`3VdoA -Kz@e@W4J_KYwHqajEPXy%4g1f561d8FYo|!8|h!AVx-XZ};qS7WtX>e-Z_FQ(Q9O=E^So%B%{ -ZTp35GssNM!B(#z~OcWoX-wBFjLMP<|Q?={lyVg}_kXn4+!5Djm+n?A&D7tU#?JAY;*h9$6!83p8@kx -^0uyoK9V{O>~$I!z8(66W5jFb58MTKgTT`J}0bl!%flI>r6F!AISjKiYUSE6)LsYxKWfD+q_kiuqJWS -}bd>^0XXgI}*7FQMLBV+|gf*%8?@Qi+q^!dQ3H>tch*&}oC75I*VtFA)hZ{11#T8rn*dE;U`C=i{ -#h+3gyqKy?ob)${th+Ywy3||p(9mRKy%M7r%D}p6$JMv;#Nl?u@vsmQL?(;uVr+82fESsi6P(ZJJDMTlh3dE_P=7idMQf-LZNpsk67nMKxuq*>dzArWsN{oQ%(!9U*_+KIcO@~eyVf1lLzQK%F -zSg6CgO>O&>f)yOZRvrm1WWVRMW(=Xs7YikSKdd&T-oFq|tmu7U$Dnw1AE>+^u##OfFnYoO_~$oYwa1 -8Tvu%%66~Jj+_;EY_Rrdj-PXIJG|&1uY&H%$?8XV~EnR#yC!Z1= -0MMPin)c=2SJ_>}#rButCd`#XJah!$0~?$vm;wVI9#*PoEy#O#dF8jTKkx-7akeal)euHIt$f_tHZq_ -d8)4jy-bbgeb4tIhjeO5N^RC^mn<4j^-U0ZC_Vb1b_NQ`;@l@O@(^eAA`dWiTHeWVI@8csITG)Ae!SC -yK7UU{vF1!)%t}^@Pqq>;CyE`MI;PE^NE)vs~U1VjT2(wJ!GOX#Ox>mN@ob>Ta_T6Qpy@jajx?S{kg= -^2KvQLm{=*%)f9(ZOALW3@?%Ne#3UjcO_)>a4RQ1dRF?^;vYCnEVeZ+&ym8?RXFz!#uxU!Evp1+85Gu -o5BH3arCKi=TDvbJAI1UVjk9OZ_(W@kqF>m1f3v#gRwn0{@U6^Esq#?Qgba-~Zc{wRZ~g;NO9KQH000 -0802qg5NSxqpoWcVD0M-fs02TlM0B~t=FJo_QZDDR?b1!CcWo3G0E^v9JR^N}KHV}T#UtvW$LAHeJ^G -ZDJb*rjXuWEJe!}Vl2cpz?!?b>Fu+rPeJ8v-P(s=Xp5;Q8U<`(`{tV|7O;ZF=8Zpp=NtXzK|(I4ye*G -_5zALGE7M<=F_cX@WaGl(p^*^P{`-T#5rVr`Am-2j&3HI;Qw&_mGy8jW%GJ7h1Wpu>k+TXqi@%;rR$4 -N97-3@xbaMY&M$@-~CR1`SJVTNJa7tO|ybz4t#IC*3yL)7tUL8KzLylcgl9?Sxe>|d5n>UH_M0n?tVk -?+HSY;{;~I_N3_a$CM9rUSbbtGI1~{JfA_*d2g*CrXxlMgZlc@8idi5M-vT|+fsq=i+2d}Yobciah(l -PY)Ru!S7oipF0HI}xkSxl2hr6P1v?1i3vAQLnwTWDQ85OQaH1;L_z!P;#>Ze_a!pF@MCCY11$<_=_2kUM5uhqt@ad7wDGCYC|INJK$`!>+ve&pkJjCmNM?$0;IrCFAh{D -abDt;mY~%e>!?soCytZ)T+pu2x>SwKBsp-J8CuXax5%SvW2+~yBPY#GbLdtwd -MOd_064yz=4M;pmxY#Z>iq|%rv}lR-tw;+h9xT-o9TQ(-Qr=2hlSpfX;(IC7$*po0)O1ZfQpSt{#b4p -<^Osi``}FHiYlH=Ps8ABZ26$r`7YJO`M{*WYk^^$ZITpFskO@gjB2%%tNu?&>AgRsyttDvp)T?4&%1E!0}<5yTFS^fhd&aEul!33m -5-Becij*Tvk1Qo1IGFRBWyiN4S$xG`nx6yYR;`tD-j4H~7Ev`b%QW&VK;?A>=j!7??RQYE9Q4tb$zhE?MQ}%{44-<_Oi-X( -g90w&{U-}9B5=2 -_~#cLGMPV=kNILl}?d%k^Z=1Q -Y-O00;mWhh<3kiR`4k0ssI#1^@sQ0001RX>c!NZ*6U1Ze(*WXkl|`E^v8$RNHRUFc5vuS6G!+Nvf>H^ -NJuHF*}UBl~@H7ZTkK{PM!U0M -c2~h#rVjq*iFxz#TvJ5SfHT342UG*#s0)!0gh%mqv@XAq+I_v{5bjrnD1E+8^X?Ab&?Fr6bUK%+S09= -$zIhEv9vmjzyp7R6~PZB&2YRWfuCTAlKZq7XPlFo+q_Nc8waJDO!*K&*|cQ?zg%jhE_w(Flfg|C*Wb;`gi@`n4~mD|?aKsxAknPTPFgf~D37Ks|%rA7(+*mLaTLX-ngkr5xUT1HLt(5s%KI7$7xv4b@5-M?4JJA -rST(llG!JU6q8zCKw-In1#zzMIxn0$jbfZIk+gTCCXYLs&bbg?a{Mj7lvH%`f`NXEIw)EkwhJA3#p$C -)be9Yrl(YdSrFvRFeX2n%jZH7txrc5$7DxvUf0|~7J(zvRIhm3!JjeuEKmLdP)h>@6aWAK2mly| -Wk?|&!MN -V2*n=T>RJyZnXLc)%ChA|VRbR`d@s#RYqbQ%+R={|83Q4WwjTJ^>0$K9mOCNsj5ESiSMIRZQgYQK@M* -ao;Qa4&dOe@-VEnAnc%hYh!oFIoEg_^HxO4D^gINpZ`rZE`5Kd7P=U(5S4+v2PQdpqB?7fZNWkG3T>AI?>aU)15V<_6O+DG -bf}NxMitCHWhEDyX1U+01cus9EF?kJt!TmK{)6GdB6s)Q*>rbDB&5a47UzL&~l$?)v{r0!)Arc%loYF -drTSOhM+JAre19HWs>k$HTW5am#4HC_D5|Cu+>C+H&xn$P@6aWAK2mly|Wk^g)eL&O=004VB000*N003}la4%zTZEaz0WO -FZRZgX^DY-}!YdF30~j^j4+-Csd?Fpxd5HQCojJiuJoZ6F95>6x8}-hn_%w5_x(DkSA@zvSDuiibo=l -#fn!vA_yJ#x|=;b+In8H-hg;l5A>K3zj4#-&b5HLf4WPwPK0+KAG6x3%=dv<<@=W6%$l((R`QglVba7 -lNZc=m%Cc!h5PnIMG0i&M2X|gguvq#c8nDh_MEaxkz4)SXCb&SKzfy|$S=HPlgS2IuktDu?01L%EBn3 -91>3Vy-80pKtf^!v-|uOe1wcQVl@`3C7KJ9R1zQmNa{KL0utUyk`Jv8>>{Bkx&y#sW+R;n*88>fe->P|W0IZZ~(kvG}fOw6x9hiF&OT -muEK4``&SkdgZdfW2@%WliNy!zXZ-;5>ucxZZgl;8yozdM#bCKXk?gg=$cW6D_7f_=j;A1OdD%xBFXf -KMjqdrW}&trNyurheAH&61>~dvM=5fTabM(ka?1cQ!Qk+_=RtJpjY_WK@svAUcMy{){VcDpMrXPM;@$EmRjx020o7C5lTzZA! -dYr389B21HKNa^ey=AEJD#SEjs-voS&>kk@njL81X{(ip8>A -(RIPDK=Cbq1)2_v7%vg$BvP}8Pqb9g0{*{Lpf(1dJF6L!Y89C!q}^NIGer+ntfJu(*p$|VT4A#5uIyM -*tp>gTyAgKfPeABhOrj2g;q$}M(Ije@OY)QhcF7flm#Ux;SlBb!Uo!TBP)OF^_j5#t$GXJG*JE|fHNkIUSqZg&8^Tnk)_4b7n>9srJt5LD_ilAP@Ycxf2t4M-9S -fWJT2Qa?>)w&z{ze^OJDnXH;Ec7OL6#*ON1%{BI-&uEy$rs!c5SdfdB76j=s9_1ha$mbl*8m}66oji7 -B$qNhA`NE(2^Ew?9$)O#%MFLzzZA?R)sRj#Ofm*@p>Cn-Dv7ZUc-?|Dz#jJSvC -5(HDvsojx4MZd-Y&3!e*OOL?$i4(?_dARz6SmaQThgjA%BuFncXwyvz76*#(KhZLdXpY9)G?6{A=>z{ -r%@4H3Zw#Cb-Gk3Y&S01s~P|(FT+Wd#=bInQkp$GwiiSLlR~jYETn7 -jRY9roHrqX-1ESu1I;1U -tfp@{sevY6*#;sj@M&>BVWf?S}?X6=$!-VR-@W+M6-TWFfr;d3$3=wr{=OC8v1;L?)Hwo$JOY-7iT_&;qW6f$|<{>;Y(sad{86TK -}t3_)yzj3U@0L`Bg_N*gyansSFn~cZ!VKxY4u!lAF(qR)YG9)sn#oeeDs#Oe%y1QR9Jv%(WZ+WQVnPQ -1!X8$qyRO>X7?r3SdbzFsZoY!!*eT_q!v -CBC;s4hwF(9#3)60|?pa**WZz-`Zp^(t6^UGA&;{V?%(4Pujr#yXxV+*-<2z6G00YSZ;?JMw37wu0*d -eXeWoqnku4XRDug`FH2+(ZI(q#%q#&$(f(mYiu{S|d%`(v7kW!)SjOkk9zq0|1XF_kL -bq3o)w376ia6}Ea=h$#Qd=ClL>6*KOc~D*{eKSzLpgFWg{+Z#>$Fw~^($bYHhDX}a@2FJ|=?Ovq}`^kIyyylq)4nZzl1Q`chHu)p0*I_z$+h!^gV(4yOomzYII -W#LG41-5aU6-?oI@eGyN7}B*v$nh}HcL|w#KE=xfK&QP~&=Av0h}g6VF-8%p610xs#lSlUn9dgm_Zx; -U2{@j)(h%F4Va(}({dN)>y_OLQpM&o~TZA#WU9^sf41{VeO>TYEzcA!H=U@*iYV&~ -2!8!DndwGdnPd)LcXIp)|kU8#TjnB+Qln7~fy{8$s{5k)jA6Z_EjsRRMS1t59LzY6z~*aA&KbRj)b<1 -N+5sgW>-eMzbe^4Tk+51~wD$2G)EHgZ9(iYAg?)L~joEawhu}{IxKV-pSm`3pf7nBZ~3M##OMG3?sHF -Vbx0!BUU!ycxC?$164;m;cGat-$&FZd4b(bt26sUvgc#Q^#b?G`y2-DPP>}dbbk(Q@xf1K)WoYA`Ad2 -`D!hSuKxWG14V_HFYZ9HgY7^rHsk4$_!ZBndbAhA7MGP|g?8{-eFuX&gP>AIkm|UeFBj~Y=mQXG4yttY!fS}*R7_f1=)7cTft>mto4yTcv)&UPe%lL)@& -q*$w|Dz3^z;n_Wj*HXL=VPi#yN($1|MLCol;7N>owcpk+ti1M$w+ut`4%dD#K1a!u{#IWj*~Qj7c)q` -m2ZUr=hzg>Phgh4)J+Kyz_j)WL>BmDAYGJ$HvE4+Rj)`&}dsFeW4O*Nxb~~7HdlP$&qiMY{v8JrDAl+RIC?-10Cl1{2|F6=I|M -h_4WLLfAi8KS_CgM~x2K{L0lY^E*&5(qhTRDF!-_;pYnPSUb}TJYK3&p-d>ipud!;8<$fa{O|_)Z -WM?B2oMs&r6LWPvC4?wA0+$ZZa=7d=}k%F6_M*y2nNP^(DE-1*5JI59jvRd0*^0`S^Zp85kw^{iHfu0 -+;LatebQd?kP!Y`~sfw8M1BtHTY*CVeSW$lE8G&J$mppSbbuQp1V-jS_{+H~C8~1WEn!js|#a;=<26!(>uN+4gsLUa;;lRSYTtkO*fgec!NZ*6U1Ze(*WY-w|JE^v9> -Sxs-_HW0o0R}d}&Nn2akUIVy?Zo5E$1$NO(4~sycCEDSJCIym8;(y;6ij+iAPLus8it3O=+77n$;A=gW5mYNjo-TjUgYvWDij8e;3!z)Af4oexn2#TqbP~mBC$W(KWCE3I3XL<(7Cj4WXJ50qbEMmxPiU -0=NZTOd4+a10x1Jf=Q(T_cGcR4Fw-ET%Ng+&VWuB3KE1|wOSp?XAjVAm@pmqq0hMbg7Ib68``h~Bq*2 -MR#k48IBdups*m8@n>Y859*ACL3-C?A+7J{k%3<|%-D+3|;lo%Num$DI@SaMPv}t@;^I;O7Y^$;j{M@ -R}HWbjxHqkt-am_9>R>3K=x??i|aFTYy?%q>jSUh#dM7!%j3fc#Zvf?#1cD;>VYZa>bF*XM6Uuh%jZ4 -BA^V{8oC3FPJ%R)ayWGg93IMq-HR^LJ3Nj$%3d**6_u+>)l -0PkK^cE6bK@fN`Q>GFoC&&^A`_b~D+ec+Ij9nPO7r_Z=5a<>Ol&T&wpr-#3u3U8jXXn+^IuX#!*8A>^ -HAScqsy7}6^ww1souYL1gu!QtYjKSG|jl#ALl1BxVL8WN`xs;>l>_h#p4r&FS!bdIyOh8Z_v%~t49=o-@gPd{9xFo-KE?Me3 -D*Mtya8|QcLCsOXWB@L!Y9$~;EmY+teL_iOeP|SwXTlB#Xg=k7lSnQS&RA@E%IDx=(tI0$@bmQ`E_m! -=`Z^F7^Z~B_7KojM>dFtCHD3qexc+|)-~xUFO+_EWVBm!@n3{6vi__cR0-_)NlQ;m#tF2LY -5&!uMRV20G$mY=egDSrLPF1y!mF)47)){bORHW!afQOPF5LVlFMh4g~m)Yo_IMG%Tv4lL~rJ+TCq8M1 -UOJaXmNK5PNPhQTl6K3a>N3^{K$Cs#t>=OiEMao}G7sp7sqi*=y2%etTPbPVH{7B^(6kl0^~w3`oXXk -YAncJ!5FX=ykGPe{BXvaZsrv_+(d*Cgg)teof%z^Od>HQo9GodrBEx&(Vrv5yDi*Btp-_cPV6J=LdEc -Zph^@dP{yD5)Nvyrq#G$DYd2Q1!CBykYzVVsWg5KaDvPrM0l6kWaSsdWj=0D6mi5NQ*XzkH75uCbeIi -EiTU@;SD7@H?rK#b;T`CR8%m>qv_BTakyLK@{t*igpMjDLbqNVYvN|zK;q1{on>$L8V0^<*8-_UQ2E!7o^+OC12xBm))(EgcjVfWO9%7eDneD(ocD; -akn96vT<>Ry}4S96jDPF2z_!lGbNy~Gv&w%xSJ>D@UmND=N=p@qgOZm%eooR+|!PDo=nP@y -eiCuEdHRd{j30>}+-%=u8Rvunb8eUu$X5U3(&M~-~x;0@KGoI2zc!NZ*6U1Ze(*Wb7f(2V`wgLd97G&Z`(Ey{_bBvv@j&4iqK)( -mtY>yr5RQrO@rCN26%?RNVLsH5;c;F?G^p+yQ4_z&F%VOE>O$l-NSo*j>=YwlBDUj(T!wjN_bfdsR?~ -lqG&Wr+uyU9T`$CL$E%(DsZPp0YIf8IN$c5ci;y+1lSispCd!glxx>9~f5v3_aD9EVTz0~_I8=q8Uh< -yTf0l);vz4TkGDdjWQsk^qUh;JRuG?!g@3wYKr<%!OZzoDy+7x<$PAB!L=4m5~p~#*U^&S7frPGTr1cxwzuk`5%$J*AXZc(-9UMjMmO{*y(D4bvgs8%3{d&_KSxXMQ1PlR*Qz66669gGhZ*S-7ehtY1MTH}zeg1D96Gfw8Or5p9G76JR!gW~YWDm4PNmFs41y?HvpelhabK -phcdd!3+=b3Jsidrnwa0)01TJ!PL3yHNQH3ADO(p^q`DL&JpVNi@+lA$;k1uY+QN{(0L=-ucfB*W0L# -Y#4|I9ejJ1lB*=64|+#Wv-$byjql%9b4E_lUas~Hb3_AFyw{yP`^J0o%ZTfWI;-L4A+Bv7OV<;ToaL3 -ZVB(xVOKmcSqGOOu)h3mGvxCTmH9mb_Cs3@aKX~R)hthYRJg7axCwt`nKt+J_J(T+r2lG=9E5xVf`U= -0H~%|#;9U9mo9G<6#ABH4>Y7(Z@7EiOz^*O(v@E%!;yWLXDZ -{8{5aH$zF)q68CEP_ZE-{L$Hu%YA!0PqHJ6a?{d+tqy}eX3b -I6$J`$Hbt{j4_lgQg_CfzbkMxw^u2 -17R)g#<=;T26?DB{=MR+GXLWX*m$-pcP$;q+lAWZ$&P^v+A`$OWL-1% -#&He>YO9KQH0000802qg5NRY`?VC@C~0JjzZ02BZK0B~t=FJo_QZDDR?b1!pfZ+9+mdCgc$Z{s!)zWY -}YUMwUnY+=tv-8RT$8#J2)N!p8HXc>vNwU#B7l;Q?O|9fZn5-G{q-CKQ#Bywgr&yO?N@ajO4WYab+XG -ucxLtXKP&}UHwnI}CwG -2c9u1*@>vH|OcwnekNqJ?1o){YhRQu(XdF|uikf)u%c=xrQ#$x{iqlQ5d@jQ8Ron)nu~^pAm&@kX1NY -9i@xw`PwS4kWnDK8;~!$6k6g4`DjiitqhyyO`p)M%`d4a~r& -K>b}|oI#eoCkn?$pQ`e}`hrIVw@w^h=My6&M -y(TQ*9VGC70KT!xY2gf%6SP)c%i!Ra`#GdERtx}2Hqsrc*be+nf7sdsw%peM>EHj83VwaybF}d#L5od -q3IhaqV6a!dAwyb2FHb4?sc2xy&^LG<~OqrgZb|i?|b~{Hy6>el8G=&;?jXi@qeU?CyHeJoF!a9Z=`3 -hVT@#3O7LUl(oQb#h|YdqKk&iGjIJ)~M9ShH3UmQI%Zm=7ygkQ5?w4&+F>wPe9%v;~(}R=*XTv{vWtx -0XXoh8)tXmVVvkiplRK`Nu~TWDXYcFwayU1p*yPLun%G6PYj@DjRm6%!C)nRvGg>tSo2JuTq8~4x@D^ -=3o1UDUe7!T9RA1Q0&G5(l?_G?XQEIav?obrYyuGX1xGjz}hwB%!QLny{dah9xl)C9xeodOQ|g~@(fr -)xdg=+Wy++pItY&$G?13}M5~V~g$i -`Bl(0U{Scw?lh8xw*l|uKZ=b=+2 -7s~#7c`@eclY1&{ZNi4Mx*S?qYKs`(Oqzq)uH4gC^cfm_3h)++0D)6LtlMa&!C}2%`x3ia_a+;EJnzy -!x4hMx-Ru7@iS+1-@kuWHhZ~ri~Z_+@{au4R9UsakpiK02+p?C*wGiJdj!S#1?@%#Oz$t4J-aIVrMoO -(+)DHWwseLs#{!2W#`n;8u_H0^6YLd0j?0o3m0+$o2+W?nVqC(&h?<+r6k2&bll1y=^9y#>fz4}h9Ma -Mj%;DYJT#l6ZUF0c)mbxEU<8;sfo$V=(DH6g)LIjgbL2W8>=^YxS2? -w_Y~8ZL5yHim&Ka;$0`TVq<+FiwXkV^y$nz`Txz#o+MEa&W#qM6)0~9$!85p#%YwJY=n9=nj3&Sz+f| -Jrno?hu1vMSwt=-ncpPY2 -2^Z9%ljcsGj1}O^!Lwu2S1nEE$O2Op+pdJyx5_lVM`BllwBW5F9@uH^nS>O-rBD5KX9O8YI01-0>3W -K4IrCGiy)Hugie(o$YG7(eIVkzJ2H^&H}Nc;jYy@-LBouIT&z8QSjmkKa)Jr1|>)Ks}V4{0C4=0|XQR -000O87>8v@tBDyu&;tMfL<;}_82|tPaA|NaV{dJ3VQyq|FLiEdZgX^DY-}!Yd6iaei{myB{_bBf1Qy4 -K*YxvpzAxJrN}-fqODV^pDE2rOD@#5}-dz5DNAi1|>~exc)@WWIjb>CC-2h`%=Q@LofoNK79Pp#nvUA -A#brcQsRx97ul}ll<_>8n}xv|L1OtrJBBez)Srs -1j_RX+F2nFqfY*gBz=y|T(Z@fTyXiK3{G+*)v1r}Y`X6|AGCgM*L0sGuiWIZhww_KM%m^I{QS?=IA^M))$G!bV|wW^z2G%eJ(gSg2%&YFDx)*qO-Ob>YZX(y -v@cL;1BF(VFij{ppsPh)C{bca*=(0YYwttdGZ}HBafAEpWOUc=e@fQ@CUdr$P7R4V+=Uc;&Mi_kl#C$ -Gj-8MtvuLe{W)MioZ5U`ztI#MEih)ry&S*(D$CpzD;??~6WZR|H`)yGPT^FOr0bhB?w@y2dRRa$EPmI6V&kyEngV<;nmHM+cIu!lGw<} -Jelt9h005Xw~D&)2Du;hXy|@ugZUAW`;70V$T;qRm289Zw%Sr3P>H5U+0MUt_);PwcxSh6j336)q6+PzA%*I|+0h`BcF -7ymOij^?gclY8y%PKTt~p1QY-O00;mWhh<3YVc>?^2LJ$Q9RL6o0001RX>c!NZ*6U1Ze(*WcW7m0Y%X -wlcWF-+pIEN}?>_-}z?X)_wqS&>z)ld{n92=!A<6EO-YoYM}XR|Oa)qal>y1&YFPiL-co -G>#lu@OzahjCpV#0d=BDa~F~tC=3y4I|T{`cPL;mjX;aa}qqSz$k0hv4!8lAc4WNh1!tY -I8zg)qj7>d$gE<)7wbx#MAdTXsXDO9Z0>=qjt;T#tGl7rkcdrhTE@O_wA!=J!I8eOcac4rX`mE40s1} -5^ZW`&rbkTZ8bZP?hR+$`I;YYwocI%Ch_zaNok6M+Rqa?BZV$J3-UUx3*$+K&kBw>3)6rWH9`D{BLJ$ -1Juk(fHs$hq?QPjc~#nOP>t=Su{_vl)0-aMT{*m&^`23{Oy_pOc$tw!Hp4p-u<6a=C2o#Pw#8V%+go+Kn=K*=n30Pai-wo8^3jr>N0i- -y!MyH4SMOfSn-92=sS865~H}TtSl$o|S&0=&rhSo6lFnx~rSG3>h-V*z*CmT)mp7R>e26ZAFdVbq9~C -<;yF%E{vVIY>@fMq@^7};5OH+ou1|;jcyjap`5-**n9rVv(+5y!oKyew@e3D0Uov+<3YD;YE8z~7IOgwQGrGLbJ+MUHBi|bk1Aut$@{PSzTP|-oho`vW9XHMY71xQq!!N -=DVZucy3-n$xP6u<$`V#4PDkP6Z>_sdzl;ll!A%iX26=e-387Qo7FA2Kz_mN8j$<+6$E@fK*7AeRyNW -g|Sv_vP6s&O?Hn-t@$%6@w3M6S~*kXi>y3|LX?$}r>Hc@QwpgLk-Ji94(B4~yc!%>m5F32|fa+*PcI< -;b5PNGkSPwawT75v7;lXvUbj`qyx6KqvK#=1~ZK+wp>L*U|1)_po(vtcI$O-FW#_1R$y+G-Jj!0a4w* -I7D1{o$m`{_Z@3II^T?gAZ{bWE`03_-Cfrn=InQc?(&Rt0=o;{){mF*I0uhf&@BSz-^t&N!*HrOM-0`|@^-7eCGR=@xjF -P9&UKOF&3N28eN%Hwcf@X!UjlRGPY_UB%KBQLXg3egF=5lPa?zSSQhlIJZ%yDNOH#rE_B&b<;*vC5Nj -()-wE3~OQ@S_1zh*RMD8ALKC5M7WUwia7c*;47G`UPGp=4j2j!TOwFacIgGhO>Ms1`vd{WK!dUIFA8dFnT`n!Df- -W!nO@^gxs=!!~F9jvteWIM-2(kD7eq -9rV9GrokL#&-my(3`L94o{qpEINT+C$7}FmWq49|4}uh$pm>l6!Go7vBC#F}?q7moLK0k~D07`7v}?2?K -^#=SSCSn{is{ZaOgMeg1M?#Q5^1moXl`C1jP3EN`Ck*6(JyO5^I#c*S8Q{>(peA^n|=HwvZ`*1c%)21 -;FJT-&XG$CrMUm)`WGtZW#in6#(47kQVXXje>4S9~o%ce%<9054*|k+)4jFG1+w^FL5a0|XQR000O87 ->8v@HVxB=>IMJ+JrV!_761SMaA|NaV{dJ6VRSEFUukY>bYEXCaCx;@?N8e{82_HX!U~#_Y$1iR0mq4x -cE@(&geZbd+-X9Y8#}E<8b`L%Qt7n6{hr58+N3RP`*3`K*nVDr&x;@9`~ID&^PHF|HH3*g=SmY%HlMs -STaj|1a*X-bO%8cpkkzg>lXO#D@n;G))CgMAszrX1H`HMA -$58VMjN-x=bU)QCBEJH&5;nbv~g+M$_Nj}t5M{_^@_bbn`0=!}E0yp_`_P1t$>n?;UTB~4FFMVU+T+c -^o3V)C{LPuE274FOt5+{GExCm8oO?3Ok~f>g^|W!&S1=KV%6t9D7BQcaCfAsojf*pd@OPDbA2Tc^<$0 -~X~By499Jwr{MAdXD#6mi$GO8jLWp`=JYUC%bz+t(!~< -<=W_UyW|G7$P40mFQ)sk@}94`&;di0#K{nG&A)`$&IPV;TSCIMd -3nF*q;vhq49@*OnSEI;Se9=UC75zwF}rG!!-`}?GJpb{g==T9H^PazY^z>u7Z;(0(;H~({>*ch%r+2y -c@rWI@`iwlWW9m~qf1R*li^$Vb# -#1i5W(NW1M>PcIo?!^G6$+a35@-TMi3{b0XK*Q1Z4tEsBr2IvMeDz$iKH9MT -(ZHx?z@;O>eH&awg$2G7=cg%#DH(;(9+wfpzC*O%9So)IHvoJc6>DbF6sye>+v=vbH~G?f~i)`l!NLO -_@#i-9Dqncd>UtN<&}wM1ydt(#bDwX%R4Zv8|6z0NX_jSx@ioRhDut6zJ}eJm^#R>dY5{hD%WZK&HwL -;5O+vjQK<0ur?kK0H9=ocDW1mpkko*;II?dl~a9xfexYA!&*btwyD7!T98rO|+gU7rBO`b+)Nk{!~E} -;Z9Y%>h*&?CEskMzaf=v&qmem0nsGPzEsJ&bar9cLe-Az353wb10D{OfzZ0hZE4s;2$eD#kq(1zgCMeIwnBQRgF&}wI(pjAFNbFjU6|T^fN2kuWsh{i?W@$lwBqJ2*9> -NK>5|Ld5e1Bt*jr}YP(}@PsopdUjN6!lJj77gfs6zV8)rd6T!R7)a6uW7+18R-R_G9n!{|8^B`}SthT -l+idv@?n3Yj^zVYM8doPFIM|KVEr5;(Jihp#dEv>YCv^$Fc)I;Tp9!PVvO7a#6E1ySUD$T?N3M>jNoB -^poZ5~R)OBX`z=fzU-Wyq064C19i2z8h-1&b$iHhk?rooEsK;A{5WzwtQ~l8ph5#xB4B>X -Ogt;eb3a*{Z@h!m*V_T2Ib=lo0Ifv8$5rpDf!wopLmfDhNCEy)(?>rs2|BwuK-W`d%7J`;9*d8J_MfZ -){F;F1&VW&)Cq?%=rm94sjjP)h>@6aWAK2mly|Wk`aerI$?=00 -1~z000;O003}la4%zTZE#_9FJx(BbYpLBW@%?GaCz-KYjfMkk>B+zCQ@7kZ5pwCANHb(F2$87n<<&nk -+i*AGQ$FaA&C_TZ~;&f?<)WOx_cfN00WY;y?eV!P%euEW_o)1-8~JSu<dA>7Rq_Qw6rJvxCo=YVP&E|xia^Xk=~U%q+u`W2gP*~c -*Uwru466s}#?jUqP0F&P{3jHmZJ5R-2-f(Uby@eXahgL?|+@Yjn27KB-zhO*xn -tLe=W9GrO;Bm+^S^_5mpjiTxCm{8yGz!^J?7IpTAY_~MI!SZjc6nnRy$sX -IIxFLiWQ5_I1%6cM#@8Vtnrp2q=*ks2939>E^wk$1=c^AsfdXYc5!e}O -Ex?8DyA0IG|&DvY?(`unZ-@_(ii{QuR@E)j1PxWmc!CWvY5+pm51IiO&4BaJq7n^sdbaw#uEh|s3Gvh; -i7bg#~TtO0FENsEEzQ!$moEXSwys|@Da|~iDm%P$wdofakFL;Xf)nlz1!sgWyB7?>@UNoR83t4zGdWs(d_#IGI`5v6XxD5;w -aS{fDimYAD}_FhzkV?v6MzNJ%bj(hBIshn4CAI;;;6HCdSOz2;COn`s_A*J74 -8Di{l95VMD5RGIzYce4<8oyJ20+sU{E(OPXe;81o(FR!aaabfD}so}gFO>G2%0vjmm|A!6m -j^EEWUL>|!j1>V21s4TQF4!1ih{2=_po4?NkZN*#eB4zClTQyx@x64bP;_C)QK6C31<%8ruclKc#Z9 -ZBXP|}%9LfrX=-dLP6Ef!bylNQGM>yh98)eZc6S!j#{0bzlvs3NxD#njdF54E%LaVuJ54R|3)-(ukhY -|39O>M<)G&g}g1!FpR%Y)}C#8{^Pw!dSc;DyGYByFmUH{@Jr?0+@KT41bO?3DhhF|9(7TK;@NkKu3S@jnFg6ZVO-6%`8~JP;X~ZF -q%iU=Un%7|Ch8UR4HBI*&`0rQIyKXYFb;FAazZu?ku|gpAXL&AIodY3q@ha0^{ODdNp#QUq -M5Ot5>&i~Vw6t(VAn#ZGuSWec5sIV6_wl<`;dKC*Wq(}ybzIO;nyEZ|VeY%QXM&+k -^?c3Fp*Ow-Yo)?9chB}mf-eLI+X}rx`7z=_sPn`Dc>(fbT7?yF&`p>x+OY2X<^b8Ad85UNY3>2naM(i -Hkknh0`r_&C7$y3kgcnUI|0$f7DQv10tMB7ktDc-^@l|=kP7*rkXr6d>_{=sxGl6c;bCw>ySrm#w?5j -T_uNuQ^eH%!QvEDzInET5sXB6#}dN(sGbz-7f0CzRar3SBX#LALst!L#EJG1Z!k8q~A&7+6;E@Rm}Xz -!Cr4u3R1mPP-x=K{nupj1yKuCV -p@vDx|Lw$OH$L<=}aXK7<4zUR8RoMH80YXzX)CN(m2`J?TVJhbSX-6lZDGNLhPB -Onm=KoL*%=td)jF@mz=2`-_8C3>Sq3qxk`4HKu~{ajjKFOySVPcmps|iTJW!u)Wfj{QeG- -h5uYunRMAnvu1=uD6T5d%oYun}u}kbz*DtU@JDpX~bX!%XpRjW@vLxK%RJAyP&-5tlZCYfq2=6#|ilVo7lQvDg!$eJ{zhSvq;I3^cP -WjIB=K^SnY=m<)V@xRn?5HADEJnFl0)wckE}@weG_hF6oLxU2u9QQ)Y)Xcw**$*9WnY)-JP_p|eTXak -AfDEe8+0pP=1I0Ww^_Hc;LEBO`j%2vt6Ju&)VB)ZR}BxMQs5cqsxL8oQ<{O@O+51<1;+j{;n@r8N@ut -Veq9-fnw8WlQB`~cgq~eMFJ~bpC0o+zw^ZMQ7_u*AGR}>IglBO}^DMhrX}dG(7+3O_=hl#J_WPlx -I*ti*@R%rTUTz+|Zr0)NMelz@%>2PfFX9lN6sttIk~CMy<6#$V1swkrbQLSUIv`v-8uBY;e{fN?(2ksME(TXyarV!|%hn;K2_lL{+ZU2sC88?4?LwtGpL=8j~^mgE4_>pg72YftJ -a4Zh;d6NLz`mQfTGUbR{m#0yzow-NI09ur||o19_a_f9b^9ewes035EVt{O|vToV4wy5L3`!5wwD?-2et*h6_*mBLr(0CzXA3_3^Y2c0mxUj+ -D%z>qp#6Eool>W~@3IGxgikNgeD7gSbi*LFjNz6ysj1+6M+Zkoxgd6m!5?;$CpeHbqoG$|6-fc!DYyl -fFROipfH0t7qGMsz*3aw`n^T`jrLwDKe~4We1st6E@(Fpq>FpM;H-T4-g7~1#UV9Dv}}YK1D3V -j4zQ1F|$!1=&@58^;_m9;b~{(E?(-uS29zx(a>HjCrCP2Ky|YMi0{P(gajF{pLJM9ZyZ3zcE`6iKqxy -=lssSt$#Z;>B}QB5vm_0$se4{d^}catVG317IhUhmHiY#%rK<>ED{>_tIOSlpIiOR~pgcae<}_6bXql -EbeqFKsg6QzD=)@3P6(ClnR?C1Xvlc)PT$(M_;}PNeY2s8^`e(UBy$R`rWsrn&Dc|`rQTpvRN#}+i3iutSB(XG9Qv?e5(#$I1uG0dD^{BA10fug8XcgH%>>CWtwc=d7bn! -z*W%@eyxo$A~YoxoQTW&%P

qgqpf8&s*N7)vQMH6E!QTTLVEF(ds%3)2xNZep?EQ+ymXz4ib5`ew*;KnL)8K~zTyo0yjrg-r?EnC=LjT4O5PQJ8YY5Se7Siu}+Z -CX$jz~ypaXcyX1l2^Ry$Q6dJF7WITP;>+s0BmK(&yw7kfD&P> -^jY76r30goay+evwNQ$p{d?!7|?Owty$eTt-M2&r+}*UZ&2#XiT|fpeQ39)HP6K+2Yl4^X#;G?28QoO -=py{zd-Z+76(sIbWm-vf1T78sgE(WEiJUN+5RSo?u@85rQ=oYu_J?IVKD6ISk|Cg$0~})7u9pyYaE0O -ih?aEls~_*_EPwSwDe(v3v9bUn$A~wQO~g1*dfQjgHFZJvQrbfHMSQ&3Zvq9KM5$3*FW;1An@9+wbY? -G^!fl8+>4(8)ugU2ijRPOYf`BdOR6#aYG8u;=l~2UZp=1ZBd3(^QmYh9=@7 -WRJs7nz-htXEifCV@ieF7!S?Dq8g|~oMt=2wLiOrUy#-=uvAdtk#Bz43$3^wHbGNHPGuBcF{PLEggQb -`^zUv65VS1}r^wN}fcfV+U_Wx+#N>xHZoPj2TEs{uO1$?S42p5CrmRGk|EXj#xyUGFeS(8?2_8YT9tb32^(Qclox@Elt0#?WL{KwD1nkK;iZ#;D#5}m+stS -UhfwL%e&Wb|57~0MeJ8#))B#$#mx3{E=M!blI+6xZFA^&ww~@;19l7JHBeX8@9)9fY$Q+l;av(WH)qtA6&aI% -jLrt>Xu|vZbN+kZ7JFd5GM(w>lt_uSfBsY!2(v5A2Efrxd{htQAQ+tx=h~H+uhek^AnbU6Ae;DX}8vi -yOj5O7b+sZ_V^?_UH1Qb+70kpY$2SGb0eZujngWUJR}-bDk(!Kk)n5) -4a;i#-T9Tbc%n_DrhK0uag0Lynqm}fKp%U>=URTkc`+{;=xx>_Z%ES$-P#R@sa&5qeednQ_ -e?XPg%@D#Zh$7WsFhq%y_@z;u%IvR8RYJ5KU6uPqShD9(5f&z@p+^}liar_DryKg$6wR -Spk}4I&+tW#-2#LbJlA>iW~!YH@89EP0eXtPnygO8 -y6mteIa`WN=TOrAyW-I)H6o6^ACZUtO>$*$(w8zfYs8$R>gHeVgnTllIBK30{A;(U{7#W}^Be+8$2V% -4vH+)k7#MxX=8LQaCx;?ZBOGy5dNNDF$JM>u5NnVr}iX -HHHo=c7wpI}M-f7iu{UIkW1HO#kXHTgo!NE#l7y>NsTNA&d3k2$ncdmt7=}f;5&U7Pz)Je?@xzB-A&f -VxB2Qt=l;ZgV6TRc!F-)0QaVdF`1DCL5g57W6LBzRYsSOLk7!(Ugma%wXHmCyPd;=vDvdGc9xK}aH5e -;HUFeKTkB?8G}plMpYtSI7wXG|iDrIrkiW|hxTUk6HAoB_{)bXyg?aJ4Mf3 -Ivl%@Ptro1mJm+ty9WlYmo63uc8ou=0XzeT4EiPs|~9n-&t$r41=3`1`d&TS>x%<>ZIe -{9Fi-EXxRkGifSrG9(zP86%ZMt*Xehu&;MkC|5IXp7-Ge5Xu?w -@{BD!`$7rYrv!{D1Yaz}9F%<%olhHEbx2UiiGMd -Zv5HS+u~aE{)75W&Q|^df|g0v)yj^xRp6g20zVdPvoUtH}QAXcc&Or{mY_1M)4jP2mxF!BPQ3L6G~6ymFm)rV0HaP7@p8=TOaVcHeMMHpZxq8$A -K4TavmYp|#t-L9btye=CH~n!}=Ym5A3+Fk@u!AYh -9D(x)r{4yrY5s^b;c!&D!B%mJVgC9XcAsN-WE=SDr_XSUKWr!kd$u8Guc}FjIzDKX(r&n9iYvCNd#*v -A<7~~k4W}qL(P(UuVV7S+!hznMBSd=?P+d5-qp$m1IwjyNMgz&Q$u98s_z!it}|uXu34+V3hR -(jZL>pfC)q(+lvc-@3{7ohT#M<&R7wV&ou`3_@b^nb|jw3Q!nPhG2ehVxFHMPhgTQmp-jYi89~wy8!{YA!pm*ie3KRjSRqejA3+y -AZT7&S1-s;b%Z?ZjBn2Np{GKdVbhLFW@b+dy7jjh1Z$ZQtvh^T_jt4a+k}>nI?66q~a_>k9eI?x%CM?F7?dCR0E!pY>6|t+tMOE@^wOedKZ?zVBputaG&b?ZcfG;o-)1q1r9}P0 -Lmc|NlvAB)Wv@Ws+ogiy%hg`;1k4^Nel+?X!kvSSDM`U++O22)GNkzFmIO?O|v(`%bcHn|bzBpI!z2= -ODWDb4fcP&J$*7t=+yE_Par=Tb`~RemK)FyO2AXT*Nr&I!Y=HH}wYWJc_v2M*iF6K~D?5BOz -cf(c+ItKRGGDrO$Pi+!&_9lrH%61*Y*_kov{R2=-0|XQR000O87>8v@sv2j!K>z>%M*si-761SMaA|N -aZEs{{Y;!MPUukY>bYEXCaCu8B%Fk6O$Slyy%}+_qDb~x(OG&Lz$jmLsFDg;+1aWi}0xAPMxwyFE;}d -go;^P%;6{3~EGD -Md?crI{xeNaJ;!Y~ZH_Z7J{NUP+I!~?kTgQ&FW)>Rruu|e>A;!QzRi1jJ)%y{e>GcGDYGVbc*fY}h-% -z{@>=8{8zvw|gIW|g{3UUj5u+x`8Urs+dRq>gd$H;w1n_q^LTs?^{|L9?j -9X$u^EAny^GNk~lWzfpm>ZoiRysJ~629B`|;7PX?Zg*Qzx+nN55%5C*$<2iEUy&^=E3Be^1@g|SnaJ} -klc!ZaAk5~bZKvHb1z?CX>MtBUtcb8c>@4YO9KQH0000802qg5NEkmuqEiF_0MHKr02 -lxO0B~t=FK=*Va$$67Z*FrhW^!d^dSxzfd9_zdi|aNN-se|{87{UV_M*!`wigPQOSx?~9R{J;$BBp{c -_ev~nbQB>Bl#sKwv(3Di$s>b^M2^K)apbiEo)nApp@`arL-k%XOyTd(B^%$YWIcO@44J}uNwN3idU=B -F~dcs;jQEJvow|o@dMs!uHgjI4zSvCVLIR*EaeRt3R-YuHQymZNi$H;caVkB9ZhxIQw>JdI)__SRx+D8wjZNDoC>;9zVvY>*?qiG -J1IZCBUijQ`qHlV|IbL16Tw1>+TD-_IXVd;Bru1+{cidiVOUzsq${`mkPXwLEjTv0e7+c-%!tFe-3ee -i-_etBJWczR+sKOn-fTKR2W2I_7`+f}xbjaXhaU&jqJkarXncDGpcrbHPM)XNJwzph%(nOybq`L?$VSEjjNv22mCGtZzzxeE@`dBU4pdIa35rKZLqhe2W9SCiYyg2N -0Sce`k?kH_B+i2?UN+A%_#RY&BKRHx5+Yz%aycT*6mdhKA4x+QIAsaIW}>SLcuND3Ak?A_t;IN-Q!DV ->;GPU+Pr2|OC`zGL(RBUlW|7P7+{+2=;7aUiAx5IFpAP)h> -@6aWAK2mly|Wk_6U(iwmN000yL000vJ003}la4&LYaW7wAX>MtBUtcb8d1a143dA4`MfaW}xR%~v4lv -u!awx%p2!hr&iT59i2$p8|llSv~;F19`RIN;i5c5%3!T+SBs)St|6E1FHIx+5|KBYmvyIkq4ktx%z6D -y>Hn}83go3%`i5EBi{;aLcI1`T2hYdv4^HPFW1|5|t3$fchRt?S-rK)Y<_4^T@31QY-O00;mWhh<2Ff -b8jM4FCX`E&u=%0001RX>c!cWpOWZWpQ6-X>4UKaCzk#YjfPT@%#M>jNHr}lM@>!AJVy2)0xEbM4hpv -+UmG7wnIZCPREKzFaW93HOYVP?&3j!55@L$`lUCCOai-$-NnAJ;K|9!;+6QhE0x%by47r3R)RHL>9W3 -EoWSg2tK^<#*|uvtC9;f_`$npk@f$6xt`(ViUo7mn)b2?M_oR1~c$YRfBtcFO}ok6Fk=T}fUzgj>;OsGKa9bqlKMA( -i+U|If-=x4fzx!i9MawA-VIhg>vmDQnNqD%^$Vu+9|8MUIIt -!;hZSSmfuujG!e=%nye-4Qz0Hk6EvEE6^&-LFqKtheK*0w18|qkqk@X%ApeS -K_-40b`79B4d=yY&oSVx8Cg}9^L!@=CfN`r$TwFeo=5v&k`k{Ab}U6zOhi^j8&HF>qzj_70SWn@IiWT -x48!E`=@rlK_^o(OkP&jPG$3%P%3IhE*qvOJq`cwk5w6v*Svj -vtHxL0?{ca|?bD1lJT=3ce4!CpM0r(eN2F?RpVsk#IzMnUWSD-hcz%qI5_xo|>1>3tBu}72X8~pnNLb -#o$f6J|tjg^|AX>9s6c?qU*eQZoxTt&urGGWpQ+n1Vzdg>T1*qt~Yq$(I!w;h6Q*hx%iX&G4d&7wjHUqmMhUo2qQ -rwk%vzu=$sCiZt&^Km}ZbgBT?P9B-vs3|y-vo5C>eYz6EdUq>!8>GgrfM~)F02L6Mq3nx&jk$r+$C7} -0~tM=?^GR8!iG6UC-Pr#M8wNz-^d*e!v0_(f70nfV?4*^_Ys>b+8dHuZKoKLgFnY1)Eswhtl64EgJOr -Q^QR*<0#$6yB-TB^auBFlef*E@(_IY$fCU4`k1yv%0O3A4y2#ic-Yf}q>KDWNEF -%q|AUEKIUKmoC^%IfPIgx&Fb%%4nbw<2$e8dpFbj|Y==f-!g?b|gTZ%f^&Pt&P{gswumq{NPrcrposG -NR<|7ggo%3Ymv5z`ebdzw4jA#wfro3iJFo55qW|CM>(gO54b046B2mtVb@{CvX0_9miJ%+WdoKHI`)c -J?M`;LP?B^Cgv{wEh?0;v7-2NiU4sWVQ6y$Z@OX*flyR^P1IcWd@-5{h)CN~ub8!f(>=(r?$Mt3pu*{i6`P0`Z{4?dmd@g@7yp2MD1(iI}b6ErpX6Agq>M -yZzJBFb31$URl38roKWkRl}|(|n1l3o2CsF&f6zawdOxj!bo6lXwtJ+;+Ha>{VZCS&^}LrKN5t+oK$S9!gQD&UQ92r->mck850&@UwG0`t{Mc!Zp86IOD5|&F2{EYqbTU1if>2dDdNaxI$eGto`AZ -uxs6=mLL8O5n++!mj?hyLgrFdo+u!SnF!; -0+J5ux(t8tWQ+NR{GwS{;ye~53IQkD1@U!d>>$cU-ypPSpTskZ@+#Ym2Ja4iG5>oe?lS7g6H+bW<}&a -dXH>3*+r@8lD*%iXkIEa7^Kt9APB@5u}7d$VL)_F5JH0PB3Q#-7KFdE!FJ;gK2ASi^JB1oo_NFF>g<>$m7(Qp+lF(00#S5-kT($864T~=EMm*|9CrqB!YI9gG^ -@G$B1Qj+`KEhY@kk%2D9u)B2Rr%qR)^<>J*<+lES?dr9xqP{Y{H?efjKi!`|$U#A^qp{2}l-$c_;7ZB -G5=w`BQ&LETQ5CkIhgfAxfX**4KrnPl6y2_A0)%(?i;-aaO(x~HHWVECSG1{fVRLgf@A7WGK?R)V%6K(}C$Q}ikcxQdwK_0gW -z(v$IteDa1X2D4#&thw-p<@2-$e?MJ=6XIGOeVzs<=z&uICmQT(i0|dL6vgN1ugCxY(w;xD#%i;tNI$ -FT4kto*KceNY<1pV+x}PN4gAI!U{Pout+dZ6L+bRn$`c{+2+hoLwyG2D#url}v>BjO5E>HFE5J7lR7p -qC!F^m1J?JzSxzxd~i^9`EBL3WTFz~w#8I-F2lAIQ2qYQt8(mR&AJ!p-u=&2v;?@cYdSs-@b}S1_TUKSt~K;Yr5VTRwQG2q9U0fd-IyXq4IK)|wsYs%bFo!b+czK5QqJO?~SJiNP}rl>` -ZpWc|F)%(FniK~+@Pu7gCz!^LyR2t!{Kch!WCAI@U*PrqY@9z-+3L~<4d%=WuThj^Ko%iYBXs)bN#@^ -Vr?197S0f5aHK*Xi(EZ6?)pCs&Ktf_Rx0VJN9Q#>`fd^eS=JE2U~bl8>oj_UPXD%CZZOKy&4wKQr)w( -?F;+XDr#y{jd&%`RHQ$EdzVVvyc$%s92MaD*}qO+}>|4W)GVqOlRTTAnTw=8)nEn2EQVcHxoZ8)?IuI -?;<=&wnu?o7LCRjSC2AJQpdzxxv5pH;Kj!q89BYQr;_f^JPy4s0r6U4uY3yeMgx7DB(6c4g1I|UVt%* -s1cmxsJ|Ai<$#{&O*7)BXz%l_BAk7S(pm_WCxkDRWsq|54cr!GkR#7Ja -vu=q^GtGUL`(rf(6w&HN3fwQI40!J@z*&GOySG^o+LA?n_jvNIQEY-J?GV_yyXhOybT($lUzTG87vE7g -RUKB-A?Yg!flX5k*^?nou|NZEZd|$U$S55ZzAbM5kjS!o6^hUPTRb -5`(ie}U5GTqHp(Tkzo_iHiClcD+)6wR>7_iD=d%?Zmp;R -#5Fg;oL#ax@_8}T9M$(0!4 -#|DrvPkwy+?W^y8{9vkW+PkK1%ZM({)l8kG377Ts92jqZH -*D1ql~GmK?jm-y**-?z}EAAqJJFNb}H^~B~!9NhfT%mA<3bs2%Zs+yv^pC|pkK{74&%|`UaZPkzavKG -zlJbCx_UGeJm*RQ{SU3~N9`+tA^zIgNY>(}3Y`OWJ&)!qYQotB&_<5qx%y1KlV*ywj-FT@-O=Z4-C;u -Fvq&>yaweOGL%p0K2bxT*!cQ=rB|Hak=!fKhMouYxHDiCJ7-6?N5c6I>T4r!I#5s_R>zcPP_~a#M`r( -?~NhPu`R_;tl;OcJQ-U?W=mj{SkoN-s%Suh3uf%0QEs<+Gd_KZNFps2|eBBxD3_z(W`(W6I*BVeUVwKwE2-CxtYtRT))6fl)AzP=n=V2VqNo#h|Y&%Dqj@*8nKWKiePMr -;%0D^&+{58K6%A*IaDiK=!0sTaU&$MtpAr+>-vr?c$i#l>GfUS2-^cscu*l$ptlN^^av@@?A)dQ)wa9 -Yey07FW|2y4)_m>f3#ny_o4T!MB$Zc0Gs#knqJL`V8ED-#1=~xr7VjUDxF>C?57+{B>XTVh0qLwX6RW ->k$Ud>l)DlE2hD7z&(2*q>;l!Zo;|LeLuG*g=Gi?KXa<`)-+v@1P$7T!X}~T?T{mV07 -vP7-(V-gV)&GsO|cuUpoeE~Q7&t52AIMdk(5MVdGe}7UO6VTVs ->YKoh(P?8UZAt1JzBy9Fs$SjSo}uUg>J!L_w7E}iLyzS%Xr7^^%QSH3y=90w~-MR(>j}lb68e`2&IB7 -3jOSD!@cdzY@H|RlOg@sJQ>cCCkYdi?8VG&&Lv0W+-hArC77z>|UE;Q)M+MHvc_Ek|2I$7vCIc*VL -4&@k+f`XZ*FYwNm-k3U@{4Mh!eRqh3~}oTd`Y` -av{dNLev%>6?%H9fa5#Ni*T^Yn-YBfvL37xG!cCdf;;I^}9S(ZaE|BtIBVDx3!pNl{o5TM*kYWMcA`$ -sjZh-?;9i}@$wIeZV2P=yDg*SP!8PWxHGs}de0B`@De&;TU5{E6F9^0 -&XcHMH1+B7*vTDkXr50>T3mZhK(Wb4yg1CW!~;(Tp|6 -u_WXxv$3pl!+;+jh=&&UMSd2Ksg}pY25_1uun=4`5LzvT?W++vL`5^x&TF|?|}`F91en)8N4gBIY_X^ ->`0XW7^KLu9|z>4S&F*TdFqKn&i-qeHZu5a=@NmAcO^(ACwLnX2>T#VpdIC -Bc%?*?EcyG>_CMi-ekvf#U2`Qv(bNVqbNaz4lrCaEeQ5S}C4rmLqjRVjvL*1GEeO$6apbj92%CNr?Z)|!+5RXoQ9@YXf>|OK1emi=j@e -UgHL+J(yZ3<1nsfbqD(^7TEoJ++jieS>8nDe08>lKfhjwDr(29Z&{Gt9Zh;ce1hrbwX)-jUhl6Ym -bvZ1I42bAZqf`5CCM)gTGd=az4QRg1lqN!%oR2)WZr8IE`7Ke#}4S&w$%gX%rK}S+a)}#e_Z9qfCbyq -QyP)2<$O$iZr6=V1W3jMA${HxMZU@H=#&kM!Gg^n`(KIMTiQ%kwvB`n4sp`uaq5J_r3$_z_Ncb7;BoY -B*y>>1ZfW)OfMS3a5Yat;tp=7hJjDSf<_w#bYi)bHZTUGga`aBeFHuNh*|MK7ZkkY5HM&+qv>%2kICByMC)T#^t32|gx3PiWguG>Y4UWEjE_Ft>MXFo`eHo$QD -8vQjbbfW7?ZNrdRiLVI>$a!Ujvy#8%0CfACjlQSXl;sGNiDUbm81)9PJ;U1Fd1S`@i*xz06}&gQPke`Y$;wcQ>W1ljAQCvl6KS&v@ -7tG?Wbx#EKbd*59(^EG+;fj5_gKSJI;m!(ifmXfDgncudfgkp$#SKh-In7tFtCJS -ui2ksX_A-$JGl+p4^*nHcv;cH{3)$H4?H#)4qZp=;Fq+_xl*jUY*t>lNW+m!f${xs?Ym4RWF^<8;CkR -^R&sja@Bvo(8DZMU$QLC0isFyw|BATPWIr2=JOnL=jwYJ^3aY -UwVjPv?XR-*hXIW)_5`xXk6e3hz{myu$F4`O>v*r1yOv@m7?0Z1awG0cKmyU4LRB_CB$)R13=H~54yw -0Fi54F$jD!R9Ew+Em8SSCS8i_HO0#siglU0)m+8!6x@NbTmO0|9ME25kUrfp){~zzk3)m;0p -0*deZbZpsw8JnmJHFrYfN9w*DzDP%Q~sFtPkT$dtx6f -a#wFKP-6D-dXGHwL;ha2l?vz{=+K&}klM%6Q?|@qzGH&`+EN%47+yC*+>+|I8z}hg(&`ONxR3xPdkt( -MSNkDh?izK_gqWy36b8kUw1oC2#$sA^38{s(0G>feReB5n`d=0oW86N>{NT#+^*$WrVBE=;7Rd4>Yt( -we=FXbK8h|_T*ifm2av_PU!dcvrM7==93d!nn@<6eckki9gABty4%e09#|lgo2*VeT6YWWS?Tm8ivrT -cb72!IGi1C=tjDnIk*QxPxYbX^LKsDk*r2wl4f~bmP}(4jK9)2++DpHOHY2h6k(_J}5d+%8+rJu5y$m^iO9dmL#W07_GZvrRsg+Z@-_dj0}`8)a_x5kPeo%2Dp&yQ^~9acYc(O?tVa6jP54>Dv?v>& -}kAFb`q)PglvHbR`OPn@=%0uB`haZK+g_@wXoAXlPLn~)p!{0S)jTD74_O1x?*6_blpeM>JrR`esBzd%?l2@~B -M`I!$(UTdf;*w?Hx={{HG~AA$7&SDPWn%?)Lqlk`-J|u-*XjEf5Vs4Q`3DqJg#j8iy9045JQPK_w?-T -b9Yw -Q@?smY2i(1@@dZ~`jy?Xs`Km2LB@J0;lW!e$nH#|Ypt?QS?Z6NN)bgh3uYxuZ6TaKPpf&)h;-$;HvyE -v2Q0naX@fuo5!FBq{RwjG@qTAm_5sIp#R>tM|>M{~HHpD_VnM7te~^IKr;XP5CYwq5-FG_u0-m$6etQ -x=XxyUI?C<)$3VLYkF=1L5Hu^4LOqYrJM4%%d;EX?4<{aWH!87i`ND;ae$mZ?j(m3sv3XdfaKY#XK1> -Z0-{tN$aP;JjTP2q;axZFys>fV3V?dix=xl04C&q4f@rm<1seV!>~blf(?B}j|z!t(7-U4@>@kFg}bu8;Wp#JdsvrjlM -SA(&*3LE6fZ1$E6ZbJ)8+8N5;+^xvZx@0&*S9nh)zb4v=j*df&s9|cANb46W0IfC)D83L$d>^dh|s`0 -|jF>1`Jw6Pku!pk?9i9it>jfQiko@Jw_K#z{x%ckz9{sH=I9vb_EK694 -na9UtVH7)r1D`rz%niPx$PA81#ad-^PY1;~e#D9MHMuzsz=N+<-!{=eLG@rP`N65f$Kw=7%iDEm#b~0 -0m9nZD5^PD%iwMvDGfUe3pWnliYT#oJe?K-XkVigYKHXqlu~G)*K;s~1E1D|AwGHH>Q%%_h5u6VGv+F*vjCu=&fdn!`r7RAGz7&3hO(@Z -n~o&PEv4e)-7f*>vDQ)r<%O4>=8yek--^XUb+<3F(+{!|{lfTN~#QaZ5@Nx|{RlW2i5j()L-bZBKUxo -ZJcan(x>d2P_u@mMDG;z353-t#8<=)^?;M;`f{#a~?G0*PC7e~gv|+pdc?y#Xy^2+^Cx7P$@6105%;3Fu4!kYhl}Tzhh5nbkO7@gc;-#q9*Rn^Nm* -uTfl}gh#aoh=(|7;=r^VZE-+Y$_0xo`!8YCyRG+cfP9!COlsAm;%zEQV=aAH@wwK_MOGMje!7Dk-W2& -GrXLvfd{Yaq!?BG8T<9Et%}D)1C|$ytNsfo(j|a*^e~`C|6*a@x-m`c2|4sko2D3Ify+nb8zd#+Ct_Q -_zp-H7YUFGO#nG@{?r|gE?2_`SjR?dFUt!pz;a40NBHmfsawikt26dO-x#$Cv4MUlCTMCc(4)u16y2~ -O=GSC(?xXxR;ilKQT$U0+RRJD{uLce92FLgaB>NAUY&Koc7^tClSxszPV}IyXI`l^)TU+bs6q~=nkE#Cid}wXDE8M%@4Hufa=nkVs{292Eu|?DL_ -u>V)``I3rd~Sm?vkea)Sr;&XXto+2oNAdjh$|gCQ@sYx2%fsssOMh&-hXy>X5wqpuZ2H7JdWE3zv2CX ->LNjX{2MI{_6714dPM8V>`ViC#k)z=D-?nP4HEV0W;gh)?LyorV%Zkn{F@9#QVn9~uBCNB~m20)?f%t -rNsnk`XVNQSfz88>Bxg3|t}DUfe39iCos@eryZMzuE6%=lS(-od!EO>~~@1r3a6eN$9hnY`0?qpAa;L7LJ7yjoL%yI0NaGvQ)$rU{YE&hCn`#70yLmJ!=8203zEww -*ufkyBAg#BMgv3_ryWijmA|r~0I_@d#5AWMV3qg#x6~Cj)O*b4^t?Q9KzUI}&vew2b%{D-){y7&1Myg -E7YA>5Fig#5lW+A%oVcu`=rVbn}aGR1WeOR>g;p&_uabeS<6$1 -m*OHF0eIbilLRi6ew5c)A}Tsxg0Y>s(iC{~?zk-f9=nY|9rtJ%A>y^t~=$%s@3i*f5!{K${wTamhx1m -s&MAFJj4ARuT9@(naSDKlFh8GMN<-i^wsg_rJA4?w2kNEr_s-Vh0nINaud>uU -`r#wbKH27#>#j@EZkz2rC=MiM;S|JGue`3n{n_l_l6KRt;XNRkR2we(MLPzOh!reHGO23Ez2v|ByYS* -?HU$<%mleuU|>)!7IFbB&^+8XcUNI%UE!W4SzONf< -&mq3h0A{}GBlRyui_q#VZj3u>)@Sv4mtN>jGG`MH==S5c4!EFxX~U`tkIe7}`pCK*T&GqO2RGSi6JMC -GhU4gmA2A_93VWL7N`zMw;&n$>4>V&L{dC03#Rfpnk!REJeJsnrNHI{ed>{a7JK@wQ=EIX_c^kui3Lf -?_n|{vn}pYQJ(PE;hBAu)Y1=XE@fWNIh?$_lR=1SC(9_Jja1@`nGX>jR|=4mO;@aD)dwO(1#im>jC0! -%ithom5ba8Kwmu1KU=?evFPq!=AR+J`Ae)g={Q`#?qr?f?dk|K7tgRKf{B;{@4Qw@+2vrX5{z8p90#B@gj#nmV>5y6f1{hdmYc1Rdg --g2;)gED8psj>l}lKD7WNHW%P@BE;AR@GV$;pGjzQMv6tk8zFg*NKL?^r8LVB1#V@E-5}pfx!<_gK>$Bqb1p%pzi|7UAr17#zV{&Gc_N)_aCsY9`VVbg -@V$@T(@_QhDTX%7Rgs=Hanm6z>L$VdrV10tWA!O6bwuC!?(70(yA=QiXa!zp_QCU(i90XEssSgkx$oD -A#GE2t^U}z_DoF)K6t5+JjX6;JU|3?D|^Q@@QF>j>-c5}>d3|{6wY8g@*1dV$@D=@kVLmC$26R7OitF -;qBw1iMKkvI%%q~ah&d`13qx0F-K_L&)5x}pgRECqI}EoQ&dxDKNIFkuE)2efi!XT4*Ky?^%B}c{UP! -P1Z+;`e&LXvGzu0!+qEojvWq~$Qu-*~D%OO5Z={TrG#IE$RXQ?+8Qw9Rw6mH9UPu2km39z#pPJ$tO@Q -@XQ&k3srpXrqo0*S01d`>5C&pPOh@ZcJnYUtp8cAyuu#14k&O)IDWjDF<1vZ=NhSNH!whZanZsKFlS& -6Md{1^LSEeKm5#c1$|Xhdxpq@$2Tn@rG2)S@ -YWy`v$Pk_J*^auHgW?yAo?H3H#xBz)A4o`aB=&(5!l?AizMDm5WtImb(kH{>$&mkCb*bwz49_mz>VsY -ZuxMQN3wDwHQ#HM?pk4_Kl>MP*4ibMbhGvwY|f)A0W1L!US$P3m=U!lXDU4H5Oz!*ZV+3GN4r{Ix3ts -;He0KaCl|I)gv@&MS#K%xvb{WpL{ORAIEKcWf(^%;=iWshtU^GVy -!Bo&(R&6^zkT5hMG%!$Rjgz -JC59(y$O>|NsPH?hh*6Lfx|cIt0L%Qr;%kRi6Nc}JhZ91gN%zM_93)lU5m0QVB!jCC+n4^WFn9sit@B -r1oBn|2@}&sF@@}0=8r?5a^){7(DMs`pMQGs`c?A#-zP6VL*43m``>@}yQ%v5U02Edda#)_rrM2bDchWp|f>YU=Oh -#pRq%WKCSX#TcZIkrBqvS=#a16L)G6ki$Jw^Stz@+YCJDPyAX369sP!oSdT9iNl`?FEy1oSdVWwuU1u -EjrWNRTKh`Cqk&S7lh<@KjC^x+ytCb^phe2}BVBjOAl2cD-q0lytp$ -W)!qnk=%%8!_6kh|03??VFll+i2BMZuRvJsUacO!3Sh5Q?>Wm)Q$0XBPKDMB8#$02xyB!~!^$1>e`U0m{+S -@$in!Y~7l21AMHtdxV1M5-Jt63L{>ABw3B)B!!$Av_YE7o!p8Ig^Mf`>P?A9q4Sa_cHm|HFCOqa+6^W -q1n+=M=};o1Hd_*zWYjPt3vDYO`h;uXyC=DAzW;DHbO|^uzQf6mCnkKo -KKNu$0OBH7fyULabb=BqxKw$~U!hQm%s9`r&AC`~-5#WyVloXgVzb{Pecq}jt^rBn5GmZpob*dX^254 -Mbz0nrW@AZxqai>HOrxw|@D4`!yy0G)Orx8s|Gp^Wmz28tyCA6fU0&HTO_Gam#pA!IvX|tUH%=;z>!>L>N%P&NMoJgup)cis;{|6?UgKHdJpL5B< -jctb{hoshm9b6VEf=!7*!@zDlt%+OSpZUOVTw&Y=F`t~lT@-;@YNiPeS;O(TPuRgOd%bc~78>EidpwPfm2ihN$tZE -oad`QiXsI-e*BGCHxth~9q&VOl~kmWC(2W#7$Ca=t{nwgsLF*qCOlIkB>w8y -32ECg`5%-k2J|Tanwg-gPTtT{!dJ&2bt{PlXEJ~Zt2G+<%Tud$hG^?&a(qc3`OT4Cgj08rE#eo+zR{M -*bCumP<944&Xq|)7TTa7tv`4>d^399LGN;|GI0~@gYe-e`lkS3=LQffjX$S~+4^@FL~mKF$i7D>!SQY -ptDSSa^?qk{_&h-;9i=;`(>~^lJOm-Ee!ICw{v<%gp^)UEU+Wsm62*~nCWAV~Wo^g@XnMmuwFU7zukrrEf~$?KK(8oOmT -a&ks(UlzK%V_k*~>ErIbj?3$nM77b)JLLyh*$$K!|datZH-?rl$G)$b(!ze77xcMuIB^L}$EE)u5K7 -}LQ4~Z<6RoN*OPlO7bkAheWwAh^%QtothLI;V_!_pmOM9alf*VBg4=)r3Bs9s_Z^}$ZBs|W8?KQVhCK -4(9ov}+~HP$%XPMU>#fKFBq3HckVpG1QUls3p6z*T4cAzL@QaOb&SHVs>|s&iv0^j87b%QO_wka~?9U -f$uqb6{xxQbmC6Y$bCDin&obfxliQ{^TvBh9_$H`B!Ccd7%SX@E?L*mouWtt%W!SEg|6G3;6h{B^Xm4 -%7poBDK9Z5y&=roXYT@53W->PzjT)OY#(vLF6f ->GH^@wx_QI3|I)}T@SuqF5H|ku60%?pCZl0UM^IOtkN4(5cy+8nN4`vt7FMYC}#+gfWlf&{!!mRkE?x -tHtjen_rOy(;@D*@hetOTFqm-Lp+JuB>stG3;!+o@3@G+y{}P;mo}EUE~`TH6-p=Z7XkCcKqtx2#AJ%`@0?C#IGeY8T&o$G3Xf;qn}03UKCi3 -e1Cw1o@FmQkW!KOy}OsFSP)l(j?+yfW@}T|d$(xFG>Emv^=EnCc!RZv#exDW#BL=v8;+ES##eBDCoF4 --+Pf>91W0pL_+o*xSk}81WRgw@l{G>Kau{8|F0TeWST1w|v2Z>!MKQQ5Vs4imteIOH>DCG%yNRWt>nKb-qS<)75@+C17NjAr}yShk1;Mh|Bs4R$ua?74 -*(Gvw!y7NNGrhev2=IGCh#mh^+6n%sxOh+dAvRN}>;yd#yb%aQ|_Jkq)H{OLw$8g$tVYl<*l -GCn(5^|GHKzezp`s*aVJ&^Px{m@j~EhkiT_%d6=^$Ny(oS+4J89iJtTpCI6LCHEO#YdjVD;9aT#Orz! -ynqJQAm2&p0Ym*ol$#t7XeZeC*JyqXP<7TfeBIQ`35wx+p0xc?{U}IWmP@y|T@SYK4U2_Nr4Hn@lQkS -H0XwlWtiZMz;L!d^o1*oCa*+@A3)P0?boA-VHEQgDPkY-mjSjcXQ@p3*{MoZrU0&n=|JaLLy4>eY+pf -y~VpaZp`CsjuK$kKUr{I?wR(^p#&4T)Msrd!{AX30^Qr?B-HIrW>%cSp|vDHQ^Q#B3IvFAQc-fl7T3r -1D&>3v&-qFtP8fkLBs1`-m#Wp=S+mPEXQInvWv_VME4FCQ;2pMJa~cBJ#RB0Dk&gV#mV7P@CUm)AJGR -^KF5xtgaB2ov9`_FT89Q!>nN>{-?wcY;&Zxi9mVUV(h}%G_tJZ=+Tz6Yxfq1P|s|Et$ZVJf`hCJ)E%X -JO*7`-lh%`7GR6+2qf_nGYF}xm?@JGSri53^qNF9osh%hjLi2fB?f8C5mwaxsWwj4VT?&c -YgH8&dx-ZL~M*bePzlAekw-Xv=-5C!nKlCOzeA1$zvjW$2NkUOjx|ndnXwvdn%O^zgJEQVOVJ&~$kQM -t;nTPq>A!l9Sb{(6vC&M|0(0LHNwZO~>%086=cV2OeH>iihTNLqN&TE;440BxD9kcd|SfkcwbiO}DOx -qc;Bj`K%?(!#t(hfvL(m95cFb@Sey`*e5wa$En_iZNs@zn?FSR}K}N`Tl)?k(cExR1<}t7?RQudm=As -K5HTe;PYK8nAiw$mJ(9Lo-iw6L+K3CFfOR8!xV#Zd{yAKR5z#5~P%BCGzQe6iSTRadGwh6LrbO2$4~lRei8#B$BrA) -_%QlW?$043Xrl~t0a&sZs67z*6?1TIqH-Q~$m2^#vC;5@&@{ar9&!uGxq&_j{@l+(c|1(@6aWAK2mly|Wk_));YbA_0020C000sI003}la4&LYaW8UZabI&~bS`jt%{^DU^s@a;4 -t34YE=ABgSv0F=(Y}LuX9i{4BIO}$QD0n1 -6$F5!q_)u1z{rhFD_;2`6-nTix-YwsVAyT>)LK9ehVx4lk6 -0H^RDh>QJ!@pSx${8#21z7yXxtPu)b~kui9f%F2#49e5u-_`t7l*%P*>qN0|ilb)j~8GWJi!fB&&LR8 -4bvCLK^n+nxW|)!a)R54d3HDE_oo!FLXdU?=h4yy7JrJZTL40rnX{?`6{uU0}Tfoy}=pWu*>vapqmZP -dxz4ar+fe=2A!)8wedXcqy8;+ZT29cS)@m0o-f{QTN;kL<*QITP7d_P?j^=6T)}dpL~?Xtu5krG&G5HE*B(3OHWezu^_j4IJRAsro#hD_L*gh1m`KnB~v -%tYDl^kHPnNXyN;R{Br<=O+z4^1m4g%u!iO@MGdDf(c#lD$|vadC^m3x;SWHM1MJaV#ky^J;1)`+WQp -8lKZcCo`cPaTO7_^m!hrWno~Z+=qO}4E^gf__kGhBNv*K_7#xsY%7ZEh_0v~u7cQV`YWKyWLaNPc#Ja -prIWxV5_&hFH`xKlH6C+668{=8TU`UAUsdVX9i81HtOw8e|Za1Jw%#Qt%o{<%P)v!$42e{HK~&SMTFi -9F}s6Y!maB*H+I2=wOW#=KLaJ@%sCNpadqS&J71SZ`MR;Q&)D;5;JtRd^Drb;is82pu|SO0_Cj -v+!N=2JZ!t7#AZq^vWY>rvXNcU!xy3kl!($g!=I{thZxgK#MQ#GMNyU&;<* -5%fqW^pfG<7MQ+upS(|uRG(6AD~0DF1Dy`{09&Z2A)8jvHfp`wF>2tpoaR+_Qw91ztt;Hm-X^de#LAV -;tzaxx`mWs9hRE}b{cV*{ILVIU9pHjwt|rKo7vW`G31&q_3UqJWmuu3GP~nE_EsRdv{UKrm8)T|c4ypfi68;V-BW-koTkPfQOpJojI7WW3oumw0FqzMHS1b_A@EdCb*R1~Qi;s)hh=Rktt|G?a&;B8?=@Czb)T1kd -8w4A4?loX(^yx$*c!oDm?;~@AMjWe7JId%;N-rXyj}poNzS3L8j3rZr&eU-HR-J`%noLVH)!|8M7t>i&Jn^)u -xhV4hOyM2wtICE)elEH7ba8O+(Xjxo5d>FWvzUY=Am}I0KeH&>a3BB&v^v%#1UVckk7QFCti@X0jdw! -ubOl%GmcAsOYrH$SB|A_lZ%|_0uvE&j1Y2;3{HMr$di|LSXJ0HyJi1Ix^>8&Btm;u=ZiY7K=~+{&?j% -xfNf%|KXc2?{|HFQ1@@&ZejP2PiQi|J)954Tj1|+LD2<4|Ms)bKKrHZcYyUfcwjvJL_84PKSUP=2bQP -37|mFy9f~;=Jis-#b^8PzNG!_IQ|qul>h8H=jq{o1!RH)FMrHkv{cPc@dOXbx;N@X+e>2PyXtHJuYzT -^QVkEEsA|wa|cIEcPa0Zelm)k9T-n8?@WK-BYFwS#LI{TtugDnFFHCdcX9gLprI)NKOUpSh8hvG-gd_1xgp}4G$|0XY_x7I{ommJ4UCWl|Iv*m}l;yB((r;xh{Ze^k7<;=4>i6 -L|-a@Zqyoo-?Wgy=N=4zVjr2ty#RI#5?z6bU9V8%0!=kL?P|f+0y6~Q2Hs3hUDfvzblx@-?>7SHiCWW -{)X(81>1^(3f~J9rVdla6fCdUK0rf_=J$yc(76~%E0Y>yJx4HThhdwYHDvgJfN8M6@>nzgUPpJ95NlhI&gh%1>>FXxSdI;k~#S1DffH%x(g9? -z3FWZ$QM^zx^hf51|a?zZ_xr!w)~)`*eo;H9i~yl>e?S4ghgqG<~&ZX$Woi#o2)eL<0Qa38Yhr$UMmz -9)pVQ8X#dZ7YBRX(ikvgg@$p}IKZqC4+IEk2P^3Sh7#zTGrRY*%wMZF=Lc_PenVqVKNx=OzYdu&v4eF)Z9Dz10^$_mD0lbzjlEa2DQHA1sUCW{YyL;S%#Ui}mKZ5nT0u1-s{p>d#1S+(1KENh`Z7 -ImHx?1rI(UNmLSd(TP4~B*KudSej2KMg&?Z!XyV5O$ZnIa;~7e(V!`w%#t=i@ek3Ks=3{xa}MI?UNNr -%zEL)$*IIe&-9H?iJ%)QwNf)SpCZLZ{+PJcV35nQlEOA$kDv^P5nFvjdSevMA}1w!;w+8;Qy;;e10?S -miF~-y=f>3G1LGX4im-|J$eIxVb%5M=6$G({Zm=sm-o~IPij`TC)v#xBPD)j+@zmi>T?~-Sh&+f;E#* -TzTH=^^5@PU4m8Xeh8%pomlhE$em?uJJEdJ2|0(WD*U2(h3v^KbYBt)8JHf7%JSWXQ&odMESc+OUIKC -ohHcxQj;0~=cZww9=45YV2bq-lCS|13M6jom~eOHzqF&9b{o>Lgaqp?~>>S_R_Se_H7SlQx|L3-IKg=Ya -mzlS!F2WDkr~S?H~?w%DD_oUvLoCTlgO1b2QTE|^jFF;P4as3qQ7l>?Xe?uwb~zQWDfKVdFv|N6AIqC -fG~Ur4LvL@F|bg8n%Hb)he{m)baYnh%^KY3p3DVwsddrdK49!v8UOjGM_?M>;Nj+0ig-$aQ(B|Di#%< -g9LhovuIJXAI~T;zos>A>zv8d#A%@*&Y(*7hBAEx5jx$de(HzfxPj)|8|J^ef{`0^M!cocWRCEofIrE -$MsAHhQgK;L^N8ELSLjVLyoZDj;(U_g*Mm8^1*EYD?o8@S>lObh)>NJ5xfJqKFJ`RV}?PK*3M>~l&40 -V&S!V>M~=_!Hc67AqB9=;|L?iz6xZ>_hOg&@eTf+OqF(CKYUo?=+)rYp7-D0h1%)lC!Gbfy@`uS?Okf -$vDgr|-`9qy3aWf`kV$BF3PXjW}|dI8MzUnX@Lu@8p@RyNh6viFj=QkMn?28#|JYnb#k`0IWx8(QOE- -LxUbMWQNe^a(nqBv<8{(A5;D9)uZCS>LAx6K=#FAj3Bm;N2w-bI~kwZ7wu7p1uR)@!5VB(fQz|d-=GH -EsU;ET!(F!+3wC@!xO?K*T?+SIEW)iql6TvUmvf8!+nwL2#Yj#1gkwGWEHVODJDE?&{fj4z|B;lm>N? -?0{aT1Gvu(B%IY^sKtbQHUFdaQh{L4e+6sIO@48>0_2%Nje(#->s9<%~f6{9B;pYiF`SXAn`ClTJs8O -krxPK*#_ACV5E%|MsXJ0AfA>Oy;Ldf92shIB=Q(RePS@suBAQ1f#boun2DawrltEQhv`Y%9?W4vY($p{6yj|I+Rv54bWYksACI)dK!LH0eXeL~N=Z&3`~OsD^i*1U -{iAhp5o=y;ycQgQhaahCX&3=G8`X3Sh}#pgxSHs~hB^P`O;&DU0>U%Lc>FQ1C_b0;?uQ0tvsKLecugC -5VywJ3cK5sJIegdBl#Y^j9G6o44xb?kZhH1v7AR`p{n59T*USI6e}p=%L=JdxXxjuMQ!3h -xvXtLnI$R9AvI}Cpw_O*aK04J3Bm%Al*_=~IplE9N3Ta(WUELDXLR&bq(ekYj_eLTarH?v%5_&!*kwK -&q$>u(ZHz;dCKq}(KA5kP<2S(wn8`!_`p8%$$>Rd}&dZLDU}oglb7&JcHe^4O=N?JXoI1D3GRg|>!); -8mgXH!lc*3=S%~^g0{0r=0z$R_BF=OX9-?jz^?0V~s@9(Zd9mqrIfPIkqOfDMl#SwMLw%JtOeiVAYiV -=*Eeo3*${OqbZjyze}FgI^j?uSsakPCia_4&{@$o8{|!4vt0_V4ew10oMkMkaLT0cN|}(*g2t$o@c<{ -}y#u3UPB%LEOo9%gK9w_;bY5E`@`J(t~-Ty%S3&Z5Q)?`}VM^<%{(EmE<2R@V`5OrhkPAHCWu>hVO6b -{fe{|ROR+6;k@2poW(|C&7h`|P3{x?-K&;Af!m} -fvvw%?#9@1%X1?@+rtr~u9Qm$dJ7r_ -4q<#3Pm9S3TKG^d~jj|AQab_O~JGsgx`tSB$YwIi=igUA#aBSsn%_732_<1UR>cS4Z5JoggQEF@-AD9 ->k7;y<5n8lW2qNz9+&?y-X)gJmdaIFMBTa;nHYJ4?_%!Vb;j0D%xULrjWJ}PgG0{M#%&Wj>CS5>FiSL -=tLN%4q(VfK=;I>kb21M{S|x2(x$M+F)@2*{TLzCZRc$5Io -f(Pf6XX0HmI3d|TOTvaHfMG>WF2*$auGIAsVtwp7j>0;XLy{ew7Mhu}3gdeYB8F%s9(gS2J -9e5MW2d@$mP6qX=GuaSPFv06`k~*h1>W9BE(TE;`8*%mPvF9@(f-YZ@M>I}oF>@h%$Y_dZJ -S5Y_-J4iM~XnwHqU5lDRuYGPjJB{hLj;J=JK!k87>^+nN^uUo!r3)hBv^(Rdr$QORrU>Y_UI5ltr}&5 -%I)?d}i40V0=nO%D>~ItlKcAQnYqiO>y%);d;yYkCkf+fd9g1i!M?Uc*#_xQ9^!_1iHody- -U&I%HPGeTKK25vkyR+*^NZREpZb36?BQUC7>54btH^rI2MVwviv! -6#Z?pQ2_^%@E1cvKXcN9X2KnS^V9hTPcQfw+%Ri=K%>>j~ezPb0^}s&7{%oV$1g44=`ak6aAF#A1cq= -^5R^Aro%h2%JZ%{8lY-mA@U)K!_2^0D+_*a3MUwnv0TbEHz7^^pn#R1kiM0_jGg0k|qkXsWa)G-|x;k -kpN~(=?+>fgZ48Ht(k|*0m5%JDf$?XqwZD@Y7I}cYd1AkLjx3&g62gkDs<~~VH?2h7FU?uv*PC(OvQl -B6PB{iCwSXb1S({odKoGwRdMGg>2R!MmQGpUJxLBsp+GP(vq7)l;~%ex -HHY$=p%03Ca`dvPu*_C9@bB_!$FfvP_8Mk#7Fz5sUB?h-?SMV$(xOefG1AZt*^$zc9ViHQQ@YXX!z3D -1W}7=8G>t3#+b5avICIM`|^P+)F#=6&fgiibxpt~EDIR3LSK-xVI8&ku3%}_1bh=t=xW{5S+_egEmye -EDv_`gowg=s_=AuPnnA=zMBOi-nN2rw*CkVYCIBoWVP=;y<1=!2l}#7bH(V@T>?GY7QR>Dg9?QYNV?; -@v$_9xRAuHGj40D2Bh%7oRE;SD)j<`1o#YWHQ?x0wI?`VHFuu?*g7u?}Ql(9!8iiHvA&n-8OvkeP@l! -oEDn=qu1;%ZU;raQ1FVENRC#LgtJNzM^J*zbbKj}9lHf#T7q0jt6r8*ngBkSXa%OG+q_Tfh1B|B-yJEth1J0cdNP` -uxm(4?q}a^>#3nwrIzR@R?*t^wl*YQ%8UrK;S@02Z(#vgjxpsC=o*TJ8^mw^Vhfxmzl0_o*A!!Z8@vj -#*x8gUy!JVRr%30wxR`wIHf?qlh5J1~r@;X;)q-{)bkv%yrOH5>Gi?(KSg>=d&Sstj3o)d8ezj} -cO~3^cEedzGk|b;vXL8LYsT#g389VNa$HBoub);XPn85DLvnCvRb~wb-IHk_qF=iaD_wGz9>HE;>UL( -*e-VjU_2ZWy_frMU`@>O{WIh=2b33Mrw_)EwFEGGXFV*o+wOOozQ -JF4|c4zUz7iaKV8^DO02V9^)7XfQqe4OR=Ymz>N&6FK!PT})#U<&n0#NGFuuV5D+o&>9k{nBS>I#?ck -{X95g5?}4Y70TC)K$&aA2^B)LBmJ_ftDusvfo+Qqxlj?wEXli>XVvGsc%mAlmg)vR8)8`yW@m&q-uc_ -U~&J=urg%(>d^RhIkk+L2tb?${SQ79KBw;!#I!q7e!&ruuRA`xQ>-AF}Ednm~|Y1v^^GGDa!>-#2k4{%>ZSq+8+wKxr_X|#0Mt5Ww)-u7n}A+1Ob!A+PlybKIEA?b -!4kwC1VWlH=Of-GR%{CqxK%hoX|Ks_6v_quUn_0urMjP&>qoKG`)0d*US?)^l|&fRwM3dcp(`#jZEd+ -_GH6n94n!CUSV#*(wPE1I=INU^-LLcu{_^s=!LGKBCe-eu=w_veSk(&x2L=EgpMYUp~0k@0a2NlW1vU -=SfD;*r^S%R_oPPP(S3;GPoIAffBeJmp(BUVa1iXPuPe=>n(H-+A|E=6E^|m4;47rO4z!?Ois7CzO_K ->A!)>^nd4niK#jUSQO3DC<#W9966Tr+}hX{dg69)5l1qXC5QN9+%L$VhOmE@to?^s{#uUpMB&wY`bKDP_AIo#3{swpCNq` -9(JhJ`n&gbG~PIV$Qe1Tt@7x&xLxn4(g6;*@*EL$m%AbnP4cEULW|5468IyEFT0GD9;tx3Un5TP$%G3 -eAx~aa(9@h2deO)tCTU&@eUd;ou#aRso)x4%smlAo9YX6asT%1TVxP~Akw%1Z(OCUS{(;7gg*g_OX6U -vdmUxNrSKM{=flcwOT^v-mv?(9 -i@Cm~*T|4OC$QYSvx~sYV(7p3%sX5a_N*rjjtkb_9k$wH~<9E}!sQ84HKWWF}P_SdXk%5=rn3faHNzj -d1sl^b?l{=7b2aAfojH`FAk7@ibEg5&huA2lbnm2UAeX`%kwSkL`CZAo2f&ahZ1iXn0?~xYS-$5GayM -~4nMxJ@?ujd=Z({{;3923-1Fj+FJ3FU7R`Vl@qEx^HR3Hs`EXX52_Q@obqrb35_A?7=M-YOjr5J5sH` -?d85tKs7Q2$cElXaAABGwjOTf(S;^8LH{cn2z=6KHj~Frcm>`De{7Ub{ZUmd37qBgVvv#4u{VuCGKJm -e12lyT%_wObLTD$e(eFfltX8pX$9nBDu#KYDH%b2bef|RglPKo{j^vcFFf5gFgLD9rP9r?!e045(QtK -wLC{PK3ZLr}vp4xcZfo=!I>kmlq~U(MxjH_<{~}ZgFWj3Jhp?oxTk(CxoBcG{Ym#UZ#Xsf_Y44iMcw6 -_@=+Whr<1SKUP!)c)}IX@I}V7s{b%kJ}xZH+|cVV}!SYf1pB8kY4V`M$goR*cWlBBg$414fO_kH=KJ4f?`m7k9 -Vx8SAHH^`uVq_2F~Y6t)g>MAuT8^j)!VZori`gK_I~4j1|vfeO}`)Mc|!&AbGo|s;A#Q -ql;_@HpRE*KV$lpt}3;W>*R$HADzO|%KTy!t;t-mfWProV8EzowvwlyM|Qy2YD?>Xx7RuFVw4UMX-v^ -|j7vAGzyapzv6N_!ms|^3ball_2&i0@r24@D+j@@-7#_!~fv2yA>UiX(&e=ffKqo3Dw}e&m<@4=>VXF{7=VIl7}RMRJ1630u{E%1E0|&+By#KV~$2mYY&CccF7YzCId|u;@zJR9GfQf5~FmN*-CmEu2AsTpTzgBR@k`4W{mu|-gGW}W -~)GLw6k|X6?t}Mp@HCJC4iYgQ4e0Ojx@J_@bHPT`?<)ECWH~%UpUj*^Fp3h`{h}+Lh+q-5e+6f~bV|* -pOXqVUbf4;WJ1zMA+m{#-fnf@^t=l4MW2T)4`1QY-O00;mWhh<0wM#OMX2mk=_8UO$o0001RX>c!cWp -OWZWpQ71ZfS0FbYX04E^v9RS=(;pxDkEtuOK`yBw=MamzRYDBM9~aED+2tW;U4z&p>8NR5#ZWsgRW0o -gn|7DpHgvi9McKL@#cMWU*Lv>QoioDA|#+Yz8wZR+fZ!y;O$Ky_W4@Sn1w(yYYJ~4+ky|{#9!K%doC* -c*}OX1|{~qFDlvf)Od-5X1eUp6|IgeCm()#`~A-!-si-=m$;_Xa!?hU*Rz+3^@dBKy^MbdF0`R-OY!p -$N=Y?O9}I7&nW{62!9G+!<%H|9l@)DsqL`o^(>W1Rb+qOGW+kY2TnQ=0Q+i=s)JzzVaoFv4IK~vY_og -Tg%>3PcOUn|amt_Xis+MX!EBp)W@}8Mg?tf#I$?kRpo~x^?zY)#Aj158C5!6K!BgvkDs-1jhHEd4Ik& -$6>bX-BL48{I6aF`1U79h`lt62js<${~COf_qpoYY+VUGc$>h1=~OHAu7NP^nl04vP==<_H7-`S0>8? -f{?h&vHQ1XI9o6P4-p_w%W)Zvh=DveA>+?=$Y!cF{~cXfRlnV_|xL^jsbiH`Be>!@WvYKz@M40Yry{m -HYO)Vr~)H-G9Ium{FS}vXdNv%!^47i-(~J!*~*ehJtkU|Vzi0d(d}M>L8rwgKN5Cp*XW<#rq8%vX*`<+c3s?dKifKMuKyHM|EPH8oSF;sdG&0@zm1Z%{pSPbJG=Wr(kv=Yej}U=Zq -dE^wBG!^R}S?HB1lXJtmUmoGga7+>1|09}z#t%(x7%$j;Crq{%NzRt!qQ^n5F_zmg;;DQJ|FL?l_ZK2 -(?mwhDxsCvO3jHek?gpu8(E@g;U0%^W) -%ZKrLj#)MCpS}EB-^lf^>OFFPoM+4dBp~w@w#BAax6)maU5O70T9@yX8YlgChxqPYy|>a&LuSv!brGL -Ih#(VpncE8n%cu>Y((vheA4{F(F5{HVwH!NWc83Y^AXo_HXr*JVxpo+vQa;^ -UYO_R1+C$Q50F(#q3B+a_O#Qn -h?1!*<3Zv&ClYtDRL_m>BLrzIM+CSsby1?L-L5+%Eco{_+-dzO9>hhd+{dVsMzr1J%%OUYkftoC+#kT -inYhshHj?XLBC1-iVJOBmk>vGhBbHO>So>h+V|knW4h5;^^R}I&w_k_ib|gJ_WlEFUZ6yyEu%S^SX>4 -H|HekQ2Hzf;E(rPc2;joo1F0}A*$D9mG)HPcZ(S)4Q1y4WfhdQUpyJTnup6i&p6G#%H$})GsExh#(x? -mWaiXdY1bKZzyo`6b_8ua{PS9F7<2?M~(-J@4nD#GZEhG(=CLcdy<#n56QFn%&daNl4oOL0=Be5(1Xp -89?Q1iaFES5cXihd9@Q~Ly(OIW+Qn0N#8t;6kg3`st!k0Nn0`tzTkoSDa=dEigQ%aTb)L^hbmX>DKA2 -v@8CZ2yLC!Jc_P>Hsu~;}$gb$M`{KYlqnVupYMyKXVllP7Nk{Se_^99xc7fE9!i)a$dX3io0`}WEEu# -oB6WGfrEVGRm?zzO4cmB8ccKZ?bWKFcmHM}|HSNK$!n`vPlJv$j)~I+On?_p3WCkK(b@dAhIC<0E=q?02KfL0B~t=FLiWjY;!MPUukY>bYEXCaCz-LYm?hHlHc_!uruc)Wo -tN(B%7_-D2`)~b5rNoE|0xVVtZ+dgd}DtQsHAXO6`xoegGgqQkto3uIfIVwl*V?K%>#E8Q@Wzcq5Ce#Avy%iB6jXZndO%e>G(Qu8e<;wY1& -L$Q%!8Qn+`iK>~ciloV;$fK1^gqIbIMVc=>v00|^Qf!JQOGLGdl42w3WeOO~v3pZN<9dm}O1WsVs3ZX -A?N9_pM3t!OBve23+tT1(RQlVxsM6bYR4?^|YT49jW`5OCZN4|Nby>u+GM%ff`C3QiJk87izo%>Z$)s -GT>p?SSi29_+pnTYm#m>1z9D`rtPA!wTt!6h=2@Mjg3*U%H^J -Q2B7Wu#|Jp3MwfQO*@IblzI -(n7cRep}1x@)a`9Rb4{Y%U2_rRdTSgX)y{zSU?b@srcZPtGblZn+PvSt92G_Lu4!bJp+o7@6k4dL -KUQ}Ji*gm!utL8R)U*my#W6ErhCVcCP~GG(>Vn$iT7)E|d0lmy(uiRuqXbu-KnZUdRqi -9FOODuNfQy=z^RkpEh803t$U3ZLxk~dWGq~eJkex-RTa$yF5Yu3zCI=z1ej%rtpD_};mB0?5I9j7hOF -%x7qN%Y%DUp-Hx{P9p3qZ)pyh6>_pWTx;Pyg%k9~a@pPj4??UR}O=d4do7WXU=n{BhE}M3 -k-iF%L?Sq`!pJjQ2EFbSi{mp0W^O>&;2k&l7-=TH2VIRGiw0o;d=p)BAESthFApk?00WG`stU!Mz_Cn -8~ry^f8mw$brk`OUoKzwfWQ(^v&}Mt7H^-t860MzU@Z(xXxyh7b|Ct6MtUSVlYAsPBRvw%1jnNx;sf{ -|U>D8^B%Os;2`dm$h(nPVzeV(T5bUFA2fq=kYjH_HNINrBUezFxvu5FY2wWL5uu*vdw)7@SNkD--EiQ --RRi14HNVIspt(QeEz6ic}WTS}-bT2#|D%}%J0=@^&Q6lFGk+3MkGD<7yqppS@+zky6@b5%iXfyV9y9 -UGYFe(>dN*+GE-r%<}k8b9TA5gwcnS2o*rc!;O)V}ie!9GR8$gr(1fY{{&){rSUtJNJXgFls1uTsW$Q=@YnX+FL@DS&=_TV`wq_vLphlmIO -HSV*hB5mt~_YuIXZ$6xS8iS<-Ne#gcr+F9#>n%#@QS6O7@@20<*cOB-o~eM50-Xx)5KeN7d*%ve$ -l97i*Z%E&3SIcKXH1$;m`)A~0O@^j0R*uTD-)-c1<yp|2{F?hBD4PJ%I8GtbtQ7M6zQF2s({ -s3>U00&v-dd6d5k`!9kg(5ndJcP2Eh$t7vR|1&)mAT^?hGy@>O -L@bN7oHrS~K*u7@8)+m6SQofwRT&vNM5e{rh#I&?#7zlaLJeIJx|mvi?Ia-++QMiQKk!YRs^mSQH$WNB~XXnuVqQzwCR -ftEltq!JIju78@m#2`-L6F^h1hXFAk>~iJK?`>Y{i=VgYY+#X+Eo$g@SS7GnoownD_4L{k$Ynkt2*Fb -jeGz;J5)fsR}ZI>grxApC1F8f!496_h(e91B`TOsC?^7IuP5FcJ{mgjEvgG2-EERm+vk;W^;zkI5y2n -Z;;vhConL0|Mw3HCerxW!j)W)dsusc2TkzD2MBKLp5yaRwhE)zkk_yE -<5_)x%Rco$A76XzLOxFyV0r{f1g<73YEjH#4WS1_(*>wMST2M_Y7d(5;=*e!#(tFGlm$Yb(0|WhtU4a -+1CYYLbycm1io`zwnUlJlb5$?Rab~FOxHBG=#isSacX{cAVMdw&wf6(NzwtPr~|FuIKYiW?TkEVEvXhn7`Ivj99iiF^}z4U)HOvB?kRvihvKHNP>)xZ -AG*OSEj7U?6Aj^qU+8t(KO`(JxwsKMU^7m9Iy%zkOHuyQ5i4&()*@P>r8(8;h*1pdiX}(q+~8WCXdcR -KnOGR6+7JD_I`~r55hOw3jgu>H_?ZGe*5X+<8Q~0ubP?8pD-NsOM}a?*3C{r%g0}6T@@Kf_kydIn-}O -X%)Bv1X7hEA((`qIz9Z-$<+^xHJwN)JiH7lI7%!Xrnuh{M5`=V`7QwTN=TF|fcpHBI?)meJH{sRgFBk -1n{G7@xfxt?}AOJwzB&s7$A4Y`#QI=i<)n~r~i;4zZ=uBL^dLFoj0lA<>n2-QgBfce^5feveA`4S9SL -e*2^|g7jCDf>ynqw~uXmh^pnq{o7y<@P%`uoR^Eq}ucRMpf4!vvpRJC6v?95|1lh;Qr%2x<&|_&~_TA -A+Xr!LV?$qeo5xz@LayOKsV>?Cl8cteykvN(s29=+{Y@67{OCj%pI=iHBLhB}n4LDE6kRZK04kYl_jfF6vuotw)C+2~odLPe+K_I5pF8cH2+*cdFJ -|NxmimAwpkta}kcQK-mufQ+GmMBLd^Mj7%pU{23O#YmX;D0dORab00(Su87(rX@ID*TZaOkH}((M!(7 -&iWJ^yT>{4p_`ve6C-gAARXWdyMGxP(|hBw#DSE?60o%hfu= -Ai1UlgP0X%2@^}$V%UHK>aYYkoB$QUYD`@c`-qpO5H=&FOD*w>?c=EM3>#VGvXh+jYGfzPFgw)Fe{>f -z&~g}=c04D$sSw~Sl!ZdOmUgbS6AEC;(RfI-*W6)9%RMp?RY^^{nSHgNGZqX+mDl0bv*%xPq_L=&o&8|87rlK#7g*)JW%jJ7MEqCBPP;AtCP^4% -oA`!Xtc>5IDnA!q0+dWWf(-Uv#Mb?%hiElw()zjbN2iHlqpg#1cX|l3V@4%wdX1AYeGPSJ6mCv&a}Sj -tV{=S!_os(pm%#2*q&RDhe3(ZW=3bymld@zv%?&@YN@CBSmUgHq!0#L39Ta>7g8=3AsXVLIFPzcXgCn -G0_B1>e8CQR0dzdVj-KO4b)rNKeRvCJU!>^nN!v57d=iZiv+v~53~h6`pXxeYPpvAGyL+U0oHdCwi%{ -KjB-5VJqZ!@Zi`c*IFwEhR$bbqMkhR=Apt-76w}1ZJQtC5h7D!5bm6rxfPx~WU=UAmYya7hN(IrF|zp -v`Z%nYlu&!3UxFlJOP8uSe)tpQ_Tt`RvEWd=80L!{`lVGS{mqLaxyZf3Y+u6;bC(;S2h~3yJ0aCPSo}gz++m&lYho99P)i|bZz&%DGYCHJJ9ud@I -_L$yoKe>b&i5)fj^AB;_Y -1Ic93ieBS37SMY%~)YhW0!r1358D|;1g>cLsdrbDWqW%;`{TJ=#Y -i2`#tbhnj<`Vaevn-tUnn43S^Y667J4ntx}(n;H7km1?5GCNK93U7=RohnQwo+2zaVfFJ76j74Ag#-P -(lF)+R&R?(HOlLy+8o_%-A2&o@PNn)2<|VXbCvr5S{$mRJG=Nt~)3z-X)nA5P!H198RjjD#uSI7jchJ -1)2?&kNO>HB;Wz2sq>`c*I5al?uN>JdMPA~hwMV=L1{hd0gbX7&OR7-VQO4WvT2H?D#yhf@-Vq-x941 -rUGH+gbGh5MR4;9x+V##|wef@Gy>3M6NKef$AaA;*uIC*%M%^OUcazSk% -PYF*E^-Pem~h9iMm+>j{faLY{%$Nff18ftFxe)Y|;eqx*c;$1|BSBx>(lmgwce!d>P@MHkekuU#iE1& -RD2q@>i5|DdIW{e|UfS^6D3VZX4QOhDTL8&f~n!hF;JxMK$yaIH!qS%NuiPEj|ypR(N1Pc-bjbrVk+y -5aNLGM2{Q1y?phQCo&P<>Dd=wdONpwi7y9b$K=@6q9dA9;h#*z$*{0F9n`a!{al)&L*NtKCUGB~_8*- -YmZOcKa?hH~6ILy>bB2{uM2rM8negjX_~zNGmoI)E_bK09cc%pc%8E)lFNTO_>IDaIrD6PTS;ULWmls -%@efAZj=)8RT>czVssdW2qCvGAI%LU@{!|5mJrM{nh(pdu)o`uZ{%Rj85ET!0V_(t#536(Uz!CXA;xGjv15ybt#v*7Y%TO%pdkqFIWs6BD>8~@|riw_ibZIQgSwb?4KIYddd%!l?T-eFHuR49DfBPGx7v -xf9}tYg#!t3YzrmY+5F&jDuRSX#L-5!0DiZwEtCq8uyv1lM}C(I)B^CKhM>*2q*#x0r6-K%nqI -vo)AJz`_tU~417sMBMerSyxGq9 -sQ>Pa##^HiTX3Vun~&yiaQ2Bo5l4Ltknm&_w=NUZi8r@f6#=o#REm^AUnY%3I>nchemtP4bwf$i}-w| -7OHl3r748t93*5fW~Z6ZX8jO^%GKq_7yR*AGK4#<}m-QIG!KC4mMd(1-4LSC(+)E+$y{);5M;REXVeg ->|pFwKats-aswq8SZ)a@jet!+Qdfht;H?fA^ZMurvv -cV9LT{eJkN`d`H}SY}wF!-J*B;@fY<>0bx?P`NR6L~eC72tKfd*H#+y9c2epA6y=jGC3wi@_4YFJ*X} -Sug?@sU4~RqZaJ{f(cXA|F}d$+x -3gzl~&x-22Yz=T2iS6uRMq6Dn#EqihHJO5E7oTyALS$aM}>0+xy|vD^xHLbaz{mt8-kNsKjiG8{v3Qq -nuK{k^?l1YgfpSRF6l8F6N5BSlT-=NVOr+cwin684r}ZsXo>mbo>xfZhb!lkFVA8TrtZx)3e|!;22q9 ->Z_6p}7@Y_U-3x1&zNdfz;=sUtQ=!k?{JU-tI1gPXs8bMhha1!qjtgc7JelqhypFeGL#Vc^AqYDi52S -YpZUJT~LWbh=A3kN(_bHTSHU;x^TJJU<27f57iWSOISvG4r*slzS=HPW%Buy+SX|)4_U#(@F&QmIV04 -oE&fvVXWc6j!Qx&84jITfan0=ZTP5{MZeRJ>t-6J=>a_58c{Pc30?nUjpzaGzns2U&918QtSZvZ4vpn -v!S0NbOuo|G(u{9Yl1`D31SsZU0x-7C)A3cS9lT44M6DtFu)J?M78*qswMjXWJiDdlnb#El#MI{pw>p -%UyAQ}QTZ4km88s|_i8((;`JNURqn9=I -WyCaD|DT+A!V_`$*=;a>`8j#4{kilFAF_;8Xrvk#74y9W=8@lmOajZE(pxss9218bv9Bm%t81G5w7OC -%c^W19+de!YZSCdwu`r4vn*+MJ#H#n{<&W0XtTa_S0SJB{WxubmVgKllr&F?42?a^Y0BHCeuiK(yc#O -_bM9(VOFi|Ab^;X3FJ4flncyQYf)p#8uuMf0#I`4~^Ac>Qg7`oXPW1g5E?IYgYn7zbnSe9$y`R)G7fN}wN9Mz)V|70~XD|K2&Q=f!Ot6xaF`ggRT-v66QxkAcvLPh0k -GYq~2KbMe5vG(J#hHuc((be(zU#u^`ZVBP5L)-{T8i5|UQdj8|{kMAF0npYga_u%39fb^ae{0m;1FCY -I2{~15-op3C@t1ZaJEi|MF{p&ruDC2VlM5zVFf{t$@-}wp=JP0#UFO3Q{_bbr=>JR}n88P1|0}GT4)K -~H&#(bm`cYx<}NxLoTAgQihD+~w-x%4F}BWoCiU3u{FA^uPBy&OCU29btLHri?f$h}<1k5|bA+>c_T? -i1nyMm%Ic#qGLbC#uLp#sqhlryg<54j&EAtAIp7Pddzh9rkMR9j%&4#K@O -&t*x!6z3dr^@qOSz^hoEDeW`2t~Jvu4Q;WkmlH#%2rm_wbt!MsqN&jDEer57XuGQf_gh^~5uu-uM>%- -Vmt1B-DPpX@EC3cx3zk+vleg90WVh2X$ASr`5Q7@xxRe&p&;}I^pz0UA39tc2nE_ -_NIFR9E2HONd|a|djm%MI5m=u0*tK^Lejib4YdqfvqK{yUN$7W3@n^sh%JXGdpe3IVFB)bJuP2Z(fuL -Zwkar2p~sN`c#ySf^6bsDZdl*d%SFMsNXCE7OE3X={OX1tlGQpUF7F3pG+B=YSa24B^U?@CO>#vqc$Ue@1XF$%eRG@FSAXU`zzVeO7x_sb)10@0Srquv7|v_pVRzh9$Fu%ZG{K}{kPk?)j8%=A0&NhpCIdR6$deSI;nD}f|GrTbIH(pBV( -hT!VwqjAB7MmZ2284KkgHVKx#0^|FdZLNh77^jS$|k0c+8?ir$Sb;T*!n;r&iFHD3o|-Nk`VzF$DKbY -oAni>tLko{+Rde!UeA{UtegTwwCT6V+11tULg34ZQUGx_*?R8Q`x4^s+Y@ef`(#!-)S+Bti~1XtSx0Iy408`9-LY&uiid;_3kabNsZ -xUzT-cxy++3Y{y#zVrg+eaXLwgua<2=0!2S(*&3tH_WBeXrG3Vn@cc+JIM1{Ft-4o~XH2O)#9kiHw=- -fKZ06o%a=K@n7sTe4OikcQ%`Qu)S8HdMLea3bVwp#~>4_n`7Yu`y}DYG+_Inowsa5?RdY8c>OMLSw2yl|}* -yS?D(Og40B-QZ54tQuktPG7b34@lo1>@D)`^Z>gf58CUWb`6I0uC_N$$qRPKd-~Sjv)14Cm%114>kpE -{vN8i38fqY{Y8cJ}dr!$ns*M3v_b_X5#``sfMOlCPiO9PLRSLWnkg<3)-11|7bk9%!HXRqUdxg}RBs6 -=pVPYOpZkBAnRN-AhnFFwa5MB4WKM=k{urA9`OgFYUaFBH{ci-WhyR!N&;>#!*M7Y0#ccbg}xf6Fj3f -95i)LnAN-epC!JpaHO=y+YcOWt?jTj|a_vF`A_QO*bJkbS3Dd(&J;a(elLeqLmNXPtXnFhA#J{CYPBQ -5c{ya~PgH)GZ(C0~37IgKvjvD^N@yVnVtzhVN~})6}rUGSRgp`szA@=F4Ca!1Aq6&%QdNf)m__QWrmp -RRj@9E~_!uqN7CQrxW(NF?Cde|G}>8Gs!@~x5oRD1Ye%Y$Z$6fyE)+Gbu^x}P&-VU0!$=@`GyqApNkWEW96!z+W@`tVK_{JpQ>Q5ApY3Z9X2_;_mMFRR;lChGEd)Xs*$=1xF9@Ck$P%suu;(7} -c#NIe5`PX)9>2LI@*fS_CN?tyoCitmX2a^M<*-ewv5DD)EpGOTaM8*8ap4oeOAjx4a_FL>EhfKB@~c- -l;=092m`fPRX_L)_vCXAQ)#lshxu6qDOmIkfM1iFVxCZnqna;tVhr&jAM2k@DHtR)XG_(b_suiylj*Z -lF;Y01zj%o*l{&VnhI*(t+{UQCZRM*m}Kn&J%Q-W*F36sS?EMetEVwJ_85`ElZ^TWelMpaCU396` -fn^k12XQ=ID~+Q%Z@n=Ldue)>V#ki?ktHKeP~dsM1L0pC!-+3?wdR7xG0qJFc2F?h_qr0mG~C)WNE!D -xHppuifH4MJ<&E0VRw@xOjQUMA#%4v1kszowz(0*z9G&9}k4+d}Z1GVSBsByYa9`Gns#9Nvy*kPx+0V -&noy0hl^@Ty*^{^`U6>Pg`FtogMb})IX! -p8iN=-6nL*iu4faT2>ptgSZAJ5zMGnAY-6f@;G_NcLmfcN4U=nPq&1SmBuByffh8`jyQl+jgTR96BW> -n6UXUg0Hs?y@W~>6UT32)g@LVkGdR?6#A1`2-28DigJS*xtlk2Veay-k5+3`tmdK!GC9syAgUz~pZl_ -BCTyM=o#J+l=iMNI!#h~{X%8QfDZtTY6*$G7Mv!5@1ZJO!wSfn}JWZgmgc!qeef)o`;J@6w@)VG+fc% -hFq|v}SV?gzTnM|Pn&5KW%chgUim%0~eGoZoiO!SU69ZPi; -us*NV-tnRL$P0Chl}bUPnSK<9lgR>x8R0W5y|XltjJ?UXsP6`@f5v&qCrcc&W4T>ed3K@Z3G{O(HDt@6aWAK2mly|Wk^jvl%h)s007t?000yK003}la4&UqX>4;ZVQ_F{X>xNe -aCz-nTW{Mq7Jm1y;HnGQ4jeg6r#GX?!f=u%m?X}|Y12hrsI)}eoXDb9q-uKyGrxV$Atmc#`I2JyVSzR -BpjhO0?%z42Ez7DsOt~bk@EDm>Nfdoz0U1YPMy5)IveRx)xSGZz+ZD5R*H!!n8z`z+n}(ssBS~oBk(u -!Lm@}`Y1d%aUM8pvZ<0ur8NxNo&7!Bz-<5*4^fMds& -x0`)$LjTanK@c5rQ7EWKpeF^Vpc+qvKL{${f(L0m=q+#+scPflq`A;ln)xZNS{v_X)sG3Fxn;Oz%FFA -#=ehmuMIQFk~zucn|&pJfFK%aS_-x4TT>o5n~Zpf;jjO5#x&G56cj~@U^5&0CSayI0O8Mbyl!9)Fu4^ -`O<;sKik)Q6wzqe?%^ic{?HA>TM%=)TK*~Ei%#tT%Q2PuxWUIhLU(qS@T5-le@&|lvS&Tfe(Txq-n(ex3LNTw7ICX! -5qMoX4|?+vcbuAS?CuY2uu@9xgJ58dx8XDw}_6Bz$I7AWmh6GaUYh~O|1bOyi}1>%7TI+P8>PTW8uod -?V#qb2#SQwP^!=6rA31Wc70Oqrm8Rbi~ss3D*fGd7>Hh>?Rh@K|)9HJ?DO1X>urvdI8eX&knov(RTU3 -$bn6!yEB0?)!9j0j7<`Vt5ycF^5&d56M&B-}#MQcgBW;IAjrMYTgfD+DA!D?yhdH`nMh9rg0p&Iu_sy -7I>0adbyP*ezmiIP(Nn2hD$4lkJIAIjCeslGNoZgR5D#qt9QD(PN1x9e6j-(VI5B!&V(sv&I4yGd~jU -Ds!+gcHfuQ{UC-0}g5lH!h78mzl+Y6G;cAog-vkdD_|zgv#kNvMn2KON*ayU8hdxzM^fPjDLM)F)psD -pNgEL}kSmoNawwv`#6H?73du~J^1T6T`uz(nxUblC7?p*e7&aGBSdBY^Tg7Ivv4I>*O)(YkbJgvgUDo -jZ&pIlHdG@G%LlkQ-gH>8a!ng%Za*UUno3SMj?hIrk+>2g72T6jgP>B{WH5btnN{>UmA;k|9ifS20>Sm?|&Vk85 -;R1|Y25#L$F}|4a0Qis<7kt2p0|pMo+$qSkXWWg16l0}l4;CE8vh61oZmwAqtT8Zz2epcAh0k -BHe3347^|p|qA&EF-TE5AXFL$I72$hDgiRwVRKND^|gZ^pexk1v3^Zg%N%}ym?P2LlFU|4rUsz+W2va -cR`{@7QU6~LZay(N|2*?SsV1FJA`GuXXZoP*PN7mxs$IW010kk-C0=DPB6kV9J2u97C{N@-x6l+}C-C -!Ca9tW9ZW?e6ui2!P{GGNu6)lHQ$mZOq%scD}Vc?3@s^KWoSJLLlpF-8dIH=*c^E+16&krDHy+ZnW5LvOc@#fl1^2?=)F$W4_1gu8X!I7bK0gphbGj`RPV -miT3|SyQ$GiJ?G2fRJc!DHK|z9ijklXDmGP1`bi${^=~h(K5k=TI!>9mtmz&koUqA`Q8{fvKf#lg$lG -}>&t5t6*ZIrXi^qv|kmnp0UL+jD@+lax35`6g9LW{S5uDVQ4M-~zc1p%fF_iAP!evO?ArIRokJ~TbzI -t`4`LYaGx>?F7k`kW-K|w8qLgB?kQUHgVvArBe%KzH6(n`ecV%u+!ZC93Dn<*EYeLm&SuI>k4`M2CwZ -6$*vq`6MmG}4wqN<*WOxp`3R0v($9DsRZ9X%W6J4uiwhvd<7bnpoJlpxplp3^N_Pl$k_yhP!k)D4BB4 -W56Y*4IN}Hpi&|zqmk{x)f;)6NV?Quzzwk?^9XW{j(ogGOBPi^7PTtdvIl?cdnwcF-`wV|^qo!yxAZMj-smsY$GQ7|C*LOdZifKNB3+rpa^Xk!z(;U*AT$0BIMrh4WTFtdbG3}D -{Ewa6As&Qa&0Jo)B7SYit_t0dQ!;3$>O){=$At>0Gv6}?qf@>~Aqhx&86l-idlQImkleSe7!bk2N#Et -s~scWh{P)z|qP*Jw^ZJX}phX-7@(E+xD)!)qx|9sHHR0qO^kXxVTj_k5>F&PfwC7six~sqc@d`$Y64# -QzDj1pRr$Kv8m4pXu>W#u39%#u8H|9RyP;8Fx=&8qq*8hO>jFX;(z(4j?EwXJ!-zdBhacfw`Ur?PRIZ -o1Zq&E)ZfoB&Xoa)Jo0%C*=36KNQbh9bzR1N~={~Qz~It+7q)Gl+VT;#iJW+Fz4^$`aiyA&Q6l#i7i3 -N#0t9)QU8@Kv$Ib6;_}z?e>lBQ_xIokPnS-lPNSFiDDGF9zQR>N7Asq{Q%i5&9 -xx(QtsCf=LZMC5=N(uCHjhMI5yZSx&jX$1GZY#-?<>Xn@|Wb$>VWiJCEdbsElLkZ@cSBG;49e32fXw* ->L{&F<*Edc8AV$L}_VXcUh@1*53#MvTH}69*iB3v}S!7})0J(eW?F_GB5D>G{5`9rWth9$bE8%6R(@% -J;Eh&Sp6yTgh?l2%FkCsrVPc!fbZKmJFJg6RY-BEQd8JgrZrd;nz56S;^e_i-{Q<+W!?4RPJr(FtkZr1AD@%eTr -^$-``zYB??4a#oGXg{s$;ZbhQp(#Ngiu2qJPHBSJL@CJCRja06yv@qrgIx=C*ywJM(mv;jYXjnjw4qO -k#v}0d>=iswP>{r0e%joyIiN|zYY}>AwyA!GN4u!7}?|Q&Kj)Psxgim4uI*qf!_* -VkbfS**a(kwKm*bM9-XF^B6M@QijTW@ofmMIu$&L9i)2j&anGJF_*Lt~Qx2MIx1$r8$NG#wDw75Zkf` -RA6V!BcAwL$KZ9BO40~Q~_tYdEA0l}lx=_X*Gs%fy5LqG_YgTg8By#(0H1193+FPwYFdR(D}Q?JQ9W0 -y*jEDS?lWUB%0_sk=r=|b{otx+jglP>cbs`cbzsF+Anx@9y&t<>}c!fbZKmJFJxtKa%E#-bZKvHE^v9hRZVN#KoGs_R}3lv8(h=oP-v(Np~)c-l3)Toij_5zHmr8Vep -vOt@9eH*Teh6Gmx_R+otZap=i@0&)8rN!RXBlQYi5Brt!PkM16C=)WktV$lI7%p%_Ex8*doW(4@CrN)UR8rfU@u|Du2U`o$^>?qqbN)NW%U)AWr7_Q}pLg?}jf1GLt-awpQ}`%f#*e1?&U-96Ivt<9B$B<%9= -mvC<))oU);;nWMfiml(LAxWw}qC6FXnF?z9FSlO#{p>k(^P -WEz<99Wtsxni#OVmTGG~4Cxmqf_8zMDg}->3tyOBK+;iV=yvgJ!z8>pg2~V85?L$(p2<~r+|YoBpfFqc0gT -H~dCE+qh`;PKBi~RzDt>@N(J6)+LNp1gQAAGVTdth(b=ejB!#$S~2v4mgx`W)CEVGlu5Mv58n_zh4?0|XQR000O87>8v@+tMVKBLM&a -g8~2m6#xJLaA|Nab#!TLb1!9XV{c?>Zf7oVd4-X|YQr!LhVOm~kxOI90&Sp_!7d%^sbhm~r&5BGD2=I -W7e{5}?We>`3u9qA+486Vmt=juI8RWz3d>gLa5Jt{289sox7Qs!!CnAI9H5kw`RB)GmB8#u^=c-Qzn< -sI@IGHo0{Qx7WF88f#n-}h!+&n@6aWAK2mly|Wk~Wv?ZBh~004sn000*N003}la4&UqX>4;ZW@&6?b9r-gWo<5Sd2Ld`Zrd; -nz56TP+Cx(yGj<-})39N~fc4M;I|iyu$J%VsprpF_`_Zz}G;nn=Wb(a7@`-MoSRy!7?tH5!6Z@*_ghx -znsx6NiLCGH}9u~kMxwfjRmO9v;?7ub0vnSat*Za>E^xWb{hndi_zl-S2Gr?dsCq!O(h&_S2j%;+AR8 -?@NK^HIyTMA*nKzvb~>24VCH-!ijLK;bv+MHI<(pjr@qFOAcI#XN+&r9WCVw4@L+>OpaTIi%^3o*R4;(TuLB@eFXETxmq;-Yy{?WYrL5+NUv -0!e*ET;)siyL&VZK3z+%@{bc)X9k_ww}bOXbSx0&RTIvGrPYf*L59%c^h7e8-Vw7VkT%$pP^O3Fi;!B -z>|)O;;^TL8gE`h4c9he$zie(l}O+7 -buS;XAle)<1%c=&ngbIU%j#(sU_2CvM#9J-?3j8w+`4^T@31QY-O00;mWhh<2qH0cC=1ONas3jhEU00 -01RX>c!fbZKmJFKA(NXk~LQaCxOxQE%fm41V{oAbL2&0a63I?S`R4ABwJ9`>?x1+ua_D!ciPK)*?%uB -sa|t_uofSwrjU3Hf#oh$TCHK{78{3ovk6y%Qm!5<~gXku`YmEd8^wX^Z1@5-F_u}rPV4)N;=o5CX -@@Nz5n-ReQ;R{A7K-oR}oQD0%}8js60y8uFOEZ7nPeylBi2^pMM!fYgNDG$3!8u=g{3n1noQ5u!et3b -mA%laSD80@T>Au}y*uSBuk3AY9`*PxdxrB&Ex$KVDzmUgVc&HI^VT_ZM(-CWZ#{vPe9{`UAp&SAJcidJLamiYHOEeRg -)SQ2j=-|y=JxvIhhG@)#SJ39>me@~{I@@R*R9@v^KN#JeiT}G&(W4XwW~j55#}ce;B$0zbOt*onuhsA -5UW<{wTHUZK{Z-3{0PsQLb6{)C5x>m=rxFqP?$KZY-{Y!9B-^$Lm%VL2LG>RiP@}-3VEJ-smn#D{PY7 -d&;c4Bpy#9!PSl;?37o-N6@daW1Og1lsIkffOk@Zaki6id5Qk{cMo%RlQ3)nU@CPjtvMld|dkSB^f+~ -9&F#QNDNbP%SlEFFVCGQyNu1M!-e3|-qPU66H^(4omF(D`r0r-Z|tr06tdERNFjPF&gMG#psyj}~1*v -1!~@EbzSZzIvM+Z&6Nk+;K6X&nR0iC|D>xW?&(qLEYd1h=1V^vdrBUH53GyHA;syL9+4pB#+U3)0V&O -I(o_aJz5fa{khYW5-{Vv2^yE;mnNHh3uTbTR(%hFfu&xnmHzqBiU0^JuN*WG1XIY@c<~2%-RNHBvaP* -%+vs3K_vA$xo#d(iIK$J40AZ9a5|?WlX#4HKERR0USgi*IujSIJtcFu8`Jk0689p-rYX@Kc)OP~>DGl|C=_(a6n)w=7R?kI!bDeqAINRzCau;=!;;OR(d4RMz0CPHtK2ar@Z<5!_~V(%j{*;CzUab8F#v0QM -}=h@(P*W15L=BpF9yZ`!Mstg(85_LDxcRwzL^W<+(O9KQH0000802qg5NM|P3c(Vim0RIdC02BZK0B~ -t=FLiWjY;!McZ)ay|Zf7oVd5u>50k>f>D;0P+Dbu_B*`JYxi-X5IO|h@xhx1zv*;Q#Q35z(je+P#(EWYj4u{v82y& -za=!J0(vA3E69%|`FvNcT$>`HW?!7MFN+4~R2pg;>~DmOTXIgK4ZA&Wt_Q>4R$N;QeWM4Yc-3p$Kxp+CPwZ{!DQzn{-o?x -mAhcj)`pbD73g7CcWXS_FOPVsKFUp?f@%_blUCW8IGjBc$K`$o&Xfqt+5@`JT6rO|0uPB-Wds?k6xFk -YgzK?GSuwhTNd#2^rgN+)$9&vck^Zthn7{~^TN`_27s51fs;xIxc1HlK_=8yT~7#ujncUY$Ucd|1 -j=n5y2nhQ2Z8Y!l#9=N-b1ieoKq4>|wS00!S7xZkOBrdiJBPVPCr{@L3#u_H4&~uZjNtQmgebS|8(9nOq@pl%CbNq%U&n7 -K0r&^A4@>O1`Vd!ojF_gq@m@=(mNpa4kE)_rBphn-n@gQ2!>EX#(+n!v8_-&fH16<=|_9ae6uAyRT_H -AryZ?cGaZPg)}B4du9oAEK){jTKB_O|bz@Tpa-&Zl_jd$(JQH2MBlr% -Z-x-07sJguEH>xW{a6##>k;UXcP)h>@6aWAK2mly|Wk^U=*y@P}002)F000#L003}la4&UqX>4;ZZ*_ -EJVRU6=E^v9(SZ#0HHW2>qU%{yeA`iCqf?)%K=0lLAYtZxsP1d5z0)dgvF%yYYNy?5<^uOulw?f`FpUOl9x -69_!Y|Lb`pxp{lMDYq_sx)gwllz6pV6*^LB=?-uHcMBajZl(t!`ivH9_($9s2q21gsieWiwz`7re?<8 -pS?PH{r;j`ovRcw)2x28d2`myp>5ov@^<>Ki_^F9@zL?~)A;115v@LlCa@DCQt?0C -aGz4`j^o-qtjfFI5Tn1@-3STZFU=TeEd7~m(LUJDyy0KbI(38pJ;`IC(T6Fv(J~_HPji0@|WQQ!+`!L -_j=X>ep-t&XKmj`<6w%*l8hU}-nHksfNXJ0J6~9gf>7PuL1L!|6N -XO;3ydNmg^lCT;KF*G9{P+7$Kd9?_i=GQ{IWg~s`Z84K{_bCPkVj3~kaG^f$_gqqn1%2z$Xfg%^f~R3CF>NK_A%ViO@a*-e)(XA*j%Ovf_o=Vf -)A{N1q=nG%6*wbjtk@KyiHI3XO07ZKm~yYD>d8n;iyB%=yEqJPF4We^=d4?}r_wunsw=@R2fHynHe09{jc-*B< -I4Co-EpY~s?2Y%6hSrRvzZuW&&aneKzpk**(-jxDXx5|a|D@+0UfYP8xS6Pd+!D8B8MhmRbwFV7Jl4^ -Ly_l);_3`$o^0)8jT4rl)Xw1Xzr@^}NC1PpltKK^Zf{LmZ*gkW~%7S`w_eCvZ<&blc3+tF-q2Xf*GTA -V-sB+9u4l0=dE3gS%1KdZ@qWomP}$_c}!7$bgbvm8>pSXW{6+*l3|^-9T+6Dpi~VB-d(`vg*mHV}@KR@?Q{0;Oq}N -~>2*o1A_;?7AH4=PZgem#wIMDQzOvI-K^1^#m;FTu7kfW3iGzHA3Mr(e-BOp!SPBuIp0;dC@JX3n^6y -p8*Cd{H*OXZM7Jgr*O=(o$M-&SABb~$C*85twGn-|H9*W*S*j~b>1_0%AT@6*pKYj-|)}xKf6C;N*cJ -?Guwmdx?k_!_TXy&s}NoVhlsqt+|$cZ+RAs;<<`xQ;=;HQ_y{Gj5pnCBMNxp*tpc$7-#AbaGS -$V!BIlZ7;=BWiy{-%4frS5SPpvX>Bx{sb73GxMsiVJn$TY}0Q72P#>%nT%7q2G-SXLgy8y-+XHga;Uif-7MPAk>UQrjo>)yIVPa&u+73_t`fC2mwO=luxHh%l;~^vC(Q^y~UXk^<4 -TxN8`-P@<(qp<@S=fQ#`r5p%%_A4;)`p>|G7Ol0tcHC`eL6G&xn-PRoj`PhsI`lKI2?$3(}VU6JIr*2 -VMo+umnc1VKcNIz>DYTAQ?+(=kFS(S(Qs3O8OT@73u>qSl&(lrPT>KfA-mMS9&KR?JV}zL^uvp9#15@ -rUv?-_{azChEudZFWC%W}(sk^msqJnlXV1*oQmp~Mdt>CeV629(xXAbo_S*kQQfZc|%uSVP?t(513UH98laDZ?Z)-S3RbwPXX6abs*n7HZImPAC5YP)h>@6aWAK2mly|Wk^IS24k!M008g+0 -012T003}la4&UqX>4;Zb7gdOaCC2PY;#{?b!lv5E^v8Gjll}SFc3uV{fb4ABvASTrAI+QsgQargiu!- -Y@3j-R{eStYQen>yEAWg4YD;okcG)>2P4wMW$q1Tc-lsju>(wo+Swp$yD^d5>U^ro@*tJm?T=6ZPaw? -7^v-(vi>9IXj)VDNuGH0Zs8q4>`D%{i3me|p-UWl-kTj9!3y=b{4hNP(6u}o>WFGZ}i(g0BT&El4tgd -}s$2^N7Qdo#^iJzDVj7YvvO9KQH0000802qg5Naf;?m0}J608K3b01p5F0B~t=FLiWjY;!MlX)bVi&0 -1S;8^;xX=dT!0FvPW~mF=`aU{paJ$4P{`c3?U1LmVuIyF+rK<;-?xRuqH&_O*!{t~w5s*07RICt-Y)!(*K>BQ@>*<&Xo= -4!Hm*fkiMr)`_P&OuOZ9hoFV%d){_UTiuD^eoz4+zr&6~GS{!ee-hY!h#vrQ$H5-sC5{^Mj~>)kVV^f -6uJy3{q_@W~l_rLAFl35QuOMGb+OsA~;@a;~s$BRmnimS)bR%FCu8<=^a#6`Pl|l))=N=-QEXPtu9F& -qZa~OZNv>@$AH7*%f=Om6%M(l;Hm=I!g*;zH-lIS*G{~4xLOU6Zjy@j3`BJvnYI)m)w|HRSU3Pi(<}} -yet<-ZQqkzBPTH@b2Ip0lKHi3I-3vmR#i$fj}pcI|RG?-DG4&RwnM9nMy7l+I9wN -Bc6Wt6qx?5rJ8lJCaDue%%Vhzj~5@Zzj&{F>6W-?KJImHTWQ3>iq4THlk7X#0vHO2m{1!S5Xi%N5k)l;Q~aTr_8kh9HPs^bYn0{IVzM)4A7(U&9a}j{H)Sm_!T*L -BkvK%ro^QW!Kgz%TTG1;!q1;a_^qHcZD=LwCCw`Jh{4og;IesNL31C#GT*zmD@@R6Ofit&sMdIr6S+X -L5+fnFQI8hv7-7)_ttEYWV@F68kRtetU(lu#eD_$z{*0De22dRnJvdr(uXWU58ACIr5Tu6MM{mTv4J) -oItDY838~ -JWZt1_I3%2O`^m9Y9lXMz_Nvh}e3_)j-bOA8-_aiAG=^h5#+=Wn*woL7_GwDcBti|JFt%#`i73C}zlx -~@Si}DVOZI6aayxducfgXEi`V+SG?1c -|Tv2h+IT;SvZ<&UR9!3#GSdof)|BkJ|}`3%J-2Rc?nVg!oUE9T-qhZV1F>++}_x>+iO~3qetv%LcjYPJkx)LQ -o)JcwIam)B8vS0ixpbeJ-Zbtopqmlj9z9caY*xwJ-^ekV1I~Ow{wNiC8p^is_;AXTvzOI`gn*Vfww94 -OiwZVgk83TNF&orGd;#R@_|LWffit&>&OjWjkAFl?17oG2kpAd7W;hl^D<3Pg40MlXX@%n?>I`Y_Y@J -*4v*VOfsnaBL{!u%cBYaeArQU#JuHVZ^oyO@=bAtbg=)QVaZhQ?<+|9EQV^q96_<$hZKzk-^Vc`mjQR -o7qvvWg*E+_wK4mvL^ep`voXBN?62RmKj*bBs(jyhv8fHf6OLeA%uf10~;u!JhgEMhU5RLlas2(lK6o -N_wSHPuC6ALsY9a>S}<@LR#@r^HOr$jBNVA><++eZ(etsPiO6dWU$X -iPU|Yp`o6`8Eg`2m5C7)9%!|ri8V+ol?Ng%~vM>CAhlKeUZ-+%eSKND*3$8tSyeJ0B7|0r!pa~n_EDH -Xt_llHSp}IN3=Vwa2ESo_N|6d60csMC@HRZkQZ@k;o^+9?Zn!nEJF1K0(S}il&^Jx#8bPI -pAA5c}Gsz3w2&sk^fL9OV^l6I9N7T5H| -?9~rZV{S?Kv`3|siin(_`64GP*5rF}lmY>z@6#KT6noubC0k6sc! -l_%k_UKMFXW%#lmmM%}eH)IZWS01G!NW}(epIaSAv -?6H1Bb9y;`{cY2@i|S4>`Wo6``UD~G9?qMi^_)*gG&1EToWG-NvSw-HE>N>Fq9#d?|iwY$Ab!@nD@VD -H*pz4+l}s2gT3Lq3Z$_5VasIS<5~n(rhZ7L-F1wD%$*;dgQ53hh9twOqXB$jipsFbx32R7gjmbXB(2W -&b9?RUYSc9Lha6SPQH>@pg|6n6zOj_C4-!+s$ -wHVViCRuY5y6zj{Dih|(W3HwbA3pr^ayn8SoUxaiN&<5IaP6Cr1I?uu30|vjffX_rhkH+G;_7kw95=)Z8PiLyO6# -`#f%CVb{a3$UNyVnFPtEADi$rFzVC5K+0Z|_p+ve_Xp^(9UHQ>#vL2A?bqO=hY`XiCwB({o$$RXy9 -mp@I=skPzP~zLj64$cvm-qDqi-Bp&Kpa{-W6|+6v3ep-6QhJ0OquUxv*TL -dEWqITJQdeHCjgU5{2ZHbeksI?p)`atWB(Oe=ruiW>Lp$1G+~1Yx-3N#L5G#dCMD%-O9F)derjJ64-FmsT9IMT(yEa8#{D)9!$XNjGFG*XcQpwk -kKOv7C0qo;(^!ZsKbc<8iwYH*|_Pjgmb&{V2y(i*5E(yh^R7*XZJ}}Q{Rz4r7{noUR -@I3kVU8v@_vxpg=?wq?Jum?y-E^v -9J8f%Z+HuC%a3SxtW{xD!gtB4t+Grm-F78cS}?Ae8II|8oP91yb8txM?#XOrxQ7 -j;#TTP&(6-y;uvAX?2>&5c8Lz=ELe*w)+&jW_*6?Jwj!^Bk1!RvBoxfF?22IGPc#N#DC1(ZQ9eSb@l8E~ -_{Wwa8X>6Z|}2ztK*B@wQN+*<~-Q4NAvIYE>w<<4P8_W2E67~r9yveU`A&?aOZ823y%ro -$h!H*(FNNA$+mKwGvaX6Fnrb#0x)_g1SP;4b6OZRl8>6%7QzeAU)ZZUH1JT#0W&=TF9SGX59p3(wb0> -g5++y-D(Mu=4|dPU&Lh>ZV@z5a99|!%HX(yFob2!8srkod@Yp6@G1}!$W)9nd*c{8vcq<}ht%1-2*O6 -%tH&C#=Ko!)aWbhJn3%L;Vl*#yjXwOv+9v}=}@f;UA;N+$+(2D?wsPzgy2!o)%HO?iLBPiKdmmV8B?> -@MtAAzm#Jces3mNj7Nyn#pwNz1=yEw%{8Q|US%a_kb#YtC@yWzW)(gGj<1=%*s0%NJ+fma5Dvz8RI&zy;99WjVv(Ey`hfpKq96$^O10NY4zinvOD41k_p5?Rc -3><|k_I5DoIuLW>^D{}#?06H%;1q4)Bqo`8>zRw7UiqYx*JnJI-NqzT$+C3^@D_a8GQbPi~q^~F|_Fy -x!`&!0msIbN{s%e-lwJe@gIVZ?V1A3qt%@QAByJ#9ipCfkizy%u~o`J%_=3YMJB}qotUm1nzCQ6p2L1 -2itL8%#dzx{>eW)HH(%4M~I#3o`R)soPq;O!um%s-n-!|z!Lvz)9Q?}?4^(a1yONgm&i|R+@7G~iheWO&Nx=s!-0|VIgbI4)*)PzY=CT{ya*KrCZ$9h$Gv5rrM+czy*a96(YKra(&>XIq{jFKoUA1V!w;yX2i!36KV?_zw8@{NZ< -yQddP%WY0lgzk^lJ=lx|A<)I3Eh-wY9wg<$10V9i_5C1942cWc<^wchFEf4%IWb7<>7&H+HcA59cyJ+ -6{MV|{INDP^F2w>*y@$AFnHrFx^7D6PIgmY%f_D{7y_F(B{9Hv*3smuU_2j4^NU?RnkMh#aictL0*%x --WpyO>4fLX<`D1tHj@qQ8Hgo7v$}eh+7$^khml|7 -~YNCds>8y<(U#ZJv^e0CNi6lwLzU?9AM}H{2Q2(Fk)Mu-Vyl0jsP&W0=O*Xu -?#SM-T=w5zeOP`)4DD^y#@s<><>HyYORsqK0i9(?4eP{g34xB}7NNH^F-sE`Wm<`p$08eJICIm$S#`d -UifNahz+-R@Sc%c{RFh*r4=3r!(>DMwTTq(RD9vd;nMZs45;IN>MOGVS4*spY*8q_YA-^5m4!m3e)5w -M`d!V?Czrz9pk@;Su~z83_?AlBw@}%Im1*9yl%Lk$r8VpGdiebcQq{m4xKtm{I{p`KEKQ$R7a(Aie!S -4l6T}v@a&D|Hn!uK{c=XL3(z{D?LmvD5J;qgdKfy9^Tn5O|j{jie0LyVuABwjSv~4m=q4+ -}k9fDDHKEjBBhV>7GbP0|DdkVb+>M-~pCPRQsTKt>YipqTD!c#P*2$KE;$zHRDOkJ?g)QDre4rO{FkVBlPUC&pA25xd$LLv@K-KP|KlazXRreS{lu%)oYOn8 -#BQShI|`2`=F~36Is$!HaL#pas)wyi+3{sLVHnTVz-kV*h_$d1cJ>w-5RBI-fVr6#OYoO-C(g2cdJil -imkKVYyadFi`>)NKd&+Y4hM)!YRD1uwt+*=xzlaMT(@;X4FswOTUYLMzdPcFkIwPSB~U=|i;D(!ipF7W)oH&i_I@VW -81Wj$sLUtWB64X!WLZ~Y8T7EiCpt$lg)+2+czDQ#C$s4rhzrMk-IzNgBlFpvV$iSa-o-s214f$LUc!) -8zp{~e5*{@;Dkc9zgI?%m-D3Gx(Lz7}m&=W$~0qPaT{+Z2>zQ|x11rC{Wj=mB7OpPJ*rQJ)1;H@+b$Q -gIp3#uL%d?iOcaC$dX-uk7`km+ya=H!ndT$&u~EZKZfD@|{#gZci;W)Bw3q{Yx4hj@M{ncr>BwrQND| -`3{fN;7?0n)&b`26)#Jn7*})$p?C4loYwO+HK#EEv~4gL$aI`*GEUMmyTs0>1Ct$>8MX+=`xM(0FAg< -H)<^ab$C~$3$LT#p!@fXCmM0}r9jVT{k~;~2FV1;aYA3C?GHt!Exl74@xgs^jOP9SW;eom38NGS?%bU -0F@P0x8!cI11N_4#Hs)OFLA9K?;V*5g+P$)PtiCdA>rjIMB7dI3dejvbzh_=Q&!N*~kFc`CJFM&2Uaa -Iy_#cI!m3fz&@@Jh#cd1L?sF-kl#&&}OaM36U)^vWE4u_7VQliI6obVI?MDmM;dc(a9Wirp3CQQ^&=e -oM~+!{+(&U^=3S*V6xy4h6T69#pp53CW65GKGLb<%x4+OmDfu3rne}K$^Z|+=^twbE&uHU_qB6fl!om -H}X;K-o_!s&G7>!UkC!eu!9vvQ?ZQ}Vi%-lrk^S4(8sQov(cyQ*{%Ur23ME2CeZkqc8FVR}qJ*(oxm}A=9%yNBu2+Pf>jy<|DdJ+tQfX= -j$0F9fXh&{+1>xVXqFnH5r<4DOpcHNRP&xq>|ug%0M4txdWq3xzz{r=YDLf}e-Ir~9Y%(mx$77F~0jcl?t7#ksP#MtrVxxna;gO4t7ZP)h>@6aWAK2mly|Wk~zL%*8VW007$#000pH003}la4&Xab1!0Hd -SPL5E^v93SKE%;HV}RHR}fhkl6PgLX$llrz(w0MEdm5h5wv|V76ezKEOsSQA*r>UukQ@03uVb^suxqE -;atyLTxeAjN{iOEno~+d-6(Addo-$QEvNo{wd&}VIvj*N^smbFC(G-m5EWmoZpfb>e)(`ue&awbg&{& -(MT|J5wR+|zZFN#OwX@tt2s|dE=*IgGcen7WPQ#CMmQ$qZ?26{@{N*Kt7_!S)lpW8<)i7gq*hU|V6|Fd(6}bMK6>ea+x -LzZQz}^vao+%2RxxA9&+Ic}I-Y9Ucps4$#6*vub>s-S&^;|3h?+Nw4y>BVtn?pDR`Qw~Ajj?dI~t?4CDo%OGsfUjSa7g1hcaHT}g=fEz<`G>h0~*5l@|y&eFDl;P_A^ -1n+IqK@13(-yN>rkU!40RFeF#Bj2Xqr{5*yb^4m*iW?bOQnF`GTEh)3G&|H>0*Ez{O43qr&2(0Zj8%c -OkNhBvh4fVod2B5R0&YwAsZkzftcxo_l(&t`zYj$^h!F*WoHJ{69D)+!I57nRi;)QeflqiNo$#mpWVy -`aeFSlT*j&GL>EGqWAy?>@wq2$wl`#t-4D5OF;>B=&d-9cE@(#2Pc}7^J&xH98UCtMf#fZ!e>+(aFB?P0ZJ=q?xK~=5jN*+FyT2NOsJ3y?4n(!0a)lI#OeE)!N;! -tTKGmUO*zM>cWD~)-iSiD$G&Xtn5&Md305rK)#=;YS6Mt@yYnhJOL9BE}}PTh0FVBi)|H%4>PWA)L?feF#ro_0((7Kn@v -R4_&(g>khU-C`J(tJwAlFroho~xK`F+|=}baV7Zu{iSt?|q;u!*?H>d%I90AyYM{9$%vo_Hjtf+HF4A -jm)hC|vJzM1lD{;Q(8X}E|Lc-y-$py044h8S?8Xbi^@OOY{O&B7MRmY9i -f6Zlfs1ByT8O#XU0&Mlf>}Dfi_zG1A9fk(4#*o((xVTJ77gy=xMJu`)@{cWvxaq(~Ds?r1WaIqqX^QZ -Eirv4%#-%BZsCW6qw8`3)-b0G0*=0%U29^Sp9ygj_eGZv@vb%7q9NRfpr_V_L{-^5blVbtqwl$*9rcd -rMg?xySBMm{R>b_0|XQR000O87>8v@oUEgAL&mI4gdfEaA|Nac4KodXK8dUaCwzl-EZT#5r6k -zK_o~eZER(?X!~Fw!0v7Dx(|o8xZUtG_;kBP)W>iqQAd>ddt3bOe&9o9q~5ODPHz6W -?VKUJter=HEAo=GH^(QtsWOM~>(S)|wNDuBBl)0F>8UB}ph*nXgeJ_kRZo*5nOs3()YJH%~`=f*tijT$5i#`y> -f%MOSBp)`T{oLslzP1=)3lM+%G?LGiYCceGVJEm>Ru23aiH;?6uFl;eZZ;3hRHME%^o`_o_k{Pu0sv* -S5Rxb43WYFd+;sZykCVvqwMYfo#Ex3s8GFl}sY-4Gt45eMLEn8W~PdE{yz23lLucqL5>+TyfKBv^j0j -f3kwY&RvVLwt-&saoEI!CeqVkF)tTJ0jJ{sF(QPE>ews38DK((2p -5_fY+Ky&kb1g>@b-m5PLv*F0aJZQ-;rBurb0QARxqUJRG2SaaT7qeF`0m;jL;<+o>DXLiGZ8&UClrNs -dkjS^Id4pd5QQ%FuRolA6plH||3KhE1PDr#1i{_KX|ZQ2jU8e>Y7FsS(HBI*)U!%Dp%yQf0vA{wG>Wf -OLAy?&sSEgwnE{Y~AE3Pf5L{wQS(1heViUY>f^hYi&3E=w6N?@Db-~0|N=i;7BYT*|A)-jJTa65HN_c -T2jSe3Sxv7vgZbxBfFN?DaMqFo&fMHBckPy-0m>u19T=Hd!y^c=H -5>D`fse(J --(@^J{;tc1m0T7_z0m0BO57ONl(#xE>6T}!etYg2{95It(BLFUlE`Y5_G(_V*x69gHH5Zqu*03}LS!F -)tW#Z`P|&^O{1ElKJDa|&pitSy*dz;XZl5lhyVX&9wlt1AY%r!I^EBx3xd0lI^#bAmP0=pgnEVDFfqg -^s)|2`!KGBuOK9t(}4*|CoH)bAUag8qVMwbP5T(g62^(*qwGKlofpAOWU!Sd`2OGO;;*jftQshB5L>p -&*)Wys}+>jQ1Y^8n(FPzbH1UK+`EN?g>i~lDyWuB{PYz2@b=LQF8Dg#kBp!b&$1Omlr)| -a8z;bmBG9}ZO&BzOdWn$@L^|Mp3os#jLikK3*`>)qm5JXV>H>-h$e51}XuR(z< -|9r#lCeB6A9svk~FaRx|^*kEQKj1DqIohh!RKM_MZgrhSYK0lt7NoF -|c4C>Ud8K~?AceL(|yXC`&-X`8)l(_S-MYcDbw^gHb>b_(3bv$tnrhiyNZz!)kfoa*+HPIcU!y|>XTOB=9Bn%@COjs0lX) -x|A)J!h!B$4Q}iL$mY>=|8u6AYxRr^oT;E@%&y5lFidBd4ElO$?0~T0Jco>C!o{Z_Km2vx_nG3>lf|X -LOF`+Pnii?>^I|wXJJY%3GR0Uo;H=nG=wypMH@MxB+xDWr=(^S&FLpJis3iaT`x?h@zUuroX#&nP0R1++1^@(3tibn9v{<*EC9gp*5kz1=Xa -SW!$&iKt38II0kKP`&YJfgAJ^4hneSL6NzqT4mybyUoQ$V%RCts)>#5`uZS8Ksa>n*t~b>5Ywsr4_x; -y;{~8APP60bC(6LQ|`vU*!>W812x#U@L>YjgJoyx9QZ_62Ksz8hrWldKiFAXm0Eb%<@hFBPfhx -fwwDcdx2yC!;M}dw30b?wuVr58W6_e2}UuP)A|GzJ)}d->kFm1r3u4%FdQo!7hj(xvO2s>go+tcM0U -s5Xk+MqjQ=Y|z>E= -kxsvgx^GWDhUUM0B}l#69VUV6z~U*>3Jm+VPZoT8x}A?NWi#^MS_)Jo}xx(so?@> -4StbsQ3>4&aqzN?%?zttgV|(IO{^m&aiHSnp@y|cyNS|z}uK;=HCbJ=0T&W+hrV3jR%{r!IL>AD1qR> -^vOG-3%ZrAiJOxl_3Ro#7NLd*1q2hsc^en{C1~7f!Fprd_Yi?`|aIbFq|QO*;B0cEGRRwgPO3z-)NK=ZHhfcp%cN8&! -0b;FH%%Yar`YoCYY>)!LN>@eKHGCVpg)(t0ZJ`5#!b4zS~7H`2pIiBQw`V)X_ko={aF?#2-qKPYf&4B -h#Jn0c%rpByR3|A);lbhqLK*Q9Pd$XSbPzl;oL|@!*XS55)8Rjn8J`Pvo+P{V$ -QsBblnC72CPVax0PbBRub8|kdWVypy-U{<0#@Da6aFn2DPEXVQGn}(E*Q5k_A$22SEz4QJ&4eT>m!B`$5{`A$G=gT@ -hZTeVLOnh#-|dbjh)8@l~p1qfLptZ7Etq!RKA1*ka@FNPM) -&BudO9KQH0000802qg5NO@vv9bp6j0FVs;02BZK0B~t=FLq;dFKuOVV|8+AVQemNd9_zti|jTGe$THk -TZSZ@OlV&h23nR=`p}*h7W%Rb#wT$SpLOgU+nL#=^uJefZq8)((pzA_ab)R}^jXqWMz@60s&~ENloHW -)+Bm{KTP=IX>F^#!_nIMPDnc`EQRePd!`+8O-4-X4CjmJsh9;}nUU -YQu|lgmt(?(vkt_9CNzKYh7`0KSX>S4{dg7|oCb!}#Z}o}Kstc_uQFqK(ZlWkcU$`NM$*H{N?q|Q3P^ -wr9;YdNOkjz^03pYh?1e3`cTY4K2cu*WP#Z4Xi_fnW?E1&iZ?=+ou3#k=6S(8i<@52LbokbWwQE;&qK^ -i(TEd|7pIRIv5h`c>q_0b!;ef_o{{2Q9=0Yca8k+(vja&3fOC-TS-7GZgN!lCFlaz)exkZV<2DGn&-= -hVh?BNCvNr#sg-V9A-oKGCKDnj{f;FEE>6_oPir@K--~2}MO8ka{x#6r#0x;<{A_2*YaA0j`x -ZU}g83lKHOPTmi_p&5PyD^>16%X$OT!fvygX+4WiBq?VigWQ!0U1Iee5Y%PDui -Zf<1wy8M2keWQXss$IcJWAq`j}j=16imly-HD`Dt(Qg&>?`iq^ZH5ogJ7MXB$DZxY- -PhZa;>-*uzd=Vl+NiI5IRZq{v?B6=B8zpwi=;YW_CWM*6(m-LQ2X-sl$(F}8)Fp7`E`#V(&J-Ekd?7d -psnI=9Ya` -$@>s;SbR8=f4im=+Gsq>t4}@+2&oYdC7n7n$jizKYa7B*1lO&z?ZMRFfj9}X;6b3>8|>S8US?6IP7cJq0?ctA-%)!f1q=#%h~#(a!?(w -4P%aJn=zN08@n#0MB-r{q>Wg>w`8v@8L?i~TM7UG?;!vH6# -xJLaA|Nac4Kodb9G{NWpZYe*3wR=-yHrpbvLvcv6qo{MKQeB+WN*;wy8&#IYqvL+yN(;s{v%?wv_SP45F3yhlQ&B(lQ;jJ~?WR; -^ln`^47y3~0XW(P@Bhs{$*eyZUwaH+#Cp>OCj_^8XKMAD^S-+RHQL>!PGJUMGb5&*_Zl(mghtsATSsj -W}#1RbMU&X6`T&-`Dt0=M5N*qH=&zqYEm@r2CLZGgg`1%L+)fP_6!lh;@K -YkG=s|zuSdJy?R!)od7KY!&={L#SSNI02W;0Z_P(aygJ>3cOiN@kAt9b<$hGd(PYt8=BJRYs4Rh>6LZc-?BfcdvV64*t9DXn-6frCr6A!7)f3 -u@>ca4yW621M;?l;&XL%^;v)q)tbeijyZE$P3G*h7ofgJY4(i6i#7^WqhUq28q^`c`f6!9-l;;mLma( -|k`&xYp9-_5{nJ(t!1SST>9e@36a|M3sel11;MfAxe(_f~PU*@H};FQi&tEaBi5EFOzKcq{hgvrHYvx}oYS*4pjH -*902coY^0^Cf$zaZ@%vDL#sFS%?o<|6x1)HGM$&68v?*$0(sA>F!aU@S=uX@5L5T7qY@7@&2y9F06z?|`=RzADPbkbi*Is;JxfHsN033@afkK -ca;bk(&E)?T6BZiongnT+gWQ)aCs*PaV;tXNI>~1E#0!!K&R`_KLWh`!$E08!_1LtGBXir -?wjJA|DMeYHt^h@qfn=z(V2oas&^@ti>iwF<1=t*CE@DPB6y8?{1BNtGUP_^0y=5$(%FK_v7h1ocpYj -Nq8n7p1@|@0e#xSZ|}+hDef7XeZTD&fSa!A5@=6YM3=Qe^0oNotBnw^)A@@tk^%h-w?iaErPTgadrILwWYNc5C&)FJ#a*&`y&lJ9iq!*aaBg`q9Cz6lvt0p&ZJ1E|Os_#KBbLnYCm^K~Q^P(7BPA50t<94Wp{M#lWjDTOuV({medL-dTm{~5rGcA;FnZ$U -(@XKw;fgDm&@R#9fk~)w99=Efu=8sDqP^Oq)e`vDP`cyhh$c)!Azg>T( -mmOp=^IfKNSa748g#&c<{d1{p_`wjQ!n_41M0|7&Y%5;+pNHfjp1I!79%-0~p^zb;8IZ+Z6Y<^-|jzS -iX7%vycb+^7_d}a{w^xQ&Vf4$qV@hT*64-k=-L2>Dyj1wH_k4WdAz-Dg^V4@#$X%yKD9liY~Zd-L@d; -YGi{ca6vf2Tc@@8@tp3Fm$KgJA{QPXg0S@Pj2^Nl!;`R~e1b&K1sb1M&aQBhe1>Cpr$R56+HSgHMCE*I_iT)ijoLj}{7b(f9dr!Sr$XN(a`a|(=C)2sjlFky150C -|(EFk{4LaP1*U5{v{<-y58U-R`$iSyN)bz=RD5;T++;RG%+PuM&5hC#N-kP3?;KL@4&A86q1x96 -%H$Cg10utG)`==Y4re2^k^O+vA1W8<#IeO%x*&o)FN0LciEbcTd++j}{BOsEhp`e7?|Q$0r0ztKg|mIPS_}B -1V9Km8Xj*fUmphDSl<}fKigbf$)6zrkE(yHI@=ld#f%DZ9H;yy>5Ko -c<{q@#x=K5gz7>#i*&F@cjlKr{_x4* -APrPNsh~sxZsKj51*18NgdB68R%EEq+aAS`qmS!o?PjDhI6;$=%(-bmJDOjUE92)Upoel{Y3_jDC)(r -4Eo~OEdgUbZm{dJ1LqzTtI6!&P)h>@6aWAK2ml+0Wk?A(qmT0e0055!001KZ003}la4k44F)lDJWNCA -BEop9MZ!bheQ$tcoP*h1zPA+nDbd^-guG26O-TNy>Vu6Hi?0rN*s65nNg)ZyXO{R{)u_Jqu(!Xcyv`U -N01+t1V=P`3U&R_?zTCIe5?hy9d^Y#NoY(n3oZ?P3`JE9@gN)5XCp^f!M4rl6ummwO3UO7#eNHt@O=t -;fX?*xV5;20uUpGZ5$B-j=b%qp0q5N}|=nd_P?mlh{Gu*aaK4cUMp2Zw6Wk{$If`>?{t(2U$Ey&!Qxt -~>1wx?uihZmKVeL~jjMT|9uN0u^MqJ)fDn$b4!#{564m`9#3$B12akNS2zkXQAF=hoEdgu7rJ -dIa8kRcfgy{*&mI39B@6aWAK2ml+0Wk_!_`M4?o002Y)001Na003}la4k44F)lDJWNCABEop9M -Z!cwTbaHuLaBpdDbaO6rcyx -@6aWAK2ml+0Wk@upvs`8Z008L)001EX003}la4k44F)lDJWNCABEop9MZ!c|SbYWy+bYU)Pb8l|7RNH -P7F%bO~%TKLDH%&{qL;_JL5|IjxNZwGja=p`ysn<5XB#o;4J2UnM62!wwh^O5hpYb_!W@ewb)FK3Ruf -PXBVc+-_tUz1Wfr)G$MNENBChUTpu$i|_l2KVR!h<^v{2Aj@6Tvjlu*=jaFcJve-$7R*Z&#Rs-=C)o)*lmcVV#l`pWFMm79QBX8jQbBN;hm_t2qSlM9Q>JCt!n8@WnP#?$OKa%uY#^+9 -F%4Y+*0pHL67DI*m(e0Mvg`x|xtp<`wL`HNZj4z -hJkcu@2GwF+LE)rhm`If3~tW4xbO{qmf13`M_$RI*reVl*zI-M=1H(x3rBG1CT{SGqL$@j89*+RUG(S -^xqgyfamI8iA(I;a&Mo%Qiot>k1~=)ylcx-DJwqAYcU5OET6b6YUrMZ~Hx<*rd7*6SsJW=E&h+3V``m -@a*Q&D#2=WP=EsTA2`q)^tcYDXOxA0b+7-w~^4c^!VMJOsV@7M?&0q0(98tMnqAsupzy>XVvL`1QY-O00;mZhh<2|AD6-g000 -040000X0001RX>ct#E-@}JE@WwQbS-IaW^XTaZ*X61Wp-t3E_8TwE66P10sv4;0|XQR000O88;4~`tJ -{JTUjP6AZU6uP82|tPaA|NYI4&_RFfL?ib960fZf0*US4c%gObgFQP0i5_OD!tS%+Iq@Fw`^Pa!<`mE -lMoOFS1fdO35rPi7yAKQP42ZGqTh((Bulr&o9ySEY=MuElSPFOtMlaDJo6n3Q0`2QYff2(#^}yOVv%x -tAvRdBSg3WP)h>@6aWAK2ml+0Wk^nsWTR{Y0024(000~S003}la4k44F)lDJWNCABEop9MZ!b+nR6#^ -RR6&(i+j84B5Pip2>`Wh=c0w>tnzqA6NnJT@HAx&z>?DsiOs+^YAc0^(s`~pbNZOQKwUzY5!Jf1Ci{4 ->Sg$gS9gr2l*GI%PVhznI?22MM1|NN6YWx-`z*UE1)xT+AAo8=jJ%xzs`Q(ytXS_3QVK{phXF}f)k6e -`<*Q~4KFB1!Rwtub-@ok10XqbyC?kaQcTq_(LzYnk8&3oL*FJ2cj@AEPM2T4}@X^_r7T`_L+bP4`RbZ -R<#u)|Lh(`z7A$92;U&?-p0$4Q|%f7nA`i+FXxLLZKWf;R~p&X^T9>VLV(MlLQ9Zk@Ej6YdRw%4;0&fLxHY)LTQp%v+*w;_Y;ugW^C{kK>=;QzyuVz)nRDK -DOwIb(?!-HkE9-O~0he|a*2?3Kqx}ABt4+Z$4tR&iUYB0gb*$f8)s!t)E8@pm^4HU2@$!eG89yG$cy= -sa9?$sxXvXs+8H?H(j0IA$YX;eJBbeCf0x5a@SMVx>fD~AX_<&zq?fLzjyEuc|=3jmWj2M{B;ADiIJj -6MVvGopuZldH6B;{Va5T)?w%bHF>dbZ?OUDCp?{W$tu()+F=dnHgBi0?o}}&=M^^wk7@aTh8kSc -wu#?$sM=dt#-6&Tmi=Q~hH2CgRgLfkQHyZpInWsQe=$-Wu{07t5_*U`IMNIkao|Sk!v#FNr2kmecS*t -SSVc}Hd=UnI@5tiAJcDnd064L~dBnyRC|Mk}Ptr)+MF=4!=sKRF=@#j_lvHUn1;uPLQ>R^D0P0Es09j -DJ#ISX}7_!}JJta2l0qVKSJ55ub$78qG7PALR%0cR7I$;usIE2Z~w$RP;cf|6(y#xy5YFbhJ -M-~+`i5eV$KN!yMnoG;(Zgz?oWud-(?VDN3v&+A+J;r- -rAbKRa5M=3Nf13gkvTBKMx*bW9+&&Ff4}$KHgl*K2g{Egj2xl^?EztyQRVK#IBNjfT!F$e*;iU0|XQR -000O88;4~`N)(e+1S|jm9$Nqa8UO$QaA|NYI4&_RFfL?ib960fZf0*UQbj{gQbe`9$&#a3um!kpv;Gt -5HS@6O1p*{MOkxylFlG?*Bm`gIsIoFkp@_)L*Ver#i;PUGbMD~R{kUJhw0Zk~d0t-Uqv!okyX=O{hfw -VQMm?^(qi9w2FiZjBL|@uKJTARC=K@qTZFb57$#INunRg%n;qd?Yf3*J~ReoN7K(erNw&z)fJ+>nl`k -BUJl7BOg9*A^;=3Rz+jYv1!y{_yIC6FIDfj`7yQ&oN)y+FBTis>u@8T?IKMz9J;K%|RQeuU?9Jq+;UP -U*tJbW_OgKoEh*|G-+mAL9N6OD;p)tjW|LA^b5OAVjavq6fFGt27w~rLwF=IyN$z>ueK5F!TpjSVc`c -=1o1kU@1ZtSQ@roh$0A1VSNWQ!(Hz4{n)MN$5<&ysX9v~9nR7YLyI+T!P2Kk-Wl -jbG?v{%9CRt#$*CLdA;qkS|!oe5h`{th(pj1r1%lWpGI|xn2b|2<#5{Qb0(JnVnK|{c@%6pcg)?`&kL!C|;rrjC*+5^L)%KP}_13MT8N$>|u*@XW4fT -Ye`b;9AHnep-FxEhxkStVouxG)FMWeOgN=@rKHTADi)ga4gd8@C9Aj(++Ig{X2*W)Yzb*ZS;ePzN>Qn -lMauM1$jDHeG7(ULF)(>ilL2Fi8Bs8b*Jd1}|))_!aGlP0R^9-Ed7lUj&n>BLcsiJ08T$VL$EnwD4f;5;e?mAmb52fo?ITfOVAuD~vOX(4i48pMa9fBeFN5H)Gqo|LE4->7^lV@SnKnOJ>SQ?muIly -4E<)cfep9M$BaK7h}WE&$+k^u4%vR$6XD6i8G)4t!Wvxhw{jlW8PV!Ldk4aZ|pUc$Z{V#KXk&`s_}Uu -G?_F+LPa4XK*f|DsMj1EkHejsDfG;u$5ZU=@L#Uy7vR`0AIDh0Nm3@LC -sn{T+;%2A;KmI`f{v!u%w!5Dc!2WVhK?$`BS{?k2sq@g=YhhqNN}wi5073N$AL>8R`Wh8MQ(qx5;@9?3l`(?zcmX)T~6=dxp2?AOL%?=|RbCj|Kx`p2J -Mcix$fEwFEfDLL{@SFa~n1Y9U#=m%nzHEfwod=(&u@<4tElQ{ViI`qSA^T+krH|1C3o>p9W3}kkx%*X -g>BG6u}#=K#IAK-G5i!7~j$0^)Hkhv`i^gHm)!M?f-_2DLN1c_&6wfX@?ukcKP{lG)cq!RI6boKdi10 -ucvcHnlZeuL~semi~$0dce5qyFunfBY;zEDBXDxJjBWXm!(=-1D-$m39tJ1p+MjfR&ZM5}`L=#F^NE5c%6xN%At -Dd%tZzFx(0nI4MmZ*nGQ?^s2(@{Lr}vDnRu0Kt1H(#9;(isi3rjK#ceeK^9MaK8$(zYU=hV^wzN0w^k -+X#>^QjGF(@a5A=$FrzkLShAw$IU<+Qm36#ku0_?M=SqkQT|Ai3r(ngdl1ezBU)ZZis#W#fE|QF$Zt@`X3lFEdiC*2TyeG$(d$f2tGm}xDpI9U(rKV+6& -x*zr7!^hr-x*L*8IF*IvD59k_`%xA_EiNR6rI -K5uXA-S)E4x#Z{+Uv&u&)b#R*-9240AD8(CrkNP7VgB;<>m4>a6z6p=pe1Vt8Ol?vRz_4$|8~LOE%ywRodzYJ6L=I -gS=f*3C=yK>|ys -()-4ue{zI|v|g>}%M6(uC%NQV}y&4RSy2%DX~gs(qT|J$15}*e`T=DfF?4J;gZ`l)2@L07AlFBX6nW= -9GumMHY@IyJY~hqSk}G*;{m_qZ@y$_im9O1k=rc5kAhhsm`Bb0)2O{{~^DNt6b|Xdu|tNz7=OdzY;Qg -At6uFq`B3UFp*DZn%UDCc#$?^BcN|^P-U7)-frm1rC6^*$c{Oo@k1vi4$Lqn{EfNcWLz#dI-0pHNF -D7*6DP0gxLnLo;T1`$rK>DhbXN;7lITmOLpWVbhSc$ORP}{`ey&e%}>OS_?HHgZ1b%@%lmmh1r8ffDlv6uK6J -aF=x^_|B2H4}u0gSqn8{n-$#=p=+H)zIGMT!=iuTJr?Sr!>80ZruUM4h4yKh4T~jz8Tq;LBD#->n)(P -H4~B>hfB+GIrQ)qmF-m@@z%lN(nrs<9WaOuAD33_>+7{U@0|b~R~;xHOLf1fxl|Sx*QO3 -Y%t`327J-T?b_9yPCxyRIKQ8SlICn^Ms8-IsMq%rVQ+pQUy1Fp9$6N>JN~nx#H=YSgI}+LQ;crmivh2 -{D?|qEZ1|W5WKh9{)U*&iLqn@ra0-p2UGkj?=c=EF4F#z(7b$Njw7H!jyU%B|8)4j-0IBc@X{FFw(v? -i^3Yp}bEJX+PoizRUd=q~&74#K}#j{g{{dmhbyz9!KKzz?b6Q-vcvnhxL{?>?ScV^t;gaAHyAyV?n;!~?!n+hc3Vb56hw0eVIcnZBrh(?jvN)3gPF)z4vZ=GBvX6tYA!; -btvq!)dt#lN17gbj#zCUrn9zSDqu)QZ>g!hD7DHBKH`qJuwdal^`%v`BPe)y8R9}%*XcGyKxu80BM?x -6%HjiwqO7t6{5LcpORHTP*;YQ(Wij33{;d$$bpKJ;p+Y;iuK-crE#g!>opwkTW3sy}$x11ic8Bd -L3=WLt7>6&Z=cD|~(cVC{v0bem2QMvZa)V0W8$c^AFEu>bkPr^0AM9+f_e#FMi{fozy!YM8EbG%b;zQ -u=*<2taAJC#QGP9SGv^dqL+He4hDZ{HR`SQ!+p(w64_IxA&B27G^AiILfuAio=gHv8kI@T`xqKCj1V> -KFO#5pzC%Thp~_SSIsvJ@O&FfB~Yx!I@!nJu#QgD)@G;q@wy1WU|f?ZK*R;v!65doCV!z1{!;<3A68` -umeZoI$CYNhJ0LiY-eZI4SYoSSL2GTNjFHa4Q48sL2jaxLll+A}Hbq>&VPj@O42)Ua4lAUx;?$E1 -zqy6OYTF0_%W;SE)(o5SyLjD)_km8T<{RE*=Bwak9p{q{y6q|FpVGdP||8*uwn)sqsx^oPSqln0s$Q3 -!X6B5KX3(1)eT^h;$_ot-)-+ty1c%{;pjajofl@$BlappA7yz5d3>rLS-7}H0*l6Qhk-eRwis*Rb7i; ->eC4xta-IR^Pjf4`?B@lYh=m5wuT1D5fRYVz1klL{V}*I8U3A>TKqVh_GeN|f-F*(cpU{6?clhOd1-@ -Cu`H)JQ|A_Itd&G%&X_>5r^ZtxGTtkYfej4tsBRwyOP~cD7|J9+B?k98b?$GGlYI@3Uk73qyGv|^=-$ -|QqlGSw|>#^yWTd+r=KQPQ$zHNEyhp$#928tQdzuLeZCn{KxTnRQGx98*&-c{)C1obLu%yAr`gT(->z5!lTXg?zrv9doKDF$Mf(0`}OtCXYX5FRciPo -X#fb`6d=CigL^M5t`&P&=|$aO@f!Sw&Pga3hJ@L#h_xJi)v-8UVc%0s-Z(>Ry -~3qrD1(Q!8=(^T+1`x2*{C*K_TcVXhm5KMU)eKU*eid?Y+%21%TM^B69BghRHggGg($ds1=kPj2N$A> -d)?qKA-l=ltIn;W|(z|plrYvwOHHnKx3L^pIk2hJ1*GF=DMP!A`hROEcyk*uYtxj9@$i`DI-gj6MT -R~$Qk0y!Qi4(l0)G0bRF`&$t@yKm>R%HGN>3 -{j&IG?<;7J_^66Rl3ZQ_2$N(!OK%w2V$}fVdx3#CmFe-^Nah`iW8eAU4=it6PuAD*I;K+)+2><f?VkK3c;9yt!!k5f9Cg0%A$NNSm8GH~qtx~}U%GY~$;3idK026;W%=B*IwK?I?y4c!`JwNY>E1N+2(~L -kH56tI7Bx+i_ZL6lL!JNQ$FMe=Q7t}N`gIGsq={gqZ7Xd@9I~DR_xFtm2Z}$cr`OhCO*jvQt^rUdL$q3jzGi5gZkRpqDM6T>^BBiRGgrhuoeMryD;`< -%mz}w{oc)nH_v3IoukrrECQlNq}0<1{B&O{AA2><|9Jnv?EqS?eb{^rNaCM=$5;a4kdbnQw4%zSo|Od -XQdQiPH~43WUw=*Xh6Ds$15dJKqB>`3@MTmOZ;DYW9}FE8jcu5d7ioT4*EOvv$)wnS2>P@y2nVjB8#g -RfXnrb!coTY~#8aDJgzMU?knz{hZx^5Q-oT}d4DJ)DdnncE)0UtkR2}sEW8}fgE{YD;Qc1=7kryX -Uw}^qM@|JMAE(D+RVVb4+cT1%J2)|L>DHO)bR|*$?x?)8!+>{D^9y|F%1xDhf!{!eR7jcwpU;!}tDdadpTcc)AAo~uErXCkbg_UUO5a|7%fN=g`QQ;o -#4g7HSM$;iS_^E%zE#t)k5quEgGTqKq*r;1eZ%iYF&|D0 -!DwV+DzD`Nfy+Oc)x#m0p9%j6pmB>A-U4{ilqf4^k$><4ab>XN95Q%4j9R&gz5}7Dy3yJXrS8x{d8&l -ANuFgds|r-LeEi7KHd!ap_R8vG4v7RbNO(m4v^_)EkxCneouEe{C?T{A95deQ{Kl_{IS%cGk9bPr(2| -&-Pmvef_L*7n{C0kM0@vY-yW@y60jK_s2u{o`@{d>hc*uLB>y;eoszV;UllkxLws>`Y2PiT+4V(_J$0 -O{tVmnu$UUp&XNC(v=y&Mvi9cLn_I=CkW%Wgh%OTgyAQS^Nyi`4sd7x9&K&*wczYZ4cE5dcXVjJoj|3 -bTS`(+G-s#4%y|z+NZPy@xBcjQ;%{@Zaq_LdlR;!0p39)eGg3A@}TdyTb -je7Q|ydRyP$btWwZ#l9m#9BW)*7>MF_2RShaJe~$#jhmWNCoMBQ9!w6_SBZiW5oLo{sax1ss7lTQm`F -G+-x?Q`w&w-((<(j9^4deF39vz?k{9x1s8z0qtBMKcLnhaIE%~nqtPAXESX*7zIhEg8G=wM@1^DeBlm32DX{D~($Pl~j6_p%)fhj}Ji-MM9EioEC3z9^rT@6tm1BN*S0t9DOhA|3VG@Fzz? -UA6LIxeIKiR)OuY7+~JzrSX`xqdGTh1#7hI|Z2AsEib80h#BNzIg1-yO|DZSE5374cIWW}o0}64$E%U -lN97{HlbsT&2dgPMdJ%pN*HJ{8K_8zPLLw%1_S%=eSu2~9sAeQy4QbMj2+(l3uxg{xrl%~XpvXZWIXA -36p>Fx_|Q{*3_RMq!-uQ}Oc87GlB_dFUd94d>*dT%Khhn>5EQ}sypkL}FiF!7zCec6p$ZbL=+{aVPhO -s+r}-?I8U9zWf(WwkOkNNa`OZ;%)5{BK1{Gu~G{M;(!uI)M0s16^8Xy -+6Y1~sE)~<6hM&cOmV2Jqcn0zT?a(Iz^p9xCDOJ(64g=e^6s#q7B2Ix`p%JOy-dfg~5+tA5x7v()%w; -amM&EmSZd1Y)5QpCOj{DB#uz$2^M>L+tjp?b24mo1$m-Wzv*;r@u+Uq@Ej%HmGe!Yf};ISRnImObJwp -t=aSWc?<Vq2V5#Yd9Q%iPj=90mH)sH~_ZSH7E8U -Jv#{hIGdkTL7t+?_RlX=4AB_Tx<5(yroxQDw*{uc(7WU*B1glIDZn~YfO{n!E;|B3$-kb~lWU80!G{Q -Uy^W? ->Qph)4!qlijXj7S};O~XN-F6I9~_N=}~}@GY+l5E~?<3Cz#NvAg3FPGTnjXyRiBVUlVPb`RSD6$`7-={!sZK(B8>7OPa|C!wOcK -U$PcZYVp)#In)y#eo<`cY;eGqYyNtz@HeDyu@T*b($aHtoPYv*LX}6M3Z>qmWpNX&mOW)~uBOLB(H74 -V3;Lah`hw{5HT2%B9m2#}6<^83SR#O2Iv72$g??lylgjU8lSLq7g9Ea;6!@>tdDxN&`Dx#d(mfKF1D? -b;^{Nd<5c;@y231%y@AqM-Zl=7c?_?!b=XnA9*ZUR^;U}&RHNwj|wmKNJAH9w1cVfA4lUL| -(|IbJiJqMbVoPN?7D8wKQ#StJk$sw)-QZE}qHI5V^#S4b73!B#t~``UFsl<+&b02II`+Z1PtW{b0Q@c -6IKNHp>}+9{{O=4ha|xk*rZj2f@Ic!#0XM>foQ)kf)FWnI<}X<%9yaYmYAUp -SkI0PmMoe`5PIIm@nZjph@ugIQP;u9E*)%zmRmh@7rzs9uXrE3>@Dh0lok*K}DN^zrAlB-*Gklfk+so -`@W3A&R|2JnI+-EqraE7(v7nP#-gqbN=yIe|_L?`S7-qZYUxe8W$WFsL&>dPJUrYhao?AuIp0NVa?`z -KWT;14m$*g!M`@iTN~En>s{%Rj8zL<{3E6b3aSeO!Q-NdQb(kpRS1rQn7(YYmB1}K2FcAL|CXur>ol0 -k)bDe@{wG6to!_Dk6i#B2Ze>oC*qKD9XFDvWmO4}QxwAtSrb{@!Va+D;@V^&BPu6hh^RmptchaN?BUH -IJsoCyNz0BB@tEPoB=o-yHP1TGPd+%;#C7#)5o&UXH_IuK_kK^K<@xn<94APv}j2LdxV>R_Od+yE;$e -tVj+|Q6hJI>(@iSs|#p8u=G{b8l=lnAAl=zTp4YMmn7wsaPE9yNysV57aXab0r87DZeR?Ah0+jekiw) -nD(Yew`Qs-g -olK1&<9^75c35$g+T1eVjPUN+cme&r&o2Ejufb1eAA$*iQyv`QYf~gRki6w>cXYsW;1a{9sVI39=1OM -??&%JN-ru8qPge!sDNV9h=o@1Vl`gedjCatmbbbLrH@xfv)6IlTieo4ZEb;ju^t}uCw~WKrT3Y9{RaV -88xh9ezQbe24oMi%b+65^?%Ma!vHRb8*;mfv(*ix;=&Ocb=vz)c4+X%JBPV#rCNhPF6*uAILRkakThot^w8gDCc_pjK9QM@Qsok}j-$gDI@2fzfG}A< -&F?~MURPwC?A%_BTPrcyaH`hEc2mzl?Qjx2G@gR=vK0MuRuAzp!LmxWcV6JGagZxQogACzOA7S8hwOX4E$OO%(7m1mjcvNl%}_*4{S>9e$SP?F4`#YmI>2{&sghY~) -CK#=_Ng)QZ`j(V`4H;Ynn1d0b637@UpwZOQJ{TTN0RmC6uw?_AQ=P#@U7>2K@fVsPx_wm%Q~sz^<{U< -EL0VaJw$BVgg8KvCf>y>31rY7_>`oF!D3Iu4n_WKlt0OmJT9XT&qOWrt|s~_AL@K2*R#bmVo5G&(Lmk -#a0}Lg+Xu!)($mWh{Rr4ddY`jQzw|N2#>sM -*~PlwHk%&&X_xp0D(XH{};1=*24ZjZgTuH-}B{foqjA6#WSD9E8Q>p4d_9gHY!3I+qz^i_9(RMWly=8 -e7nY2#O`qPz4PtQob^~%Z-<*@gn0=UzicIjAesDhMBAfGyUNKpTn;WXiZanCRPLAImZ-nCBLA7PU7Bs -4!|=)}97On}5Y7~G*CJ|7e0rFth%{3@8~BVeiH4R0Mrjay*jyO!=c?_m^YEJjcwN$2e_D$hT~rx;CAt -3Sb)0IZ&5#W0rLcJ6k>_Gokm*MH$IR~&WwV98K90O7|4_!YF21VBNoGkyKpZgmn&PgqL_){2-ffMg%# -vX>4zVVkcLe$8BH1V2k6))S_r0>W5B*Um)CU*)m0n^^ai(vbdISkM%iRFzR2Rdq&-&kAXtw)%4|>gQ;l_c&io9vADyWR@rj7}Bz;Ay8EyEDWg -iEu%_BbwF9~U^bjJ5xY+}?0=y9QWIe6ojH_e54$iTx3Y|}pI9mD&LPw2lcpfBH3kTOO^S7!ApGr}*Vl -}n-M%X^@vNQq)vY|^(6F9({ld?IpY*eB?L`EX+6g9wKCqkEg*M@!J8btwK9A8Q0$RQjwi4d -Xj%GNqr!Dn(c@716(2;07+)j3YaHw$mWM|Bh_TtGJn7H!g1|-f73< -aya3!>Gg!?Rtkbjc>yec -WXYhG -?18ImA{A7~h=Hqd)y~J}+FhGAP+UW9cI3|+nO|V}^G2zBwHZQE1`g&+5pM2oW?V*h<}x7w$+j+fB0kH -kM;4_e!0M*)kY8_Oe<7Cn2RAk0n28#q!d##fg$LcFkPfpmXBzNn2qt^tjf*hQwF+-)3IX58H^hD)zAh -d~msEIM3&F#wVlWE1H{%HH)}bxi!zh))VG+bGcaPn?LvZ|k@rJCKUkzm$4*^eJtv#K!77C8TbXZbX5S -0DJSD@3lqD3GU2ikpp77u(k=Pj@Qw0`4Cgx3|ZH%S%MBdp!q=rVJdr_0g-1>*F;yS5KTa}qvVyNGvNd -4Fo18zf{lMAurpSb9!Dh&VMCN6R@?4iL9!!^F!)Z|6cBHZ}Uw&HuQP{4y-sGH?K{lEar#Qss4RU^r|!gziZ3j8;g{%D~2YSw@@;GRYy%p+ -C`+d%0Kp)V`b>uh#+LK!wVDD>AJhqmnbr$zsZ_WsR{)=UD$^X_oPkheWldvrOgI?-Co11X`IpO?Em5+ -{k)p1$Q@``hGq|Km%1{~x6|FMyxC6^A|AM+{!mMy}8VHm_#{FXRPKE{TNR)&?=n`7+_By|~Ff;AiD!3 -oKua?S7nHhw!Wrv`uh5QkyVHL22=Nk22sI4`sk6<14@(yKQ%WUVJ^Vw*S1hW^Xw5ty+~#>XsK`M58&8 -xh9nmKBWW;WIig&IwmA?RIbhzX-SCqi1zuc`6s~3ZiL;5qRFA7f&j{quS5P}qJUK){jqbx2bxulQ*3z -?N{}A`KfVXO&bbd}cmuicrAhr5);OM_PvupIXNZa~>WL5!D}Jhu(hh!JqRN2U#h7V`)Ox97e -|zZGv6FKO9VU*~@4(%@5{^N(WimVZ={Q|4(Pqm(8;gWm#s4-nOlTCpgyN%Q(Si1uIo-C8dVj;GvSLd& -NA%(67|=2i}(IM)`-n%d^qCl$_k089hUz3wSqKj#RIZ9+&OB;biC9aXI@!1B5%A_<3%Bp#AfD?A3&aO -4Ry9r>kpqV{5CHju|u_*j5g8QHwDFh;Od2pKXDC_MJknSW(>earmAuFd7`5yS2GH6RGA29+H6povS%6 -5}QP2_%QGY>Y)=%@H+_lc`g3~F|L0!g1o@>n!ZwY;u?!0?a2uhyP2}`TUa$W=%8trR3#&bZ&~j7b(o( -YtoVWUwBlhI#`v}P?y}ZlawItEddMqNh1D~}!4vhkDRBnDk=3d^36=uXD7KXH!|0xJ;6q1SBNccU!D= -9+YpY+DYqy=SQJNw&!8la$A;&?th)H)C|9oWqVI#^WO+U^QU+f7<-Fbfr%Nu?rfQuEiC%cFY`*b!CF* -mJq$O1^)H(MS8eK5LBsYd=7zwT@z&k8M~J;WxXddgDTIZ5XUfFX`1LTfKjJz2H@R~e18Ly@0vS%1K72 -W)D`rYQ&S-Czj*)UHg!~UJ@K^ssSNiA}bBF{;lO#md0}9j-dVY#;n}ie|VZaLRvCpA^*e6J-xTWIE!>Q#s3!d&?ba~dAe_lmCfvUI>zRD!Xl)q)8 -|VA-HIY&Jgm1Y70r+xjQp&)KGB9L`1}~5IvMytt4)*ZzS_GDcFJvD>*!v2t;fWV6Kx5}oThMZtCC=^38qy; -5h5&!)*|QYW;x$?~?;MlI=P6D%0?HLAwaipK1AU;nG -kv8?#(d_%=1C-<5p^Fn1te2AY-%(69)?PV>nhD{ssj`ua50dgEMWKV_2wt+Umv}?4Nd(Mc%{4asEJ6M8|+GDRN?y- -E^v8JO928D0~7!N00;mWhh<1hNu*^n0RR9<0ssIH00000000000001_fs73R0B~t=FJE76VQFq(UoLQ -YP)h*<6ay3h000O87>8v@NG{~WwG03Nr7Qpd5&!@I0000000000q=D=X003}la4%wEb7f<1ZEaz0WG- --dP)h*<6ay3h000O87>8v@+AzdDrVIc8;V1wA5dZ)H0000000000q=C^J003}la4%wEb7gR0a&u*JE^ -v8JO928D0~7!N00;mWhh<3B%D*AJ5dZ)MJ^%m_00000000000001_fu<+`0B~t=FJoOm00000000000001_f$v=a0B~t=FJ*XRWpH$9Z*FrgaCuN -m0Rj{Q6aWAK2mly|Wk~ygfqSAS002jk000aC0000000000005+cu4n)NaA|NaX>Md?crI{xP)h*<6ay -3h000O87>8v@bW!e_lL!C+02TlM5C8xG0000000000q=9;n003}la4&3cV_|e@Z*FrgaCuNm0Rj{Q6a -WAK2mly|Wk|@tkqs>m001^D000pH0000000000005+cJDC6gaA|NaaAj~bGBtEzXLBxac~DCM0u%!j0 -000802qg5NZYJ;_I&^V0Gt2-022TJ00000000000HlGJr~m+PX>c!dbYXOLb6;a`WMy+MaCuNm0Rj{Q -6aWAK2mly|Wk^>v3%YtH003Bh000aC0000000000005+cMX3M)aA|NacW7m0Y%XwlP)h*<6ay3h000O -87>8v@nYvru3kCoHClUYv7XSbN0000000000q=D(r003}la4%nWWo~3|axY(BX>MtBUtcb8c~DCM0u% -!j0000802qg5NbNO<|2jJW03bL402lxO00000000000HlE_)&Kx-X>c!Jc4cm4Z*nhbaA9O*a%FRKE^ -v8JO928D0~7!N00;mWhh<3T|Hs8ejsO7rXaWEj00000000000001_fte8j0B~t=FJE?LZe(wAFK~HqV -RCb6Zf7oVc~DCM0u%!j0000802qg5NMIfZjb;G=00jd802lxO00000000000HlEwpaB4IX>c!Jc4cm4 -Z*nhkWi57PZe(wAE^v8JO928D0~7!N00;mWhh<11d%OzI2><}}CIA2z00000000000001_fv}+g0B~t -=FJE?LZe(wAFLGsca(QWPXD)DgP)h*<6ay3h000O87>8v@nOIF)NF4wGl63$85&!@I0000000000q=B -@n0RV7ma4%nWWo~3|axZgfcrI{xP)h*<6ay3h000O87>8v@cLsvS0s#O3Gy(tsA^-pY0000000000q= -78S0RV7ma4%nWWo~3|axY(PVRCC_a%^d0FJE72ZfSI1UoLQYP)h*<6ay3h000O87>8v@E)(3O#svTXi -w^(*ApigX0000000000q=9bA0RV7ma4%nWWo~3|axY(PVRCC_a%^d0FKuCRYh`kCE^v8JO928D0~7!N -00;mWhh<1k04`9j0000k0RR9b00000000000001_fpgFS0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gD -5X>MtBUtcb8c~DCM0u%!j0000802qg5NZM3Jr2_&004N0j03ZMW00000000000HlFq(E$K(X>c!Jc4c -m4Z*nhVVPj}zV{dMBa&K%eUt?`#E^v8JO928D0~7!N00;mWhh<3dkaS%61ONbJ5C8xp000000000000 -01_fuz#`0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gJCVQ_S1axQRrP)h*<6ay3h000O87>8v@5~{}VE -CB!j76SkPApigX0000000000q=DYp0RV7ma4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zE^v8JO928D -0~7!N00;mWhh<245=-#T0001>0RR9Z00000000000001_fl1l{0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^ -QY%gPPZE#_9E^v8JO928D0~7!N00;mWhh<2|ZU^lH4*&qfG5`Q300000000000001_fmhoB0B~t=FJE -?LZe(wAFJob2Xk}w>Zgg^QY%gPPZgg^QY;0w6E^v8JO928D0~7!N00;mWhh<1X0weXI0{{Sc2mk;l00 -000000000001_fu8CC0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gYMY-M+HVQ_F|axQRrP)h*<6ay3h0 -00O87>8v@4pbie!36*SEf4?zCIA2c0000000000q=AI)0RV7ma4%nWWo~3|axY_HV`yb#Z*FvQZ)`7U -Wp#3Cb98BAb1rasP)h*<6ay3h000O87>8v@+l{C1;s*c#p%?%FB>(^b0000000000q=AX_0RV7ma4%n -WWo~3|axY_HV`yb#Z*FvQZ)`7fWpZg@Y-xIBE^v8JO928D0~7!N00;mWhh<2Md&NS$0002;0RR9a000 -00000000001_fv)@k0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%h0mVQ_F|axQRrP)h*<6ay3h000O87> -8v@o*h>F%K!iXaRC4TD*ylh0000000000q=Bpb0RV7ma4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zb -1z?CX>MtBUtcb8c~DCM0u%!j0000802qg5NWrHD|62tB0L%>l04)Fj00000000000HlGx{s91RX>c!J -c4cm4Z*nhVVPj}zV{dMBa&K%eV_{=xWpgiPX>4U*V_{=xWiD`eP)h*<6ay3h000O87>8v@(}>1YrU3u -|%>w`cE&u=k0000000000q=97w0swGna4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zb1!mbWNC9>V_{ -=xWiD`eP)h*<6ay3h000O87>8v@J+sw;vj6}9@Bjb+AOHXW0000000000q=8ok0swGna4%nWWo~3|ax -Y_VY;SU5ZDB88UukY>bYEXCaCuNm0Rj{Q6aWAK2mly|Wk^01r(cT$007(x000~S0000000000005+cN -CyG{aA|NaUv_0~WN&gWV{dG4a$#*@FJW$TX)bViP)h*<6ay3h000O87>8v@Qt3xxUJ3vJNhJUPBLDyZ -0000000000q=5|!0swGna4%nWWo~3|axY_VY;SU5ZDB8AZgXjLZ+B^KGcqo4c~DCM0u%!j0000802qg -5NKvz{bX)=e04E0k03rYY00000000000HlGe6#@WoX>c!Jc4cm4Z*nhVZ)|UJVQpbAX>MtBX<=+>b7d -}Yc~DCM0u%!j0000802qg5NDXbGJxm1v0QD3A0384T00000000000HlFP7yc!Jc4cm4Z*nhVZ) -|UJVQpbAcWG`jGA?j=P)h*<6ay3h000O87>8v@cY9|c;spQzk{AF09{>OV0000000000q=C;J0swGna -4%nWWo~3|axY_VY;SU5ZDB8WX>N37a&0bfc~DCM0u%!j0000802qg5NDkTJ5?uiR07U`-03QGV00000 -000000HlHLBLV<$X>c!Jc4cm4Z*nhWX>)XJX<{#5UukY>bYEXCaCuNm0Rj{Q6aWAK2mly|Wk|wI*vL; -R007jU0012T0000000000005+ch$R95aA|NaUv_0~WN&gWWNCABY-wUIV{dJ6VRSBVc~DCM0u%!j000 -0802qg5NR=n$qQNcz00+VV03QGV00000000000HlEuPyzsOX>c!Jc4cm4Z*nhWX>)XJX<{#AVRT_)VR -L0JaCuNm0Rj{Q6aWAK2mly|Wk})%&Rhu<008h&000~S0000000000005+c5Pt#yaA|NaUv_0~WN&gWW -NCABY-wUIX>Md?crI{xP)h*<6ay3h000O87>8v@PXTaUNHPEbJ;eY39{>OV0000000000q=8eE0swGn -a4%nWWo~3|axY|Qb98KJVlQlOV_|e}a&sc!Jc4cm4Z*nhWX>)XJX<{#JVQy(=Wpi{caCuNm0Rj{Q6aWAK2mly|Wk@{{t^$ -n*004>@0015U0000000000005+cRn`IkaA|NaUv_0~WN&gWWNCABY-wUIZDDe2WpZ;aaCuNm0Rj{Q6a -WAK2mly|Wk?|d40fR-001tR0018V0000000000005+c9^L`~aA|NaUv_0~WN&gWWNCABY-wUIZDn*}W -MOn+E^v8JO928D0~7!N00;mWhh<1}ilb_N3jhFoDF6T<00000000000001_f&2jj0B~t=FJE?LZe(wA -FJx(RbZlv2FLGsbZ*_8GWpgfYc~DCM0u%!j0000802qg5NMOM+MF|oB0Czh803HAU00000000000HlG -q4FdphX>c!Jc4cm4Z*nhWX>)XJX<{#PV{&P5baO6nc~DCM0u%!j0000802qg5NaFr9;HRnp003M802= -@R00000000000HlHa9|HhzX>c!Jc4cm4Z*nhWX>)XJX<{#QGcqn^cx6ya0Rj{Q6aWAK2mly|Wk_sly$ -n^j0001f0RS5S0000000000005+c-O2+1aA|NaUv_0~WN&gWWNCABY-wUIbT%|DWq4&!O928D0~7!N0 -0;mWhh<3N$QxUUIRF5h$^ZZx00000000000001_fop>V0B~t=FJE?LZe(wAFJx(RbZlv2FLiWjY%Xwl -P)h*<6ay3h000O87>8v@ZX30fuo(aVpIiU{9smFU0000000000q=7BH1ORYpa4%nWWo~3|axY|Qb98K -JVlQ@Oa&u{KZZ2?nP)h*<6ay3h000O87>8v@eno%c!Jc4cm4Z*nhWX>)XJX<{#THZ(3}cx6ya0Rj{Q6aWAK2mly|Wk`L%oge=s004uS000~S0 -000000000005+c8v@BkyPY -!vFvP5&-}JDF6Tf0000000000q=6k+1^{qra4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mUtei%X>?y --E^v8JO928D0~7!N00;mWhh<2wiZ5l(0RRBY0{{Re00000000000001_fhSo80B~t=FJE?LZe(wAFJx -(RbZlv2FJEF|V{344a&#|kX>(&PaCuNm0Rj{Q6aWAK2mly|Wk@us(aK01001&%001Wd000000000000 -5+cI9mn)aA|NaUv_0~WN&gWWNCABY-wUIUt(cnYjAIJbT4ygb#!TLE^v8JO928D0~7!N00;mWhh<1_7 -67gG0RR9w3IG5r00000000000001_fyH+Q0B~t=FJE?LZe(wAFJx(RbZlv2FJEF|V{344a&#|qd2?fL -Zf0p`E@NhAP)h*<6ay3h000O87>8v@5kE*E=pFz7PH6xDDgXcg0000000000q=5u_1^{qra4%nWWo~3 -|axY|Qb98KJVlQ7}VPk7>Z*p`mb9r-PZ*FF3XD)DgP)h*<6ay3h000O87>8v@p+h)ZV_5(I(`f+!C;$ -Ke0000000000q=7V<1^{qra4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mbYXI4X>4UKaCuNm0Rj{Q6a -WAK2mly|Wk^n#XWg#>000XE001BW0000000000005+c-SP$iaA|NaUv_0~WN&gWXmo9CHEd~OFJE72Z -fSI1UoLQYP)h*<6ay3h000O87>8v@kCt+f%t-(MSyKT3ApigX0000000000q=Cxx1^{qra4%nWWo~3| -axZ9fZEQ7cX<{#9Z*FsRVQzGDE^v8JO928D0~7!N00;mWhh<0wZXB%tL;wI|$^if)00000000000001 -_fz>_-0B~t=FJE?LZe(wAFKBdaY&C3YVlQZPZEQ7gVRCb2axQRrP)h*<6ay3h000O87>8v@I?pK#1rz -`P#XtZ6AOHXW0000000000q=6NL2LNzsa4%nWWo~3|axZ9fZEQ7cX<{#FXkm0^cx`MhaCuNm0Rj{Q6a -WAK2mly|Wk{WBhzZjk006Oj001KZ0000000000005+cSeOR@aA|NaUv_0~WN&gWXmo9CHEd~OFKKRYb -#!xda%Ev{E^v8JO928D0~7!N00;mWhh<2|Pk2IT6aWB>MF0RH00000000000001_fo!)20B~t=FJE?L -Ze(wAFKBdaY&C3YVlQ)HZfSIBdS!AhaCuNm0Rj{Q6aWAK2mly|Wk^ydbLc!Jc4cm4Z*nhabZu-kY-wUIb#!TLb1rasP)h*<6ay3h00 -0O87>8v@000000ssI200000C;$Ke0000000000q=EbH2LNzsa4%nWWo~3|axZ9fZEQ7cX<{#CX>4?5a -&s?VUukY>bYEXCaCuNm0Rj{Q6aWAK2mly|Wk@1``^1$10015V001Qb0000000000005+cL+=LwaA|Na -Uv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLFJEF|b7d}Yc~DCM0u%!j0000802qg5NPiBHP9OmQ0B`~T05$* -s00000000000HlE%@CN{JX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bVQg?{VPa);X=7n*VRUqIX<~ -JBWpgfYc~DCM0u%!j0000802qg5NELT^I=BJ=0J;hQ05bpp00000000000HlGB@dp5KX>c!Jc4cm4Z* -nhabZu-kY-wUIW@&76WpZ;bX>Mv|V{~6_WprU*V`yP=b7gccaCuNm0Rj{Q6aWAK2mly|Wk?X2%D|ul0 -03|h001Na0000000000005+cnDhq#aA|NaUv_0~WN&gWXmo9CHEd~OFJ@_MbY*gLFKlUUbS`jtP)h*< -6ay3h000O87>8v@(UWfo$p!!b1StRjEC2ui0000000000q=9<)2LNzsa4%nWWo~3|axZ9fZEQ7cX<{# -CX>4?5a&s?laCB*JZeeV6VP|tLaCuNm0Rj{Q6aWAK2mly|Wk@)$@6NWpXZXc~DCM0u%!j0000802qg5NM -p5H70>|y0Cofb04e|g00000000000HlG100;nZX>c!Jc4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJW -pi+0V`VOIc~DCM0u%!j0000802qg5NCGj#0O|n%04o3h00000000000HlGP0tf(bX>c!Jc4cm4 -Z*nhabZu-kY-wUIb7gXAVQgu7WpXcHUukY>bYEXCaCuNm0Rj{Q6aWAK2mly|Wk~n&jZ1b7001{K001! -n0000000000005+c_5%n2aA|NaUv_0~WN&gWXmo9CHEd~OFLPybX<=+>dS!AiXmo9Cb7gXAVQgu7WpX -ZXc~DCM0u%!j0000802qg5NB{r;00IC20000004)Fj00000000000HlGt5eNWqX>c!Jc4cm4Z*nhabZ -u-kY-wUIbaG{7VPs)&bY*gLFJE72ZfSI1UoLQYP)h*<6ay3h000O87>8v@MtBUtcb8c~DCM0u%!j0000802qg5NV^QFvO^940FN~Q04e|g00000000000HlGH83+ -JyX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJEF|b7d}Yc~DCM0u%!j0000802qg5NMR~59G -3?G02Lts04M+e00000000000HlE+CX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5FJy0RE -^v8JO928D0~7!N00;mWhh<1$&wD2X3IG62F#rH600000000000001_f&DKC0B~t=FJE?LZe(wAFKBda -Y&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7E^v8JO928D0~7!N00;mWhh<2UpC$Vx4FCYoHUI!G000000000 -00001_fk`?D0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7Uu<}7Y%XwlP)h*<6ay -3h000O87>8v@pEi9ph6Vrth86$-EC2ui0000000000q=CCf2mo+ta4%nWWo~3|axZ9fZEQ7cX<{#Qa% -E+AVQgzbYEXCaCuNm0Rj{Q6aWAK2mly|Wk}7#G_L{%003qi001cf0000000000005+ci -%tjtaA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJEF|b7d}Yc~DCM0u%!j0000802qg5NS&_< -c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWN&R -QaCuNm0Rj{Q6aWAK2mly|Wk_a8CYkL7008w7001cf0000000000005+c5?2TSaA|NaUv_0~WN&gWXmo -9CHEd~OFLZKcWp`n0Yh`kCFJ*LcWo0gKc~DCM0u%!j0000802qg5NG%w!*TDh+0Okk)05AXm0000000 -0000HlFNTnGSgX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bXJu}4XlZkFa%Ev{E^v8JO928D0 -~7!N00;mWhh<1J9@rfz1^@tP7XSb)00000000000001_fmL4!0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-Z -Wo36^Y-?q5b1!UoZER(9a%E*MaCuNm0Rj{Q6aWAK2mly|Wk_{7M^L;100907001ih0000000000005+ -c$z=!taA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFK~5iY-De3E^v8JO928D0~7!N00;mWhh< -2qRo$qD0002g0000b00000000000001_fzD_M0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZX=N{8UukY>bY -EXCaCuNm0Rj{Q6aWAK2mly|Wk~d5g_C^&005r@001HY0000000000005+cmuUz9aA|NaUv_0~WN&gWX -mo9CHEd~OFLZKgWiMZ1VRL0JaCuNm0Rj{Q6aWAK2mly|Wk@7dovEe)005c<001KZ0000000000005+c -RBH$TaA|NaUv_0~WN&gWXmo9CHEd~OFLZKgWiMo5baH8BE^v8JO928D0~7!N00;mWhh<1hPj1c~0ssK --1^@sb00000000000001_fjeyo0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZX=N{Pc`k5yP)h*<6ay3h000 -O87>8v@^O+-nf(iftnbY -EXCaCuNm0Rj{Q6aWAK2mly|Wk@UD@+Tq#003MH001Na0000000000005+cReA^jaA|NaUv_0~WN&gWY -;R+0W@&6?FKlUUYiw_0Yi4O|WiD`eP)h*<6ay3h000O87>8v@Att(^b0000000000 -q=C782mo+ta4%nWWo~3|axZLeV{2w2mk=e7XSbw00000000000001_fp38b0B~t=FJE?LZe(wAFKlmPYi4O|WiN1PWNdF^Yi4O|WiD`eP)h -*<6ay3h000O87>8v@yM^$*2?YQEf)xM&CIA2c0000000000q=Cwb2mo+ta4%nWWo~3|axZLeV{2w8v@ks}G&Jp%v$I0^s&CjbBd0000000000q=6TY2 -mo+ta4%nWWo~3|axZLeV{2wc!Jc4cm4Z*nhiVPk7yXK8L{FJE6_VsCYHUtcb8c~DCM0u%! -j0000802qg5NP%{i$`}Cv009C303iSX00000000000HlFqln4NDX>c!Jc4cm4Z*nhiVPk7yXK8L{FJE -72ZfSI1UoLQYP)h*<6ay3h000O87>8v@=4ndf%K-oYTmt|AAOHXW0000000000q=CAX2mo+ta4%nWWo -~3|axZXUV{2h&X>MmPUt@1=aA9;VaCuNm0Rj{Q6aWAK2mly|Wk~w{q!M!h004*u001Na00000000000 -05+c!VP|P>XD?rKbaHiLbairNb1rasP)h*<6ay3h000O87>8v@jitS0 -R|xMmPZDDe2WpZ;aaCuNm0Rj{ -Q6aWAK2mly|Wk{|OpYwMH008F@001Qb0000000000005+c4WkGEaA|NaUv_0~WN&gWaA9L>VP|P>XD@ -PPadl~OWo>0{baO6nc~DCM0u%!j0000802qg5NPI -c!Jc4cm4Z*nhiVPk7yXK8L{FLQ8ZV`*k-WpZ;aaCuNm0Rj{Q6aWAK2mly|Wk|*9Cl3(;005-{0015U0 -000000000005+cm%<1DaA|NaUv_0~WN&gWaA9L>VP|P>XD@YhX>4;YaCuNm0Rj{Q6aWAK2mly|Wk?S1 -#-U*i001N{001BW0000000000005+cVP|P>XD@bTa&u{KZZ2?nP)h* -<6ay3h000O87>8v@<8HPM%5nezyQTpEB>(^b0000000000q=AFc2mo+ta4%nWWo~3|axZXeXJ2w?y-E^v8JO928D0~7!N00;mWhh<3mT5*h$1ONcf3jhEh00000000000001_fsjNA0B -~t=FJE?LZe(wAFK}{iXL4n8b1z?CX>MtBUtcb8c~DCM0u%!j0000802qg5NDsqC12hBx0DTGo02=@R0 -0000000000HlFnNeKXOX>c!Jc4cm4Z*nhia&KpHWpi^cVqtPFaCuNm0Rj{Q6aWAK2mly|Wk@Q2@h}eq -007Fa%FRKFJo_YZggdGE^v8JO928D0~7! -N00;mWhh<34><>=+0{{Rf3jhEg00000000000001_ff-Q=0B~t=FJE?LZe(wAFK}{iXL4n8b1!IRY;a -|Ab1rasP)h*<6ay3h000O87>8v@cL-8@=mG!$A_V{d9{>OV0000000000q=8XY2>@_ua4%nWWo~3|ax -ZXlZ)b94b8|0qaA|ICWpXZXc~DCM0u%!j0000802qg5NFF!1U*rP-0M`cq03ZMW00000000000HlF-S -P1}dX>c!Jc4cm4Z*nhkWpQ<7b98erUtei%X>?y-E^v8JO928D0~7!N00;mWhh<2LedO8`5&!^KL;wIF -00000000000001_ftXwg0B~t=FJE?LZe(wAFLGsZb!BsOb1z|JVQ_S1a&slM04o&$02=@R00000000000HlHGZV3QzX>c!Jc4cm4Z*nhkWpQ<7b98erVQ^_KaCuNm0Rj -{Q6aWAK2mly|Wk^n4a3uT)004d+000~S0000000000005+cXLJbwaA|NaUv_0~WN&gWa%FLKWpi|MFJ -X0bXfAMhP)h*<6ay3h000O87>8v@nJPeOk;?)Ah(ZMb9{>OV0000000000q=B7%2>@_ua4%nWWo~3|a -xZdaadl;LbaO9bVPj=-bS`jZZBR=A0u%!j0000802qg5NUbq1j%EP>0A&II0384T00000000000HlFy -NelpRX>c!Jc4cm4Z*nhkWpQ<7b98erV`Xx5b1rasP)h*<6ay3h000O87>8v@RNlqhFaiJoy#)XO9smF -U0000000000q=5`e3;=Lxa4%nWWo~3|axZdaadl;LbaO9bZ*6d4bS`jtP)h*<6ay3h000O87>8v@T0@ -2o4-)_Y>q7tl9{>OV0000000000q=9%%3;=Lxa4%nWWo~3|axZdaadl;LbaO9bZ*Oa9WpgfYc~DCM0u -%!j0000802qg5NTf4CqRImR0N4ru03rYY00000000000HlG$Uc!Jc4cm4Z*nhkWpQ<7b98erW -q4y{aCB*JZgVbhc~DCM0u%!j0000802qg5NL~Wtym|ou0RI920384T00000000000HlG=WDEdsX>c!J -c4cm4Z*nhkWpQ<7b98erXm4+8b1rasP)h*<6ay3h000O87>8v@9zrO+2q6FfU2*^b9smFU000000000 -0q=AEG3;=Lxa4%nWWo~3|axZdaadl;LbaO9lZ)9a`b1rasP)h*<6ay3h000O87>8v@SpPz^=o$b3;9m -d$AOHXW0000000000q=CnW3;=Lxa4%nWWo~3|axZdaadl;LbaO9rWpi_BZ*FrgaCuNm0Rj{Q6aWAK2m -ly|Wk`vp!m9BE007Vo001Na0000000000005+c?V=0-aA|NaUv_0~WN&gWa%FLKWpi|MFLQKZbaiuIV -{c?-b1rasP)h*<6ay3h000O87>8v@Y?BG2Z3F-Sp$h;2A^-pY0000000000q=6u)3;=Lxa4%nWWo~3| -axZdaadl;LbaO9rbaHiLbairNb1rasP)h*<6ay3h000O87>8v@pq;?0h#UX_RaF209RL6T000000000 -0q=C(>3;=Lxa4%nWWo~3|axZdaadl;LbaO9tbZKmJE^v8JO928D0~7!N00;mWhh<2fI48xq0ssJL1po -jk00000000000001_fsn}z0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiIUukY>bYEXCaCuNm0R -j{Q6aWAK2mly|Wk_*sO8kui000jK001!n0000000000005+ckjxAKaA|NaUv_0~WN&gWa%FLKWpi|MF -K}UFYhh<)b1!3PVRB?;bT40DX>MtBUtcb8c~DCM0u%!j0000802qg5NO7FMF?W#w0HQ(x05Jdn00000 -000000HlFz&kO)?X>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6FJftDHD+>UaV~IqP)h* -<6ay3h000O87>8v@{(u<-8v_6Ulm-9*GXMYp0000000000q=7PF4FGUya4%nWWo~3|axZdaadl;LbaO -9oVPk7yXJvCQV`yP=WMyAWpXZXc~DCM0u%!j0000802qg5NO`hXSu+Fx0LTdd05bpp00 -000000000HlGQWDNjtX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6FJow7a%5$6Wn*+Ma -CuNm0Rj{Q6aWAK2mly|Wk~pt!T=5l000Uk0021v0000000000005+cB54f(aA|NaUv_0~WN&gWa%FLK -Wpi|MFK}UFYhh<)b1!3PVRB?;bT4CQVRB??b98cPVs&(BZ*DGdc~DCM0u%!j0000802qg5NI8k6pL7H -O0M8Bp06PEx00000000000HlG3at#1*X>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6FJo -w7a&u*LXL4_KaBy;OVr6nJaCuNm0Rj{Q6aWAK2mly|Wk~oL=!07W003?W001@s0000000000005+cRC -f&kaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4CQVRCb2bZ~NSVr6nJaCuNm0Rj{Q6 -aWAK2mly|Wk|r;5H#Qe000gN0027x0000000000005+c1A7esaA|NaUv_0~WN&gWa%FLKWpi|MFK}UF -Yhh<)b1!3PVRB?;bT4CXWNB_^b97;JWo=<&XlZU`E^v8JO928D0~7!N00;mWhh<10``tNF0ssJo1ONa -p00000000000001_fjoZ=0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>WppoNZ*6d4bS` -jtP)h*<6ay3h000O87>8v@{8hluwgLbEHw6FyG5`Po0000000000q=DLj4FGUya4%nWWo~3|axZdaad -l;LbaO9oVPk7yXJvCQV`yP=WMy8v@s|F^gTaCuNm0Rj{Q6aWAK2mly|Wk{@iyhRHI003SN001-q0000000000005+cRFe$=aA|NaUv_0~WN&gWa -%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4IfV`^}4a&KZ~axQRrP)h*<6ay3h000O87>8v@D&#JauTua3 -nzH}^G5`Po0000000000q=B)R4FGUya4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy8v@1+&KF83O8v -@IgRuU>p}nktBe2uGXMYp0000000000q=B_G4ghdza4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`y -P=WMyX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FJow7a%5$6FK1#hGchu7a&KZ~axQRrP)h*<6ay3h0 -00O87>8v@WppoUX>(?BWpOTWc~DCM0u% -!j0000802qg5NE?OZ$V(Fd0F+Ar051Rl00000000000HlE^c!Jc4cm4Z*nhkWpQ<7b98eraA -9L>VP|D?FJow7a%5$6FKTdOZghAqaCuNm0Rj{Q6aWAK2mly|Wk^U;35*O2008hY0027x00000000000 -05+c!1xXTaA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB?;bT4dSZf9b3Y-eF|X<=?{Z)9a` -E^v8JO928D0~7!N00;mWhh<2dp0{KD3jhE;MF0Rg00000000000001_fh7bF0B~t=FJE?LZe(wAFLGs -Zb!BsOb1!gVV{2h&WpgiMXkl_>WppoWVQyz*d2(rNY-wX{Z)9a`E^v8JO928D0~7!N00;mWhh<0+Aww -Ny2><|8F#rHJ00000000000001_fqD=R0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>Wp -poWVQyz8v@AEJ8-o(TW|HY@-DH~;_u0000000000q=7UV4 -*+m!a4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMyWppoWVQy!1Xk -lq>Z)9a`E^v8JO928D0~7!N00;mWhh<3RnyQ(51^@tX6aWA<00000000000001_fq^;?0B~t=FJE?LZ -e(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>WppoWVRUJ3F>rEkVr6nJaCuNm0Rj{Q6aWAK2mly|Wk{u* -adVvn006`c001}u0000000000005+cPeBg=aA|NaUv_0~WN&gWa%FLKWpi|MFK}UFYhh<)b1!3PVRB? -;bT4gUV`yP=b7gdJa&KZ~axQRrP)h*<6ay3h000O87>8v@&Ww*cSpxt7uLl4CH~;_u0000000000q=7 -<44*+m!a4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMyzDE^v8JO928D0~7!N00;mWhh<3m8Wp&D1^@uL5&!@=0000000000 -0001_fr?fS0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>WppodVq<7wa&u*LaB^>AWpXZ -Xc~DCM0u%!j0000802qg5NTl55za#_z0NV@z05|{u00000000000HlFhT@L_oX>c!Jc4cm4Z*nhkWpQ -<7b98eraA9L>VP|D?FJow7a%5$6FLPpJb7yjIb#QQUZ(?O~E^v8JO928D0~7!N00;mWhh<2YVenS{1O -NcE4gdf%00000000000001_fz)9S0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&WpgiMXkl_>WppodY -H4$Da&KZ~axQRrP)h*<6ay3h000O87>8v@t(-PGR|fz9xEcTeIsgCw0000000000q=6!54*+m!a4%nW -Wo~3|axZdaadl;LbaO9oVPk7yXJvCQV`yP=WMy8v@U;7|y9s~dYTnYdHGXMYp0000000000q=C_H4*+m!a4%nWWo~3|axZdaadl;LbaO9oVPk7yXJv -CQV`yP=WMyAWpXZXc~DCM0u%!j0000802qg5NQVMTd=vx#09XnD05Jdn00000000000H -lFHa}NM;X>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJE72ZfSI1UoLQYP)h*<6ay3h0 -00O87>8v@2|9Mm)(ijuE-3&2H2?qr0000000000q=BP%4*+m!a4%nWWo~3|axZdaadl;LbaO9oVPk7y -XJvCQb#iQMX<{=kUt@1V?GE^v8JO928D0~7!N00;mWhh<1m#^Z$q4FCWxC;$L6000000000 -00001_fy{&t0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyveZ*Fd7V{~b6ZZ2?nP)h -*<6ay3h000O87>8v@-KPC2-yr}1^?3jQHvj+t0000000000q=6xl4*+m!a4%nWWo~3|axZdaadl;Lba -O9oVPk7yXJvCQb#iQMX<{=kV{dM5Wn*+{Z*FjJZ)`4bc~DCM0u%!j0000802qg5NNaLB!s7-206-N00 -5bpp00000000000HlFcv=0DqX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJ*XRWpH$9 -Z*FrgaCuNm0Rj{Q6aWAK2mly|Wk^%Y8e< -{w00000000000001_f$qW&0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvhX>4V1Z* -z1maCuNm0Rj{Q6aWAK2mly|Wk@?i0cc4J0090Z001-q0000000000005+cjK&WDaA|NaUv_0~WN&gWa -%FLKWpi|MFK}UFYhh<)b1!vrY;0*_GcRy&Z)|O0ZeeF-axQRrP)h*<6ay3h000O87>8v@eKT~fmc!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFJo_Rb -aH88FJE72ZfSI1UoLQYP)h*<6ay3h000O87>8v@VJexA&IkYiYaIXpIsgCw0000000000q=8oM4*+m! -a4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kV{dMBa%o~OVQ_F|Zf9w3WiD`eP)h*<6ay3 -h000O87>8v@4{E)Ss|5f6!Vv%fIRF3v0000000000q=9_*4*+m!a4%nWWo~3|axZdaadl;LbaO9oVPk -7yXJvCQb#iQMX<{=kV{dMBa%o~OZggyIaBpvHE^v8JO928D0~7!N00;mWhh<2_o?+-(4FCXcCjbCC00 -000000000001_fqwlD0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyveZ*FvQX<{#Md -2euKZgX>NE^v8JO928D0~7!N00;mWhh<2QU7&nW0001O0000w00000000000001_fiMdY0B~t=FJE?L -Ze(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvrVPk7yXJvCQUtei%X>?y-E^v8JO928D0~7!N00;m -Whh<1c!KP>M3IG7-A^-qB00000000000001_f!GTW0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpg -iea%^mAVlyvrVPk7yXJvCQZ*pX1a%E&+WNBk`E^v8JO928D0~7!N00;mWhh<39SRS!l4gdgbEdT&D00 -000000000001_fg~3Y0B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvrVPk7yXJvCQb -7^=kaCuNm0Rj{Q6aWAK2mly|Wk?XPXyx4i007JZ002z@0000000000005+c(MtBUtcb -8c~DCM0u%!j0000802qg5NUhYI8w3Ub0KyIc08{_~00000000000HlE%ClCN|X>c!Jc4cm4Z*nhkWpQ -<7b98eraA9L>VP|D?FLiQkY-wUMFK}UFYhh<)b1!pqY+r3*bYo~=Xm4|LZeeX@FJEbGaBO95Wo~p~bZ -KvHE^v8JO928D0~7!N00;mWhh<3GK<$%Z0RRA20{{Rv00000000000001_fsri`0B~t=FJE?LZe(wAF -LGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyvwbZKlaUtei%X>?y-E^v8JO928D0~7!N00;mWhh<3E&kLbA -1pojv4FCW*00000000000001_fkrP70B~t=FJE?LZe(wAFLGsZb!BsOb1!gVV{2h&Wpgiea%^mAVlyv -wbZKlaV{dM5Wn*+{Z*DGdc~DCM0u%!j0000802qg5NX)^!KH35R08j`305t#r00000000000HlG`G!O -uAX>c!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFLiWjY%g+Uadl;LbS`jtP)h*<6ay3h00 -0O87>8v@)9}{;q5}W`cL)FgHUIzs0000000000q=5lA5CCv#a4%nWWo~3|axZdaadl;LbaO9oVPk7yX -JvCQb#iQMX<{=kb#!TLFLGsbaBpsNWiD`eP)h*<6ay3h000O87>8v@9oBWKWDEcR{U!haGXMYp00000 -00000q=EN45CCv#a4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCQb#iQMX<{=kb#!TLFLGsca(OOrc~DC -M0u%!j0000802qg5NJfo?5KRvN0EsLB05Sjo00000000000HlGeNDu&UX>c!Jc4cm4Z*nhkWpQ<7b98 -eraA9L>VP|D?FLiQkY-wUMFLiWjY%gc!Jc4cm4Z*nhkWpQ<7b98eraA9L>VP|D?FLiQkY-wUMFLiWjY%g?aZDntDb -S`jtP)h*<6ay3h000O87>8v@XPIj@4+sDN_Z9#EF#rGn0000000000q=D>X5CCv#a4%nWWo~3|axZda -adl;LbaO9oVPk7yXJvCQb#iQMX<{=kb#!TLFLiQkE^v8JO928D0~7!N00;mWhh<3lo+h=E0ssJE2mk; -W00000000000001_fl6!;0B~t=FJo_QZDDR?b1z?CX>MtBUtcb8c~DCM0u%!j0000802qg5NSGvFLGc -0r0IvrC02%-Q00000000000HlEvZx8@*X>c!NZ*6U1Ze(*WV{dJ6Y-Mz5Z*DGdc~DCM0u%!j0000802 -qg5NMDYCn`s3A0K^gi02lxO00000000000HlFDau5J;X>c!NZ*6U1Ze(*WWN&wFY;R#?E^v8JO928D0 -~7!N00;mWhh<2d;BB130{{Tl3IG5W00000000000001_f!lWw0B~t=FJo_QZDDR?b1!CcWo3G0E^v8J -O928D0~7!N00;mWhh<3kiR`4k0ssI#1^@sQ00000000000001_fzf*q0B~t=FJo_QZDDR?b1!INb7(G -bc~DCM0u%!j0000802qg5NFEl@ovHx<0M-Kl02BZK00000000000HlGyeh>g~X>c!NZ*6U1Ze(*WXk~ -10E^v8JO928D0~7!N00;mWhh<1iN_{}o4FCXpIsgC{00000000000001_ft-L40B~t=FJo_QZDDR?b1 -!Lbb97;BY%XwlP)h*<6ay3h000O87>8v@#0hnoA_o8f^c(;H6aWAK0000000000q=BW55CCv#a4%zTZ -Eaz0WOFZUX>)WgaCuNm0Rj{Q6aWAK2mly|Wk_e);O~M4005~H000&M0000000000005+c`IQg=aA|Na -V{dJ3VQyq|FLPyKa${&NaCuNm0Rj{Q6aWAK2mly|Wk`_8RAB7}006fZ000yK0000000000005+cubdD -7aA|NaV{dJ3VQyq|FLP*bcP?;wP)h*<6ay3h000O87>8v@tBDyu&;tMfL<;}_82|tP0000000000q=C -+(5CCv#a4%zTZEaz0WOFZdZfS0FbYX04E^v8JO928D0~7!N00;mWhh<3YVc>?^2LJ$Q9RL6o0000000 -0000001_fz_uF0B~t=FJo_QZDDR?b1!#jWo2wGaCuNm0Rj{Q6aWAK2mly|Wk@y+(}?N@002D_000&M0 -000000000005+c;;#?@aA|NaV{dJ6VRSEFUukY>bYEXCaCuNm0Rj{Q6aWAK2mly|Wk`aerI$?=001~z -000;O0000000000005+c0JjhTaA|NaV{dJ6VRSEKX=8L_Z*FF3XD)DgP)h*<6ay3h000O87>8v@+ZWg -^AO-*c4iEqU82|tP0000000000q=AFX5CCv#a4%zTZE#_9FK=>WWpZU?WNBk`E^v8JO928D0~7!N00; -mWhh<2r8fUyg0000-0000M00000000000001_f!fj#0B~t=FKusRWo&aVUtei%X>?y-E^v8JO928D0~ -7!N00;mWhh<1;PBLxb0002z0RR9K00000000000001_flt#A0B~t=FKusRWo&aVX>Md?crI{xP)h*<6 -ay3h000O87>8v@000000ssI2000008UO$Q0000000000q=8}75CCv#a4&CgWpZJ3X>V?GFJE72ZfSI1 -UoLQYP)h*<6ay3h000O87>8v@7(YXzQv?72&<_9r7ytkO0000000000q=B2%5CCv#a4&CgWpZJ3X>V? -GFJ^LOWqM^UaCuNm0Rj{Q6aWAK2mly|Wk_6U(iwmN000yL000vJ0000000000005+cB-s!EaA|Naa%F -KZUtei%X>?y-E^v8JO928D0~7!N00;mWhh<2Ffb8jM4FCX`E&u=%00000000000001_fz{a%0B~t=FL -GsZFLGsZUuJ1+WiD`eP)h*<6ay3h000O87>8v@gLngh0WAOk6|w*T761SM0000000000q=9ee5CCv#a -4&LYaW8UZabIa}b97;BY%XwlP)h*<6ay3h000O87>8v@aV6nM1t0(bIDP;C5&!@I0000000000q=BRq -5dd&$a4&LYaW8UZabI&~bS`jtP)h*<6ay3h000O87>8v@1xCbhPzV43@frXC7ytkO0000000000q=DH -q5dd&$a4&LYaW8UZabIN0LVQg$JaCuNm0Rj{Q6aWAK2mly|Wk}Z|2_Ui|004_?000#L00000000 -00005+cUp)~3aA|Nab#!TLb1z?CX>MtBUtcb8c~DCM0u%!j0000802qg5NKHPJqDu(?0N5S?02BZK00 -000000000HlFLUl9OsX>c!fbZKmJFJW+SWNC79E^v8JO928D0~7!N00;mWhh<333{5To0ssIY1pojN0 -0000000000001_fx&1I0B~t=FLiWjY;!MSb!lv5E^v8JO928D0~7!N00;mWhh<0+=*X>c!fbZKmJFJ*3HZ)9n1XD)DgP)h*<6ay3h000O87>8v@@4;ZW@&6?b9r-gWo<5Sc~DCM0u%!j0000802 -qg5NU1dG1bqYm05S^z022TJ00000000000HlFkaS;G;X>c!fbZKmJFKA(NXk~LQaCuNm0Rj{Q6aWAK2 -mly|Wk_cx*Lbr80093C000yK0000000000005+c3w99zaA|Nab#!TLb1!UfXJ=_{XD)DgP)h*<6ay3h -000O87>8v@NL1MBi3R`wPZa4;ZZ*_EJVRU6=E^v8JO928 -D0~7!N00;mWhh<1aDh6Y$0002+0000T00000000000001_fvkZM0B~t=FLiWjY;!MjWps6LbZ>8Lb6; -Y0X>4RJaCuNm0Rj{Q6aWAK2mly|Wk}`Xkd8GIS4FCW=FaQ7(00000000000001_fg+I+0B~t=FLq;dFJE72Z -fSI1UoLQYP)h*<6ay3h000O87>8v@`@qb_GX($u+YSH#5dZ)H0000000000q=7u25dd&$a4&Xab1!0H -dSPL5E^v8JO928D0~7!N00;mWhh<2dtfO+}3jhG>CjbBr00000000000001_fu5uh0B~t=FLq;dFK20 -VE^v8JO928D0~7!N00;mWhh<24Vrm^>1ONb#4FCWX00000000000001_fv>O;0B~t=FLq;dFKuOVV|8 -+AVQemNc~DCM0u%!j0000802qg5NExwS)msVx0Pi6H02KfL00000000000HlFJwGjYtX>c!gV{ct#E-@}JE@WwQbS-IaW^XTLZgg^aUvO_}Zgg`lba-@7O928D0~7!N00;mZhh<1 -Krn6jT0ssK%1pojc00000000000001_fpEeR0B~t=EjTVQE-)@+X>)WfX>Mk3FKuOXVPs)+VJ>QOZ*E -Xa0Rj{Q6aWAK2ml+0Wk|;#m%;`B000C4001EX0000000000005+c6~z$%aA|NYI4&_RFfL?ib960fZf -0*UbZ>B9Y-M(3Y%X+obWlqH0u%!j0000802_y8NUPg|6JG!T0B!&P02u%P00000000000HlFh#Ss8-X ->ct#E-@}JE@WwQbS-IaW^XT7NJT|VP)h*<6ay3h000O88;4~`PLE`xYy$uQItc&(8~^|S0000000000 -q=E3o5dd&$a4k44F)lDJWNCABEop9MZ!b+nR6#^RR6$Tn0Rj{Q6aWAK2ml+0Wk^aClT-vO0016a000^ -Q0000000000005+cmdOzSaA|NYI4&_RFfL?ib960fZf0*UQbj{gQbbTo1qJ{B008#@_W-0%007eU5dZ -)H -""" - - -if __name__ == "__main__": - main() diff --git a/tools/gpio_0.bat b/tools/gpio_0.bat deleted file mode 100644 index d8fa62b8..00000000 --- a/tools/gpio_0.bat +++ /dev/null @@ -1 +0,0 @@ -curl --digest -u admin:441b1702 -X PUT http://192.168.1.1/api/control/gpio/CGPIO_CONNECTOR_OUTPUT -d data="0" diff --git a/tools/gpio_1.bat b/tools/gpio_1.bat deleted file mode 100644 index 5e336ffb..00000000 --- a/tools/gpio_1.bat +++ /dev/null @@ -1 +0,0 @@ -curl --digest -u admin:441b1702 -X PUT http://192.168.1.1/api/control/gpio/CGPIO_CONNECTOR_OUTPUT -d data="1" \ No newline at end of file diff --git a/tools/install.bat b/tools/install.bat deleted file mode 100644 index 9ff9b624..00000000 --- a/tools/install.bat +++ /dev/null @@ -1 +0,0 @@ -pscp.exe -pw 441b1702 -v RouterSDKDemo.tar.gz admin@192.168.1.1:/app_upload \ No newline at end of file diff --git a/tools/ll.bat b/tools/ll.bat deleted file mode 100644 index 90f58098..00000000 --- a/tools/ll.bat +++ /dev/null @@ -1 +0,0 @@ -dir /O %1 %2 %3 diff --git a/tools/ls.bat b/tools/ls.bat deleted file mode 100644 index 90b66e50..00000000 --- a/tools/ls.bat +++ /dev/null @@ -1 +0,0 @@ -dir /O /D %1 %2 %3 diff --git a/tools/make_load_settings.py b/tools/make_load_settings.py deleted file mode 100644 index 756698e0..00000000 --- a/tools/make_load_settings.py +++ /dev/null @@ -1,347 +0,0 @@ -""" -Fancier, "Load settings", including converting a "settings.ini" into -settings.json - -The Router SDK Sample design is the following: -1) confirm ./{project}/settings.ini exists, and there is [application] section -2) if ./{project}/settings.ini, [application] section lacks "uuid", add one -3) if ./{project}/settings.ini, [application] section lacks "version", add 1.0 -4) if make has '-i' option, the open ./{project}/settings.ini and increment - "version" minor value -5) look for ./config/settings.ini, if found read this in -6) look for ./{project}/settings.ini, read this in, smartly over-laying - sections from ./config -7) return the final settings as python dictionary (not saved!) -""" -import logging -import os -import os.path -import shutil - -from cp_lib.load_settings_ini import DEF_SETTINGS_FILE_NAME, DEF_INI_EXT, \ - load_sdk_ini_as_dict, SECTION_APPLICATION, SECTION_LOGGING, \ - SECTION_ROUTER_API - -DEF_SAVE_EXT = ".save" - -DEF_VERSION = "1.0" - - -def validate_project_settings(app_dir_path, increment_version=False): - """ - 1) Confirm a project settings.ini exists, else throw exception - 2) confirm it has an [application] section, else throw exception - 3) confirm [application] section has "uuid", add random one if not - 4) confirm [application] section has "version", add "1.0" if not - 5) if increment_version is True, then increment MINOR value of a - pre-existing version - - :param str app_dir_path: the subdirectory path to the settings.ini file - :param bool increment_version: if True, increment MINOR value from version - :return None: - """ - - # 1) Confirm a project settings.ini exists, else throw exception - if not os.path.isdir(app_dir_path): - # validate that {project}.settings.ini exists - raise FileNotFoundError("SDK app directory not found.") - - # 2) confirm it has an [application] section, else throw exception - file_name = os.path.join(app_dir_path, DEF_SETTINGS_FILE_NAME + - DEF_INI_EXT) - logging.debug("Confirm {} has [application] section".format(file_name)) - if not _confirm_has_application_section(file_name): - raise KeyError("SDK app settings.ini requires [application] section") - - # 3) confirm [application] section has "uuid", add random one if not - logging.debug("Confirm {} has 'uuid' value".format(file_name)) - fix_up_uuid(file_name) - - # 4) confirm [application] section has "version", add "1.0" if not - # 5) if increment_version is True, then increment MINOR value of a - # pre-existing version - logging.debug("Confirm {} has 'version' value".format(file_name)) - increment_app_version(file_name, increment_version) - - return - - -def load_settings(app_dir_path=None, file_name=None): - """ - - :param str app_dir_path: relative directory path to the app directory - (ignored if None) - :param str file_name: pass in alternative name - mainly for testing, - else use DEF_FILE_NAME - :return dict: the settings as Python dictionary - """ - if file_name is None: - file_name = DEF_SETTINGS_FILE_NAME - - _sets = load_sdk_ini_as_dict(app_dir_path, file_name) - - # handle any special processing - if SECTION_APPLICATION in _sets: - _sets = _special_section_application(_sets) - else: - raise KeyError("Application config requires an [application] section") - - if SECTION_LOGGING in _sets: - _sets = _special_section_logging(_sets) - # this doesn't need to exist - - if SECTION_ROUTER_API in _sets: - _sets = _special_section_router_api(_sets) - # this doesn't need to exist - - return _sets - - -def _special_section_application(sets): - """Handle any special processing for the [application] INI section""" - if "name" not in sets[SECTION_APPLICATION]: - raise KeyError("config [application] section requires 'name' setting") - - if "path" not in sets[SECTION_APPLICATION]: - # if no path, assume matches NAME setting - sets[SECTION_APPLICATION]["path"] = sets[SECTION_APPLICATION]["name"] - - return sets - - -def _special_section_logging(sets): - """Handle any special processing for the [logging] INI section""" - return sets - - -def _special_section_router_api(sets): - """Handle any special processing for the [router_api] INI section""" - - # if we have password, but no USER, then assume user is default of "admin" - if "password" in sets[SECTION_ROUTER_API] and \ - "user_name" not in sets[SECTION_ROUTER_API]: - sets[SECTION_ROUTER_API]["user_name"] = "admin" - - return sets - - -def _confirm_has_application_section(ini_name): - """ - Run through the text file, confirm it has [application] section. - Although we desire ONLY [application], we allow INI to have - [Application] or any case-mix - - Throw FileNotFoundError is the file doesn't exist! - - :param str ini_name: the relative file name of the INI to test - :return bool: T if [application] was found, else F - """ - if not os.path.isfile(ini_name): - raise FileNotFoundError( - "Project INI file missing:{}".format(ini_name)) - - # logging.debug("_confirm_has_application_section({})".format(ini_name)) - - file_han = open(ini_name, "r") - try: - for line in file_han: - if _line_find_section(line, 'application'): - # logging.debug("Section [application] was found") - return True - finally: - file_han.close() - - return False - - -def _line_find_section(line, name): - """ - Given a line from file, confirm if this line is like [section], - ignoring case - - :param line name: the source line - :param str name: the section name desired - :return bool: - """ - if line is None: - return False - - # remove all white space - line = line.strip() - if len(line) < 2 or line[0] != '[': - # then line is not [section], so no need to even compare - return False - - return line.lower().startswith('[' + name.lower() + ']') - - -def fix_up_uuid(ini_name, use_uuid=None, backup=False): - """ - Given a new UUID, write into INI (if it exists) - - :param str ini_name: the INI file name (assume exists at this point) - :param str use_uuid: optional UUID string to use - :param bool backup: T to backup INI - :return: - """ - global _uuid - - def _do_my_tweak(old_line): - """ - Create our special line - - :param old_line: - :return str: the line to include in file - """ - import uuid - global _uuid - - if old_line is None or old_line == "": - # we hit end-of-section [application] without finding "uuid" - logging.debug("Fix UUID - adding random, because it is missing") - if use_uuid is None: - _uuid = uuid.uuid4() - return "uuid=%s\n" % _uuid - - elif use_uuid is None: - # logging.debug("Fix UUID - keep it as is") - return old_line + '\n' - - else: # we are replacing the line, but don't care about the old - return "uuid=%s\n" % uuid.uuid4() - - _uuid = use_uuid - - # logging.debug("Fix UUID - try INI as {}".format(ini_name)) - _find_item_in_app_section(ini_name, 'uuid', _do_my_tweak, backup) - return - - -def increment_app_version(ini_name, incr_version=False, backup=False): - """ - Find an existing "version" string as major/minor and incr it - - :param str ini_name: - :param bool incr_version: - :param bool backup: T to backup INI - :return: - """ - global _incr_ver - - def _do_my_tweak(old_line): - """ - Create our special line - - :param old_line: - :return str: the line to include in file - """ - global _incr_ver - from cp_lib.split_version import split_version_string - - if old_line is None or old_line == "": - # we hit end-of-section [application] without finding "version" - # logging.debug("Fix Version - adding, because it is missing") - version = DEF_VERSION - - elif _incr_ver: - # we have old line, so parse it in & incr - offset = old_line.find("=") - assert offset >= 0 - value = old_line[offset + 1:].strip() - major, minor, patch = split_version_string(value) - minor += 1 - version = "%d.%d.%d" % (major, minor, patch) - logging.debug( - "Fix Version - increment from {} to {}".format(value, version)) - - else: - # logging.debug("Fix Version - use old line({})".format(old_line)) - # there is old line, but we're NOT incrementing - return old_line + '\n' - - # here we are changing the old line - either adding, or incrementing - return "version=%s\n" % version - - _incr_ver = incr_version - - # then exists, first walk through, find [application] - # logging.debug("Increment APP version - try INI as {}".format(ini_name)) - _find_item_in_app_section(ini_name, 'version', _do_my_tweak, backup) - - return - - -def _find_item_in_app_section(ini_name, set_name, process, backup=False): - """ - Scan INI file for [application] section, seek either the requested item, - or end of the app section, call 'process' to explain what to do - - :param str ini_name: the INI file name (assume exists at this point) - :param str set_name: - :param process: the process call-back - :param bool backup: T to backup INI - :return: - """ - # logging.debug("Seek tag({}) in {}".format(set_name, ini_name)) - state_start = 0 - state_in_app = 1 - state_past = 2 - - state = state_start - - lines = [] - - file_han = open(ini_name, "r") - for line in file_han: - - if state == state_start: - # seek the [application] section - we have not found yet - if _line_find_section(line, SECTION_APPLICATION): - # logging.debug("Found [application]") - state = state_in_app - - elif state == state_in_app: - # we have seen the [application] section, so seek 'set_name' - # or end of section - clean_line = line.strip() - if len(clean_line) < 1: - result = process(None) - # logging.debug( - # "Hit end of section, add line ({})".format(result.strip())) - lines.append(result) - state = state_past - # continue down & append the blank line - - elif clean_line.startswith(set_name): - result = process(clean_line) - # logging.debug( - # "Found {}, add line ({})".format(set_name, result.strip())) - lines.append(result) - state = state_past - continue # loop up, no append of old data - - else: # we are past our spot - pass - - lines.append(line) - - # handle if we hit EOF before blank line - if not, we should be in - # state 'state_past' - if state == state_in_app: - result = process(None) - # logging.debug("Hit end of file, add line ({})".format(result)) - lines.append(result) - - file_han.close() - - if backup: - shutil.copyfile(ini_name, os.path.join(ini_name + DEF_SAVE_EXT)) - - # rewrite the file - force to Linux - file_han = open(ini_name, 'wb') - for line in lines: - line = line.strip() + '\n' - file_han.write(line.encode()) - file_han.close() - - return diff --git a/tools/make_package_ini.py b/tools/make_package_ini.py deleted file mode 100644 index aee7205e..00000000 --- a/tools/make_package_ini.py +++ /dev/null @@ -1,90 +0,0 @@ - -from cp_lib.split_version import split_version_string -""" -Make a file such as: -[RouterSDKDemo] -uuid=7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c -vendor=Cradlebox -notes=Router SDK Demo Application -firmware_major=6 -firmware_minor=1 -restart=true -reboot=true -version_major=1 -version_minor=6 -""" - -DEF_FILE_NAME = "package.ini" - - -def make_package_ini(sets, output_file_name): - """ - Assuming we have a 'standard' settings file as dict, create the - ECM-expected file. - - :param dict sets: - :param str output_file_name: - :return: - """ - data_lines = [] - app_sets = sets["application"] - - # [RouterSDKDemo] - data_lines.append("[%s]" % app_sets["name"]) - data_lines.append("uuid=%s" % app_sets["uuid"]) - - value = app_sets.get("vendor", "customer") - data_lines.append("vendor=%s" % value) - value = app_sets.get("description", "") - data_lines.append("notes=%s" % value) - - value = app_sets.get("restart", "False").lower() - data_lines.append("restart=%s" % value) - value = app_sets.get("reboot", "False").lower() - data_lines.append("reboot=%s" % value) - value = app_sets.get("auto_start", "True").lower() - data_lines.append("auto_start=%s" % value) - - value = app_sets.get("firmware", "6.1") - major, minor, patch = split_version_string(value) - data_lines.append("firmware_major=%s" % major) - data_lines.append("firmware_minor=%s" % minor) - data_lines.append("firmware_patch=%s" % patch) - - value = app_sets.get("version", "0.1") - major, minor, patch = split_version_string(value) - data_lines.append("version_major=%s" % major) - data_lines.append("version_minor=%s" % minor) - data_lines.append("version_patch=%s" % patch) - - - _han = open(output_file_name, 'w') - for line in data_lines: - _han.write(line + "\n") - _han.close() - - return - - -if __name__ == "__main__": - - import os - - settings = { - "application": { - "name": "tcp_echo", - "uuid": "7042c8fd-fe7a-4846-aed1-e3f8d6a1TEST", - "description": "Basic TCP echo server and client", - "version": "1.3" - }, - "base": { - "vendor": "MySelf, Inc.", - "firmware": "6.1", - "restart": True, - "reboot": True, - } - } - - ini_name = os.path.join("test", DEF_FILE_NAME) - - make_package_ini(settings, ini_name) diff --git a/tools/module_dependency.py b/tools/module_dependency.py deleted file mode 100644 index 81e8a311..00000000 --- a/tools/module_dependency.py +++ /dev/null @@ -1,264 +0,0 @@ -""" -Obtain module dependencies for Cradlepoint SDK -""" - -import os.path - -# from make import EXIT_CODE_MISSING_DEP - - -class BuildDependencyList(object): - - # these are modules on Cradlepoint FW with Python 3.3 (/usr/lib/python3.3) - COMMON_STD_MODULES = [ - "OpenSSL", "__future__", "abc", "argparse", "base64", "bisect", - "calendar", "cgi", "chunk", "cmd", "code", "codecs", "codeop", - "collections", "configparser", "contextlib", "copy", "copyreg", - "ctypes", "datetime", "dateutil", "difflib", "dnslib", "dnsproxy", - "dummy_threading", "email", "encodings", "fnmatch", "functools", - "getopt", "gettext", "glob", "gzip", "hashlib", "heapq", "hmac", - "html", "http", "importlib", "io", "ipaddress", "json", "keyword", - "linecache", "locale", "logging", "lzma", "mailbox", "mimetypes", - "numbers", "os", "pickle", "pkgutil", "platform", "pprint", - "py_compile", "pyrad", "queue", "quopri", "random", "re", "reprlib", - "runpy", "serial", "shlex", "smtplib", "socket", "socketserver", - "sre_compile", "sre_constants", "sre_parse", "ssl", "stat", "string", - "stringprep", "struct", "subprocess", "tarfile", "telnetlib", - "textwrap", "threading", "token", "tokenize", "traceback", "tty", - "types", "urllib", "uu", "uuid", "weakref", "xml", - - # these exist on router, but you probably should not be using! - # are either Cradlepoint-specific, or obsolete, or depend on STDIO - # access you lack; shutil & tempfile in this list because large file - # ops on router flash is risky - "_compat_pickle", "_pyio", "_strptime", "_weakrefset", "bdb", - "compileall", "cProfile", "cp", "cpsite", "dis", "genericpath", - "imp", "inspect", "lib-dynload", "opcode", "pdb", - "posixpath", "shutil", "ssh", "tempfile", "tornado", "warnings", - - # exist, but not in /usr/lib/python3.3? builtin? - # maybe inside cradlepoint.cpython-33m? - "binascii", "errno", "fcntl", "ioctl", "gc", "math", - "pydoc", "select", "sys", "time" - ] - - # others? _ssh.cpython-33m.so, cradlepoint.cpython-33m.so - - # these are used in sys.platform != CP router only - COMMON_PIP = [ - "requests", "requests.auth", "requests.exceptions" - ] - - def __init__(self): - - # # load the CP sample built-in list, will be of form: { - # # "cp_lib.clean_ini": [], - # # "cp_lib.cp_logging": ["cp_lib.hw_status", - # "cp_lib.load_settings"], - # # } - # json_name = os.path.join("tools", "module_dependency.json") - # file_han = open(json_name, "r") - # self._cp_lib_details = json.load(file_han) - # file_han.close() - - self.dep_list = [] - - self.ignore_pip = False - - self.logger = None - - return - - def add_file_dependency(self, file_name=None): - """ - Given a single file which ends in .py, scan for import lines. - - :param str file_name: - :return: - """ - # self.logger.debug("add_file_dependency({0})".format(file_name)) - - if not isinstance(file_name, str): - raise TypeError - - if not os.path.exists(file_name): - raise FileNotFoundError( - "module_dependency: file({}) doesn't exist.".format(file_name)) - - if not os.path.isfile(file_name): - raise FileNotFoundError( - "module_dependency: file({}) doesn't exist.".format(file_name)) - - value = os.path.splitext(file_name) - # should be like ('network\\tcp_echo\\tcp_echo', '.py') or - # ('network\\tcp_echo', '') - # self.logger.debug("value({})".format(value)) - if value[1] != ".py": - # self.logger.debug( - # "module_dependency: file({}) is not PYTHON (.py)normal file.") - return None - - # at this point, the file should be a .PY file at least - file_han = open(file_name) - for line in file_han: - offset = line.find("import") - if offset >= 0: - # then we found a line - tokens = line.split() - - if len(tokens) >= 2 and tokens[0] == "import": - # then like "import os.path" or "import os, sys, socket" - - if tokens[1][-1] != ",": - # then is like "import os.path" - self.logger.debug("add_file_dep:{}".format(tokens[1])) - self.add_if_new(tokens[1]) - - else: # like "import os, sys, socket" - for name in tokens[1:]: - self.logger.debug("token({})".format(name)) - if name[-1] == ',': - value = name[:-1] - else: - value = name - self.add_if_new(value) - - elif len(tokens) >= 4 and tokens[0] == "from" and \ - tokens[2] == "import": - # then is like "from cp_lib.cp_self.logger import - # get_recommended_logger" - self.add_if_new(tokens[1]) - - file_han.close() - - # self.logger.debug("module_dependency: {}".format(self.dep_list)) - return self.dep_list - - def add_if_new(self, new_name): - """ - Given new module, see if already known or to be skipped - - :param str new_name: the gathering list of names - :return int: return count of names added - """ - # self.logger.debug("add_if_new({0})".format(new_name)) - - if new_name in self.COMMON_STD_MODULES: - # scan through existing STD LIB like "self.logger", "sys", "time" - # self.logger.debug("Mod({}) is in std lib.".format(new_name)) - return 0 - - # if self.ignore_pip and new_name in self.COMMON_PIP: - if new_name in self.COMMON_PIP: - # scan through existing STD LIB like "requests" - self.logger.debug("Mod({}) is in PIP lib.".format(new_name)) - return 0 - - # handle importing sub modules, like os.path or self.logger.handlers - if new_name.find('.') >= 0: - # then we have a x.y - name = new_name.split('.') - if name[0] in self.COMMON_STD_MODULES: - # self.logger.debug("Mod({}) is in std lib.".format(new_name)) - return 0 - - if new_name in self.dep_list: - # scan through existing names - self.logger.debug("Mod({}) already known.".format(new_name)) - return 0 - - # if still here, then is a new name - self.logger.debug("Mod({}) is NEW!".format(new_name)) - - # convert from network.tcp_echo.ftplib to network/tcp_echo/ftplib - path_name = new_name.replace('.', os.sep) - - added_count = 0 - if not os.path.isdir(path_name): - # only ADD is not a subdirectory - self.dep_list.append(new_name) - added_count = 1 - - # handle is file or sub-directory - self.logger.info("_add_recurse:{} {}".format(path_name, new_name)) - added_count += self._add_recurse(path_name, new_name) - - return added_count - - def _add_recurse(self, path_name, dot_name): - """ - Assume new_name is like "network/tcp_echo/xmlrpc/" or - "network/tcp_echo/ftplib.py" - - :param str path_name: the path name, like "network/tcp_echo/xmlrpc" - :param str dot_name: the dot name, like "network.tcp_echo.xmlrpc" - :return int: return if files were added - """ - # self.logger.debug( - # "_add_recurse({0},{1})".format(path_name, dot_name)) - - added_count = 0 - if os.path.isdir(path_name): - # then is module, such as xmlrpc, with includes: - # network/tcp_echo/xmlrpc/__init__.py - # network/tcp_echo/xmlrpc/client.py - # network/tcp_echo/xmlrpc/server.py - self.logger.debug("Recurse into directory ({})".format(path_name)) - - dir_list = os.listdir(path_name) - for name in dir_list: - if name == "__pycache__": - self.logger.debug( - " skip known skipper ({})".format(name)) - continue - - if name == "test": - self.logger.debug( - " skip known skipper ({})".format(name)) - continue - - if name[0] == ".": - self.logger.debug( - " skip pattern skipper ({})".format(name)) - continue - - # still here, see if file or subdirectory - file_name = os.path.join(path_name, name) - if os.path.isdir(file_name): - # then another sub-directory - added_count += self._add_recurse( - file_name, dot_name + '.' + name) - - else: # assume is a file? - # for example, name=client.py - if name.endswith(".py"): - self.dep_list.append(file_name) - added_count += 1 - try: - self.logger.debug( - "Recurse into s-file ({})".format(file_name)) - self.add_file_dependency(file_name) - - except FileNotFoundError: - self.logger.error( - "Could NOT find above dependency within" + - "({})".format(file_name)) - # sys.exit(EXIT_CODE_MISSING_DEP) - - else: - # expects network.tcp_echo.xmlrpc.something.txt - value = path_name + os.sep + name - self.logger.debug( - "Add file as dependency({})".format(value)) - self.dep_list.append(value) - added_count += 1 - - else: - # might be file, like network/tcp_echo/ftplib.py as - # network.tcp_echo.ftplib - if not path_name.endswith(".py"): - path_name += ".py" - self.logger.debug("Recurse into d-file ({})".format(path_name)) - self.add_file_dependency(path_name) - - return added_count diff --git a/tools/package_application.py b/tools/package_application.py deleted file mode 100644 index 35ac9295..00000000 --- a/tools/package_application.py +++ /dev/null @@ -1,228 +0,0 @@ -#!tools/bin/python - -# changes by Lynn: -# add the package.ini to the MANIFEST hash table -# in the MANIFEST hash table, force use of linux "/" separator in json text file -# use the Python modules tarfile and gzip, do not 'shell' to Linux -# make sure the 'package'ing directory is in the build subdirectory - -import configparser -import logging -import hashlib -import json -import os -import re -import shutil -import sys -import time -import uuid -from OpenSSL import crypto - -META_DATA_FOLDER = 'METADATA' -CONFIG_FILE = 'package.ini' -SIGNATURE_FILE = 'SIGNATURE.DS' -MANIFEST_FILE = 'MANIFEST.json' - -BYTE_CODE_FILES = re.compile('^.*\.(pyc|pyo|pyd)$') -BYTE_CODE_FOLDERS = re.compile('^(__pycache__)$') - - -def file_checksum(hash_func=hashlib.sha256, file=None): - h = hash_func() - buffer_size = h.block_size * 64 - - with open(file, 'rb') as f: - for buffer in iter(lambda: f.read(buffer_size), b''): - h.update(buffer) - return h.hexdigest() - - -def hash_dir(target, hash_func=hashlib.sha256): - hashed_files = {} - for path, d, f in os.walk(target): - for fl in f: - # if fl == CONFIG_FILE: - # continue - # we need this be LINUX fashion! - if sys.platform == "win32": - # swap the network\\tcp_echo to be network/tcp_echo - fully_qualified_file = path.replace('\\', '/') + '/' + fl - else: # else allow normal method - fully_qualified_file = os.path.join(path, fl) - hashed_files[fully_qualified_file[len(target) + 1:]] =\ - file_checksum(hash_func, fully_qualified_file) - - return hashed_files - - -def pack_package(app_root, app_name): - """ - :param app_root: build\\network\\tcp_echo - :param app_name: tcp_echo - :return: - """ - import tarfile - import gzip - - base = None - if app_root.startswith("build"): - base = "build" - app_root = app_root[6:] - os.chdir("build") - - # we'll make "tcp_echo.tar.gz" - logging.debug("pack TAR:%s.tar" % app_name) - tar_name = "{}.tar".format(app_name) - tar = tarfile.open(tar_name, 'w') - # this will add the sub-directory recursively - tar.add(app_root) - tar.close() - - logging.debug("gzip archive:%s.tar.gz" % app_name) - gzip_name = "{}.tar.gz".format(app_name) - with open(tar_name, 'rb') as f_in: - with gzip.open(gzip_name, 'wb') as f_out: - shutil.copyfileobj(f_in, f_out) - - if base is not None: - os.chdir("..") - - return - - -def create_signature(meta_data_folder, pkey): - manifest_file = os.path.join(meta_data_folder, MANIFEST_FILE) - with open(os.path.join(meta_data_folder, SIGNATURE_FILE), 'wb') as sf: - checksum = file_checksum(hashlib.sha256, manifest_file).encode('utf-8') - if pkey: - sf.write(crypto.sign(pkey, checksum, 'sha256')) - else: - sf.write(checksum) - - -def clean_manifest_folder(app_metadata_folder): - path, dirs, files = next(os.walk(app_metadata_folder)) - - for file in files: - fully_qualified_file = os.path.join(path, file) - os.remove(fully_qualified_file) - - for d in dirs: - shutil.rmtree(os.path.join(path, d)) - - -def clean_bytecode_files(app_root): - for path, dirs, files in os.walk(app_root): - for file in filter(lambda x: BYTE_CODE_FILES.match(x), files): - os.remove(os.path.join(path, file)) - for d in filter(lambda x: BYTE_CODE_FOLDERS.match(x), dirs): - shutil.rmtree(os.path.join(path, d)) - pass - - -def package_application(app_root, pkey): - """ - - :param str app_root: root path, such as "network/tcp_echo" - :param pkey: encryption key - :return: - """ - # app_root = os.path.real path(app_root) - if app_root[-1] == '/': - # this routine (or os.path.basename()) doesn't like trailing dir slash - app_root = app_root[:-1] - logging.debug("app_root:%s" % app_root) - - # expect like "network/tcp_echo/package.ini" - app_config_file = os.path.join(app_root, CONFIG_FILE) - logging.debug("app_config_file:%s" % app_config_file) - - # expect like "network/tcp_echo/METADATA" - app_metadata_folder = os.path.join(app_root, META_DATA_FOLDER) - logging.debug("app_metadata_folder:%s" % app_metadata_folder) - - # expect like "network/tcp_echo/METADATA/MANIFEST.json" - app_manifest_file = os.path.join(app_metadata_folder, MANIFEST_FILE) - logging.debug("app_manifest_file:%s" % app_manifest_file) - - config = configparser.ConfigParser() - config.read(app_config_file) - if not os.path.exists(app_metadata_folder): - os.makedirs(app_metadata_folder) - - for section in config.sections(): - app_name = section - assert os.path.basename(app_root) == app_name - - clean_manifest_folder(app_metadata_folder) - - clean_bytecode_files(app_root) - - pmf = dict() - pmf['version_major'] = int(1) - pmf['version_minor'] = int(0) - - app = dict() - app['name'] = str(section) - logging.debug("app['name']:{}".format(app['name'])) - try: - app['uuid'] = config[section]['uuid'] - except KeyError: - if not pkey: - app['uuid'] = str(uuid.uuid4()) - else: - raise - app['vendor'] = config[section]['vendor'] - app['notes'] = config[section]['notes'] - app['version_major'] = int(config[section]['version_major']) - app['version_minor'] = int(config[section]['version_minor']) - app['firmware_major'] = int(config[section]['firmware_major']) - app['firmware_minor'] = int(config[section]['firmware_minor']) - app['restart'] = config[section].getboolean('restart') - app['reboot'] = config[section].getboolean('reboot') - app['auto_start'] = config[section].getboolean('auto_start') - # this actually makes an INVALID format, because it uses localtime, - # but adds no time-zine offset! - # app['date'] = datetime.datetime.now().isoformat() - app['date'] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) - - data = dict() - data['pmf'] = pmf - data['app'] = app - - app['files'] = hash_dir(app_root) - - with open(app_manifest_file, 'w') as f: - f.write(json.dumps(data, indent=4, sort_keys=True)) - - create_signature(app_metadata_folder, pkey) - - logging.debug("app_root:{}".format(app_root)) - logging.debug("section:{}".format(section)) - pack_package(app_root, section) - - print( - 'Package {}.tar.gz created'.format(section)) - - -def argument_list(args): - print('{} '.format(args[0])) - - -if __name__ == "__main__": - - logging.basicConfig(level=logging.DEBUG) - - if len(sys.argv) < 2: - argument_list(sys.argv) - else: - - private_key = None - if 2 < len(sys.argv): - with open(sys.argv[2], 'r') as pf: - private_key = crypto.load_privatekey( - type=crypto.FILETYPE_PEM, buffer=pf.read(), - passphrase='pass'.encode('utf-8')) - - logging.debug("app:%s" % sys.argv[1]) - package_application(sys.argv[1], private_key) diff --git a/tools/pscp.exe b/tools/pscp.exe deleted file mode 100644 index 81f638e4..00000000 Binary files a/tools/pscp.exe and /dev/null differ diff --git a/tools/py35.bat b/tools/py35.bat deleted file mode 100644 index 7dab2559..00000000 --- a/tools/py35.bat +++ /dev/null @@ -1,5 +0,0 @@ -set PATH=%PATH%;C:\Python35 - -d: -cd \root -set PYTHONPATH=D:\root; diff --git a/tools/set_ip.bat b/tools/set_ip.bat deleted file mode 100644 index 5b7c9b92..00000000 --- a/tools/set_ip.bat +++ /dev/null @@ -1,4 +0,0 @@ -REM ['runas', '/user:lily-lyn\\Administrator', 'netsh', 'interface', 'ip', 'set', 'address', -REM '"Local Addin ENet"', 'static', '192.168.115.6', '255.255.255.0'] - -runas /noprofile /user:lily-lyn\lynn "netsh interface ip set address \"ENet USB-1\" static 192.168.1.6 255.255.255.0" diff --git a/tools/stop.bat b/tools/stop.bat deleted file mode 100644 index 71953090..00000000 --- a/tools/stop.bat +++ /dev/null @@ -1,6 +0,0 @@ -REM curl -s --digest --insecure -u admin:441b1702 -H "Accept: application/json" -X PUT http://192.168.1.1/api/control/system/sdk/action -d "stop 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c" -REM {"data": {"exception": "key", "key": "data"}, "success": false} -REM curl -s --digest --insecure -u admin:441b1702 -H "Accept: application/json" -X PUT http://192.168.1.1/api/control/system/sdk/action -d data="stop 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c" -REM curl -s --digest --insecure -u admin:441b1702 -H "Accept: application/json" -X PUT http://192.168.1.1/api/control/system/sdk/action -d data='"stop 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"' - -curl -s --digest --insecure -u admin:441b1702 -X PUT http://192.168.1.1/api/control/system/sdk/action -d data='{"data:"stop 7042c8fd-fe7a-4846-aed1-e3f8d6a1c91c"}' diff --git a/tools/target.py b/tools/target.py deleted file mode 100644 index 3797de40..00000000 --- a/tools/target.py +++ /dev/null @@ -1,1021 +0,0 @@ -import ipaddress -import logging -import os -import subprocess -import sys - -from cp_lib.app_base import CradlepointAppBase -# from cp_lib.cs_client import CradlepointClient - -DEF_TARGET_INI = "config/target.ini" -DEF_INTERFACE = "Local Area Connection" -DEF_COMPUTER_IP_LAST_OCTET = 6 - -PING_COUNT = 3 - -# set this to True to force the router's syslog message to include -# the UTF8 flag, else set to False -SYSLOG_UTF8_FLAG = False - -# suppress interface show like: Configuration for interface "Loopback ... -LIST_INTERFACE_NO_LOOPBACK = True - -# suppress interface show like: Config for interface "VMware or VirtualBox -LIST_INTERFACE_NO_VM = True - - -class TheTarget(CradlepointAppBase): - - ACTION_DEFAULT = "show" - ACTION_NAMES = ("ping", "reboot", "set", "show", "syslog") - ACTION_HELP = { - "ping": "Ping my router", - "reboot": "Reboot my router", - "set": - "given an alias/nickname, edit the ./config/settings.ini to match", - "show": "Show Interfaces / IP Info", - "syslog": - "Check router 'system logging' - show status, force to be syslog", - } - - def __init__(self): - """Basic Init""" - from cp_lib.load_settings_ini import copy_config_ini_to_json - - if not os.path.isfile("./config/settings.json"): - # make sure we have at least a basic ./config/settings.json - copy_config_ini_to_json() - - # we don't contact router for model/fw - will do in sanity_check, - # IF the command means contact router - CradlepointAppBase.__init__(self, call_router=False, log_name="target") - - self.action = self.ACTION_DEFAULT - - self.verbose = True - - # the one 'target' we are working on (if given) - self.target_alias = None - - # the data loaded from the TARGET.INI file - self.target_dict = None - - # the one computer interface we are targeting - self.target_interface = DEF_INTERFACE - - # save IP related to target and/or the interface - self.target_my_ip = None - self.target_my_net = None - self.target_router_ip = None - - return - - def run(self): - """Dummy to satisfy CradlepointAppBase""" - return - - def main(self): - """ - - :return int: code for sys.exit() - """ - - result = 0 - self.logger.info("Target Action:{0}".format(self.action)) - - # the data loaded from the TARGET.INI file - self.target_dict = self.load_target_ini() - - try: - self.target_interface = self.settings["router_api"]["interface"] - except KeyError: - pass - - self.target_my_ip, self.target_my_net = self.get_interface_ip_info( - self.target_interface) - - self.logger.info("Target Alias:{0}".format(self.target_alias)) - self.logger.info("Target Interface, Name:{0}".format( - self.target_interface)) - self.logger.info("Target Interface, PC IP:{0}".format( - self.target_my_ip)) - self.logger.info("Target Interface, Network:{0}".format( - self.target_my_net)) - - if self.action == "ping": - result = self.action_ping() - - elif self.action == "set": - result = self.action_set() - - elif self.action == "show": - result = self.action_show() - - elif self.action == "syslog": - result = self.action_syslog() - - return result - - def action_ping(self, name=None): - """ - Attempt to ping the router - - :param str name: optional alias name, matching a section in TARGET.INI - """ - if name is None: - # note that target_alias might also be None - name = self.target_alias - - ping_ip = self.get_router_ip(name) - - result = 0 - - if ping_ip is None: - if name is not None: - # then - self.logger.debug( - "PING - assume HOST can resolve name:{}".format(name)) - ping_ip = ipaddress.IPv4Address(name) - else: - self.logger.error("Cannot PING - not enough target info given") - result = -1 - - if ping_ip is not None: - if sys.platform == "win32": - command = ["ping", "-n", str(PING_COUNT), ping_ip.exploded] - else: - command = ["ping", "-c", str(PING_COUNT), ping_ip.exploded] - - result = subprocess.call(command) - if result: - self.logger.error( - "PING {} failed - return code:{}".format(ping_ip, result)) - else: - self.logger.info("PING {} was good".format(ping_ip)) - - return result - - def action_reboot(self, name=None): - """ - Go to our router, and reboot it - - :param str name: optional alias name, matching a section in TARGET.INI - :return int: value intended for sys.exit() - """ - if name is None: - # note that target_alias might also be None - name = self.target_alias - - reboot_ip = self.get_router_ip(name) - - self.logger.info( - "Reboot our active development router({})".format(reboot_ip)) - url = "http://{0}/api/control/system/reboot".format( - self.get_router_ip()) - # we do need the 2nd quotes here - # data = {"data": '\x7A\x16'} - data = {"data": "true"} - - # result = self._do_action(url, data=data, post=True) - # if result == 0: - # self.logger.info("SDK reboot successful") - - return 0 - - def action_set(self, name=None, ini_name=None): - """ - Given a alias from target.ini, edit the ./config/settings.ini to - make the alias the active target - - :param str name: the alias or section name in target.ini to use - :param str ini_name: optionally, change the name of the - ./config/settings.ini (for testing) - :return: - """ - from cp_lib.load_settings_json import DEF_GLOBAL_DIRECTORY, \ - DEF_SETTINGS_FILE_NAME - - if name is None: - # if nothing passed in, assume is the alias from command line - name = self.target_alias - - if ini_name is None: - # if no alternative file name passed in, - # use default of "./config/settings.ini" - ini_name = os.path.join(DEF_GLOBAL_DIRECTORY, - DEF_SETTINGS_FILE_NAME + ".ini") - - if name not in self.target_dict: - self.logger.error("SET failed - target name {} is unknown") - return -1 - - # these are the only supported fields in section [router_api] - user_name = self.target_dict[name].get("user_name", None) - interface = self.target_dict[name].get("interface", None) - local_ip = self.target_dict[name].get("local_ip", None) - password = self.target_dict[name].get("password", None) - - if local_ip is None: - syslog_ip = None - else: - syslog_ip = self.make_computer_ip(local_ip) - - self.logger.debug("SET: open file:{}".format(ini_name)) - try: - with open(ini_name) as _file_han: - source = _file_han.readlines() - - except FileNotFoundError: - self.logger.error( - "SET failed - settings file {} not found".format(ini_name)) - return -1 - - dst = [] - state = "out" - changed = False - for line in source: - value = line.strip() - - if len(value) == 0: - # these always copy verbatim, but we reset to no special mode - state = "out" - - elif value[0] == ';': - # these always copy verbatim, with no change of state - pass - - elif state == "out": - # then seeking our sections - if value.startswith("[router_api"): - state = "api" - - if value.startswith("[logging"): - state = "log" - - else: # assume we're inside on of the sections - - if state == "api": - if user_name is not None and \ - value.startswith("user_name"): - # change out the user_name - value = "user_name={}".format(user_name) - changed = True - - elif interface is not None and \ - value.startswith("interface"): - # change out the interface - value = "interface={}".format(interface) - changed = True - - elif local_ip is not None and \ - value.startswith("local_ip"): - # change out the local_ip - value = "local_ip={}".format(local_ip) - changed = True - - elif password is not None and \ - value.startswith("password"): - # change out the password - value = "password={}".format(password) - changed = True - - elif state == "log": - if syslog_ip is not None and \ - value.startswith("syslog_ip"): - # change out the syslog_ip - value = "syslog_ip={}".format(syslog_ip) - changed = True - - dst.append(value) - - temp = line.strip() - if temp != value: - self.logger.debug("SET: [{}] > [{}]".format(temp, value)) - # else: - # self.logger.debug("SET: [{}] no change".format(value)) - - if not changed: - self.logger.info("SET: nothing changed - doing nothing") - result = 0 - - else: - bak_name = ini_name + ".bak" - if os.access(bak_name, os.F_OK): - # remove any old backup - os.remove(bak_name) - - # save the original - os.rename(ini_name, bak_name) - - # save out the file - self.logger.debug("SET: rewriting file:{}".format(ini_name)) - with open(ini_name, "w") as _file_han: - for line in dst: - _file_han.write(line + '\n') - - # change the computer's IP - if interface is None: - # use the 'new' interface, else our default - interface = self.target_interface - - if syslog_ip is None: - # this is problem - raise ValueError - - result = self.set_computer_ip(interface, syslog_ip) - - return result - - def action_syslog(self, name=None): - if name is None: - # note that target_alias might also be None - name = self.target_alias - - router_ip = self.get_router_ip(name) - - if router_ip is None: - self.logger.error( - "Cannot check Syslog - not enough target info given") - return -1 - - result = self.cs_client.get("config/system/logging") - if not isinstance(result, dict): - # some error? - self.logger.error("get of config/system/logging failed") - return -2 - - # FW 6.1 example of get("config/system/logging") - # {"console": false, "firewall": false, "max_logs": 1000, - # "services": [] - # "modem_debug": {"lvl_error": false, "lvl_info": false, - # "lvl_trace": false, "lvl_warn": false }, - # "enabled": true, - show always be true? - # "level": "debug", - set to "debug" or "info", etc - # "remoteLogging": { - # "enabled": true, - set T to enable remote syslog - # "serverAddr": "192.168.30.6", - set to IP of syslog server - # "system_id": false, - always set False (for now) - # "utf8_bom": false, - always set false (for now) - # }, - - # assume everything is as expected - logging_setting_good = True - - logging_level = result.get("level", None) - self.logger.info("{}: Logging level = {}".format( - router_ip.exploded, logging_level)) - # self.logger.debug("{}:control/system/logging/level = {}".format( - # router_ip.exploded, logging_level)) - - syslog_enabled = False - syslog_ip = None - - if "remoteLogging" not in result: - self.logger.error( - "{}:config/system/logging/remoteLogging is not present".format( - router_ip.exploded)) - logging_setting_good = False - - else: - syslog_enabled = result["remoteLogging"].get("enabled", False) - syslog_ip = result["remoteLogging"].get("serverAddr", None) - - if syslog_enabled: - self.logger.info( - "{}: Remote Syslog logging is enabled".format( - router_ip.exploded)) - # self.logger.debug( - # "{}:control/system/logging/remoteLogging/enabled = True".format( - # router_ip.exploded)) - - if syslog_ip is None: - logging_setting_good = False - else: - self.logger.info("{}: Remote Syslog IP address is {}".format( - router_ip.exploded, syslog_ip)) - # self.logger.debug( - # "{}:control/system/logging/remoteLogging/serverAddr = {}".format( - # router_ip.exploded, syslog_ip)) - else: - self.logger.info( - "{}: Remote Syslog logging is disabled".format( - router_ip.exploded)) - # self.logger.debug( - # "{}:control/system/logging/remoteLogging/enabled = False".format( - # router_ip.exploded)) - logging_setting_good = False - - if not logging_setting_good: - # then at least one setting is not as desired - desired = {"enabled": True, "level": "debug", - "remoteLogging": {"enabled": True, - "utf8_bom": SYSLOG_UTF8_FLAG}} - if syslog_ip is None: - # if no IP, use this computer's IP from the active interface - syslog_ip = self.target_my_ip.explode - - desired["remoteLogging"]["serverAddr"] = syslog_ip - self.logger.debug("desire:{}".format(desired)) - - """ :type self.cs_client: CradlepointClient""" - value = "debug" - result = self.cs_client.put("config/system/logging", - {"level": value}) - if result != value: - self.logger.error( - "PUT logging/level = {} failed, result={}".format( - value, result)) - - # false is 'not a boolean' - # False is 'not a boolean' - # "0" is not a boolean - value = False - result = self.cs_client.put( - "config/system/logging/remoteLogging", {"enabled": value}) - if result != value: - self.logger.error( - "PUT logging/remoteLogging/enabled={}".format(value) + - " failed, result={}".format(result)) - - return 0 - - def action_show(self): - """ - Do simplified dump/display of interfaces, assuming multi-homed computer - - """ - self.logger.info("") - self.logger.info("Show Interfaces:") - - report = self.list_interfaces() - for line in report: - self.logger.info("{}".format(line)) - return 0 - - def load_target_ini(self, ini_name=None): - """ - Convert the ./config/target.ini into a dictionary - - Assume we start with INI like this: - [AER2100] - local_ip=192.168.21.1 - password=4416ec79 - - Want a dict() like this: - { - "AER2100": { - "local_ip": "192.168.21.1", - "password": "4416ec79" - } - } - - :param str ini_name: relative directory path to the INI file - - in None, assume ./config/target.ini - :return dict: the prepared data as dict - """ - import configparser - - if ini_name is None: - ini_name = DEF_TARGET_INI - - target_dict = dict() - - if not os.path.isfile(ini_name): - # if INI file DOES NOT exist, return - existence is not - # this module's responsibility! - self.logger.debug( - "Target INI file {} does NOT exist".format(ini_name)) - return target_dict - - self.logger.debug("Read TARGET.INI file:{}".format(ini_name)) - - # LOAD IN THE INI FILE, using the Python library - target_config = configparser.ConfigParser() - # READ indirectly, config.read() tries to open cp_lib/config/file.ini, - # not config/file.ini - file_han = open(ini_name, "r") - try: - target_config.read_file(file_han) - - except configparser.DuplicateOptionError as e: - self.logger.error(str(e)) - self.logger.error("Aborting TARGET") - sys.exit(-1) - - finally: - file_han.close() - self.logger.debug(" Sections:{}".format(target_config.sections())) - - # convert INI/ConfigParser to Python dictionary - for section in target_config.sections(): - - target_dict[section] = {} - # note: 'section' is the old possibly mixed case name; - # section_tag might be lower case - for key, val in target_config.items(section): - target_dict[section][key] = val - - return target_dict - - def get_router_ip(self, name=None): - """ - Given name, or self.target_interface, try to guess what router IP is - - :param str name: optional alias name - :return: the IP as IPv4Address() object, or None - :rtype: ipaddress.IPv4Address - """ - router_ip = None - - if name is None: - # then just ping the Router on self.target_interface - if self.target_my_net is not None: - self.logger.debug( - "Get Router IP - see if we have router on " + - "interface:{}".format(self.target_interface)) - # walk through our target list looking for first - # router IP on targeted interface, - # which is like: - # "AER2100": { - # "local_ip": "192.168.21.1", - # "password": "4416ec79" - # } - for router in self.target_dict: - # self.logger.debug("Check IP of router:{}".format(router)) - try: - router_ip = ipaddress.IPv4Address( - self.target_dict[router]["local_ip"]) - - except KeyError: - self.logger.warning( - 'Router[{}] lacks an ["local_ip"] value'.format( - router)) - continue - - except ipaddress.AddressValueError: - self.logger.warning( - 'Router[{}] has invalid ["local_ip"] value'.format( - router)) - continue - - if router_ip in self.target_my_net: - # this is first router found in correct subnet, use it - self.logger.debug( - "Found router[{}] with ".format(router) + - "IP[{}] in correct subnet".format(router_ip)) - break - - # else self.logger.debug("try another") - # else leave as None, router_ip = None - - else: # try to find the alias - if name in self.target_dict: - self.logger.debug( - "Get Router IP - try to find IP of alias:{}".format(name)) - try: - router_ip = self.target_dict[name]["local_ip"] - self.logger.debug( - "Get Router IP - alias as IP:{}".format(router_ip)) - router_ip = ipaddress.IPv4Address(router_ip) - - except KeyError: - self.logger.warning( - 'Router[{}] lacks an ["local_ip"] value'.format(name)) - return -1 - - except ipaddress.AddressValueError: - self.logger.warning( - 'Router[{}] has invalid ["local_ip"] value'.format( - name)) - return -1 - - # else leave as None - - return router_ip - - def scan_ini_get_ip_from_name(self, name): - """ - Given a name (a section in target.ini) - return the ["local_ip"] value as IPv4Address object - - :param str name: - :return: - :rtype: ipaddress.IPv4Address, bool - """ - self.logger.debug("Given name:{}, scan for local_ip".format(name)) - - # walk through our target list looking for first router IP on - # targeted interface, - # which is like: - # "AER2100": { - # "local_ip": "192.168.21.1", - # "password": "4416ec79" - # } - router_ip = None - in_subnet = False - if name in self.target_dict: - try: - router_ip = ipaddress.IPv4Address( - self.target_dict[name]["local_ip"]) - - except ipaddress.AddressValueError: - self.logger.warning( - 'Router[{}] has invalid ["local_ip"] value'.format(name)) - - if self.target_my_net is not None: - # check if it is in our subnet - in_subnet = router_ip in self.target_my_net - self.logger.debug( - "IP[{}] is within current subnet".format(router_ip)) - - self.logger.debug( - "Found router[{}] with IP[{}]".format(name, router_ip)) - return router_ip, in_subnet - - def get_interface_ip_info(self, interface_name, report=None): - """ - Given an interface name, return the computer's IP and network info. - - For example if interface_name == "ENet USB-1", returns 192.168.30.6 - [ "", - "Configuration for interface \"ENet MB\"", - " IP Address: 192.168.0.10", - " Subnet Prefix: 192.168.0.0/24", - " Default Gateway: 192.168.0.1", - "", - "Configuration for interface \"ENet USB-1\"", - " IP Address: 192.168.30.6", - " Subnet Prefix: 192.168.30.0/24", - "" - ] - - :param str interface_name: which interface we are targeting - (from TARGET.INI or default) - :param list report: a saved report, if any - """ - if report is None: - report = self.list_interfaces() - - self.logger.debug("Get IP for interface:{}".format(interface_name)) - - my_ip = None - my_net = None - - interface_name = '\"' + interface_name + '\"' - _index = 0 - found_interface = False - while _index < len(report): - value = report[_index].strip() - """ :type value: str """ - if len(value) > 0: - match_cfg = "Configuration for interface" - if value.startswith(match_cfg): - # then we are in the "Configuration for interface ..." - value = value[len(match_cfg):].strip() - if value == interface_name: - # self.logger.debug( - # "Found Cfg interface:{}".format(value)) - found_interface = True - match_ip = "IP Address:" - match_net = "Subnet Prefix:" - while len(report[_index]) > 0: - # walk through this configuration only - _index += 1 - value = report[_index].strip() - # self.logger.debug("loop:{}".format(value)) - - if value.startswith(match_ip): - # like "IP Address: 192.168.30.6" - value = value[len(match_ip):].strip() - my_ip = ipaddress.IPv4Address(value) - - if value.startswith(match_net): - # like "Subnet Prefix: - # 192.168.30.0/24 (mask 255.255.255.0)" - value = value[len(match_net):].strip() - offset = value.find(" ") - if offset >= 0: - value = value[:offset] - my_net = ipaddress.IPv4Network(value) - # else: - # self.logger.debug( - # "Skip undesired Cfg interface:{}".format(value)) - _index += 1 - - if found_interface: - if my_ip is None: - self.logger.error( - "Found Interface:{}, failed to find IP address".format( - interface_name)) - # else: - # self.logger.debug( - # "IP for interface:{} is {}".format(interface_name, my_ip)) - - if my_net is None: - self.logger.error( - "Found Interface:{}, failed to find NETWORK Prefix".format( - interface_name)) - # else: - # self.logger.debug( - # "NET for interface:{} is {}".format(interface_name, - # my_net)) - - else: - self.logger.error( - "Failed to find Interface:{}".format(interface_name)) - return my_ip, my_net - - @staticmethod - def make_computer_ip(router_ip, last_octet=DEF_COMPUTER_IP_LAST_OCTET): - """ - Given the router's IP, edit to make this computer's IP - (assuming we are consistent) - - :param router_ip: - :type router_ip: str or ipaddress.IPv4Address - :param int last_octet: - :return str: - """ - if isinstance(router_ip, str): - # ensure is IPv4 value - router_ip = ipaddress.IPv4Address(router_ip) - - value = router_ip.exploded.split('.') - assert len(value) == 4 - return "{0}.{1}.{2}.{3}".format(value[0], value[1], - value[2], last_octet) - - @staticmethod - def trim_ip_to_4(value): - """ - handle things like 192.168.30.0/24 or 192.168.30.0:8080, which - we might obtain from various shelled reports. - - :param str value: the string to trim - """ - value = value.strip() - offset = value.find('/') - if offset >= 0: - value = value[:offset] - - offset = value.find(':') - if offset >= 0: - value = value[:offset] - - value = ipaddress.IPv4Address(value) - # will throw ipaddress.AddressValueError if bad - - return value.exploded - - def list_interfaces(self): - """ - Dump of interfaces, cleaned to be only as wanted. Might be like: - - [ "", - "Configuration for interface \"ENet MB\"", - " IP Address: 192.168.0.10", - " Subnet Prefix: 192.168.0.0/24 (mask 255.255.255.0)", - " Default Gateway: 192.168.0.1", - "", - "Configuration for interface \"ENet USB-1\"", - " IP Address: 192.168.30.6", - " Subnet Prefix: 192.168.30.0/24 (mask 255.255.255.0)", - "" - ] - """ - if sys.platform != "win32": - raise NotImplementedError - - command = ["netsh", "interface", "ip", "show", "config"] - - result = subprocess.check_output(command, universal_newlines=True) - # use of universal_newlines=True means return is STR not BYTES - """ :type result: str""" - result = result.split("\n") - # for line in result: - # self.logger.debug("Line:{}".format(line)) - - report = [] - - # use in_configuration to hide interfaces we don't want - # - like hide 127.0.0.1 / localhost - in_configuration = False - - # use last_was_blank to suppress consecutive blanks - last_was_blank = False - - # suppress 'rogue' subnet or gateway, without IP - # (idle DHCP interface might do this) - seen_ip_address = False - - # delay output of 'Configuration ' announcement until we have - # at least 1 other value like IP, etc. - hold_config_line = None - for line in result: - # we'll ONLY show certain lines, discard all else - test = line.strip() - - if len(test) < 1: - # then blank line - keep for visual prettiness - in_configuration = False - if last_was_blank: - continue - last_was_blank = True - report.append(line) - - elif test.startswith("Configuration"): - # Configuration for interface "ENet MB" continue - if LIST_INTERFACE_NO_LOOPBACK and test.find("Loopback") > 0: - # skip: interface "Loopback Pseudo-Interface 1" - continue - - if LIST_INTERFACE_NO_VM: - if test.find("VMware") > 0: - # skip: interface "VMware Network Adapter VM1" - continue - if test.find("VirtualBox") > 0: - # skip: interface "VirtualBox Host-Only Network" - continue - - in_configuration = True - seen_ip_address = False - hold_config_line = line - - elif test.startswith("IP Address"): - # Configuration for interface "ENet MB" continue - if not in_configuration: - continue - - if hold_config_line: - # add the delayed config line. - report.append(hold_config_line) - hold_config_line = None - - seen_ip_address = True - last_was_blank = False - report.append(line) - - elif test.startswith("Subnet Prefix"): - # Configuration for interface "ENet MB" continue - if in_configuration and seen_ip_address: - # only add if we've seen an "Ip Address" line - last_was_blank = False - report.append(line) - - elif test.startswith("Default Gateway"): - # Configuration for interface "ENet MB" continue - if in_configuration and seen_ip_address: - # only add if we've seen an "Ip Address" line - last_was_blank = False - report.append(line) - - # else: - # # self.logger.debug("skip:{}".format(line)) - # continue - - return report - - @staticmethod - def get_whoami(): - """ - Fetch 'whoami' to enable running priviledged subprocess - - [ "", - "Configuration for interface \"ENet MB\"", - " IP Address: 192.168.0.10", - " Subnet Prefix: 192.168.0.0/24 (mask 255.255.255.0)", - " Default Gateway: 192.168.0.1", - "", - "Configuration for interface \"ENet USB-1\"", - " IP Address: 192.168.30.6", - " Subnet Prefix: 192.168.30.0/24 (mask 255.255.255.0)", - "" - ] - """ - if sys.platform == "win32": - - command = ["whoami"] - - result = subprocess.check_output(command, universal_newlines=True) - # use of universal_newlines=True means return is STR not BYTES - """ :type result: str""" - # we'll have a EOL at the end, so strip off - return result.strip() - - else: - raise NotImplementedError - - def set_computer_ip(self, interface, syslog_ip): - """ - To set a fixed IP: - netsh interface ip set address name="Local Area Connection" - static 192.168.0.1 255.255.255.0 192.168.0.254 - - To set DHCP: - netsh interface ip set address name="Local Area Connection" - source=dhcp - - :param str interface: The interface name, such "Local Area Connection" - :param str syslog_ip: The IP for syslog, such "192.168.0.6" - """ - whoami = self.get_whoami() - self.logger.debug("WhoAmI={}".format(whoami)) - - if sys.platform == "win32": - # then use Windows method - note, it will pause & ask YOU for - # your whoami password - - command = ['runas', '/noprofile', '/user:' + whoami, - 'netsh interface ip set address \"' + interface + - '\" static ' + syslog_ip + ' 255.255.255.0'] - - self.logger.debug("{}".format(command)) - self.logger.warning("!!") - self.logger.warning( - "Enter the password for user:{}".format(whoami)) - - result = subprocess.check_output(command, universal_newlines=True) - # use of universal_newlines=True means return is STR not BYTES - """ :type result: str""" - result = result.split("\n") - self.logger.debug("netsh:{}".format(result)) - return result - - else: - raise NotImplementedError - - def dump_help(self, args): - """ - - :param list args: the command name - :return: - """ - print("Syntax:") - print(' {} -m '.format(args[0])) - print() - print(" Default action = {}".format(self.ACTION_DEFAULT)) - for command in self.ACTION_NAMES: - print() - print("- action={0}".format(command)) - print(" {0}".format(self.ACTION_HELP[command])) - - return - -if __name__ == "__main__": - - target = TheTarget() - - if len(sys.argv) < 2: - target.dump_help(sys.argv) - sys.exit(-1) - - # if cmdline is only "make", then run as "make build" but we'll - # expect ["name"] in global sets - - # save this, just in case we care later - utility_name = sys.argv[0] - - index = 1 - while index < len(sys.argv): - # loop through an process the parameters - - if sys.argv[index] in ('-m', '-M'): - # then what follows is the mode/action - action = sys.argv[index + 1].lower() - if action in target.ACTION_NAMES: - # then it is indeed an action - target.action = action - else: - target.logger.error( - "Aborting, Unknown action:{}".format(action)) - print("") - target.dump_help(sys.argv) - sys.exit(-1) - index += 1 # need an extra ++ as -m includes 2 params - - elif sys.argv[index] in ('-v', '-V', '+v', '+V'): - # switch the logger to DEBUG from INFO - target.verbose = True - - else: - # assume this is the app path - target.target_alias = sys.argv[index] - - index += 1 # get to next setting - - if target.verbose: - logging.basicConfig(level=logging.DEBUG) - target.logger.setLevel(level=logging.DEBUG) - - else: - logging.basicConfig(level=logging.INFO) - target.logger.setLevel(level=logging.INFO) - # quiet INFO messages from requests module, - # "requests.packages.urllib3.connection pool" - logging.getLogger('requests').setLevel(logging.WARNING) - - _result = target.main() - if _result != 0: - logging.error("return is {}".format(_result)) - - sys.exit(_result) diff --git a/tools/test_convert_eol.py b/tools/test_convert_eol.py deleted file mode 100644 index e0e7810c..00000000 --- a/tools/test_convert_eol.py +++ /dev/null @@ -1,24 +0,0 @@ -# Test the tools.convert_eol module - -import logging -import unittest - -from tools.convert_eol import convert_eol_linux - - -class TestConvertEol(unittest.TestCase): - - def test_convert_eol_linux(self): - """ - :return: - """ - - convert_eol_linux("build") - - return - - -if __name__ == '__main__': - # logging.basicConfig(level=logging.INFO) - logging.basicConfig(level=logging.DEBUG) - unittest.main()