From 7a82c76dfca091ac8f2540d2b222b5f175e6dc05 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 20 Mar 2025 15:37:38 -0400 Subject: [PATCH 01/13] Create files_by_uri.py @cklunch This is the first draft of the new files_by_uri function. I ran an initial test of downloading all GEO URLs to a local directory. In the next week, I will develop more testing for the function. Feel free to review the function whenever as I work on testing. --- src/neonutilities/files_by_uri.py | 174 ++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/neonutilities/files_by_uri.py diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py new file mode 100644 index 0000000..a417df2 --- /dev/null +++ b/src/neonutilities/files_by_uri.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import os +import platform +import logging +import importlib.metadata +import pandas as pd +from tqdm import tqdm +from urllib.parse import urlparse +from .unzip_and_stack import unzip_zipfile +from .helper_mods.metadata_helpers import convert_byte_size + +# Set global user agent +vers = importlib.metadata.version('neonutilities') +plat = platform.python_version() +osplat = platform.platform() +usera = f"neonutilities/{vers} Python/{plat} {osplat}" + +def files_by_uri(filepath, + savepath=None, + check_size=True, + unzip=True, + save_zipped_files=False, + progress=True, + ): + """ + + Get files from NEON GCS Bucket using URLs in stacked data + + Parameters + -------- + filepath: The location of the NEON data containing URIs. Can be either a local directory containing NEON tabular data or a list object containing tabular data. + savepath: The location to save the output files from the GCS bucket, optional. Defaults to creating a "GCS_zipFiles" folder in the filepath directory. + check_size: should the user be told the total file size before downloading, defaults to True? (true/false) + unzip: indicates if the downloaded files from GCS buckets should be unzipped into the same directory, defaults to True. Supports .zip and .tar.gz files currently. (true/false) + save_zipped_files: Should the unzipped monthly data folders be retained, defaults to False? Supports .zip and .tar.gz files currently. (true/false) + progress: Should a progress bar be displayed? + + Return + -------- + A folder in the working directory (or in savepath, if specified), containing all files meeting query criteria. + + Example + -------- + ZN NOTE: Insert example when function is coded + + >>> example + + Created on Fri Aug 9 2024 + + @author: Zachary Nickerson + """ + + # check that filepath points to either a directory or a Python list object + if not isinstance(filepath, list): + if not os.path.exists(filepath): + raise Exception("Input filepath is not a list object in the environment nor an existing file directory.") + + # if filepath is a directory, read in contents + if isinstance(filepath, list): + tabList = filepath + if not os.path.exists(savepath): + try: + os.makedirs(savepath) + except OSError: + print(f"Could not create savepath directory. Files will be saved to {os.getcwd()}/GCS_zipFiles") + savepath = f"{os.getcwd()}/GCS_zipFiles" + else: + files = [os.path.join(filepath, f) for f in os.listdir(filepath) if os.path.isfile(os.path.join(filepath, f))] + tabList = {} + for j, file in enumerate(files): + try: + tabList[file] = pd.read_csv(file) + except Exception as e: + print(f"File {file} could not be read.") + tabList[f"error{j}"] = None + continue + tabList = {k: v for k, v in tabList.items() if not k.startswith('error')} + + # Check for the variables file in the filepath + varList = [k for k in tabList.keys() if "variables" in k] + if len(varList) == 0: + raise Exception("Variables file was not found in specified filepath.") + if len(varList) > 1: + raise Exception("More than one variables file found in filepath.") + varFile = tabList[varList[0]] + + URLs = varFile[varFile['dataType'] == 'uri'] + + # All of the tables in the package with URLs to download + allTables = URLs['table'].unique() + + # Loop through tables and fields to compile a list of URLs to download + URLsToDownload = [] + + # Remove allTables values that aren't in tabList + allTables = {key for key in tabList.keys() for substr in allTables if substr in key} + + if len(allTables) < 1: + raise Exception('No tables with URIs available in download package contents.') + + # Loop through tables + for table in allTables: + tableData = tabList[table] + # Find URLs per table that are in URLs.fieldName + URLsPerTable = [url for url in tableData if url in URLs['fieldName'].values] + # Append the URLs from the fields found + URLsToDownload.extend([tableData[url] for url in URLsPerTable]) + + # Remove duplicates from the list of URLs + uniqueURLs = set() + for lst in URLsToDownload: + uniqueURLs.update(lst) + URLsToDownload = uniqueURLs + + # Remove None values + URLsToDownload = [url for url in URLsToDownload if url is not None] + + if len(URLsToDownload) == 0: + raise Exception("There are no URLs other than 'None' for the stacked data.") + + # Create directory only if it does not already exist + if not savepath is None: + if not os.path.exists(savepath): + os.makedirs(savepath) + else: + # Make the savepath the working directory + savepath = os.getcwd() + + # Check the existence and size of each file from URL + if check_size: + logging.info(f"Checking size of downloading {len(URLsToDownload)} files by URI") + sz = [] + for urlfile in tqdm(URLsToDownload, disable=not progress): + response = requests.head(urlfile, + headers={"User-Agent": usera}, + allow_redirects=True) + + # Return nothing if response failed + if response.status_code != 200: + logging.info("Connection error for a subset of urls. Check outputs for missing data.") + # return None + + # Compile file sizes + flszi = int(response.headers['Content-Length']) + sz.append(flszi) + + # Check download size + download_size = convert_byte_size(sum(sz)) + if input(f"Continuing will download {len(URLsToDownload)} files totaling approximately {download_size}. Do you want to proceed? (y/n) ") != "y": + logging.info("Download halted.") + return None + else: + logging.info(f"Downloading {len(URLsToDownload)} files totaling approximately {download_size}.") + + # Download URLs + for j in tqdm(URLsToDownload, disable=not progress): + parsed_url = urlparse(j) + filename = os.path.basename(parsed_url.path) + file_path = os.path.join(savepath, filename) + # Download the file + response = requests.get(j, + headers={"User-Agent": usera}) + with open(file_path, 'wb') as out_file: + out_file.write(response.content) + # If the file type is zip and unzip is True, unzip + if unzip is True: + unzip_zipfile(file_path) + # Remove zip files after being unzipped + if save_zipped_files is False: + os.remove(file_path) + From 72563cabd0639a3c6080e8bbe67b98b103ac9878 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Tue, 15 Apr 2025 09:05:50 -0400 Subject: [PATCH 02/13] Add tests for files_by_uri --- .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 723 bytes .../__pycache__/aop_download.cpython-311.pyc | Bin 0 -> 52493 bytes .../__pycache__/citation.cpython-311.pyc | Bin 0 -> 3052 bytes .../__pycache__/files_by_uri.cpython-311.pyc | Bin 0 -> 10173 bytes .../__pycache__/get_issue_log.cpython-311.pyc | Bin 0 -> 5498 bytes .../read_table_neon.cpython-311.pyc | Bin 0 -> 11874 bytes .../tabular_download.cpython-311.pyc | Bin 0 -> 23515 bytes .../unzip_and_stack.cpython-311.pyc | Bin 0 -> 69197 bytes src/neonutilities/files_by_uri.py | 14 +- .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 541 bytes .../__pycache__/api_helpers.cpython-311.pyc | Bin 0 -> 34545 bytes .../metadata_helpers.cpython-311.pyc | Bin 0 -> 4149 bytes .../geo_surveySummary.csv | 29 ++ .../variables_00131.csv | 342 ++++++++++++++++++ .../dhp_perimagefile.csv | 37 ++ .../variables_10017.csv | 54 +++ tests/test_files_by_uri.py | 76 ++++ 17 files changed, 547 insertions(+), 5 deletions(-) create mode 100644 src/neonutilities/__pycache__/__init__.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/aop_download.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/citation.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/files_by_uri.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/get_issue_log.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/read_table_neon.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/tabular_download.cpython-311.pyc create mode 100644 src/neonutilities/__pycache__/unzip_and_stack.cpython-311.pyc create mode 100644 src/neonutilities/helper_mods/__pycache__/__init__.cpython-311.pyc create mode 100644 src/neonutilities/helper_mods/__pycache__/api_helpers.cpython-311.pyc create mode 100644 src/neonutilities/helper_mods/__pycache__/metadata_helpers.cpython-311.pyc create mode 100644 testdata/NEON_uri_testdata/00131_allSites_2022_RELEASE2025/geo_surveySummary.csv create mode 100644 testdata/NEON_uri_testdata/00131_allSites_2022_RELEASE2025/variables_00131.csv create mode 100644 testdata/NEON_uri_testdata/10017_DELA_202306_RELEASE2025/dhp_perimagefile.csv create mode 100644 testdata/NEON_uri_testdata/10017_DELA_202306_RELEASE2025/variables_10017.csv create mode 100644 tests/test_files_by_uri.py diff --git a/src/neonutilities/__pycache__/__init__.cpython-311.pyc b/src/neonutilities/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b30405903eb0a8a424ca519440f1b1e76b93bfd9 GIT binary patch literal 723 zcmah`&5qMB5Vo7NP137L3*Ag3xKk;kEoRZL=!$8x4-WajZi&Q(GZkEeiBk^+w2 zp=EppQ=ld8lO4A-7nWI3mCLZ7Z}xO6B&V#ZywdK5(h$ z@lTeQ)O!xj-xp%sP)xK4()M&WGu_iT%LD3lB|Gq zS7^OF>Z8NJI)E+u#Pu(#^U_gu2rfN#YU0$?G44UNMlyZJid-yq%6&|K+>|gdWo1GA z1bXg538?Sr1~j@oeepCqFoS z_5;w2&NH{)-#|vGD21i;Nxi>~o+)@C$)D>5^l{%}zJR(8FvcBv&<>?T@7w=LhgR)S aI`ps|N{1e|L+Q|?b}0XHey8aBFyAjOh27u) literal 0 HcmV?d00001 diff --git a/src/neonutilities/__pycache__/aop_download.cpython-311.pyc b/src/neonutilities/__pycache__/aop_download.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45a7e0d91ceb354273b63d6f63407ac88817aab7 GIT binary patch literal 52493 zcmeIb3v?S-nkH5t0HR2MPw-8VD!xQQB0*B5UesHnUY11bWy_Z3W`it{ghT>#0hB}< zaMM2CG3}$1raN*>_tjUC6Ug*g9#wU}e9y3pVy^zhGy-`4=4QSG-Wb zehV)Y;@39moN``ps_(ikxY)Cz3q|a=_(HL4m-8n}rb;iAB92{lOqNZRUnpl`5n=ZQ zHwza`R!mh~s9@p3$;zp!3sn{&Pw*FBs@4jRaE%t0oi|-S$Po{L-NC10?dHmXvUz3D2wW zyjiYB>=wBOuvM-FY?JE%H^}vX?eaRn4!Hr)BR2wWl-C1pk~`!kWNXiV~DVn#AC!_)rJz*qo9ADNYqy7bh!lhSA?q{x9FDi$v6@cY7% zKyVy6%2E)?P2j>q2~upmXp|&MJ>iQ;v!NO3sxKHZ*1=m?xL@{76Q;(L(3EsW3i?CA zr|?!F62OZivxkuYQuCf+8I8=I)2=%u-((2+Uq$7m5JSL(e{!1epkd2fI1!0VhX=a5 z#}R2}#5)?A>K;1uoM-?-ls_T;7Qlm09I$#gz?q`J2 zbC#$;A^y1{DDB2;*;1=L66CzgjG!nr5qe+!T+)e*Z2bZ9`B2XbM8|ek|M%E^e~d?k z5 z=qp!!VGO(o3N#uT4+iEihG;BOr0JQF$-tqpO^EDFAxU3p(2~gn2GVQ>{=J$nqj|-!L$_^p4&m~wHC6x(y3Kg z(lIpTv8Ekr1E(G6(Br6S+7<|oh0wJ{4I5$VOwz;W4h~#=Dhy0s3DjcC^}k_O4W$zK zZNz{00>HfRV1ppmFHIyHy>SP9Z*5QZ9gGj2Pxd_%zv#Qu=Zp9G?ujMWT`AYmUs){$ zPgs5<5PYy2S_QG25@GNB8mSAWQAx}P8bF+ArK1dKn7#muB1vqUX_i1GJcUMw$oNzRK3rWEauOfiq8n4guRHEKnQ4wmA6 z*`Af6_{bFPIa9PnZAh_zrC5_yD>3IA_NX0i6w({fJTL@K-#Q?U_1`SI5(-}ND;W4R zVri1bL_FumpvNqL8J0+ePnrs01^_Q1`$iCflS?m4>=+w_JO%xQ;{i-z?Cp-@2QcBE z^pE@0>yvbSB%og(p=*$?bACmOa_)eKB`y1>{UALU+Zf4II#w`sY7YdbXCjY5L+G7h zmiGYa6rrrfz_vtK4m6t2<*YWSXG985gk~nedVrvS#09&8s754k#V-YCrbhhArxA~E zMA38E5Bdc9s!~wJKaH_V!*Vs&ABAkayllQgeJ~r#jImLX!wZH6nw$(>&2UW&aWg>> zd9(+b1OG?xPiQ~j(W>#O1n$r^-_$htC{`&G5!|z950PT(1q|22lwrq~o}M1wlI>f( zJ<{<5?VOb6jtxplAN7UJc;V!uU+Cj6WZvn+J4YV z2*0Gsdd`Xx2ZX=)_D;plFHatrL<~VFeZuocRy;m(000OC=c3T8>d8+RX1ZJ2!NQkE z(giF)qes0rHKGQ_wLoMfoR5^LeI-e$Krmgl3ZY69?jOK^__qKcyN4};ShO$~cQoO9 zhry*o{FmPYyjNbmRJeR7QL}Mnf1=!*EcY%rQq_%%PtxB)!9qc*pm?z^UeJQ?9R`>D z{FmPY{M01w^CVj11+8}q2riBBUj+I0Q>9)F{YOpWimXVziy`ZTB4K`&*aX4@>b$x zlTYQDl=ayWGK`ri2rZ~e?Liczy;Eu@noPf=%4px_QG{sG(vG6H^N`}7glNIvu89(e zGtxSQ`iwP@UMb=u)CZWDGd0OkYK?D~o;dsDkcQSTIQ>#bly*zcwQI5lYIrYo0k!o> zhSQBzbN7{=ZhF=|c&g8fIkXpZb5C#g*@Fj;bwe<;xvOW3x+c`nO21z;*}krxt?kb< zWYP3b>o6t|qGERtKvPW3cASkJ(FmQi{9;5TszR7SXD{hXWW(Gr6x1wUJA^@#)(wHwm zsK#JlbG1(eB>V+}GpHBCFx4UGzZRi7@zOzd;y_ysqhe`0Yp|iYLp1dG3PP=*Z<1w* zwZ3$aq!$MAN|HZ%j3J4E4tWg5I;}#yX&bd;+M(7oZD#||V^P{L3#6@6@|LunjoEWl z26bqs9as)b`bPaE@Cl4V4(K0-)WUzw<4osG%jp6t8lyo9&0k6xfpSXFMq+|=K33dV zY9MbVG@Z^z^+EE|HX`6@YuF$06r^p`hDs~FQ<6pLXd%vsML2CmEQO{oWdp@5()#!? zBq))zGjll>E7~*ZPd%qpB2-USWCf^L+2+lv5Dk+;k znksQW5Zv~?3@ljhx$BnuR{U{yf5P3LboVcaDW^N`tiR`SUq6v5J&(0_(X+Jn2E^K% zpeVTO-|qTW*PGopx|cU3%RMWeWO+Y0FR}ESC2y20ITE6j6s5Q*-LH|pQ}pekcS_zZ zS;G-K(m(F}YF{uiBcZ+M2A|y5LMTbu2g+k0r#$2l+xl#a*%Hj##tY zwo)B$>b>0`7i$vYzNEM>F75-2ln)@pi=ntEGPLuLJ-TvVQME$wCRz zaMiyz_^1 zVwT*@UescaO$+PX%$}S#o)uTNW>~M>44#=mB=$6Fg_!X~6rs$!F&o4fHr#KFl%THa z=Te5T60qpEGhsnKgO2xBF^-@~YriXUK?8$@QA*A6o@lN&m|R;7;uI!SI0|Gk0=X8u z6`t@Z#7;~G#wW1cHPAWEVsM(kb_s|VV`&k+;qMKr?|DbVSER{M=<>`oqcEKkXQfm^ z&k5-iMlmh1NBohiet(c91p+Y{!eB-bXfuHF!6pa9jUblb2z`?ShA^8Ed@sy^1sOHo z#}b)FsY*;)S#?ROO5(GsNi@t%W;J0d$SX^1W*Tu&7FJiVG`r;1X>I0X-HT!oQISEM4tvGm={qKXXkDN-d|WVAZdLY3?M?%-w7&?3I&@rzxsP4zn2%zrA`2zM#mAgC+H$zeiwT|Y2n@Q3q9X$IqpXZ60 z=H0p7Tn8GjV0hG#r?Ek%jhuE##=bJg z3`|hLv4Ap#Uo{b|WWlny15?CiO$J6X8)!;9<_v{I3`!ROu`WJ&7_9{<744i6)0B2Z zLX2U_@JSxKvYVp2%`L=u_;gv;O?hlpJYs*8JxC!;(h(YWs+6mIb^lk6Bg&sq+%C@d z9JUCU_3QiZiIw1B)>VTySXZ5@?V<~B+*S=XqPG5BXS{V=qGmgolDMt%e%+?KbzAP# zZAlfBEQp9$UrroFd8)keH@J}grPTgC$9D_fFL+=T>^1it&Ui_4+(F+|WyeZIveLUy zm~xiH-FMHS$_+|>^Rq3ocI z!}#;##!G}xSf93L-HKZGsnLxOD5F6eF>!-BAm zl+a5akd=MFvd5te;wNiJD~IS_AAufM;5UEv6Bg zc|kdihqN&lejWg;l0uvtro29i|+SjgDicWrfdY;{Xp6Sn50tvPOMzVEDy+v-;H_j+&UoAO2x zCT-CFq3~W_RzJh$-m-CpfHL!sncp_{R3ophZl2kO=DHq7t^#qIJTn>zck|V%Q@tKS zkMQL2rDr#?N1NE>>42OrBS@+YM4;Jre%htJGfdsc+!B@464Yadjd7IoND;f8LU~bRU>Mk#bgp zmOcXK&mei)a>Xb&U^F9SkD>+4;@@z&Nw`j{iM;FoMjI zY63hOy+3%W#oIz3q}=j>HJzvU(*1HNG)Jw%Lv3L(MoCmpCp0|f}Zew?TSh$ObKXlWk_b8*tWmATxpj%e$qt2)f&?^E^hnues$*#+1zY#`0E`Fh81yxio9L4V2n}E+zU%9LabW5X%7Qt}4YChh{`D zqjH&{#gcEFR?duYb27>WEjshY=qQM`w!f0m2^(PfbV)>Dr4C4JJOhldXHk2SPH4o~ zv@X})M@dz(KA?#dSdwxGg{L`%k$)zD@n3_^Da|M<$tNudb>dOiu^P{5C^onyY0kqY zus$|{fl^}*V-pvj%P^@K=Cbozl0KDd)rBA#v|%BsY1XPYN!&q-9ju({oJJEGGUF4P zcWzXI{w^e7Xo>I)HVLtwYD=r`Q3g|XO$KtB`+daQizF4dwBtBski%6e;{|W)9UQ`J+2fA7)3MT;aCRr1-85|n^*Mer?xHWv z@j*7n2iY7CC<>zMueDVtX9=*(D>|LWVfCUj?&9LGD zIs~x=sjlV+^~vcsz{J~_nwIgq`Xgsaz`+X=-Bmw~0_NZaK>=-A9F+nZu9podWMZwN zfYxQhJDGUKl(Q(HWmVar2$GWm+Cc&HeWZQ(JD`9kNE=z|(+0;Xfv-l*e&^JXF$;X} z5A4IIQ9xs{vj{UNI*~@@>u83#v26OFz1VT0&$EiyF(zJ%LF7WCKRJmY<0Lo{RQ#0L zQ=dIEPMJ>V_|Hk4?1~S`bGlTXAtsZ3oGd95^fUmVtsiPvN79InaWPS0qB&}E$}|#s zj>yhGM1YYUSLrwRzOi>{OTyWdbT$#OiRr}V#kh;UG@4^-4=Jy_nugH zS6qKbT%Qn|l44U_Y)Z9u{P5WKj{UWFU!rw?vUNXh{njOv)Uy3Wq38&sNeoU}4rMOz zfbu%ZDAG|zkpRmCaRYK$O*v>(GTv(|0z^4vi>fMLZd_5WMH{qrqP7z1<-Ff()X*g4 zIZOf)i4ocfgQDffU;( z8*hQv&=jL7U%bTTOl(opWQ8AEHgc70zO7rsRi{+}4lf^HGAf8UOU=6%0W29E&V_H> zaWuvqDw!s#|2)ID1_|F^WBC3W!#5yQrq|<*N8tN+y>b_y+uwEJ`4{_RbMI4QK!zR& zvhK`f=$umlJA;usqpYzRh4&Qy3p3affVKcr!y7 zWbTHXtNa)9GE@2-Ye5ZC3%=@AlZ4ha?N9r=6d2>(iwq_z{6zG)vl2JPsaKh(n>ZH z*@w80w@T$pRAat#e&5vokr5?r1<&$uOJ;Oo{}|e2SZf8tv_-4Fm2=j?2q;7GnT8Ui zHAR|mm~YCl>BY!86!ZB-Tk9yV;NAa-kgf_M9TkgDCmrkJ4O^0qt+(6mIIvvU%k6PU z4EY5A0@DR2h-(0(wZ#d`eX)2UpB4fT9Xs}NTpKxXeI@WJGJMVQTArCYEm+1ZvgK6~ z^Sogn$S7UUwwcZ1%3(%hRFsa{7V?my9Vyzf-;6;?n>h0!ldy*hOB*4-?gx0wa$K;m zF7bRh$SEWlJ*iHNVMqk$L|teJb3$)c_ChTkbjp(|53{=`9JmivFCO;zWD&Yi=+G( zkRbpCv5}slCtrD)3PD@etR!TDfu=Cr91l{b;* zdsMtDR6LaLUTKA~*^J>-?S|zm$y)Eqi^VjJw<{jv4@xJ(N`YMDuQD zwPnm|UiX^EAN0H|%f?)^gt9E%&H8fLT5>P}D9~R-irIM>cZ?%NhM3merUqu7$4E_# zw8ET=`3hzq7okUIfvH7KV}{4eA-aYM2PD3oUt-EelMVaz=L=w9g7WWyp-8^$$jodp zhb%4|WXA?P=F0_{cxFt*M5C$2CDNW`pQrx+$$HK6mXlRzazc z6+|-G2(pu{;#|zMvgkd#IbTSUi(>^Z6@+odk31)SvS>MQ}uh$qnBB|~j3lC_*sQ+eXaGefG9 zmr1!?RjK18Qnj?H(r0*8rE|V_*ZkDWaR?WYqSRzZqn)@IA#iKC|DS$w)q zp7b#JEz?qm>NPj((2V<38Iuu5q_Cf>Q980P(b;aO)0smj4(&gCsEd>sr2U$#nRNI4 zFqk1Gw5w-(Ck3`^fgNgKbFUVF%Knaw&EVse;pmg64ybrFpeR=nHuqEV)MlSr*sWR_ z^-}AFMH#n$dKr7Yy*)iU*iKh_XS<<{PaAe7&e77ANu7d_F;MTzv>cohXx-Yhy5c`?=~`q*usmfdu%W|0pX7xcG40XPAe$KRZDEukVG9| z;ckdMT0jjL3!=PEp??5i;6T*w8)k~}5A!-DsL^Y^;v~!sCc@ZOSk+JFdOtHx=88;+ zP!6ITL!Kh8eL;KlY5NSc{S~&7XNyIcZ~B7c{)Q4g0RXu2L`62UA0(w0EFT(2S&JX8|_JGNhxS#BRtnt49OaFDjZ-UKCJ zgoFVuLaNBs_m%G=<_hs5-vyfN_luIszz2k+TxmMRjq#-yz=Zfm5-)r*mYZC%o~E^b?g z>tgp?eT&!Lj(sck<~MG918hw-u{G5@6YgC}_pU_I?qt#K2ZGhUlYs>b^wAc#zrFig zyWiYJ{)(_H`V0%VZ-+tzzQ`NgDYw-P8C-`L$A3i zRn?frRUcq_zrr;K-!-$!|r$)$l;D zAu}TcHXs^xUHHfrcU_?G+4E|^b%EBa7n$sp-mw&1w0wfk0|%vnK$zyppLFlH3~m;F zy1DRBwe_dFst%P~|GeCe@L`i>D={aS6NO)sQjK|Om06}{Z?kaQR@k?i>{`t%4Hioz zd^Ie}E>S-0lY8qVTPZIy(=$fIkP?`a@5dZ#8Ae1`+m?#Tby$_=Yxx_0&85BwEW`?8 zg=;Za5n3{P)WLwy`mnksVw0w8->smLfu|*z|7p(~_twS~QjyD~%RMOYmD3Esd5% zonz#7%H~^#RVABK4_$*89=dRhHv!!og`{0m6=2kqq#y=YPF(I}wC;lzGpS{=)R;CL zqU-KnvK&EqF;76`1zwKLkYi+Ts)>y=hOs6K6WxN3)}*@Cl{#sfl}+&!+FxNZRm&*8 z6EBJ>7hCKZD>*hhV+Be=9#~$oqIa}=BY`opHT6!wv=Tp)zB%@b&4zZ)yWtg%EUMNu zhO*gJ$YH7JJOI;JWoTi(DzY#<1*bSuGcavN>85x;AXA z3=}H@%G#ok{aC|r*r%HH0%K(9%bJ4~Oj^}is``GmA2Gg7*+mA& z!>sn?2`vFvW14Cy8%9G%*3?=`(dkr~M0SwUFHI2@?ia<{p$r8^L1_*y6Rh~aChRa2KV z`$aM^wk(iF8K7fO2D5qwyjKjfk{}Kth}9x@Z|Oh*XFZ1AFR|K&C3TRXe=)0ls?Iy1|~-B}jMW>Crd*` zjnHApo+mU{(s5CKacj&oOuyS=9*C?+J=iv9BQh~(1(84yBmxiw0-P&p!;q=#)HD)S zNS3Kwr&}UO57X#N=Z7I|hx%&%I2@5ohneDc+6s3!X)E;C@tBno*0X^ifbW{8Tp6Gg z3N!sZ?VO2BX(Ii!culQRTAbBFMSL`Cmky$J7^KP{QA&2MKWn*`w#=rBOjyA)>U9(a zy^dea@~c_Oh>thQh=;;tn44jMwGd~7ONdw-5Uh})$S;ToKE!z7-wp`w${QQw4c&?I zo@9B?{Bbb4rR8KQ3wzmn71eJZx^Za!BsRoL>XtSwk0l{;tLwd6H*lwJASJegeXI9? z4`rZaq z*SB`Px#z~7*9RB&FT9X)RebZwH=ca&;IieN!|xtWxLT5~mbj~B?fbQNT@80!4ey1Q zd*8Y8?v?L{S9-sD<^3!1MsLE^opg1_UEQC61+A=K+MTHIBr81gCvjO+x+uPW^7_g7 zL-$-Ij}G21tz4X0cnLVs0{5Nn#b=gcac5`T*2%?-w2`8Hh|gi1=&OKcP82HN!25^z z=O2jsD5mm;Eocr9?M2LEF4-8Em8ThJzF@mAPgg0V}~s??AX?NAYBgqczk^M^!fF?2(4dvnbX9=CY#o)*GH@32TA(Cc{%ZM5uW`Kzn_tw^=Md z(qf2_WqK^nk;+KjnGt=WEO*jm8>@usmfSt;qI zS*vYa**%-uCPMae>L+_Oa}ju;6$&_W+E!6N8L_Hd6fJ;AbycrgBX)7LFjwrT6MS}w zuMA5j#$}i}Rg;{Q=p@aWkT`JfCs}e)Oka={aXTm41!3u_4k}cJZq?pD&AM5ylAG zW-puMv!r~t+Qu4L!DR-NOyJLgyWPyV64Ip5VDqM7X#%#Hy9J@Gc^IUe@SZv|^wdf3 znL|%wU)~D|vq`|rSyP$?SxZmNfVE~G=}0rLLsgLTnB{`V>1s&1tgdCaKpH!AtTH?$ zrc9&j;xKb?Vph>1g5&5kx)O+5qV|I)4m{DWrboDE7+{KlL5t>dgq!4brkgw` zai^WYKsOVRtGdnH7m;2x@})wNvB-g!RldBPA;(IrLfX1!ZNqpY!^k~HVE|??oU~C! ztT10M5h7_V!MZ6cjA3s84A!dE-kK{(Rgir4JXmZ6E~Buli`k-F))knfmKGSBlr0~1E#w7~ zx#2O<)wR%l$l!~52<=S1sHF~d+3g`se{!kACHYX*(da@K7uk1FrN5ZWJ}nF>{~_j` zhy6peAXPmCvq8f~H2SV)JC4Nx?AfsRgiIyK2c}YtR~0ZX!o#9-#;u5l>-ZSX5KH4$ z!$TV(Z7$%I3eQ23z&Gj)I~N#tYOOWm%?N^4dzx?fG=x#4b~x^4Jol!P$NPbW`Qdq3 zfl9KZXpu#D#cbl8E7e-z09|9RX|7Cf3+Mp5LmIfd!Uyo(joy`h$-6${1*iN zl)!%m5Ox6&vDQB7P5>={vWJxNPY|~aEf?Mh0HfTmS<_B?E)v;1^ zD{}i>qVhnp^1%EMZi&u?Jqbr$(owhMyW@a1?Rr+G2YEu#Y~Io!so~BtwY1s1+p_WM zpOS&T{)~r7k!p@q)&OY>U@ng-QX!j$jVs=d?D2Jn6YCBq0d2MO#}{jO{yP(n>ZGH3 zsrL>vwH)g{CU?p4%1sHeGbwh)#ZHD3MD=XH?M!qGCOZZbwzm1B3lmF4AKRR-o`{!u zmyZJAo3M2!ZR8;79)Hli{7k&88{dSjCu!@6+j^+Ep-EE1mU`!YDPXZTYSbRk7ksyH z_qFQ3*GoQ}SYcT5IdU%f9Oh*dTxN=&amnXkOFr=tOTN`!n8fABS@IQ_mV5q6Lp%GaC&Jz(~=L%@mAxqPcAa2 zibeAp%f0M)!5vX&)F~Hdw$J1eu7#SpgT>?H)SzOGJWI3ld{k<&qG-{l;-oA)MJA$f z%H^3dFIQ+7H~wY`ZHgnCGEbw$=2T-PQJbhNb06!u3yX@b-&yYbuSx)MOu91U5XZKVxpYp_iS>@ z%~tccgUWb8_fc!Ekdj^TDjbu zc_$N!R%ByLh7eEAGv|n71Dq{FuMoCf&EvXh7U5aJg1=xXwB8P9(u3BUm7WD{nigB+ zSmx;^T|^~sGUxk7^+GlDm^-1z>{_MATxA-sRpvMKv6>fkWl_59n2qY3bu}Bu(JI-X zQ~22U?lQlJcJRKJ|NGYhM4LR_z8#R9vNf_KgL*Lx{!wmHb{y zqSQTM@~U$sK>POa3JKYyaral3{+#7ULKvd*B`73vT6_BCz1jvZx|+7Nn^Po}%Xp1e zv|V9-(gGJxP5FZG3`eK9@N31-iy7|Hl*;ttk!(Q|Td9X;$bqpjs1@RzDToc3V@wh} zXcc7&0%|hciH_P-+@^%QWPchiNaDf-U~s>Qg!vji5@POsNdQf0AZrvYgCmR(Gm=QM zlaX}7(v$KJvoTaP>=vf*=yvXF~Tuk`=h0piu$O2PFGM z4mwsKo){tbJ~+liMjYUXAJj!>dN4T%^i=3r4|)%WgIw*T`tSpn*hv<{LZw zhE5BNp*g9$t6fg>U&)M)*)er0-ZW)u)&otu2P7jk9ESH%_Z{15&}`6P%gNhoaBYGO#XlZ27ld0r&3rFof8j#Ga4-&6(CG>t>;1ZUOJjLE46 zD25?Lj2Ck1!sr}3-GGS+$;?s1Nv17VjW)Fn&qK*4bTy0v$}o&i_=Ds0Ln43X98k3w zI8WqeMirGKS6!#fIPO&zflivBnj1ETAS3o+KZs}@P2?nc$I%Sn6d3J*Ix=O-_8EAl z66;VPd|Khcvv8d<1-&fo26=z+VG7{-MbNll`vS(~KnXT)?Ey8w!E2CsE7*I}=uZ%x zgeaeeB%YmCCq}c#fxHx;{Q}gMm5hj!|JtbE4@B$!da26168up(D^DWcKE2Sq7M(mh zLJ{cuWg_&f8?)2=5L-Q#&Z|xiVK}W}rO=M6BSA9s<~g>_ZH-+lIKBd2QEO;=^Lc~e zQfqz830j9V&|S15CicpRFB^`1zrHVH&wJKrY&Vx()1eF}t(g zjx;$={8MTd&eHtfyqzdPKPTu`zDbGKrJTILxFfaPB*1Ynx$5xgZoqGd#54Cc(8XpN}^`T$}Sh_jSzK6(5iFox*iYq_>K3}zAx0Yo~8TB55O@?)s@vI;_O znCJWtYY&1x<6sEtndT|xVWT?m7~@CJ88-;>1L}`8%|nS?FS9PI9lnaP64lr%btgJh24y{oCu8%KeUmT)x*fi&U_$i@itJOE9}+|5FLJ4I&A0< z-pIAc!@R4T9@=LjV_n-n$V=OZ#U#x_sx1Jyj)qQI@CVlqwU*0<{ooQJ_0nYK=8&v#GI3TuN7q(o-7h28wS zShn2!L(lg-EBn9O`F@YNH zEU0{^^Fm?hKKdi0SLL)w2eN=Zhq(`2R7p@sx~0$theA6W>3sONMr$i%X_L0Wm1r<+ zr=xG-3iJ?WGIrn+_d)hPVt8t@w(;Otn>ztFt{ADO;fNE)mMeEDs}zA&0wf;h7Rieg z(s%sQ1vsydsh)?^Zf&Tt6!?jShU0B%_!%x~mSKK>(>9v#V3Cf)LDN=r)3jwQZN-94 z`B%s)Tmzu(A7=gT32u_0yu zY9H}&_7ShC`-s=rKH@cXA5pvF`-s{V?IT_z*GdJmY#)*K9x04%JGe8?|1x$Gk(5~f zKycX);)_Y~#klyQdQj^%wvEaBLCi5rCLOwj>gMH5D}iM5_S+}#G{fTR zi3R69XYs;|31@xMS-s%g9 z)Oc0~6E!_JEytGs>hZ;*6hb>Aly-5cRnS$zUh3!xfqLA z4B&gq#(x1nYFlt7#G^^^Xk0u>XsBEg;|>Ym2YLAgW%tS(mj`ck#mo04%J(J9_bnW} zS5~>y5U)Cl@2yt;3z({3ztWC^mG_}w<$Wkvc^`^i)R5Y`Emd0fw&PolCF`568?Hsy z13PZBXAJy`V*Q3d=3cH4S>WY}^$Dp_m(~&z$}%A3DqR>PJeA=!L@0D!ufu6Z1&zsq z#`yy&ZCb<}xHtijDC|xacF!NY=O|q0d->X{*A{zUiOt7S?)n8W>z)BCEw_Asydhq` zBT>F1S-wNZ%%OPIA;Jv(-ge(U{ijtt4EXzuCA7WWIe$1z6aQa&JwtZkXVq0FcUXVc z<)ZKo+sWOIpY66H{8xen;lHx5u)T1o-1=8#7QlZlZ9kna#A~Wf4Ort|7lj9Gr}jAF zd#ng2EEa?lc`Te?c-n1Elv@BtNkvBc=sFA6U_bDh^)=gTJ+Jk?w)wTbzt7qWMs0=H zv;6r+$j(Cj%vcob>(+708zzucNRP`3bJ{*{j5An}G*zK&>!q`(>rHuV74quJAUUCIR{+sb7f%4c;o>e}A z-mrb9@?@D?e*W^v`MI$pJ93xOsIRfA%?#@M*{!4Qh*zzF-&Oh}G7L7Og>r$e-C~Uu z8rA>d0fZYg6qxhQmNLW%%Gt~yF&Y}(y53REgSl0 zCMGR(KGZK`#nEE9D6_ksOSeL)F!dzzy4k)qh~9?^mVoILy58F zq9x}3ESKrV54kZ{&i1w8SqbH78v?UG)pf; zFU?x*xLT8f+A$yA%yXB%##~$MmY;@x0dyk_ZBw^O*QIog)atWSdsJOw<W;{!zLia*%xPrL3bAUr?NuRKz3P6Aydl^9H+mL&7J5;Jc5@kwJ>vBq zb1ltRt8%K1)+{$>YMu#2Yt3(hdUc>oHQ6b=VU;~O@7vaB=Z&k<-Sn#P#%Ab%2(dai zYp9Qq16cL*x)qcii|`Ne&p*)KX=9_>6gStBMp=j$65HbeWQ z3)(03+4YJw7`4hwx;YKzbj(+=#%SYmlU7>eubEl{pWfWMIdb=>kGI(RHQ(;ZHIrtQ zQHna1=hkSS-st*VrdDgj-W-)aL+n0tIbf>Si1w_CHRVn{nl-kek5w)AqqpHLGd5$* z(dK2t9Gbzqyk%9MM2oj(Xz?Y;clc+SBvh+D^0wUV6wRV1=6o{g$6BKJTO&tZGs>84 zc9}~I?ipjProPy^M!Fk}>1knkySa>VS*DccC$To*(_r4Gr635|Wdl7oXndN=mW7{X zlBTZu7~hVznM?7gtJnr3)r)R0$Hg2-a!x@xX5@F|9p+f*e;d&MwxRzS<`#KijrML@ zHgG+eipV?7sXpooJ$SpR+%4wVruntV|Kblp=U2KSp&RXJY~x~HOiz@U5Tf$r&ZnY=D} zwXnI&=66hQ=$flhk9_dvpn0w^_f3q!Lm6C|OBmZ|!u4T8osVSK8L9fTa$-CjHK&B9 zqnW4Ctl5s~o^Fcx4KX%FJ<$SiuE%odo{k&hfg8H{gt>mP&S)oePdk?lJeJ`N4_fRv z2xN&9J;6B%+pJ>!@xfBQ|5f%=v(N+?Ef@o|FTu{ zNw;Z~cAMYSIqbY>S=QPjJLXY!rE?X!rzRXnyN%TMvGKjr{2tn23v^F^?^=L3ch7mB z4OVnawcrcto??5H&JtvLr!2iJtLq#Jsp=LiGlW}H8I`5=Srt-Y&bL|_m8JBRLh6@q z8tvzHq)fYy6ht)JR_H1+oYPL;ehGVrBhX*ao*PuBNQFjCBgg z40zNeQ&6V8V{{R~bVW7KS4^d0A^;^(ZU&7cbflQHHhWYY<=6xMG)-B9cE(p)!|0=5 zX$|Yc`5e%ZQJOP!KJ;a?dhVB`HN3{4!l3y|V|Y!k4luqLf%@K>;~c7qsy{bU-+~)4 zYSkIlnw=?3F$ESNnS6gR^2@zTu$UUXA>CB6XjcghcO}7kib<9D%hFO_owBN=%$zpB z)ci4&l=XgWR5DSgpxw`H*}AoV%hpZ_zgzouJl_d*MeZD7_jCO_dV02P@9UHh=TO`bVW_*KzqCKeQZ5wUl7JJgjsGz0}VBz`qW5tY7|r8&YG;4w+2?Cmanv^ zAEgnm>Cx(nv1;el^pa0~rA5ujz*kz-Y{32hT8o+yjNBTZomiHu2d$K0OHtqM#ujLn zx3xF(u9vF^tdLeJ?2>#DXr+!I0c9D$cn(%Zy;Gyvxzsy9fi7g2Ha?);Sq{A%`Yv1E z+Okyk=Jp%g^;=A-bM7*6B-$bZD*oz;ZZxOSmEoW%W^s^Nj35NFj8VR5;nN&HZfaZK zW*lEm>y_ttuv6OBM<<#;KlCNyN^tX%iVCO2kIHY>+-mxu{$@SyEWDsUVF>)4)kkrm z@zs%%wgf+WH*gv%Q?f12zKgyeZN9nv*8UH6-rR{hixz$xQ0)ze48EWAUcnab+>8fD zn1q;_26L3kgg4z73hJsIi+L#Ua|coI(Bum;lLGEjjynJYQ)pPbdYO?1^Yx^B1hjJ- z4Qc@8B?8|dFi(Is5wq+zlz&F|eoEla3H${C+B{MI8v=hx;Aa47+kq4N&0`ao;YUW* zA)xjKT}ZuoW1HCH3aQeqgOTV;onh z!SIiRx@@PE(51BX;F+^?&z@54ho)z-dcJ5Va$X> z%gL~UyF~25u?>L$FdMi8Ku3d(D&b+Wo=fLbC=$xurF&3;w{SMvIGpa&(POXYMJ*Yb z{OeZZ)mR=7=vD_NKU~YkN&dS?CGAiNa$3~7SeRNwlID$vY>}G(qvipb(7F}xLCqg} zk2O0oAhoUm=N;BHEtkDpxBSeC@BQcEHNBa~EVO#`QWp>^8F5}MXkGb=i&%iTwMA^_q&6^bHpuHI3d7QG?k%h;SE`a;` zV3=lmkFmds+~s%g_H-c)#NO?ZQ17`GhvRofpI=X!N-Qz2kcZZ-h-P8%y;-*gHi#+^@-ay=v+Mx@x4-Et&$Q7%F zBjOJbQ=fE3Kl`LZHYyseE8iGr-=+PFy^E)B^z$pgFIrM+El!ZzsK3(O@R>##HJw&S z_og`eE}dT7zv#O$$gcodA?N;=1Mu)F&q^IcPdyS2;e}L1m6=MBt0A_~o`az^gtw|s zSlybBXfEVXz1xN`7luL#EUB0cG49bO2)!lsZXe?Q0Y>~V!NoKR*$nw_1U1Kd^rMMW z*m{E`R1u_Mv02=bZ3yjTZTk5gEFTduS~4J#@CbA%#qO zaMa=oev~4_XG?>Sf3W)1OjyWYBX;SMpiM*acaoM?%^p8 zp`m*e%8y#3&@uze2ZZBm1`!^&;V2mgu@gglE)EMJUQZN7E!qh0a-Qz`Q$Y!Rx?SQZMQWnWqs7uDk* z^`3&9^jUx?q;5~x8nfgHMDLAM-rS?q=SEtObThq>&DCv~t0h7^kvQ#;&kb~A6Tli~ zhL7SuZV-k}A~80Mi*pdne%u=LW>ODn!xmYZ^7eP42Y zUtH`5QuH_Y+l=feZy{OJc%-IfUGgXbDk)!YUMU&JC1J}v-RPGY9a4qK{Pr%otsWr)S1xcPJ?S4KFSnU#g!T%V6c}_I z+l1OY;g<(YJ%m*aZ9KB&K)Dy=Nd>8@YA{yrs<%77)j=m%EH}Mxd*|4@$LLIo zmD4x-zdQT>Y`nfVQNB4@zB%(63-R&zzf)oV2;iY5I_IP_Pc{wX-!nWna-MrMPf)jW?mBR|(+?)?Bog7<#MWy)X1mXk| z1em#b$)MTXT)Bfs=am1JB5Wt{*980oXl_&f1%dyLK$^gR4{%udA1L@g68M({?i2Vq zf&YoX-w^np0Zg4spDFj?iB7UVzX`bVx0KB<0X_#o{x|3V%Ku6UQ~`K)KCW}*v@J4= zlN5>gumce@Mxp9`l&I6CoT@6P!)w^ToPSNra{fAniOhHI1>& z4qQwZ&_a9IKOG)Vi!6vGx3WZU=QDTXa@va1a`@>u6O?>_0Ij{TPUl`_!pisQ(xILs zl+LFk*{}H1g&7xFX*-UY4Mda&^l}pcScD47zb5c&0>2?ZEKinAx$;}O2VO@|{tbcu zlfWkgE)aMhz|)^)N1kD36faf!|Du==3EU-+BG3wucBuP~e1%LjNqL#TB7p${+X(#M z1TyHe14F&r((Gq@+Qr7zFh5F(Py%{Lk1N#BcAQX2Yc%EsUFj$M7n%4UwlYswn(hrV z=Fu;w%gnd237Rg)c|yJ0hE<70co@#_(r$iF7hbTNc2)w|&r^};PB>joTj6w)=CC|A z9G)4W^xUiOZBEyk&Ln~eM)!x5F69*)h9qs6X+$ai58>zE68Lun+6nx70s>**LLiTT zl|VCr76QcOaBKR0Ld*_JHXHh=VEnE7sdIr?h!t?<(JH)A(qcO0#8N2^-H962-J z-(cPUj@_c;d?DYR^?T%w_+_CqbSd=Pp$F-wR32P@9)=n_2 zov>(r%fFBV^)JKmfmV>u;6R*1rAxx1L`) zbh~Zw`9$TuWaU0ulA5H)NL&>r-)q{i{KAh~R>nW*yxEy(+LmnEh80`Q4p{it>_ACs zb}YEi78OS<5Y?`%14y_J<51uQF~ww{bn8gMeI)5V0$L+xM19CY7yI$>y)6TO)_42b zf4=)qcPF+SOKv%aj1UDm0U|5FxVR3P6_kCm-`M@I_<6ZL8-+~0!0mog?2Q8 z4jP5Z>iLuRO5KY`mYzwJZb+7HpvZd|c-^*OS?EoPm3PH;cSHxSv>ZtS)*VUKAwuPZ`e(qLPvTb{^ZP(qlLwDK^r9|g9#W%#o zx`encDXv3VHZ&p?2I|%W1?VXCU2*TekE)X1qe<_HyWXenc%QNgnOEQ?#Fj0#&;oC1%XfBud*^aEQNJl!zbR4PnJn*2 zxVnfub+mog3CZ8=xzcnGYCB?S5*ruJ{QQx)lbh57hZr%1fb=wnl z1IfC9)m~aakAF8_T2xOeaaZeS_tNt5nqFGsWi88xl4Tncu1!hTrmx(zeC4L)|ATH? z{vnnz|9G40piTIRt?=MZ>raYX4sNynWUHORdtHMr;is;`Lssigt6B#4Sbw_5j__Y@ z5l`$9{&G*_Ny+-N^%lVUo3Y(2>+tdCO12N(QKt~n}xy~Sa@Thyv_QHtrkF~3(F;s z<*>(o_}q~4U!(j(9tV?w>DPxnJ!!`NJ)+oO6!&!hz9)q2O3xJZ(ERbofbrpi~pRK=KfN zpwwv!ohR@$0^@Ib7zJOSp}0DCrDu+G)Nv}KP4=L4I; z;k-TzI0GF_T3i4ohtooR_QvyaKLR2UR;P7f})eyawEL5$=rOXr3UoK5$&MSm2>+72IbN w+BU)`R;spvU{&3Nl4qeM>02j+`5e&Cn-e=Ng$2W5@?;MDMhVvFL$#uaW-A= zn%!}mrfU=-B;2YJ1@$U7gSAqnN(ia?pJL+6-X% zYBFhY&>D1h>*iaWmMwnXJ!SXad7tAWzGv#Ih`%A{{f@j5l5a}dcnshJn1cwf%V?{(hX93aMtvrPA3ScO2+7z>8 zij%W5_@tRLAe=SRz>{+`n3*_D89t#co};U=CDY0%j8TGBjfq7zhB2I)o=suUQUkAQ&)$*By_ zB(b?9>>`F?<3-9TQ&QIvf<<+z*z^z3sgVtQj@qR(w^(onBSI5wr<7CgfA#+gAtiaMsie27BfVX5+)jxuN(_B9R!0ClW)^s>{z#pPrtapNJjrTX>^hYa5Qx=7|AsAxvq^04>TpAC*h=&rR!^cO5M{r}y zSCt%FHmy-Sr7N06@o9*fWpPlqeeUe+`I-5d+0^7|XU716>GViX%{`BlhRvWyW;1=X}#vXKK zZX&n6I*@`Z=*pa~+gf$8t=8W-kcjv@Y6R+|W6=ChdIR3H8^}Tbj^rgrBB+t?u6rtP z&aXNg={(wyBy_WzNRY=ouD^<$`W!{Qt|y*wCCv%}p3TuQUVr@7)aU{vIm=!!H1!fZ zO=BTxs<{k>fL=JIvDb2o3*7dx9Mg1-Y1EEouR$aEI3?7HW|k&t84pHkPIa zUCmk(Ykkd89v{<9aI!rSuP=C+6B>f%mxt&fT1Um6Dfn+HcZcp?xHtCAOWz$mQ9OEL zQ-Y@P7i%7Y5=yNdgbW2Stw1#(3G!5RMPtiYT&T5>$cD9R(N*|{@h?+_fMufX+@6rMRqA>n*;w3$uGcWHxH1nD?#Fj> z)MmEpg%i8-3K-Ls!3fN4$wF>|AwXo&FoXhJb>RqC2BXcmEzlM4g)*3a&P~SUa9ZdACVM! z?kC<9d=I*MHV%Az;G;vg4;6gnQ1`8qH~+JFrW86-@I2^Ug2x zEr%n8z|R2`+5Z;-!~eLobo1TKl~OobkncyHs>D>K-8u%(?VhPp&(yc)wj-~XBCi+x z585MzSMCS9ijlA&hAi2p<#9VsX3&V*fcz; zoRG>gg2??5(E|hiTqd8)j>F$nGR46_<0PM%=LBhdVo~D8#bln-{27kVP$VcD_zXHDM6MZ9=Ls zOAv2E`@LFUj7tuLM*qwrO!!l&8#TrdCaa*upyV}YS<*WD3i(Z`5MH@W6kY!xN($Td z5?}NXlGCJRwkD(PkR*}$uU&)`FkyES!YPuF>QsxhS#Ws_SgWdldf4BzTX_aN^;CS- zN?Vb9fylhIK-{#yKrE1%UzCU`l6BmoEEupkqW!naevq(IQ*h1vR= zwO*;)ge`cj5@5Y^*jfauuKTP|pzB|S!#{`ZX&a8fb{rsWFY7fA#%vXR1^BW-bA%1) zbeSV}+HbeBjb{Jh&eAH2I~~(R(O>ES%?sb6K=$84es?!rw#(J}*+zd6VM;Wu>ebdxWj*GS`@`Ld=Yew&7`c1fJdIYO_@!3oTY{Ew)-T zb&mmCptfu3`@P#%My00>XwbA)cE5H0Fi~tPv{mZ7TOD+Cw#VpM`9UX!zE)fiGkJFr z2MN)!VAG=&5^ix4z6CqmnS=PL&|2v=FxEfW$E^5;9_gw%Uub>5&pHnn9c({fw0fhO z4=J>6hhP35q+`8t=HKCpZD^q3GsH^HLfl*0+Z$>P7raof9r!g2z8c`W{%UNGaQ}bCmAWpXJ~$k zmY|P!L&#_d^Cc#k(Kj0rjC4KMv<9+7>yE7qZv#npOdmVi=YtsNKO-2PJJ|=MT{}2I6cTs zF}ai^(tu!6;DBL}AVcp2Xq4DoK*HWhPw^=h7z3dqOoo_D26fUVXSm5Zoc0{2#Th=A zVljA5gry)I36S6ciI zt7kbWCuDHbVn}DVY0LsI%pt`xEusgs9@*%YHus^!6SLf8aw^HOy?ymIz@&6#5$QC? zVf}n5$04HP3Mpr&e#QB>{I-BAiBxo!_CoYYiKTeNiKpQ2R(*fy)87@OlLs2o??&L4aZMbl!iU-s~sq-ZT&t_;&Q#<+I;I z^;7b7Vwxxr-ybiMlIiq`+1iRq?L9=@H1P;ndp(TNzHNi}6tI|YG_3c-uh0B>*j}?O zzj%dsm*{n9HeS>m=omCtQbZEf9J72f(`(mk@y9iCN~6#q0uT|YG`;lY^>fE>Tm?-M zZ)A*vy)np7=FlODH-?hZaBkuTa`pYScR&4E{HEuIC`{h4yal>}u#51{Xgnt*#h(_22pm^_K@q?C9%ZbA$LMdGtieX&Hyp?STmz4UKS{_D_xBz~Xx!}ag0 z*C*~@pI*N{Eze|SK~%0w>UBxEm{Tw20Q#rl)!6GDKj|pXtdUCNezkFb4a;ZfEb>mI zEp$Etrhz;7&e~JoSY!=f<;r%TN11(gV1=LNFqJy#BV3aBAjUjMj!mJ+al+? zZ4gn}{=&B01rY!#pm!heeystnba1>2aLxAG1@u}+Bj+@7e(P9hAqqnvKDZFRMtn7h zXJCbh!cLH0_!+(YY_NQ`{L;OH|8Vl}PJZN8PFz+`T!xvIgDXr8MnDOVg>MK_biUi5 zv1K1r5ni4a$a&(|t^x@f=Rw?Po_Pk`)&v~7-~-rR8)ij7>qKNXw9bgo1nsX^fKjw; zCse>e;FMlgo>}|ey@PV!Nhqb4^e5DqXaQ}J|ITv;4Z06mU~~^O#pmEcL!Q}i#&{kn ze_TLEn?LY0x~!!ca6^2CqiekAcxvknlZk5-ikU_ivz%rJ1=pMq)1)|y=xQpn7dAl7 zWdT zMqe!9{D}m_5_oqS=_*8|BO=YrGszShMA9{46gmqrDl^UD`wsbz_`?NrXiG5I>qa00 zCvV$W!pu8oVP5}$m>RLJMRi?+`!vNSAgBSwbImiU948vK1ILll94K5i{2G?gH`Kc{ z>{k;{Y*fajV!wOt-oS6Y>WPc$i7WB|DrLL=g!(J2h6Z%Cd!+%69**4P4{XP4Idt9p zh5fKf^I%yIf|h+7K6u0jO6JiqcU{oS-CgCywa6PU*9IP>99;$2&mm8+W$!L@GJoU1 z@<+J~nZPRg?v{lkYQKer-$cuhEP%kKA-$9*?}~SSxH5V7hv@qtbv2VZYEl=2+q!l` zLuR?{O*>2I?*M+?gz0a=`eV;98PFU%7?ptBjr@<0Odu^I~hGQV_5w#$= z6MkV_;Pe1i{NqQ!PX59nA@H1a`^jl#BIU?@98)X;iX2AM06oKA{3 zRYjgLo(}*X3AXIN$YrLbnS3kiCo?%jEDq8P>gjyf7U?*JLi0=(;`E!zGz6rOFXU;+ z=y-aO&(RAEgo_Z=!So~tK|9^Ecp%eDYvf`+G`N*%(KAd60@--pXN(|}Uy^aSatDAz z&9*R+cc3%}2?^yUa1C7O?b8~PKt9MkGYPR03RVR4JA;5;?`tj`oQj&~{LM)Y=b0Ln zW*`wyf*{?OuAD~^gXYW%kaB6RY4FAitmZ~Y07CO{83>{TU`MlMS&f2Fn;oAN=QSq; z`)P>Nz{`u8J;h}-S1yx;OaN^N(qlnKIY5}pf>1d?$WKoLH<}}v0f{4HMaTeNlmH7l zSj~wtQ%3UwBT`NrN1?xF2dZ@5T_9ROE1=)u0Cz48B%q*OKo^QeO)w&t0k+KSd^Ux?GKGzz|^cg5anV{B) zCN1frMj2_D<_52K5u~O$@ja%Ivzi07G!iz4TN%%x#sxGOKyH4NH_ln;Mw|Q4;AKw0 zH>K8~%M*=(fB~^h%xi8?VT=`oCuVubo?yR}m``ecLo4+kIvem$qb$U*DLr0B!qGfv z?KqCDDKG$>^9txr751R2A1V>OAbk;T=tq2>c;X3sEonZ4%ZQT&?Q(~tgmY>* zC;M`ry9g@0((~q^;_g=6-J3+%`7Butx0T!vS?}~5+q60S$2LOYRd0D%3GGosdt_X` z*od^jzW!rowGnEPoB2OneO|r#1Lf+A>eUyas~07dRYO@hl-=l}-$?#!?u|LQ{it46 zDD>TQ0xDo`(~b5F5RD6DEj<75$=^J6@1`;^tPTvrpc0x_L-QqPH5>(^V|1lSiS1Kk zz^=>FimTg0rL!ez)8_QIZqPg5@~#ak^g)$AxZ*B5Uh}W`f%c9~!tMuXqr$WwDYJ6( z5xpp(qiX1=96DOv_ZY16R>-oq>{UX$tDzR1}*DWPj>=-SfACqUz|AH96*6jIKu6l zgwr`p>NT8Cw7qYy?dZI#m{-QevZOY_#P2m}B;x{l8*X~7X|-wTscNtjRNJ`&S0#Iu3g4$%*QwU>E`{n+sV*6pYSRwPTJtao zkwoPOP$^AAYSYkCqKa>4G&G_#ji^l{O9^Ba-qLBs-KM(R${^<*vU>;S@rn`{RRg0k zH3~fXqO!OBA%;SFc5e6r%ik+gimzMsb<4i)F92nO{K2M)U~QVSh8)z+RKT1)ZL46?5q+ytH#c552UzA%PQ+$1@uTS>%RU@&Lflu_&xex4X38m+R+H*o~ zokQjN%vPgyPVuEwUrP3+fYd;ECB8PZ)+hUqDE=d=|Hvj0boSz^L~b;7mYLVCuU;=T zY($z?kKB*!T953iJoVNkB{HB!21?Wh9Np0C7k_fG(z13`i4LgIfs*HwNc-x^3R&$v z{9#V+Ikjmgqh~%Pp!yT4O4KL*#+BaksN&zL`gbDl+h3Wf`l3Hh+)0#=s_iC6u+r0y z0dNI4JpF*9e$1RTTgc5k3<~tJab>V{s&uLvY`GuoUJrIx_A0?%H3$ql{Jk5Y&Pq}a zJ+6cvS3{3Ot66)VI#|GoJ-^ClwcRGJ}wEcXG^Iuw=&{o~;qj9GicZ?qLsE0|Yd!32V(F>!! zEy86$g<~hU*i(!IkxE)fz?kNkPVp03IGIMTnp4S%IQ(Y8pm0(1)W$Sd7T!KHqHrEz z!6A+hu3rR=FJnDY3F`l)1d!u6O4h@Wju|eM<%IDx&x$qN&elL%3+ z=0FWCpufo)uZ<$oH5ZTG4@&}i7lm(UJw-=%w4QoXCtGC{-dN8>LD#$<|UOx@Bvr68mLqsS+n- zYe8$WkDyDjQ^tJ=e~_c*eam z&TfLErmKC(0}uO938Y;l)CbBIr1H>*KJwT{XKSgErAU#geb~3GXr&6NRnNI&k3B#Z zwCY30o;&y4x#!%UbIy0po!@tMh6r4rKKyrns*8}nBhk3zdgb{dRPGX$%n(&leY!Lw ziKlPIC!YQpS@o;39w@ZUv`NHAnD1t=Aq48}jk+3m()Q3t$YZ#jOCoCUGcwbmhSm1Z zq?r&rJK!0HX9%7V)pw1=BTBDCNNAih#bzos4SE^wu_CARBppnR92*)LK1qjCL&rm* zvr5UHGx-S3%qf-?3SDG2wdWYsEUUz*ZsuvuwUo6Jq0pGB z6mgIoHw*MCHJE8k+nTP~8nbAg*^?;el9^9j4b9nh(Hco6^O`+Z$|SO8AvtyagQ;s- zjTu=c>I0<)Z!)8snPfrHjHE|q@@mp6^<%EtHZy2uVWhd3ab3|kqnAoXc8>OCb$p)9 zD9};Y8Oxfmu7!lj^YKvVTin>GZ(+koT&e{@cv>lHi{T&l6_{lyc^0?gzFI(M^GvU` z!DbW`JBz&=7i!%P_w!#u0a}-qB)jGPO}Ac_%EYTpYnL5ph#{m*z96dazV9J$Ja)N0 zl~h0IR9>uvC|n;Zx&|tM%|$Ia)1hV#G=zKU(GH!QpnYmlQ{$i~7v>xufGkP!bKwS+Vt%i?o5?^3W zTHh91+ul`)eo$IrYZ|V4&3*94H=*d4B2*pj*y!Tz$9hggBC|<$Ry+; zR3E#f+P2rNdu1>AJ#yO`-O7G7*yK^$oAsCY<+2P;)}j0r{7x9>Rc3PyeBSZ1?3`lc zTjB5(XZgV{=J*0Sq;1{;97B)nr)JUCOheIIeRJ%}q|+xsCs3h7qt}6&fkyBFc}fC((Q?8L zMu8*%esVsQtVcB~2|y5pwseW>wSZ8|)FKrahj>^M92g-6YI$J%^PgdnokXiOWaN!I{_bSU?*pSGb_dLMU1T_Dvx!Y2p8~~6B zD!m~5H6SCb*G;)HU^_ZEA5+x@aMpQ+YYL=Ep0H>Af1(afxf$Kwggjd<)ZGrNAG{87 zIJJRQ!ujgfP9ce@Bav0g479$2-4P}a#TzgS0S;l~EwJm2kmZh9ANy?(%t6Q30av_x zv2)_eAmlEo)O%14y5wNWLmoTsC5OG_2`}jtb?Q{>+rgA8c?jB5sgsSj4(WClhywbUO>OIcMZbrBH%ckwdz(666UFIe5%N4yABU24BGDVCNxc&T&a#;*@~L>yXYz zv^>Bl14vZaCjgrWa}6zzptn^G(?L*-FyYlCnIfhFG7f~+COG9e+eYF#hcw5~yz zeiu1+zVt@TS(tt}+IP@)7ox4V8}EZY=i#cClet{v`MxqR=)YUIFLX7ejTXAr=J+ffJ$$vLJEmr>gm}p__{6;iTEcQbvbQ3{44J z<+UOq);9R19@wK0e&T+;>wAMi{8I7y7 zXljNHS^-nEXdgy22s0@R3WM+s2F>xznKKl-f@ z;=n9GRbV_cG&AH#tF>5&W-y$C%$=Tb_)&P5gWDR10&?J!B(UrA!>e+%Dn}8BgW;85 ze;wRk3GQEyA_xaoqbI7-6DyIG$TyJSc0VeuMu)4>;g!fn7#Ref#-*uvXDygct7bNx z=J;`s?}nn*=2)#0fTD9uhc6uIf~i^wJbO5_RbN!5Gh+ zRpZpZB4R+Xv={B8AgZLZGkgeM;pZglPf$P>FG-tnM3OfN7W;@4TJFH#8rilieru$w z()h2D{)+d%LAsWMRnm2@_sjlAOBHf@m7K1U(@@zAbxQc1p`|>$N7}ndyv5Ld>D^7@ QEzU`Yq}V3eq7bY4FY18czyJUM literal 0 HcmV?d00001 diff --git a/src/neonutilities/__pycache__/read_table_neon.cpython-311.pyc b/src/neonutilities/__pycache__/read_table_neon.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdd446522df1fd47209a0f17495894b41529c860 GIT binary patch literal 11874 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 L5&EK(5b*y4@B7gS literal 0 HcmV?d00001 diff --git a/src/neonutilities/__pycache__/unzip_and_stack.cpython-311.pyc b/src/neonutilities/__pycache__/unzip_and_stack.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a3ed8da4fa8bbd59542a82b81bd6863a3bf27caa GIT binary patch literal 69197 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 literal 0 HcmV?d00001 diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py index a417df2..e597f6e 100644 --- a/src/neonutilities/files_by_uri.py +++ b/src/neonutilities/files_by_uri.py @@ -5,6 +5,7 @@ import os import platform import logging +import glob import importlib.metadata import pandas as pd from tqdm import tqdm @@ -116,7 +117,9 @@ def files_by_uri(filepath, URLsToDownload = uniqueURLs # Remove None values - URLsToDownload = [url for url in URLsToDownload if url is not None] + URLsToDownload = [url for url in URLsToDownload if url is not None] + URLsToDownload = [url for url in URLsToDownload if str(url) != 'nan'] + if len(URLsToDownload) == 0: raise Exception("There are no URLs other than 'None' for the stacked data.") @@ -167,8 +170,9 @@ def files_by_uri(filepath, out_file.write(response.content) # If the file type is zip and unzip is True, unzip if unzip is True: - unzip_zipfile(file_path) - # Remove zip files after being unzipped - if save_zipped_files is False: - os.remove(file_path) + if(file_path.endswith('.zip')): + unzip_zipfile(zippath=file_path) + # Remove zip files after being unzipped + if save_zipped_files is False: + os.remove(file_path) diff --git a/src/neonutilities/helper_mods/__pycache__/__init__.cpython-311.pyc b/src/neonutilities/helper_mods/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d1c64eca8d891da3ce3a9ddedd2271cd442ab6f GIT binary patch literal 541 zcmZvYy-ve06ou{lG*M}RjggW842UJNWQD}E7S4}sM^$+d9s%`jGDa0l zObkd!-8yldmP!GvtK-inH|JV@>G!(`*4y6e=o9vLbNE@-8WsV-GfGf`1xj&_Ffj{4ZXvh-<$6q+`SbneV1d zM@gikO{)*~3@t#>GCn^ccM1YX#^XoW^Nd{NahbvlMXtEMF7HWjc@qpv%>~z-sbMkI zV`x({CK=1K%?Gl&3r+Jx5lVT+HKoVJbm^ahRSI?{w_&Dv95n&?vN6Ur+OOJGqob-_ XH9Dx;RineIUH>`1b5^mk2eZEcR2-fL literal 0 HcmV?d00001 diff --git a/src/neonutilities/helper_mods/__pycache__/api_helpers.cpython-311.pyc b/src/neonutilities/helper_mods/__pycache__/api_helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3b09ccdad4ba76c1ab8761db6c0f7451e546ec0 GIT binary patch literal 34545 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 literal 0 HcmV?d00001 diff --git a/src/neonutilities/helper_mods/__pycache__/metadata_helpers.cpython-311.pyc b/src/neonutilities/helper_mods/__pycache__/metadata_helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f356671e47a4df393a7ebb4c8f4edbdb9a30f23 GIT binary patch literal 4149 zcmcguZ)h9I72jR`m!%|jE_c53T{7vV#HwwbtI&em9+M1 zSIn+b+mSh42t*eQr+z3g*B&)3;Avf{{rqG*r`84joJDEF!wl#lX#Vzv|E0X zmxSBgq#(I~x+MM@7jZvDf$?Nr4tuWJyY+LPG9Jwe9{yq+q2fQ-itDReM+`rA~u8{bcCB_Nu;^PO~Dqfj)wTHx!W>!2pmoDW@wM zg9|-o&! zM?Ymo>~<0tcoo=sVn$Ayq&=x?rl>%a9P~OdSg@F~AlDbTNM%5rg<&X1$*B)XPj^qR zjq;$FH#0izCl^&wp)&bNUQ1?}ElKC#!QS%1dl=rWkAVD|zsIGy0(bq`BL9W30$+AN z1zzH|H)Z1i`R=^}|0`jMT;smwEjRAP2yb}}S)|EK#AON5Zi`P@f;n&CZ9z!!#q7oY zsh=9K6{a*LISZ|>O#rXmU!EbYmr|r!iJ+JCXQ8fmDsgJ)0 z_8Czi&x_nzu+RRNn^%Ny0>2Ne9$y`K7^#rnD(Quh(kW-g7UD(mN*Z5i`9*ivkFVeS zaB;$B&MZzgAn%7+40*ip9OTDdjl9XOI+zU4jKi-Wl)Ik&qSP3(756S-_#o}Lq9Ll| zaQz4r_X~3{;-Po}T7qs^?wn|5EI-}}7<9{%rg}aXaZ&V<25_1;hnEAb2c9UPU7*Jp0KyO*T?^sAd}`VJ-OaCV{;}ub*~cB{);rEU8hVua)8$IXNVQ|6 zax7jw7Kdr2tM+uD49JMv5~ytX@K8biv0SRGSuU_E60p4XWsWUgr9@2S1**bHUKkJLQo| zpsgBcE5Kq{f}YG}MTjN*NtFWg=zU%mBN@iQj={ekB455JgMnkmLX zQ|Q1_e>q)gI$CWyTD-Ku(6vEZc0pSSjaEaW#o;HR=JN5S(Nh1K`^fG+iH8yo_Eslc z)=-i01uiMf3A6ACF!DkrVsr0m0?%o0zg%q;VD!Y$S9CMA@8x`1TyLknVNRp!?G%-QY=yu^ClEckmW^4%aAG|t4f&K zu`+^U!mbW3o5a!J2wYKA-R`tsiA6fen4A`C{W0vv6{jD^K2!-4GObUUh((y4lAMFe zMA6b%Phnlc48w{Gcty+Q!2~vOzsd<^rrFy4AOj^1B`H-GO(uvZ39F9XZbu0?W@Pld zDq)c$5>3~-G;qU&>XK;rY(l0D<;)&Mn_*ICl_9A8D7s>1SQ(b1G9-n)P~Q)1$6mXd zQ?#U-moP?kaBRaMK&q<$5-Om?{PtL0Q=ovl5`%ES8;2KRkJXE&PesL$_wZs|*JK;! z;W?;Yp|WF6HI%0V0|WJ4z_6Z1)Z6>IySwd3p6rWulPj^4ww}Fb_rUCJfgTVT+h5?c zEwC+f8f4=8%k(-j+hV;SHUVPFKjDBVgRz6$Q@k7&Twt-n{q3uBPq8qB?pREP860=$ zqsSjz(2vJ}0D!#ZfxNCs5w8{8-fotU@v{jlz!*F|wQt!(jY-!TGhtu_f$=58zSx^< z$7&l;+5uB~;5D8BDRP@5Jm(LW7S?>n;lIx4^03|7-+=zT<*nu5%5bIiN2^1XmT0vl zTJmkYb+r63{z`#TU?Xs#{LWh7efY04x-8qR{SD}ARXnj(oLCE-SPvjwPT4K6_W!ji zAVC~%1w%(FtsSdZD=i;XTRvzo^hwhjYwkB{$(6nd0N5n}K@NfWmSckqychoSQ0?Yb wd4OV8$M_jBAe%0p=QsTvA1ns&x52Td7})fl Date: Mon, 21 Apr 2025 13:37:02 -0400 Subject: [PATCH 03/13] Update files_by_uri.py --- src/neonutilities/files_by_uri.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py index e597f6e..236e46f 100644 --- a/src/neonutilities/files_by_uri.py +++ b/src/neonutilities/files_by_uri.py @@ -55,12 +55,12 @@ def files_by_uri(filepath, """ # check that filepath points to either a directory or a Python list object - if not isinstance(filepath, list): + if not isinstance(filepath, dict): if not os.path.exists(filepath): raise Exception("Input filepath is not a list object in the environment nor an existing file directory.") - # if filepath is a directory, read in contents - if isinstance(filepath, list): + # if filepath is a dictionary, make a savepath + if isinstance(filepath, dict): tabList = filepath if not os.path.exists(savepath): try: @@ -69,6 +69,7 @@ def files_by_uri(filepath, print(f"Could not create savepath directory. Files will be saved to {os.getcwd()}/GCS_zipFiles") savepath = f"{os.getcwd()}/GCS_zipFiles" else: + # if filepath is a directory, read in contents files = [os.path.join(filepath, f) for f in os.listdir(filepath) if os.path.isfile(os.path.join(filepath, f))] tabList = {} for j, file in enumerate(files): @@ -119,6 +120,7 @@ def files_by_uri(filepath, # Remove None values URLsToDownload = [url for url in URLsToDownload if url is not None] URLsToDownload = [url for url in URLsToDownload if str(url) != 'nan'] + URLsToDownload = [url for url in URLsToDownload if str(url) != ''] if len(URLsToDownload) == 0: From 658731ca57d1b247757d204b96dec86fea11905c Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Mon, 21 Apr 2025 15:50:31 -0400 Subject: [PATCH 04/13] Updates to files_by_uri after running flake8 black --- src/neonutilities/files_by_uri.py | 133 +++++++++++++++++------------- 1 file changed, 74 insertions(+), 59 deletions(-) diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py index 236e46f..e1f1cdb 100644 --- a/src/neonutilities/files_by_uri.py +++ b/src/neonutilities/files_by_uri.py @@ -5,31 +5,32 @@ import os import platform import logging -import glob import importlib.metadata import pandas as pd from tqdm import tqdm from urllib.parse import urlparse from .unzip_and_stack import unzip_zipfile -from .helper_mods.metadata_helpers import convert_byte_size +from .helper_mods.metadata_helpers import convert_byte_size # Set global user agent -vers = importlib.metadata.version('neonutilities') +vers = importlib.metadata.version("neonutilities") plat = platform.python_version() osplat = platform.platform() usera = f"neonutilities/{vers} Python/{plat} {osplat}" -def files_by_uri(filepath, - savepath=None, - check_size=True, - unzip=True, - save_zipped_files=False, - progress=True, - ): + +def files_by_uri( + filepath, + savepath=None, + check_size=True, + unzip=True, + save_zipped_files=False, + progress=True, +): """ Get files from NEON GCS Bucket using URLs in stacked data - + Parameters -------- filepath: The location of the NEON data containing URIs. Can be either a local directory containing NEON tabular data or a list object containing tabular data. @@ -38,27 +39,29 @@ def files_by_uri(filepath, unzip: indicates if the downloaded files from GCS buckets should be unzipped into the same directory, defaults to True. Supports .zip and .tar.gz files currently. (true/false) save_zipped_files: Should the unzipped monthly data folders be retained, defaults to False? Supports .zip and .tar.gz files currently. (true/false) progress: Should a progress bar be displayed? - + Return -------- A folder in the working directory (or in savepath, if specified), containing all files meeting query criteria. - + Example -------- ZN NOTE: Insert example when function is coded - + >>> example - + Created on Fri Aug 9 2024 - + @author: Zachary Nickerson - """ - + """ + # check that filepath points to either a directory or a Python list object if not isinstance(filepath, dict): if not os.path.exists(filepath): - raise Exception("Input filepath is not a list object in the environment nor an existing file directory.") - + raise Exception( + "Input filepath is not a list object in the environment nor an existing file directory." + ) + # if filepath is a dictionary, make a savepath if isinstance(filepath, dict): tabList = filepath @@ -66,21 +69,27 @@ def files_by_uri(filepath, try: os.makedirs(savepath) except OSError: - print(f"Could not create savepath directory. Files will be saved to {os.getcwd()}/GCS_zipFiles") + print( + f"Could not create savepath directory. Files will be saved to {os.getcwd()}/GCS_zipFiles" + ) savepath = f"{os.getcwd()}/GCS_zipFiles" else: # if filepath is a directory, read in contents - files = [os.path.join(filepath, f) for f in os.listdir(filepath) if os.path.isfile(os.path.join(filepath, f))] + files = [ + os.path.join(filepath, f) + for f in os.listdir(filepath) + if os.path.isfile(os.path.join(filepath, f)) + ] tabList = {} for j, file in enumerate(files): try: tabList[file] = pd.read_csv(file) - except Exception as e: + except Exception: print(f"File {file} could not be read.") tabList[f"error{j}"] = None continue - tabList = {k: v for k, v in tabList.items() if not k.startswith('error')} - + tabList = {k: v for k, v in tabList.items() if not k.startswith("error")} + # Check for the variables file in the filepath varList = [k for k in tabList.keys() if "variables" in k] if len(varList) == 0: @@ -88,93 +97,99 @@ def files_by_uri(filepath, if len(varList) > 1: raise Exception("More than one variables file found in filepath.") varFile = tabList[varList[0]] - - URLs = varFile[varFile['dataType'] == 'uri'] - + + URLs = varFile[varFile["dataType"] == "uri"] + # All of the tables in the package with URLs to download - allTables = URLs['table'].unique() - + allTables = URLs["table"].unique() + # Loop through tables and fields to compile a list of URLs to download URLsToDownload = [] - + # Remove allTables values that aren't in tabList allTables = {key for key in tabList.keys() for substr in allTables if substr in key} - + if len(allTables) < 1: - raise Exception('No tables with URIs available in download package contents.') - + raise Exception("No tables with URIs available in download package contents.") + # Loop through tables for table in allTables: tableData = tabList[table] # Find URLs per table that are in URLs.fieldName - URLsPerTable = [url for url in tableData if url in URLs['fieldName'].values] + URLsPerTable = [url for url in tableData if url in URLs["fieldName"].values] # Append the URLs from the fields found URLsToDownload.extend([tableData[url] for url in URLsPerTable]) - + # Remove duplicates from the list of URLs uniqueURLs = set() for lst in URLsToDownload: uniqueURLs.update(lst) URLsToDownload = uniqueURLs - + # Remove None values URLsToDownload = [url for url in URLsToDownload if url is not None] - URLsToDownload = [url for url in URLsToDownload if str(url) != 'nan'] - URLsToDownload = [url for url in URLsToDownload if str(url) != ''] + URLsToDownload = [url for url in URLsToDownload if str(url) != "nan"] + URLsToDownload = [url for url in URLsToDownload if str(url) != ""] - if len(URLsToDownload) == 0: raise Exception("There are no URLs other than 'None' for the stacked data.") - + # Create directory only if it does not already exist - if not savepath is None: + if savepath is not None: if not os.path.exists(savepath): os.makedirs(savepath) else: # Make the savepath the working directory savepath = os.getcwd() - + # Check the existence and size of each file from URL if check_size: logging.info(f"Checking size of downloading {len(URLsToDownload)} files by URI") sz = [] for urlfile in tqdm(URLsToDownload, disable=not progress): - response = requests.head(urlfile, - headers={"User-Agent": usera}, - allow_redirects=True) - + response = requests.head( + urlfile, headers={"User-Agent": usera}, allow_redirects=True + ) + # Return nothing if response failed if response.status_code != 200: - logging.info("Connection error for a subset of urls. Check outputs for missing data.") + logging.info( + "Connection error for a subset of urls. Check outputs for missing data." + ) # return None - + # Compile file sizes - flszi = int(response.headers['Content-Length']) + flszi = int(response.headers["Content-Length"]) sz.append(flszi) - + # Check download size download_size = convert_byte_size(sum(sz)) - if input(f"Continuing will download {len(URLsToDownload)} files totaling approximately {download_size}. Do you want to proceed? (y/n) ") != "y": + if ( + input( + f"Continuing will download {len(URLsToDownload)} files totaling approximately {download_size}. Do you want to proceed? (y/n) " + ) + != "y" + ): logging.info("Download halted.") return None else: - logging.info(f"Downloading {len(URLsToDownload)} files totaling approximately {download_size}.") - + logging.info( + f"Downloading {len(URLsToDownload)} files totaling approximately {download_size}." + ) + # Download URLs for j in tqdm(URLsToDownload, disable=not progress): parsed_url = urlparse(j) filename = os.path.basename(parsed_url.path) file_path = os.path.join(savepath, filename) # Download the file - response = requests.get(j, - headers={"User-Agent": usera}) - with open(file_path, 'wb') as out_file: + response = requests.get(j, headers={"User-Agent": usera}) + with open(file_path, "wb") as out_file: out_file.write(response.content) # If the file type is zip and unzip is True, unzip if unzip is True: - if(file_path.endswith('.zip')): + if file_path.endswith(".zip"): unzip_zipfile(zippath=file_path) # Remove zip files after being unzipped if save_zipped_files is False: os.remove(file_path) - From 428e2cc2e32345354159d265bc49ae74101ff5c5 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Mon, 5 May 2025 09:36:52 -0400 Subject: [PATCH 05/13] Updates from @cklunch review --- src/neonutilities/files_by_uri.py | 43 +++++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py index e1f1cdb..e6bd4bb 100644 --- a/src/neonutilities/files_by_uri.py +++ b/src/neonutilities/files_by_uri.py @@ -18,7 +18,6 @@ osplat = platform.platform() usera = f"neonutilities/{vers} Python/{plat} {osplat}" - def files_by_uri( filepath, savepath=None, @@ -65,14 +64,15 @@ def files_by_uri( # if filepath is a dictionary, make a savepath if isinstance(filepath, dict): tabList = filepath - if not os.path.exists(savepath): - try: - os.makedirs(savepath) - except OSError: - print( - f"Could not create savepath directory. Files will be saved to {os.getcwd()}/GCS_zipFiles" - ) - savepath = f"{os.getcwd()}/GCS_zipFiles" + if savepath is not None: + if not os.path.exists(savepath): + try: + os.makedirs(os.path.join(savepath, "GCS_files")) + except OSError: + print( + f"Could not create savepath directory. NEON files will be saved to {os.getcwd()}/GCS_files" + ) + savepath = f"{os.getcwd()}" else: # if filepath is a directory, read in contents files = [ @@ -80,6 +80,7 @@ def files_by_uri( for f in os.listdir(filepath) if os.path.isfile(os.path.join(filepath, f)) ] + files = [file for file in files if file.endswith(".csv")] tabList = {} for j, file in enumerate(files): try: @@ -93,9 +94,9 @@ def files_by_uri( # Check for the variables file in the filepath varList = [k for k in tabList.keys() if "variables" in k] if len(varList) == 0: - raise Exception("Variables file was not found in specified filepath.") + raise Exception("NEON Variables file was not found in specified filepath.") if len(varList) > 1: - raise Exception("More than one variables file found in filepath.") + raise Exception("More than one NEON variables file found in filepath.") varFile = tabList[varList[0]] URLs = varFile[varFile["dataType"] == "uri"] @@ -110,7 +111,7 @@ def files_by_uri( allTables = {key for key in tabList.keys() for substr in allTables if substr in key} if len(allTables) < 1: - raise Exception("No tables with URIs available in download package contents.") + raise Exception("No NEON tables with URIs available in download package contents.") # Loop through tables for table in allTables: @@ -132,15 +133,17 @@ def files_by_uri( URLsToDownload = [url for url in URLsToDownload if str(url) != ""] if len(URLsToDownload) == 0: - raise Exception("There are no URLs other than 'None' for the stacked data.") + raise Exception("There are no NEON URLs other than 'None' for the stacked data.") - # Create directory only if it does not already exist + # Create savepath only if it does not already exist if savepath is not None: - if not os.path.exists(savepath): - os.makedirs(savepath) + savepath = os.path.join(savepath, "GCS_files") else: - # Make the savepath the working directory - savepath = os.getcwd() + print( + f"Could not create savepath directory. NEON files will be saved to {os.getcwd()}/GCS_files" + ) + savepath = f"{os.getcwd()}/GCS_files" + os.makedirs(savepath) # Check the existence and size of each file from URL if check_size: @@ -166,7 +169,7 @@ def files_by_uri( download_size = convert_byte_size(sum(sz)) if ( input( - f"Continuing will download {len(URLsToDownload)} files totaling approximately {download_size}. Do you want to proceed? (y/n) " + f"Continuing will download {len(URLsToDownload)} NEON files totaling approximately {download_size}. Do you want to proceed? (y/n) " ) != "y" ): @@ -174,7 +177,7 @@ def files_by_uri( return None else: logging.info( - f"Downloading {len(URLsToDownload)} files totaling approximately {download_size}." + f"Downloading {len(URLsToDownload)} NEON files totaling approximately {download_size}." ) # Download URLs From 9945cd29d32b6cdca0a7b93af9adbfa912c83bec Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Mon, 5 May 2025 09:37:13 -0400 Subject: [PATCH 06/13] Improve NEON-specific messaging --- src/neonutilities/aop_download.py | 66 +++++++++++++-------------- src/neonutilities/tabular_download.py | 54 +++++++++++----------- src/neonutilities/unzip_and_stack.py | 20 ++++---- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/src/neonutilities/aop_download.py b/src/neonutilities/aop_download.py index 051885c..2f1cad1 100644 --- a/src/neonutilities/aop_download.py +++ b/src/neonutilities/aop_download.py @@ -111,7 +111,7 @@ def get_file_urls(urls, token=None): response = get_api(api_url=url, token=token) if response is None: logging.info( - "Data file retrieval failed. Check NEON data portal for outage alerts." + "NEON data file retrieval failed. Check NEON data portal for outage alerts." ) # get release info @@ -180,11 +180,11 @@ def get_shared_flights(site): flightSite = shared_flights_dict[site] if site in ["TREE", "CHEQ", "KONA", "DCFS"]: logging.info( - f"{site} is part of the flight box for {flightSite}. Downloading data from {flightSite}." + f"{site} is part of the NEON flight box for {flightSite}. Downloading data from {flightSite}." ) else: logging.info( - f"{site} is an aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}." + f"{site} is a NEON aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}." ) site = flightSite return site @@ -220,7 +220,7 @@ def validate_dpid(dpid): dpid_pattern = "DP[1-4]{1}.[0-9]{5}.00[1-2]{1}" if not re.fullmatch(dpid_pattern, dpid): raise ValueError( - f"{dpid} is not a properly formatted data product ID. The correct format is DP#.#####.00#" + f"{dpid} is not a properly formatted NEON data product ID. The correct format is DP#.#####.00#" ) @@ -314,13 +314,13 @@ def validate_aop_dpid(dpid): # Check if the dpid matches the pattern if not re.fullmatch(aop_dpid_pattern, dpid): raise ValueError( - f"{dpid} is not a valid AOP data product ID. AOP data products follow the format DP#.300##.00#." + f"{dpid} is not a valid NEON AOP data product ID. AOP data products follow the format DP#.300##.00#." ) # Check if the dpid is in the list of suspended AOP dpids if dpid in suspended_aop_dpids: raise ValueError( - f"{dpid} has been suspended and is not currently available, see https://data.neonscience.org/data-products/{dpid} for more details." + f"NEON {dpid} has been suspended and is not currently available, see https://data.neonscience.org/data-products/{dpid} for more details." ) # ' Valid AOP IDs are: {", ".join(valid_aop_dpids)}.') # Check if the dpid is in the list of valid AOP dpids @@ -328,7 +328,7 @@ def validate_aop_dpid(dpid): valid_aop_dpids.sort() valid_aop_dpids_string = "\n".join(valid_aop_dpids) raise ValueError( - f"{dpid} is not a valid AOP data product ID. Valid AOP IDs are listed below:\n{valid_aop_dpids_string}" + f"NEON {dpid} is not a valid AOP data product ID. Valid AOP IDs are listed below:\n{valid_aop_dpids_string}" ) @@ -345,7 +345,7 @@ def validate_aop_l3_dpid(dpid): # Check if the dpid starts with DP3 if not dpid.startswith("DP3"): raise ValueError( - f"{dpid} is not a valid Level 3 (L3) AOP data product ID. Level 3 AOP products follow the format DP3.300##.00#" + f"NEON {dpid} is not a valid Level 3 (L3) AOP data product ID. Level 3 AOP products follow the format DP3.300##.00#" ) # Check if the dpid is in the list of valid AOP dpids @@ -358,7 +358,7 @@ def validate_aop_l3_dpid(dpid): # f'{key}: {value}' for key, value in dpid_dict.items()) raise ValueError( - f"{dpid} is not a valid Level 3 (L3) AOP data product ID. Valid L3 AOP IDs are listed below:\n{valid_aop_l3_dpids_string}" + f"NEON {dpid} is not a valid Level 3 (L3) AOP data product ID. Valid L3 AOP IDs are listed below:\n{valid_aop_l3_dpids_string}" ) # below prints out the corresponding data product names for each ID. # f'{dpid} is not a valid Level 3 (L3) AOP data product ID. Valid L3 AOP products are listed below.\n{formatted_dpid_dict}') @@ -367,7 +367,7 @@ def validate_aop_l3_dpid(dpid): def check_field_spectra_dpid(dpid): if dpid == "DP1.30012.001": raise ValueError( - f"{dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data." + f"NEON {dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data." ) @@ -375,7 +375,7 @@ def validate_site_format(site): site_pattern = "[A-Z]{4}" if not re.fullmatch(site_pattern, site): raise ValueError( - f"{site} is an invalid site format. A four-letter NEON site code is required. NEON site codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites" + f"{site} is an invalid NEON site format. A four-letter NEON site code is required. NEON site codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites" ) @@ -393,13 +393,13 @@ def validate_year(year): year_pattern = "20[1-9][0-9]" if not re.fullmatch(year_pattern, year): raise ValueError( - f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. AOP data are available from 2013 to present.' + f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. NEON AOP data are available from 2013 to present.' ) def check_aop_dpid(response_dict, dpid): if response_dict["data"]["productScienceTeamAbbr"] != "AOP": - logging.info(f"{dpid} is not a remote sensing product. Use zipsByProduct()") + logging.info(f"NEON {dpid} is not a remote sensing product. Use zipsByProduct()") return @@ -468,7 +468,7 @@ def list_available_dates(dpid, site): # if the available_releases variable doesn't exist, this error will show up: # UnboundLocalError: local variable 'available_releases' referenced before assignment raise ValueError( - f"There are no data available for the data product {dpid} at the site {site}." + f"There are no NEON data available for the data product {dpid} at the site {site}." ) @@ -630,7 +630,7 @@ def get_aop_tile_extents(dpid, site, year, token=None): # error message if nothing is available if len(site_year_urls) == 0: logging.info( - f"There are no {dpid} data available at the site {site} in {year}. \nTo display available dates for a given data product and site, use the function list_available_dates()." + f"There are no NEON {dpid} data available at the site {site} in {year}. \nTo display available dates for a given data product and site, use the function list_available_dates()." ) return @@ -771,7 +771,7 @@ def by_file_aop( # error message if nothing is available if len(site_year_urls) == 0: logging.info( - f"There are no {dpid} data available at the site {site} in {year}.\nTo display available dates for a given data product and site, use the function list_available_dates()." + f"There are no NEON {dpid} data available at the site {site} in {year}.\nTo display available dates for a given data product and site, use the function list_available_dates()." ) # print("There are no data available at the selected site and year.") return @@ -782,14 +782,14 @@ def by_file_aop( # get the number of files in the dataframe, if there are no files to download, return if len(file_url_df) == 0: # print("No data files found.") - logging.info("No data files found.") + logging.info("No NEON data files found.") return # if 'PROVISIONAL' in releases and not include_provisional: if include_provisional: # log provisional included message logging.info( - "Provisional data are included. To exclude provisional data, use input parameter include_provisional=False." + "NEON Provisional data are included. To exclude provisional data, use input parameter include_provisional=False." ) else: # log provisional not included message and filter to the released data @@ -798,13 +798,13 @@ def by_file_aop( file_url_df = file_url_df[file_url_df["release"] != "PROVISIONAL"] if len(file_url_df) == 0: logging.info( - "Provisional data are not included. To download provisional data, use input parameter include_provisional=True." + "NEON Provisional data are not included. To download provisional data, use input parameter include_provisional=True." ) num_files = len(file_url_df) if num_files == 0: logging.info( - "No data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True." + "No NEON data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True." ) return @@ -818,7 +818,7 @@ def by_file_aop( if check_size: if ( input( - f"Continuing will download {num_files} files totaling approximately {download_size}. Do you want to proceed? (y/n) " + f"Continuing will download {num_files} NEON data files totaling approximately {download_size}. Do you want to proceed? (y/n) " ) != "y" ): @@ -834,7 +834,7 @@ def by_file_aop( # serially download all files, with progress bar files = list(file_url_df["url"]) - print(f"Downloading {num_files} files totaling approximately {download_size}\n") + print(f"Downloading {num_files} NEON data files totaling approximately {download_size}\n") sleep(1) for file in tqdm(files): download_file( @@ -1036,7 +1036,7 @@ def by_tile_aop( response_dict = response.json() # error message if dpid is not an AOP data product if response_dict["data"]["productScienceTeamAbbr"] != "AOP": - print(f"{dpid} is not a remote sensing product. Use zipsByProduct()") + print(f"NEON {dpid} is not a remote sensing product. Use zipsByProduct()") return # replace collocated site with the site name it's published under @@ -1048,7 +1048,7 @@ def by_tile_aop( # error message if nothing is available if len(site_year_urls) == 0: logging.info( - f"There are no {dpid} data available at the site {site} in {year}.\nTo display available dates for a given data product and site, use the function list_available_dates()." + f"There are no NEON {dpid} data available at the site {site} in {year}.\nTo display available dates for a given data product and site, use the function list_available_dates()." ) return @@ -1057,27 +1057,27 @@ def by_tile_aop( # get the number of files in the dataframe, if there are no files to download, return if len(file_url_df) == 0: - logging.info("No data files found.") + logging.info("No NEON data files found.") return # if 'PROVISIONAL' in releases and not include_provisional: if include_provisional: # print provisional included message logging.info( - "Provisional data are included. To exclude provisional data, use input parameter include_provisional=False." + "Provisional NEON data are included. To exclude provisional data, use input parameter include_provisional=False." ) else: # print provisional not included message file_url_df = file_url_df[file_url_df["release"] != "PROVISIONAL"] logging.info( - "Provisional data are not included. To download provisional data, use input parameter include_provisional=True." + "Provisional NEON data are not included. To download provisional data, use input parameter include_provisional=True." ) # get the number of files in the dataframe after filtering for provisional data, if there are no files to download, return num_files = len(file_url_df) if num_files == 0: logging.info( - "No data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True." + "No NEON data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True." ) return @@ -1092,7 +1092,7 @@ def by_tile_aop( # importlib.import_module('pyproj') except ImportError: logging.info( - "Package pyproj is required for this function to work at the BLAN site. Install and re-try" + "Package pyproj is required for this function to work at the NEON BLAN site. Install and re-try" ) return @@ -1203,7 +1203,7 @@ def get_buffer_coords(easting, northing, buffer): coords_not_found = list(set(coord_strs).difference(list(unique_coords_to_download))) if len(coords_not_found) > 0: print( - "Warning, the following coordinates fall outside the bounds of the site, so will not be downloaded:" + "Warning, the following coordinates fall outside the bounds of the NEON site, so will not be downloaded:" ) for coord in coords_not_found: print(",".join(coord.split("_"))) @@ -1211,7 +1211,7 @@ def get_buffer_coords(easting, northing, buffer): # get the number of files in the dataframe, if there are no files to download, return num_files = len(file_url_df_subset) if num_files == 0: - print("No data files found.") + print("No NEON data files found.") return # get the total size of all the files found @@ -1223,7 +1223,7 @@ def get_buffer_coords(easting, northing, buffer): if check_size: if ( input( - f"Continuing will download {num_files} files totaling approximately {download_size}. Do you want to proceed? (y/n) " + f"Continuing will download {num_files} NEON data files totaling approximately {download_size}. Do you want to proceed? (y/n) " ) != "y" ): @@ -1240,7 +1240,7 @@ def get_buffer_coords(easting, northing, buffer): # serially download all files, with progress bar files = list(file_url_df_subset["url"]) - print(f"Downloading {num_files} files totaling approximately {download_size}\n") + print(f"Downloading {num_files} NEON data files totaling approximately {download_size}\n") sleep(1) for file in tqdm(files): download_file( diff --git a/src/neonutilities/tabular_download.py b/src/neonutilities/tabular_download.py index 86d45f0..5afac02 100644 --- a/src/neonutilities/tabular_download.py +++ b/src/neonutilities/tabular_download.py @@ -62,7 +62,7 @@ def query_files( if package == "expanded": if not lst["data"]["productHasExpanded"]: logging.info( - "No expanded package found for " + "No expanded package found for NEON " + dpid + ". Basic package downloaded instead." ) @@ -121,7 +121,7 @@ def query_files( ) qreq = get_api(api_url=qurl, token=token) if qreq is None: - logging.info("No API response for selected query. Check inputs.") + logging.info("No NEON Portal API response for selected query. Check inputs.") return None qdict = qreq.json() @@ -267,38 +267,38 @@ def zips_by_product( # error message if dpid is not formatted correctly if not re.search(pattern="DP[1-4]{1}.[0-9]{5}.00[0-9]{1}", string=dpid): raise ValueError( - f"{dpid} is not a properly formatted data product ID. The correct format is DP#.#####.00#" + f"{dpid} is not a properly formatted NEON data product ID. The correct format is DP#.#####.00#" ) # error message if package is not basic or expanded if not package in ["basic", "expanded"]: raise ValueError( - f"{package} is not a valid package name. Package must be basic or expanded" + f"{package} is not a valid NEON download package name. Package must be basic or expanded" ) # error messages for products that can't be downloaded by zips_by_product() # AOP products if dpid[4:5:1] == 3 and dpid != "DP1.30012.001": raise ValueError( - f"{dpid} is a remote sensing data product. Use the by_file_aop() or by_tile_aop() function." + f"{dpid} is a NEON remote sensing data product. Use the by_file_aop() or by_tile_aop() function." ) # Phenocam products if dpid == "DP1.00033.001" or dpid == "DP1.00042.001": raise ValueError( - f"{dpid} is a phenological image product, data are hosted by Phenocam." + f"{dpid} is a NEON phenological image product, data are hosted by Phenocam." ) # Aeronet product if dpid == "DP1.00043.001": raise ValueError( - f"Spectral sun photometer ({dpid}) data are hosted by Aeronet." + f"NEON Spectral sun photometer ({dpid}) data are hosted by Aeronet." ) # DHP expanded package if dpid == "DP1.10017.001" and package == "expanded": raise ValueError( - "Digital hemispherical images expanded file packages exceed programmatic download limits. Either download from the data portal, or download the basic package and use the URLs in the data to download the images themselves. Follow instructions in the Data Product User Guide for image file naming." + "NEON Digital hemispherical images expanded file packages exceed programmatic download limits. Either download from the data portal, or download the basic package and use the URLs in the data to download the images themselves. Follow instructions in the Data Product User Guide for image file naming." ) # individual SAE products @@ -325,17 +325,17 @@ def zips_by_product( "DP1.00030.001", ]: raise ValueError( - f"{dpid} is only available in the bundled eddy covariance data product. Download DP4.00200.001 to access these data." + f"{dpid} is only available in the bundled NEON eddy covariance data product. Download DP4.00200.001 to access these data." ) # check for incompatible values of release= and include_provisional= if release == "PROVISIONAL" and not include_provisional: raise ValueError( - "Download request is for release=PROVISIONAL. To download PROVISIONAL data, enter input parameter include_provisional=True." + "NEON Download request is for release=PROVISIONAL. To download PROVISIONAL data, enter input parameter include_provisional=True." ) if re.search(pattern="RELEASE", string=release) is not None and include_provisional: logging.info( - f"Download request is for release={release} but include_provisional=True. Only data in {release} will be downloaded." + f"NEON Download request is for release={release} but include_provisional=True. Only data in {release} will be downloaded." ) # error message if dates aren't formatted correctly @@ -379,7 +379,7 @@ def zips_by_product( siter.append(sx) if indx == 1: logging.info( - f"Some sites in your download request are aquatic sites where {dpid} is collected at a nearby terrestrial site. The sites you requested, and the sites that will be accessed instead, are listed below." + f"Some NEON sites in your download request are aquatic sites where {dpid} is collected at a nearby terrestrial site. The sites you requested, and the sites that will be accessed instead, are listed below." ) logging.info(f"{s} -> {''.join(sx)}") else: @@ -399,7 +399,7 @@ def zips_by_product( ) if newDPID == ["depends"]: raise ValueError( - "Root chemistry and isotopes have been bundled with the root biomass data. For root chemistry from Megapits, download DP1.10066.001. For root chemistry from periodic sampling, download DP1.10067.001." + "NEON Root chemistry and isotopes have been bundled with the root biomass data. For root chemistry from Megapits, download DP1.10066.001. For root chemistry from periodic sampling, download DP1.10067.001." ) else: raise ValueError( @@ -418,7 +418,7 @@ def zips_by_product( other_bundles_df["homeProduct"][other_bundles_df["product"] == dpid] ) raise ValueError( - f"In all releases after {bundle_release}, {''.join(dpid)} has been bundled with {''.join(newDPID)} and is not available independently. Please download {''.join(newDPID)}." + f"In all NEON releases after {bundle_release}, {''.join(dpid)} has been bundled with {''.join(newDPID)} and is not available independently. Please download {''.join(newDPID)}." ) # end of error and exception handling, start the work @@ -439,7 +439,7 @@ def zips_by_product( if prodreq is None: if release == "LATEST": logging.info( - f"No data found for product {dpid}. LATEST data requested; check that token is valid for LATEST access." + f"No data found for NEON data product {dpid}. LATEST data requested; check that token is valid for LATEST access." ) return else: @@ -449,7 +449,7 @@ def zips_by_product( ) if rels is None: raise ConnectionError( - "Data product was not found or API was unreachable." + f"NEON Data product {dpid} was not found or API was unreachable." ) relj = rels.json() reld = relj["data"] @@ -457,14 +457,14 @@ def zips_by_product( for i in range(0, len(reld)): rellist.append(reld[i]["release"]) if release not in rellist: - raise ValueError(f"Release not found. Valid releases are {rellist}") + raise ValueError(f"Release not found. Valid NEON data releases are {rellist}") else: raise ConnectionError( - "Data product was not found or API was unreachable." + f"NEON Data product {dpid} was not found or API was unreachable." ) else: raise ConnectionError( - "Data product was not found or API was unreachable." + f"NEON Data product {dpid} was not found or API was unreachable." ) avail = prodreq.json() @@ -473,7 +473,7 @@ def zips_by_product( # I think this would never be called due to the way get_api() is set up try: avail["error"]["status"] - logging.info(f"No data found for product {dpid}") + logging.info(f"No NEON data found for data product {dpid}") return except Exception: pass @@ -481,7 +481,7 @@ def zips_by_product( # check that token was used if "x-ratelimit-limit" in prodreq.headers and token is not None: if prodreq.headers.get("x-ratelimit-limit") == 200: - logging.info("API token was not recognized. Public rate limit applied.") + logging.info("NEON Portal API token was not recognized. Public rate limit applied.") # use query endpoint if cloud mode selected if cloud_mode: @@ -508,7 +508,7 @@ def zips_by_product( # check for no results if len(month_urls) == 0: - logging.info("There are no data matching the search criteria.") + logging.info(f"There are no NEON {dpid} data matching the search criteria.") return # un-nest list @@ -527,7 +527,7 @@ def zips_by_product( # check for no results if len(site_urls) == 0: - logging.info("There are no data at the selected sites.") + logging.info(f"There are no NEON {dpid} data at the selected sites.") return # subset by start date @@ -541,7 +541,7 @@ def zips_by_product( # check for no results if len(start_urls) == 0: - logging.info("There are no data at the selected date(s).") + logging.info(f"There are no NEON {dpid} data at the selected date(s).") return # subset by end date @@ -553,7 +553,7 @@ def zips_by_product( # check for no results if len(end_urls) == 0: - logging.info("There are no data at the selected date(s).") + logging.info(f"There are no NEON {dpid} data at the selected date(s).") return # if downloading entire site-months, pass to get_zip_urls to query each month for url @@ -584,7 +584,7 @@ def zips_by_product( if check_size: if ( input( - f"Continuing will download {len(durls['z'])} files totaling approximately {download_size}. Do you want to proceed? (y/n) " + f"Continuing will download {len(durls['z'])} NEON {dpid} files totaling approximately {download_size}. Do you want to proceed? (y/n) " ) != "y" ): @@ -592,7 +592,7 @@ def zips_by_product( return None else: logging.info( - f"Downloading {len(durls['z'])} files totaling approximately {download_size}." + f"Downloading {len(durls['z'])} NEON {dpid} files totaling approximately {download_size}." ) # set up folder to save to diff --git a/src/neonutilities/unzip_and_stack.py b/src/neonutilities/unzip_and_stack.py index bc92220..7913d71 100644 --- a/src/neonutilities/unzip_and_stack.py +++ b/src/neonutilities/unzip_and_stack.py @@ -801,7 +801,7 @@ def stack_data_files_parallel(folder, package, dpid, progress=True, cloud_mode=F # stack frame files if progress: logging.info( - "Stacking per-sample files. These files may be very large; download data in smaller subsets if performance problems are encountered.\n" + f"Stacking NEON {dpid} per-sample files. These files may be very large; download data in smaller subsets if performance problems are encountered.\n" ) # subset microbe community data by taxonomic group @@ -842,7 +842,7 @@ def stack_data_files_parallel(folder, package, dpid, progress=True, cloud_mode=F # if there are no datafiles, exit if len(datafls) == 0: - print("No data files are present in specified file path.\n") + print(f"No NEON {dpid} data files are present in specified file path.\n") return None # if there is one or more than one file, stack files @@ -993,7 +993,7 @@ def stack_data_files_parallel(folder, package, dpid, progress=True, cloud_mode=F # set to string if variables file can't be found tableschema = None logging.info( - f"Variables file not found for table {j}. Data types will be inferred if possible." + f"NEON {dpid} variables file not found for table {j}. Data types will be inferred if possible." ) else: tableschema = get_variables(tablepkgvar) @@ -1205,7 +1205,7 @@ def stack_data_files_parallel(folder, package, dpid, progress=True, cloud_mode=F ) if len(rs) > 1: logging.info( - "Multiple data releases were stacked together. This is not appropriate, check your input data." + f"Multiple NEON data releases were stacked together for {dpid}. This is not appropriate, check your input data." ) except Exception: pass @@ -1290,7 +1290,7 @@ def stack_by_table( # Error handling if there are no standardized NEON Portal data tables in the list of files if not any(re.search(r"NEON.D[0-9]{2}.[A-Z]{4}.", x) for x in files): - logging.info("Data files are not present in the specified filepath.") + logging.info("NEON data files are not present in the specified filepath.") return # Determine dpid @@ -1304,7 +1304,7 @@ def stack_by_table( dpid = list(set(dpid)) if not len(dpid) == 1: logging.info( - "Data product ID could not be determined. Check that filepath contains data files, from a single NEON data product." + "NEON Data product ID could not be determined. Check that filepath contains data files, from a single NEON data product." ) return else: @@ -1324,14 +1324,14 @@ def stack_by_table( # Error message for AOP data if dpid[4] == "3" and not dpid == "DP1.30012.001": logging.info( - "This is an AOP data product, files cannot be stacked. Use by_file_aop() or by_tile_aop() to download data." + "This is a NEON AOP data product, files cannot be stacked. Use by_file_aop() or by_tile_aop() to download data." ) return # Error messafe for SAE data if dpid == "DP4.00200.001": logging.info( - "This eddy covariance data product is in HDF5 format. Stack using the stackEddy() function in the R package version of neonUtilities." + "This NEON eddy covariance data product is in HDF5 format. Stack using the stackEddy() function in the R package version of neonUtilities." ) return @@ -1339,14 +1339,14 @@ def stack_by_table( if dpid == "DP1.10017.001" and package == "expanded": save_unzipped_files = True logging.info( - "Note: Digital hemispheric photos (in NEF format) cannot be stacked; only the CSV metadata files will be stacked." + "Note: NEON Digital hemispheric photos (in NEF format) cannot be stacked; only the CSV metadata files will be stacked." ) # Warning about all sensor soil data # Test and modify the file length for the alert, this should be a lot better with arrow if dpid in ["DP1.00094.001", "DP1.00041.001"] and len(files) > 24: logging.info( - "Warning! Attempting to stack soil sensor data. Note that due to the number of soil sensors at each site, data volume is very high for these data. Consider dividing data processing into chunks and/or using a high-performance system." + "Warning! Attempting to stack NEON soil sensor data. Note that due to the number of soil sensors at each site, data volume is very high for these data. Consider dividing data processing into chunks and/or using a high-performance system." ) # If all checks pass, unzip and stack files From 2fb206782099759b40bdc9bc65e87ee85c4c8a26 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Mon, 5 May 2025 10:02:06 -0400 Subject: [PATCH 07/13] fix failed unit tests --- tests/test_files_by_uri.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_files_by_uri.py b/tests/test_files_by_uri.py index 86403ed..63affc4 100644 --- a/tests/test_files_by_uri.py +++ b/tests/test_files_by_uri.py @@ -30,7 +30,7 @@ def test_files_by_uri_NEF(): # There should be 36 .NEF files in the directory files = os.listdir(testdir) nef_files = [file for file in files if file.lower().endswith('.nef')] - assert len(nef_files) == 36 is True + assert len(nef_files) == 36 # Remove the .NEF files saved in the testdir for nef_file in nef_files: os.remove(os.path.join(testdir, nef_file)) @@ -49,7 +49,7 @@ def test_files_by_uri_ZIP(): # There should be 21 .ZIP files in the directory files = os.listdir(testdir) zip_files = [file for file in files if file.lower().endswith('.zip')] - assert len(zip_files) == 21 is True + assert len(zip_files) == 21 # Remove the .NEF files saved in the testdir for zip_file in zip_files: os.remove(os.path.join(testdir, zip_file)) @@ -68,7 +68,7 @@ def test_files_by_uri_ZIP_unzip(): # There should be 294 files that are not .CSV in the directory files = os.listdir(testdir) geo_files = [file for file in files if not file.lower().endswith('.csv')] - assert len(geo_files) == 21 is True + assert len(geo_files) == 21 # Remove the .NEF files saved in the testdir for geo_file in geo_files: os.remove(os.path.join(testdir, geo_file)) From 5470aac836b7c2fb3f51f46f4e4ce3285ef40acb Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Mon, 5 May 2025 10:05:46 -0400 Subject: [PATCH 08/13] fix failed unit tests again --- tests/test_files_by_uri.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_files_by_uri.py b/tests/test_files_by_uri.py index 63affc4..5d47add 100644 --- a/tests/test_files_by_uri.py +++ b/tests/test_files_by_uri.py @@ -14,8 +14,6 @@ import glob import zipfile -os.chdir("C:/Users/nickerson/Documents/GitHub/NEON-utilities-python/") - def test_files_by_uri_NEF(): """ Test that the function works for NEF files available from DP1.10017.001 (tabular data saved in testdata) From 4e9cafc00d77fe512ce57d969e7963befa41f2f6 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 8 May 2025 09:27:29 -0400 Subject: [PATCH 09/13] Update files_by_uri.py --- src/neonutilities/files_by_uri.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/neonutilities/files_by_uri.py b/src/neonutilities/files_by_uri.py index e6bd4bb..9e64c8a 100644 --- a/src/neonutilities/files_by_uri.py +++ b/src/neonutilities/files_by_uri.py @@ -143,7 +143,8 @@ def files_by_uri( f"Could not create savepath directory. NEON files will be saved to {os.getcwd()}/GCS_files" ) savepath = f"{os.getcwd()}/GCS_files" - os.makedirs(savepath) + if not os.path.exists(os.path.join(savepath)): + os.makedirs(savepath) # Check the existence and size of each file from URL if check_size: From f4838f1c9971a7f1bc019a157f110dbe29c21695 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 8 May 2025 09:46:50 -0400 Subject: [PATCH 10/13] Update test_files_by_uri.py --- tests/test_files_by_uri.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_files_by_uri.py b/tests/test_files_by_uri.py index 5d47add..cde2cfc 100644 --- a/tests/test_files_by_uri.py +++ b/tests/test_files_by_uri.py @@ -11,8 +11,7 @@ # import required packages from src.neonutilities.files_by_uri import files_by_uri import os -import glob -import zipfile +import shutil def test_files_by_uri_NEF(): """ @@ -26,12 +25,11 @@ def test_files_by_uri_NEF(): save_zipped_files=False, progress=True) # There should be 36 .NEF files in the directory - files = os.listdir(testdir) + files = os.listdir(os.path.join(testdir,"GCS_files")) nef_files = [file for file in files if file.lower().endswith('.nef')] assert len(nef_files) == 36 # Remove the .NEF files saved in the testdir - for nef_file in nef_files: - os.remove(os.path.join(testdir, nef_file)) + shutil.rmtree(os.path.join(testdir,"GCS_files")) def test_files_by_uri_ZIP(): """ @@ -45,12 +43,11 @@ def test_files_by_uri_ZIP(): save_zipped_files=False, progress=True) # There should be 21 .ZIP files in the directory - files = os.listdir(testdir) + files = os.listdir(os.path.join(testdir,"GCS_files")) zip_files = [file for file in files if file.lower().endswith('.zip')] assert len(zip_files) == 21 # Remove the .NEF files saved in the testdir - for zip_file in zip_files: - os.remove(os.path.join(testdir, zip_file)) + shutil.rmtree(os.path.join(testdir,"GCS_files")) def test_files_by_uri_ZIP_unzip(): """ @@ -64,11 +61,10 @@ def test_files_by_uri_ZIP_unzip(): save_zipped_files=False, progress=True) # There should be 294 files that are not .CSV in the directory - files = os.listdir(testdir) + files = os.listdir(os.path.join(testdir,"GCS_files")) geo_files = [file for file in files if not file.lower().endswith('.csv')] - assert len(geo_files) == 21 + assert len(geo_files) == 294 # Remove the .NEF files saved in the testdir - for geo_file in geo_files: - os.remove(os.path.join(testdir, geo_file)) + shutil.rmtree(os.path.join(testdir,"GCS_files")) # Potentially add a test for the microbial file types \ No newline at end of file From eb844409d41c4abfe3a379f1f0b9d9d52b54eb00 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 8 May 2025 10:08:14 -0400 Subject: [PATCH 11/13] Updates to test files --- tests/test_aop_download.py | 48 +++++++++++++++++------------------ tests/test_zips_by_product.py | 4 +-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/test_aop_download.py b/tests/test_aop_download.py index 323e456..b4cecda 100644 --- a/tests/test_aop_download.py +++ b/tests/test_aop_download.py @@ -60,7 +60,7 @@ def test_invalid_dpid_format(self): invalid_dpid = "DP1.30001" with self.assertRaises( ValueError, - msg=f"{invalid_dpid} is not a properly formatted data product ID. The correct format is DP#.#####.00#", + msg=f"{invalid_dpid} is not a properly formatted NEON data product ID. The correct format is DP#.#####.00#", ): by_file_aop(dpid=invalid_dpid, site=self.site, year=self.year) @@ -71,7 +71,7 @@ def test_invalid_aop_dpid_pattern(self): invalid_aop_dpid = "DP1.20001.001" with self.assertRaises( ValueError, - msg=f"{invalid_aop_dpid} is not a valid AOP data product ID. AOP products follow the format DP#.300##.00#", + msg=f"{invalid_aop_dpid} is not a valid NEON AOP data product ID. AOP products follow the format DP#.300##.00#", ): by_file_aop(dpid=invalid_aop_dpid, site=self.site, year=self.year) @@ -83,7 +83,7 @@ def test_invalid_aop_dpid_suspended(self): # ' Valid AOP DPIDs are '): with self.assertRaises( ValueError, - msg=f"{suspended_aop_dpid} has been suspended and is not currently available, see https://www.neonscience.org/data-products/{suspended_aop_dpid} for more details.", + msg=f"NEON {suspended_aop_dpid} has been suspended and is not currently available, see https://www.neonscience.org/data-products/{suspended_aop_dpid} for more details.", ): by_file_aop(dpid=suspended_aop_dpid, site=self.site, year=self.year) @@ -94,7 +94,7 @@ def test_check_field_spectra_dpid(self): field_spectra_dpid = "DP1.30012.001" with self.assertRaises( ValueError, - msg=f"{field_spectra_dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data.", + msg=f"NEON {field_spectra_dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data.", ): by_file_aop(dpid=field_spectra_dpid, site=self.site, year=self.year) @@ -105,7 +105,7 @@ def test_invalid_site_format(self): invalid_site = "McRae" with self.assertRaises( ValueError, - msg=f"{invalid_site} is an invalid site format. A four-letter NEON site code is required. NEON sites codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites", + msg=f"{invalid_site} is an invalid NEON site format. A four-letter NEON site code is required. NEON sites codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites", ): by_file_aop(dpid=self.dpid, site=invalid_site, year=self.year) @@ -132,7 +132,7 @@ def test_invalid_year_format(self, year): """ with pytest.raises( ValueError, - match=f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. AOP data are available from 2013 to present.', + match=f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. NEON AOP data are available from 2013 to present.', ): by_file_aop(dpid=self.dpid, site=self.site, year=year) @@ -156,7 +156,7 @@ def test_collocated_terrestrial_site_message( with self.assertLogs(level="INFO") as cm: by_file_aop(dpid="DP3.30015.001", site=site, year=year, token=token) self.assertIn( - f"INFO:root:{site} is part of the flight box for {flightSite}. Downloading data from {flightSite}.", + f"INFO:root:{site} is part of the NEON flight box for {flightSite}. Downloading data from {flightSite}.", cm.output, ) @@ -177,7 +177,7 @@ def test_collocated_aquatic_site_message(self, year, site, flightSite, input_moc with self.assertLogs(level="INFO") as cm: by_file_aop(dpid=self.dpid, site=site, year=year, token=token) self.assertIn( - f"INFO:root:{site} is an aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", + f"INFO:root:{site} is a NEON aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", cm.output, ) @@ -188,7 +188,7 @@ def test_no_data_available_message(self): with self.assertLogs(level="INFO") as cm: by_file_aop(dpid="DP3.30015.001", site=self.site, year=2020) self.assertIn( - f"INFO:root:There are no {self.dpid} data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", + f"INFO:root:There are no NEON {self.dpid} data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", cm.output, ) @@ -200,7 +200,7 @@ def test_check_download_size_message(self, input_mock): result = by_file_aop(dpid=self.dpid, site=self.site, year=self.year) # Check that the function asked for confirmation to download and prints expected message. input_mock.assert_called_once_with( - "Continuing will download 128 files totaling approximately 93.1 MB. Do you want to proceed? (y/n) " + "Continuing will download 128 NEON data files totaling approximately 93.1 MB. Do you want to proceed? (y/n) " ) # Check that the function halted the download self.assertEqual(result, None) @@ -216,7 +216,7 @@ def test_all_provisional_no_data_available_message(self): with self.assertLogs(level="INFO") as cm: by_file_aop(dpid="DP3.30015.001", site="WLOU", year=2024) self.assertIn( - "INFO:root:No data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True.", + "INFO:root:No NEON data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True.", cm.output, ) @@ -232,7 +232,7 @@ def test_provisional_included_and_data_available_message(self, input_mock): dpid="DP3.30015.001", site="WLOU", year=2024, include_provisional=True ) self.assertIn( - "INFO:root:Provisional data are included. To exclude provisional data, use input parameter include_provisional=False.", + "INFO:root:Provisional NEON data are included. To exclude provisional data, use input parameter include_provisional=False.", cm.output, ) @@ -255,7 +255,7 @@ def test_invalid_dpid_format(self): invalid_dpid = "DP1.30001" with self.assertRaises( ValueError, - msg=f"{invalid_dpid} is not a properly formatted data product ID. The correct format is DP#.#####.00#", + msg=f"{invalid_dpid} is not a properly formatted NEON data product ID. The correct format is DP#.#####.00#", ): by_tile_aop( dpid=invalid_dpid, @@ -272,7 +272,7 @@ def test_invalid_aop_l3_dpid(self): invalid_aop_dpid = "DP1.30001.001" with self.assertRaises( ValueError, - msg=f"{invalid_aop_dpid} is not a valid Level 3 AOP data product ID. Level 3 AOP products follow the format DP3.300##.00#", + msg=f"NEON {invalid_aop_dpid} is not a valid Level 3 AOP data product ID. Level 3 AOP products follow the format DP3.300##.00#", ): by_tile_aop( dpid=invalid_aop_dpid, @@ -286,7 +286,7 @@ def test_check_field_spectra_dpid(self): field_spectra_dpid = "DP1.30012.001" with self.assertRaises( ValueError, - msg=f"{field_spectra_dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data.", + msg=f"NEON {field_spectra_dpid} is the Field spectral data product, which is published as tabular data. Use zipsByProduct() or loadByProduct() to download these data.", ): by_tile_aop( dpid=field_spectra_dpid, @@ -300,7 +300,7 @@ def test_invalid_site_format(self): invalid_site = "McRae" with self.assertRaises( ValueError, - msg=f"{invalid_site} is an invalid site format. A four-letter NEON site code is required. NEON sites codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites", + msg=f"{invalid_site} is an invalid NEON site format. A four-letter NEON site code is required. NEON sites codes can be found here: https://www.neonscience.org/field-sites/explore-field-sites", ): by_tile_aop( dpid=self.dpid, @@ -336,7 +336,7 @@ def test_invalid_year_format(self, year): """ with pytest.raises( ValueError, - match=f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. AOP data are available from 2013 to present.', + match=f'{year} is an invalid year. Year is required in the format "2017" or 2017, eg. NEON AOP data are available from 2013 to present.', ): by_tile_aop( dpid=self.dpid, @@ -366,7 +366,7 @@ def test_collocated_terrestrial_site_message( with self.assertLogs(level="INFO") as cm: by_tile_aop(dpid=self.dpid, site=site, year=year, easting=[], northing=[]) self.assertIn( - f"INFO:root:{site} is part of the flight box for {flightSite}. Downloading data from {flightSite}.", + f"INFO:root:{site} is part of the NEON flight box for {flightSite}. Downloading data from {flightSite}.", cm.output, ) @@ -387,7 +387,7 @@ def test_collocated_aquatic_site_message(self, year, site, flightSite, input_moc with self.assertLogs(level="INFO") as cm: by_tile_aop(dpid=self.dpid, site=site, year=year, easting=[], northing=[]) self.assertIn( - f"INFO:root:{site} is an aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", + f"INFO:root:{site} is an NEON aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", cm.output, ) @@ -404,7 +404,7 @@ def test_no_data_available_message(self): northing=self.northing, ) self.assertIn( - f"INFO:root:There are no DP3.30015.001 data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", + f"INFO:root:There are no NEON DP3.30015.001 data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", cm.output, ) # 'INFO:root:There are no data available at the selected site and year.', cm.output) @@ -422,7 +422,7 @@ def test_no_data_files_found_message(self): northing=4900000, ) self.assertIn( - f"INFO:root:There are no DP3.30015.001 data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", + f"INFO:root:There are no NEON DP3.30015.001 data available at the site {self.site} in 2020.\nTo display available dates for a given data product and site, use the function list_available_dates().", cm.output, ) @@ -440,7 +440,7 @@ def test_check_download_size_message(self, input_mock): ) # Check that the function asked for confirmation to download and prints expected message. input_mock.assert_called_once_with( - "Continuing will download 7 files totaling approximately 3.9 MB. Do you want to proceed? (y/n) " + "Continuing will download 7 NEON data files totaling approximately 3.9 MB. Do you want to proceed? (y/n) " ) # Check that the function halted the download self.assertEqual(result, None) @@ -463,7 +463,7 @@ def test_all_provisional_no_data_available_message(self): northing=self.northing, ) self.assertIn( - "INFO:root:No data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True.", + "INFO:root:No NEON data files found. Available data may all be provisional. To download provisional data, use input parameter include_provisional=True.", cm.output, ) @@ -484,7 +484,7 @@ def test_provisional_included_and_data_available_message(self, input_mock): northing=self.northing, ) self.assertIn( - "INFO:root:Provisional data are included. To exclude provisional data, use input parameter include_provisional=False.", + "INFO:root:Provisional NEON data are included. To exclude provisional data, use input parameter include_provisional=False.", cm.output, ) diff --git a/tests/test_zips_by_product.py b/tests/test_zips_by_product.py index 3618a5d..1f66f3b 100644 --- a/tests/test_zips_by_product.py +++ b/tests/test_zips_by_product.py @@ -26,7 +26,7 @@ def test_zips_by_product_dpid(): ) assert ( str(exc_info.value) - == "DP1.444.001 is not a properly formatted data product ID. The correct format is DP#.#####.00#" + == "DP1.444.001 is not a properly formatted NEON data product ID. The correct format is DP#.#####.00#" ) @@ -44,7 +44,7 @@ def test_zips_by_product_site(caplog): ) assert any( - "There are no data at the selected sites." in record.message + "There are no NEON DP1.1003.001 data at the selected sites." in record.message for record in caplog.records ) From c5415e51eabc2cd400cdc2863235d3a24407b284 Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 8 May 2025 10:11:12 -0400 Subject: [PATCH 12/13] Update test_zips_by_product.py --- tests/test_zips_by_product.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zips_by_product.py b/tests/test_zips_by_product.py index 1f66f3b..0550932 100644 --- a/tests/test_zips_by_product.py +++ b/tests/test_zips_by_product.py @@ -44,7 +44,7 @@ def test_zips_by_product_site(caplog): ) assert any( - "There are no NEON DP1.1003.001 data at the selected sites." in record.message + "There are no NEON DP1.10003.001 data at the selected sites." in record.message for record in caplog.records ) From dcc95159dd82c995bd9abdf4de2aefd0b4a2e6ed Mon Sep 17 00:00:00 2001 From: znickerson8 Date: Thu, 8 May 2025 10:33:09 -0400 Subject: [PATCH 13/13] Update AOP messaging --- src/neonutilities/aop_download.py | 2 +- tests/test_aop_download.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neonutilities/aop_download.py b/src/neonutilities/aop_download.py index 2f1cad1..215c64d 100644 --- a/src/neonutilities/aop_download.py +++ b/src/neonutilities/aop_download.py @@ -789,7 +789,7 @@ def by_file_aop( if include_provisional: # log provisional included message logging.info( - "NEON Provisional data are included. To exclude provisional data, use input parameter include_provisional=False." + "Provisional NEON data are included. To exclude provisional data, use input parameter include_provisional=False." ) else: # log provisional not included message and filter to the released data diff --git a/tests/test_aop_download.py b/tests/test_aop_download.py index b4cecda..7e56c0b 100644 --- a/tests/test_aop_download.py +++ b/tests/test_aop_download.py @@ -387,7 +387,7 @@ def test_collocated_aquatic_site_message(self, year, site, flightSite, input_moc with self.assertLogs(level="INFO") as cm: by_tile_aop(dpid=self.dpid, site=site, year=year, easting=[], northing=[]) self.assertIn( - f"INFO:root:{site} is an NEON aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", + f"INFO:root:{site} is a NEON aquatic site and is sometimes included in the flight box for {flightSite}. Aquatic sites are not always included in the flight coverage every year.\nDownloading data from {flightSite}. Check data to confirm coverage of {site}.", cm.output, )