From 37d331addc78ec686cbf84cefef42a6a6ea29aa0 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Sun, 17 Mar 2019 19:56:38 +0800 Subject: [PATCH 01/36] Update imlementation badge Signed-off-by: Tran Ly Vu --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index f85d0c7..91dbde3 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ A web-scraping application to find the minimum number of links between 2 given w | :--- | :--- | :--- | | **Quality** | [![Maintainability][13]][14] | [![Requirements Status][19]][20] | | **Support** | [![Join the chat][17]][18] | [![blog][1]][2] | -| **Platform** | [![python](https://img.shields.io/pypi/pyversions/wikilink.svg)](https://pypi.org/project/wikilink/) - | | +| **Platform** | [![python version](https://img.shields.io/pypi/pyversions/wikilink.svg)](https://pypi.org/project/wikilink/)| [![implementation](https://img.shields.io/pypi/implementation/wikilink.svg)](https://pypi.org/project/wikilink/) | [3]: https://travis-ci.org/tranlyvu/wiki-link.svg?branch=dev From 3da1f3307b6ad5c528e5575bb90aa42183144468 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Sun, 17 Mar 2019 21:29:44 +0800 Subject: [PATCH 02/36] Update link to project architecture Signed-off-by: Tran Ly Vu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91dbde3..e1542ee 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ Feel free to add your name into the [list of contributors](https://github.com/tr Project Architecture --- -An overview of the project can be found [here](https://tranlyvu.github.io/BFS-and-a-simple-application/). +An overview of the project can be found [here](https://tranlyvu.github.io/algorithms/BFS-and-a-simple-application/). --- Release History From 610dbe5a21859fa489d24737f2b77da1190b1257 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Mon, 18 Mar 2019 11:22:53 +0800 Subject: [PATCH 03/36] Add operating system Signed-off-by: Tran Ly Vu --- setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index d3889cd..49c7fa3 100644 --- a/setup.py +++ b/setup.py @@ -28,17 +28,20 @@ package_dir={'':'wikilink'}, license='Apache License 2.0', classifiers=[ + 'Programming Language :: Python :: 3', "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "License :: OSI Approved :: Apache Software License ", - "Operating System :: OS Independent", - "Development Status :: 5 - Production/Stable", + "Operating System :: POSIX", + "Operating System :: Linux", + "Operating System :: Unix", + "Development Status :: 4 - Beta", "Natural Language :: English", "Environment :: Console", 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Artificial Intelligence', ], - keywords=["web-scraping", "Artificial Intelligence", "breadth first search", "Graph"], + keywords=["Web Scraping", "Artificial Intelligence", "Breadth First Search", "Graph", "Data Science", "Web Extracting"], project_urls={ 'Source': 'https://github.com/tranlyvu/wiki-link', 'Tracker': 'https://github.com/tranlyvu/wiki-link/issues', From d254ce7e6746aab50a7b9e172088b479cd721549 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Tue, 19 Mar 2019 22:41:11 +0800 Subject: [PATCH 04/36] Add intended audience Signed-off-by: Tran Ly Vu --- setup.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 49c7fa3..37816ce 100644 --- a/setup.py +++ b/setup.py @@ -35,13 +35,18 @@ "Operating System :: POSIX", "Operating System :: Linux", "Operating System :: Unix", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable ", "Natural Language :: English", "Environment :: Console", + "Intended Audience :: Education", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering :: Artificial Intelligence', + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Education" ], - keywords=["Web Scraping", "Artificial Intelligence", "Breadth First Search", "Graph", "Data Science", "Web Extracting"], + keywords=["Web Scraping", "Artificial Intelligence", "Breadth First Search", "Graph", "Data Science", "Web Extracting", "Information Analysis"], project_urls={ 'Source': 'https://github.com/tranlyvu/wiki-link', 'Tracker': 'https://github.com/tranlyvu/wiki-link/issues', From 6b691f91772decaa5ccbf5cc986512bce66ed36a Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Tue, 19 Mar 2019 22:43:49 +0800 Subject: [PATCH 05/36] Update universal wheel Signed-off-by: Tran Ly Vu --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 9a11c8b..ab86e17 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,4 +10,4 @@ license_files = LICENSE # support. Removing this line (or setting universal to 0) will prevent # bdist_wheel from trying to make a universal wheel. For more see: # https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels -universal=0 \ No newline at end of file +universal=1 \ No newline at end of file From 62fffc93c0ee6f0b676633e18ed6819716150e7e Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:03:33 +0800 Subject: [PATCH 06/36] Remove universal wheel Signed-off-by: Tran Ly Vu --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ab86e17..9a11c8b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,4 +10,4 @@ license_files = LICENSE # support. Removing this line (or setting universal to 0) will prevent # bdist_wheel from trying to make a universal wheel. For more see: # https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels -universal=1 \ No newline at end of file +universal=0 \ No newline at end of file From 6ef0a24fac12b461feb5d7fa6495462015019618 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:03:40 +0800 Subject: [PATCH 07/36] Update logo Signed-off-by: Tran Ly Vu --- README.md | 29 +++++++++++++---------------- img/link.jpg | Bin 10822 -> 0 bytes img/logo.png | Bin 0 -> 42194 bytes 3 files changed, 13 insertions(+), 16 deletions(-) delete mode 100644 img/link.jpg create mode 100644 img/logo.png diff --git a/README.md b/README.md index e1542ee..9317e03 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# **wikilink** [![version][23]][24] [![Downloads][25]][26] [![HitCount][21]][22] [![star this repo][27]][28] [![fork this repo][29]][30] -[21]: http://hits.dwyl.io/tranlyvu/wiki-link.svg -[22]: http://hits.dwyl.io/tranlyvu/wiki-link -[23]: https://img.shields.io/pypi/v/wikilink.svg -[24]: https://pypi.org/project/wikilink/ -[25]: https://pepy.tech/badge/wikilink -[26]: https://pepy.tech/project/wikilink -[27]: http://githubbadges.com/star.svg?user=tranlyvu&repo=wiki-link&style=default -[28]: https://github.com/tranlyvu/wiki-link -[29]: http://githubbadges.com/fork.svg?user=tranlyvu&repo=wiki-link&style=default -[30]: https://github.com/tranlyvu/wiki-link/fork - -A web-scraping application to find the minimum number of links between 2 given wiki pages. +

+ +

+ +

+ + + + + +

+--- +wikilink is a A web-scraping application to find the minimum number of links between 2 given wiki pages. | Build | [![Build Status][3]][4] | [![Coverage Status][5]][6] | | :--- | :--- | :--- | @@ -19,7 +19,6 @@ A web-scraping application to find the minimum number of links between 2 given w | **Support** | [![Join the chat][17]][18] | [![blog][1]][2] | | **Platform** | [![python version](https://img.shields.io/pypi/pyversions/wikilink.svg)](https://pypi.org/project/wikilink/)| [![implementation](https://img.shields.io/pypi/implementation/wikilink.svg)](https://pypi.org/project/wikilink/) | - [3]: https://travis-ci.org/tranlyvu/wiki-link.svg?branch=dev [4]: https://travis-ci.org/tranlyvu/wiki-link [5]: https://coveralls.io/repos/github/tranlyvu/wiki-link/badge.svg @@ -36,8 +35,6 @@ A web-scraping application to find the minimum number of links between 2 given w [19]: https://requires.io/github/tranlyvu/wiki-link/requirements.svg?branch=dev [20]: https://requires.io/github/tranlyvu/wiki-link/requirements/?branch=dev -Combined Image - --- Table of contents --- diff --git a/img/link.jpg b/img/link.jpg deleted file mode 100644 index 53da2b78f5a73945ebf7c415c57bd4340adf2ab8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10822 zcmZ8{1ymeOv-ZN`?ry<7=;9tA3ju-?G{Gh4;!XsY;4VP|iw0R-gS#*8PH+tb!oPXn z@4NS&zvrCR>6x0Is_Lh!>S_LI9YCO>1XcndAprnLhzszv1o#BNL_;UQCm+9^0my_%KtiOa9su=U5>XLE{%=A8qM)LoBO?YC2mnYxWFQJU5E}>tq9SfVLd*m{ zLm}cvmD7BOM$9PS8WJzB^_7H4+uZF#LUm2oq+tG;#l|_h!Zb4^@y8V6AykNU03!We zjQ>f=;*3Qvmw9N<5c5CUWXC+x_8U1H2TV6rOC@?A%o_&4OT3hW*-`}2W1IRkN~ps z$gfe5kT5W12?&B2SOADllz-lvgqkwZ5BkE~C?uqsbucvXo||`5J%;`go-G1$gKEl| zWb~S**9r;gF|B4eUO8!>>%)=t##XH%_tDPD57D~=ORp-IM?I|v&}1FvHQXh!2?92p zBXqgm?WiqOg)N+!6okhpy8qF49!%4XdiGxe_**PU;9)j@GAVQNZbO&_awU5x&tkAn zm2hE>Mm4tq%lrCOLWzr$TpdPKGR0I({#$s3cui}o zD48mBU|yW7VJ)m=(b-ifOZMXtaX}bTk58$0s{oXo<<2+N#k*Kth3Bzq;D8Hf^*>7t z>@}=-n@X`Uat8D0*;RO0x%4k>r8tru0p!;h~8S2&LYLfX&d@aT9%u=~ki zQd%1Fr4bClpOBFL{dE|!DuCbuZF03jbRAn>27ob2V&1vd>%>t;Ue`AuyA>WEzkMyK zxmh1C0ZuH7$@)t}Z(-XZRh`tL$5-VRZb^(+j%$6Zp%S>&Q1Qg&qpogA%!uJRia4yd zcMy{zQx!0ez|Ea#H!eEbYSZrratdwtX;6f*e{p?vBT~*Z46W|hf+uduz7f<*RO+GKOn>v@%fI|!pUua%efK2^KH zNoin_f_LnlI-37^Y70eptOZw4Lqh(Jk%XB?$ItPIueA!kMyF2z=QsuJ-g=rd^WM&# z=;mxDFVq6`gz8jb>uCuzoSceu*wDgx2-j}0W3k2Ivw&o`8pP@%I1@SBw~5NwKaA)- zdIU4R@R9CH9P?47T{iOp_JQP7{stUd2U=-%wIGhv`sb%M?q}`TnY!h+g~Z6-MpbjF z)4+jQlh|@~3&XFHkmdj@TXz=~xG9WX8vJHRcI``?`_ff&N4#I7FNSm?`bEQ1{aWo0DNq^pGKxzZNnBB{1&8J*QF?;zZUZkH)`}}SS(!{Lp zt@J}%L%BOcc>YOF3tY@G5a*-Qq}tl~FzX^sIAyf8-735>SfTMw?MTWrVlS6;9FL}yB14LE znCIZK>3nM?K&5@8AKl7>bNd|Wl!|bbNQf=)qRztbb7jVbl4Z#zajnMaT_(EH4XvT` zPtHVm7cOzyG+x%YYpdvBNNeI4$EGV`!a{6Lo9c4g=Wca@#hmuk)%#!6FRNx7VI#w@ z&_bmxsf|UI%|$c0AG3?xPkuq51+bc<*_6Gp&x^b6|B5J%^lJW(IrK5Zih=zZvBC^z zz#uM*GA8Ii1<}!@M?8Pe1x#JC$HHO_>+(kS+n| zeWnPmtap01yv+CnfKjSfeud}JTYU*?Y;tM!hHM+uHMINp4;Hs+?v03C0W$rG_Uhid zI_GpNYda~Jv#7XJtp22u1kfi$CHmIRlGG_GZXg8j8RDZ!$~J)v#*O=v zZb2a<3o88YqYLBJ>ab(&zgsBb?e7L`jojh>Tq&%Hiy$StObv0w#~|NRJsnL)Jv=tQ#5TW_vHlY zh_}Es%cb7XK=eWVva5$GwOg5lUV5mFeN>x2wf1=hOGWY0b0_Qgm0_nz&v@67lhxW; zk<1i5q2?Dy1=$ahDs?4DEGxy-qVX-k)S1I@wHf0^BF*~dl(d*MY&XE1;QpbApz?xe z#jn176DGs4dugua9Zrri`$ie_-fX%;oM>94EzMK2icW8^&(0;3+{y7hCI%L^=&Bvs3dI{>4&%Skd zq+K(cwd0uf3}2p(I#m{wK5`@&lh>9NeUrH-xb0m&UZSa|OKu??nE7U9I-q0)u37mH zEc#D|+V-B^{ma{IM;_0UY4r}Eqt2Fuz5MI;P_>+B9c$6Z z7%R5%H+ly4;62h@amrWHs1*R3CZ%Wx1qytc0_`m)mDW0WC!l^vJUdX3`Zt_M53 zdvIhr`9bNAI^D#tvuA253sM*=2v#ogk5hj9c@=^HB(N z4{T#+`RIq8JhHFVYyJk@bSNoY;1`X`{zJ<%vWaqxPd~ASk0K{y!|+{x!Ch#uOykEh zeWt7XDkEOsNUi%AmZCK&n@B0qZelzurOVLxGr=>{j$$1{Atf2D!fQ3kBecnaaLSSy zhsM5w-fvr@I#3n&mBMn9Vnb5W^%E0b8`-~mF(A0EC&)1Mv&H~#`~$t~m|4Op@0?(s ztUM6>1Ssj;&GS;!cw;_$@04(IDgkj|EtakQ~)^p!AXd!Iq2JLcU^U5)hj~H!sty|8l@fcWU3fY z{uP&Qm6}jiO6#jwYEt!*QS|~-QCL^^46nea{9_i2RyNAA-sSTAmkg3AA4{fRG*(;G z-<0ts#V8x^_l+)}(1a1Syv{!yx1!-E-BO~~Xxx=hwVmzhkkSV)OQpmDoQ!6mN?DF1 z{*Q*WWjs)UNq4bNlAIgo_=W5DPCPCXAJHHCStu33hAR6>R_tvhErJDCX3jW9*k;IO zex0@(r})cGmj-F&BJwA7);GkY(_Aux5Qu0gIRe*Coh&A+ zHInE)ZpDLMW-@Idfa|MHXsUQ(w5-oacL4Aj-DAtMnRC`f`$KzKnHlmdw})7Z?fum` zNZdJ-5XD}`wTF|(pgnusp?ey4sv^nNzzu#o4-rg9#`f>J{_KEhln?bP7)*9e5;ycT zET0^qnc{vyXpS4s^jdyZ+8cqY+uZRj$$$~wAI?+Kq>l!@vM{#A%CiDI*d{9$GoAqI zd7I5(=jrc@KXq}4{6jCiky^{QGliC3extZG85`nRc&{ER=Xy@1lr>XD`pwdjtx^#` zEvwDrDz<5%4!F8qbofN8AzRV@h7EoKNGG%RpGkNwhiBnGDG0RuInAZm} zeHg2F-~Gd--zk2J;9%7;rAy-1y}*8vJ|_!E6}_NuBuf_U+h*gU;+;M)Zj9u12z~-I z=~%n&!uPud9kjZvZrgXptfh=SgUnC;x3xRnE4tRC)C&D(`cytp58KYK9(FnVIsJ7L3Asud?@c$ z!y!{GOG?IS3?`j`$EG4|!U4Uss04EDju&Gf197L+pcnS)B@)vKX$)QiEv&kg7X5jhUj6;NoesrC2xlBG%NTQlWuhB+Rb>UtV(I_ev88tC>Wo1o@bj^kj$Ph` zDl~dT(5=fx=sbdOYtYKVmc0#DFjqIM_$o_7|yCYE; zh?$JYU6UiLvgo;2Jck^T{&I@RL4YzA>DF=~TjJrD^IZVe2`-zKRMnuRW?wyYxZ0<6 zRNL~XPv=lIP1RIF$r#CNh-x7I>wsD-{ImU-gq}~;!`jSPfxN&%^WYQUa5b#0tY&{Y z&t98IAq{E7FSbZ=Ve42Dzhk|%$j}X1%ZuN4Y&1M@1EWyNnenc|W($%L6qy(rKct*z z<Al2R||?hN0zXXI)B; z1o$+rAGmOOF;Fu@HDac6-mj%0!1MvO94lJJrD&wsB1KldPt99mBmf&Iq>SdIK1W|0Vg0GgSvg-=UpTgJ?BHlxDv%<{IX6N z{hm#k-8~gD#Z`khlxCVik;lT>xeDch#)UVWUr+`iwr!)~b9Iwefra%Jfw05qd)LL} z=G+3?@%0-PbpE1ahKHF5uli*U*NA0%od!4!a1&KQm--yT_Lxcb{nm~i>3HF7qkURKZ0$_p1E+Tv#;y_KypN{E z{>1jJj&u9&zQSfn{*x1a3b%rX)y&D)BIYY~QrA`YOusT8N{%X-D}Ukr_lr-4HhKT2 zm9${2xP}Dp*pJ(EzR2VXZ&{Zuf9Rxofx+A&`2K}#MCRHg$iiXy(q11HZ9VVfH0I-U zukdoR6ZyS`yt~PM*(T|)b<2D{L zlI#?XIET&@m~skJ89IrhE3M?(2+B0xtTM6l0*zjv#zhPh9QRTi7@rM2P-f&?<>de` zbWMw3U+rLg`2{a|cl0{ zHO9ENAuEzT6{X&hkU6%lG+mcjyMv6)NCVoKi1N4E5Tj$Mi#{P|=4^K=7cFRbG45@E zVSI5|nPi1k?{;iNH6Lz%AFMVxhQ|)XJ7RUpl#YzHE>>=MO|!i;dlBCxpYTb`kk?4S zA>{3D0Pp-sHyqM=*Ze6ZyE8K-hdga(UGD_18H(+!Jig^}lC`vaR~_!j3l*?$&0#rV z^O9DW;F2ITiZ?3ECExUZ4#^|hb3z6;Eu-h(4AOxXrka^*)XBo$E3869T^g*~&C zDNS^944M2;<)(^n?MF6CjGrNm+$(~A8ADdPlTtmR`S5+ge2%LlWH(frWpZrua9;7AB+=9#r<>?zg#Pe>tm;(3s!1iT!6{8) zHIobe))V$`Rblm|`~~XbJhx|i-;>H_QmP%!1(^&!31sq?7wm22zIQLuL8q;rQ4xt_Cr^GdJh zTve2ZIBMshgVFq3#)TJ66)3^0Y;GT;IoO0xH0+D4^VSl6iCeM4-`8RJ$c378?f_ma zndo7=`0T+0khR}fIrg{CJ`M%Nto zjJkEtXYzbutU_M)c$zg2kjU-jkF2=9)``c!l_p+FDmb}wrrkFMDNZSqNz4`M?Yc4o z@e{ABLwfFWFI}!o-_WgmX_Ew1emJ@%mmpHiz@~IG{Ykuz+Iq+R5TI+N(RM>6qfvNr z=*2tj|E+TZFabbM7;rk-9n#a2dXA4cTQXo^KU0Kpbm0BLvm^G8%_>%>C-8_c@yxRP z*5Ws2#Hr~LkloL-=qL5dH@4`tv8|V=lgqZ=BWE>~wj@_fF9{0nQ!L~S6WBxg5a9bE z88n=|k9JE3VAp|Cxh|%BI+#udv-fM4G~lsMTGNKPylo|7syoKr)QIeKgJfnNOUKB` zV`Ur(U`Na|uy0k$|%~(w(9lUK&^tlMU$|cC&NQR6|Kw+BnUL$$x zsIz2{ThhDkLMBb`dt+?moBeA}^RpEpw^_S0h{{Y>KUrH*B^kr~hX!peeI=ke@3{7l zt8iHrV8Fh!@+keIqILH`sL&xi8@#@CM#o~Y<*E{7DYWYN)}@kuCZ{C^YLg)cko74U zB;!-;`lEFI2Vz=m(N}42X`%QfAh7(*8Oym_SWx0dwTU*=EI@`&IbIGCMtmRC{jEN7CU@me_lpzlX z;q0w-VYV;G+rOUc7)_iH@~C2EdVW;bx)~QwG7O+OQ_es7erbPm5rmE&@h!V*w_SCJ z;+ssfb2;_PP0?8M;iSAR=%S$3L}mWVya&aL@76AGA$q%|R$d>Dl2ac7x8+)n`efq@ zNY?6iha`f2WT$!Nc-z+bh^hXizGEKAaS|KLrSah$-$>DB5`Lz#s#j_omDMxIh8xYB z>!-wBDn!{LF!b>^n0?v0wZh|?QBs&cMs~|+1Th{3&-G-|bn7=eE)GRjt@sSyOOtrG z8!-2T-ekRZ+VnD5PP^9K6HkU@a%+DUH+xSmWi^ZCblQ3oK4ClA>3Y64tT?7-TPLn4 zWm@Dx^zQE=Iyebc9A-FnJ|_nSr2&l7&=n7(eCfnkpmve2K9<5;T;1iAi^x29g5)cK zgF8}h>z%))th!~k!Qj86N<9|0sOV4#JeU-k7b~53Xh{+|*%Mfh5QhWOh8r~;1=a!y zbD%Nz0V@@ocVfG4w3N@ZXVgGN_*eD())edNi2XO?2{|mECo{Q1P(bFEYQ+g1^d$ie zNw4k#Bu*BhQ+Nt$U(Tqlhn8mTb`G3lcJ8 zIw)oQELVYQ6Kl0Bw8gvSkR_wS9BL(TmZ>?dCJ;&iQ&tWGpH@|Ba$k&o-uJm@F7aq` z=M_BUE#S;l1Ts^u0_?T_8p8`0q5*#grt?Y8RqK07Ce}2KHY+sHzd5lmo1ggNca8vP zAN@y%mb>3=VSIxJ=60Na4TH||WkuSRnsagK zNKWOfv(U7uB8G8J6<$n-N!*Xc*YcwizmjaH26VqS#`t2al!ZZvA_~UFEBECv?7QZ; zs&2d3%b8~GLkqVB-~zrI8##l#z})d%-BWa(8+d?Nl$WO8;9)Uri5&j1xs5VE)N`1V zd750As_9Tw+!rTtZGpi6TqZ9D62wF>bB!`=WB*^niFfHkURn+?0tB4(OZDpnA>_LCT4D?s+5hn}^D9R}wt;VoAc@-<8QV7kk1pBzA1bVu`4B$Er)n{!|c!_LNlX<-Ech zPsq6K56^GK9mVssYo@oW;GLVZYl88Ut^L2YRBV0V%YhSTx>Nq)aP5bhRfY3$S2qks z`4EBEe~m=il|OdB%#kJFhyvc8@V(cswv&KC23cnlt6zX>1HQ@hkkJcZJ|tD4&&W?3 zQ_8S=pK5tUsT#}h1!C6ZMBmy~Xj;^}22Z)Xdp2=_glCsB<(G-nNxX<_QZoH}_G);R z#$o0+Eo^);Ysu=hqsq+u#|f-DO953=Oh3-9wrx(tRU-I>+L;RSQ+N2J z7QJ)DS!DcTCTDn{MHLqvWxMv2FVhTfQ8%j(!*)1jdstNtuY)p28*7$Q4B=f+ZzW-! znphD>(|9fY>jE$S90E6$xjfsOfZqq*2TFHrFX{qi|FP`-C&SW(IWO*ZsOV z%nyHV)m2H}Exa*R95V2A!nU(neG7u2O_%R}SMU>adR13P*8F>*BjTBH0UJB(HHThg z^dbUZ>MKVGaS%fbR043EA5+K}m)si2oZnSV_B{|L8Dpa`JUhiDdI^m9Zdw$8PhA~Z z9BHG+(Oe;2^Yw0|3I(%e{U_l9y@!{JpFViOE>)_?L%1e?u?0Tfzd`wKP@g;D@9wS- zRM|WdkMsJ8XRMr&yHi;#(=YthHA~5kkXX=mOOoXMskxV4lk(1cacjwL8s&Io{DhQl zrusPdb6nS8=gtq)G;h0N9s0>?5$<2c|G0luNmV7V9YKulUo17a%o38UY}^E6W@F#f zgCO5$F+Na%2Q&_341&<*$tN^ROO>mFB=NJV6sM9Koc1CgQz+wzS`4;4b-LfJ>2^gE zY2*R5)OTZcl(b&!TNY5NEzl~zeT$0qtTlLw8B;*%0Fvb$EW7-jpVSw;d`~0H8;eOP z-1+_gTCmYmBj|T`4kc}*4xG9?#AB<}ie^?-vyt6IZwp=01zXgKYd`it73DRAcg-za zDg_{sgLhOTsQ%*!Lx)N1gF*;HB*VkjW~Xy3Teq4@m1*<0ia*B#)e3lrEl+?3zr7?6 zDja};Y%l2B;Qh{g54QJ_47d0-Lb0kBe=>@!(u>}> zD{nk(*R+oD)_I`w7Yf1|-Ee(VzMt7e1S~M3hIRX?^$Y90u6M}sPT7#yzS|3NZ^}l% z8>*buB_J+po@fV5Jw2o)FkmAI%cEpH3V-?!TET3_kA7-HkVV6_uy>va z@(`$$hrbcaBLpA$exASrj3W2Yvq=6@Cy7p3Y$|#-{ganM39*=Rd zhl2Y6Y3BVr#YLHndBL_R`-vbs2uNfmd=y^%RMLz zEJ)ihd~D^Ta*6D;js3*d3`aINxvpWpo>m|mn|!91t0_ja10bZ)7Bwt?4^k0{;vqsm zy(%?kad@$^QTPOqUNc+3@2;J`QWCF21P$bYnT$WSDhoa<$o!xkYhGAW{ms4BLG058 zG>uPua}95lEPKRO0+wU#!Iwn*r?92n&rBjZ%gVE(nC=M>+dFotV~A$Q4q&E4!)Rpe zp<~D0D`?T#w9DG%)E>_IJvs>soK|mDY7Q1~74j9cDwy8taIO7ZHPwGI>u|Z)kf}%Y z&HBR&>M%S!H2kCwwcF*R6fODr5saY{_f9k~e5i@fwTtfM1k4eY)8_NCRl$ zfBfMmge*#|X#5R^#wWbrjnJ}{+nSaIn$bOP-T~FX{=U4Bo*@nihu^=%yRj+3Wv#jW zgYW7+{cJsaRAW8S!Qn*c)=OQ9>${b8_RXp7drm%^E>()!u!geBaiG(_uDr|Do`*^_ zy=8+KeK@{ib_`f2AmTTbH`=}E4eZ2GRdNy$`0zIh2(h(c$Py4C{IUN!&i-`(BAB8# zT7vR5e?B!2ADGX&NP##n1+@6aYtb!cwPS;HhQ!LM_|As6M_-ukFibyTYjaDc`5vR+ zioK$LY{*D$@N@+F`3F_-7uhgv$0xvyEAT=yYsUW}LOwG!H9f&g*&d1II6Aa&tx>>Q zTCV@BlD=1Q$MNQxTaqTB#jk25{o@0-3IOW z4X=o)@{5I7J{v8HtxW@@Uv=e%Ws=R0YKt0i`d@vUXf;ToVRd!;>vER_ZR%rt3Ho|M z(rHHxss2S3nP_m}0^|j{a`Owb?#Q_=J+QlT%T`@q|7))U@}Ti|RoMl`Pk_~cd|>wk#>XI-t~vVq?zLCU7c9m zj(nEdC`3XY%9K7dp*faL2Hue^Zf;u#jceUg`E|rrC1x{!L?*R5gwdNwN6;Ek=Jzyt=ntcK$A0+95FOX0xd0 zZs^N=VjK{TPj#pTG_h*CBmX!@cDRl+a#vL*sqllFBgO{C(i-$ zH|cfzLzOG_1Yk4UEQPF`na_Tx&e;D<*(>~Kl(p^T441)YkS?-PK(BC)nh}mAQl7Kp z>CTh!11VccZ=9wA@C0ym>nk~V7#&g>9%wU;Eo9)(%cFq+@xtRj4MfFU z^3C3h$nBbh7HW{S7cm5G$pL2#V|neY*w%B?wG71Fe0sfM+oo?lRXH#$C|}YlFgIQZ zt8ER?IV7u$8MfR!(ArHz3xPfT)5(N{5aDqKNN0-QSCF)6{^&iH~c+FddY_&D7N<1^&>JfPO(%= zKipmBKWiMPl4Uah-?vAiKzLbroj-V0}1h2HeI2>Z|wG>1FZoW+nf^gS6cR9Cp(!2 zzO*EcWxh9OM@Dx_{qkQd6g*;n4~j5fvT9@$uz9&X=q5)-bx$D}0dm^7|*hk%IMfX<%Tu z>uos1WYc~cg>VzOTD`8|m$V=KQH55t&0XKaxmkW!8*P_*;&4^NP4sZXxo(p({RX?+ zNF_Ehg}`JX1>Sd>!QUEQj(=pmOD{{7TC*P>>J&5FlXUYcb~dlAHE=_5cFkEC074GR zHLGSBRUFHw6wLaXtp6B4Fl7-%(%*1E@)Uk$(~&dWnUVFtRa~PGbH;?vM({R zN7>gDvj0Z!&-b7B&5PH}Yv#G{`<&}s*E#3DpXcQxJ#~5-4jK{?5_(OI`vxQ=7jWnQ z)a1l(rp{DPh;Mfu=@_Z0yX$PwKiZ;wxJmzr`1c3$@Qg%vi|)}G$=xm5N87Y|2b8-1 z{i>;{Im0NXrKLsuKztEDyt}hIN}Pw;%RtYNB-FDEEk>V4jnray?RrJYY0!Jj+@e-MQJbYSo|(&%@W6+h%GKy3?PcZZtU{PA;bt-n+lH->QnXt}Rp&z&=TJu}I_a z$=zF-8pVpWrf))}wW|pJ#@@MZL8AMAv~Yrsw^^MDu<7oA%2}?v1#tZ43I?hF z$XI&L2z&$DRd}t9V9SaJYOSZgLR<23f=1h|3v5fI*WT56uaB0}s^VDI?8`31B)7&eghNM33$ZO<`qzU{ZH3$oF#-!EqpWqP{o(qS~wHby`g~{5A`p#>1DI-^bvnV zhmY!ZbfgMgLl=TnL8Uw4I&=tB%z+1H7-|4n;t5%hSa7&;}-$7UddNz66pXeymk0 zu$E-sU|bd7Fp=$pBTiKPl4zIDOxZ3Gg{xp`ng|Udde^;TCeN(XGLsS%a6_TjgG=sP ztP=r|0_q;Or`v-V+o1O*A>LKiWj+>7Xx5wHk51(0+6f{wez-zwE_6)Sp^)PWMN8Fxq)63BgBwC)cT$F!}z0ZGT z_5hElbNkk+mGI(ykZ)J*$Ul?fe;W(KB^QOwp@gsLyU9&|ZORTwv9qnK=Y|h8ZQ?dA zqn}q2{>>RFjk+yDvqZGSRod;a%#lWcKL6e|2#Rq z1`iomiz(z-wYqTo}NG7t%9s)ccoACZ({BH|!fYC_z73 z>_fl6Q#m$gS)t=W_BCrFy~|soom)}uPAV1VtV*e1NxE^qnMql(6p}tFqD^<-|1It_ z@0zLu*h92{hRXRhA>zl2(X*yB71nE7gw#HNC$U4z6LvQw{Z!3XX9~rd& zFp%X?i#Wf?^UAg17p&!(`!M6VFl*Nt@VytbAK^d__qhaZ$G-D6XZ{?wA5Nr52Botp zbEJJLc`cw)qi#W@{USVzx|0XhG3*2>Pg^p!v}j?kiVwh|#k1mV*cg1}MEi?f>WBkP7kj?{=Bl!R?#xu=@d^@K#r^{g%H0`BFK z0h+VAGNK#s7OjU%I#uebpMD2FzPV=BO6RHKkA7ABu zICIYY1-EAoh(L3L(=NKzJ_c7>$3t0zlJ4{R$A zjXF`61}!E$xgUVz8Jvr=r>t2ODZ6!jG!h6XIid7Sfs3h-5gKF@^eN6;Bc}DX z5;QR99E$*8oq|*ay%y445J#+2M0LBNlePUYG7l}zN z5H~C_=cgyDlvCjmCIurB`n`_Hg5~`X-C=YpX|Nwdc~%STA4j;>zj;4tB?-K11O*+FOZ!^5Z{onX+}!-hX5SyaKdaH= zcRarcPZ0;Xe+t8JGZN+hzG=7ng|v~g3HX>cxIzpJ&Lnc+o5v;W>P`l|g{&2;4C>@% z>yn!tt1H1v>F?6oK(HN^v+pOgv=vjN3@09bI(=XDbm z{IgT*aT8{1m>Azy2Y#%`Ei3vEw%Z(w>JeuZzNI~J*Ckj=-NJ<+GiT2nm^cS3A5YT1 zc_lUevZBe}$+<~hUW7|g(Ki>D^$`8;A(#Z-PY}73WD$-a1&dC?I{a0{5j3=qC9B(( zkKB6a4~4x`!!ZuDJy~8MwVSjbY3hsrzB}VT6Aoqdro+L<6vJ_NsnTUH@`J)CtU#lj#6$LIY-W9@};f#B)emRamaDVX#U6NMW zZ}pg5TNVg(F%krWeaa4>w{uvrm{@g`R6S#Dj+SQwVC5|WY%J1jn>&`@o}0*uhhbo0 zn5D+VKjEtwhL=%xyS4^rwlnp}=^6To(KUofS1HXKlR#Igkx34%@pI8-EZxvOAhvULpT$J%mh&`s zy$K~7yGJ$+O%HB*qj0-X-ef-^5Nxa2P5b8>J;GeC<}i9JJST zsVsI-iCro^B#Pskb^KN5M1j<%9M2HvYim+&LB(ZcfWCSFu&%`W8mJe#a3qedp_|@% z3F3oKyQ$S=BB#b=eW717?=aU2N!oI(oje7Zt9K>-jUtIrVip)@)PwiaaXq;=%6MxI zU55p4T>?%=`^0hbxn*AJj)E|mwVAs1+JsI=knP){`9E?{!P&TYU%XbiC!Jti^`YV> zK9C1@jLn`^XlvWpX(-}7| zRzF-mis@xK3Xcry2!Bv=TZKccEeq0VVCeVSS^oW0Ovw_Q){@A^i==Qp&Bb^kD6mVAD*c?K|(()ztlF;@0Hdr032T^h0V+2C;u8 zn`SiqLC=Kz#=T?dC_#%3vbzG9BH3xMx6OU8{a7kv$(pli*mSS_H--*5bhXLgRdyvH z7t2PpKW-QIq;+$*r?;$>VHsv|e&-aMX)E_{h17gDa9G!0 zgWmD`V<8cmVzK zl;o)otw4O$*)CR14qQ%aSv3z+OX2wQR)O0YC}l2pRZ&O1ENCrbF?mBI)@5=w-{}-y zJaW93wDa?h>hZ-HvA|{Q5?`Au2FP4PCJFhb#k+{zeK#Qha|9LSASd2Rgb>RDmgeYM z8Qe&4d@6v9x-IDR!J+j=WI0mtZgt>R_rr)%^zIw4+vJ>oy~1tIXwlNa+~<6J?FL~s z>o-v{oxv2~Z6nErAEOn_Ot6iIu5`O5{thvZydxgkYMSDQhD2H)b`nLL(eJak&i&h56h50Zsg_1CPYYB`rTc>)H(_-d!vBkqH#4u9; zPPe0A%Hdmfa!RSw%$o(}upsXv{?W2tj*sCONEIDz4-hQy-cvXbJo5AKYGW(&Aac;4 zIavVCWC-*OiFws3C;fJFhG9`Ira;<6FTI)??xzwE1Cj2FBRg`8u9y;9tKPPqPoBc|c-!OU z*N!m0vl?s8$aa2Q*$;%%0Z-vX(Z5 zK>g0%YK|B&mF!Y{;uD+m!6i8Ph3X9TrcmFAp45vI83W53YPXq8TAR}8!LW)|-trxD zmrr5d;XUfQP@hb9XNtpdY7g^FEdF0$=wJsrZ+=2~Gkl|xf5o59R8+vfIW-Lia$k+q zlL>EXT6i1WbfrSTDDud<7VDsb<%em5M^!Iium0yRoXoQvMMiG)eX;(D#aV0W5!x^b z5n*!4Qc+kU*Ul91c%9hJm#~w+`Z+RQ7AGG}`@iULNZ|x9uXKWZ8SL2tq&~_(O4h7& zRcCLOC#Wj<5kl9f8?Be{deZL*Tv6e;iu(#8oo>*Ef=mEK!t%$FCD~BFt zFBXohT=!_Zj&jnQ^Q;oARFHjxxAr`y5Gw0^Es?FNbD7YRia=N&))Def^b!c(z_VE^ z*W4J9+X{>%J<%DidRZ_-HGV(*coj|#)z73rK8C5Ia)YnKv|!%qOINvj{#B*b=GBk& zc{jZl1sMK3Ge*j7#S_YXyp_WJ40v#CUuBFH*@EmIu zNh)$Hn%{xrr%-hIAmyiH*a%^z-f5JX5|^iUA$63Yd|MyahQG(GUB<|P3NeTYk_qu< z#$7jvc~R^Fg%wuZxr7~?C@vLxrL`7lxj>Y8L8#q+gumPpFMev{>YR-n*BoJK41}F3 zeG$MFRhz0$JD@N{OL{O^7i^$Sg;j3i$-$aul{Wc}86j7UOPI7|4+0`yXUk|4C*GPe75tHQLz)rX7c^=C%} zWe^i#FAjgkqdna-XR}>B#O!vx=S`ETaf2hkgP)Es0dHMiX0!J@ z#Y2)P;@~^zf=%XOqoxwKM{n_BO-y-XOm2;rpWiC=`cfo5^ZQU(gC*4lB~qWRD5o^L z{lHKoLpj)BN#~!ADM8VKz95yIAs-xa?S*2oByyQTVPyTX|5kXL2)*rG+NB!9FtvS1 zhfcT}?8digpXr|8n>%}Bq)g7?h&caNBI0Sy98e}@1b=W4xHn$e+Fa||nWpcg z>1nVjXtb`sRirlUqZEOOOQNI&-kO*qT_a?^)KmIo&P$yeUrGsUYagz8_mljx0WK-Q zuA}7OW3rv=q?E; zWnu_;%w%2xW_WR}o;gF8^TtHr4VOwIk9a6?iy|tepuvpajXzA@SFMc;WQux<17y(B z|H$3MpT7?ooh$3Jg(Y&(8tag|>DC_|p#x8~N`ErMb?iaLrN|}9quJP~z14Ed-jaW7&hoIbY9Oz7L!`hb6!QJS`L2MA}u@+^lpTrfLJAzIoF z^+S!KQ|qke3BJ@gFy*|7sJ(&%9aQ%81dmjod}`VI-f&x`ZAxWd;M;T96|ZBAuh%>T z$6v@hM5UTuEJ{CVm2^tkl$kx~E}Q}6WkTwVPXaDuOADmPeHyu&0F0G0AA z8%%rJIeem$SH@BiOR`^}nO`fsuydW)?HQmWZ@L&@XDGbYy0yn;)%84CO4a(sJldJsapKqI9^}b87ZsE|T7Q zx}eobqg_l>3N*HvcvM11{tz{NU7Ae5XdoN$XP6*#I_qPL%Kz1lSuj z1TV0`^`xt>T!X@M=<70rl(Noc-ZaQ0P{(afBJjqDCV&cNeySpQHf>;juvet)6~1;q zy#GF2g8r@Pf)I!S>=BO4&R>1FMJ{1ol?dAPPCGj1HQo%R8ystuE5x!*_TvLXgCZB# zeN_y>%S@|@q4)qg!BLu-^TthMqE%(DVOwk2r`E>m$e>71<@OHV^2fR2t%;kIeLZTn zNxyS<152L4kX_*QEx!Uy39Qx50~&V!Da1o!XtA2R6#3_YDR8-e?vklI~eDf3s^`=-_$BTDhSxYf|n$sRd-M76{#eoHD&+uQY>=gW75 zF5F@EL0*bua(Q^S@J1g$SUhU2MXX0u;9iv}AUsnaLze7!5e>E&@ z9I?+TFdfwJBDGccx^}#6gBX5h@qfz8df71pxw+QGF!fU@BCZf8HJS5g^t%^&;LM_3 z(_a-iF5NJ~v`3hwIK!VGIsA}k5FNHP%3s+JndX|TWk49PWk!Ge-rp}Pw=@9PZFG_f zjm#@YMytB4FCTxeKsU8R$x|CDNPFt+vlIJVk*<#=9PLkUd#v473C&&#^x?zVC!xcS zKJog(NQ_9d3+L-Of$e?hduvy3U{L(9e`7 zVNm)v<$-$3K#jS!-Aq%S7VT;8;IE-(O{_na>30?$xRDDeIQ0!U3#Af%^AXfF2$OAU zP@Chmq<3WIz^$K9pji!=Dp~iYO`uR-ZRnita&G}L_W=9Y5b^!ej-lcBCXb`hAVSZ^ zxbrG}?9-59p5Y#{=v9rSRY>(2O&J}qr$-6$rB%*`GK)pRu6 z(+H+=#5(?qu$IURdRS38+l`n8?b*zDPzWr7aY7mcyf4umn%s5NrTR!yM?5X4-=TyB zyeA{@k;lY&Q58cU`$8V4Bq{w+SMEzc4MiN@XmEx8 zl(e1QUN9zmOH@bXTGqStd)?P*KhcNCUcw5MKlm3-VSbdxM=>CuSzdEi&~}fe=1yAd zs&`qUG8fCxv<3(3cUsP0Sy#oatzScT(18+j>TK!c4`*ZCk zmd$a9mi2l$<*!vgUB~9~eTPuA6l5kJ=0& z__v+%pmkLkpEFflq~@$DU5~m_R!`b9^C6T!1JIk|f0Yni*}pQPy!CU*eCv`P7bT&b zeq(UKsOn~a<_gp2MO?t~}V2IonVAJxV;zU1ASh;rh;;*(8`sN1Hrr=zs(+kdR6qZ&$F_?(GQgYZ$ z*-hC%BhxU-Q65$Czc(ypEu=T3arQ`&>pf53Z63x3JgnClDPvxAqTsj@^I5jJc>6@* z4NpTz5`q|k*Fq4k*Dux5F#C;0C9Hc3l2bl3;G;1#UzKvT-fXpCwYg&a zd~x}wyPLyz2#+ke-2Tas9ogWizTj;2+o3(TJ&>O+W#?H?^2)V6(lw0y>YgC1R{M$q zGp7f%eD2yxS-J@}!8oX8z$R`1L&Odf%Kvw>=pG+1EPP*nnrFH*ji<4&q25%DjqciR zpTvIFg}b#ZUEyo}bXG~MnYyUyImAa}**AzNXBA>1o27t1a#lJ0D_LIQpZ;947DuzG z@GvtiD6`r0!BwA*8cBqwb<2gmpg+5>Qeo}DTvjuvh^eJGv)(1ugQAr)w z@-+gR5+!E>j`nXur=l^>aKuZzf85(Dtc}8}S2RP^k(7`SOn1h)N^yuxCnac_sHTjeR zUWK1o_022#4=Kw)zK_1V$eQ6q;E+j*qn9Z*_j<=IlDlg*mK3swGt%gev`zf)bnhQm zDNg8JS4e2Dy(lgFCpme3-ty^jIIpaQSZuxX?UuLxuGZ>`g5ELxFO?_acwYziwNbLI?sa)79r7KlC4 zf!U`driRmem}xrsVGIc=`=M!o1dn{qxY8g2;lW|%UhP5T zZpUQ0Xyj3F&*b>yS6d>iHcYx!7L45s1b^qHA%ImdK;2q(z>z%e%{Wb4%{AC8FI*fs zUdMpbYV(;5?n*(t97WI;UrSGsj-!A#Xut7($!c~HF4-;?egkc4Q9ZHaYfj(zO!|i~ z=C(JH!*qZ}&AZi+qia+>q(=JI^uAMAQC|&Wh?~G|G1fDqu&GDJ#M8 z+|EoI;BfXV$AvL@TmMP~X8G4k|Hc!%|7LkBO?cRD`)E^$>A|fUx##GVm5dJFUlVC2 zpvD6Z%itrhY*A^>(fSz?UPDr6D_y~|)$h;tVcf8{C)g5g>HzdtJ#uUYVp9{F6`MA7 zdo`=FhBA1{g1e*mvM2m(Rjv5rAILQFp9yq899QKlK#nN=zDc>qrBqNfMN;K#^MpQQ zBP;x|iHTpnMQz@@j1Be~BfAn`EnKa5Z-Kvd_I4Kx3fW-C$zRT~V^;co6OzQfwYr(} z0353KHv^u$9Q~5k6<`jQ+UJJ8r+!U`MS&%E-xQY*qKAxg&vT8Iiu&^@xj18Vg22l9 zebZy;vc=1Knzk@wVRS(zh5T>qZfvR9efu8YMNwy`p7v$Mx8$}kDd@v+&Z?q z=ieJbb5Ut2M_~OIgVGxxF_MMt9vk`Hz+yRUCGmp@lRN4yR}mD|(UbL#(*m%VW)5}O zKb)b}KG}&euTg6%1p+!|A&(vbJkIvAWiqtRY=9+;$s-Fj+1ZOm2yNiQ?@b2NM{Z#U z(7~Hm!T%{}^d>&EFqwG2Y`%Tko%C;xuyb};B{@Jr10JZ#eXm*Sy-ne{L?fjStWB4h z7d86_OlUjPLNS9KXvdOW`AP68!2 z4b#izEIAiXDFuup6kUA(yPNdhQ!jJHy&Wfnnrr}&bL;*|>l7LM{(`j_m@YlnaM<+I z09S3Q2B!{}P9+AWQcJ}@Tb%WP+x}Si$1&87VGRvF*%o&BJbE1`W6KKStOxM^g!b-3 zR>zv_^<`EI-IZF~myH0`7p&)+rGQ0IP1aPWZKQ}R%I{MtTp-vSMlM`)`r&jSdltW@ z5~9e!Y1e(;26>vy%z#@tm(4@Pqk~tcnI3a>BeR#;t0OB zR34^n**6ra{`gfatXzs5N)}YvXT|@QaSzhz_+N0aC%)HiQ&uK3h$`Ql;&vI0cw$Mf z`=B&|8BXTRh~TWg_z9vA2r8^ulO|zWc6idST6n5cIl-O~v#>C7qYT2HX&mx8@&;A1 zzzuEiHvYlyXj6c~7>&V!pB)mzUy7d9Fu&&f9YQQLe zW#s*ilW9mC1)Do~C-X_slE%|<^sjwR9^q;dmw3@*1 z-Q!c9xmV_ldym2GBs7ICO!iPI2JAOh85HQn|9H{gFwi7SR_=%Pes#X)Whlg=&7<=) zfJ{NIbigV5Y2{9F3BL6vTWXYRYqYuKXRSIu7m+1fyO~VxCq;e>3(4YPO1Yf*_~*p9 zc7zrPV;}U#w!!P3V}-pwij=ofvB7ym7{nTTX(aZp8g5A7->m2d2YQuI1-0OBk(j*= zC3e%2$lR%+({%A%e;fkky5m$!%!Mt(#kWeod{j-o3Vs%fzIbL~BGh}&Tq0%jLoq~s zkLF)J2XXHOE*!~Pdyr>`6!hL2VWtmKL;W3TCHqQO-|i%~)b8^lJ9V@F5B24n!>+C$L>$}$T@Sp zL1R=1U%EL1I3a6g0ZZ8hHr@)^ci4 z5Crg_P@tcmk5m;4+fLcXbcQ`hRG-ax*`NnF&qmQ z*d76C0{rT(nc}|hd#R7Yp(S?&>2e#3WpXY%(l4_TcR&^;d|u{nbQlA{0s*h&)Zss@ z<e*ajT)sJiuA&z&e=N?Z74t6FUCTeXZORXW!zMOeqtS{_yQ z^H_n?$s8sZM{qkAZOj^>wq5Z^o^Z2N!HVvABD>FNad1`zo~gRX)ETSF(en>xn$i#- zqp7e5qo~BXu&MpRXvD}Dc^X*Jd-oKE?veuS>SL|G6@4ESqZpS^J5*^t=BBR7=C?7= zY=$oE4UQhpe+5jDsps}Pj?mupYnOtkqE?~3ps6j_4`Vx&alA3=1blW8dr=q8d_1yo z)%9KCV%MX%gINCS6^fQ={Lrxgl7~mSn^8z7|og-jM4Iecgu2*F8ynf)-PtQWCY9=c?s06z*><#K{Wva^fvQ6ozpXXemKivey zi6>ysLe*qHC{JFUIUGGr#oM!hq8+y`4)1)3D7Zci&29?-*9}t8U+E^vQhu$z6;#nvwQg=7P&O77Y9=3kOXV3FX8OeRQQgDDo+r4=2^Rh`K z>SgJpB(Tg9cQnGjOb=XPFlQ7n4MhgYL+Oaa*^&C`vp4U*@mA{Qt!fI<@rj^)!~9(I z*Nu9u7Y1R_!3S;fDCZy7qBe%*M*AAb;|^G#^+aHmjxo64UB*tL0scJ5g0RK@KiEQR zU39eq7m||l??ZW&1f+3V;wTgGNh@u^?Tm|0m#>1e{`Y(45P5R1mJ4{aw~LK}*HZO} zM@lzHvCJ>L|Aa>cOTV{Xp!0&(8Gms=pXEjHZ!Ij_SoMWhc3*qC@dzv#qw#n;vw$$F zQ=ID_^Avwq-;dWKj{hMAZe?r%rgCtqqL612Bf5L>pP+ z?yhuZ)ULleSFj!A@?+c~BmRxkZbn5b30M?BN|*|n8oA^sc%+}!^u|3_`1%4hko^*X z=odSs*r%sT+XFr~4ku*h4sT!{-yn}unv?6Y&K5_>ttIS7kOKN=cFkIXwOfIPGx^2z zklz}9>WLTpRVnQje!yTq+YFXG;veU#;^dFeG&%Aj^LwA69?LA>`17NwQ)Nj-6t`Z> zu~8t^riteW0V;QE%6>shk-H2~2P~GH#L#DdXGl%uK7M`Wu$w)V7cg^Z+ID`>F~QN4 z%+ixapS+t?`d2Ae^&b+Fq%nm;sh<9M>#@j}`ngaW(g4SSrPJgoF*AIzI=*?uUtS8< z2QsuVz3@iu9#EArF}CuhjNNN}Q@MH+0k0ZPh7<4NoH;b_P@>Tg#8lL1kIVLsTIRq^ zkK6)Br!`Y*-(}4EwTFM^N1Q5HXm`=y!qBE|yA%iqLyc+lRUJIM#QMZd3H~prv}RK^ zXTr3*tLyYf(1=s;QF%%EE9_B3K6UCJOXK@^VYadnsGQi_#^06K+|}ORVO#=qUEd2g z>025Un)7xZQN$aQ;qtB(p&FJV7OnLd-?aoeJo4tb!h$0KYMQ-EnJj+*TCGeoPKhE@GbaGg3ch4#jb1_<4HYxno{#WI6c~Q{$DwHU=q)^OMU}QaQbO(+` z5+-OaQuE`QBHzCARjkwVj(Wr`$3heBAbQ$lk^6X=F2L>npA(;)O*ULW=xD=AJO*U6yrOq(Ce%b>3oP`)%2k!~VJPm9E$CF8CVl``x|! zpM>~sTSc136Yq%_W?N(K$_Nmpcc>jI{Se=LnzLwR@*{OOq1H9W=Gk3E;BvTnNrZ8i z4i)xrlcU97nJp2k)2#5d%+i9_{91cjSHv(r`7I;aLjvEq{LTfe#l6nF6R1yaG+tS^ zxR)-}#@w$q#W!_&|_ zVC6gStRPq>$bK5R9X`NMog?RoAZBBm= zz?(c4Bi^DUnvNVfsP}1Szk97SU4x>-OCMm!L7R@*A7Nw8Vpm_PD0d*k`!>ay81f2HtoXMO~K>1Y5p!u2)I zYPB?8ix(YC*@l}5SMk^?eBKNj?yf@`lYylhtdvMKMV9kwc{!L{-4LdOlaG(ItAFt2 z1117|2GO?Uy?~-x9p$f_x*?J{8l3`s!&W^0u4QUP1yQ17BvAiBplJ1%mlBb)4nMn$qkP}*4%9V z0|}7h)`>NSTxfHHs^Gh*YYm>d(7l%ZU#edef=e8-+89J5U#9WySBts1DbPF+&7&A8 zT37)pwejmFDvNC#I@G-Jxi|LiRmkT+CF&F&W1{bT6*`@%d1tf|H7dO+-o<8%1(={^e)+)Ts4GQu5~vH3u&v8 zL^4?s`AZf>;Jc;w@_mNk=_B^9btjcGsZ&|y8RHBf5%je=txB#kYuBkZP7Os2I zzUojDaPUs0NjM$(bR~!n#U9-uVlw&{XCA%4RbF+$*#bINa;1)`>)lVT^qd(|u1&6v zzFF3PH6ZIf3e#~l{R>&cEla^Whb@XrcQib$)+ZzdfOOR3cwb z>}Y_j3|>&{>5o?7)S=R-7W#=BC4GP4EpYLF?TQGYt~q7W_>;m7APwWI%xd|5OmoxC zs>mD0gAyuJu!6qgVAJ#PRpGR4?|t2l^~MBKe&xvu_I<9o$(Cz7HJcXg9cPURDY$u_ z!~;T&OMgLG0xC0H_V>JYuynay)4`$>e8PMpkGWZ2sA;qn-x5gc*?fa?ZR@3Me!|Ogv;0)EAhJ|Vr(A} zUf7)X7Ie7f7qq+0>Y@2WQq0gKPsp;+Idclgyr62u zR<7!>BYF8dJt!>R?J1!=NDo*^9K^6s2&n@L594_UXI<+r=sQU?wPm9eHIU zFnpBlu$E6v!WHW@%%{ zLW0$(zr}#*+ciphUP&HLu~Q}$&k8}b{;`aG(cumAkOH+#UAG(ovnV@WmThqGSiffZ$O9}0C=Dj1quu=Zd-H^iD?MhNI|k* zVKU;LhgIlqaVIYIr#_5**+DRsC{%4+Gp{(}ta$M-f=1V#B__Y7<^DmKJB%Ub?Fj4g zT*G-F)k#u&%IpPGS1wIst z;D*|zofUQA<`idAoEs6zWFk5hLf`WPu}H$34}2NQ=>zoaRb`m(*|aeQ>r3BrPlb5f zmlp!a*Y4qIL7cLPv$d@+ZRlovn@c^6kgH zieup_*pgE>8Tj_;X&^l|`}Z{>dXmj~+Q;3wz=n3tr!8x$iQBbeHQ!oztO1&#^oZTO z%hXx5EIm39e0w8%v^UReL?!QsW?KS}{|3*v1BWbrBu{VnlkeKUM)fu_ZktAg<<`3O z3f;9%)3bSa4(WPk`jY6N{Ox#>HZm6N1=PWXmag|F>4Dqd`k{%ss;Jg|e^-28ckRJ--nUR_0oN-QhI*Q5LD zpLb6*g}XUBt?6im*!tQgcmb1$c5OyypQu(B@|mxNbq=Xg>6w}gy_*{8Oop@2swOIA z$AM^bdjBt|9$v7CsRDUhKloE#qwsY1Ezqed)2=q#<;A!x+bAy4n)tl-wKmgZMBbxA zpZmVA^?V!3Bwuh8Xb4E!8$F3vpc9Z5cbmnMfU;JUzyV*=_3NoqUsi7PHOL0I46E!Mj=oR=| zF)Hxgadg~s5B^ST<1ulF1WMp|0dUp8XW0PQGp#+&cu#xx zi#ii}!nbKw=uvy&_TpoT&phfbYjA|JE@WOOHbWxgvQ)LZi4 z79`xE$lA`rgza0r>@;*hud(T)-jDOyC~^JsqhBPn6dL7SR!BNLgzLd0nvJ}kp1Z0d z3&6FOVO7-EF1l0cx%sAUvXU?IbL$Tdo_^`}-?Epw{2aALUt)kKf?>A%&w?XMqMph?Kj>0SjQkkb6!x6QO=Jwu3wj~juRAAf@6GcS9> ztQhQTU*E9vgOg;wpzo1dgN>ojLiUL#FK^38!4)Y>6DVBh4h8L;cB5ktH7{M-@|G0W zffA!Yyvg_tpmp&SsUckT#OFa9%(sxDLq2h|6_~Mi$o0ZDQ00!?}vmL9(@=w zv`@%Ehwb8hWl93@c~$t+oDw74-B=D9VfyN_iInDNkfhUtIyDcVr&7L&Rkjc>u5qWr zaTrdFCQ@SuK3R>n%i9J$d?S_6O3$^l!i77x@ChmS+mB-WmO39)p5-|e?JH=5%JMgj z*E35-ioHAkCQH5&@fLkRH|*p8+!{si)vC}&KD3!u!?{{)E#%sbZ!l~1cDqjO^nRn7 zg-^1nQ@N;LdXJ=k^w-yx!w4cA{a!1-or#2Z@;gp3fH3%HQD&_q zZ3!4??tX6G1e8Wm?$%j%snJsXR{O-xG_!r8K<3Zej_==B7u3HaICfNUzA^~W^S(s1 zP_xB{oPi$q4^Ep5s2iqgP6FgsY0NN8l=N4ux`7oj)_J9<}MArD7&#m{+2*^qzu5)pB0|9e!#3g4!BsnT-ij#yXqo0y4-M#dG&jLi|&d6#%pj^&1k=N^0d->$;F*`P&9vR*L)cDjDeah6QY0< z&huHxPFme9N>{k=>c_)H`&Svl*45@AoGk%hKbW<)v1m(xwuTxT1w4|k=5?wa^|OM) zn81i<^!!8`HqirTrB+WzyZc;tSofGeWbx?kF4{kOrlqK^dOa9cYgC=rw=P9|)`v%= zV?U_5mbWdLf6&a4T&%Cl(w)3FqN`GpZ-OOroq(xWR)w`-ywHW1(nl+O*nP&B->5C)C()R@|MaJpLBc(k_*9yr# zCR)3eO~vbj5lY~FBUr`h+exH}13UH2D$4ZN`d>6ak3?d})^R|MJb_w1Ce`jT4iab2 zBei@+7*cXdL)b}Nq|6KC!kW-)XE5bgo%|AV-^dnXt5(mw*I5McO=a$YHbKStr?j;Y zZE;XGgc&FL9l<4bV&2gm;9zx!ezbUvm7O4!Mm;P->&#tZ+pi!sUe&Rp7a}$hj7B^oW6ZbH>I%3Qe3$D0$B6tmpDI;S|ShKO@21`@rT#IFVWMI`9p@> z!pGK>RN^i#VX(_3wy!VxD^awOosB-piUr5m^}!pV-ky7&$wbw(Ce}<_6mPRewob=g z4bb%)89!ItNvJo4WlOvIF z8E-!pi!pi=@L4#3DS%(WSklfS?c<;!`3Hyg^owi6-tFnkMF7zo&JqlZcqV*2@yM}z z3Hl59;qdBIB>Wl}#DyhoNq(7I>pb65W-8;sYP9t>AJDXqFYJq^z*w~%N4hAit!U~o zdKvUYFy(5U14xS#ZGqM~eoZq~lSM??`m)VXvBBI^R_U{T9!$-BU9?et%ZgI#rk#Ut zy=A}?Sh>fM17b9qDSCVVkEXA1h~oR+reWzux_c2xiH{;6At1f%fYcIFib#hjv4DW2 zbVy6=!tR1JNJ~nnEYboJ(jlOPzJuTQ_Xo_(y(gaY%)QS&_l&CVu97k~Eze=2^pL|} zvKl6VYSWx?co&?isDdTJNY znqiZQw&`Eaj@~+0qF`?$GP0P-nTNlb*D`N~8#@vtF6wrr5F}Ms&d^zj2`Q^dx{b~d8sn99NOrdiXU||PgJwT@T2l&S z$!T7vtUskJxy)*pm)R3Lw#gPf_2?FG>1L#s99<+p>mRt!ZLgLh{7B)aNfI3S36bv( z?{-Xnxj{eo$JOOd|Pd{NSS!oC*mUZQy$7sWfSk^1!p?iq34QHGrSk4to8QMC~Go!{=*YL)|koZw+Ob_5@@ZuT8+@E%?+poK+`}am$mTV^X1RtF@2Uuo2U16 zqr06zN66Q9=vTOO$0kKrwnUrCl4XF&dYcY_AEFk>+p1c}w8o|)Ec$-UpfaW;e-r=( z!aB_YU+aTX!ephuv$*CBSZHn>`bdxNB~UjQAZqe_yEwUTa6Ai2W$`HB#)LdM^PJAr za@Oc9!InnbW~j03M1Lbv!0O@S7jHyzf}{MD6`P7^M~X}Cs)?myLw~k3-r(f}ba88l zC|X&xP_(8ylQ0W&s#}6I#3n9XB5v2wMH4KM0fhMUBR+h{@8^=34UZLgR3K)g%7>Km z$Dwb3z$)qJ7nvSYLw``F&VVivz*jrx?$!9<0x#w2>>r;aUw;Fbz9*@mqVHMh#IIWX zvmU{g?DKlQ47nYgaK2|-nQ0ftT8mk<^O-lPPyR|#- zTfdHWF`F)Kj51fBOUK3mYinQHMgH4(L;=DlPiMA&M=JG$Z+$r** zSnsGtrbSVIXoy(z$mnWRsKP<5%J#k0uoh^BGx6IL=2F0EZ1@i*t6`-PU zJMHnkJXyH#hcGWbk}2E|C+y!$|qyhy7HL@BIoT z{1A4n54j+3AilxD2o-Rk83AI;M}(>!oP$NP?gbOuG7b4pl8~Mt9h65=S2-L4=A9bt zb+#xT?c)?;eH#Y7Z$#=gEZ@x^l(ryGWu(tX3&rMXJeD_7r>5SysMEzbM)*Axd1UHl zBs)%L4jof;=iB*q%-123_Py%Yzf6fI+_HKeMU6_P4w$KuP-bPPfQ^oH#+2#%Mt>mP zq(0Hv%k1F=3?E2dIP4C;+4UXh^_(P)>Pk$25o-hxjDv5_qZ*fb=rysKS zf^f2*t7_eflfi|T?4|do?`)V@$xKI*u4-IanBVWqZ*hRl!0x5)gIf=V!;8F%YgPSH zuX7(qiYFVzML)aTWgO+~U?k=zQ+RhPB7OFKVA?_+r+}DP*#OQ9Gv@xSl<)nAVEYI0o3tXfv+E8hL=u4pXr=;^oY+=c`_Ah7uc;LrfrEx5u&pT@3iz^<@MvRyIDY^7$!=PctW<=dq zIGfdfGM;DS zK5gzQk9IPhp9X;MdshRU7r$hQHj$w}3H2@ZDtk)#gmUZdefZnL;@zg|ymtYLEE+tu zRhC+D3VeVI9s=)ZZ8Y9U3f~O`#(0-JUK*S#@$y2FigmePG+pm~_GjaceLnrOv-MCa zGM0}=wMbjm|5ow$pw~)^3&L?6AU8^m_L;I*j%QZ z!#Rq^50WHk*wcHL&0X!G4=cR|fy?ir*9_bd^ zKKAWj^$eOUSvP0yMLTb#y_)m*c6WeXd0JEC@1=ATC2m`ffn-8Id^Bf*bvrl?Jg(q; z>HY2uJSq&*Vw&i5T3P7C%Rjs!@OPZeH9dT@VH2Id|1P$#@A@*v7Uwysd>$o`ZJGCs zH_H=^C>E<-WI*-)Zs+f>Md5T{(3{JTABx-gTJ6uRo%c7|{rt=OZe14oD*whuY;K6) z8a(M$?OKh~ z^;R}E2`{fw)emoNf+B>=tX28mN}OR&!wh10MQoNh@4gtt-C!eke9B>-V>xd#hCE)% z9=8_yQoal$vUqRD1Mw&qZ_=^lhTQrRv0>hHYt}w9FuH#6vyH{PI6z}m-X{Ozrq4q8 zhw>ZfwOhYMB8VhDUc{R2)TEzm)U`mdI_6`2|K>*uDAeT5isOayQWGM(-e5@_@MMq5 zvSzM@^B;&a$4ilnkX5mk-j<9c z#vyFJ!!3g0GIz#4{2Px7qs2|8Dup5}8Hc=Y%qW@n0c*e(1I<>^{UtGeCw3iTIkk;V z2&q%`WtM&YCB3WzfM2tZ?r#15+)cHKJhgi0IjkT74gr2Yr^8D8-8|v1WA-AlkqCfn z!#T?JNSu*~e0j%a*PpI@)PeYD$Hk5+%tn+*{r9O%AgSLj=Jpw!-$S=O0tBA6$OP-h4H5H!Ra^vX^z}+5J|NTJt@8 zzNr)w1Jy{8Vj$tL;giK1^FQrQGXK-Ac}0cdcULgAt==n42=lxCHio1<=RA+?+rJo? z#H#o_ygV<=Ui_?uI9*rnW9Qg-Jn9)hCyzebHOVAg>6yYgzR$WWd|LP?%@JXueH5eo zpIZOhohIGhO3N>BRAoh<=*quWk)sPm<@7G_7kcznEzP44DVkT_u~XQ)Ww~}a5biFA zA25|(_h+H(aSCzm%fBnf+SQZl1(bpOVdOjmUc&s{|neJs{5Fx%xHhp=Sds@QniBKfSgoS*pO@bXgE zbr-U=#s%w6C%NC{?PUbex?0H8hETxQ#qnXfD@9DxG39_}d4ws=mPw*>q-w5_YnTE; zPO#+G&=Sqe5+Og|oc>89==?w4LcZoyNJ$DpHl8``A`@$>jAt73MU5tVhV{=@Izuek zR#S7y3>za_T2`f@-yexz_GHf}1H=L?KW|tjYK3pPy-a5q{Jo&tIJAF1VcM0RVp*w6|CQ)stLW~Z3` zO_!I@f+o>tB+=L?oKaZO_qD231>#H8uk?`Qf+jcP;|*&!Q74jN6X*?jE*WUG<3z{D zawXSy46`8tf4|mHJQeYcTp9wkgreP0(~qZ~)F=r_Hw9{aBK6^byseOK#?n`;Ja?=N zTqFO~34TqpYW_O0G*(E{H{<30o9H_zI%$wZ8b#YH>^GJya+CF6+8zDO>MVraItXUJ zO6rhEsU@1aXQi)Bok)v-&{?LlkM{h#Wa~V~`BjZZD{+2l^iWOQms_LP8+mV_ZTfTS zEB>6{sdM!?*i|HsH%4zrxH$8e-o+iq5+jVjIC&Vhnh5Eu+)Y5Fd%e))$C_tVIB$tj z26OVM{Qhp>>KD+?HYd)?G`r4$&vy(;^S;oNfetwH$?PT=qxZCy^inDfxk@Q)#`c>} z!SeNF09(bdz-buHC=1Q@#q-YJ(kzx$9+IhBV(;V_Uycezf}4{WM;sP1L#kSmtwQgj zC8u70rtg97<&gLH-YM%i();ICML`5yxr9-Poupe=bsG{d{hW5$`Dd~5HeW?Mu=?FV zN%wg{5x`uLAxPsf%Q91$oPeaMOCIcXVSV}WgTtjDkp7t5qqo&UuNKetuYjqMLRYvI zCI*POwr;R(H5~*|n<8}G(hd1;xo}GrS`+rvI(*fN)-rwl6wwrw5FEWKZqXQ`QLlT^ zfT-@@y5m0HvM&E6bLk4a0_*`O2GNa03bzg0%Z3z@pa}XteQNa-%lkFYw%bc758Y~M zDMp0)K%)$L!~N8R9s%mtWYC_Qo9R#A33L#JhYGU;Gx&WULX;@zPdIoiO!N4(o z1+j_}&wmwI-uD^w)}?Tbi(;uF;?^T-j|v<9#`ZU@i{G~eqhp6rq1w`6 zzIw?yK{RQfvPMPvGN8zbnhoh*md>}#95uE@hibY~QPN5JrZGtt0ym%6f2Au%^!*$< z!+9~ji$T7PB(?$4i7gWqC2<6ata(L%Liq*t%%*&iP4ogPL$^JRxx0ag423%{Be zmu`(ygP3*VCuEPL*I1$JVgd1~khg^e!$g}s&%p7Ko9tzxmi zHzX@0l;vOKQbofaSGC@?dsza%?)df>86}m;a}zOl6EWI$LwVD*>^M>d8C6+-IMqXw;X|4 zR%0;;i1}jjn4h~m4fR?~$xae9^kIED^G3$#Zt;wvEjdPfZKj^I-TTaO3yY0E zt}oOptO14|cioyCe(M3O`?~3*UN;UKw0xj|NC(rjpa+{4E$n;2WU9V&R|-?fmNWhB z)1Q~0ZPf7xQHD#*9`xDs3~1qpfF=#{M320h<_)5L;rAzcPM<%Q)|cal|Iydi*YJ%P^<+ff{&&La zUx6m9TALlvxmRV|wB-@=@r!h1v9f2g`n^~(S3q!6@==!POlP{5>BAiV=BZGwxpo?L z4PwbkyA*`YI^5e`!~g`l+kyyDjn+n)yEi9nb|9jgFod0iL!S?4qL{@2GgW_Q-)~O; zuge^)vS>s=6vuFvtJW#d$HVVu`D_TBRf$YqrgvPCwMy)_A%QU^M{Sf#F+B;TJk*qb9xSxs1uxqbQJDnlN1NBu-Y_X?LK%7ZE) zaE@Y~n*z5vo1<7xRF;jbKNzqpYDIU$y7<&Wj9r5Z*Oy9?7k5Xt%@~F~fINEy*^is{ z3^yokfahW)*eVrvs$;)Q@e-Xb6gAa9R#HxA!H^NO>UeRjhcREqEaElS+R#ip(0whV z!HL_&LEQL(Zm(E5aQmse2(YQ=a@2f2V@ZI$RXDH~dr{m4$WMN8D4shqHdJ zdfITLLiq(dN{>!(#0{-$6Y1n)%-U@jic5-AJ3?GJZ0cY74Su<^zuB8}W&T-0R!gQK zr~5An<-;qBsHbRlN_2>MGq%I`d_*(Y3C47R@Mym~*by6qN9kcUY2sPw>4)Di9rk!X zQ#4B*ySx=QkGkAH7LgJr%oHuGGq0SmxPr8#skx)`OKa&=?Sr{rKcn;q>`OW-1+-v< z!blDMcw8&?*%tGfFujE$o=fqi6v^2 z0ITY4N^A>Xe=68k;jw<=mJ(V>ay8ZJ&)>!TUF%1-ck~R$G*jOyy7;TKo_jq+dEKEW zNuHW_1_odHjzzo4$8SuOHnC)WyWhFFMxsQI`!2PVWzWEm+TS46?5=f(uoXC5K2t0y zPyP^pow_)~faAs9x9Dz9#*7WS8Ac{FJcgo7I$74Pw7he92~ zA9Txf75@e#oq%ULx-Qyh?f-p9dF7#X5tnXAAP+7-Qehl~UY$0CrQca05r7SPgyF*n zGDzQS><$`ZQWte6tR}@n6G(+_J~J0=aC?H4TvB+}rES~5A zoC2K7mWJz8)2p?5c$Bn<}>q8wFfTFOyG6EDur`}h&Aq=H3Rv0rwHS?~v6nJG? zxk2^{e*fi8n#Q!^sYo5qLx_diHH(yTCA zgm~OHhRWzke%7^zD>rY5zPJ>Tc!1m};|a(ON02y=;K=;`j;^_9e8t1E0_W zgCjZjtcM(Kg;v5_GvBWieur-F#`Zf1|Dowcy|o5Fv8?0Au`vd;!Jj~Av{pxXVhpJ= zhIQPeDNbQYkq^Lg|Gaj!QeE8NFY*dL_Z6DGraY4LH9Sc`PIIw=RPpA>i^2%f~2W(w4uAJ}qX8S&Uu*>qFa zt@h0vh+&I8d}&8m#J;Pc`_;alo$89BMtSBwbyqQ?g}4qIsb`*;#0|?g%7PokClmd! zQGe^@KxX|Drctn|q(xgJIU3o3R5_cUX3B|xDY|bT$K6PY+?X9Bz@)v@k`EB2iNAq1 zBSFbUXwB2qdPVG#^YTEmUp~shr3IxCrfcGy)RvfSX@D(b z#x%Y|ho^Tx6dU{DrjQ?xns0JYyhyc|fyDp9*-M8mL1^Q}0d|59nsYHE9!1l@=Rg)g zL|YO8^!)j_x6>q{(4fKTh(hq1=NM5X4>RgVIc?#6HrD#?n>^1h{_6^cQbn z%r(L$4&}R0uo9t(B+MsEDF`9L3i~D9_h}<(y9Cn5%SMSUKsCYX2?y&s0Lh)7wpo&4 z;?OJ%;*`?lBr(`w-r~eu4;X0iVn9Vm@i!pqOOVBCnpi&jdz=;~3Cg5(k`*0wIT~dR zsw7)V!WR!)uTqhh7&-~7s9;fxu2Cw)sFK91G#Cfb=F>b0^h<%Fs)#o#rwaH1-R@%M z_$Bic>ftH@YyLQ!7l`t3#XiQ-=)J$9?tksT{%aO(Y{ckPZ7zLqn~}V?-vBtxz7bhT zcTL~TpVf|*CwnlU>(cu_*GKfQD($ZuD}vYXTwSsv8q1zyRfb?WzGLbvbY?>szuNSG zTEBF}`oR6U$jBA&Q)2Poy`g_8bD#`+3llRg_=sRJl6Z~aET3lG-WIR8$xZe+VR^K2 zMEd>C2o2S~>o>1W>*ZU@_j&)Cw0UaR@po-AHv0-(bHTQ##EI#JC!^Og-aufiwfnY} z-LB)Abx$D5wlb*-W?3a2TaWaW^s!(o=5&DRrf1KRXjzwL?XY3EZ(b zzt*XT&BJa;oL|1d8YYXE+8F08tWkR>hxh;?{KKUwl35%25!*JJ zryUGweUuZJVb49Rk-Xv(zw>;fLcp|#dubG7ID!QCSr^s`KzY!h0N~akiv9z6mtFXb z#2(jde246^BK5r=A$I`}I zD1X}=xzaO(aBjganDO9H%Uefv0w_=ZI6s{Xl)hSnCzP-;;YK}x{Np8R3uFya zSm4qkBch%|u4v*=OHXWV+8tvX!zU&B{O=ipFCQlM{kmyJhuzyWG%~B z|1!GoN16`yAwhgV1#3p6vuG3Qoyn(*{|2#?E>{c;M5F>TU{I_iS!gTHJYC)5ga0RL zrbV|xgQ1lmuPGdTkGo-Q2;4VrBr?lBK^W@)*32SV-d(}iE3uv%?Bv=KQik+Z3w$mD z1yka-Ue7XnDI_%U7D8cWQ%=az`^kVTZv4N24VWORTOnn>%>n74_54JZv1ZmFUF z|9h(4avZY-@Qy|RT?EXe(VZttukI5j$E`c~f^}WEu`9!v5xuf*W(m~r4|j`V%=|!1 z(a6Bq4}zYo2F?3|9ShKBI9T?-!6s&eTg*$80e&}V6(w5;>7iJOG;bD1>(5Vjd)~2g zhdTespa73DaboKenece8VQWY{yz&9G`=f88&7R_5(~-20Cq$q`DXBk@Sz@CxC8U6zmj$+F7h z;X$&FEc$R3tIM%0#6+dn`0xKp94*5`zlI-sz0!;Jib&gaxINjL@UI-M8yg38>bN%q=*f{z4gOupj(!G?jG=yk z=M#yKXk5%sugWMLuT}W_de9C=(>|BTPcMu_wLM6Oe3-kT%X^K9|QH6LI-(qUI?!qaH);f^%R~ex5 z-Cc3`v-*$hwJQu-tEOOa!6xuc)e3nk(%04WTs6zH=yLEPa z3kUPC6^8>-RsmhyS#=Wk1r%lm5&~5!F^RErN>=HU61h!&D{#AvxH4_^O}~J983P72 z@^mQu2vRY;+;wQmj~cqdRmf5nLW-<|NN%b5DDQv1xpJ}7&842XFoB2Y+3r`rCbKKC z;VJtLBT+iE;?=)W!J`N0^#jU8q_)JNe2;`HZEp(A?*NZ0M3bff*2ObMgVY*gn5%Xl z1bm)fPyi1kqi(r88joCJ$OYBBLmZ{z=*iB+GwFa(jqcK^B=TvY(Y%Tjtq?{x+UW6ImoPLXP@o+RMZpzd{QC@%>ma z;WxHDyLNl{?HTzo-@xQAt}qs9f_VQQLY9B~Jj9^l@e6BS&lOVVZ!vg$^Rlm2X6DPSkt;?t>er~wSSM>GGO)2 zNc;6t9iB1_yJN~P;*26$Z7nmEZ(zHu*lK%C;_Y!{=XuZR01?~j*>_>!34rSC!RSH5Y*?~z+y zI_?={a`yX)S*O)g-&W)zmd9;2s-PL4NxtVusP;&x zi&rAPN1bBcy^9WcZ}$@%BsTejo>Mt}io}D*(C-~pUI375b6V^>U%uAn z)srrPy~8G%PXa|4Y!uM#wkQv(vYz~iTVcmCLEeWdgSxMnc zThffT-8ny{odN}QSb;kZ`F)ZyqhvXwWN3MEGA^xUYoV z<}?!XCo%5@YAHWbsqQib->E;v-7!)6%IKk#3&-+p=g@U4=8=lyXH;&BP=l;V-~N^; zj*&LN=#gPy(xU;+*67^#sa87Ueo zRj;g9Ah|M>(Ac@B{}`HMB+f3a70Vn8d$qIvAey zgqBmV@#?{CDnL_3%Tc(+NSDn=c3%#Asoqfd*>7ifwsQtv1ot+8r2(SETX(d3#PIKy z*XnASxjW<7I&0-wD(x6<$8`L>Tpzq${(369Td$mWYo=cXpxgZ>d(5%| zCy=xy(4PVz!8%LYh!1CR1`rdfg_BmcE}czw218~b=+Zg-SP+*n-1VCer+CdGdUS5$ zf2Pke`~=X6S`cxg4I(+?!o-Sr(C=H3WfT4d=pN?ifz8K=-+!M3?5%wNsL&EBLHRZ07(Dt{#K_ zLxwH8vU&s2M`b@PY9Y7%ZtpSr#aWf!GW#3*F`LU|GS|T%yS9bo=ug7H#gMHNq91KIm~WfUu-Yw*H)_?<_Ai__D=C zLzL#NYuUux<5^%E*}ePWes6m99vL?I)%n}1KZ4x!dFR7S6WJpt1%=jvoz84%pgNXf zKTzAaq@k(8TpzGeciR&()&izA@ZJ&_k9f&5?QQoXLg##ynw zWh01h%XrU7Llc6l=yQN%T%|NYMkQ`P4g07xVlJntf)QF;Ymz4Hn9k&`u^|}BjbV)k z?nx4?=CNxzPGGp$!JgF1q3ZX3VI75CyCg!^-3u6pCS;+mYvVY~j~f`pl;hxVKiqnx z<+vsLUy)deaO>)vD?{Xlr*}d&Sbm@`GbPrWhIgcNh=-+df&2sPcp0H*ad7WjS&wZ< zH#ORYN&|dp|IKNS5`FN@K{$DgKs1uG`b3z!0Vd|0l(EM8?#KI_U?IE_(p!ZM2$5;J zHlU|cRg1*reGFk<$nl~8nW~t5z7$T;!5NL|Ov8T4@4h#OT(d!IXyW3sRa96qKj#mk z;b6aK2g~zOOXA5tzx7V@>p5h=q8sKcD7km(Pa*L$m-&^J+P0^|bok&yDT=CYUmmP> z5i7IG-r%_DyMkkiLHEAIV8uf4q-7f&F@B%O9`^?(%=@LvB`J7k_xjexmubI_n+u!K*VeHu|A2G^>#sr=$DdOPv=K~AElp} ztZq@nrkneOu+CbsFT!jlSMwSI%>UfJ0`3YSgQ;V#x0tN2Ib2;OumK-wownWAaQjPQ zi?iP;qpOp}@TgeJnX-kDwIVs~Qq^vFBZeRJ04fZ;Q_%S*PAH(KL-X_Y^ zca92TXgEgXk>w5g{xki7R&Qt8acz<1voz6tfw7RGHs*A%Y&Vjti&~zAw2z zyO~BQ`p(~5$Anv-u2aCLwyU&JRjPgp<^AE`8} zr{lf9-^%~+Sfo7OZ5`Khwf&kdUUIM6fK5pGY54lBVR(luPI1FbV_1~~oNQTVYM{a* z|M4~U+9Bh6yng9O*D~ym-_1VQG_wWA%X&ih>vY&O0eatWQ*O)O(;Ya>^V0`yV8^NM zs951pO%N^Zj2Va8a&G#;c-=h~9vox=vQ2q5=nu}_gUUj4DWoGMg($yhXL++&3WW@Z z#{;yd6%p5G|JC{Q&QF2$?6td;TVyEqqbdQ;NDi5m!xp3dqw~TnjZ4&y2W`|OnjFrP z9oZ2z7w6kQS(Kp?cN=W>2Syn<^%Yc*f?!mXutj?9MehwYL^i!ndt*NEjtE-YIinp3 zfkoZa#DBVj7u!|UjB^%D82`_#pm}siYri!<5&h<0pm7p=1Swz?9VV*_qS|Rc{f1Q4 zy&1UY$yIdc=`JxFaL)F5@{YUU)+ewf!Ohz&RhYQ>{#t|m>`dez<*R5rB^+Lxd=&zW z8@BMr=T(aSF&WcC2)@U>oE`w&XaoAY_t z+qXV9k_7?r6>3i-y*~LKPzPT3&3x=rxMvsE(I9-x^#Vf-?%-Vel_&?QD_L zSI27^v^yNB8#C5sotb9k&Nn1Jl)&|ZH<1L$UxAsP1|8;G1-%dDOr3kWpl;cbav#F~ zNq*H6Ni<1&37v%$oqIio-eIubYB<^3EjQufHqR{9ot50t@T&U5Iu8%r$>vYU3)bR< zS;c8WIQQ~xPE&XI5dy8sZh_{Js&!{vKr=rhFRy%3C8Zr(eY&)x2>9KSFoi#q7{$-3 zIc&=uxMJ2eD@NAei*1cRJX^5ToOs8(6qUjULqF|UOqp1Js4h3d55?oCkLHElNM#h8 z`mZYW9yNM3Gf-i#iV_~lSc@ckHhgDuuXz|TNc;mKTua{0f}|qJtiZ4qNm$={Fc*+& zi5oa9A9|R$@((Lc2~aWc!XsOk=<8{DeQlPiwQ=4#$vo$q0WE?WPotuaSv2vkb2)tH zkwj%*3HTqdqM=%sPu*5g|M)O9&?9}|{stH7TT@D7r~EE>q~y|#W+0bz7UpY^6&;=t zi3AeZIo`fK>`WU@5t6_^XQ(}~M;$L*4Z0)YX;~EV4-2a)|XL(ch? z&3Q@D2d9C;dn||Cnp!SFc>wu}v&CjhH)Z@wiC;%Us6O4hwluINrnsT%N^x}7cwBuwpFN+MH60r77N+nmVh+gUb&^WFkmN58 zhasK!2B%LUKXwyU#O1+Ni2ree__i4hT?EEP(J{7cNgdP5IECHY!j>>q#P)^2Us|fQ zwamRRE=%vbJ0=$ywdI!Oue&h_G?-rO=TdF#P}~z{yNxBdm4qCJL(#y?TKf z4Vo!X0sgl3p=d3dz;w$r)Xf4J+sb~z1@=U5YO9et&!-5E|uPGuPr+Fqp1^ z(ESR(;+O3=u^;TffT{CuHABE;K8?MOU3EjGe6FMmuitOaBX1+ImFm7!cc$sDp@^Sa z=tR>vEZU$TG2nN>|I@?swj7tLs0+nh##a>Ct4qqvwUD5BJN-LWnYmF=@J}9}qXHo$-eMr$K ze75|r*=Joocp0y;?q|f>F(;;GXZE1aJ-Y{OrcdgJ>EbQqFxpNj2zhsCecH!@WAkg< zUisExqdD2*;j4ZS`^RIF8#!%5&FX@Z&jW+DJQacE*c-QLXpiXHS~-+38Q0^}n20Ha z-pxWygP{*{STb0Ly+61QJqRGrfUkCPBrdaf-QS(E-Vhpy(;(lZF8S^!fp!aF$)EeY zGdbr#(}KxSGd{_B>FtJX{;r`wPbT+^Dgil62TuEU_#{^!O87#5lk-er4Un*Oxj0*b zJ+s%VMrBz^8&V#O za*rWtp_uptkZ`g1nV$!A!Fn*z zRW>U<;hE@{l-R8tlP|&!tOs%eV5rPg|QyxyCW<^;qoIRphvnG`wA&%LZ}aNy#}v8&5QC3)si@nNjq)y5{|dSxPs5kCE}!6+R%%buvWD z-)HgA;RbBkeqe9+NwPXh3sS)!<9eSgTcLlILjYKkad$1%t1w#S{%0?&j0Kp)dLjxW2dPth(K300^yFAM#M9b4aRP*}IC|;sGHhm>mWv8W_?- z%6#SL5ebWNmMvY3oX{e7?0@Hf!X^K+Md$r>t>U~d+exX3u-)T1{Fh|MogD7p(qH|x zLL4kqw&opVP9kKCaY@_$$r$Xr9K;*yaPdQqTaok5KV~&bpBVi$4;JM5t60i5{(s)O z&(gN6aq4Z#?~|_n;Ph^ca1QEsPhGeac;}|mz^8ttj%@OHe!LPZv>i#C7F#}EWEWK5 z3ZmO~r%D7|1yMgXD;!``<$l9OLl00O+WPGz0R8cOL8Oc6hVODfTk2_)L`p<5OG2%P zFq73;O|;Q{Af^QkUV|fvHCxwne8J zBjVRMAM-NSf`R1VTmS*Ry-tibX-h?&)(?>dm22uvhuJ|u)r^ytXn=2~TZz z$#AJ|pm)};ZmK2K#k6)E#^6-5M!A_cT&^Qx_tsW5zB%6-QPbO6SHX|4j7za~cpU znfw61{VJn&+!-5w_e5&x;;vF?{HUHHAZ3(4ur~4JC3Mq^@>;mvFEqW@Mp(*?uPs-_ zEhr2|g=s?iUM|~+;nJ!IA-Ff*JdfT~by;b5Cma`RvBiuy(^5Dp6#Hs0I||!a92#DQ z4+w5Efl(K@U{sHU7!JC$VNei-;xm%ivk4^)#LhkZ0Nq=>7n@U&5ZI z{y?w!<9A+4xEQYKk^Dq+e@j_Y%SV~9+#j?0DyBmxa5qoVMN3kr)qW9;i+`VC$L7og zrXyci4(K%k$pHUE3%LS~VpA7~FwnDbRJgNzM|k~IBRW*~iBD?46$DI&!8kEzAZAvZ zCcV{%`Uo*I@h{ll?=&5mc@0kiNOoRB-p*-CcO*9{eq|d6jrKYLIeFl|i3`q$H#zJi z&T+_Qj(uQS8bLk1#+9P5eQ-Og-q?2OY{BnszTHddC_N4Le*is70Bt!9O&KjFoy%`P`%i1VOe_LHF?!e@l=voc8l|$U(*k(cplxeA=b7i zKlI_jf68A}8>Z^sGYVYdVWBa5G5O-9-|HK?=57|N4g(5-prYk-2@)JP$-V?jC*Y4U=ovu6)X3=f#2hM)vcp}F(Z=fk%ZLz zc|K>H1$X?h!Zn8glVjR(IwJbs^6lgUC^jH7iq1D|m73X!bVn}oUCEW@2|Q>p-}3_1 z?2q5TliZy(Gp(5((n7-TYnsdN)5gmhWw$a6H;D+**Ub>>K+{n}{oQ1@|6gz49?s+& z|Nn_7rzsglNJ4B1g%BzUEtG6~$YCQ1p=b`nq{jLnIv|t~D)wM#$$2J}bIJ%K=fgq{ za}2*npTEC<{I2VJeRo~Ew(EKB`+eW<*ZXiEp7(RV|2k`aR#S!Ey=#rF1yCAdne%7O zzPJAlIG0&t=MV70M!ab95M8D-BU-AvRa;^ zuhNdVQ208Pot!XbD$t|*815X$ov}K--GnMXYrVow!hW4o=-U%{OhLv(0bk^&BRTU8 zij;973@Am%9HNcJ<;q~` z-IK6K-OkNu3`5=@4sho@3`znQv1y8leP!4DXDoVfp?T$Ur(q5fk!$6ss>ePCrsm1#^KhD2vS@#~}@$A-1b{R%XjCwsY35JjKhpxfC{2i7wah+|{ z3KGI)+zn+)ptyR`R(Lp`aYr(y2ZTEHqBRXL9otl)DV<7SLT8g3vcEL&dzE7I>Af)h zC2X%Ibq{Lqtl)#>ood_D#@s!C@=$`Va?h(T&}yL}4~qP-N)iGMo=GjA!FU+=><9?R zk#%Kw90VMpEy{|Dp}xiz@}(#~0Vema7;937%~X-32sH5TE){*X5rb`mxQKIz#-5v| z*>e`>-dy%#C*OJ>ej!u8Z#X2z)-&^NhkfVu?dfgj4v;!GC0neL7*JdKMXFjqB)PV^ z&*y03<{rn&CggrcIm-FU2Iq6&=quW`M)878b5V34uC6}A?Gy2#tdJ$S=Tqo@*hpt^ zbK@(?-0;i4*v}=JAcGO59s5kZvtXzVF#VMLfoQXT5$xeBtrj9=C)1V(FVs=4S1*&0 zF}m$F5KcY!24R?}&a`n3 z3D{v-M9U6Nq9W@%j<8T`Sbd}-T%0Kt$mK8AD_^up%4c7L!~|!QAq(CZrCk9yHagb`F0%_vhZAm2YJr5vyzi|Fs+i_%~Iy})k% zSQctOP^DZPL+%X3CQY%WwYNPQjGvVqREM2yo=t5bFR%>W{i~iO2=)YWQ8(nxB_=a2 zPq2S%t1}0{IizBj-+2LVnBI<+0r#=P3WMEF8=f?8ui)Y1H)AGtiCkbzZXtnpSv1w?3g z(68wsrzzZ!RuPtHem&pzE0L~!#8e=?fd@mnwJl{CVX(ReF{%B{>UczIdu$^Nak}@8 z?E7`9U@6`Fee6S(5W(r2(E3hK90-fRejBQc^8{Bg@*bv0v5@1`@sH zRw&T)ds*CJ@Q^gwHN=M$Q$|btp`b)Yv@R9U44AtAd?a9@~kqbYzEw4{2&r zhh^Z{7fj=5MOqSq#$q|i5W!e^j9(;yjoUbW&ap-3r_`Kl>e52thdj6D-N+k)U?&%w z=ceF;A+O&8({+$@H~V*h)k+$}Ig!zmPpj55Weu922ulfO z*2n&)>fRoBWAs|_I*a>UsI4G>i!CmQydealm#__<5n(!crmJ3UUGt(Rmg5G3FBFfU z^H*8voQQ^9Gbio9Z?7)no*2+rJx*FPK}Lp>oW&0X3$e5xIInTt3R_LiSkv8B9qo|a z$oX9?;yQJ~VD?dP;fHazvb0wZxQ1696I0)|kVr9j!qrPv$ZR!~9vji}o>oCy(|inNC!Rsco;(+tTs_5eBYC~ zyg`7#e8qT*)ggkl3O68AT!L@uE1R%<;^@_ukRlv5g^2eWu%O_t_2&lF&W<9V^V*u) z^;-Ky|K)B%J>LV&PmN7%!~hf(XhbDbK*^>g52mNe9&ZxP2Tq)XH31McI>UkIFkYL*J=d z^Fxp&00Wf|D$0O_w&>_@eE*7CBeG1-Ej=fBv^Wf$4%=?-Hu<=;K$M$2daN{99O9XM!;? z#rQt@1GfB4qr5?re9hE?$6orx<9n7b*x8SaiIK)oR3kQf|Aoyr&OwyLB_>7Or*bgp zJ2o2-rIIad(emF1s^8i-%B$T@IIJD~wM+Vy$(5OWE|ToR7T#DElkMQas=PYS5wXge zI$HONcl7Z)vZ<_fx51tpw%O!f>+lGM7{DX6GZFex|4N>`l*=O(WTwLFx%UJzoKL2! z+TY-Kk}@Lw4kccbrUX7^+>du{FQ+|&w|_LgCq&W;wi`OUki=$UrvFntlHQe5b_GL*ab8ksJ{oMBF0=&#UZv zg5i7*ekx6=bPfBB>ibQ0b*zIF210~GY)6C|4?81W2!BQ!rceFxF(wTF%8h@k`}XY zK34t*A8L|RT$@Xj93kp~?L-;i%?ya~#NCuG*<^22OcUHFLHwNN3eStn_z zwPeG*?~^<8fj1NLcLz=_DdJFl1ZPzFq5gs>e;U#9$2o*y^Fv<9(Wvy)AN2dx!s2Doqz37Ph$y1a(Pw@UwD4-%YX7D1!l7=hf=Wc;Pabes1K$ zT+Y>^U;6;Ul68#M<*)cr2Z+((j)B#V_SMiUgHwy5KTsMh>cU?lhkNWL8z(QRC5i)R zFgyx|m?aut;x9d2hs88{q5 z9Kf4LfX-9H)M(DXgiUKCrNZVcMN60FoTu~2roY08c8d>F4i`ER@2vInNwpsMB^>Tq zDenit-x2YUHRsX|5OZ8}su3u)N^Gl2W7)fO0z6eJjKSYZ< zI~)ELZ!tp~0(H(4_MSH>zS&rxBU>NRQLk3la*V0;AJDncKWrBABkK=b`_Q(Te6cW! zX8spGvjYH{Q~ZAJi(TB01D}r75!YJI@@ykzGYTyg%XtyaXz0)ci;FP~;x!a5K|2Cz z5&TZ7EKO6Y-ygLb%-6jwPkYLTA#)=?_5@d!5q#5lsr$Jq(v{*m&vaDGd7zMbMo+hR zk$T=U?sWYN-znb%;aZ`vgNn2%`p(2s{b(3C#G+O3V|6M5qE-w(;Yvf+sk)b ztfRQ;78jo~1Vu@xIVc?PcBOn?{t$IQ;vg`3&|;w0VDO&A$QPcbzEn2>aH`e+PL;K= zvDjgAoPkFn6T!!#aUhobmz{%6u-CDago27udObO40i{V-~AzJfLTHb)Nf&-{R zsrGMKlQda6&WagN|0Z}5@ zB?#`|i2UmY1UM$!+C=yLsvxw6bDG1sp?h==Q}d&`Qt%>xk69|9uoXLdX7=Nd+{Gq- zq_u6tnht%c42Gr`9{K+p`Waij<>fOrL2AP3mA;FrP8(ZWgpzWPPJZF|LlZBtZZAv9 zAG>dqRdUUHK?Z2zU$Sa%gt>X=D0{SV&4e2$+IWk-e zDS)aa5-4#R?!TD9qrrd}YksXI!=)W(kdCL@TOASbT<#)&!MOwLkL%aarG94%r%s$^ zi(xM5R^GDZrqosoV>skn{U|I;K@8LV{nzqMKqD+Pz3)S2qen!fmg~6>Y|w_aCg6G1 z^_wz2V#)3b#`6gH8<`0f5kG{>od4Zjh}RYM2JzEI_#7}_z;o9FMZ7s6mPl5Fu5S`< zye4NC9YG3s%E84wTn^)Npbj&89808Dj$UF~+d}2&37(92>(&fmqXK1!gqs;KS>A~H zyj-$B&gfc|+^vuS8)iz8!a%v^V1uOeSkbRB%kr1SwqZz32$&D1xvNB;87FO7f06@~Kk_Q*{lq&p+!!o-0Sa z@6YHQF_@Aqg-Ke3b$zc%IU(;SjajLz9F&O9eWH2r!)04BOj{&3c&uri9BJKwkz!@Z zlv+j^OsNn28O<2n;7%H3korhY3K0jlP6G%Xu)XTZTCAw$ATdD^Pq+nS>pj7%J9S>X zo)<~jf1KKrAw9!RODghi`$LASp1qkIiJr!~nBSSgwIn|-6`|UEb52P_qKJ20%>e^x z)*XHMw}C6A+Lr=gaZj&CsZdyO9|?CLAhyQ0|H-0XfCAf7Str;!(Q)X+L8^XQ!|I+$ z$-bmMTE5cA?f$qwAHHvs{UL(r+RWATC&xebBX0efgAyZ(=0jqf`BIsU4;|0Et#?V( z_5IJ@$Q&^8wOW4GIQ}YR%V;*EEqS3kwZmjlKm@TU(IFgQ(<7zL`sp;*%Yr3*(QclZ-VOVPg95KZ5S;jAF2X76 zYchLeGlV@Iw#1zv6N(iYs;My|?da;LLR91_%ev8z5s57)Tk!AT<_Gg$PClS}Fw539 zW?g%1*W(Li0Z4$$WHgLk{QYy9_>^f&?LJmZY)oiia&E)o9Pt#PJC1tC(*v;Jy?jCG z2vQ61R07qNH7$g{I3Q5WsaG^>LaPgBwW8~fg^_6gNX)5u16$teidv1;h2Q9)7vE`i zW9lp`5BFcv%r~N}DCm$lg}4>%xs>12nYxMox!`jvU{cgH_-bwAbg#@QcBPL3esnGE zMWpDWjod;9-YEZrM9LC~|EGgNnRlT~kJQ+mz>n7ZjeK`1wu2BTc6|8IKnioP%#6({ zC#$(8Gzfe2U3ls39 zU9X)_09HYRQ7iZG9iIc7s(%DCFYfbk<`dv_im`s)5+>~|t|n{UJtJo|fs&0M-&Liw zWh~%1qD_V@pPl+BW0x$f90dfNyAF_yZk()H3I=jypQg#<9~rtrsTKg2VvYCQk0s;; z*>;*zwSU)13b!@BLA$h=Oen$EZ6TZ!1ZGb_`n(V0W#c@2rO3Lr$AKxe(b!-pLR~y? zZ|%4od*rGjm{hxnK5_(nCE$q(ziSRqGBSe#mTB`*B8XNIFN-!j+UTQ9*hrw8fahY) zi2WEcSOK4xN|W^yMHpBx8#&5gyW2mO1^XRlt|K#>EPo;Kk4}O$35;!z1>#KvsK&J( zFKQP#Ugb7+l3)9bnNhXRON>x55idLBW0wpBI9xGIJWcyKjRfm`>K7%+Ax4xKif}Xp zs2w!esI$oEf=>Nf{AFg@s_1$(1^z9HuZt_o+KPq0;r6+$#?z`olDFaV0)jWQep zyrfNqYgBVRfNQJq>Y*_qXH+4m6zI@7Kfs+9MFrz=GI|o5>9kqgM21K5U_Ab4iLr!~ z!117t%3qMfhcV{7UPl+~dlVX5LpcJT+T=#Lw#upCE6tcv#2dk3vts$niuLSSe0C1H zsGw7dKTSp2%n@?Jgy88X%V`j9s0;p<0Dj7E(W*$6)$m7gzarRW2HcrA3$od@YrXT| zddnzUt0l9XWHS(2N%Jn5pFe9O%lb(n%Ryv1{W;@ZYyLq2BA!!H|MXe_lcmzE-@bqp zbSscEKIZhYT6pcwGU?58w4<6cp^Bol@m_m*e@swBB;2lG_by9awS}G%$o+~af;Q`~ z8^2Em3;3;H+&a;iE1Wk~6$tmQq7X<=2P*@ZDeCgY(wGgc8Ef0Lte!^SZ&blh$o6_H znRfWPTPUuZF>l2h6}Ecb%Q&qPa2yieNR&mI)>3 zp@p8?Y^NQ2MKn;8y;{GtJ|6x0xTMdi(=>ixaQbBY z)<}6JF=6uDPerPd<@2)N+B|DfCJ&R8kB4Z6!b~^6ba3z%Nsp^ff-YX!Y4rSh)SPEt z+R6 zOTyVxg*0>KoBLZI$R+f;0|}YK3)dyePJ%$C(LPB8l~@;s3`i)pWJWnjp3FWO0IoLR z*2&G@K!W1=T5|MYn_bB9<>jn^|7ut3v9K#M-)OnzHXZj}(FY2Pej1D>h_Sd*ThAp+ zI`cLr_uqQ?cok|`;LddhY2(+HpM`hSm(*>UT&gW|G+SR^X-$>F#OOpDM3_{Us&3k$ zRI{q4KU}RXlc6t=5+dSiSOUodG;fx>T-xJ6Zt+_`izZX$R7A9w?lFC2>b-w=sI^(I>@4FXO5_$ecK$-Ke06&G0>QQd>tT$xXz`#mQNb^hv}`9 z_0;vvOM{_0(T4i!E3P#+tbY@IP)i;F2)OEtw!j%yp#m3CQ@rD3k~IEwd`+=5(P*tAvi@u>kM`j$yE{Yrj_i#!HxVJAca`w@v7g?jk^&Q0 zEb1n5^;KFVA5yZAKQYxzzh|F_kYkz4bgq*-@LNpA6DOZjRP5qZdmnrI9UY{VD?opr z{|;k^e-k5R+pF2rN+J_3DOa5DDq9%{wvYVD@oT-lgAb={fGy$qALf|<_m}To*!l+x W Date: Wed, 20 Mar 2019 00:07:49 +0800 Subject: [PATCH 08/36] Update logo Signed-off-by: Tran Ly Vu --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9317e03..c60aa7b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

@@ -11,7 +11,7 @@

--- -wikilink is a A web-scraping application to find the minimum number of links between 2 given wiki pages. +wikilink is a web-scraping application which purpose is to find the minimum number of links between 2 given wiki pages; subsequently to examine if “Six degrees of separation” idea can be observed, i.e. the pages can be reached within 6 links. | Build | [![Build Status][3]][4] | [![Coverage Status][5]][6] | | :--- | :--- | :--- | From 99a595b5adb9cfc1494933fbb68256fb696d4111 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:09:10 +0800 Subject: [PATCH 09/36] Update logo Signed-off-by: Tran Ly Vu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c60aa7b..59a7f57 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

From 00087393055698a436d9b850af9a7a07c81eb331 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:10:39 +0800 Subject: [PATCH 10/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59a7f57..9b1ec13 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

From 64a31f6c6199e963103031b18488871d6c4e93fb Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:10:54 +0800 Subject: [PATCH 11/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b1ec13..880fda4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

From 3b4a868c953656ff11f722b0a4116dcc3479a331 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 00:11:11 +0800 Subject: [PATCH 12/36] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 880fda4..9aba1f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- +

From 1954437f01fab591febd395365e75e3fae0f4ed8 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 15:04:38 +0800 Subject: [PATCH 13/36] Update copyright year --- wikilink/db/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wikilink/db/__init__.py b/wikilink/db/__init__.py index 71a8e8f..53f03dd 100644 --- a/wikilink/db/__init__.py +++ b/wikilink/db/__init__.py @@ -5,7 +5,7 @@ wiki-link is a web-scraping application to find minimum number of links between two given wiki pages. - :copyright: (c) 2016 - 2018 by Tran Ly VU. All Rights Reserved. + :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. :license: Apache License 2.0. """ __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" From 7f9ae684cfed6570cb259bd0a25ac3c164734ef2 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Wed, 20 Mar 2019 15:04:58 +0800 Subject: [PATCH 14/36] Update copyright year --- wikilink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wikilink/__init__.py b/wikilink/__init__.py index f1859de..39bfaef 100644 --- a/wikilink/__init__.py +++ b/wikilink/__init__.py @@ -5,7 +5,7 @@ wiki-link is a web-scraping application to find minimum number of links between two given wiki pages. - :copyright: (c) 2016 - 2018 by Tran Ly VU. All Rights Reserved. + :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. :license: Apache License 2.0. """ __all__ = ["wiki_link"] From 90bd0de349de14ac5a3ea7f94cdcbbc78682d0de Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:20:30 +0800 Subject: [PATCH 15/36] Add docstring Signed-off-by: tranlyvu --- wikilink/__init__.py | 7 +++++-- wikilink/db/__init__.py | 6 +++++- wikilink/db/base.py | 14 ++++++++++++++ wikilink/db/connection.py | 17 +++++++++++++++++ wikilink/db/link.py | 17 +++++++++++++++++ wikilink/db/page.py | 18 ++++++++++++++++++ wikilink/wiki_link.py | 40 +++++++++++++++++++++++++++++---------- 7 files changed, 106 insertions(+), 13 deletions(-) diff --git a/wikilink/__init__.py b/wikilink/__init__.py index 39bfaef..600e95a 100644 --- a/wikilink/__init__.py +++ b/wikilink/__init__.py @@ -10,7 +10,10 @@ """ __all__ = ["wiki_link"] __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" -__version__ = "1.2.0" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" - +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" diff --git a/wikilink/db/__init__.py b/wikilink/db/__init__.py index 53f03dd..fafd046 100644 --- a/wikilink/db/__init__.py +++ b/wikilink/db/__init__.py @@ -9,7 +9,11 @@ :license: Apache License 2.0. """ __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" -__version__ = "1.2.0" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" diff --git a/wikilink/db/base.py b/wikilink/db/base.py index ec3d0be..dd3a032 100644 --- a/wikilink/db/base.py +++ b/wikilink/db/base.py @@ -1,2 +1,16 @@ +# -*- coding: utf-8 -*- + +""" Provide Base- the base object for db access""" + from sqlalchemy.ext.declarative import declarative_base + +__author__ = "Tran Ly Vu (vutransingapore@gmail.com)" +__copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] +__license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" + Base = declarative_base() \ No newline at end of file diff --git a/wikilink/db/connection.py b/wikilink/db/connection.py index fa136aa..4c81eaf 100644 --- a/wikilink/db/connection.py +++ b/wikilink/db/connection.py @@ -1,8 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Provide Connection - the main gateway to access db""" + +#third-party modules from sqlalchemy import create_engine from sqlalchemy_utils import functions from sqlalchemy.orm import sessionmaker + +#own modules from .base import Base +__author__ = "Tran Ly Vu (vutransingapore@gmail.com)" +__copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] +__license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" + class Connection: def __init__(self, db, name, password, ip, port): if db == "postgresql": diff --git a/wikilink/db/link.py b/wikilink/db/link.py index f3323dd..3915eba 100644 --- a/wikilink/db/link.py +++ b/wikilink/db/link.py @@ -1,6 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Provide Link - the main calss to initialize link table""" + +# Third party modules from sqlalchemy import Column, Integer, String, DateTime, text, ForeignKey + +# own modules from .base import Base +__author__ = "Tran Ly Vu (vutransingapore@gmail.com)" +__copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] +__license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" + class Link(Base): """Link table""" __tablename__ = 'link' diff --git a/wikilink/db/page.py b/wikilink/db/page.py index 2552bb1..938fec6 100644 --- a/wikilink/db/page.py +++ b/wikilink/db/page.py @@ -1,7 +1,25 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Initialize Page table""" + +# third party modules from sqlalchemy import Column, Integer, String, DateTime, text, ForeignKey from sqlalchemy.dialects.mysql import LONGTEXT + +# own modules from .base import Base +__author__ = "Tran Ly Vu (vutransingapore@gmail.com)" +__copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] +__license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" + + class Page(Base): """Page table""" diff --git a/wikilink/wiki_link.py b/wikilink/wiki_link.py index 45270d7..dc71be0 100644 --- a/wikilink/wiki_link.py +++ b/wikilink/wiki_link.py @@ -1,10 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Provide Wikilink- Main class of the project""" + +# built-in modules from re import compile + +#third-party modules from requests import get, HTTPError from bs4 import BeautifulSoup + +# own modules from wikilink.db.connection import Connection from wikilink.db.page import Page from wikilink.db.link import Link +__author__ = "Tran Ly Vu (vutransingapore@gmail.com)" +__copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] +__license__ = "Apache License 2.0" +__version__ = "1.2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" + + class WikiLink: def __init__(self): pass @@ -38,8 +58,8 @@ def min_link(self, source_url, dest_url, limit=6): """ # update page for both starting and ending url - source_id = self.insert_url(source_url.split("/wiki/")[-1]) - dest_id = self.insert_url(dest_url.split("/wiki/")[-1]) + source_id = self._insert_url(source_url.split("/wiki/")[-1]) + dest_id = self._insert_url(dest_url.split("/wiki/")[-1]) separation = self.db.session.query(Link.number_of_separation).filter(Link.from_page_id == source_id, \ Link.to_page_id == dest_id).all() @@ -57,13 +77,13 @@ def min_link(self, source_url, dest_url, limit=6): queue = [] # find outbound links from current url for url_id in temporary_queue: - self.update_url(url_id) + self._update_url(url_id) neighbors = self.db.session.query(Link).filter(Link.from_page_id == url_id, \ Link.number_of_separation == 1).all() for n in neighbors: if n.to_page_id == dest_id: - self.insert_link(source_id, dest_id, number_of_separation) + self._insert_link(source_id, dest_id, number_of_separation) return number_of_separation if n.to_page_id not in already_seen: @@ -77,7 +97,7 @@ def min_link(self, source_url, dest_url, limit=6): print("there is no path from {} to {}".format(starting_url, ending_url)) - def update_url(self, url_id): + def _update_url(self, url_id): """ Scrap urls from given url id and insert into database @@ -115,13 +135,13 @@ def update_url(self, url_id): for link in links: # only insert link starting with /wiki/ and update Page if not exist inserted_url = link.attrs['href'].split("/wiki/")[-1] - inserted_id = self.insert_url(inserted_url) + inserted_id = self._insert_url(inserted_url) # update links table with starting page if it not exists - self.insert_link(url_id, inserted_id, 1) + self._insert_link(url_id, inserted_id, 1) - def insert_url(self, url): + def _insert_url(self, url): """ insert into table Page if not exist and return the url id Args: @@ -137,12 +157,12 @@ def insert_url(self, url): self.db.session.add(page) self.db.session.commit() url_id = self.db.session.query(Page.id).filter(Page.url == url).all()[0][0] - self.insert_link(url_id, url_id, 0) + self._insert_link(url_id, url_id, 0) return url_id else: return self.db.session.query(Page.id).filter(Page.url == url).all()[0][0] - def insert_link(self, from_page_id, to_page_id, no_of_separation): + def _insert_link(self, from_page_id, to_page_id, no_of_separation): """ insert link into database if link is not existed From c2e406d0e2b71ccc919e2a54deeaa006623aaff7 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:30:50 +0800 Subject: [PATCH 16/36] Update docstring Signed-off-by: tranlyvu --- wikilink/db/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wikilink/db/page.py b/wikilink/db/page.py index 938fec6..89418f6 100644 --- a/wikilink/db/page.py +++ b/wikilink/db/page.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""Initialize Page table""" +"""Provide Page - the main calss to initialize page table""" # third party modules from sqlalchemy import Column, Integer, String, DateTime, text, ForeignKey From 7dedfc6a8839b41883c7b7769b61049dcc0cfea1 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:31:12 +0800 Subject: [PATCH 17/36] Update syntax Signed-off-by: tranlyvu --- CODE-OF-CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index 43490d3..c5392cc 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -1,4 +1,4 @@ -# Contributor Covenant Code of Conduct +# **Contributor Covenant Code of Conduct** ## Our Pledge From f40cc2b60d1567e78e5cd679c91890d4cc8db337 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:31:20 +0800 Subject: [PATCH 18/36] Add change log Signed-off-by: tranlyvu --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 38 ++++++++------------------------------ 2 files changed, 24 insertions(+), 30 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d2c27ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# **Change log** + +* v1.2.1 + * Add support for python3.7 + * Fix pypi shipping + +* v1.2.0 - Jan 23, 2019 + * Re-define API + * Publish to PyPi + +* v1.0.1 - Jan 14, 2018 + * Fix database connection bug + * Test PostgreSQL database + +* v1.0.0 - Nov 7, 2016 + * First official release \ No newline at end of file diff --git a/README.md b/README.md index 9aba1f5..c22643d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ --- wikilink is a web-scraping application which purpose is to find the minimum number of links between 2 given wiki pages; subsequently to examine if “Six degrees of separation” idea can be observed, i.e. the pages can be reached within 6 links. +I discussed brief the motivation and an overview of the project in [my blog](https://tranlyvu.github.io/algorithms/BFS-and-a-simple-application/). + +The project is currently at production stage - [v1.2.1](https://github.com/tranlyvu/wiki-link/releases), also see [change log](https://github.com/tranlyvu/wiki-link/blob/dev/CHANGELOG.md) for more details on release history. + + | Build | [![Build Status][3]][4] | [![Coverage Status][5]][6] | | :--- | :--- | :--- | | **Quality** | [![Maintainability][13]][14] | [![Requirements Status][19]][20] | @@ -41,16 +46,14 @@ Table of contents 1. [Usage](#Usage) 2. [Contribution](#Contribution) -3. [Project Architecture](#Project-Architecture) -4. [Release History](#Release-History) -5. [Contact](#Contact) -6. [License](#License) +3. [Contact](#Contact) +4. [License](#License) --- Usage --- -Download a [release](https://github.com/tranlyvu/wiki-link/releases) or install with pip +Install with pip ``` $ pip install wikilink @@ -104,31 +107,6 @@ Feel free to add your name into the [list of contributors](https://github.com/tr [![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/0)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/0)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/1)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/1)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/2)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/2)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/3)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/3)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/4)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/4)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/5)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/5)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/6)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/6)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/7)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/7) ---- -Project Architecture ---- - -An overview of the project can be found [here](https://tranlyvu.github.io/algorithms/BFS-and-a-simple-application/). - ---- -Release History ---- - -* v1.2.1 - * Add support for python3.7 - * Fix pypi shipping - -* v1.2.0 - Jan 23, 2019 - * Re-define API - * Publish to PyPi - -* v1.0.1 - Jan 14, 2018 - * Fix database connection bug - * Test PostgreSQL database - -* v1.0.0 - Nov 7, 2016 - * First official release - --- Contact --- From 97d49b7c70ae1a8b31b9cc676b8756f0c8016b7f Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:32:15 +0800 Subject: [PATCH 19/36] Update requirement Signed-off-by: tranlyvu --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fe08c55..abfbc96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,5 +12,5 @@ requests==2.21.0 six==1.12.0 # via sqlalchemy-utils soupsieve==1.8 # via beautifulsoup4 sqlalchemy-utils==0.33.11 -sqlalchemy==1.3.1 # via sqlalchemy-utils +sqlalchemy==1.3.1 urllib3==1.24.1 # via requests From 3686be8610de893529ee4ad40d80c538f13b1e79 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Thu, 21 Mar 2019 23:33:57 +0800 Subject: [PATCH 20/36] Update version in docstring Signed-off-by: tranlyvu --- wikilink/__init__.py | 2 +- wikilink/db/__init__.py | 2 +- wikilink/db/base.py | 2 +- wikilink/db/connection.py | 2 +- wikilink/db/link.py | 2 +- wikilink/db/page.py | 2 +- wikilink/wiki_link.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wikilink/__init__.py b/wikilink/__init__.py index 600e95a..6ccbb8e 100644 --- a/wikilink/__init__.py +++ b/wikilink/__init__.py @@ -13,7 +13,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/__init__.py b/wikilink/db/__init__.py index fafd046..79e66a5 100644 --- a/wikilink/db/__init__.py +++ b/wikilink/db/__init__.py @@ -12,7 +12,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/base.py b/wikilink/db/base.py index dd3a032..9bbfcd5 100644 --- a/wikilink/db/base.py +++ b/wikilink/db/base.py @@ -8,7 +8,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/connection.py b/wikilink/db/connection.py index 4c81eaf..f7f51bd 100644 --- a/wikilink/db/connection.py +++ b/wikilink/db/connection.py @@ -15,7 +15,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/link.py b/wikilink/db/link.py index 3915eba..4116e3d 100644 --- a/wikilink/db/link.py +++ b/wikilink/db/link.py @@ -13,7 +13,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/page.py b/wikilink/db/page.py index 89418f6..ca41001 100644 --- a/wikilink/db/page.py +++ b/wikilink/db/page.py @@ -14,7 +14,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/wiki_link.py b/wikilink/wiki_link.py index dc71be0..fe3a176 100644 --- a/wikilink/wiki_link.py +++ b/wikilink/wiki_link.py @@ -19,7 +19,7 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.0" +__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" From ae67d9a34a682074b7ca6e81576b015da0cfa20d Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:19:16 +0800 Subject: [PATCH 21/36] Add release note Signed-off-by: tranlyvu --- RELEASENOTES.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 RELEASENOTES.md diff --git a/RELEASENOTES.md b/RELEASENOTES.md new file mode 100644 index 0000000..19bcad7 --- /dev/null +++ b/RELEASENOTES.md @@ -0,0 +1,25 @@ +# **Release Note** + +# Upgrade Steps +- Update unit test ([#11](https://github.com/tranlyvu/wiki-link/issues/11)) +- + +# Breaking Changes +- +- + +# New Features +- +- + +# Bug Fixes +- +- + +# Improvements +- Implement function to print path ([#16](https://github.com/tranlyvu/wiki-link/issues/16)) +- + +# Other Changes +- +- \ No newline at end of file From 67b8565c18c86cdd031db063827d290bbfcda08a Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:19:32 +0800 Subject: [PATCH 22/36] Update release note Signed-off-by: tranlyvu --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index c22643d..0fa9b76 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,8 @@ To set up development environment, simply run: $ pip install -r requirements.txt ``` -### List of issues +Please check out the [realease notes](https://github.com/tranlyvu/wiki-link/blob/dev/RELEASENOTES.md) for list of issues that required helps. -1. Implement function to print path ([#16](https://github.com/tranlyvu/wiki-link/issues/16)) -2. Update unit test ([#11](https://github.com/tranlyvu/wiki-link/issues/11)) ### Appreciation From c4357123b53a359e64228d0688b782716d8b2448 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:19:46 +0800 Subject: [PATCH 23/36] Update version Signed-off-by: tranlyvu --- setup.py | 16 ++++++++++------ wikilink/db/__init__.py | 2 +- wikilink/db/base.py | 1 - wikilink/db/connection.py | 1 - wikilink/db/link.py | 1 - wikilink/db/page.py | 1 - wikilink/wiki_link.py | 1 - 7 files changed, 11 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 37816ce..18ef717 100644 --- a/setup.py +++ b/setup.py @@ -13,9 +13,12 @@ with open("README.md", "r") as fh: long_description = fh.read() +with open('wikilink/__init__.py', 'r') as f: + version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) + setup( name='wikilink', - version="1.2.1", + version=version, author='Tran Ly Vu', author_email='vutransingapore@gmail.com', maintainer='Tran Ly Vu ', @@ -27,6 +30,8 @@ packages=find_packages(where="wikilink", exclude=['docs', 'tests*']), package_dir={'':'wikilink'}, license='Apache License 2.0', + zip_safe=False, + platforms='any', classifiers=[ 'Programming Language :: Python :: 3', "Programming Language :: Python :: 3.6", @@ -56,12 +61,11 @@ }, py_modules=["six"], install_requires=[ - "beautifulsoup4", - "requests", - "SQLAlchemy-Utils", - "SQLAlchemy" + "beautifulsoup4>=4.7.1", + "requests>=2.21.0", + "SQLAlchemy-Utils>=0.33.11" ], - python_requires='>=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4', + python_requires='>=3.0, <4', tests_require = [ 'pytest', 'python-coveralls' diff --git a/wikilink/db/__init__.py b/wikilink/db/__init__.py index 79e66a5..4dab51d 100644 --- a/wikilink/db/__init__.py +++ b/wikilink/db/__init__.py @@ -8,11 +8,11 @@ :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. :license: Apache License 2.0. """ + __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/base.py b/wikilink/db/base.py index 9bbfcd5..a94ece9 100644 --- a/wikilink/db/base.py +++ b/wikilink/db/base.py @@ -8,7 +8,6 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/connection.py b/wikilink/db/connection.py index f7f51bd..084b1b9 100644 --- a/wikilink/db/connection.py +++ b/wikilink/db/connection.py @@ -15,7 +15,6 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/link.py b/wikilink/db/link.py index 4116e3d..9218dae 100644 --- a/wikilink/db/link.py +++ b/wikilink/db/link.py @@ -13,7 +13,6 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/page.py b/wikilink/db/page.py index ca41001..e9bcab2 100644 --- a/wikilink/db/page.py +++ b/wikilink/db/page.py @@ -14,7 +14,6 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/wiki_link.py b/wikilink/wiki_link.py index fe3a176..69bf0ab 100644 --- a/wikilink/wiki_link.py +++ b/wikilink/wiki_link.py @@ -19,7 +19,6 @@ __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" From bfdb3a88063c18733bc1e264d64f14f8d1d847d6 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:35:32 +0800 Subject: [PATCH 24/36] Add issue files and update contribution instruction Signed-off-by: tranlyvu --- CONTRIBUTING.md | 2 +- ISSUES.md | 17 +++++++++++++++++ README.md | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 ISSUES.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a65b97c..1118c98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # **Instructions for contributors** -For bug reports or requests please submit an [issue](https://github.com/tranlyvu/wiki-link/issues). +For bug reports or requests please submit an [issue](https://github.com/tranlyvu/wiki-link/issues) and briefly add the issue into the [release notes] For new feature contribution, please follow the following instruction: diff --git a/ISSUES.md b/ISSUES.md new file mode 100644 index 0000000..e437da8 --- /dev/null +++ b/ISSUES.md @@ -0,0 +1,17 @@ +# **Release Note** + +# New Features/ Enhancement +- Implement function to print path [#16](https://github.com/tranlyvu/wiki-link/issues/16) +- Update unit test [#11](https://github.com/tranlyvu/wiki-link/issues/11) +- Enhance loop prevention mechanism [#20](https://github.com/tranlyvu/wiki-link/issues/20) + +# Bugs +- +- + +# Questions + + +# Others +- +- \ No newline at end of file diff --git a/README.md b/README.md index 0fa9b76..5a616ac 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ To set up development environment, simply run: $ pip install -r requirements.txt ``` -Please check out the [realease notes](https://github.com/tranlyvu/wiki-link/blob/dev/RELEASENOTES.md) for list of issues that required helps. +Please check out the [issue file](https://github.com/tranlyvu/wiki-link/blob/dev/RELEASENOTES.md) for list of issues that required helps. ### Appreciation From 29c042616da267a71b9c97127ad30516e6cc4295 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:43:12 +0800 Subject: [PATCH 25/36] Add issue files and update contribution instruction Signed-off-by: tranlyvu --- ISSUES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ISSUES.md b/ISSUES.md index e437da8..503764f 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -1,4 +1,4 @@ -# **Release Note** +# **Issues** # New Features/ Enhancement - Implement function to print path [#16](https://github.com/tranlyvu/wiki-link/issues/16) From 022d74c88a4e031c268734a4a61d049866e9c9d1 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 14:56:13 +0800 Subject: [PATCH 26/36] Update changelog Signed-off-by: tranlyvu --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c27ab..d0678b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,36 @@ # **Change log** -* v1.2.1 - * Add support for python3.7 - * Fix pypi shipping +All notable changes to this project will be documented in this file. -* v1.2.0 - Jan 23, 2019 - * Re-define API - * Publish to PyPi +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -* v1.0.1 - Jan 14, 2018 - * Fix database connection bug - * Test PostgreSQL database +Changes are group into "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security" -* v1.0.0 - Nov 7, 2016 - * First official release \ No newline at end of file +## [Unreleased] +## Added +- Added log, changelog and issue files. + +## [v1.2.1] - 2019-03-17 +### Added +- Add support for python3.7 + +### Fixed +- Fix pypi shipping + +## [v1.2.0] - 2019-01-23 +### Added +- Added setup to publish to PyPi + +### Changed +- Re-define API naming. + +## [v1.0.1] - 2018-01-14 +### Added +- Add PostgreSQL database support + +### Fixed +- Fix database connection bug + +## [v1.0.0] - 2016-11-07 +### Changed +- First official release \ No newline at end of file From 2eba63b069a5a7c7e08ece393632225e656f6177 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 15:04:08 +0800 Subject: [PATCH 27/36] Fix syntax Signed-off-by: tranlyvu --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0678b4..751b4b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Changes are group into "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security" +Changes are grouped into "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security" ## [Unreleased] ## Added From f5abdd1b888c81c716d3131cc721814762f6efbe Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 23 Mar 2019 15:04:36 +0800 Subject: [PATCH 28/36] Fix syntax Signed-off-by: tranlyvu --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 751b4b5..d19e8f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,24 +12,24 @@ Changes are grouped into "Added", "Changed", "Deprecated", "Removed", "Fixed", " ## [v1.2.1] - 2019-03-17 ### Added -- Add support for python3.7 +- Added support for python3.7 ### Fixed -- Fix pypi shipping +- Fixed pypi shipping ## [v1.2.0] - 2019-01-23 ### Added - Added setup to publish to PyPi ### Changed -- Re-define API naming. +- Re-defined API naming. ## [v1.0.1] - 2018-01-14 ### Added -- Add PostgreSQL database support +- Added PostgreSQL database support ### Fixed -- Fix database connection bug +- Fixed database connection bug ## [v1.0.0] - 2016-11-07 ### Changed From 389a49f85fc60723e039721c47ccc746deeba3a6 Mon Sep 17 00:00:00 2001 From: Tran Ly Vu Date: Sat, 23 Mar 2019 23:50:36 +0800 Subject: [PATCH 29/36] Delete RELEASENOTES.md --- RELEASENOTES.md | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 RELEASENOTES.md diff --git a/RELEASENOTES.md b/RELEASENOTES.md deleted file mode 100644 index 19bcad7..0000000 --- a/RELEASENOTES.md +++ /dev/null @@ -1,25 +0,0 @@ -# **Release Note** - -# Upgrade Steps -- Update unit test ([#11](https://github.com/tranlyvu/wiki-link/issues/11)) -- - -# Breaking Changes -- -- - -# New Features -- -- - -# Bug Fixes -- -- - -# Improvements -- Implement function to print path ([#16](https://github.com/tranlyvu/wiki-link/issues/16)) -- - -# Other Changes -- -- \ No newline at end of file From 83f6a85f4e3cd362dbfe8867fd74bb4e4d7587c8 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sun, 24 Mar 2019 23:55:10 +0800 Subject: [PATCH 30/36] Implement multiprocessing Signed-off-by: tranlyvu --- requirements.txt | 3 +- setup.py | 8 +- tests/__init__.py | 6 +- wikilink/wiki_link.py | 203 +++++++++++++++++++++++++++++++----------- 4 files changed, 165 insertions(+), 55 deletions(-) diff --git a/requirements.txt b/requirements.txt index abfbc96..5dc3315 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,9 +8,10 @@ beautifulsoup4==4.7.1 certifi==2019.3.9 # via requests chardet==3.0.4 # via requests idna==2.8 # via requests +mysqlclient==1.4.2.post1 requests==2.21.0 six==1.12.0 # via sqlalchemy-utils soupsieve==1.8 # via beautifulsoup4 sqlalchemy-utils==0.33.11 -sqlalchemy==1.3.1 +sqlalchemy==1.3.1 # via sqlalchemy-utils urllib3==1.24.1 # via requests diff --git a/setup.py b/setup.py index 18ef717..12ae734 100644 --- a/setup.py +++ b/setup.py @@ -9,12 +9,13 @@ :license: Apache License 2.0. """ from setuptools import setup, find_packages +from re import search, MULTILINE with open("README.md", "r") as fh: long_description = fh.read() with open('wikilink/__init__.py', 'r') as f: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), re.MULTILINE).group(1) + version = search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), MULTILINE).group(1) setup( name='wikilink', @@ -63,8 +64,9 @@ install_requires=[ "beautifulsoup4>=4.7.1", "requests>=2.21.0", - "SQLAlchemy-Utils>=0.33.11" - ], + "SQLAlchemy-Utils>=0.33.11", + "mysqlclient>=1.4.2.post1" + ], python_requires='>=3.0, <4', tests_require = [ 'pytest', diff --git a/tests/__init__.py b/tests/__init__.py index 71a8e8f..dece6fb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -8,8 +8,12 @@ :copyright: (c) 2016 - 2018 by Tran Ly VU. All Rights Reserved. :license: Apache License 2.0. """ +__all__ = ["wiki_link"] __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" -__version__ = "1.2.0" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." +__credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" +__maintainer__ = "Tran Ly Vu" +__email__ = "vutransingapore@gmail.com" +__status__ = "Production" diff --git a/wikilink/wiki_link.py b/wikilink/wiki_link.py index 69bf0ab..10a17f2 100644 --- a/wikilink/wiki_link.py +++ b/wikilink/wiki_link.py @@ -5,12 +5,18 @@ # built-in modules from re import compile +from multiprocessing import Process, Queue, Value +from time import sleep +from queue import Empty +from sys import exit #third-party modules from requests import get, HTTPError from bs4 import BeautifulSoup +from sqlalchemy.exc import DisconnectionError, NoSuchColumnError +from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound -# own modules +# own modulesf from wikilink.db.connection import Connection from wikilink.db.page import Page from wikilink.db.link import Link @@ -26,6 +32,8 @@ class WikiLink: def __init__(self): + self.alive = True + self.visited_set = set() pass def setup_db(self, db, name, password, ip, port): @@ -43,7 +51,7 @@ def setup_db(self, db, name, password, ip, port): self.db = Connection(db, name, password, ip, port) - def min_link(self, source_url, dest_url, limit=6): + def min_link(self, source_url, dest_url, limit=6, number_of_processors=1): """return minimum number of link Args: db(str): Database engine, currently support "mysql" and "postgresql" @@ -54,59 +62,111 @@ def min_link(self, source_url, dest_url, limit=6): Returns: int: minimum number of sepration between startinga nd ending urls + + Raise: + queue.Empty + DisconnectionError """ - # update page for both starting and ending url - source_id = self._insert_url(source_url.split("/wiki/")[-1]) - dest_id = self._insert_url(dest_url.split("/wiki/")[-1]) + self.source_id = self._insert_url(source_url.split("/wiki/")[-1]) + self.dest_id = self._insert_url(dest_url.split("/wiki/")[-1]) + + try: + separation = self.db.session.query(Link.number_of_separation) \ + .filter(Link.from_page_id == self.source_id, + Link.to_page_id == self.dest_id).all() + except DisconnectionError: + print("There is error with DB connection") + return + - separation = self.db.session.query(Link.number_of_separation).filter(Link.from_page_id == source_id, \ - Link.to_page_id == dest_id).all() # check if the link already exists if str(separation) is not None and len(separation) != 0: return separation[0][0] - number_of_separation = 0 - queue = [source_id] - already_seen = set(queue) + self.limit = limit - while number_of_separation <= limit and len(queue) > 0: - number_of_separation += 1 - temporary_queue = queue - queue = [] - # find outbound links from current url - for url_id in temporary_queue: - self._update_url(url_id) + execution_queue = Queue() + storage_queue = Queue() + storage_queue.put(self.source_id) + self.visited_set.add(self.source_id) + processes = [] + answer = Value("i", 0) - neighbors = self.db.session.query(Link).filter(Link.from_page_id == url_id, \ - Link.number_of_separation == 1).all() - for n in neighbors: - if n.to_page_id == dest_id: - self._insert_link(source_id, dest_id, number_of_separation) - return number_of_separation + for _ in range(number_of_processors): + processes.append(Process(target=self._worker, + args=(answer, execution_queue, + storage_queue))) - if n.to_page_id not in already_seen: - already_seen.add(n.to_page_id) - queue.append(n.to_page_id) - - if number_of_separation > limit: - print("No solution within limit! Consider to increase the limit.") - return - else: - print("there is no path from {} to {}".format(starting_url, ending_url)) + for p in processes: + p.start() + + while True: + try: + url_id = storage_queue.get(timeout=20.0) + execution_queue.put(url_id) + except Empty: + for p in processes: + p.terminate() + return answer.value if answer.value > 0 else exit(1) + + + def _worker(self, answer, execution_queue, storage_queue): + + while self.alive: + while execution_queue.empty(): + sleep(0.1) + + url_id = execution_queue.get() + print(url_id) + print(self.visited_set) + try: + number_of_sep = self.db.session.query(Link.number_of_separation) \ + .filter(Link.from_page_id == self.source_id, + Link.to_page_id == url_id).one() + except MultipleResultsFound: + print("Many rows found in DB to find seperation from {} to {}".format(self.source_id,url_id)) + self.alive = False + sleep(0.1) - def _update_url(self, url_id): + else: + number_of_sep = number_of_sep[0] + if number_of_sep >= self.limit: + print("No solution within limit! Consider increse the limit") + self.alive = False + sleep(0.1) + + sleep(1) + neighbors = self._scraper(url_id) + + if len(neighbors) == 0 and number_of_sep <= self.limit: + print("there is no path from {} to {}".format(starting_url, + ending_url)) + self.alive = False + sleep(0.1) + + for n in neighbors: + if n not in self.visited_set: + self.visited_set.add(n) + self._insert_link(self.source_id, n, number_of_sep + 1) + if n == self.dest_id: + self.alive = False + answer.value = number_of_sep + 1 + sleep(0.1) + else: + storage_queue.put(n) + + + def _scraper(self, url_id): """ Scrap urls from given url id and insert into database Args: - starting_id: the stripped starting url - ending_id: the stripped ending url - number_of_separation: - + url_id(int): the id of url to be scraped + Returns: - None + list Raises: HTTPError: if An HTTP error occurred @@ -116,6 +176,9 @@ def _update_url(self, url_id): # retrieve url from id url = self.db.session.query(Page.url).filter(Page.id == url_id).first() + if url == None: + return + # handle exception where page not found or server down or url mistyped try: response = get('https://en.wikipedia.org/wiki/' + str(url[0])) @@ -128,16 +191,21 @@ def _update_url(self, url_id): else: soup = BeautifulSoup(html, "html.parser") - # update all wiki links with tag 'a' and attribute 'href' start with '/wiki/' # (?!...) : match if ... does not match next - links = soup.findAll("a", href=compile("(/wiki/)((?!:).)*$")) + links = soup.find("div", {"id":"bodyContent"}).findAll("a", + href=compile("(/wiki/)((?!:).)*$")) + + new_links_id = [] for link in links: # only insert link starting with /wiki/ and update Page if not exist inserted_url = link.attrs['href'].split("/wiki/")[-1] inserted_id = self._insert_url(inserted_url) + new_links_id.append(inserted_id) + # update links table with starting page if it not exists self._insert_link(url_id, inserted_id, 1) + return new_links_id def _insert_url(self, url): @@ -148,18 +216,32 @@ def _insert_url(self, url): Returns: int: url id + + Raise: + DisconnectionError + NoResultFound """ + try: + exist = self.db.session.query(Page.id).filter(Page.url == url).one() - page_list = self.db.session.query(Page).filter(Page.url == url).all() - if len(page_list) == 0: + except (NoResultFound, NoSuchColumnError): page = Page(url=url) self.db.session.add(page) self.db.session.commit() - url_id = self.db.session.query(Page.id).filter(Page.url == url).all()[0][0] + + url_id = self.db.session.query(Page.id).filter(Page.url == url).one()[0] self._insert_link(url_id, url_id, 0) - return url_id + + except DisconnectionError: + print("There is error with DB connection") + return + else: - return self.db.session.query(Page.id).filter(Page.url == url).all()[0][0] + url_id = exist[0] + + finally: + return url_id + def _insert_link(self, from_page_id, to_page_id, no_of_separation): @@ -172,11 +254,32 @@ def _insert_link(self, from_page_id, to_page_id, no_of_separation): Returns: None + + Raise + NoResultFound + DisconnectionError + NoSuchColumnError """ + try: + + exist = self.db.session.query(Link).filter( + Link.from_page_id==from_page_id, + Link.to_page_id==to_page_id, + Link.number_of_separation==no_of_separation)\ + .one() + + except (NoResultFound, NoSuchColumnError): + link = Link(from_page_id=from_page_id, + to_page_id=to_page_id, + number_of_separation=no_of_separation) - link_between_2_pages = self.db.session.query(Link).filter(Link.from_page_id == from_page_id, - Link.to_page_id == to_page_id).all() - if len(link_between_2_pages) == 0: - link = Link(from_page_id=from_page_id, to_page_id=to_page_id, number_of_separation=no_of_separation) self.db.session.add(link) - self.db.session.commit() \ No newline at end of file + self.db.session.commit() + + except DisconnectionError: + raise DisconnectionError("There is error with DB connection") + + except MultipleResultsFound: + print("Many rows found in DB to store link from {} to {} with number of seperation {}".format(from_page_id, + to_page_id, + no_of_separation)) \ No newline at end of file From 0088619d3fc11470b06d0764c6f6c6f30ec83b90 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Mon, 25 Mar 2019 00:10:36 +0800 Subject: [PATCH 31/36] Implement multiprocessing Signed-off-by: tranlyvu --- test.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 0000000..d74ca00 --- /dev/null +++ b/test.py @@ -0,0 +1,6 @@ +from wikilink import wiki_link +model = wiki_link.WikiLink() +model.setup_db("mysql", "root", "13061990", "127.0.0.1", "3306") +source_url = "https://en.wikipedia.org/wiki/Cristiano_Ronaldo" +dest_url = "https://en.wikipedia.org/wiki/Software_engineer" +print(model.min_link(source_url, dest_url, 6, 2)) \ No newline at end of file From e3bb1e0cbcfd157f8769f4ee798864cfbcc7c696 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 30 Mar 2019 15:09:04 +0800 Subject: [PATCH 32/36] Update API Signed-off-by: tranlyvu --- README.md | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5a616ac..e637c53 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ wikilink is a web-scraping application which purpose is to find the minimum numb I discussed brief the motivation and an overview of the project in [my blog](https://tranlyvu.github.io/algorithms/BFS-and-a-simple-application/). -The project is currently at production stage - [v1.2.1](https://github.com/tranlyvu/wiki-link/releases), also see [change log](https://github.com/tranlyvu/wiki-link/blob/dev/CHANGELOG.md) for more details on release history. +The project is currently at version [v0.2.1](https://github.com/tranlyvu/wiki-link/releases), also see [change log](https://github.com/tranlyvu/wiki-link/blob/dev/CHANGELOG.md) for more details on release history. +If you like this project, feel fee to leave a few words of appreciation here [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/tranlyvu) | Build | [![Build Status][3]][4] | [![Coverage Status][5]][6] | | :--- | :--- | :--- | @@ -46,8 +47,7 @@ Table of contents 1. [Usage](#Usage) 2. [Contribution](#Contribution) -3. [Contact](#Contact) -4. [License](#License) +3. [License](#License) --- Usage @@ -65,17 +65,20 @@ wikilink currently supports [Mysql](https://www.mysql.com/downloads/) and [Postg ### API -setup_db(db, username, password, ip, port): to set up database; supported "mysql" and postgresql" for 'db' argument. +setup_db(db, username, password, ip="127.0.0.1", port=3306): to set up database; supported "mysql" and postgresql" for 'db' argument. -min_link(source_url, dest_url, limit = 6): find minimum number of link from source url to destination url within limit (default=6) +min_link(source, destination, limit=6, multiprocessing=False): find minimum number of link from source url to destination url within limit (default=6) ### Examples ``` ->>> from wikilink import wiki_link ->>> model = wiki_link.WikiLink() ->>> model.setup_db("mysql", "root", "12345", "127.0.0.1", "3306") ->>> model.min_link(source_url, dest_url, 6) +>>> from wikilink import WikiLink +>>> app = WikiLink() +>>> app.setup_db("mysql", "root", "12345", "127.0.0.1", "3306") +>>> source = "https://en.wikipedia.org/wiki/Cristiano_Ronaldo" +>>> destination = "https://en.wikipedia.org/wiki/Lionel_Messi" +>>> app.min_link(source, destination, 6) +1 ``` --- @@ -105,19 +108,6 @@ Feel free to add your name into the [list of contributors](https://github.com/tr [![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/0)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/0)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/1)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/1)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/2)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/2)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/3)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/3)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/4)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/4)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/5)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/5)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/6)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/6)[![](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/images/7)](https://sourcerer.io/fame/tranlyvu/tranlyvu/wiki-link/links/7) ---- -Contact ---- - -Feel free to contact me to discuss any issues, questions, or comments. - -* Email: vutransingapore@gmail.com -* Linkedln: [@vutransingapore](https://www.linkedin.com/in/tranlyvu/) -* GitHub: [Tran Ly Vu](https://github.com/tranlyvu) -* Blog: [tranlyvu.github.io](https://tranlyvu.github.io/) - -If you like my project, feel fee to leave a few words of appreciation here [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/tranlyvu) - --- License --- From dd5ef76a6b4bfa4d5561ac180dcfbeb98338bf63 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 30 Mar 2019 15:09:24 +0800 Subject: [PATCH 33/36] ADD pypy implementation Signed-off-by: tranlyvu --- setup.py | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index 12ae734..697d41a 100644 --- a/setup.py +++ b/setup.py @@ -18,47 +18,51 @@ version = search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', f.read(), MULTILINE).group(1) setup( - name='wikilink', + name="wikilink", version=version, - author='Tran Ly Vu', - author_email='vutransingapore@gmail.com', - maintainer='Tran Ly Vu ', - maintainer_email='vutransingapore@gmail.com', - description='A web-scraping application to find the minimum number of links between 2 given wiki pages', + author="Tran Ly Vu", + author_email="vutransingapore@gmail.com", + maintainer="Tran Ly Vu ", + maintainer_email="vutransingapore@gmail.com", + description="A web-scraping application to find the minimum number of links between 2 given wiki pages", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/tranlyvu/wiki-link", - packages=find_packages(where="wikilink", exclude=['docs', 'tests*']), - package_dir={'':'wikilink'}, - license='Apache License 2.0', + packages=find_packages(where="wikilink", exclude=["docs", "tests*"]), + package_dir={"":"wikilink"}, + license="Apache License 2.0", zip_safe=False, - platforms='any', + platforms="any", classifiers=[ - 'Programming Language :: Python :: 3', + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "License :: OSI Approved :: Apache Software License ", "Operating System :: POSIX", "Operating System :: Linux", "Operating System :: Unix", - "Development Status :: 5 - Production/Stable ", + "Development Status :: 4 - Beta", "Natural Language :: English", "Environment :: Console", "Intended Audience :: Education", "Intended Audience :: End Users/Desktop", "Intended Audience :: Information Technology", - 'Intended Audience :: Science/Research', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Scientific/Engineering :: Information Analysis", - "Topic :: Education" + "Topic :: Education", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Framework :: Pytest", + "Framework :: Flake8" ], keywords=["Web Scraping", "Artificial Intelligence", "Breadth First Search", "Graph", "Data Science", "Web Extracting", "Information Analysis"], project_urls={ - 'Source': 'https://github.com/tranlyvu/wiki-link', - 'Tracker': 'https://github.com/tranlyvu/wiki-link/issues', - 'Chat: Gitter': 'https://gitter.im/find-link/Lobby', - 'CI: Travis': 'https://travis-ci.org/tranlyvu/wiki-link', - 'Coverage: coveralls': 'https://coveralls.io/github/tranlyvu/wiki-link', + "Source": "https://github.com/tranlyvu/wiki-link", + "Tracker": "https://github.com/tranlyvu/wiki-link/issues", + "Chat: Gitter": "https://gitter.im/find-link/Lobby", + "CI: Travis": "https://travis-ci.org/tranlyvu/wiki-link", + "Coverage: coveralls": "https://coveralls.io/github/tranlyvu/wiki-link", }, py_modules=["six"], install_requires=[ @@ -67,9 +71,9 @@ "SQLAlchemy-Utils>=0.33.11", "mysqlclient>=1.4.2.post1" ], - python_requires='>=3.0, <4', + python_requires=">=3.0, <4", tests_require = [ - 'pytest', - 'python-coveralls' + "pytest", + "python-coveralls" ] ) \ No newline at end of file From bd02ed04df7459712bd4aa1917b7deaeeb006cb9 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 30 Mar 2019 15:22:31 +0800 Subject: [PATCH 34/36] Add version 0.3.0 Signed-off-by: tranlyvu --- CHANGELOG.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d19e8f1..1712f4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,31 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Changes are grouped into "Added", "Changed", "Deprecated", "Removed", "Fixed", "Security" -## [Unreleased] -## Added +## [v0.3.0] - 2019-03-30 +### Added - Added log, changelog and issue files. +- Added support for multiprocessing +- support pypy implementation +- Used deque instead of list to implement queue + +### Fixed +- Fixed multiple flake8 issues + +### Changed +- Downgrade version from major 1 to 0 -## [v1.2.1] - 2019-03-17 +## [v0.2.1] - 2019-03-17 ### Added - Added support for python3.7 ### Fixed - Fixed pypi shipping -## [v1.2.0] - 2019-01-23 +## [v0.2.0] - 2019-01-23 ### Added - Added setup to publish to PyPi ### Changed - Re-defined API naming. -## [v1.0.1] - 2018-01-14 +## [v0.1.1] - 2018-01-14 ### Added - Added PostgreSQL database support ### Fixed - Fixed database connection bug -## [v1.0.0] - 2016-11-07 +## [v0.1.0] - 2016-11-07 ### Changed - First official release \ No newline at end of file From 87dbd377953ffc0400101440a2f0a1498821ba96 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 30 Mar 2019 15:22:50 +0800 Subject: [PATCH 35/36] Implement multiprocessing Signed-off-by: tranlyvu --- test.py | 6 - wikilink/__init__.py | 6 +- wikilink/db/__init__.py | 14 +- wikilink/db/base.py | 2 +- wikilink/db/connection.py | 33 ++- wikilink/db/link.py | 15 +- wikilink/db/page.py | 9 +- wikilink/wiki_link.py | 599 +++++++++++++++++++++++++++----------- 8 files changed, 473 insertions(+), 211 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index d74ca00..0000000 --- a/test.py +++ /dev/null @@ -1,6 +0,0 @@ -from wikilink import wiki_link -model = wiki_link.WikiLink() -model.setup_db("mysql", "root", "13061990", "127.0.0.1", "3306") -source_url = "https://en.wikipedia.org/wiki/Cristiano_Ronaldo" -dest_url = "https://en.wikipedia.org/wiki/Software_engineer" -print(model.min_link(source_url, dest_url, 6, 2)) \ No newline at end of file diff --git a/wikilink/__init__.py b/wikilink/__init__.py index 6ccbb8e..62347f7 100644 --- a/wikilink/__init__.py +++ b/wikilink/__init__.py @@ -8,12 +8,14 @@ :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. :license: Apache License 2.0. """ -__all__ = ["wiki_link"] +from .wiki_link import WikiLink + +__all__ = ["WikiLink"] __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] __license__ = "Apache License 2.0" -__version__ = "1.2.1" +__version__ = "0.2.1" __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" diff --git a/wikilink/db/__init__.py b/wikilink/db/__init__.py index 4dab51d..0fd1740 100644 --- a/wikilink/db/__init__.py +++ b/wikilink/db/__init__.py @@ -2,13 +2,18 @@ wikilink ~~~~~~~~ - wiki-link is a web-scraping application to find minimum number - of links between two given wiki pages. + wiki-link is a web-scraping application to find minimum number + of links between two given wiki pages. - :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. - :license: Apache License 2.0. + :copyright: (c) 2016 - 2019 by Tran Ly VU. All Rights Reserved. + :license: Apache License 2.0. """ +from .connection import Connection +from .page import Page +from .link import Link + +__all__ = ["Connection", "Page", "Link"] __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." __credits__ = ["Tranlyvu"] @@ -16,4 +21,3 @@ __maintainer__ = "Tran Ly Vu" __email__ = "vutransingapore@gmail.com" __status__ = "Production" - diff --git a/wikilink/db/base.py b/wikilink/db/base.py index a94ece9..a226e5f 100644 --- a/wikilink/db/base.py +++ b/wikilink/db/base.py @@ -12,4 +12,4 @@ __email__ = "vutransingapore@gmail.com" __status__ = "Production" -Base = declarative_base() \ No newline at end of file +Base = declarative_base() diff --git a/wikilink/db/connection.py b/wikilink/db/connection.py index 084b1b9..a436950 100644 --- a/wikilink/db/connection.py +++ b/wikilink/db/connection.py @@ -3,12 +3,11 @@ """Provide Connection - the main gateway to access db""" -#third-party modules +# third-party modules from sqlalchemy import create_engine from sqlalchemy_utils import functions -from sqlalchemy.orm import sessionmaker -#own modules +# own modules from .base import Base __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" @@ -21,19 +20,29 @@ class Connection: def __init__(self, db, name, password, ip, port): + if db == "postgresql": - connection = "postgresql+psycopg2://" + name + ":" + password + "@" + ip + ":" + port + connection = "postgresql+psycopg2://" + name + ":" \ + + password + "@" + ip + ":" + port elif db == "mysql": - connection = "mysql://" + name + ":" + password + "@" + ip + ":" + port + connection = "mysql://" + name + ":" + password \ + + "@" + ip + ":" + port else: - raise ValueError("db type only support \"mysql\" or \"postgresql\" argument.") + raise ValueError("db type only \ + support \"mysql\" or \"postgresql\" argument.") + db_name = 'wikilink' # Turn off echo - engine = create_engine(connection + "/" + db_name + '?charset=utf8', echo=False, encoding='utf-8') - if not functions.database_exists(engine.url): - functions.create_database(engine.url) + self.engine = create_engine(connection + "/" + db_name + '?charset=utf8' + ,echo=False, encoding='utf-8' + ,pool_pre_ping=True + ,pool_size=10) + + if not functions.database_exists(self.engine.url): + functions.create_database(self.engine.url) - self.session = sessionmaker(bind=engine)() # If table don't exist, Create. - if (not engine.dialect.has_table(engine, 'link') and not engine.dialect.has_table(engine, 'page')): - Base.metadata.create_all(engine) \ No newline at end of file + if (not self.engine.dialect.has_table(self.engine, 'link')\ + and not self.engine.dialect.has_table(self.engine, 'page')): + + Base.metadata.create_all(self.engine) diff --git a/wikilink/db/link.py b/wikilink/db/link.py index 9218dae..549ce89 100644 --- a/wikilink/db/link.py +++ b/wikilink/db/link.py @@ -4,7 +4,7 @@ """Provide Link - the main calss to initialize link table""" # Third party modules -from sqlalchemy import Column, Integer, String, DateTime, text, ForeignKey +from sqlalchemy import Column, Integer, DateTime, text, ForeignKey # own modules from .base import Base @@ -17,16 +17,23 @@ __email__ = "vutransingapore@gmail.com" __status__ = "Production" + class Link(Base): """Link table""" + __tablename__ = 'link' id = Column(Integer, primary_key=True) from_page_id = Column(Integer, ForeignKey('page.id')) to_page_id = Column(Integer, ForeignKey('page.id')) number_of_separation = Column(Integer, nullable=False) - created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) + created = Column(DateTime, nullable=False, + server_default=text('CURRENT_TIMESTAMP')) def __repr__(self): - return "" % ( - self.from_page_id, self.to_page_id, self.number_of_separation, self.created) + return "" %( + self.from_page_id, + self.to_page_id, + self.number_of_separation, + self.created) diff --git a/wikilink/db/page.py b/wikilink/db/page.py index e9bcab2..97661c0 100644 --- a/wikilink/db/page.py +++ b/wikilink/db/page.py @@ -4,7 +4,7 @@ """Provide Page - the main calss to initialize page table""" # third party modules -from sqlalchemy import Column, Integer, String, DateTime, text, ForeignKey +from sqlalchemy import Column, Integer, DateTime, text from sqlalchemy.dialects.mysql import LONGTEXT # own modules @@ -26,7 +26,10 @@ class Page(Base): id = Column(Integer(), primary_key=True) url = Column(LONGTEXT) - created = Column(DateTime, nullable=False, server_default=text('CURRENT_TIMESTAMP')) + created = Column(DateTime, nullable=False, + server_default=text('CURRENT_TIMESTAMP')) def __repr__(self): - return "" % (self.page_id, self.url, self.created) \ No newline at end of file + return "" %(self.page_id, + self.url, + self.created) diff --git a/wikilink/wiki_link.py b/wikilink/wiki_link.py index 10a17f2..9d180ae 100644 --- a/wikilink/wiki_link.py +++ b/wikilink/wiki_link.py @@ -5,21 +5,26 @@ # built-in modules from re import compile -from multiprocessing import Process, Queue, Value +from multiprocessing import Process, Queue, Event, cpu_count, Value from time import sleep from queue import Empty from sys import exit +from collections import deque +from contextlib import contextmanager -#third-party modules +# third-party modules from requests import get, HTTPError from bs4 import BeautifulSoup from sqlalchemy.exc import DisconnectionError, NoSuchColumnError from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound +from sqlalchemy.orm import scoped_session +from sqlalchemy.orm import sessionmaker -# own modulesf -from wikilink.db.connection import Connection -from wikilink.db.page import Page -from wikilink.db.link import Link + +# own modules +from .db import Connection +from .db import Page +from .db import Link __author__ = "Tran Ly Vu (vutransingapore@gmail.com)" __copyright__ = "Copyright (c) 2016 - 2019 Tran Ly Vu. All Rights Reserved." @@ -32,254 +37,492 @@ class WikiLink: def __init__(self): - self.alive = True - self.visited_set = set() pass - def setup_db(self, db, name, password, ip, port): - """Setting up database + def setup_db(self, db, name, password, ip="127.0.0.1", port=3306): + + """ Setting up database Args: db(str): Database engine, currently support "mysql" and "postgresql" name(str): database username password(str): database password - ip(str): IP address of database - port(str): port that databse is running on + ip(str): IP address of database (Default = "127.0.0.1") + port(str): port that databse is running on (default=3306) - Returns: + Returns: None """ - + self.db = Connection(db, name, password, ip, port) - def min_link(self, source_url, dest_url, limit=6, number_of_processors=1): - """return minimum number of link + + @contextmanager + def _session_scope(self): + """Provide a transactional scope around a series of operations. + Args: - db(str): Database engine, currently support "mysql" and "postgresql" - name(str): database username - password(str): database password - ip(str, optional): IP address of database. Default to "127.0.0.1" - port(str): port that databse is running on + None - Returns: - int: minimum number of sepration between startinga nd ending urls + Returns: + session - Raise: - queue.Empty - DisconnectionError - """ + Raises: + DisconnectionError: error connecting to DB + + """ - self.source_id = self._insert_url(source_url.split("/wiki/")[-1]) - self.dest_id = self._insert_url(dest_url.split("/wiki/")[-1]) + Session = sessionmaker() + Session.configure(bind=self.db.engine) + session = Session() try: - separation = self.db.session.query(Link.number_of_separation) \ - .filter(Link.from_page_id == self.source_id, - Link.to_page_id == self.dest_id).all() - except DisconnectionError: - print("There is error with DB connection") - return + yield session + session.commit() + except: + session.rollback() + raise + finally: + session.close() + self.db.engine.dispose() - # check if the link already exists - if str(separation) is not None and len(separation) != 0: - return separation[0][0] + def min_link(self, source, destination, limit=6, multiprocessing=False): + """ Return minimum number of link + + Args: + source(str): source wiki url, + i.e. "https://en.wikipedia.org/wiki/Cristiano_Ronaldo" + destination(str): Destination wiki url, + ie. "https://en.wikipedia.org/wiki/Cristiano_Ronaldo" + limit(int): max number of links from the + source that will be considered (default=6) + multiprocessing(boolean): enable/disable + multiprocessing mode (default=False) + + Returns: + (int) minimum number of sepration bw source and destination urls + return None and print messages if exceeding limits or no path found + + Raises: + DisconnectionError: error connecting to DB + """ + + if source == destination: + return 0 + + self.source = source + self.destination = destination self.limit = limit + with self._session_scope() as session: + + try: + self.source_id = _insert_url(session, + source.split("/wiki/")[-1]) + self.dest_id = _insert_url(session, + destination.split("/wiki/")[-1]) + + if session.query(Link.number_of_separation).filter_by( + from_page_id=self.source_id, + to_page_id=self.dest_id).scalar() is not None: + + sep = session.query(Link.number_of_separation).filter_by( + from_page_id=self.source_id, + to_page_id=self.dest_id).one()[0] + return sep + + except DisconnectionError: + print("There is error with DB connection") + exit(1) + + if multiprocessing: + return self._multiprocessing_scraper() + else: + return self._single_threaded_scraper() + + + def _single_threaded_scraper(self): + + """ Return minimum number of link using single threaded scraper + + Args: + None + + Returns: + (int) minimum number of sepration bw source and destination urls + return None and print messages if exceeding limits or no path found + + Raises: + DisconnectionError: error connecting to DB + """ + + number_of_separation = 0 + queue = deque() + queue.appendleft(self.source_id) + queue.appendleft(None) + visited_set = set() + visited_set.add(self.source_id) + + with self._session_scope() as session: + + while number_of_separation <= self.limit and len(queue) > 0: + number_of_separation += 1 + url_id = queue.pop() + + if url_id is None: + if len(queue) > 0: + queue.appendleft(None) + elif len(queue) == 0: + continue + + else: + + _scraper(session, url_id) + + try: + neighbors = session.query(Link).filter( + Link.from_page_id == url_id, + Link.number_of_separation == 1).all() + + except DisconnectionError: + print("There is error with DB connection") + exit(1) + + for n in neighbors: + + if n.to_page_id not in visited_set: + + visited_set.add(n.to_page_id) + queue.appendleft(n.to_page_id) + + _insert_link(session, + self.source_id, + url_id, + number_of_separation) + + if n.to_page_id == self.dest_id: + return number_of_separation + + if number_of_separation > self.limit: + print("No solution within limit! Consider to increase the limit.") + else: + print("there is no path from {} to {}".format(self.source, + self.destination)) + exit(1) + + + def _multiprocessing_scraper(self): + + """ Return minimum number of link using single multiprocessing scraper + + Args: + None + + Returns: + (int) minimum number of sepration bw source and destination urls + return None and print messages if exceeding limits or no path found + + Raises: + Empty + """ + execution_queue = Queue() storage_queue = Queue() - storage_queue.put(self.source_id) - self.visited_set.add(self.source_id) - processes = [] - answer = Value("i", 0) + separation_queue = Queue() - for _ in range(number_of_processors): - processes.append(Process(target=self._worker, - args=(answer, execution_queue, - storage_queue))) + # putting source id first + storage_queue.put(self.source_id) + separation_queue.put(0) + session_factory = sessionmaker(bind=self.db.engine) + self.DBSession = scoped_session(session_factory) + + answer = Value('i', 0) + event = Event() + + delegator = Process(target=self._delegator, args=( + execution_queue, + storage_queue, + separation_queue, + event, + answer)) + + processes = [delegator] + + for i in range((cpu_count())): + p = Process(target=self._worker, args=( + execution_queue, + storage_queue, + separation_queue, + event)) + + processes.append(p) + for p in processes: p.start() + + while not event.is_set(): + continue + + for p in processes: + p.terminate() + + self.DBSession.remove() + self.db.engine.dispose() + + if answer.value > self.limit: + print("No solution within limit! Consider to increase the limit.") + exit(1) + elif answer.value > 0: + return answer.value + + + def _delegator(self, + execution_queue, + storage_queue, + separation_queue, + event, + answer): + + """ The function acts as jobs delegator, picking up url_id from + storage_queue and put into execution queue + + Args: + execution_queue: queue that store url_id to scrape + storage_queue: queue that store url_id after found from scraping + separation_queue: queue that store number of seperation of + url after found from scraping + event: to signal when scraping is finished + answer: shared-stated value that store answer + + Returns: + None + + Raises: + Empty: if storage queue have no url for 15 minutes + """ + + + while not event.is_set(): + while not storage_queue.empty() and not separation_queue.empty(): + try: + url_id = storage_queue.get(timeout=15.0) + sep = separation_queue.get(timeout=15.0) + + print("take url_id {} with sep {} out of queues" \ + .format(url_id, sep)) + + session = self.DBSession() + _insert_link(session, self.source_id, url_id, sep) + + if url_id == self.dest_id: + answer.value = sep + event.set() + exit(1) + + print("put url_id {} into execution queue".format(url_id)) + execution_queue.put(url_id) + + except Empty: + event.set() + exit(1) + + + def _worker(self, execution_queue, storage_queue, separation_queue, event): + """ The worker function that pick up url_id from + execution_queue and scrape - while True: - try: - url_id = storage_queue.get(timeout=20.0) - execution_queue.put(url_id) - except Empty: - for p in processes: - p.terminate() - return answer.value if answer.value > 0 else exit(1) - - - def _worker(self, answer, execution_queue, storage_queue): + Args: + execution_queue: queue that store url_id to scrape + storage_queue: queue that store url_id after found from scraping + separation_queue: queue that store number of + seperation of url after found from scraping + event: to signal when scraping is finished + + Returns: + None - while self.alive: + Raises: + MultipleResultsFound + NoResultFound + NoSuchColumnError + """ + + visited_set = set() + + while not event.is_set(): while execution_queue.empty(): sleep(0.1) url_id = execution_queue.get() - print(url_id) - print(self.visited_set) + visited_set.add(url_id) + print("take url_id {} out of execution queue".format(url_id)) + sleep(1) try: - number_of_sep = self.db.session.query(Link.number_of_separation) \ - .filter(Link.from_page_id == self.source_id, - Link.to_page_id == url_id).one() + session = self.DBSession() + number_of_sep = session.query(Link.number_of_separation) \ + .filter_by(from_page_id=self.source_id, + to_page_id=url_id).first() except MultipleResultsFound: - print("Many rows found in DB to find seperation from {} to {}".format(self.source_id,url_id)) - self.alive = False - sleep(0.1) + print("Many rows found in DB to find seperation from {} to {}" + .format(self.source_id, url_id)) + event.set() + exit(1) + + except (NoResultFound, NoSuchColumnError): + print("No result found") + exit(1) else: - number_of_sep = number_of_sep[0] + number_of_sep = int(number_of_sep.number_of_separation) if number_of_sep >= self.limit: - print("No solution within limit! Consider increse the limit") - self.alive = False - sleep(0.1) + print("No solution within limit! Increse the limit") + event.set() + exit(1) - sleep(1) - neighbors = self._scraper(url_id) + session = self.DBSession() + neighbors = _scraper(session, url_id) if len(neighbors) == 0 and number_of_sep <= self.limit: - print("there is no path from {} to {}".format(starting_url, - ending_url)) - self.alive = False - sleep(0.1) - + print("there is no path from {} to {}".format(self.source, + self.destination)) + event.set() + exit(1) + for n in neighbors: - if n not in self.visited_set: - self.visited_set.add(n) - self._insert_link(self.source_id, n, number_of_sep + 1) - if n == self.dest_id: - self.alive = False - answer.value = number_of_sep + 1 - sleep(0.1) - else: - storage_queue.put(n) + if n not in visited_set: + visited_set.add(n) + storage_queue.put(n) + separation_queue.put(number_of_sep + 1) - - def _scraper(self, url_id): - """ Scrap urls from given url id and insert into database - - Args: - url_id(int): the id of url to be scraped +def _scraper(session, url_id): - Returns: - list + """ Scrap urls from given url id and insert into database - Raises: - HTTPError: if An HTTP error occurred + Args: + url_id(int): the id of url to be scraped - """ + Returns: + list of new url ids - # retrieve url from id - url = self.db.session.query(Page.url).filter(Page.id == url_id).first() + Raises: + HTTPError: if An HTTP error occurred + """ - if url == None: - return + # retrieve url from id - # handle exception where page not found or server down or url mistyped - try: - response = get('https://en.wikipedia.org/wiki/' + str(url[0])) - html = response.text - except HTTPError: + if session.query(Page.url).filter_by(id=url_id).scalar is None: + print("There is no url for id {}".format(url_id)) + exit(1) + else: + url = session.query(Page.url).filter_by(id=url_id).first() + + if url is None: + return + + # handle exception where page not found or server down or url mistyped + try: + response = get('https://en.wikipedia.org/wiki/' + str(url[0])) + html = response.text + except HTTPError: + return + else: + if html is None: return else: - if html is None: - return - else: - soup = BeautifulSoup(html, "html.parser") + soup = BeautifulSoup(html, "html.parser") - # (?!...) : match if ... does not match next - links = soup.find("div", {"id":"bodyContent"}).findAll("a", - href=compile("(/wiki/)((?!:).)*$")) - - new_links_id = [] - for link in links: - # only insert link starting with /wiki/ and update Page if not exist - inserted_url = link.attrs['href'].split("/wiki/")[-1] - inserted_id = self._insert_url(inserted_url) + # (?!...) : match if ... does not match next + links = soup.find("div", {"id": "bodyContent"}).findAll("a", + href=compile("(/wiki/)((?!:).)*$")) - new_links_id.append(inserted_id) + new_links_id = [] + for link in links: + # only insert link starting with /wiki/ and update Page if not exist + inserted_url = link.attrs['href'].split("/wiki/")[-1] + inserted_id = _insert_url(session, inserted_url) - # update links table with starting page if it not exists - self._insert_link(url_id, inserted_id, 1) - return new_links_id + new_links_id.append(inserted_id) + # update links table with starting page if it not exists + _insert_link(session, url_id, inserted_id, 1) - def _insert_url(self, url): + return new_links_id - """ insert into table Page if not exist and return the url id - Args: - url(str): wiki url to update - Returns: - int: url id +def _insert_url(session, url): - Raise: - DisconnectionError - NoResultFound - """ - try: - exist = self.db.session.query(Page.id).filter(Page.url == url).one() + """ insert into table Page if not exist and return the url id + Args: + url(str): wiki url to update + + Returns: + int: url id + + Raise: + DisconnectionError + MultipleResultsFound + """ + try: - except (NoResultFound, NoSuchColumnError): + if session.query(Page.id).filter_by(url=url).scalar() is None: page = Page(url=url) - self.db.session.add(page) - self.db.session.commit() + session.add(page) + session.commit() + url_id = session.query(Page).filter_by(url=url).first().id + _insert_link(session, url_id, url_id, 0) - url_id = self.db.session.query(Page.id).filter(Page.url == url).one()[0] - self._insert_link(url_id, url_id, 0) - - except DisconnectionError: - print("There is error with DB connection") - return + except DisconnectionError: + raise DisconnectionError("There is error with DB connection") - else: - url_id = exist[0] + except MultipleResultsFound: + raise MultipleResultsFound("Many rows found in DB to find url \ + id of {}".format(url)) - finally: - return url_id + url_id = session.query(Page.id).filter_by(url=url).first() + return url_id.id - def _insert_link(self, from_page_id, to_page_id, no_of_separation): - """ insert link into database if link is not existed +def _insert_link(session, from_page_id, to_page_id, no_of_separation): - Args: - from_page_id: id of "from" page - to_page_id: id of "to" page - no_of_separation: - - Returns: - None + """ insert link into database if link is not existed - Raise - NoResultFound - DisconnectionError - NoSuchColumnError - """ - try: + Args: + from_page_id(int): id of "from" page + to_page_id(int): id of "to" page + no_of_separation(int) + + Returns: + None + + Raise + DisconnectionError + MultipleResultsFound + """ + + try: - exist = self.db.session.query(Link).filter( - Link.from_page_id==from_page_id, - Link.to_page_id==to_page_id, - Link.number_of_separation==no_of_separation)\ - .one() + if session.query(Link).filter_by( + from_page_id=from_page_id, + to_page_id=to_page_id, + number_of_separation=no_of_separation).scalar() is None: - except (NoResultFound, NoSuchColumnError): - link = Link(from_page_id=from_page_id, + link = Link(from_page_id=from_page_id, to_page_id=to_page_id, number_of_separation=no_of_separation) - self.db.session.add(link) - self.db.session.commit() + session.add(link) + session.commit() - except DisconnectionError: - raise DisconnectionError("There is error with DB connection") + except DisconnectionError: + raise DisconnectionError("There is error with DB connection") - except MultipleResultsFound: - print("Many rows found in DB to store link from {} to {} with number of seperation {}".format(from_page_id, - to_page_id, - no_of_separation)) \ No newline at end of file + except MultipleResultsFound: + raise MultipleResultsFound( + "Many rows found in DB to store link from {} to {}\ + with number of seperation {}".format(from_page_id, to_page_id, + no_of_separation)) From 1639408b54bc56f31eb83790759d1905bdd68569 Mon Sep 17 00:00:00 2001 From: tranlyvu Date: Sat, 30 Mar 2019 15:24:40 +0800 Subject: [PATCH 36/36] Update to version 0.3.0 Signed-off-by: tranlyvu --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e637c53..32e2418 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ wikilink is a web-scraping application which purpose is to find the minimum numb I discussed brief the motivation and an overview of the project in [my blog](https://tranlyvu.github.io/algorithms/BFS-and-a-simple-application/). -The project is currently at version [v0.2.1](https://github.com/tranlyvu/wiki-link/releases), also see [change log](https://github.com/tranlyvu/wiki-link/blob/dev/CHANGELOG.md) for more details on release history. +The project is currently at version [v0.3.0](https://github.com/tranlyvu/wiki-link/releases), also see [change log](https://github.com/tranlyvu/wiki-link/blob/dev/CHANGELOG.md) for more details on release history. If you like this project, feel fee to leave a few words of appreciation here [![Say Thanks!](https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg)](https://saythanks.io/to/tranlyvu)