From 599a1d44c4372ac8a3999c323686f3dbdfead44e Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 10:10:40 -0400 Subject: [PATCH 01/13] Debug download method --- kintree/common/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index 363fae8d..b55db485 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -87,7 +87,7 @@ def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers= else: (file, headers) = urllib.request.urlretrieve(url, filename=fileoutput) if filetype.lower() not in headers['Content-Type'].lower(): - cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) + cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type: {file}', silent=silent) return None return file else: From 735b6764003fc4b7e49b5206b47d26fd8deadf06 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 10:25:05 -0400 Subject: [PATCH 02/13] Add TME token and secret to CI --- .github/workflows/test_deploy.yaml | 2 ++ kintree/search/tme_api.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_deploy.yaml b/.github/workflows/test_deploy.yaml index 15fde40a..29b556ea 100644 --- a/.github/workflows/test_deploy.yaml +++ b/.github/workflows/test_deploy.yaml @@ -51,6 +51,8 @@ jobs: TOKEN_DIGIKEY: ${{ secrets.TOKEN_DIGIKEY }} DIGIKEY_CLIENT_ID: ${{ secrets.DIGIKEY_CLIENT_ID }} DIGIKEY_CLIENT_SECRET: ${{ secrets.DIGIKEY_CLIENT_SECRET }} + TME_API_TOKEN: ${{ secrets.TME_API_TOKEN }} + TME_API_SECRET: ${{ secrets.TME_API_SECRET }} continue-on-error: true strategy: diff --git a/kintree/search/tme_api.py b/kintree/search/tme_api.py index 09951e77..d5025dc0 100644 --- a/kintree/search/tme_api.py +++ b/kintree/search/tme_api.py @@ -38,8 +38,8 @@ def check_environment() -> bool: def setup_environment(force=False) -> bool: if not check_environment() or force: tme_api_settings = config_interface.load_file(settings.CONFIG_TME_API) - os.environ['TME_API_TOKEN'] = tme_api_settings['TME_API_TOKEN'] - os.environ['TME_API_SECRET'] = tme_api_settings['TME_API_SECRET'] + os.environ['TME_API_TOKEN'] = tme_api_settings.get('TME_API_TOKEN', None) + os.environ['TME_API_SECRET'] = tme_api_settings.get('TME_API_SECRET', None) return check_environment() From a5ec0ba84062dd06017916177dad8ff8db7bb489 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 11:04:16 -0400 Subject: [PATCH 03/13] Fix for TME request --- kintree/common/tools.py | 2 +- kintree/search/tme_api.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index b55db485..cca29625 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -99,7 +99,7 @@ def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers= cprint(f'[INFO]\tWarning: {filetype} download socket timed out ({timeout}s)', silent=silent) except (urllib.error.HTTPError, requests.exceptions.ConnectionError): cprint(f'[INFO]\tWarning: {filetype} download failed (HTTP Error)', silent=silent) - except (urllib.error.URLError, ValueError): + except (urllib.error.URLError, ValueError, AttributeError): cprint(f'[INFO]\tWarning: {filetype} download failed (URL Error)', silent=silent) except requests.exceptions.SSLError: cprint(f'[INFO]\tWarning: {filetype} download failed (SSL Error)', silent=silent) diff --git a/kintree/search/tme_api.py b/kintree/search/tme_api.py index d5025dc0..2d3c348a 100644 --- a/kintree/search/tme_api.py +++ b/kintree/search/tme_api.py @@ -48,10 +48,12 @@ def setup_environment(force=False) -> bool: # https://github.com/tme-dev/TME-API/blob/master/Python/call.py def tme_api_request(endpoint, tme_api_settings, part_number, api_host='https://api.tme.eu', format='json'): params = collections.OrderedDict() - params['Country'] = tme_api_settings['TME_API_COUNTRY'] - params['Language'] = tme_api_settings['TME_API_LANGUAGE'] + params['Country'] = tme_api_settings.get('TME_API_COUNTRY', 'US') + params['Language'] = tme_api_settings.get('TME_API_LANGUAGE', 'EN') params['SymbolList[0]'] = part_number - params['Token'] = tme_api_settings['TME_API_TOKEN'] + params['Token'] = tme_api_settings.get('TME_API_TOKEN', None) + if not params['Token']: + return None url = api_host + endpoint + '.' + format encoded_params = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) From 476790d0262b6dfe9ce5f42261c4c30f22b9dd64 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 11:25:32 -0400 Subject: [PATCH 04/13] Fix getting token and secret from os environment --- kintree/search/tme_api.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kintree/search/tme_api.py b/kintree/search/tme_api.py index 2d3c348a..612a264c 100644 --- a/kintree/search/tme_api.py +++ b/kintree/search/tme_api.py @@ -47,19 +47,25 @@ def setup_environment(force=False) -> bool: # Based on TME API snippets mentioned in API documentation: https://developers.tme.eu/documentation/download # https://github.com/tme-dev/TME-API/blob/master/Python/call.py def tme_api_request(endpoint, tme_api_settings, part_number, api_host='https://api.tme.eu', format='json'): + TME_API_TOKEN = tme_api_settings.get('TME_API_TOKEN', None) + TME_API_SECRET = tme_api_settings.get('TME_API_SECRET', None) + params = collections.OrderedDict() params['Country'] = tme_api_settings.get('TME_API_COUNTRY', 'US') params['Language'] = tme_api_settings.get('TME_API_LANGUAGE', 'EN') params['SymbolList[0]'] = part_number - params['Token'] = tme_api_settings.get('TME_API_TOKEN', None) - if not params['Token']: + if not TME_API_TOKEN and not TME_API_SECRET: + TME_API_TOKEN = os.environ.get('TME_API_TOKEN', None) + TME_API_SECRET = os.environ.get('TME_API_SECRET', None) + if not TME_API_TOKEN and not TME_API_SECRET: return None + params['Token'] = TME_API_TOKEN url = api_host + endpoint + '.' + format encoded_params = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) signature_base = 'POST' + '&' + urllib.parse.quote(url, '') + '&' + urllib.parse.quote(encoded_params, '') hmac_value = hmac.new( - tme_api_settings['TME_API_SECRET'].encode(), + TME_API_SECRET.encode(), signature_base.encode(), hashlib.sha1 ).digest() From 88741d3ba6cff6927cf65039e4320891b827d179 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 11:33:35 -0400 Subject: [PATCH 05/13] Add default country and language in config file --- kintree/config/tme/tme_api.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kintree/config/tme/tme_api.yaml b/kintree/config/tme/tme_api.yaml index 76645ddb..841ee909 100644 --- a/kintree/config/tme/tme_api.yaml +++ b/kintree/config/tme/tme_api.yaml @@ -1,4 +1,4 @@ TME_API_TOKEN: NULL TME_API_SECRET: NULL -TME_API_COUNTRY: NULL -TME_API_LANGUAGE: NULL \ No newline at end of file +TME_API_COUNTRY: US +TME_API_LANGUAGE: EN \ No newline at end of file From 164efd43584c2b95e24bf15e4d84f2c267a526ea Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 11:52:28 -0400 Subject: [PATCH 06/13] Debug download method --- kintree/common/tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index cca29625..3c3bd593 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -80,14 +80,14 @@ def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers= headers = {'User-agent': 'Mozilla/5.0'} response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) if filetype.lower() not in response.headers['Content-Type'].lower(): - cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) + cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type\n{response.headers["Content-Type"].lower()}', silent=silent) return None with open(fileoutput, 'wb') as file: file.write(response.content) else: (file, headers) = urllib.request.urlretrieve(url, filename=fileoutput) if filetype.lower() not in headers['Content-Type'].lower(): - cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type: {file}', silent=silent) + cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type\n{headers["Content-Type"].lower()}', silent=silent) return None return file else: From 64760526b3b68fc0952e22f5fc2d86c5dcf07b2f Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 16 Aug 2023 14:10:41 -0400 Subject: [PATCH 07/13] Bunch of download fixes --- kintree/common/tools.py | 16 ++++++++-------- kintree/database/inventree_interface.py | 10 +++++----- kintree/gui/views/main.py | 4 ++-- run_tests.py | 8 ++++---- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index 3c3bd593..8a097280 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -80,14 +80,14 @@ def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers= headers = {'User-agent': 'Mozilla/5.0'} response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) if filetype.lower() not in response.headers['Content-Type'].lower(): - cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type\n{response.headers["Content-Type"].lower()}', silent=silent) + cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) return None with open(fileoutput, 'wb') as file: file.write(response.content) else: (file, headers) = urllib.request.urlretrieve(url, filename=fileoutput) if filetype.lower() not in headers['Content-Type'].lower(): - cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type\n{headers["Content-Type"].lower()}', silent=silent) + cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) return None return file else: @@ -114,18 +114,18 @@ def download_with_retry(url: str, full_path: str, silent=False, **kwargs) -> str if not url: cprint('[INFO]\tError: Missing image URL', silent=silent) return False + + # Try with requests library + file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs) - # Try without headers - file = download(url, fileoutput=full_path, silent=silent, **kwargs) + if not file: + # Try without headers + file = download(url, fileoutput=full_path, silent=silent, **kwargs) if not file: # Try with headers file = download(url, fileoutput=full_path, enable_headers=True, silent=silent, **kwargs) - if not file: - # Try with requests library - file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs) - # Still nothing if not file: return False diff --git a/kintree/database/inventree_interface.py b/kintree/database/inventree_interface.py index ce230171..4dcea958 100644 --- a/kintree/database/inventree_interface.py +++ b/kintree/database/inventree_interface.py @@ -575,12 +575,12 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, cprint('[TREE]\tWarning: Failed to upload part image', silent=settings.SILENT) if inventree_part['datasheet'] and settings.DATASHEET_UPLOAD: # Upload datasheet - datasheet_link = inventree_api.upload_part_datasheet( - inventree_part['datasheet'], part_pk) + datasheet_link = inventree_api.upload_part_datasheet(inventree_part['datasheet'], part_pk) if not datasheet_link: cprint('[TREE]\tWarning: Failed to upload part datasheet', silent=settings.SILENT) - else: - inventree_part['datasheet'] = datasheet_link + # TODO: this is messing up with the datasheet download to local folder + # else: + # inventree_part['datasheet'] = datasheet_link if kicad: try: @@ -753,7 +753,7 @@ def inventree_create_alternate(part_info: dict, part_id='', part_ipn='', show_pr manufacturer_mpn = part_info.get('manufacturer_part_number', '') datasheet = part_info.get('datasheet', '') - # if datasheet upload is enabled and no attechment present yet upload + # if datasheet upload is enabled and no attachment present yet then upload if settings.DATASHEET_UPLOAD and not part.getAttachments(): if datasheet: inventree_api.upload_part_datasheet(part_id=part_pk, diff --git a/kintree/gui/views/main.py b/kintree/gui/views/main.py index f06bfff7..9b2b7b17 100644 --- a/kintree/gui/views/main.py +++ b/kintree/gui/views/main.py @@ -11,7 +11,7 @@ from .common import DropdownWithSearch, SwitchWithRefs from .common import handle_transition # Tools -from ...common.tools import cprint, download +from ...common.tools import cprint, download_with_retry # Settings from ...common import progress from ...config import settings, config_interface @@ -1321,7 +1321,7 @@ def create_part(self, e=None): f'{part_info.get("IPN", "datasheet")}.pdf', ) cprint('\n[MAIN]\tDownloading Datasheet') - if download(datasheet_url, filetype='PDF', fileoutput=filename, timeout=10): + if download_with_retry(datasheet_url, filename, filetype='PDF', timeout=10): cprint(f'[INFO]\tSuccess: Datasheet saved to {filename}') # Open browser if settings.ENABLE_INVENTREE: diff --git a/run_tests.py b/run_tests.py index f639f8d6..5a078c57 100644 --- a/run_tests.py +++ b/run_tests.py @@ -2,7 +2,7 @@ import sys import kintree.config.settings as settings -from kintree.common.tools import cprint, create_library, download, download_with_retry +from kintree.common.tools import cprint, create_library, download_with_retry from kintree.config import config_interface from kintree.database import inventree_api, inventree_interface from kintree.kicad import kicad_interface @@ -387,13 +387,13 @@ def check_result(status: str, new_part: bool) -> bool: # Test different download methods for images if not download_with_retry(test_image_urllib, './image1.jpg', silent=True, filetype='Image'): method_success = False - if not download_with_retry(test_image_requestslib, './image2.jpg', silent=True, filetype='Image',): + if not download_with_retry(test_image_requestslib, './image2.jpg', silent=True, filetype='Image'): method_success = False # Test PDF - if not download(test_pdf_urllib, filetype='PDF', fileoutput='./datasheet.pdf', silent=True): + if not download_with_retry(test_pdf_urllib, './datasheet.pdf', silent=True, filetype='PDF'): method_success = False # Wrong folder - if download(test_pdf_urllib, filetype='PDF', fileoutput='./myfolder/datasheet.pdf', silent=True): + if download_with_retry(test_pdf_urllib, './myfolder/datasheet.pdf', silent=True, filetype='PDF'): method_success = False # Test erroneous URL if download_with_retry('http', '', silent=True): From ec91ee619275a634c977ee333889acc0ad37dec9 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 07:55:40 -0400 Subject: [PATCH 08/13] More debug --- run_tests.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/run_tests.py b/run_tests.py index 5a078c57..0ad94f55 100644 --- a/run_tests.py +++ b/run_tests.py @@ -18,9 +18,9 @@ except IndexError: ENABLE_API = 0 # Enable InvenTree tests -ENABLE_INVENTREE = True +ENABLE_INVENTREE = False # Enable KiCad tests -ENABLE_KICAD = True +ENABLE_KICAD = False # Set categories to test PART_CATEGORIES = [ 'Capacitors', @@ -386,20 +386,26 @@ def check_result(status: str, new_part: bool) -> bool: test_pdf_urllib = 'https://www.seielect.com/Catalog/SEI-CF_CFM.pdf' # Test different download methods for images if not download_with_retry(test_image_urllib, './image1.jpg', silent=True, filetype='Image'): + print(' [1] ') method_success = False if not download_with_retry(test_image_requestslib, './image2.jpg', silent=True, filetype='Image'): + print(' [2] ') method_success = False # Test PDF if not download_with_retry(test_pdf_urllib, './datasheet.pdf', silent=True, filetype='PDF'): + print(' [3] ') method_success = False # Wrong folder if download_with_retry(test_pdf_urllib, './myfolder/datasheet.pdf', silent=True, filetype='PDF'): + print(' [4] ') method_success = False # Test erroneous URL if download_with_retry('http', '', silent=True): + print(' [5] ') method_success = False # Test empty URL if download_with_retry('', '', silent=True): + print(' [6] ') method_success = False elif method_idx == 9: From 66741cff5563f1c58242d2a5ab1eeab84a67217f Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 14:41:00 -0400 Subject: [PATCH 09/13] Hopefully fixed download --- kintree/common/tools.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index 8a097280..b34a698b 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -67,17 +67,22 @@ def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers= import urllib.request import requests + headers = { + 'User-Agent': 'Mozilla/5.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.5', + } + # Set default timeout for download socket socket.setdefaulttimeout(timeout) - if enable_headers: + if enable_headers and not requests_lib: opener = urllib.request.build_opener() - opener.addheaders = [('User-agent', 'Mozilla/5.0')] + opener.addheaders = list(headers.items()) urllib.request.install_opener(opener) try: if filetype == 'Image' or filetype == 'PDF': # Enable use of requests library for downloading files (some URLs do NOT work with urllib) if requests_lib: - headers = {'User-agent': 'Mozilla/5.0'} response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) if filetype.lower() not in response.headers['Content-Type'].lower(): cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) From d9df3343d667034f02d72e010b40a79789b1e86b Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 14:48:40 -0400 Subject: [PATCH 10/13] Re-enable full test --- run_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_tests.py b/run_tests.py index 0ad94f55..dd23482f 100644 --- a/run_tests.py +++ b/run_tests.py @@ -18,9 +18,9 @@ except IndexError: ENABLE_API = 0 # Enable InvenTree tests -ENABLE_INVENTREE = False +ENABLE_INVENTREE = True # Enable KiCad tests -ENABLE_KICAD = False +ENABLE_KICAD = True # Set categories to test PART_CATEGORIES = [ 'Capacitors', From 5f98b69dc0143833a3cd840c1596b270026e68a2 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 14:54:47 -0400 Subject: [PATCH 11/13] Retune order of download options --- kintree/common/tools.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kintree/common/tools.py b/kintree/common/tools.py index b34a698b..bfa8f24b 100644 --- a/kintree/common/tools.py +++ b/kintree/common/tools.py @@ -120,17 +120,17 @@ def download_with_retry(url: str, full_path: str, silent=False, **kwargs) -> str cprint('[INFO]\tError: Missing image URL', silent=silent) return False - # Try with requests library - file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs) - - if not file: - # Try without headers - file = download(url, fileoutput=full_path, silent=silent, **kwargs) + # Try without headers + file = download(url, fileoutput=full_path, silent=silent, **kwargs) if not file: # Try with headers file = download(url, fileoutput=full_path, enable_headers=True, silent=silent, **kwargs) + if not file: + # Try with requests library + file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs) + # Still nothing if not file: return False From 1a6a2ab0c2a818dfa03c7cddcba7fe684f6aa1aa Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 15:12:21 -0400 Subject: [PATCH 12/13] Update test sample results, some were old --- tests/files/results.tgz | Bin 102400 -> 122880 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/files/results.tgz b/tests/files/results.tgz index c08f479567aeaad142990f59f4ce9602c6d6b6dc..df05accceb43cf3eee27daff78033aef799aad1e 100644 GIT binary patch delta 24439 zcmch937A{em1dr8Y-78?2JagW+d!4^)7uwS6Ks{LRIWu;MMAEZl?y&9h-V&Kut*@ceP;)`@N<`ujqkqs zR5o8a(!ER+)ou~5OCKook)k-1G| z={;{c>ay8NzL=?;oSM#%UoBrlzPPA!+jORusm|nbHF+`}pGpqL<5T^)624PmYHY4D zTdwBFvx_YE>3zb64T_;?QYYThl#;nxC7OIZTW0xqrclk7q*^XJQz{qAM@~xFGTyk7 zq027Q_v}#>V@S1>cv`k>#Wr<=sQEQZqnd6-t*Apj_}b}u$LH#cfiStrX$IIb0I*@J zN&xVZ9m0W@0PBi4R<7ph+lq;?xEL>&O1UgW6pw0|Y^}OW>?zl3WUSWRZG`Q%W5o|ty<*473u;}B9E@v zejz?itXj<#4;AvcD*xTG#&5_&=j}eT=h*#1{Q|zg8#icX$b>ADt+3WGA*}F7%N1^{ z=-ymrI#==6O-z>ON;OdvvETUmV}T96z#8^f7qg`9mG3_D%*vjHB)0k!;T2UPmg%ji z5;09~Ol;R1nqy!^H=kE4mkZVUiuGjUnm3OAsq3oiQAfer%2qV0G%u@79#|74Pp=Wl zH_y|tXd8w4`ZZ+D+O%Mjo7P^k+IKr9TQkitUL~5`?W~&-=Z8!h$+UzSX|UKaYF2ki-|cHueE9CS*xsAijJLYDURq_FDbscOIi?_a(t|2vT(m~DRc(sCKkjFr06&YQxFvQ&yOe1CA%e zwXjXx8K0|Eu$ZDGrV)_D&VgDNTn2k}^21vU620^^`N^eE;ZMG>O)7@m4T4J)e{Zrd zLm%-tqaqskaYnV&zzgAaQ_=Ah0tx#QT{kUuC%;rx`lKIqRK>KZfr<6)PaS9%!b7=& zME4K}@L)WNkYy$kTEA?lCO%ro*;Dj>xBbWU%b6ouylK6uhj6oLC^xl+slX4lHWe%6 z=>wlcp6J}R#>WX?lKZ6oa$%Z#pU%Ad-dSNKx$$c&&itS=DUjIC4I`nIRwKHBmI%-;mLoMsV>~txTDbCL15DlelmE5-TCz*EFHuO5u z;cQ-3DdjO$TT(0)K5RH4JEpBg0b&qPZB>O6yMo+xWdytF&MObpJAPf?z_Mby%b8d{2ed zDvr(+#XWQj9^HG3N3kNe;-h~3F6qoaMq-p^Z6`@(>OjbA(N(8wZnNd5k94}_=^h6@ zmKyLZ8qf`9Fq}8pW>lm5-{0{Te7;U$T3E_NNbJ)aRviXYl%~taONDY~TDEkD2w&(W zNp<&3yP(ZxN||c8l+Q}J0{BOTJp_50EtlW|ssJ%P)@M0M+0YcjcFYiVis^pndSM-T zP>t0)p08^Rwy$hn7jkblMa@*zM!42@3Rks6tv6qu&e5+9Ky(nDm~S1PZKOtfB~{az zdF(NckT<_~x%-*b!ZPw5!*Tz0wXl8VHqMx1958W$T=Ssg7S{+DkgrrPCdY1F?S5#D zu!KBrZY7JXt%B(OuQkFjDO<}{rvp0JEv9N21e40NJFr%`0yoX#$F*uiGx52w6POna z0u+ZDe$2<3y0nmgQyiUBZ067u6Fw${ucG4@x=c^B8V!TK3*RvLzU!YUs8a!c$Yp!}d=1j&;Ho_wIgSyx#HO>OITI zGG~fW-HjV`@Y^7nu`CL$rGQm<$qYQ;!Isd%$O5iAp3CND!EX__0%u4WuBkAFtJxuj z%aO-|HT+`tU1t{l#g>J{y87*5RtCFjxI;zZXs|VpDvak+4)J(@yGC-BXCs;27cZ4H zO^GU|6IwvkfiqLc?{{zNKs(YMQ8#)nur5&q7>~xAgv;ys5AEP$=+#|2FnI{1sl01|D!> zy^&rh5t{L`rx*wZaw=ST>7s6F9zg6#&9RVUA+D2W`u-{3E<`t#XEG%?vT$lgN!m-k zplmpn0Z$gf%`nb~$nJEch5^fE3)=Sj)XWBE|7RO1XlF zxIZDPstHflna*^Hx~&u^`9m**3p`s|(saplZG#UXya+>=kV}VV&KGABa60fkyg2+e)m6JD6A!qCbwSSE*Py`k2Yc}$mpr(u~pl0!k))KhWNr}vTbCoB_{gPNPUx| zDf$3rbk;S#R|?e(N_#^$(zLPOakM_>!w2IG;KN=7e3lk6v}<+=qb=dvrXytre?Yir zg%?ekoh^#-%E>AiglGW{(-e=wz^bAqApzMXj+Dh=Wr^o-w$`pYxpyErizNe}F6 z0*n!1PKs^DgE{fFEXLpv{Fo&<(GUwZO~>*W1Ggm$8Jc^?T44+MO#1UTwhK|_P$iRv zxfBXnIo6X>Yy?+@rZM>TLwgTOX<4^5Lovu5FZkJv8YQ>wn@2WtzEGd=7utkT_%Q)w zo_aL=AK1y{4&hMCh3=Wl9u-r^^R?^@;6yreWUi2*<&S~57c7qD#Mtyrb69wq0+FV zd#xy_FnfZnt)qs$mTeh&2nfqI4PH#~U_v3!;Xpij@R1{J15ucr$w=j5?np*LStJio zXl1KhVJS*%^q{IaT6{p(qc-9%-OYtUiWtejN+v7i?OuH0mm*!t`F;zYSn z!yBCgxkUa1h_{cIPv|&YCgX5#yWv7IaKk-tb}!zr9sgcB^?M@U_*@t@gb|(qQmfEe z(BPyp2Ts|)LR?4%~D8$a; z-q9|sNnl7-g1c6?y35_;2t)O0X2DxGt+yK6z_4kl$TUNwE_cy6wt)qv&io)qF&-x2 zFjX;;t5ou}oY(`8*n)!4kc4okI|@y#unk`8K1s9O`wzbbJK*O3aT)o3*D_K&@)}mX zUU~ltV$EzUh3pvg+B0KNMw^!o=;lCW48Qj(*(P(Jo-GrFiWS18a>xQ?vJZ)w}l7Uxh~&V-Vys@yZ2RGnDArKqV9wO2MB zL({@}6)r~dpM{@|3-y~AN-bXJH*0!|OZRGyqC6=gwA!0o) z6UX<81L!Ac@m*qSY=A{Ln)2%CE{N6Ugo_2VGiKp0gjJ|DAswvd#B{!xg9jDK=^_b} zLMvkURK$ol%-*P>8WFfIPWlXD$lqbTBr05Y{bsWzT0~tL6!pMs~BZtb> z>c!sk(&{c!*f)3X#hHSnqhcJ{!bKxY!_GYBXNL=E)}>>q(_h4X4c<-hX_zTxVZ^M#NFG~oA1U8D6lxZ3B>HP6&)G0!MMuxg zYT-hzqBf-*s6sZZ8SYt$vit;InEPGD@ph4aiS)hEFRYPYFJIXQB1Ai4k)t3pHEQAnj&MIf;qAkF>hsB&-6-Tj>8Mf8kjV@k8KCliSia3-z>C8z{&%6 z-}Z}BL*nS(q=@g!zG!U!Ky0{e+-f;Af7u?(AIsNHO4MZX$8wVAjpTSwS~Ca1*knz2 zR10;zD$(;7d%Yb)CvSPjT)pGN_1F8pT?=(~P}!s2Ktb;L=L0iQK-Y_dIgV{onMR=- z5Tcn~I)VtFIh?DV^pQ>E3q_u!zUGa~&KPezC;-WSI;W+xw1}e!RQ&P>%ne5&N?{8Ar$ z)W#_I&(`0}z(>_PM9|PcrO+cA4naTl%GxovSB`xE?U|i#9i*`CCEt46+sJ^CD&61q@?f))GPRP{! z_9wG6Fs~2-5T-tz{0>RAz%w$;hk-hE4pusHml-ECK5tC?bT zu5`q=E~$v-N(K$4svI9yOeU`xHAhB9;<66TZ<#Cs=5V3k#RKGh_0xe+@1hT3#|8nwRo!AB`KhU`Bjq5MMv04J>5RFc z11832AUNRAM!8;mJh`c&~zUSMJYQ5v0n^7m&A=Lkx-k2H#S)3fBO=|o{;aI|p z-iq6y2wknX%{pYLQFK!b4lFZ$I#kqYF_bl8Cu^7!ZKfEBmaAUm{s+pmd?tMc+m079P#N>gWz+743VF6BAK`*$X_gC~CG@{$rHYrN zSLGr^8-=ReH$JHM_Z`5(>J!lTyz|eBVDL_|3IU!KDp=??NAT|Y1HR)kk*)-y{F`!S6R48}(W|)6r4XVJ1W&LV-DCU+58qH3&+SQU%R7 zHYcij_i{8ry-b$iJpbh%Ei$6^6#4E$N?{Ttz?q1P^nWvM@ zRYOvH5(bqkL{){hVTU~r=rlKp(iI3F^0eRQx8%TfXR)JDFt1|8CsJKh$vKhYOG|s| zxbPG{yX#+Jz(U9l5CdE-o)(1&9mGA`TjIJmk{E7~1TyutHJdX431W(B@hX&I zpXvVUHw475^S-`CAkM>E%OTs-5NSO8)QH36)ihIum=2{}Ji%#$jn=D=tO`Lk>jrwf z!QdrnR%rzDKuwfy2u?8|MYS1{2R^ZUL9Rqz^YC^w@}rWfT<-bE5=ttsre8`LM}t4z zi3aKe4{u$n_4ZaW$Kg(&et69?>=TScSP&VIOmZi=`_oH^{75F(u9rOrDcLLeA3}=b z1EODOpzI%}CMNmRBbN;TP}F=_buS)F5p<3xx*D>23c7a>*6mz(dE)yw2=zw;Yu6f~ z)v(z7iiXYM(|qiTM9W>Ff<;zp;`pFpHkmRk6Ouv&@+Gja^!{C9Y_3*L_73c4saN-7 zl6z!a*mUO2k6pTeJoNa%D?(c%8u5A!-J%n*seRI&;ea)o8gsS!6RURz8pbLsCS_*x za(R}{fn+K*DFeZDc81o6Lp>sSP*+s7UsHQWhSPAh8oB$4blal4Sz2E8Q(u0< zF$;aIaEk~H$F1MmzAB07ivqgp0W@UKx7Mr$YTgrJ)bP~b*Pjd`svaVu(2+yw0-a%i z5R@)AwM59^J=^`GVrR;eJi;?WcO#Bgv-tuSV?b6mu?y`ueS+&E51oXlfXWQ;d3qs3 z?GlY96%0^Zzs`1~p|}QlTil?fe?^IF2cR#bZHnbX2S($4>frNU<&SfGKN#n* zj7C3---yyyW3~XAj_X@)hL@%32&r+Jg&kONN18~bM#q>HzxC;h$net{@;5h(osY=| zmhHqK#@)`k`=e7PDgSmkSy^06@&_#P@DFb!yZ`0e%>>Nxp;w^B@^eQ#_L`C`Dw|Ow z)Y&&pUIFtuOa{tXfDW$^Ii8=+l_F4#%g6G$~dX#GkpYRxD2#S|wt-`hVl53NrUqN~Rm4`ZZAo+IE-8aAIwgh??@ZdhDBPd@*PZ=~9d&6xSgVKQ(yok~k_HL4Ev4?!B| zSdQK#+0e*`ewjrnX0=fN6<@xv4jz&?#*?ikQ#^+PF;&Y|a%BWYbhky?L zhIj*WKxpPcu9vDlm z_1j-$LyN7$sJz97n@5R}hgB#rOebjcKobd$&z{24A}B=t+9bWwb;a-O;aQ9pf{?L( zz-~y<@Ijcv<&q6VVm5l2&t?jkV+c2Esc1uD!=Y6<8QUMFjcq#0Wj%Y~RJ-8u_m`&m zLeL(MR0L5f;t@o15avJ<@4Z)FdJvjNO-0MnR>B20gafiCzu0%mQSvv>Ua~Aj`P67{ zUp#f@iAR{clHdXyD)Y|DLw1jj*N|n z2-O%;0c)b#DMg`DEBp|l`tZ0Fkl_PFo*>eb5@TI6TI7Sq}L#)yS$vPKQcP$~sknoRbj zb;$5vmKx-3&#ykrCCP1PRINlI{%7z9AB=nI=SVM$ShLmw3_ zbRQfK7I#%l_)-0LE6K{=AMh(ss5&)Z=f-X{${K*ZH3{=y`6aNAjf^$H4sJkUXPck} z>~YC9(eQ=7(!jJWodnvN5fjslv739MI{oAH3!C#+m ziJt%Srk2}*=gn+8gvVbMN8^1D?tH;Myka81*1=e4;&nOyP|PRL7#wJ|}>!momTs4b2kc_9Ll7yRsX#^;J5Emmeqf^oGgFw z209+|$xZ7~1@`xVVY6tdhr0jR1H&z&<+g*gBIMOQOpN{0K~84fURAO=f%4{{n74n= z9^oj1)O}&Ku(o5H$+;FC84%y|oN!kYw){mowzJmx?k@%5DwK}dRFfjQkp;qzmUuU) znA+6(_xoHNU&Czf^+V97!Tfi-^##KF+b-9)w&m186Jq`zF7h%>$hsOMx0>eeT_{|( zLkz7d&3^f1VAAZ8)j$xm$Ar_11V3sw3>irWjoMa-@_;~NjJd^m4T<4cYA@bqWON+O z#3Ju0q@r4 zoj{I#3QNfs7~iR9AWaukzGh#nS9Gw1ZM%kI28Fj${)C*|ubJZz`CUf+@ZuXp=uJG_TX95G4!ZvUw4g&cz0BKH7$-uy)^HBAYKq};pGw~=EEl$) zW9oaG5wV$7?-f%skjc<_)?T!1Q7Ne8cCkUR<2cU%JIfmz03ow5ns1m9HoeMgr-=6$ zR6)+cXB(OGaHA`Pxh0tHu4Zao?%gZ!5k&Wi6~gQBV_%2Rg&*@B!YAFUE)YI*Q>(Yc z!iPJ4=qoi`1q!C#eu8q}WE6ronRJzrbdN)Tg3ztTHWbr@>AaK!WrCH?e<0c{7>j7Z^kcHK#OJSSftr^hE*eVMQ3UvDBSl;4VxDyIZ;wUIfP?QJKOy zCSfOOoOk!E7M@xrmi(bt%!#p?*9nZcZdn9!A56I}SwD%DZklw>&ftKNqbEa?t~>X6 z0RiU#oQ*E)iF<-zU#rbnKr}qVj3$sTd0>G-Ric(o8&yx;IN3G~rQ8YVH}V;2u9~4N zULH&K^<#BZrEhqAzieVoM?-hG(X@r?@~lzT=TEQqP(h#$6-iVE3g*%SP+l?V!Vb^& z%3$(y6<>)-C3QxwDozA9bMdHOFboQJ2Wu#QzrJP~?;C(dM%d^|hc zYqtv>>2~w%06@I52jCFH&v974!t34ZK(s1y(54x6c$ZPM!-LU+J)i!|Jn|A zcU~_1J^Ar{_tTc}xiO(I1SS_0NvKsZBMUIm8INjUqB&7!V^4Jo_q2Ur!_hLzB(u3( zCER6TPH}w5)_c^!bh;N^<3TyrqzH%!QdWw9*>Hx?v0T>XYXQn= z&p^geE+H3<+%${R!j2T6#*(XuG&oZTb-!X&#&jhuVND@bbw_03W0UQgi2r8tcq*n0 zLa>^QkEs1AOxaqPe-ERi=^nh^W}Eg84sg2-VVYMGe*N^)dmk|bkqzFu8!X}P!hs4? zw~b~ldNFuOE<48TSICvwjxT(qj?LDcu+|tMph;PQ0yI6hY_t2{E#X~sl52A+SEMyX z$`HY4%BUVD`lnKHOc6{~$u;k}gbs6VZaV`-1%U^4Jx$bfjRPitEn}hM!)sq7ErtAT`fdbi}?PfoHz)1JGynW zh>RoR?$|9{ysFVw47K6!?Y>I5N?0Jq$eynC_rBxR!sCKFw_A7v4i*ef3q%XeM-T(p zLDLnF{gav~Waz0k!5Kbk2q*l6&wj8l!o=!8?%F*_Fk*XzceQ<%Q+6)zmv-gUprm7` z(2l*79+x#o)o@lHK(amt~7&a99IJ|e>lb^;ZE?GK> zhE|O##`6{ISbFfFe?kb3R7xJ2t->v)k`4pCdr&xX?}LLvnK{&j__cdvSa6^?#{@4j zdIUf*koidBL_FR@J1!PIoXEf7?7YnzrecQ5q_^A$J5aFT#k`$k;b;Nop4m>Zph4!u zlUp|+wz+*t;nq{Jk<@T3jVf5rK<~hKUpzf9IuaYE zU$E_M^3slJYQ#2%C*oS#Fni-LcX$=UL}ezIA!dG+->R|P=aPczE*TMia(axL@CBRK zHR>?nY0QM7WcN)ND4%2AE;gRL?nJDQmR<09=tC)Ki>Y){*Aljvq#7{& zf)#pmJ+$`6S_qg>tayW3nrhKXzGV#ch@JSCmhQTO@|HBb;+n)5&gxT6s0wT8ZE&v| z7Y?}(j0+!K5q>pyDkbdN*1A|YQid4JFBtw+O8DBnxwIe%;p(3i;V!!y+bYUR^>ka9 z(Ae-K(gN~E1O*O_x(r=ompHhE4R{$2yM%Ct`SvKi8MOw3cZ6_k2)o9WuG2DtcML2Y z>s;;LwokZ$eEp$|E`pA3-kh9D_RXi~Eh8xQxf>^hOWGc{Zy~4iFw&KACvC~tC_2v3 z2xXNx#ADxt(B&ST5X95_`98sEe_>e-*LQ?I+BtT=d9$#Ip1yl;*NwtQ7Pvn+B#ebE3~rK_T-wKOMH z+37mun~OH$*Ep@zy*(?uXT046#(zCIJq$``rV=P1TtfxJ;Q?n|aj%&c{>$mZ0R%Wh zIS8w$NwC}{G$DUZhuc#wSK&SX$N@n-d13mrAbA0ODbnnvlmBrS(wH>WCZRtXmmG(; zNpVtxU!frz4qxbY&xBK&Z_Ee^Pic1EBz&Qn5PRY?8g?*-Q}J~}A@`X~%j^l>uDy zm+Fz?Vkdrx=>ubZ5=QvwV=ba!IIP71f|@~(x4qI4d*_P9lk^-M|G>e(O5N^R;lcCG z7>>cisCvb=l29`j>oxZkx6b|jHUu8;#Yc7ugVEKOiHAHmrM*|MiSoMz(UBUO2>ddN^5 zSLhdHn%%m$SB3aNF^<#UuXM&nQ*Dzdyqk;xFrEHbMEKN5D+Fe2xO9w8N#JV7-kO1K<47iga9g1 ztgRMr%&+qc3R0-8YpWG!X{@!imu+{qtyWvD*4kFL*0oq|x7)hiR_%WOnK^R?av1tN z`#e5LW}J}k`u^|x{^#&F%MPEOcy~xrq>phN$Ez||_#NJY_c-xmyr@dN!b?04|DTsd zLHZcO;j1fx8HB13#^qJ`NXglW+xNG~9Sbpk{{rlp@QYc!{&mjqkmq5?@!!P7yfX$?_cQ>1xI2&Z||M{lke+WQoA}w7?LXePzd2Pn=cK zB;)6&*WoScnPqAp!%3WgU)WJKi4zp3i*qUX?TI^5lR`)#du+&7&F6!bL=l3PR9?sj zJ$W$=`pdN~{)EQpVRny%cWLnzOamtig332!R!@1LWajt;@XOOWA$A)MO}}rlgX26N zrjK=-fry-nM`rMummaG?shN5-X^2KKqmczY8bQ4zni_mZ<*}Oa5pl98x}2H1s`d$1 z@ocYoBo|LL9-8wh{No*!*^-EYfN$#N%02#J$T1X2l;ij75=g~+Z1XE~{S;)qG=d1h z$k~3*9YRH?K{Qjz{(1KVgouCGCjzt~#R}*ab@7J?c7fnUa zu0(WLOKSMWg*T#peC!Tp({L!6)P{GDBs51L8fFi%VJ#T)v)Zs0NXDa)P$0nu{Yigf zNYj!DmVPe5n!htZ-xx^jjt%5L#|GcR9nfj>O|yB+1wrUpy3J z8atCqS1`QHh28k-uhrtamOgH^7kK5epW)fd?a;~^{QUBtnh^<{jBWNeGfc%YlyVf` zRFTU^D{}g#%AAsqHaqlH_#_vyjUTfH9(F{Md9?wr>YSTl_#R|6ZhM8fG{%ZRFyios zV?$abI&2RG0vL(EDF~WZVD=jDQOSYdks4D<@wfp_41^K%xFxk9ZUbTNcV18UD6(hT zTyA7lL<%nT_~8`;`Xtp%Si!RRrsg?#;!kJJbi3V7m_;`Cy;W53##?0kYIC0%H81n{ z+{!JfYl=q;SZPEn>&qS#|>str?s%zRG;J#7oZ2pFM~X zBv^Nb@y0YQ2)`Tl4-Yfm&|t(r%&c0;K#jw0n9S(XCdM5(ko_2bVf(za6HUT5w=5>7 zdQrnP-XX~S5KVS?kKM7VESP|?aw_DrOHn8OMr)5*IuYKD^`T{`9RI0(gznCyx|)Kv z%DHh=_;KTyUiXOAI6{yDFA9Lft2!A=vxL zrawBI^bcxGqsPCW=?f;9o=6je;~x)AigiH4plx(acH<4(omSD^wEZ=R&V_$|ZTEC< zkIycMb_$8T8`i=BTZ{X5%rk>lISIeFL%>%*_H9GiWMK2UqD0ZqvrhfJE7e59~ga8Y&uy3avDjRF!q$=70tm zfrQ}vzlW*HoZ553G6-0PMR?2b6Y%FN@u$Pj(LZ0S6Ac)W^2ErlfIq1XM&k!oFuk76 zRI)lmbmw_bZv)P~X)Fi>SczyO3PrKmc>}_7$ear3Uq)WhQJ)inX z>Q)0T7jC{)04fBRSH@agE!OD>B3Yprl&?VWl* z%Je-^dHn7UF*k0%O%<0IifYm<9v}q9O_5CEk|O}+qRVAhRYd^oJ&{^d;@gD(Yh4qb zo~gW8cpSs8;tiJ_HV_}H@vZl4rcJZ?VQ$@&*L9EJ&`Ql!>w7tf0zL4v9KicX*^=Ya_Tk|Lzc@#dj;yfV9451j~C zk7p09Ej~fUGwy%i3M0@16xBhPoV-xw;5INl>pTq4N}CyZ3FF?<>FzQ`iXuz+++#}W zbkR`d0vLlm(L+^C3^hXi@JxB;{F!Z*Y!N-+!CO9eJB@38LJR2}K3i&7?cwMk3vb#3 zy9WZh!`V{*`$H|$E*CUVwsVg?l6tsk(2A1hz@SUA^#uWIAXhHv%+W{ITL%2X7k`#3 z;s$)=(fcnOi$r4?Nj+XP78M}VID~kC%05}K;I&7dIBg8@nh6(*4K7v9H8^(u)D+Ii z$@WIxA#qFb{I9Gom_m*U;jO7Bi$pd0nkw_v?_WSwOx8-c*zus6HdFuXY-H9$5S;kQ=Xm_L=h{-w>)?L4kTZZnpsHLJ z0UnIGH8jwvAD`4U5cFd{zV#=sSpmV$7rkY`?}4~;_~{oL%d;W} zGlsyh8}7dF={e(!t9EajyQ`I#GPB;9ZIul9@HSM9SN`t%G#h;LyBF|v?{SkD-r3J^ z0*9a9F}qGbxOt~=F@^N?dl`G?M{PXlsfycEf6!yS##U`22-t!;wiSeG?8RMw`~k{* z{7-YOS@y`E7wG$1|7<<(e!tAl+Z8*{)yLw|;7A}D@rSil>l__j_CCAU<7?!(rS%Cd z9?}x}yZG?4%SuIFadP;}&)P}&OQ;OL^S-Y1 zbp<*j0SEqulTVkTCy@uQ4AhbrOVJ;!(xooT5jsT8;I9l0M|b&$9ecC?sGfQ(9tV>1Jas2m5AD?stZHvnTH0h)6`k&OrCk(TWT&%L?r^!=#Mbr>w~Eh& z1!9|tY9=`C9>U#&7~-Fa=F$r0w5fae!LlFfCRy0tZbM>8e3@(K#U-<{(``XMJgjm&t9lD3G>YAqY5a4KP%}VAD@TlPMcb zJvtBS@XS2aVWC7l?kz5-qK=i!M-wdLBqa=bm$q_|DajmR|Nc-o1nV&f0#pm?jnyJ- zkUYEq-Dl3L0pGi>k>y7R?|I7B!4*Yd|Gv$@obHt=bzM)#6}8gG;o60q2gqt|A8ietS7dH=_MI zTg!QRqVDNgsX9`%3{{qU+8A$-kNQLfB0`nH8M2XI31}L5MnIEMIr-ai6e;Gmfp{}| zDhr~{h{XclSx|?mnsfhcB*<42ypO3ey@A zK6q%_RYkk`_C|oLvo^=Pw+|{UUmN#?|>=s4x>|z?79%et|TuP3vMwyx8Z5qsd zaz5F*1|@ay(cFleph^O{&yAKNiONhT`M`~;rZ+2aooVMKj&JrfJE^5aR$WjYZ%;3s_$r+mNs4k2H6p)jLHpBfxr4c`29749X^KAF3f!LNQL`N_(ovIq zz5|U+goSkQZoG3r?F7N0V03*oxpplIlOL=_OUa+tqKBg-RG4#k@HD?s>ic-eLut0 zkF4@gR(ek<;{m6DCYd#EB$2dlKwpDXxDBPduF%T@mzDMv#uR+GabtQzH~J|P$tn^~ zcd-jEHUdM@WODBT<6>hvIn#sIS!NG68;i)4Uet9}Ye%n$54SYXi;88rA$8Mo5Z6ZZ zl!;N2AnDf9g5!QN1NfUdoJGh z^V3#zp+J?AYqz3Jh$o-*!)554^i2GdMYWjyWyLi9GWwLG3g`Ot_A7;*pE{XSbv-7- z$!S1^5%!rKh$#qr2P`uUd-T_r4ZGpZX@uE{#$bzVg|SbiJ}h_-aDQs2x2FswQ9di@ zH|eQxAhqE$W*cO1GT(UXhIFuaDymRGSn~@gTFp`xw1FB zsS7tU8bVK-cm-EC5X#o{aDHn6gN&`s8FJut=j)df-sEfq-7yBYkISri#FiLR%W@MA zqPu|r4f!75V}M3XtR%G*`pR291bOh49=XOq&Y#R#?j}@_<2CG z#R>Ep?B{HhPG++J`=F34Wv4(aAQ%R!zPAreB465vwp*Bs-d5h}8Y>+|vSo^JC1NAq z{pj12E|GfWRt$H@Tm$BS_bGN7d)6US%1c( zYV7g?;b%b%Y&i85D;;wdirLRqBXbaxG!AFt6ThV$3IL~ zd=h<>{#5aqR=~tD#!G3WWsIeBa1PolKxd_ zhihUm!MotdzWQmz;))%0SAkx3>j&C+kgZdK~)ZxwuFAKs1PmZgW0nmgI!WE#D2%))V= zLe8>&<`5%0Wb(rls+p8~L7xk$AyJIl$-5b}kh~1X6m#>d$>1^xlYj5>+_H?_u zAVuu&o}H5!vP>@A19QV`$8%C;eBYpDm$M? z6guDvi8LNZpC_*#2S8eW0{x3cm&V>Xfl85%qBlQ)cHqC1&M8wQd)LN}UcE(iCqYBx zpW2aBnT(wT4iMOYPGY9wH|fsV#eWdojnOQ+CJw;wgjs(gp(PE79iZx6f1GGQf1Zeidk)su1^=49OEj;YW=e3r zl2wnQ??79AmwqfiOz`lO!5ipR4kRTO+<6;ol6f2jEqn)GuUFxPvC4C(-MSyJBP8(z zkQ%C_jU5=RXa*xmlx5K_HMg~HF!7Zf|7Uoz#Iyt?`69|{URN7{+Jw?y$#1-QIRy{R z=@f`QmtgEg4pGVZC($!=M0XP-!4o3%nWqj7p7Dd5Pk4OJ4xH?(=oZ+!#ZRGEENR^T zK7~GADscU6t(380&a>FIww2y#gM>=3op1xUe_)fhWuP@0i43&FsX<28K8;$-S{YvH f?eUttLXbF8@^#dZdFip`2zLDS|Nj^0?co0bL6K7e From f74cfa76d181c95678500fa3b8689ee624fcf068 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 18 Aug 2023 15:23:04 -0400 Subject: [PATCH 13/13] Catch empty product picture --- kintree/database/inventree_interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kintree/database/inventree_interface.py b/kintree/database/inventree_interface.py index 4dcea958..4ca6a117 100644 --- a/kintree/database/inventree_interface.py +++ b/kintree/database/inventree_interface.py @@ -262,7 +262,11 @@ def translate_form_to_inventree(part_info: dict, category_tree: list, is_custom= inventree_part['supplier_link'] = part_info['supplier_link'].replace(' ', '%20') inventree_part['datasheet'] = part_info['datasheet'].replace(' ', '%20') # Image URL is not shown to user so force default key/value - inventree_part['image'] = part_info['image'].replace(' ', '%20') + try: + inventree_part['image'] = part_info['image'].replace(' ', '%20') + except AttributeError: + # Part image URL is null (no product picture) + pass inventree_part['pricing'] = part_info.get('pricing', {}) # Load parameters map