From aa68165f6f4112f47f9f3fa2b66be1efb5c444e6 Mon Sep 17 00:00:00 2001 From: JSP24SCM27V Date: Thu, 24 Apr 2025 18:07:36 -0500 Subject: [PATCH 1/2] unit tests for Flask API endpoints #25 --- environment.yml | 1 + .../__pycache__/base_scraper.cpython-311.pyc | Bin 0 -> 2390 bytes .../api/__pycache__/routes.cpython-311.pyc | Bin 0 -> 1843 bytes webscraper/api/interface.py | 8 ++++++ webscraper/api/routes.py | 26 ++++++++++++++++++ .../__pycache__/test_routes.cpython-311.pyc | Bin 0 -> 2319 bytes webscraper/api/tests/test_routes.py | 26 ++++++++++++++++++ webscraper/src/Cheaper_Scraper.py | 6 ++-- .../Cheaper_Scraper.cpython-311.pyc | Bin 3206 -> 4418 bytes .../src/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 171 bytes .../__pycache__/robot_check.cpython-311.pyc | Bin 1144 -> 1144 bytes 11 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 webscraper/ABC/__pycache__/base_scraper.cpython-311.pyc create mode 100644 webscraper/api/__pycache__/routes.cpython-311.pyc create mode 100644 webscraper/api/interface.py create mode 100644 webscraper/api/routes.py create mode 100644 webscraper/api/tests/__pycache__/test_routes.cpython-311.pyc create mode 100644 webscraper/api/tests/test_routes.py create mode 100644 webscraper/src/__pycache__/__init__.cpython-311.pyc diff --git a/environment.yml b/environment.yml index a4219bb..1a6ff6f 100644 --- a/environment.yml +++ b/environment.yml @@ -8,6 +8,7 @@ dependencies: - numpy - pandas - requests + - flask - pip: - beautifulsoup4 - lxml diff --git a/webscraper/ABC/__pycache__/base_scraper.cpython-311.pyc b/webscraper/ABC/__pycache__/base_scraper.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2dc46e32476c327c6e1d9afa561b42332605f8cf GIT binary patch literal 2390 zcma)7J!~9B6rTM%pU*xZwiW(_G?Ihh5cmo@3q^{t0+x(H`Dw0THQt@|P1v8y%pBIa zKq*{M(uIP;MHGxek%A%;4JD#Vfl#y+5;SyY83`#>-kZH!-};Am?)J^RnfKnl_q{i7 zzi&1h1jgS#H@YhxA%7t=9>AM%con9*BqX5~lf>#+iQTa+V=g9WSYMY8J7{ zMnCLEAUkbjfmUQ2CuAESJHsn-@QxYoR`(*iVrTY*omqoYGoM*u zieC>T>v5UaH-Yi{GR_-)#ih@-c&c+Rz zRA(d872zY-m+4IigPJfX6o5##P)CFimWT$EzEv!kfZ<&t;Z&ql@gPe>g{s?3Xu8Su zHs>jo{C1x!txDaEA4tp1UB%-_V!6v1B(p#if`*YiU1_6Fs(mZ#vh+)Fiw#)XUghcw zo%Py5mt!K@+kA6`SsTi?ZNeXIEcFI?-S-6`?E5!3>^XbnKoNM;FTRP}fuA`w9DboX3YL%||rm@MQF!U&C* zkRc`|bInxPvD|-r zlUx9zFn&tq1(KLRQlWXGid5o_MC)gO^a{jT1}pA zoL`E5Lz8~2MK8u~0E*|r$4D;Iad6|hGIz7MAZ1j~hX|WL_I){^3G4N+I~e;2H-IGM z7(B@pxH@sqD^>OSalmSYCp&YKWmW98Dv-6Bc$HJ=gub6Qd_T#;evIo`-@n~wad9HC zcjONAFR0fzV_MFkl_?|_k>K}JzJ>&EzBFmUvNvflg)Xs_4MAJ#0LY#4_op{MwCi4T zZ+=K%dbnt&N8C)mx@I~e2M(z%?0#-Eh6}JB+S6Y1TTp^&@9q8f@4aW1N2JsMf#JpD z8khFez1ESiS)HM=x;Ovbni1H)w67knKAQh&ZU3v^-?{vM>gbj^FVH|BHd^)EVVi-( zFIRzXO%E{t1(XIw1FwOt3gy%K2Su7L%QrzCJE+Ql3?0j|o{+`Ir~RLhw;rGUAG&P| Sd^}0MSvuh5+mV9q6zKGVKv&AOZ!tqTL#NXVyC+*n<#y zEuDEELhNXJ$38@$*V36I>43&}NPD~KoX#E4cn-TU_1pWuAUh9V43)LDVm8wzyTCOvHn#Vg=(X^coevxk0YgVsS`=>s9%gSxCIU<;IIqI47_n|NDt@W zQRIN~ec0t(m;dKs5XJQ9nvcL&sJFMe&U+?$>`3X4nw}S?{ho;b!;teeE}2a5o;X|7 z$eJe=h*>sP*F6cNZ>oq`3FyT>%_FUXaC!x^p*EVgtO}V-rIt-|jU+9T_=e<)S)%CR zvPMw0ii@W(wbBy{U{%5LbaI1=Jz#aS2xvSyXTbhQ(;&gmdr}DzqUBI+cnKBh21vC` z^(-FslP~XIO+d^X{|Zj11gF!fMS?I%70kR=eqa=|b*-HG43Ra4_2n z%=YsH4d;j_<&af0h~;O=lg@be0_{nH^F-eTt2a_DT7)nrR24^=3%QNT`h|u^sY;=p zEKx!8A`gE5#b2`eTvi)Hj(p3NZ`JQLBk>l1;`9q;XkW6geqaxO=qPtw<<3*yR;KLW z)T`L#AIjgA_ZOYmbvJgsKKFvA_ai4V=0?VBVa!jg7tCf$no-VXSyn|IEmw1i%N|eG ziN~1)53tw*TBU-@x+iJN#Aih)m`2%?bOV>Q5@J49jUvisJ#m#?EzD})XQ55c_<0p3 z30_H_6?HbDSKfS@-trGvS%;sA*SCJMjfKzTkjG7cAOf@falRH@{6VVfVksfk++geE!JXGV6RZ zd}SwTU%P9EXPoei8=k34$57ZF*&Nw<|Izr?c#DH@il!A#{mTO`-cbYqC;Fg?{px|0`Tn?pn`$l?P}sckaud&`4O8RX$sQz+~(ZwjryD;47 dict: + """Given a list of paths, return scraped results.""" + pass diff --git a/webscraper/api/routes.py b/webscraper/api/routes.py new file mode 100644 index 0000000..c65a11f --- /dev/null +++ b/webscraper/api/routes.py @@ -0,0 +1,26 @@ +import sys +import os +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from flask import Flask, jsonify, request +from webscraper.src.Cheaper_Scraper import CheaperScraper + +app = Flask(__name__) +scraper = CheaperScraper(base_url="https://books.toscrape.com") + +@app.route('/') +def home(): + return jsonify({"message": "Welcome to Cheaper API!"}) + +@app.route('/scrape', methods=['GET']) +def scrape_books(): + paths = request.args.getlist('path') + if not paths: + return jsonify({"error": "No paths provided"}), 400 + + results = scraper.scrape(paths) + return jsonify(results) + +if __name__ == '__main__': + app.run(debug=True) + diff --git a/webscraper/api/tests/__pycache__/test_routes.cpython-311.pyc b/webscraper/api/tests/__pycache__/test_routes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ce37af9eca86a39c071ce051a4ccf2315ab6bf4 GIT binary patch literal 2319 zcmds2&2Jl35P$n&=P4gbf~lLdO5`*^psd&DHBR3ce?%O$BSXJv`V-)Iujy+hxOo}j=w-|CN24j;W)WS1qi7< zIei0&JusJ*Op=vNX@_JLQwCHqq-@Mo0pn(DouuRb;06=!Cw9eUBG<8ihA=D)^)v&p zN8BD+F{00hxlQz7Gq3=H-6q9orbnAL33!z=8)=mz&mRK6uAVCunC1HM_Im)C1#1&_ zsR~y5OrXQm*&Xe^U|eWLJ8xK9g`)Auu(bD?c<9=Wmdi84VO;xyeJXN1APuLWp(PQZ z&Vx-ZI*NdF z=klcjBghx#WRJk^Q6(7rQNB7He4Ek%@Q)(dmyogjC}X{v+fw%vGUWr%Ig0co@U)M< zFAP(Gb)8R@^sfr)pk~e_?Zq#!R3rf&uu{%0vy^M6Zim@Sz4!6jVVqMC`pPDA{UJj@ z3EU4puNWnNNVtYu5qi!xnNLH>n&qpe;TnfB#|H8#XTq^9!32h!O7S7&js{iFgR`(I z)hj)v=#!xt@ex3cG!vJf-r8IFc4dFLk(l!mbGx)jhfnClF`ale;Y}_)7Y(}X(d9Z_ z2EjKYk4NfB?T3jUry6v{qbv2!!$-j0|Db%;0WJL43s2xX663I|jGb}}H!o5Sx3>x= zGdXhkWq@-fxD=#0T&-KS?ik!Ci)yOB5d9jXXsEv&HyVirFR}1%$}#_Z_4~UGdefsf z>z(H%<@goc{TI4vTiTC$2||j-`4i9`js$<-aZ08-+b-D(mg#LR2hCElH!DnYj7_$% zw3v6xB|Zjr(gQ)`@mGN634~Vx{IPbUi8TxQ&&omTL1*ekVPoqRa_+@iCwptXWt0jg zl-tc$Xa1qsT@~~w9su+-UwwA9ky!H*YcDVN37tBoQ_u1z)7tU0)|g)Mrk8$tvq7(W z^m@JX1eMRE2YjmQmI23s?hopE*)}UBq=$7KPnuA|F}Zva;VME3;E)6sf>VWKfdatu zP^<&gqF+muW3w$10aaq}N-ffZgG%fcGTH#s8#wKU&>y0AH;cZ7T;kOMqLW5Xm(29yS_-jCRw{k|`3Ulxdp9cZn5kdg86iJerWV9Ci zn&kER`KP5OBnbE-Jelyuul<37KfBU-q7Ls?kCn-~GTBteYpXkVzq+?`uNM4T3Q@0s HAPDL&=E3_p literal 0 HcmV?d00001 diff --git a/webscraper/api/tests/test_routes.py b/webscraper/api/tests/test_routes.py new file mode 100644 index 0000000..4edd3f9 --- /dev/null +++ b/webscraper/api/tests/test_routes.py @@ -0,0 +1,26 @@ +import unittest +from webscraper.api.routes import app + +class TestRoutes(unittest.TestCase): + + def setUp(self): + self.client = app.test_client() + self.client.testing = True + + def test_home_route(self): + response = self.client.get('/') + self.assertEqual(response.status_code, 200) + self.assertIn(b"Welcome to Cheaper API", response.data) + + def test_scrape_no_params(self): + response = self.client.get('/scrape') + self.assertEqual(response.status_code, 400) + self.assertIn(b"No paths provided", response.data) + + def test_scrape_valid_path(self): + response = self.client.get('/scrape?path=/catalogue/page-1.html') + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.get_json(), dict) + +if __name__ == '__main__': + unittest.main() diff --git a/webscraper/src/Cheaper_Scraper.py b/webscraper/src/Cheaper_Scraper.py index 2524a63..8d0bd7c 100644 --- a/webscraper/src/Cheaper_Scraper.py +++ b/webscraper/src/Cheaper_Scraper.py @@ -3,8 +3,10 @@ from bs4 import BeautifulSoup import logging from typing import Dict, List, Optional -from ABC.base_scraper import BaseScraper -from Robot_Check import RoboCheck +from webscraper.ABC.base_scraper import BaseScraper +# from webscraper.src.Cheaper_Scraper import CheaperScraper +from webscraper.src.robot_check import RoboCheck + class CheaperScraper(BaseScraper): diff --git a/webscraper/src/__pycache__/Cheaper_Scraper.cpython-311.pyc b/webscraper/src/__pycache__/Cheaper_Scraper.cpython-311.pyc index b453941ca2e36276467a2e99f63d38f0e6fe24a5..2b9662c008db618872a585131c5a4759d0d965a2 100644 GIT binary patch delta 2239 zcma)7U2Gdg5Z?3s`23U9O(I%1ZQ0U_#QY@v;YSKIB5f3?2%$8sP>fJ?@vh?opKW%} zxQR;LhY0ZisO5x;C`frgC5R$Llm~c0fP_Fipge$t`~*V02Ni_WCua8SBdI85?VFvQ z*_oaBc6RsW>{oXTyx-N8B+y>{GFci+5%MbzI?Ze|*TjW^L*rgNANTCMtr13;QBHWt zd{XgdIpwAEX^q6lql8&YgvELCvQEf{@P#ek#k6UXvoD<4-qc&hxLfdB#-uC#mi=VS zcdHeroYPvVBaYw}VfMMc9b*_4}#0$?CI)1QEv=ppLV&Qrf8OG|+V|8Tkb3=9}c;1YB*F z6=R$1wt!uV#Ub0CH?)aF%M#qLi^|m139VOa>L523|2kH2eb*_wi=6r;PGy)gZMc4k z&H)It<_O2*J{NK*N!u-A#>BiFiAJbJo7uW3kI<9gPI)+WVv_pRw(HL1rgBx6T+BN2 zyyB10Q@~TjQ|Sx0GLJ?s1D#u3*ce>Lh4R37*;#wk!*P8&$NdK9 z6)N}(buN7w+1`0w8(vJ}n)i<3`ct_S0E7$IL>GWBwjk*amSP8}4J;_327H+_T88B1 zVmPT=%?y5wy_i8GSpjm6toM-Yo?u4zw4UG_{S_@195)W@`V}KMZ+z4=&%`c4ah@?Q zk#~*D8WbmdVbjPAVX>Etvj&Szx4DMaAgA>i($I48#qTD#Uno(?(TA96QB*zZP+8*z zx9ApNul&-jb}0lNXNFsZyMrTytcczth!Edsa6;A24V(Sg$tNbGU>w=mH@8S-y-?s% z7VG7E=v1}BX%(tVG+fCOk;`Dg+^J0kZ<>QzHu&7!V=3Hk3w|>P(};e{@cC(9^aWY# zc54R+#O`3!qFR42YthUe5XfJE0N#Bh(|7eX>uasKc;&bwplUZpb}K|w9hH2qU9%fa z5fnH8l1D-RHz>oX#@mEei=iQ-uFpLgZWW#|bRT&OrtPQ0)b)$k;%1rSU?z!e>21eXx&KW8!*6T^^| zI1D6b3bo~{N}?aQmW3e(2b0;%6ly1dsG7*#9n2&rvIrD-h9`xH zH1eA0!7d$qmORkhiDE=NTpC@g^@V}lBS1bN0$r%J1U^xUpeOk~kaN**J=SABuugz1 z>#AGrRo%CB)vZ12sD2Y^g2muAbNf00^44C}t=Ox*SGp8?BRQBcx8W@4miH~o_i~E8 zkqmBuS>wyS?;Kkvz^?3Dk*njM_I`3~6H6bK z@SzvE4+T<54@Q&__#Xs8Pts#7NI~zhVGkh@oimeC+L^iEx#zp*p8K6U^EUM$+4Le7 zix4og+ZelALC6;jwvGLnY+hJw>PS(dkbc5hk#KEAA>oEKl>KpbnBeb@cR1L+`jZl+EVyF1Ls8+M%MOe!)Lw zOJ+3FTG?ha>Vf=fdC_Q&ViP6iy9Hmw<$`fY-w%XH%2mUrb#mRXty&ebi(^InHw0d{ zLq!Z-rChe!=jxH(IZd2~@FfwrNz@^r{s3^l_Yh!y=P4@@t*cA`mes($sR`Wssle@q zL|jXh3BcpSfkT)JnJ{iKw0c|vw;K`_$nAwtDT;Iq5l5{lfRZ}woe(O92Qf5*Z+?Ab z%pJKU7}7yoJ#cw{^}1!8naJ^}tb0!MLJt06-n3k(>h5p+K>m5ltdoMAz3>gW@Bdi{ Gbp8g>>DR~r diff --git a/webscraper/src/__pycache__/__init__.cpython-311.pyc b/webscraper/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c420941df3fb20175a9987d71633081ae7b7d1fd GIT binary patch literal 171 zcmZ3^%ge<81W$HzWmp60#~=<2FhUuh*?^4c3@Hr344RC7D;bKIfc(!O$zOK*p~b01 z#rj$K8HssinOTXIiFx`ism0kP`33sP8L5c{sYUwbsY%7jMPO!eQL=t~d}dx|NqoFs nLFF$Fo80`A(wtPgB37VrAPbB6fy4)9Mn=XD3^1aI87Kw-N4P2+ literal 0 HcmV?d00001 diff --git a/webscraper/src/__pycache__/robot_check.cpython-311.pyc b/webscraper/src/__pycache__/robot_check.cpython-311.pyc index 0d139acc767d0af22217c97d3d8137f02b04b997..e7f181b912b20a8a87aa86d31dda31a3707ab32f 100644 GIT binary patch delta 20 acmeyt@q>eVIWI340}wpf&9#v`mjwVo=LM|* delta 20 acmeyt@q>eVIWI340}wpa Date: Thu, 24 Apr 2025 20:20:00 -0500 Subject: [PATCH 2/2] reviewed changes --- .../api/__pycache__/interface.cpython-311.pyc | Bin 0 -> 819 bytes .../api/__pycache__/routes.cpython-311.pyc | Bin 1843 -> 1855 bytes webscraper/api/routes.py | 2 +- webscraper/src/Cheaper_Scraper.py | 12 ++++++++---- .../Cheaper_Scraper.cpython-311.pyc | Bin 4418 -> 4816 bytes 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 webscraper/api/__pycache__/interface.cpython-311.pyc diff --git a/webscraper/api/__pycache__/interface.cpython-311.pyc b/webscraper/api/__pycache__/interface.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b9240df3ae7246cb2a435a5ae08e7c777ab75f1 GIT binary patch literal 819 zcmZuvF>ll`6n=J`>-E%BNJU6&kqW~}=pw;}5U6lc)#U`LSTYd#VjoG#C0E;5)J`34 z;2)Hc0ToaeegVIM5qUsJOl+x+sS|c`O$D5je17lw<$d3?{lW7(V8-n)o#eZje_H2q zxC>)`*BB?jfT0Y=C`4l%Vgwkrf#Df2hoe&m;LLpW2pvXtq3iy#ZWn83>+uu4PKQzn z8mlo^Nx|YpZZ$Pb_l*>x;uDBS{m}_@JRU&mua5WWqjGNy8kg=`qdCP$55gFb1sJERS@Z6_>6yPa~?z%i69SoPkJxp837 zZR>Uv<#f!WNOz)WT(D_o?X@V{pVF*e5ya$bVq4daIWkRH8Y|(MsIZqstXo*KU^lG- zyM?lE;JA5yxEm$LuJ?|8y!SpW!I+=>)pP~t4z#Y%UQ~Hk(VD>49(0Lz=r9(xex<9X v{9DFF`$65wLvh38*&~(D43rK+=o@T)UG|p5MP3R2!`sbs8~wdxsS@@FYDdQS literal 0 HcmV?d00001 diff --git a/webscraper/api/__pycache__/routes.cpython-311.pyc b/webscraper/api/__pycache__/routes.cpython-311.pyc index f665345ee17b78ad45293fb770796402c234fbf6..5e18603640a055ecbe54704be8c8611da39954eb 100644 GIT binary patch delta 70 zcmdnYx1W!9IWI340}yaO;L6yxk++dWP#`_EB)&MgD6t?lB|aswBysXo7Bj}k$!A#X Z_^$J*UE)()PzD6t@Q@_ZIE#_-A4SnPQE^Sbh8gkI!R NzQU)xS&3Da2>|M#5p)0m diff --git a/webscraper/api/routes.py b/webscraper/api/routes.py index c65a11f..919633c 100644 --- a/webscraper/api/routes.py +++ b/webscraper/api/routes.py @@ -18,7 +18,7 @@ def scrape_books(): if not paths: return jsonify({"error": "No paths provided"}), 400 - results = scraper.scrape(paths) + results = scraper.get_scraped_data(paths) return jsonify(results) if __name__ == '__main__': diff --git a/webscraper/src/Cheaper_Scraper.py b/webscraper/src/Cheaper_Scraper.py index 8d0bd7c..27fffc0 100644 --- a/webscraper/src/Cheaper_Scraper.py +++ b/webscraper/src/Cheaper_Scraper.py @@ -4,13 +4,14 @@ import logging from typing import Dict, List, Optional from webscraper.ABC.base_scraper import BaseScraper -# from webscraper.src.Cheaper_Scraper import CheaperScraper from webscraper.src.robot_check import RoboCheck +from webscraper.api.interface import ScraperAPIInterface -class CheaperScraper(BaseScraper): - def __init__(self, base_url:str, user_agent: str= "CheaperBot/0.1", delay: float=2.0) -> None: + +class CheaperScraper(BaseScraper, ScraperAPIInterface): + def __init__(self, base_url: str = "", user_agent: str = "CheaperBot/0.1", delay: float = 2.0) -> None: """Initialize the scraper with base parameters. Args: @@ -90,4 +91,7 @@ def scrape(self, paths: List[str]) -> Dict[str, List[str]]: html = self.fetch(path) if html: results[path] = self.parse(html) - return results \ No newline at end of file + return results + + def get_scraped_data(self, paths: List[str]) -> Dict[str, List[str]]: + return self.scrape(paths) diff --git a/webscraper/src/__pycache__/Cheaper_Scraper.cpython-311.pyc b/webscraper/src/__pycache__/Cheaper_Scraper.cpython-311.pyc index 2b9662c008db618872a585131c5a4759d0d965a2..dedb95d142d20a70705a8063c32c998e3407b90f 100644 GIT binary patch delta 1132 zcmZ`&OH30%7@pbgw%gZkp_NAzXd4J_qJ*cKDEJ}@nkWh2p+vECrnU%5W-A&YfddEP zMadk{gN7L6BgRO=g@Xsf#T!<`0edr^jWu{Mp8RLp#$e#j&Nu)3-~5mHcV;b`ZD@Sq z_j?e?p0A$NM=wI(#G)k5qOvu+&^Xvk5OI$A$BmeQk%G=5;+jK5rygoNR}p#*qaZPp z;C^Je7oMD-v2ZTfpJ4F`%1-w6_TI=)mW(H8qO@1c?&4zWGboFQq8KJ9rjT-?QU}ps zIH^ipL{I9(J*UMq5+q)*x~P*ns*BDDB~{`B)y+qJ99NQan-)t-Qt86|V{5SebwTZ&1$^)&XhcljWH2eZNmQ=F|+mlVKHnUaxcR6P{uIMi1i}z77~&`JsPN3iejM z?yW963|nmRd%{v!4gc*8rgY_a}LIEg=0~9f~4gf5oX|#m+W37G$nuIS{n4JQ%rTj}{ z3+Hf{)xeYsT%(y`TV4t>OyZgN-MZ~r=7$9{gatF!%0@eWO7hn( ITVx@B0T{UR_5c6? delta 804 zcmZ{i&1(}u7{+JzE0bh*)2t;DV`~~IagkyK_25TQgnFnHqy%AW4z)8S;3u=K_8`htM09rN_s%}=&hrfH_NI4g^%tI( zBQUnMbL|UxLcZa{aM-(xpR!wT)(moqaHm6fM&zEGguH^8j>zM5k%X-KdS3aowD}a| zCq$5lauOMQjGMyb7ED`M+~Jv3##zUXY+mF!P#xh2vph5-s}|2gH^W`sV}JF$jumCO zFGh6m0H<9|eWs;K0ba}V3>+)y`xo0{`L2+QD-uiM8#j!+2Z6GH@Kwi}OP_r$s*lz^ z>Z!~20~#dn?Hz-b)fSs_^p=uJ*bx^u_N_Isu_njWcXr4*fIBCZo2}A{ngLEF3)xRb zsyEb(TP~i2S7HZH)k9X)Rkun{s|~kJCz7vji#Cp-%Q--3OYJy|!WqOQq5+8UvkV<> z`!%|tx_+}f0cvPS_=ZHfxTZ3|;;QY!@y&xMW46>0XrXhYg{$*`mqen4tRV28&_Jj7 z1K{D_H82AE)PMk+uPR zKb1VJt`%EhdWty|_D=SjoxOF3uiq52avmBKX8cysKs3LQMsM^Uuq%{8;=lQL>4)z8 J`cGeH{sFx5oZ|oh