From ba497cb359e6b4bc8fdea206c5c0c146eec0b130 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Thu, 13 Jun 2019 16:19:36 -0700 Subject: [PATCH 01/44] [GH Index] Initial commit --- NuGet.Jobs.sln | 11 ++- build.ps1 | 4 +- src/NuGet.Jobs.GitHubIndexer/App.config | 6 ++ .../NuGet.Jobs.GitHubIndexer.csproj | 76 ++++++++++++++++++ .../NuGet.Jobs.GitHubIndexer.nuspec | 20 +++++ src/NuGet.Jobs.GitHubIndexer/Program.cs | 16 ++++ .../Properties/AssemblyInfo.cs | 13 +++ .../Scripts/Functions.ps1 | 30 +++++++ .../Scripts/PostDeploy.ps1 | 18 +++++ .../Scripts/PreDeploy.ps1 | 11 +++ src/NuGet.Jobs.GitHubIndexer/Scripts/nssm.exe | Bin 0 -> 331264 bytes 11 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/NuGet.Jobs.GitHubIndexer/App.config create mode 100644 src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj create mode 100644 src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec create mode 100644 src/NuGet.Jobs.GitHubIndexer/Program.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/Properties/AssemblyInfo.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/Scripts/Functions.ps1 create mode 100644 src/NuGet.Jobs.GitHubIndexer/Scripts/PostDeploy.ps1 create mode 100644 src/NuGet.Jobs.GitHubIndexer/Scripts/PreDeploy.ps1 create mode 100644 src/NuGet.Jobs.GitHubIndexer/Scripts/nssm.exe diff --git a/NuGet.Jobs.sln b/NuGet.Jobs.sln index 570231d39..a178305dd 100644 --- a/NuGet.Jobs.sln +++ b/NuGet.Jobs.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28902.138 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.645 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Jobs.Common", "src\NuGet.Jobs.Common\NuGet.Jobs.Common.csproj", "{4B4B1EFB-8F33-42E6-B79F-54E7F3293D31}" EndProject @@ -147,6 +147,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Stats.CDNLogsSanitize EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monitoring.PackageLag.Tests", "tests\Monitoring.PackageLag.Tests\Monitoring.PackageLag.Tests.csproj", "{D3F1711A-25AC-4EC9-9971-4F838BCD2A07}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Jobs.GitHubIndexer", "src\NuGet.Jobs.GitHubIndexer\NuGet.Jobs.GitHubIndexer.csproj", "{42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -387,6 +389,10 @@ Global {D3F1711A-25AC-4EC9-9971-4F838BCD2A07}.Debug|Any CPU.Build.0 = Debug|Any CPU {D3F1711A-25AC-4EC9-9971-4F838BCD2A07}.Release|Any CPU.ActiveCfg = Release|Any CPU {D3F1711A-25AC-4EC9-9971-4F838BCD2A07}.Release|Any CPU.Build.0 = Release|Any CPU + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -450,6 +456,7 @@ Global {5D10A44A-87E0-48D4-AF84-9DE1BCFF0CD6} = {6A776396-02B1-475D-A104-26940ADB04AB} {19EC1E99-89A8-445A-8C22-C1B0CD8CC777} = {6A776396-02B1-475D-A104-26940ADB04AB} {D3F1711A-25AC-4EC9-9971-4F838BCD2A07} = {6A776396-02B1-475D-A104-26940ADB04AB} + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643} = {FA5644B5-4F08-43F6-86B3-039374312A47} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {284A7AC3-FB43-4F1F-9C9C-2AF0E1F46C2B} diff --git a/build.ps1 b/build.ps1 index f7e4e2890..23f6bbadc 100644 --- a/build.ps1 +++ b/build.ps1 @@ -102,6 +102,7 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { ` "$PSScriptRoot\src\Validation.Symbols.Core\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Monitoring.RebootSearchInstance\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Stats.CDNLogsSanitizer\Properties\AssemblyInfo.g.cs" + "$PSScriptRoot\src\NuGet.Jobs.GitHubIndexer\Properties\AssemblyInfo.g.cs", $versionMetadata | ForEach-Object { Set-VersionInfo -Path $_ -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA @@ -163,7 +164,8 @@ Invoke-BuildStep 'Creating artifacts' { "src/StatusAggregator/StatusAggregator.csproj", ` "src/Validation.Symbols.Core/Validation.Symbols.Core.csproj", ` "src/Validation.Symbols/Validation.Symbols.Job.csproj", ` - "src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj" + "src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj", ` + "src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec" Foreach ($Project in $NuspecProjects) { New-Package (Join-Path $PSScriptRoot "$Project") -Configuration $Configuration -BuildNumber $BuildNumber -Version $SemanticVersion -Branch $Branch -MSBuildVersion "$msBuildVersion" diff --git a/src/NuGet.Jobs.GitHubIndexer/App.config b/src/NuGet.Jobs.GitHubIndexer/App.config new file mode 100644 index 000000000..df1d0c64f --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj new file mode 100644 index 000000000..63e84c9be --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -0,0 +1,76 @@ + + + + + Debug + AnyCPU + {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643} + Exe + Properties + NuGet.Jobs.GitHubIndexer + NuGet.Jobs.GitHubIndexer + v4.6.2 + 512 + true + true + + PackageReference + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + 0.3.0 + runtime; build; native; contentfiles; analyzers + all + + + + + ..\..\build + $(BUILD_SOURCESDIRECTORY)\build + $(NuGetBuildPath) + none + + + + + + \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec new file mode 100644 index 000000000..6fa59e3ef --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec @@ -0,0 +1,20 @@ + + + + NuGet.Jobs.GitHubIndexer + $version$ + NuGet.Jobs.GitHubIndexer + .NET Foundation + .NET Foundation + The job used to index popular GitHub repos + Copyright .NET Foundation + + + + + + + + + + \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/Program.cs b/src/NuGet.Jobs.GitHubIndexer/Program.cs new file mode 100644 index 000000000..a1308c9db --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class Program + { + public static void Main(string[] args) + { + + } + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/Properties/AssemblyInfo.cs b/src/NuGet.Jobs.GitHubIndexer/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..874b41608 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Properties/AssemblyInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("NuGet.Jobs.GitHubIndexer")] +[assembly: AssemblyDescription("The job used to index popular GitHub repos")] +[assembly: AssemblyCompany(".NET Foundation")] +[assembly: AssemblyProduct("NuGet.Jobs.GitHubIndexer")] +[assembly: AssemblyCopyright("Copyright © .NET Foundation 2018")] +[assembly: ComVisible(false)] +[assembly: Guid("42b1eb66-58f9-4d9a-8e23-ff12cbf5d643")] diff --git a/src/NuGet.Jobs.GitHubIndexer/Scripts/Functions.ps1 b/src/NuGet.Jobs.GitHubIndexer/Scripts/Functions.ps1 new file mode 100644 index 000000000..85b36a8a0 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Scripts/Functions.ps1 @@ -0,0 +1,30 @@ +Function Uninstall-NuGetService() { + Param ([string]$ServiceName) + + if (Get-Service $ServiceName -ErrorAction SilentlyContinue) + { + Write-Host Removing service $ServiceName... + Stop-Service $ServiceName -Force + sc.exe delete $ServiceName + Write-Host Removed service $ServiceName. + } else { + Write-Host Skipping removal of service $ServiceName - no such service exists. + } +} + +Function Install-NuGetService() { + Param ([string]$ServiceName, [string]$ServiceTitle, [string]$ScriptToRun) + + Write-Host Installing service $ServiceName... + + $installService = "nssm install $ServiceName $ScriptToRun" + cmd /C $installService + + Set-Service -Name $ServiceName -DisplayName "$ServiceTitle - $ServiceName" -Description "Runs $ServiceTitle." -StartupType Automatic + sc.exe failure $ServiceName reset= 30 actions= restart/5000 + + # Run service + net start $ServiceName + + Write-Host Installed service $ServiceName. +} \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/Scripts/PostDeploy.ps1 b/src/NuGet.Jobs.GitHubIndexer/Scripts/PostDeploy.ps1 new file mode 100644 index 000000000..4aded4737 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Scripts/PostDeploy.ps1 @@ -0,0 +1,18 @@ +. .\Functions.ps1 + +$jobsToInstall = $OctopusParameters["Jobs.ServiceNames"].Split("{,}") + +Write-Host Installing services... + +$currentDirectory = [string](Get-Location) + +$jobsToInstall.Split("{;}") | %{ + $serviceName = $_ + $serviceTitle = $OctopusParameters["Jobs.$serviceName.Title"] + $scriptToRun = $OctopusParameters["Jobs.$serviceName.Script"] + $scriptToRun = "$currentDirectory\$scriptToRun" + + Install-NuGetService -ServiceName $serviceName -ServiceTitle $serviceTitle -ScriptToRun $scriptToRun +} + +Write-Host Installed services. \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/Scripts/PreDeploy.ps1 b/src/NuGet.Jobs.GitHubIndexer/Scripts/PreDeploy.ps1 new file mode 100644 index 000000000..cce8f6cf9 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Scripts/PreDeploy.ps1 @@ -0,0 +1,11 @@ +. .\Functions.ps1 + +$jobsToInstall = $OctopusParameters["Jobs.ServiceNames"].Split("{,}") + +Write-Host Removing services... + +$jobsToInstall.Split("{;}") | %{ + Uninstall-NuGetService -ServiceName $_ +} + +Write-Host Removed services. \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/Scripts/nssm.exe b/src/NuGet.Jobs.GitHubIndexer/Scripts/nssm.exe new file mode 100644 index 0000000000000000000000000000000000000000..6ccfe3cfb85f0f7126648dceb7ac93d58b025a0f GIT binary patch literal 331264 zcmeFad3;nw7B<}3AS|&FxyUGLt5KsUMxrB#Xt#8rBaKF7bAz}cIxeUkS5&YQG;?Wn z#@%tjaUErJW^^P32tguAKt#X=#09tU#<(CV22t+!Jg4g3+qV;f-~0ai^81mlTXpKG zQ>RXyIdQ_l!pe&vN14K66?S*W3R#KyGM z9ePl$^Z%)vo~iv1&rjX-uiBUK`{;L*Yv)UPVeLf{|3vK_^84@Fh4TA~{GNWv1=EO; z$#HpUAaLRAyg*erXPlF^IdEX0S9T9nIy(^P#@Yu0b8E5!fj$xpBc#Ge&I{lV{yD#a z@;njNW1r_yZZn-sIxi>{i1Wj|I)=f(9oJ?D<}k21J1}qxu;0oKd_I5?|Cj$l*@4<_ zJI5HDxaj%>l8?wnnPiA{XQQY63k3odgRj2uyu^8dz-yNy0hoa+@SBg{w0~i!$6!Sk zcP{o@r$~p{|g5K4TGmELa8I_8rTaI_jaHQ55D@Et1m#3$ON(pRN()M9qH2X z|6ln3UIKCRgIMzGNhJ+&v!yI`MsuXr4h91yEoC($oALiG{9lj%oABSl|IOA_XJ-Yf z8xq6K=2*=+h2_@Scs84@aZ>_;n7Q7XfycPHES8E@)OE`agsp{8rkDxJT6U{gvT@R~ zk_P#Cga5*3p!iVC%)952Y|w3p0eeDSe~{X5Ay^Pg}7R(62e?*8yA6E zC;#0`WXw&cKd(vU_Na*#7Mtz1(S46>2(PJRZOm-8jVwIH%*~X@ zrBM?k1cj)#os$)4ttWD7TwyVJs%SzgJGE`R-8VasUI6o@B$E<(jiJ_s^vqV^J-OHb5D|HSkXtrDb#bexD&6Z)8x?q>P-^=)ja-*6WG^A2tfm_@s`5I;?ms3va(Emes~&&Qa;vF_sk{fNfb7W9 z_FK*c=fsJbs@S-xc{a78n@Z7R=A6Qrq$_#D+NT2evl`jCB9kNMp1myi>dmS?;T=u3 zQQsR3nY+czx1zyU@%$>D!Q8pFF%Yb>4nGF7A;50BVRzt3F}`_%!}$LYLIbWli=4Gv z+wqvzau~Y8*0E$A<9(ntY}`aK9=%Q?|Q>gRlNKJMngHG7E6 z89NBEW`0etHmEza(v0QS^aAgsy7o1RDzTG8^D2vgWDVYK+m!C!Rsw8LbJ*yVq4hE} zYe`R(sQ52-`9GNaH?=+kgCeRBGgnxB-PC-9tV8g}r$ti%R@)t5t=A!&W6StkTGG&3 z1TLJo@EPN2-)ULV!-`|(S5}W-#B}wpl@11jjAmiagDDwTI288N`qH7GDWk0hg;bTi z7spd`3h!V7IwtC-3?%`=s>@0bKYu)GwH~HxEY*SJ)(Av7x?vSFeWkSzf10iSXHqWf ztrPI*j2=a7xtq^MMd_irTxi{b*_a-ynNe7dE;JyPIzAjTs}xt}^!(ubNdDBE+#OSs z4Y|Sj>uuvj^a$db&AKN%Egfb(+v-%8!AHiWF?bgqtQYYI{b30^QYvo~e!TwhsVE%T zFu_J@WDm_^G&nzZ2U>CmdUmrFM~@25@4X|IG9G32oRw;Tuu7!o-CH;lm17QMI|(^X z?}K(4kZtvoWJl2F1}yuGH0HL*gHtx`y|fV;U5}?JW0`c74c1Nb0vsuOIjxNWFqRxq zcpsX!GGB5s#%cM*zfm_TzvPHLG*#Z8B8JkeEWw!ZJ>0eh)EO9|(rq%%)}woI6d{p} z&`$Xr$XhBodJiWbk_HhB#)H-R0wF3tQ^OaOASQ(-JZ`9E)~7$SLx|3z1;?SJ7+O%3 z;%GPOI|1rl9y7mZc?zL?GU^jAoMwf9kY05pP)(wnJ!1hwKH-naGVMjTc0RtN4H8hqQv1&INhVfLyk*xxQ8jFUmyo1pg z!*vA=k?oCYA!k%OK;a(=e3W)3H0YPp&_&vl?b(SjV8k{~#$cp~+L^N2+AyA-r`^gz z9&PzB6xQ`%{~0lfY2-ZYD?5t+T+9@W#r9BBCv*vW{Ox zgKokrRO}A}F-GLEs6v0%q}dAUnjDKqdM6xvda@eVPJ|iY(A##b$Y7KPnQmF-xZH`_RugvrJt*NLPI^QkJK2@@mZfL3gNzu+gdZK?T5Bsjl4IRAayutj+-@q?x! z!=$NDjI+Seq*AjnIR9PLDQd11N5?kaCo#lVQ5Qmm5f2&==+s7mf;-Iyod#BlBj-$+ z?6QOYS35l$ovaGZSZvTHIMyf61fKF}!$?Uk2dvF4jl z`Jn-%Sqnd9Qah#n$5`r&Y<7>KGI}6`ZN$-TFb;j$gTp0jBeek-DytjZsF=ChHjY8^ z)Z~o9aG>(QSZXA47d(%K5^8#wbu2?fWj#z?gdD@5xmIZA6ovuDRJf#hrz1|XaM?p; zxmdC&GWY}NXwsCnaRP!R-_S2_C*;vn(cs1WGX#xT98Hy?IaYg3xF@?P=^{&{4Urx( z-(xylN)C^<@hdb~gMWgc223bGrx*gW&@|{p)O*apZS}{6n5Ai1i3Y7Sl$FI2QCI zb75uyz{&HK(1M&!6X#`?@Cp*OgdSp$T&n^&M;ubShsY?sd86i1AN-|c%zRIkV6I#! z0%ilj1CXwInpeah5u^bhqU*4amX}|FeDJ-H#o@k#e73@nYVJ28h(?4bl_np^BF8U& zANEVg<6!4xO6Fk?`&I;j>a^@(j!dMDf#@&7vty^G;dkKuI1hh6h0j(!!iVh#7J0Ht z$`abfcUfKV|2`76Y?dIX_=je+*B$VB7jF z%eBBH^*KuGkfWT{&(Bm8oZ65 z!e2t2|Ksn)e+v0TLW=(-KK}3W@Dm75rO8Qa0@QbjzX5#0FLR)!jZ5Ks{g%BA$BdF|!s&Se3kW%BjQ#8FH8Uk*Yl3ZKn zWBtx_@a-AkH+bNaGr?)=cLjb_SKtFZ@T^R5ZUOEJ{OxUNV#(xu2@ftOqva{u;cm-lrDDv6U2!fmgmnco%sQ^@9uCR6TrdGHAZ4>!A`zDR*mkgrM0 zx`IC1gZ_hprmb~*yrVTcf&O-@=Gv9{b`dzrw~Y-*)D2pXV1`g=8FwX=yBv5aK11d= zdhk;atRKqu^Q$`Tq;d$*i#iIX*nv-t|B@yA>NC1~dHD8E9oVNvy4sQ|zggPOC-$|- zq`R@8Z}*`uLaG+d6FzkG_V!wz@182ZsUvyg-C$(CkO|31$KEq+w%~Ep1)tDI{vS)MR4Q zsP6*%+^(==9`+HL*tF>#vB?^z9HP%zU15KX^-9h68U&SywM-7|6x%10r@7n-6+9Yk zN;Z5lx!c38RM=d->(ED08ngOv$2z!?#Uaf_#C*qxKEy-cQ=wzD*PIvcn)9|Tnsb(& ziTy4TwNw@&7+HKA05dwN_>UrG6w2P}Rl+t>4ti&Dx!yxRm*|VQJ&D{a*oCO^Qc}kH zPe(IwsK1OAKI!!H%GmaOmZ*qEP~fdr(IZrmym0x za(UQ8zm@1T=AIy&3ZS@_jsltB2X65-X~Y9Qn7|bH5Qptu+x1_*>w@Vokf*@%AKTA&jfZ^&f{`U$H38qQQAaggcoA!cQ4jmk{YHI|hu#N4B^7N3 zeuq*290xsi+LWu2sHO4>f;(x-&g@spft9(7cmUE;u2D{S&}R}_1g+UVNy*2PxiILn zk4HH8)T0as;}0J6j&HI8b-k2D5h;(;>+RZ8NA4A(g+4`l3%R67CTD;zLYfxI6A1d1 z@b@^3*Q9|rXMj)fz@rF8mgv!~h59IKZDM5n=Bi^a>Vv#ky5X=y@%ZZY>(?&-pCDMz zi3W+>5~REO>|w_OUP8_^rSX^WuvfyZ2m;)(eCg9_)}{-~JJw%62e41}&+xuBqlqVr9Az%zIz?1ml|Bc@{y@{NhdwO`2HJmrieT zu-&~$cpd)byb7;`QkFnftfviwa75i0Bk*p$($;48atffvWkTn9&|kH(XAP%OAe@UW zT}#ie3E@+j#mJPV@qWoXheR!z6oU0!ApwH8Gh|=c&0X}Vv;8Iby_r#734;(Md#+;A z=t#rvN^!PXUDV-oB-D|QR*KgCxJlw%9Zkpi{*|6gZXA~>F42lqC*haP=+uwD+`NHLSErP12?qr6> z*{*|b8!v!d3e+p`r7JTA^S^q~*AZG!HQW25Ny(s}bszp12cL8q(8qev12drq?HYRX z=bH6Spg%yGZp5Vsik2@90~5VeRRNoF^eoi+%%}}Mrtf!X~4S`o6AICNX_z0t>akso`b z4|@*}yZuw)8^ZPQ9GQ29`xfY>_>{=QkGl|wS{`#1E|(pYJZNay@?FRy;m7vNV~U3z zRoI;`m~_&H1O2#u{pjxDe*KA(vupA?m9t+z8kk2Gc{NY+THxverbQd$>scA1f{EYrXV&7`M{@mFM&Kw zARqWxe%OP(m9S#>)JmqK;He3=Uq$4oVOnoLC9ME}t7I|jwsD+?zn{W~Vn^yJc1NjE zrsF71z=22{V@$)|^0CK!CiX{2(~Y?d!N{WBlqK=|i8wkWej?Q{!%w^~4V;NS$;|Z7 zFG5gpsk@FcC8bS%x%e0#mWHi{I7lW{ieu0iR#{IF~tGDVjJ=Y2u$b(~T^yXSeZqX;WXtkr|&ihOj@ zgnRk1{i>JeVSn`j6kFyf0PGKL#Y#aYe(a1js71(?#(96Apr1ve7RVd~9nRCNREI{S zJd|kg-jE;LFO@MKc5x>5@k%OPb(-9+u)o{r^8Ge~J8ei)L;e%wA`ZPbU)q%6 z?cD1@-#}>q-vAwl;B)TEY&4&6E0P8|9B(!k%y0H5W7PwfECvw^z;AKC?Yp$Far!FrkVNe^=R zqqMH_E=!xVz|v&Caf74a#5{8mIgZ2^ShjtHG|x- zfc-Pi7rl%`Es_Tv*wpwEvd!5rMd&vuK0|nU8gx@8^dt{@1nB^|A0P!z_>nq@Ovab6 zARlz!uAqN=&*l141O-TQefX}qo{ub!Kss>!cMtk@LhJKY`mDaACJ~uT;k21P=#8B> z-A-qD;G>skilo#7KWyjVge745kdxCD?6>c_;#h~E%Vd2}W5$M>y^bGyn&$B?9sBI6hZ}d zWlyYke7yTtGu9zd>vt`JK5!m3-W9mn75KFt_}KzZ4}u+&l5k|h*6b*e5&!A;U{qXJB~iw;1s|tna~${&}D>{ zSpx`10=uN4OEa~fS&^9S>N6f5+_UC)@G|3OX?$d}&y2~Re zlj|3ess(Znf(ke_0k7<;6TgDjg&CaMC3?AseoQ7h59RI(J-RvM7Yo@oJKrsLQ_3!@p7C zi(5AeFTC!`oSp7~W7#F5piwi8JHo>~fVf&M0iG^(?PHAEqD11l7(c)1w&=KPMX z(68~(Cm|SFbYe%=Wzxv9RMq{C-C+)TYQnEfhYEUnVtDAkyn#CE4b;fu-Q7}DeKdL} zeRKu$iDxJHKlt$fi$pD(hY?h*sl@yH&dpxtNB46-(L+Bz9UVinGL5@3UAy_w{k(5m z>GJ*&f-diZ{(B7LzmRLElKGp5J~JI1iud~tSDo%h_e*Akhkighy6}EH+IClx`Ef-T zynl>D-KvcU`X%G8m{VmsJq7ikgY8U4M{DN2$>_~q3FoU4a4ZLoZWz+pg`0Pri>{}( z%%+smHV*P|d%C!(2?Hr~H60!OeHTOBM=e?^!2CXQ?$v@sEtMA$RQ(2j!FjqJKxxSR zqtCv~PQ%3r>VHG$Di3#p!Udw{nFoK!OBXyJmWJJdXT!t(r5QZy(K&)wJWHqK>)5;MHj9zVVH<;xzagJPqLxU?MHfVh zgmc~wug_7U+Ly!nrvu&z=W!4FC>>)A!HTS zeX{v#xy$`(1Q+w{a7F=a2Q^TP{$AHBZ3*BBb^7$nR1f8JOmzmsE8>?%VIFG$q}&~4?84BlK?dEP zpwFCpEkvpo%UlwIWg06nRE|38^@h~=KEj;O09=!fzAO{{To3&u1S3mmP#}6Brl~s| zQ?_TP_vbikzTQsVKC6v*C*OZW3@Zp)Tp*2P&1YN$o?DH3(V zHX#U9wXKyQ8Zy>V-ycmw?jV&rJmkxXtj+rG0*-bCe542555dSHz3LC-G;Pt_wVgBm z`BE(sQJ2!qK9Q_Oq87<32s)!^N5;2wl!)mE_S=?gJ>atmED8<=WenZ51vt`=?YAy_ zdDwOXgtC}A0@wTqh(xyfz`kEuGPe5GAeZR5h-B0eK9MX!q87;$2rACeU8sy|(PGp` zeso_Ss!YQcy#qLS`v^oB2R-ak1S5;-jB9LH(KB8P(V5AkvV41_mB2(F_dAxj+<%6k zG-y4cU3GD$yJ7)^FAusFS<(dKhknw7zC%O9;1c2Y_58ED0w3jp4?-}auNT4891WnC z(VQxv&`qL_zMB79&(nvSg?&=^62C;EZqzprtXJOWsE^6J2wqG93Q`U)$J34q|1ff< zN#|o<3DsT+mm;W)iz6NuA$U|AuLz&w9PdZ>%l;4#y|+Tg2my4r0=q*02tMUbd2d0Y zmd=X^iuuyjcf^ySd z5lBP9nafMTM6D(?v1x|ukycsH3f6(ltSIhxp*FI#byhVZQ1+qlphY;9cvcrXr6yn! z_6Fh!G#Swdn(yKPvDPWXnUF1)K1Izpk;K@ciV7TRvO`FA#^*=|5h?mh#J3CiQW^A0 z=L;});xTyA`M&!%BaV}NtC{a3M1ai?5o4}XExr;_OY~d>*(4I8d4*ZW_fS}h@(GD5 zNRPTxqHySgB`T_$Bx3UGPQ~?9F%xLKC@?0MV}d!kS9l#S^Sm{OP?Cv<-=D+EQa0GwmS(BwL(Q9t5bTSH~4cn*$RIClpRZ5 zlO3!c&AUQz-(yT_PGKi-0c5f<6gd~SKl)VYKUuA<=~n`mtZm(p{w()ZQonEtZYVea zujm|BDXw6phOa_N)C4=xOI*QyK%xXL7rZJp%9s4;!`6$!*h&E8YJeFMgGpfys?`|s zGY9>~{gj&nxLNn}xhPxir+oeh8Vb2_eG^jTrs8`GH=zY^7yYf!;uf6O`)^hNV#A$~ z)1#@ArdeC_@Z1nfDLQ(b<@_^=e=6uel$VyYpabza#^>oJEeu0#(MhIH12`XtQX0jG zDdLrwh=5S`Kv8I6%+GPxID1D=mfg_S-!|?*2zR{Jv9l%HCkCS{c$M57@CvqJ>WaY? zOdadz8U=M9ua(D(p{0u@uol{E{TFRw8+!uK>xTQ|i86EZ(yqAe6*jk-1#ceDD>uWh zP)yQak4?qGb-ch5_iTExxIHs3DQ1rsg(e%rQ>@#OChc<-a!+ z(0;hg@nT=^iPd)hiTuLm2HQ9v$Swd_5jfpC11DG(&en)S^k&k-tf$)NmG7cV$na+|OQCGp?{t97F@+ zxQ;M%#zcH+U_AsLoWBj%As+%ns`rU6ISrb)797TLr#5s5!fR-cm}|<+SU!UJ99niy z{t~>ockF`K?AKy7@y7DDPl+2w1hgH&#&Xeqm@ComC=xMI!`;17d=$#sE~UR`8*5)= zq1eA(!@#UYvNgr0du)Ub{Nujd7*uhEb-tKGYGL9JyuYt``8?Hr)?i{egS4Bq#$v!sm6 zVPz@7|DuW1jrx#Xv)n(YE(*tm$es?ehg%1CqdMPkh}gGp{v`w5D24VCtX*UQdjv7h z17=%7nH{_!cT%}^YZgmmrzr=GZ9I$I9I%H%6~y+0o$JskeHfm+Z`jKaF0bBLxjQmW znu(0m#)^v=fI%y^jQ|P)HzuyRJ{O4W3}l|Nc*ELA<1(v}lA1CZs)eHDzTrMBcz}2c zW#+ard?&*E3bqsH@>wNwSsg}dFw2f&8{bmFAU#}lybQ@B_Jkvw$c0rKL3KkHYKf#y z%d?H;iZ1a5+L9uu*U#_7%{MW$Nvya^xcB!zWLkU*8JN_F1V_W|x)K0>YYeu|iG>me ztE#{nzxx4Y9mjK4;QDPS3p8mO^pa|W;JHi;O9z~<634(KA0Nd{)I)I-e|*r=sJWrk zToD~~4&JXG1W7(L2n&TPX$OEGSkoS54~?1)ppHcby@up9?KFVbE5$57`-f$oeZ@74 z4N&ZuS@7>;abfZMU_64M3M=(*vAsfs#QqktK9^OQ|MR;2AFs{rt|cH2ma3s^uZzuJ z%r!u}0BA1Okd0|r8_*Ik(g6SlUtMOt7c9kSQcwc=;HyxDzf%?Hb}A{ejF^4Uy@+*W z$zQj;P|3!smf)+b!1_OC2kHtiq6P0=20zTfv5kocBE6RBL24cQn7F$n<~WIDPdNu*?q$X^CICQP`F9N%X_8hTM-s=PRk2_^5Q4wqq? zcL+vz_M)|8a~jZ*t%(>9lvv=j;p+7n;;x1w=PZLf#`DH{WV2!>UT?y^^=m78rB2G? za4}iFv#`P%{UGvJf0O7Je6=)iM^4PFZl5A^wbymuz^jfd;2l$ATrwN8bq<3PC&`;m5}vhQI*aB{!e7d z6IIEIZ@8HvmK(Q$Z!i{UeE{z#BKH%>UDrcc9o(^;UTNiAr^?BOFeQU(0o-FdPim;j z8KTSS%W`OhTaQ%b3}ZPxrJUu+kWrr}5#`h;sLKJZWN_+pdwMybk@`H}?v&Gn7v52y zHQ=DG7xKorz;7F8x`jc(>qbLoaF4pgeN&SRRMD5UE4xQ)USJscA3}aGSJxX5$^;T9 zWE(}u1Y1>gvZ!6bRho{L#zz+rxa^`JC>kC#RZg0Zkh(nn=9@T&KIgzv$*1zzCWYrQ4l>=g_f{V~0 z(JyHS;ZGKZT0e(wM@>@-GYjr4=HgF-<}F_;TRGA;CJU}0>FMr;h^eFJ6seU@-oRh* zHKv7y8O`Y4WYjdthPCQ%w49ru_`#~h$PJgSr?gMA_1b7U)rmp2vH4kPc4p9}ZDouP zzcrWcsC-hY%=}e-Su0kf>XACW2i_AG5VkLy;%PjkiDHn|_$~ATa4ssZN6$|ok&KpHLLqVZMA2GT4@U;o zP8qcKfL7G+o5bLld7o;uwTS~9$vrtr#M3uE>;*zq_CV(fR_*weHeZ2bKv24ZMiiZ# z>DJ>xT>BCm-k#V;HIa6rxSSsA$u)XV?yo2+zk8`VpFr>r!z^OV^0gnVu@h zf&J7?B^rh>#Rpl}z)ZAVhpsOr@T+9qeFbC@phop}6mlv0|I=i^Sz-AePqwj6;*?UW zw%vdj6#QqvcV2KiG)z~V%YW>y7yaNg@y&3J(E)Vxq%dl*11N{6JG2HWS?r}>F6pmp zpXf_)Ol$BrYO;gz8!gunKF*INhsxyn>|1wLmo)oUz!;x>vyD%lq7>3a{Uu^_g7Pd8ei&MSFU@MSWRqq~E(kg2Rd;oUAB^c8)RALOWx=hAQsm&DO0Mh~KOPAoO zv)T+(e5TdrM8;&)W)DS`*17+ns2HR9R|f7g*w}c=A8N(RC)`;EBRuV7y0kwO#p#)I>OgXY0OSwHf;h zqw`uH1K{xXn@3bP+%gQGAT%xT1(DU=t^nZwA~*ezzHnr*JOM>sw<*>m$tuLSRQXL- zC5kDiI!rkUMrJK?kY85FP&fE)Ti*)SHzz;IsxkT58M>o?hC111lY*;#3qau4co$NOu>zhvYiXyuOWL%R6PoMq0qgpIMTupKwXdRb} zVMudSI=n6UUk0sPr+@+ecFTb{fZIkG?=)8w2g`g+K-LZ0Z?lzfDw$kdd4P=}DO5AwN9 zuBMWgBZn)+xgZ5u>mg}mYEn5Wo$M(^wgO~z*W^o&J|QeU;lp}?SeT(+FW-~G{ z8|DZ(5@`7Ds>-V^|CN$=rSX;rQED zfX8Ba9E!jE5G`gcQ4qUcsz`I>a5ODg?BNKrxyG(hLzf&z2A z`;`Je#m#}9$@x2~s!1_97Sb0q>47dO#O^1>ijt7d)THgKC715hrTyusLpAAmO}fe@ z48teyk(Vkr4 zj9FhHmu}Qi@l-flO}b?T1fsHys~=?ts@%<8`)FMQR>`CuQjXJn&T6&oWP7(tEXb_1 z#@&N?uZ78M3y39fw+bXK;3r)NJC!^@XO1E>^DPo?WZ4tUp}OhP!w!oGdQVM!mycL| zZ6}?6xTgOST~4j?aZlCxq@dTE3ICS za=`(D-$;44=LD-*@Rzy`k3{^cS%ts1+l|sD%Ow3AcEi(bfPZ^);(1if(-JnDOqp=8qzxaGgY+; za5W9XC)ugvYO7-C3!uZJo5qFFwu343}W^)|*!1|=Vl0rr z?)RnlLz}skqSs)q#xqkZBsH#6D|G5Orp{_?N3s0yGj>|kzwpG`FqFpnd?wP>{(nKX zv;SX=lv!W(fxKX8f>i_2rINp7CA#CA#(N=L8l3-DBz2pCHI8I^QSkO(q2?I9hh2rL zL&GK^ckIF7t5wm}SgH^12Vq@J#h`I;8L%BF|PE_9j|aK%_WAxcUo(>kBG?!H z1)aC`OvZ55#Tbh*qZz{q7GvIJ%rT7N)I}=(@_o*PMe2&$Nm?sc1?xC@5vmlZ(q*9& zL>8AIO}8IgNgTj5L41m}jr6RX>-MdHcp8V;BE*(B&0?#ze6(;gN>f!d~nTSb#m0gA%E_`aX>SL^QC%@Va{+0wz z2v!}5z^t!c16i=DnZ&(ZB}h2sYG zK`>}BR}8I9(xw;*41fGfh_8beP8SaQ?MO_vPpce~Dt}HQzWXas(9zAetlsiT=*mF^ zZ)UUyeyIYtZootv4re||p7xWBW;x_Ijxmx0h;3{XYzz#4MWC)#Vc13^p5n548m#Jp zNS@wUBv!h#1lBBC7>2b8qlaK1AMxb?h*id5{7F0MG_+fy--459k>2@$b(2x8yshto z2;-Kf#i;?4^UE+sI&_V?6L2yZpQ6SmsIl%p2{s@5GT(>LaerrX|3}L znKKU&lH(TSSTK@I_a@UA-Gx=BMmQO|<)vsUoYfiEmm)6;c@o=gWcX4RG8=_B48OJ~ z86GG+75op%0wyFeH(Ec7E(DG5{FTMzkzd7Bp6Gku0yIMMMN8rDu!b- zR^k?%LV^h%0Zca`hW^{{oiI^*C2K!5{7b8rngX#ddTASxW(2`XyZF<$rLS!38Ah(`4snzWgCrb&D6L;TKa zoQp@EfmtvLyv5MNXg1<-J!u`RwHp+#5%q~&-5Er6M486oLW-Z)D4x@levo4LLiGDc z>Xa)WHY=9K?;{2!_R*apvAgh~SABDlZNWJ50qvQHqBFH;9kMJKPazItYHHMPqCt*o zyuxI-2w`h4mAu23d>@l1GI{%Y$*uIy$$c%8Co%a0B%@}&%#)aUCR6KG<}6?GFeHN? zaNOc)T_GalJfT;|6MJm^*FEQztKGmDvFRBz&8lmyi( zb2HBIr09&>)mv|gPMzwW=cd04xAq3wXIP`_;c4c|!N9h6(3Xs$AZIEVe7^=DcGK`3 zGn$e+LctWxK8Mk1D8rd+;QTU(qF5_RpiHn(u7Ch8(jXu(m!x4() z8IZ79xy&A^{Q&mbHon4M1L|ioHy7|b$!#l=X*##~fjaj*l{A*Qxz^uF?xl+4Fr8a` zMxFZ#l@w%dZVPmhI|-5nF^X^LJ6(vcTW8|i4|yTJNf_WMzAGrc;pm=@_#WS#;#>1) z5x`&ZK-q9Nz>&?HD(N~T>1}`y5VN2h?9fge363)D#0w-ijr{J8ROzVxQRdGiIGqHW z-*ySoN101Va0Urhdjw$09c5x5&{}_rXgyZVjz%0>aaA9w1esFX4!sFhMzGre*7@k; zi0(jf9WX9;7x&?l8j;>!vVX8{!DW=*Z%|=JdY7Oq+qef*3r=PM`|q>>K<4`kkpgC_ z0v5=Q-MR&rvw%m^3g{yR3;`8tmZNGW=goYb?|ha1Sdl&h--H*a`s{Qx6s zWY!IRHsGK^uo{cK%9&L8xYlrkZsJ~a?C8Io20(gH7vw#=TQ)ZR5+}t{$IL^J4bpqL zZRC^Sg4K06j{(Bocqd^iwgF}HPTb;<008MdMAwWs4_VU~IvWhx#ylipdVUHX=nvng z9#6!hp6`#JAZPHV$1lL)yDf2Z1tigg4u(aO;QTkN!`VsIDFU4IICwTDmd#f8HB9Hm zWOJkz+kIi{b;z9GrB60akDN34TpWXaLi1G?@aM8l1G1gOOJfX8WIqi!ZhWc*$Q!dbTQAEfYzotl=QsG~?~uCg|;7c18o zu@@i~+iF*2$5W%S6KCsnZzNobgh=w&iz`RheTUvIW6u==!A00$)$Dv{6syDc$B~YP zJOSxiO|m5pv!TNf*Lo(10X8jIeJVp3VxN;mT$J;0HR{JXjg-(E)Y74CqX&Ydd`J?0 z0GX8WJOa_7gA;qB6Q0nUTWzxsuY$I!qlB*@we1f`I#4C?xS>vZNDvBossx@9)CUM> zs)X-*37C7z&o(Yr37b>`YoyQdouU$&bOJmYeemuWmGB}G^fX{NVxs2k09b@%Py_La z@O}j1-SyB=|5WL_FRg~ZEwiI{mOPhZ4S zbCk!9BlmM5AZz*wvR%19{JTn+4&U|z+_ej_aQWM6EOeB_u{g;bDEw#^uFmQ`l;nAy zoZBc*CtVJhOJ2E<^i^=!$R&73@f5xW2#Jb}zOf{P~9J}rb;&bVPO^5_B< zAXIs8V1+{aToELLgF3`8n>vp{pI44M!gAvLo+5d`%hVe&UNd#WlWoj|yUKSqq)K=v z={YG13|xr>F1JEpa()7@3mvG*hA&O*p#^jn3>^FYCvWoY*bl892T_c-92MLz%8Jfr z*7yZ0e&lgv&IcID+Ex>(idcfe`6WB3&P|=z|!ozXb@Q*XXkrw`vS^8lz>FQ$n_s zaGp*TXED8m^&mtE5tcw7(KcR0j4t5?1W|%Mrm>I4P-GXoCFDv8c6U`-aVXPExK@?$ zG16u25$Dvr63SEw`h>?qjiJtfQr$$(@W^5K%Da*noOM#X+ZNM?g6L8=j!xJnm810|A}@Nw5-o zIVJih15$}=naP~34}NUUQ);4$KR^LKN5Fv~(#JklY9htMZ@0ko%%;T5J9(gG_|nRO z=+Qm7UzwL|&#v4JCTXAKw%o*kMPO_fw1H;!q3$8J1c5OKz+`;86#QoS=vK8+ZZnQtVHK zT_=$qsD2;B+r<6_?_4x-`h-d?xsf_acd7D7+xW*#bUtgkTX!tSP65_b_RS5? zvd7|O6mD4w%F#eEcS~-&HF2=@-0Q60vbz1!>qkm5$vTd1LLH${IIEvf#c88_ixelP zA8-sHw%fUl7E2Y3;uunscY8Kw4mEjlH9$>Xje7dC`Z3kd9QBjJ zj}_zgZB5>3jG`yY0|IJGC1rdLdyBeT_bQqZil%aRZo1u?=&9N&*;oVt74$)(lWz|4 zSyNOd#+3tnjhWZ-9SSJH3*LD1nyb5(M9-5qQSsg)R|LoM%`EiH4KciL8p4|3 zzVIQ}=w4B5tzJqCcEjNdDPpAD4jJ&!DvwhH=cn!Q-Ps76x~z2r9V9k_-j2h{ueox0 z6uY?i6Uwrl#RQR7T(qV21yD~-o`7KvYag;9k9l%$7Lh9W34;XL5zE7~Fh!hfhO7}w zg`*=Jjz~w7(Sf61mto>MGP`w@!xC&3W_{qtnuWP*vVBLe>L8>+vwFen-GmB8YH|xH z6mW40VN(TH?u3i^4Sp_2*C`j*3KzMppjUzsE^vT*rL`Th5H7gxpt;zLV5Fwhk;Pn$ z0N|n(Kb^_qD4?Qs4s&Edx=y*+{2FD^t!;#~4Ooctl~TM9qgY7vMe#i$gaG7$((!ug z9`cdbw%&5$r!7zQIUG}%&Rv0}ucI=v8mVP+4+7twbM_-=A`+^1$~T_sm6_`))d)8v zx6z!VDLrND4W^}$t@D>ibq-wuE?Di;r8>)W(|Y^k*8ZrD+0@oYYNKwowvNDBI8Rgv zgCWHrdEvH&3=uMpaL1If;Bem89*2BSya?|lO5^Lk00yqYaOcY1T2?RY9a7Tb%A3O> zU*1XU5?0kx_Jp=Ql_ln`=Zfj*)h)26_b$ae#Nly9BiVRF%~@tOk}KaUyfS`rC<|kB zna=S9HDs1;WR~xjhsJU-@fPq)34!~eS>d}vtKy>T>BiE5SduZ^IO)L=K@pYwBJ_<(Nc4g%1+$MYL`bGdD+JJ ztH{$<^xcxRJc`8Iy}GMY=tVQp*z_Ven%YJQDDjp;Y3i0N8N_+clFC>is_+h!5xsqH z#^8zuDdeMwxg%nJi+y(_wsnV6hO!_V^9{U*PV#RO6(kP}Io^*(9CR`o<*7LcB2LM| zbBrMpuilgJl9rj5Tx`d3ma5J3rB(=P&oG-qa1vNY6IOzE4WSjbD|6xZRKf_&1i!kq27z*sx-8SC2lG?yGjv)&$^&y_Q8;0k5(H5|9au*5@V(-PV zU&rFvHWq*uquJpLFp&d@T3`ShEc%$-KsY$Rg`C0MvE)CY8MnZDLnOs%V~Vd2PIVZp>U)(%gVI} zvgr!sH^)rUk89Lpg?@aEk4fiR6#CZ#u%ygGjS+Qr7|mV*2ci^RS{8!YuUqTif-e*i z8y+*qhQ^w2l$x)X;kIJO&&7J~<&;~n+JxBfejuc1wkO*!Pn3-K3@V&BWW)eKV3xPi zZtF!k9|}ey=Et*^*J2TP){bc>1p+gkY|BSfTMmd&OgK7hSmkgY5}XOlXw?aogE0RZ zR!3_#Ha~8b2I_`5x&S_kKm^N8QNDu~i1s)E7CRdzP0fo?E&ABg>0UeXob2^v)uaE< ztR-5{l+&Eq&8I~Q4bfZ<<=gks06ebBj6 zmDy0sJATIC8BN`s6HJfj%*2L!8@!Yaj5xxv`* z*Mf6i!2?b7=kW`E;%ysEcy{;SPnx+%O6?h}#!v>MT3KkW!4NGQY%fg{OVV5XV$+J7 zo7=iY%t%>pE~vmit4xG|6I~V2vf2UK=2 zbjJcaxjI)01n<()lWIoxGe-`Hnq&HzXJF@lOg^MYfmkhg(>Yc%vPg!#XCP#@m$^Ki zV-0$fto^o0u=~RzHGAG|J&#qwICD$YOW%+qS*asN3Em zELx{+TJpE-$_pq;X&xCy;Eu`_mgQ;oZows@%*SBe1LB29=Kj|eN!`&{3&4Q}zl5z5 z=EI9bF>Se0hZ%)Y%oz_CWr&(f)3iZZu`9O=c{G-@jq1=Hb??|8yxyQYfXaK|FQP*= zdq9^^mym6=KpR;r@QaXbw1R(c<>LoDL8zwcQQSCXC*r|vc3~PH>DQcZ;+gYKc4--| z_>19c)&j6MtC7Ydf;)1U5IlR!msc4%(3&T1WKk=F)<24uO!y{!) z5&0Z`qBXbnLCh;EW~Rhk*RN*ufSB10JXqV<0Y`^oAm^+WcRRXo}@YS2F5{nG#N5JyX0AsEN2;o#lc z6Kv!DKcr*U0^x9F?hI*HEIjC8d>UUt@r~s&YYDsiUpT(oByg7Cf? zDv!cG^i-+VJ^zN*>-`1JyMpsqLGR_jl65Sa3%lm6{jey-L2|AzG*8-suH`iH!VQRE z6&F*YL+iP1kN^u@`Hq@*a&6sNhfke?TMh3gwKVyka2CLvJt?;v>13fDY2C;-NgSJ6 ziL(qAZ138uhMR z@fl0iS0EY@*mRTK0I-UuFh>Z%Q*1NKP{lHPC*HW2ff-%RiHHta@n5NXv!}5NGQlx} zI9GWA^{5xlS0K>VJzi7K^!PCOsk{mM@}g+i#ykcA=wC6-z*r2nlVI@ZwPIZw8p=<6 zpare-#E>5kJdi^r-+PH|SHN);>VFA5*}A=QRTGjo)wixj11p0x6r)|Vrk=85!*PQs zSoJ457rKI}I~iw%K?gft^)#4Kl0z=;yVT51;IG;jshQ1v5haeA*)tHvzNgd|)tQPw znTb|lojB65P8G_Cr&VZAsZg-`OSGtM?5`@7ttz#hb`$r#!5!_SQjh-2x!B+Wi0t<$ z9&08owFR$*n!+%&L+WrpYX!w4iKBSGeoMu(cByz2OGm}4(9lpkg+2Rj(3NqeAx-h( zsaIzr8eHvM@yL@_Jj$3lS1hx-u{9@1b=HZ|n|%ipsd#1PZg}4Zf`uiwjhhh=#bY`Y zk3Uqr*M$HrwFrT9#bYChqnECDS59#h@8pBp_!TFF!M+SnEA@ z@-&|0<(FU4H0g|>{W>!85>|R3m0`BA82KQT$6gSrJb)npQke_;(Fr5N$OzAfaZ(n? zC^n7Wlo(ui6LXQk1w5XsNVXgn?HSttL4jAfJ z@9dVc;mwu1mzgcrfiLmx4wxQUs!XO|BkQh5hOnMX9L((0`>3?ULMqW3W-uEQF|J_^*_CR?)- z$6&uQI6wE~smbbxfRWKIb4mvI@R7v3*AdB<^;IWZecuZUFy zC`#5tig^R#7A{v}(sPV8^ckAsf(>H%3SOhDh@Azz}DL|^x7-96g4ffAjE+I zCI)M*|KV`Ra2WrG;=ec=IQ1ApIJ7+Pz5k)xV2foM|6Ia9m-5e__~$bIxg2*)4mpcT znAk6daiIcrdJ>sZn5$)Kv62V zMtv+48kGmk-wThWjdAmUh>>bmotmGf-8|Vw-jH{_B-p@e<(_%EA$67Bx1*a=j zut=G<6rEl7LZ~mFM~}Et6#o_FMb2tuKbW%2S*FA7nLMIMf73Q<&!Go>81ePJ5Nd7N zHujY`*?>L~=P~h={)S~AJX?blA4e-bK7`5e_?QAdQX{WyxZq)Q?J{h1 zsJBXS9+7Qh&7n@2Cnz7(_=$mO=kw8?5c8VUKWAIH1)$AXlf#*| zc`BvC+Al3-AyZz1r)L#>!dYvgmpW=+f~;>Ow`b94ln3uz&7NbPf^`k!BD9;eS*Ey& z{y53mCw0aS^u;UAgJ;!jZO2R$m+A=RAW|+@l(<({DCNY56;Z@*rV)P&!)-mJh)aCL z{Xh)H@`3&+Gk9hGQXysDo(4nvZ=Ie|CYZN1-G~&}qd{724{Z(tOB0eIG*dTE` zTodr()&_26z`8~e2hxbwV`gHl%pjgg;!%ot9hAe7)89z^m?A!A=7@7X&B8kUfJp6D znmY4cEI6*=Y6QkO$&{MVCpBh=_1jfa`9G%tokXDV0u-!%0;TDOIDqjZbUe>n>-cQO zA0+XWl{)StxUAOhD(*ZTSI4;Du*d~1#nms;xc4)zRpNqGG!8RIJo$-cyxnDdfMWa$ z^td--n(^GTh4D3vOO3$>z=VA3?on``%=NfLq4J=aBhC__fy38R3O{QwoG(C2K?)Bt zBSyyRjkgRz_jcCXDc zP2jpF?*#lL@~rEyATFMoZS>;tY zj6|9s>lu=xk3Gg;1$37Jn!_0VPu(xht5{)Prg23}Yzzh_a3u`e#@&jwBj8U@6u0SO za3CuynghJUBkKS`a3BLR?*QkDazwmI?r}$SM9>EN4aIpq>eqE?vSM~9sh z(nnodEl4?Po|L~JfVQH?@krrJMA^o)GmuRktD0nv&c~Q^Ev5@8tdc^!Lpe5~$wSt* z$4Oi=K}6aQRl;r6sWV_a;km>y^_C{U`r8T~L~9UPSpS6Nah#&xGjWhUGfLgF+paS9T#vcpjgDjbvR z{Z*wnwoKxpVVu5(UT?$LQjxaxEE?tcr=n9bT45|a1`P);k27!^Gr14E9NS+g zyku+Yl-!cF)^mSjK3S*3YEL7o2&p~&IH$F*^Q0wQ`ep^rrswgm7~7&j=0NPgBIwoZ zuXLdXf(?X^X0OHnvk+>(_GH&@G{cJr$g=y ztxo}31ay2QJ`_HZYtBOQZET0#!2*!x##;WcjBmweuVI)-c0#`r^u0X#`-Q&p zE!;N^?M$}!O$@e;`3OQ74?d=Z;bRhY4S^px_|4X_4x@hrqdZJ>4*Djx=G-;Ca#syp z{{qU1yPdtsiQGOj8BR)G%|<9CJcKYEO+yrsbAm8oHI>=ZehZ1$!k!AZ-4YX$?fWH; z2eWOD>b{Q=J)19U((?FLsB0i|0Ypbh!&D|9qadwS{?f&?VTa%5^6#=!#V!xR`X?)o z^-nM$W#iPoQ#NW7vUW$gshe`zhVj)M+qlimSgtZ2Wi&TYxo7_U6)U^fOt}WMi?n zjVkTM{xiU}e7Fg|Rn08)&Y?Qm*ZK*7O3{gVWq0ChY9g8pOXmMLMf!HnSpd&LFRgrQ zYqA#HWKk+WE(Etl$Q!^%MUW}P;ozO$#A-dGD67!IH9_iX$2EW$oUWmj z$GH(XXdwq}^jWLjF&a#R)6b(XbKzoIKRl$)+#$YP$8SPB4m|A+*@diiNGCBPW6NkI5Z#TpuzDXX59?+!Cn|X5wd%3AL!SrIQt&I6a$s0It;BvMp5?L z;-t(eY;ek^Mo&c9s(+?#!f>QgITa4iCpnP# zJ_*p;PQX2z&~)%M?IG=e@!~CqFL+>ToOT!%My+2_2PvdvEsjDAypQVQj738g+#diZ z^F+Sxl{QL1nl!$3Kxo~-N@M;RmMiB_Y?3W;pKS9J|KpOUCs;NYLd z4eu9)h{9-`Y4{KtyWwE8j00j-OXkEVy2q}6oQ|>yi;=L(9Z)RH1YML91*H;S3pf)} z<3Izi;}irTxFa8QYMPZ^Q?MTb!_+A24G2TV#Fb;vYUZ3m_9r?X>##G7We|2a9;$IJ zR8sy%?euI+EbcxQS>y!7ghqr^f2=?X8v8=@Z>afiXYvVhq}tcn6ce-|WLh4!Tl`M z6v&89bqmD5NYXF@JUj_2UgGa#E<-+kUX#kjkwbJ2rTA5flJ&NYdq+bcOr!r_j_kuF zJ8cZ|l6eO4Th0}15teOCK_&pBbws*oonyqwg0Dzv^J{m!e24au?K42x%IV-8wzBRu zkT`9F+5}(aB33MQ#6xTw*gWjlh)pX9F~Zka*6Yx&l8w?%H?f^w2e)Xa$MEAGP(_I$ zs|s>8n_7Pnxj9Wlj}z4nOI;}%)nxQ3Ga3sJxL!G!c}(SGG~6d0}UeCRjWkJ)g^0n)09io&;hlLELAMdDeS3= zZNHb@=JmSmq8wg%bMIG159V}nDJB@BvvHXy+7i$I#Iw^vkUCoE9*!eeXQH8#7LtAh ze+=h@CgsO!ron-p?tV!w%#HGys7INJV=$kpTa02g*S{4{#n$6RV%7l1FwHlb!t1FR z@tSE^lR6Tm!l;MiHG`uyN6PU)bjbkvf0^q4sE9aMtj#Z|4g5B0y9{(|AzbvVd}M&| zin}KcQ!vUz^9v-e^UYEkXqDj!W@u`C55-4^9T}{81A&;iuYPf3A$l72aAb)S`#g9u z_rrZfDps$)J&vas-Zg?QrH7@5KJ4&42Wl0C6w| zUwGM!cW-bO0`C=~!>V0JZp}X@eO^YPgH^RBV2+hmZDkW9U=a-h=gAS|o7T+_K*msh ze1ZyBm5>g%b}3a@1tW1ilq-O~AbY}4<$Xu;{j*Kbi3*$#K(TxmeScvF&Zw+_4#E@O zyvCbkIGIH=hrL)1Ul^SNf9+I&-dU8#Az@laEIS5}oBKoop037}Y93$_HIq@Y3UGaP zWna-fuy=i1cc|F)*@>bkj%#72!m}UN6DZv_lIVSK613i=@Pu>t=in>|7S#lKFV#JB zhcCN!4g459p{>XN#bve0`6wkc+hg@d)CK3eL5+F~zHJoaNzYMs+R9k}jRjwYWbXi@ z3`|F$2wMqwjU%uF{Xf|l6GKa5i+^f7L@s=xX9EpSY4FuGB@M8AI9SgCp$eBDypJ&< z61;CIj8DW~T82;40h8-cAvC6K%#2WzEAa)ybJTQU3JwRE=-a61Wp_I!5HC>CAW0Wa zdn=$9Dk>S_XeBe8XYd#Kca|Io5@zv-^X38k78X2s3^EZp#0xH*k%!EZG2FWn7OuJx zmEQrwY{NEwLZ<^)@7}cwt}4+!V91&OcxzC)|LFJQWMnADkhFpNeYEJeBNq%(U<&8N zWH>D5uWgI~Q}SujJz)4*ZFky$L-~9^vVYaa%D(I&{v?e5it~ZsyksL@7jaf0#4f@U zB4Z(IF~ltsk))lCuUVx=?odcce$$XL^twJJ5>nrLii*XC>Bja(Y0dul!cxzYwPiJZ zTIYdj0vBTj7sA(9VZf&*hXyKpPEDfgCUPP+u9#@R4k0%2{ueR-ARkM;3Y}OP4}GWi zG(?aM^zu^KZ0}LBwx&N$?X;a%TK#$AWPAt8Z2bR-d-wRLs_XARR|uCdL5ZS>M2U(5 z8bxhlL}xU}1SS>lsHmyfNWGQn45EdaI7EquQCe+lpIWuG-`2KP?N=)pZwVklQMp(Z zZxyT3la31BxM`W+`?L0$NrJV%=fB67BQG;K(Oo2 zejttDZ?%6@@&=mPJBl}OG`WmxN84frjA|0T7;VF7dKv$^3hIeToY1K)R53O@;?gzc zd6?wVPp*;5{U?9^i1st8Cv0ti;mv#l>rbTZNX8thZ(}=z64j9l<%TT)H8Su~4BT6t zyZ^MhwC}h-@?O=k(@lU%XZGZ~H+$=q1#yEtE6 zSrFsq)kw0oG;uAD`FR5q->+R4N{Tnpc8Ij&N)t199A7ywab{iZ@@t3&p@`?J>%B~5 z?5cY2)27e4MDjk<6-ke$*#F;@^k-`xpN<7eq1x ztP@C1vogyfnT~qyQLRrt%fA);TgJbxg4&luH+}~;q{F9U6kHr0q2VO+3nE^P-2&7K z>D$ivs$j0?kx=iJ$YDz*`e+&&dw|Mmf#YDF-j@M9Lrc!$Fxk%1_`dudm>5v+^=oQ7 zIzLT_9DWP~y*ioSXZg1|Ke1l(+o!r0@CTDu`)mZoAJd)*iW1+vZd=ZlNgh0!U39HF zW+-*CqRiIjCIpEZX=X_kf3M@kH$n@rwi@ow3O3tJJV}x$X`?9q?RuvOua<^PBh{vX ziv~|)Ir8qXjk(AZnt5@mYIJNw61x>pyozLMhf(e& z=|ik(xg+*%S~d8Wt7%b1`ubk&nvYiM5xJG^-mVN<9S-KC1tczn=fVv|K^ZgrS=x`f5C6YTa!ALc(5?|zbbeS6F3{_a}ErFhSv}p{Y z0wVw?F5{G7s@^VHEWFi>6|2_{z2KJ|O^@^X2i#~P(;`MB3e`44g})r_5~BRA?eDCl zNDQXIsVi)JQG%%K2Y*kOApi1(Y2#a?Q2D2xfn@I2&LfE)G5um`v_nM_#6pkh08zWB zSlrqlAR|$)Q7`v%oqMm6d3@Pe)Esin$`Tq^Rf)snI}Hc-;^$Wyi& z>VS#3o+fyjzdsnjTGP5fDV~nA1%cXkPO``b+FcIJF(z}{30zva33S8%g(g6SDeymd z7BI~$fd8e6XUWWyF=?AsM{da`>-qQIB%NY?jc~wNm?>0m@UEMV-oo&&_~g^_;_Tc$ zkj)h+Hjnb&Gs=FnUCCw>lZ zre(XLFIsO72ugMKDK#it>K{kj5VRlCyVMgx3EJ}dl=>f+3Vd8CZ8h{RbwN<-tUjeC zXG_g-rL@n`yVN$QR&9;yQ|eup$~}mbf`G)!na{i?bgcp4TWG<@pbc*}$|Q_Yb*%oh zH*BtSshRSxCTNr?asP-SuM7uIrXz7MThg;t@{>+Vju0Z_aZ_CDK8_1M#+PX}Qd*TL zUufkwvg%TPK#=7xeabI)A3rn=cC~-BE3d^Z(z2tOj$0K8muqE^>#9CgOm`pu5PVes z0E5<*ZwA9%$e$X_icflj;U<@g{&$ptK`T*i7^k^rE)Gimi?h-_U^v=+oD_V#l0EWABKGMLTg)s+)DXy85gOcw((;E!q-N%E2k4^LeFlf2X zfnohb14Chu<-R`U|LQ(&5;Pg$!LGbk{oY`>)aB|3ay9m;Vv75CU+_`=YkukuYRHQZ z1N^tcT{yC;+BaoNg5F0BR~zv@ zC0=r=#B+2dbQ&UC;wM3gllqjHmn|{VmC(VDY>DciL|LB_2WCqgY2%O+xEpyl>UA*k zzvF%1Gu>-V@5ZeobA&pbQiLRerQ=^a-D_1CqPoIpO#WE zmk8&{U>+z=J`6VQY6 z{!1S&VuR@WBdN+Zh>rOqzuYFoy?l|C$=lH`dCx?iy;$gisGKD__%oJqVRNg>*J0Hy z6o{pknEcaP+1nh(c?CG8Nq*;emjG?&w}mO^T4O!+m~#YMk(99h0b}1qp91}go)AA?myO&Yx|)kN1VlG-##dhuA7%ZO#GAjB{4A&03YEU{uZRp3Q~W?dz$ziHlk?iW^t8R>Nas2Q0xd>JWoZY zd&Sv0p8h}_s?!0ELY5C@=nWDS2=^}c$DQt+i<8VEnqSCT_uk3`wzgv z604jsx|0(SC2hA!E>9~2(oN*H5gmr3K^Cto{NMjj&6rBU z;mul3()||Rqi43jzC8spAhsc4?aIV~Dij~mw&X7Iwk^@JlHRxN9(%jT-Uj!6i=}SX zGQx;{!*GKRJVbr9E4hU89(^N=J4IK1QBRgdxh(od7A+LiGOQ=d_i|bEjVxLk$g++F zGid0PTo(834m-hZx7Oo#KGsxf?kndq&rz5UZJ_;*Iie`9+2*_YLru?Zk6Q1G zukM?RSgg(Du8=tI4gv3 zDG}J;G3=pbiB%k@tVO6WA9vlnlp#S(nMn&iiVudErf*x@W{C_}3J2JKhKV&fpj7l8 z?x?f@MoI=b^ZVvJpm)wMKxOKbVnNQ|W5VnO=mUG_Tx~fCOCX-tEbuTR*yHb!KIWI& zyeFncz0aaGBSSyOUHQaV>Y#r7sP|SV-k^t;yT}SgmdbwN|L%HYczZ-MD>KXMlgEF~ zdA*{~6R)O0Yj<5MJh|ZzEx{JeOsgTA#aftj3SFzCjA*v3VtE+Bf`AcC>+gREhvN3D zdNwg!=(OaA73^GU2Xig+KgoNSy({9p9Bi|F$%L)3)cCr7gaIEF&hySGN`6^iC&H`?X+gX&j3iIH%yk+bq#J|vP%!OvkOs+htW!LBM`Wr^b_3JdD zzP11v*ICv^;t#BA5-q|KEpP{mOOszw;S5TuuV4JHu&(&M>A`v{ivL`1zoAs5_HT)0 z>0d2=Wk+7#d;FU~pCbHwCwLE^h5a^chQ@#CSQW7j1}h*p7Z#C7U~gO|L(#Sd8E3Dll^$ z1u0mEB1eERn4ul8o=sk?Y7XZvw)RDU>g~TYa`<+2+JC55C^dZ z=iF2fKNy}9qs?H!yn@$g*CcuwxXPvfguhtdJkj2H%w&19GamDo4ZD^P)ihof*dP<7 zI{epgCc?&E9$QMdh@E%$+R2FJEOeU!|`LA*D*>=a(S84>qqP4i0m#?+4H*MtVmROZ- zkUE>#;$SCn=eSU-qLK%#w)I!~{+VZ^$(0PTi+`O3=^=gJ)nMybpb?w-V(Uk9>uh{8 z>)xuUtF3#h@`iC9f9t(r{96?n+rhEE{HDcjzlJNop@>0iZCCQc`nF+}pmKff>p$wy zd^k|bV@80CayT@LLz=suc{1B$dX2MMyRfS1&KxGVA4@lO=JmeNvgKVRB)h}$YW*4C z(%sCc;|I2M=bb2hv1LbIm}ft$boo-+%Uf>kuy?xLI=|c&^6dTvI@?h?yLpKI?8mNR zd}xxZHTx~Cu#(h8c~!~ZX8q-lelwv~1qNcACg2O@x3E~s&w1Yipa6u`QPss8{>=JP zZmqI(xBtuhl~(!Ne;`_efpWIg^f6F=p&;WG|M@NoJ3VYRDQxV?-rgtz56deIPk!E~ zXUgvarJNBueuj+Xr$lRa#Um0M!cN-O?w>@3u)3NlFQQj?0F_l4^l{NGH%-zRJ+IbFG<907UaIsGt4l=+^)`mlOyge#-H3Ys&XoUcl*I7E)%d@F zbf~qE?z8g5*bf+;Ww3JE`HZ(fB^~4Mr4FTvFRdm=sx-`_=TPJn*SG6c4=vN^nSJ~@ z=|FoeO~vQ|J)yIRUYxc-+_A$yvC5c!p-$+`Lk0IT&_#aLF~_C&$IuV!Y9cz}O=RZi z97L}9C81FZs8e%Fu&7u4VghcGzbV7<+^(uxvR(awwtA-{rPVRYPO{RwGrO-4cPbxZ zLwz>jsl#~#ulUOxe0imtgVC4u%mHK;_Aj0-tm^)WYP!SbIhrau-x&l7I{?d%S<7ub z@amqWU%^4ptt=4Bhbk2nA6?-^XzV2JO&k3By8d-`Ph5pUX4!S3*K z8n-QZ1j&Uc%*;!ur7wB@KDJ*%EJ9%i63{-ZKQIAd{MWW$vhq*g_{>6Bkf}rPz?bRH zk%GrS+D>l2KJf#v0}sD9LGV7y?|RXp+osvq|6-N~QhGN75uWg9-`3L0DJstjh~C%oYw%3o$7!90l)enq|QUvaq3JXTf<>w0{g)?Q_F?&PMp zIcwlNFX;&pil^i$tTcW6=_O;A1R--Ru20K{iv>5Y_$PNV9>*G*Ng?b@;Lw&DznSq& z^0qZ-TYoqt+cc@BDzrfNQbbax7W>mV0U$_#w6n#TM);d9rQ2WXOduWh$Iwr0-~ELG zp(R5aynjXs7p0vX0wd=|GAo;sJH{r8iQbqlCOJM(J~2cJsZMNd@}jsd@WxvFYJ=|V zT?3to#2aCWn=y$*9+wqKnoFNu=)wD$@LI#Yd$1iKI{I>Ta=Sm3t}#WAQj{8gWshhh zzU}@8Ey2!ew_>pi?P#M;Dt@f;3S%Y|m_Y#=Grtu4{6?siQ0v7-!AINhA(u=RZqum@$`_ zt7)T;OU(By!3QC@#WT+I6FhlrsMR)al0U-r;Y9Q9VKI9vel!PF(T`XApFiacPT0y2 zwF=`y1Amz)ema~*&r590{o;`6E@%=PTtJB>GzhLK9QgipBf)?iQ4E{`T~h$2zc4Zy$Y+Xeow3>!<&hU%k% zS|}FkE}u4Y;>qL?DbG$HChHU6WTTuekO((iZCsE>{5?5q3Z8`)e4%fy$<&jT&Ae$B z6me2NX&l3A3`fMNso4BHf27(k%qSdne2C;TC&=q}*neuY{W!I=YiyaHJnt{6Pt z;mu6NL-rA75|DHr%#J@%{Rv0tg}k|{^;X3SJZVs8WY9XjVJig(xleVtF~b`sp|Sw( zIaYmk9-4XMTo-@A?3tH3hb{fa< zX5BuA*1N|tx}f=OkW>#El6BBdqf4 zxW~aU6N4iNwi3o7#AL=G;oV**_#!PBE)IWEJM(T@cl*@IjA~mT{ zdT}^c$cqx}k6`MM#G#2{_RTUnp-=4)g#-XkBg42z{7|pZ{TSM`I8*51XF6$&5~4)8 zqUMG%!S}>#$-)A!)~N79=&}zLzDGjfn1-|YSikUMqy4YU_EY={51W#mta|>ppbo=P z^Awg>AE%QpscgjWxKPJgX!I=PomCBm9sVz&p!5%^Q7#&+TZu8DCH=H1+ZfV)G)>9v zV-qEjtt$!=CFv@j`HCawZey2!zo=l_y8J=7y9J0nB{g`KKZ#mng_{kpSx&Mz-Q=AD z=y?OS|D&Idh6oG zddu)R0wdx>;8)doapH0z8>9zEQ>C|J29^*w?HNd!)q!-tQE|!l7vB*PO4k6#O=^gPjAtNNEP%Abk$ZUv&Z~`!VG6`-7wDvcOkojj(Mz@>Hfg00=glt0=&1BKy9xSMS+;owr~%Yy7V+u<;$k_?XR)$M>GiDzG!~ zw-0M@y}PN|ou=8HCj3R_p-Q%0tRH#wgIOKTG9B;N)n7f&l%MbV5X{YYd1KCgi+zY+ z$?fyvp8)g|zvJfVQaWXm0$vQFOX&*bSVNAHL5@kia?}K!j&Tk})9KHh8O&sN1eFBP zse6J}-y!t8lZB-~S$YU9H-9;QFDR~pgFW?f6MsYV=QMXm*H(9OMa6~7A;kD50_3*A zZV6?+K2GTBDR#gE*XT}quc z3dtQsb@0XPT*k^-!a=OFW#>nU<3Mab;O-KT+eIe6;AU1_W~<%a|x&#{k*JxMAGlImQ3!e4*EoqTn*n?j3j zqVLIF4CRB+t!@0S_r6_r)cQK_BMe#Wx}7wimdu1yGyYh9$7h-R&K`$Xf-3EQ`mk|` zgV|<4!Wc%52U`1vpM<9pm+pdSVC z0rwZhLy_8d68-)+KeIC3kDLWZt*@W?EIUHW=Fxs?aG0n)^)*+#n3p);-+T^ig^H@$ zxw7~|4OskA7MNiSky9(=lxv5}R~59P)Y_GnHh({dMV$b&`&{adlILNonW6gaSU+G5 zw?jt~K4PiaD4<(}CSiF9RGThWA1rQu{9Hm8!G7}m1GSOp#QgpIzn zy3_cqL~Kyc2n!p$mm;+rTzuBL(4?-M^0yat5e3$Wl!c|tyg#lRgpAnL{d?-b_Cyc- zjJAJuY5_IzSJdYbSyf#6{<6gRns-#F&xiCT{$0FrnWX9^-Wv5;#TESU3+XY#-j9t* zO|dI7ID!2>vHPxCz3ub1swI1B3)~epMo}aDtbk(XE>7LKusMjSvFOHPD zRpzOv7q$>Wfl7{m;y*0pOD%FZkouW&F?>MN% z@+9l{-=(Td`3+%Wb?xy)4LdDIO=#PZUbOU=I^aB7eKSE^G`Q@m{^@vAo24yMO*E9m zabfQ8JtjA#CriMwd#JU+D}rt@t)xoJsJ6WfdiP&FH%sf!K^Jr()Jpz5sz{79uQH9Y z^TjN-)8@&A-FK5!n?@pCYm=ipVpcgYEwVF52t1TFQPjWtbQB2SOfF40QQYVc@{n-N z<#>{~FPOJEUKdbFAsun(bU5vat(E}lDHzb^*e#Ci7Wdbk9YHwoRJrWFFzC^2NkEv< z(4@8g=&OVk--FCz*?bN)t|Ey+cI%=ejPU9YMHgea_cvc@#^mB-kIB;+^>sVuLdnWP_=ijEo7cJ~jA!f8R3!PvJXtwY%m|cEHdDx~`*-m(qk8 z3fp4SzsjW!B(;f@UFq^%T7`15->&5P%(Ib9S3~W`^Gjoxi)Rc!f7m_k+rV!_`oRB&-{UM#H2I{BB2WET2Zry~k5@@*ee=2lH4t|m{H`LVGdqEs zd2v^&jqJ&BeeWDE@*?*DBri#sFbD}`yv~)qt(n2;=;ZX?LY$Mb!?LzP!WR2#fXo+3 zgA5>jE z>+JrvT{P4NLN=?%mc49cwp)6WEhA=^Ha}5q-~_v-FBJ;+Ef)zd!@j63~EhxAbgS@xs#%3PT!sj#ZBK^kM zsbGV+So&IJ^Q7fc+Y!p_-A1>4T zQj_-zIMbs>6c+Ph0!Q`X^M_G0e7=xJkM69EsCuv@uqDU>Y}nu=>C%;S7$U?~X|UPY zjs>I6Ww`b;F1^Tb6Toeb^YL+o|zh z*2(`|iDxL$@}&wSP|LeToH@1pGovfBc;tEspaS^h;voj{K;Q+huWT7A)fak^PR#vC z#5w5Z-xiA^9%pZV?|fQlyHB6%+U}F%(78;4BZePoz zEBQD6O1`PtNp9?)~L3|PjZbyZd`#E-=iz}7A5gH&TD&OBfru^ zfP<2F=)G@;Gv#akEwHjD7OW8N2&yML_wI*KEgpO$FnZ)VP-z0ecQ846EyJwCJ5tl~ z+kULy(uf|UJDSe9PpDt=*lOlk`|_MYsVSMlH!VoRcJF36<9_c=AAr>^_dW|v-jdwe zf6lHZ#wpem5n~`CL!sseUS*qkeyX0i-kxa)o%(SwZ|@HCU4Iq-v{LR zPl$Xo<$uBbwUyJ2&D0Sz-eiq0vWPuu-|(?Jn)`lA(79RmQB3&-K8i6`QwjaXze)&X z&Q&->5c^SFCYoAcRkCMl=w-;zRECmX%CfqV=& zgf1Q|LMQU3BW(`Y+R43NML?J@8hEy>*vM<7&j}{tALUx}#90~2%Uzvi={VZ$zeK{Y^lkKO6X$u=Oj4v$tI9MwE>*^2$cgXf4LGXzw z%AU=#^b)mZOR8Jd!)Iizq;~AwZYnI5*zC-sY#q1!S|%acFuWGoYm#kxFH8E&xzn%Y z!rDw0Eedmm@3UnaZH(_Jr|kYuNmEM?wiPU0Y}V6Q>JO5E+4h{z)E^K{MFgnt&_)hM z*cKdzI^&eU_XyVkhXVKu#cH0AttWnL)O(_YA9809b_hPVe6&l(i~$iH!akp7Vm0?w zDD%jecb^R9>0w%O;C2&7iGc*Q3L6Z;poLlfUpMbUQaz@hY|OS6PKwy*+ZU{}k;%%A$3KsuXb52`oW0uF z_v`J9BFR6_>g{ST&Z-jE?t zYDL%wz)4xN@E)sHq&^+^@0D}?M*KLQ5XOn)se*{tMRW*xE8h#OY?BZJf_`Vp-~U3; zOjKpcFQx}lI1U0jd*{f z81n7sVDJ{s4Q&^oR(n44MpDbab<1m_2wq>Jg=(=i$Liw3Xu>23qko23?_hhG@`ot{ zj}4Rh_9w7FMGAxUzRSFbFs?gMy_L%Zq!II0w;!U3gd4}uSFPhcQDzY5Y~6on6s9eIOZ|V*_fJwGHF=>5(PY1#Cd+>#p5a<{ zjs6M~ziafTRFvFRl(c(rH{+u;7=Le#Bvo2rJ7a78O4p5ZC@uP@ zb))|ia>Fd&G&t9$e|WAr{BI2qRq_l&5P87PQsB?}sk|w!s63j`fULu`98AVJbf(v2 z3^38-#_VhmLW8NmAhpU2QZ^L_QG#w)lY07=o~bxHfXl}dbA8->Dn{3_#$OLK{|}q$ zisQ7(FQbO+6wJUjPB>+M45MLV|7?T*5;{(aSyn>u|NCYKi<^4dDL>F`_12t&|H$bY z)xmI@#K7dP;`qbD|35jE5mBHKOpsB@IUzc-67v*2N;G(55>JIxg@k5&Jsy*t}M4V8?4<$tNTfGCfX? z)#i&0t;SQsbN2LZ*E91xc8WqS^5Ou^b`u#+wzHIj;O*b*kOJ0b3ad$-;_qy5LMgp< z+f4ayIq+hA>^*2XGcgMlV3Qj}(Er^0^y5qN32EZ_>ZE>v4tdEZq@!Dsekdth?U#q= z=e027i;l?8`;6be&^-v$nL6#tKx<(8l-zC@Fc+hpK9 zx3Z-Su;)3u1(F=BP{Gd;rc?MpGFUB1vna#qg;~;%qpJChlMD)32UDz<3oP?2E5Mme z$#sqTGt2+`Gq&I!`80rmRoU5C4xqPx$jKEyaw3b@+4-FOd`SUpB-`ChJce#jGDoA` znf-fY_ds}DAobemAd4&@l#?Wz)kzRdqUvn*uIP9eR>yTG1}&D3qa0*c%wxhwfz61m z9Bo>5p76Ey_gDLF-)N2h7h^tlHd_u3#eQS0@Xd~4yM)chiJ{$L!&mojyTyik>h8lG z0qy#y(_HeIFydui0{4k)KG7&0oc&W;kW>OVxcKwb_T!J}%uvwY`?(xyj_L@t|F-f; z09g`xY?U<;I_agBe{AfeqU19=^-hGzD7~UjBI$OPy7i9iHH{gnjAmZ$VB{=%;&N3Pu9EM5XOEA!pmoGBCy3ep?yVoGv9u zHxmC4Y!NprSKG~(@Hf}k02-@#NPrbwN6=X&$(nwL`Ed~iYaSAOLJNKm4EU->1f$uu z9AUSBYtRBABsjzr9ql3%0Ges(Z$*QE0zeGUxDUt(C{|Lj9eo~Oq=3opFtc##%HuU~ z1*US1a8K_?2F8Pl58}$+k}Z3Su@o(YN&M4)oZ9mAg z`ruH{&zQr=ld2&Y7ZM-q18$qV_p$|f8@kLI1S+zA4ClmE%a^A9kN19VgQ@-79F$^% ze`G9Glu||Q^2Cs~hqMB41lpdubz3uE^N@^Y@gX&q$~oF4$^{E%Sr@fySr>njeMr1X z%ewJ6mi0o!CzkbTB*BZM>Da38d->->ig?l8!KsCGgX|}3{IxJVs9+=$r22?MugaBme`Pfd z=|F$y1cxc@`#b%q%CFAWxQ+a7b~M)PPF`OaT@2!$piUy zZGVuo!$WdVw54+v!)s{!AzIAOm|!F>u}>l>=_GlbR>I@NMmh1B`6mR=0YtV=^2AIn zBVwW|@UOmIcxOq(Hvl3!Dpwb4`;%&Ou3Oj$5>x9mZdUx?Y!&Z12Y2~cWmJsM9|J0z zX|mT`w|~L(DENfzQo&N>16`}!yIt!@q-DE0G$~X^k9E#+eSA=z_dN2mf9o&E+4d*p zqU}&?Kjx&a`cK-&!6r+gV6)a%D25o-g(NIX8-Q*Ehrl6!7R4kk5nik!tzO=IAH{0M z7oUYBJxK@p@3dJy=JPYt;GTuF>=uK3oYg7^*;8n;|VN)YMqvcai<^s)X3_qniXe;*(j)#9A#qnqAp@*7B4+ z`d)jw==oY=7V0}hc<;A(wye0HSL~>c0K-Jfj%fcp9P@U@W`5#x#}!?}cD%)3yWX8V z!XO$gsQ3DVaSOe%aCrK`irZQ`>^84(201I+4tZ`r5bWJsM5j6b$@T*biXPB4FcJ zh%E>G6-VU0_>Fk1vHba~(AjxnkJz6Xl(D@LuR-S$FL+{VF-xKDhjtUyb08>uhLC|x z+ns*PIu+i4njczlE^y^2H#0buuBZ=L|G9#|@lehg*p{dJN6{lR8se`%1CD6YH(*F- z(*MFE&}i^8Q7E#_N2&#t#CRzWm+Fg9(~}c_bR{KW$jOQG?AL&m#U4M18osjlMx{*H z%G#1D4`HFnSAY7@_Q+fOYI{U`wQY}RFREuNIokG! z7<$_yVyc<)OWqY^lO`j=DYl>&i%<0I23*6iN>?hzW3v!qN|?=W-v zJ20!H|nDjV*3%!QUGh$-ejiy)_1g)%+G|YkGbOtIP-Z?ul(XuSsJO> zY!#POHu`_LlWN!a*JC8cpPFfkA0ADeArpi}3tVL&zKgH$3x*^S$$#-Jxk+-O^eb8r zN;J|$m73Tq=K%i$KoDMNe(~urt;yVpUf=*T4JgU*a|rhT_Y5$X9WLDh4q1wBp)Gh! zXKs4Ssw7PO;~zpD^}uwTCx{CyGj-MOkaIX_=N&4T+tuuiNv2wTqb*tclk~79t@f9? zq(M42tLq;s&UcY5+|RxOn+Vl-NtyaI+@vCRxGd(q>j;%#LiIZH@@a5Fg-94QoG8XZ zl%A&@R<@DnGl(8{!D}@n3xXdfoO5ZlcrrJ28d+%Y;@M-z85tnpMyzbVV9#$VZ+_FE zMi|@vtzDcIJ)H1vkPO;N7n?OO*e1IIF&p}TNCG#o-WML^#>dbgDf5Kl*qj}WCx&4+ z2XsOUQerVkFO5QyQ}Dl+5Ojispr}8{0rN@;AYApGi4Ux+?PBoWtJ>?fZHQTS(W~d3 zbq{7RiF-iPbry>W6MGWF4YeMn6WHUSlk>+J*YJ~L1hE&L<#}a$sTe79QQ^0PFB)TT zxn8&r9`ra`1=CpXb;x6XOw@ZG#Kup&`#gHnGsmiP_*sEh3x|O=x+`uJI%f}YDKS6r z!a_k3O$itovzaYKn65xXAP`Q0gE`d{T5yN>Rojqgay93PpNn510hVhRc4QHQz$nwB z-jI51b4R1ZGwga+nW)^3=e;E`gcjUR6$*jl?yOnm$HodA+x_?9prT8?(vng{?mj%fo!J%_W}otPbAu3R($&uS3fTVpYCuF)H~}K|J`M8=#8S zjWWxH(H^k+7k{PBFRHW(kxmMtUZ)dLW+7%RIy>lz)KGG-%2TOF zD?4~j$TZmTwV1T!H{&|)(#qzW8f*QLagifQx-3Y#+9mDZR8t$@N3;M(TJ3*jsd3>> z_hJ^2sP|49Vwl0^4+k?!)%)}B&NAObiHaabn-tiX4F_)SOuA7jUIC{V z0`W&uXJN7?(283{0g|6#{?a$7&u5~08-E`6hQRxR;+%c*I;fRl)-$58HU5qs~H^baX$%L%s+5t2KJCNNilKOCxn5I$LPH&fDy5j(Xd0%G((Q z!Zi~=;}Jh?Nj(M7J4xwy@h>_Ic;~Y)=^dYx}Csjg`23{u6Ky>Dd z2$A@T6z5sPMO?W8889#0myBr5)E!T}AxIDIHvbR@`~Ml;2lj&t(B5rF@2x|F1-6!= z-6yaB+H{WRE#f`ftrf8k!HM3=f_NtgUNEQPKcGn%v56sr;*!^;RNc!{C{FadE!^To zb9mam2|rJj-|hCy{JVc>Xg-0YWRT>!qza+=2%$Mn8>{_t0}qF&pvuz(n@VXtps2B0 zjt{)({{z%!q31#yOVhQe{)5|b&w^{|z8>c^o|WxCN6upC)B7;*A@q#too*p~#klXPY-aqb>|Qe=x;2E&^A0HrWE+ptO*oJO%FzD~U0K#}HyI-?{F?fvD znVa;My2HRCoqHi8@jrdYsw4UVu@Rj4!yMnOn=B#G(L17eQ!Thz!^i8gL`FgSjl#(~ zuE9tB2d$38rAi88B2#MEztg4G`kO1a}>vRQ(!v5|nhAF3NoSYue z*yUz2!s>VLK@%0$@PYh=@(L>n^1;garIWi6GBr5X&w~Wu^mUr88mH^3x)&h0nAfMe zbtHLR=~Gp%DqU=k?DGB8->g$dN{SgrYd;__d?wU0rCaY;X6qO@}qC;j; zw-33)Sdc;`rJ+4;*HL){m7&6Q*05$kK|#C(k#wLsu8_j?AyzcTO%$`OA9KaN?2bCd z`Lf9yiLp`d4o4BG;#IObvQXsVnUz_!ZNmt1_pTkPP7!x2oS(Q!>}NKs+g$ENdQGKP z!H;z!<_^8lG9n>{K>x4%i-byVrDim_@2i2F`rggVXoZ~pwPENG{alF$`RoY09`}+A zPPiNLcc6}}MvQ0?fz+ov^1a_96SYy2Medj!96 zQ9BM(pVZr|@}fgSLX_!ZaaGVA_mr=z_qgzxbEW==KM@Qe_rvUQOm^iig%0RtH>#fEvrTsq-NC;?=2OanA*bpHR?IWkIqI%KycWdk zc55TXJNJL`7b-gfdo$?dzpot&=o+GSijS63?U=uGfRkS@%~tmXblSVi|K{C_^e7~s zVg`dP=M<9X8LuSpPe-WeZVHJc@Iw#mB_soQr!(|SNgo0xUMs6~z0Sq1PYlomvaGQ_ z0ecI?RV{mXHPABZsqi1LE!M=VOzSL4NL^KSW};e?hOrSw>z}e%bCP&i+`$l0Tu~D^ z7ZoM6;BmbLXT)4k8H5>OR-^d=pPOl_=^w8Nnf*{W$i%GiKR!@!Nc3+?9)HoqcvP3| z#Ro0cU3zx!9!qBM3v0mLa1nvEI>3bA@h%>t7GP&|NnHuLen(|9{h{L*v7Lm!%oYd( zG4;G`9RXon%scG1o%>C)UxM1B!FyQrrMgtj8B_zER&jlz@Mdr}Pf%tU@75L1Zfy)Y zqj;&2vjPidHKGwQjqr{(1z>%m%uP>qHU<(>ga(v_v9*lQ$U=IojFe6XY=BU7y=~U~ zSjCMB-aFoAnnOwX9xp+h3HUH{; z1ml(M`(|nDFNT;S-u+eNQuG z(Q+eJe5tc+k$cH+`GFDsDTv2*QuetQ`x45p_YfR!$m*xiAi@Mz4)o7uP-47EbX08?f|tQR{}^>r?G<(S8$NOinCzhy3Q$NcGxn_ZpX&Qb2nA6D8O0cMV^i7L;Gpi|Gl{e> z`VoZ-lwdwCfVU&+3F$=mm#D+DBIXuB2xQpM#gE$bm>T&D39t|ctPD1Am-8h?^&RL?l zJJ+aHx)spey}Tp4ex;>@4WkZq2DUg`r;;X-Q&%t(sVcN!Bsp^TXKo)GFUTxYFOY9u zqFq{z6+YLHDvCBmJiFcK4J~ax)upE7^NAZjD;uY#jwdKgxpe^&UlrUpIhceX*jUs* zVlM%&w3f?_#jBf6nGoiv23% zXGfkwRUv*h&cEX-;{ymweZ>W$WD{-cy{{OD$VkU76u<_#HWj-d&z}$S0{eOM3i29K zm(1hH;*8q-M0@X30cr^hyo9J9p___n9&&9g++k774z_-3xRSXl)JveI_4)xy`6rfZ zprujqf>iO5taUx(y=H{N=h@NPe@Wnd6Y^p{f&qWPoA8);m+J+Pr|c_u5PW}D-(k12 zOf0%5_?n&g-}BC?8g$MMx*=&TKUHt*AK~(E#gJHtfuv>alV}Ykhr9 zztDTUU-0I>g3*lkMf=(_-dFTJH{OSWui5b~;XMMT4}`_2mRS?tly|&nWt#p(^(QO| z$5!1c7bg&|rgUWU8L}1Z?6UQCD)*mJ9X#l?y;a+G|GGWZQ`mRbF@EVj4poilM(@^_ zvAn}(x^LRmE7Pvks-lN7`;iHjcrkhUNGJG<$o~`hWP>LDEYUw=3H{dQK1?Og|I#Ug z$7!lWhBs;^cN0gJ+-#g-RHnQ&XgDhvqa_EbRZU5}qq5oRSnc2DI~n=Zpj0?O z>~nbM9*S&TL286|q!pwJHhsLn-;2QJ!?TGR8s|YQB)V(jQyJ|MqeALK%L*mp14~a* zi_A=7Of@&uGB?zh#YsEJnmt-!9Q5~4Vpg*LCBNYVSZ#mIFWg)m3tbJNXZa_qhFBTg zel|?N1Z;n?fhlo~=NdNxS3A83d=e(qo_&K8d2v{@W0349)0jj+p8kV; zmAR9at=FvWtZ>fs^b|Gb`7`bpY8a`7CZz*Py~z5IrPS~7)|(b1K@YRMr(WN4{U05C z&C=pn-aR`LbuaC8|IzwynD0nZD>$1D9Z7BqxoOFfN2HBhmMeD466alOg*!LX8#ntB z=zpV61MvCA3lsm9KKEXbrOyvwI_hbbK0o69o9T1&`;I;z3QC0oN?gJ_@vpOW1*yO0 zJxhw;NS|<%)ZwMN*J!ffEGr0KhUqB?=pO_=be1f?RRq#bCRU=P{AoR_woX3imD{yhc z&g}opI+OTkQw?k}zK7UGg}mNai&npfZDByP3rWh&=%Pxi5^H87;)kl9t|qvXfk$sw zJ?m!nJnu4d*~_Vj*mTr$6sfz+N~3|JIE>(#079>0@Zs|`hvG7aAQG|Q$STPnup48m z{?N-+ZuMdRwbQWsX#AL-HSFG4ChXqemy`-A%&hK0&`vhAr+GI?>P+oBy7$~RV!roX z%?$qX!avPTV8C!w5WHmP!0pev^(CwSvj6Ig4JY1b6oTL8jJH5yrmiq;G%L zKO7p4q1!#o@hINJd8!QOC}T~^KMWjeq{W!Gp4kixpztsW*1|BvjuLHX&cJJ)RlC0J zWX{i?ypew!__vmStN7PZ-!^q=cHw?LQ$DrBptoP9yb~?~Z}?5fLGSrNSR)jx_P^AR>?TB$lgSV>tqm8>s3}TsO!mN^WOodqpP$3(ffkZhr zi|*uQbxg9aGHstnQ{yMEbTfsgrms3W zRK`>f&z`v0Zb|Rm`wIu#^d%aaM1dFm#~#~-9{ENWMQe6D@o* zxki^dC9qO2s=Sx()_bq{jrVG^Px5)9JRo3hu37!rA63=A$mCUr+P9Tt%1>P;*&-{} zpUaz$3*S!;HmMRd8qt}I2ndWe>3n058pp3{yVXsP;dp|n8E>W9iE^F-J@ zpoH@JQKg_yYd~ym4gFPw&a}SFYHVJWZBXjqEp^vH@eT@0LqYuBs z_1kGrpYrx~`XJsl79yx*wsDYuuyH2Dwq+=}Vz#!I#JwJH+=O9B!{HXD(1LcujrnM{ z3m(!>l=pl2ru&T0o4mgpesd=G)3LJ2?7}eQ^D90cugPBiX4*gX8aU_CXiWA;*m1Zi zrsim3I~DCpyz9^CuQ5!@Yu_h}oByX&Kp6Q;RSR+)P)FwouX2qCfZlJAi&P{o%b<0b+Zh0w*SlB8Ztt?__I$Eh^PZ*X( zGxm@4TJ2UQHcpTTk*$=*f`r{7Q=b2XA=%-X@@v^`2Ct^0M`;~u?w9MaTZf(j;e^Yl zp=Wt335ym>Eij)^j?vojSt@7i>rVQB-&K2F-V*>`M$E(5AEIs@qa8&9+XiSa=-GC* z_i%M~ClX{rxn~gdF7N&nwa71Yjptalf0ALlgNVSD14<8a@=MF`@SMHALWW-51xom) z^*MWeSXL07ZYLV9Tlh}7D~q`C?@7+!{`C%%CtmX3{GCJA+NXOX>m=Tox8qs&Yf2wJ z6ugmZ!tePZgI2PmL>F#V=OsRuY**WP&7Lu@2$8}T&=>tf-b6+z4CWlVkaF77r9R^c zpXFj{H<~#H%c$v3gB<`G38DQT_Exop+98x@OHJFAkLq`-{7769LL{k~@rQQ^k6-aOVJMW#tA z~mjz946zD-2i&qBPbC@J2yc(Kg z_4F^o{T#y90_fzsz!ZAna#m}&A|f99CvE*1KRiLq%CY8ic&qb9MUr1uTzkC!#>YpJ z>HPTq_BTYzki-Pw{r#6=LMQWM6TB{c96$c7A4l@z3jG)pX-RAAD?XeeW$^)|$CQp= zQSr-cSf+7NF202yZm&_jp-b6Lc5iu&?nB=tVq7k0m{G;<#I5x;shD34-n0sg1@VZK znnq0(3oAl+gBLAp@Ho!7-rE2y%KV?vsrGV*LRIO}KibphBt~BQux!duJj#5I*5|eF z8Q@tAcWN9e{@h;>r9r_;WNhvBOo#EK|Lgt|TfGFKB7}*34iO!T;u8qw7!RxQ&mjia zrdb*Gpc{VGe{7se$|7y~jC~AaPwbKIs3(P3-$6?J;r|Ma>;9bnN$>vSOx(b=jnovV zh}Db~Xac_;fMU3hWKJK$=&P4!4hla87dYr7`&(y!FSNgn_P717`uc$Vy~_SJ+TYXd zZ=L-;-2PVZ7d!ti2p_)O=`CLIlN>O4Vi>?+-uf@ap$mIQ?eb7;d32^TIky%6f-HW~ z0hDbkZarPAa7UstHLU~zt!?Byef(#IeVi41?7wthDooGSu6_AfT8p*~!<&5>+XS5@ zQ6V~B5XHxlJN<#S?zu~zOp{X4LT;9erOJIgdlAv2o!g6(?~bY$r@#EU=`n8@9;G`o z@j?9E6))Cg^y?~st~2$z)NvV?C3fd0o}@xku~@sL3uHzsq|pOmkc}gtZ5oDG`t)gZ z3WdA7hW9`QL+gyDO6N(;sD1rvj>Ht_9iiVFu032!*U~@z(f&IuI(A)j+rOfnI}7pk zi*|OOn2HT8^B+A(-2U?6A!1OZrV{32kbvzv;$WTJF^r{iekj>l=HJvi_Y`u!{b(+? z>o-98u2?0FFAWE=gC#;)6qvMGBJ}w?yxZ6|Ra+c)s4_XgIoW zw?YhCUmCm!?1&HR{5EFM>Dw}cKriUI4Q*2^n8ND%)X8H|Shv(CXZm^mAt(gY)BDbj z7hdSO&oRahEmr>>odNYiE9Z&@Dv+>ocns=5tr!*3N=>sL3ON zI5|zk?dLkgt(`^jQkPU@DMbwgR_*LAPMqd`6vmmsqJdHSSW=&PEh5=VJb^=UzgH=9-gKAwCWRp^MInXj^1stnD;Jth#~h z+nzaaJB^UIg~aWNztPyJZZ<75z3W>XZP}TJ^l|lHmr`L~Xn4`U(D1r}Va|Ou^G-KD zjB*(wIhKZbk9mV#d$G{+0sK3y1mCBDWom3Vuk~e08?LkprNM$a!%E+%bYJPNW0_S- z-J;a(i9go2_-diPWxF1sq&m5!d2nT3H`l(aySt4a7%Z+YT($cI5t%kSM>au(a4YCF zNj5>G73C%=J3-TWCMekrW70$excr_8>hC6qEv#G<|CjSqHxTXr%z;~ISQ9jyxd7Me z1_HQYu9*lj-q6G=Ha*w4>FF7-%@2F|*Sh&(E;KE#+^*=v4cL3@JlcEJ~b#|8rW zGY4uC>V2hcRN4lmt)*W!Lz=$Rn8=vxnVX;z5{Le``Dtf<9KFN;Rz`-y-Pea&Z={QU z@d;2`+sO=eaR=|wHckXCTx!3j0^`z8juuZ^7vIZkUPWqa@vI5NZ$4EvaMD_L83fqc?wn%RV!WcS?K!(pM`GV^nrFo zoCPfEVbz?D1TYIL&1^K2xfOPN0jJ0JO*#1#Ln8`#L+y&tg16~beeDa0L;Z#$>@pQr z8wKq!Yd}}tUVQdv9u8$NX*YB~WV!zqCwu}XMscP008UKu(xjT1!xEh37}b>gGIBN7 ztA2UjwM9+I|1rFR6^i2WxXGocy(5`ESWcjZ$@H z^>s^eCn9WK6=2X?S2J=fyZfQmW=h3sm&dP-)~-$*>>tbEbw0*{MY3Q;0G8PJ04(Y0 z(b|{euP}Iqy~9Sj!$#W4;BlSlJNWwKG6ue^0JfUXHMF&>5{0CfY}E6!hhIc%OaFk=S7K$vKfAgt5PO%{d> zuv%vC2Itg=T%z$m#khks1VR6_gEi_PfATm^ff?FIlb2Lh^qP8q##pzd!x;~TnJuu_ zHy0Qi6d*ME^L{&rm=$TA(PoZSGWn&(v{^NM=&nz!0NJxoY)<=Y3pVvikl zZ77>*23&BrhclyE^cCiU6$l+43uNh!_BTBBZA01?|3GiKLdM#x2PwxKwGrv#Mv52^ z1H%vT&!n6G8N)9-%qghYQH&&th39M7ERCK-iXh7DL98rlK>I{OhN78@QKU;wP8H=z zPmN&=y;H@r)_2D^)<-fgV-~IbEZ$VReBK$!D=Mq==W-gfuCi(&G8GTqLQVx$X-=8Z z1O%~kgv>da_(AsF6)9RcJUuj<;pahyB7Dd~_;pk_wavI{SLj#EQz!3=rC5MMeJq^42!Id$E51j^ETZin^$z&Z|%r)Q4H=YxV8?Hqc#K0CjfoJT0%IR7NlyYddCx%43a7<dbM;#QS`MxsL5b5Bv{no80*z2s!mLW4Z<1d%fPrN|P7y;q zl7qWk7j+SR?>!!DfdN}j9HNB=%gA*U$*pr=LXC!Z^|gNwEg<3sAp&O)OPpF?yLHaK zCxsp>I;5$s@RaJK4~ZWjzdNqvuXnrTAyPwGUdmUz^!KSq5eO=P*f<7c-={5tvK$+^MM8TBfcJI4<)G&US!S~@@!7~5-gR{Nuep{H3 zS@j_!p09drSl$_9btb5+54Q)Jf{~5G>C4uI}>aKKrr3?!Gicv z>0%z&9^zj@`%m=6=Qc!ES%a@b>BJgb#mIr=QNp0YSlf|Nyg|go3>*9{_{eA3S**4z z7Mk4U7~l;(T2^~x1> z2M&A7H{D5fEYqRv7e;Hl=CJqaP8Ve2VJjY|_!A=L9(>P^w+$`Wx)fSa4Bzfk@8_Bu zuj{F4l%$^c$q>|5_U7CA{fFryO6b=o%qEL1K-MY^7|w7B!AcyftL@Cz3e&TZlnaecPZjKi~#MoZ-Qu9yZu$(D@0fD z4+qyH9C!U7*!XEm?i!D~z>5WG#N1tDah`TXs626m3VinzE5OdllLJ*}1m}ghT$q`x zovt)%XME&IltN-6d{ve>K!vJYp~NuDOwF~^6D3XFHSomcMHAM;o+8^muk*Iod7mnK zKG}sdW0yuWTcEi*bKaZ*sZ;Jj{@mx_=Hgom3 zL@^v;&Oyl+iyB%#N{r40=18Us!dss$N$g8Cd`tt=preP-J` zg}Z1ZUR2+9K^_?j;sx8@;oumjF#2#x?U4o!5x9Y6y_R)Xt?(>4@w+14Njj7w!AOo> zyz2^&ROvNH$($j6&cDToOa>B@|HH|NN2pYE=Ie6MDV3{$3>J15f(ue6)97k(Jc<8cAeG`nbKBXVChKcjhZg%*KJ@8df{H^))S z|3nJr^Gx}HL1GaBO;LUWy)awWU&4$j)amHons)HGP_TQiJ@n#!55&(58MF5JYsMRK z>hKYA#!lJ)*ioUm0@b^`1fP(rxw|bemuKZE%Z^m-3Q)`5-~gJjwuUIVE?cs@cga9j zcTFWH9**03p7(luI3pd+Z-35I8DQfj>i8Z?`12G-n<}64KYEl-=HdT{fzi}?yr08Y zzreDL+|#m5+>b|k$bT9@a$*;%Umor)snVzUB+P**? zqStWKEwYhO2)XXPQRnS2c>3)S_u}Nl(=nV9*ybz1-Bo;oUiekF@Sq9&WCN{L(j${! zsUDn+TA}z{)XV|EV8e`$nOZ&`Y#+B9CyjaRE^DDhs3&uEG`SgtXC7`p$RC6K%?S6= zn*ND`^d=?DAR>qVPo<$!zpN))FE}xkKBRhIavylTQ?JkP8ms+`YwYo#Mbr39$biUd z&#GL5EVeXUk#1!+Nj6!foFb#29?hhy7u)MsQAo7ovnjT~RCOH5ZquvFG}?kH}!ykq(l zZ}94qZmG+!J|*JDnhkde;cQ|3S+*SQvKlCVq1s zJ?8zhzVoYMwu+aNoZK`jdimDrQ@y$m@ZaH9z5+PG11}{gugH zY-P9B^()E8_Wd%cM}Rr7`XT zz25-z9|rZ#?qd--=9>#l2@0GTO`-VtuYC}hINA>f4wyJzf)wR}fMU4Df4h{#EzZbs zCAP-JN<4a#894yKMHXMm#=#Cr+qAOWI5dpvK4bU|?S8`;mT(pd1W8|}6YA6bD%@A2 zscBW_>*fDU9rX9Jv}l#Tr*+dG%sY4I1byWoD4(j-+3o%(cy_8d^sO;1_BRFzuD+M3 z4|#+sAPAAsgxexs+Hf9HSWWRiu0`QKJ36zteC{slDP7My*OazYPn!f`luURNDhj7- zAIEi&?9t)5q%p~*3&7?f*JktVJ>kuoJGYa>b#_*$pDSz!r-9=lq;yuk1CtYr56aKu zs#AYX1nB`Z%=7Co6__01??j39|2!s}^^HoGb*i#T{`B|BWnH7Jii5k@U&|MwGYxzk zM9A}}=tDWp!Qd{&8P1=7s{Ou+%KR1xz_LoD@JA`@-SA0E66yMbNus+$u@|wJ_s2nD zrv>*Z4pBVTR$Rt|z(w7oi8o#P8h`y;Po%_lEimFr6d1abS}XaJ-rL)bb9ESlI?4S= z`Ty8^_xLD_>+yecg9Q>dAi;3cRf3{XjHYUEq3*&)cV&Z7P_ZHx11VO-G}#5LToN}} zmWNe7t!=HfE$!F#V{2Rev{nHxB;2opXc6ytX?@~)L8~FiWqApjO&r)`%p$I z_=bq|^BjF-@!a1qu5#Jk&S11p(zkhBuillhyYmUC;A2#8elC}gYC=N6=mw@qo+~qR z{RX4p9~bdBr60xqTV2gT7D(YiXH&O+1U(d=og1hr3+81ZHyL>sQW$~|gnP{imoe1Mr>I(7BH?VYcz}2DV$A3y!?*K$JUq+SUg^jRPZh_B z^v5`b`{X~>8>e8X=7S(|AI`dc(f7}&y8ZphRXHU{MtjhJ65sMlhi@5AQU+lh3fHc3 zfo0!<8E20c?&(YVOY#$Hlw>7IZs8e24j!DIBMa}TWSuZz>E90A{`%xV&d?vR1qehv zNqF_B=XkhB<9KIdV#(%l&L$tbza)3!w?$jOTn)M=+1t7N@Y&x%C?HIlNF}e9F0EAT z_v-{p^_K-=<*uh%P_2kaY2HMyXh%*Y7q628a0}KyK?)>sUUFQ1Lav`CMoL<=$wAa4 zcHNXXDVgJ1p;X`s_Hp9QeIcsgN~I`th2nZUq=h972utopyj`P&B^WyK-NF<&LCz@* zz1&j$j2CLS`e24w=qxpz5u@@pyR#hg+`@J(__3h=FU2ys-!t@2g!_ymuB(oDjz!>> zklM-;=CxE&%9vp4GGsS0I=e!4^liTAPImDvO7y+UjHRnSZ&coRsroFb`q+;$t1j17 z`&?HY^?8m)$ega6O=2blaVQ8I?lb-g6kqVlPvrT30|ougj^{GbMu&Yyo4UYp0lNt9?Mc5vuTL{y;ra|6 zpDdb72xM#t-y95Tt}!)k=pLyfyt4B4_=GI;JGqdh_|zO$0=H-A5HIii-4?2O5Axvz ziLZ{1KHvyf!&Dw<_Z3IOg{HhQCpg+8xfzzNBzmO(LS$NbuY*dG3Rx@44cGrrASu|a z1K%-ypkK%s`V`;eqb2V-lCto)6Zh+-?*r(FKArFi3MgWmmdjbXQf|XnEFJ!8NzUl( z$Q6ft#ZD+#_)=q3>0O0OlZ!7<>p{SnSm-yl(D>Tx;pVnPT*kQK=9ar$(F4a~T>G%!f1_CRV!v&V%R-N@5b!CvFuS{pn95@FyRyDo8O@sEH&yv$LX%MHN~ei z@WSa1^9M|1syycgheOO=DBEH-uO1eCuZ1PBATvvft#tub%5Qwe5vm>nY5&Udq-?Z& z2r@ile$0ImS6q0|Pz`30@)o0x!bq5o@OM!CFx$s664P1w`n{=`q`avTZ{(Y5h%Uhz zW>%!XIZCEH#9%n{UMyahBnDTa{yl(1*^J{L9qn`Z56PB~g|b+c(*HvW_EB(hivZL% z7oQ#`Py?yb@hq}@A;TqFx~RBKR(AYkfz$$&4Jvh8Hu*AfPCB7jA)Y%FAHJ~2SXsC@%*g}jk(V&7>>3&_MW&w%bvdwxwvQlLd)=; z8am*Cg0O}Q!VCJ;Y?0T}qa_=Ky1kx)CG!l+%^qqLpm0f`UJ5pj1dPkXEZgAd5?FKw zmub6fr6BVQ=E9>eaM5$9eoJA%b8I1&*OU|@U$p>R>eD14um!N5$pjL7N>dg^&UOZB zP}0iV+?%?1y1}4%=$lD4<^y;FuIqjhm2o#RBy~X>LzJhUOXoAiKe~)t%M*s0&w`Pf z53BzKsDL{M;(fs#6da5$JjTSx#VXC+Yv9kg(+?wK5oDM7XBM?Bj(o!Dm2a)-EeUG* zS*i0yE>y!HSUnvEPUwpG8;`&!%}-}-PoR=F<(Px3O2pnuEG&4zeN@kSa=$&DX@93d z5oKT6spT>?=zooQmV*keI|QPm-{#QS6a`YMbjAaH%?s4s_x!ih6O+Cwp=?c$>8RQ4 z8tdF_Q*?m_Hi};}dx)Ww@x_6fgYB*N&|~XQk@l*+o=kmsEc7TG?XBK$f%#h%cNVQr zyjz|WrKN$fyPfT=g|6lpfubrAedjpJ6|wQf&=KOI2R@B%&4!ZvP6T(63(dj%1u0s} zz{cfVy6(&U8p> zY%nW1SKrq9&`C(IO7HIf^rF$2LEd1>>x3nEd<2BdQZTdDR#yOKa z%rDPm0l6X>&RzQYk`KC{C3>zFXNvHfK5Aj1f)@7f3C5JGyLL|yB1`|Pa)D^Jti`Zx z3o!UlWgLdd4;v=G)qdrOSV<1Kr}N)$-t~sa{`UqwuSI<3Dkeku>uI6CwHuTb%6t;e zyF&9sy!67k^D^+l61@s8UCR6kdcF#;MMis$&-mOg=h&6T=RO1DtNNg)w$k%12fkpS z2%8@z{~Fl@_|}N_r$6r7>W|;FU$jwtp3miq@G2r*E7p^qBZQ5$zS5RjPwV_5kq14c z!;n2V4@BQ_C31Y=e9d}ARQhj)u-rll*Yo|eTffQ$keSAIpNAW3XTZ&)tLbVUOrgs2 zf*rEN_S|NB2HKvUJxP2RbJ-;pC~IZ{nWxot|3OgS)tmz#hdPR@c^9A2j&XWR5$&jS zH9w<5u5va1S$QU6a*V9ej>)d(sWdv;@vZP_(T*9e=GRmiHLm7?By8J7BJ4Z|qg$Lo z&w4C`8k6z77QV!__V0bxT|jKZK-W6@?AtH@R8};9>G~HrwK&-J3!?n4b)4+HD+Azf zixa>lmd$;IrXz1Fl|!Aq+}}#fIkc&a7DjS>eaxWfgdamDcx*K@Yl+L>AyxaDO;>XR z@K7})8F2ZBE~Xrx2UhQ&i%0k?284&Xo>Iz03rs)KQ%d#$ZVs1Wd@Ntd&3@0Jh5Mm= zUOzXK{0eaYs7WQ$T}$WXIpjRT>uSCl+@)DgSCsd3c`bcm$rQEM!V_w{h@cEO!2{JP zzGs1ov=~BC%?Hx3XPLhQtBoAn`kUWv%YT>*OnD3jW60J&=cA$NNr`#0XcT9Ya6A;Y?f=q`9!J*8o`45xU%3_a~lBh z(cF$3?U?IoUX5qXomS{WDzq+qYqX;Qng=0S6S1PUO1cI5W9iGp#C?w^f^@lVr1W}W zAy4VZFvE~1@{_ySxuSFrv1O)w75Ow!x*?#4{~@ybo-3p<*V>O=tHE1Q7YX~69D-MZ z@0wW~ZMl|n$0;RKeDP~PmmZv&nCMe-V~O{ZuUF!mLE{QR0Q1vp?XGe)zs1s486WGG zzRs(SU6#m>UB*GsbQy`ZKMMNp!tSW7qR6H~jHs)=2fz6Zy{a@OCoZs%D%@X2KHRg) zweG5p#A%hTr*R5ZSdXe2{JE9JRUPU(zsj?IQP1F^{lMi^W%iOX>vJlNmP*fui=5iL zXTqg4$fJKhL%U?0r z)jWVNa)JB)WrN$CeEG>=mq6zLN8t;d1l$7G6ZDji=lp0w54?#9kyC2@o_8YSttNf5 z($iMyx~9$7bnO2SuPR?W(iea37J58Tv)f!OXJ^(eEjc~v31?#*;sxvHQt6=~FeCp3 zL#Mjl_59I54F?s8aH$DDrA-i#!0hm;O7U25cuIW1J3=oKyZs~(aL4|pqO$PKw`OS?K zsYGka0D<*ea`q!9@0WQFM84zq99%pLzSGB%kx(Ct;)K`^XcEMRIQfG4EW1Uayfp_{ z1s9(!0^HfIrzI6~5U<&Wgq7%E4jb{$mpr8)NN^hkK-i=~e7uQ43JL|lKIyPebQb9| z@+>?{Kn)Mkpt{qbep`ftq@i@_lYmckkF4NGJ0R`rUCrN+DZ;kx5!8jna>6Pi1XgR^ z);Zd+BC8hSXh$#{igwHnU$JHm2~jROO|9e6j`{i_3^mxyN zUvw_6(7RBzT%ifGQ%3FT((BzlE%Kxq7BW0^beOf1^J<1Ugi|?Gjr2i zz9FX`IVY+hi9|skrduRZrYwh=&`V4eTg9S!z_%MEh$eDlo`l(DN)sTpy8QI_qXLf= z4|>&|#%o@xV--yfmRzO$2O+;8n7*wf0kSsFsvMJ0*X3xej+kAh#Bz@=}uCqxg zN=*6%U-X>xjHp6%C!=Klf|5YNcTN$p8#;X*=&hO~3dl#Z2cg@~kc&+*1pW}3T_FaW zB_J_;4L#*B@1#=EP4ALM_6W0?7_yJ?%H9a+p~yw7jB>W9BtJph07@F_mml`R_r5Bdy$Yc4WKId_<* zfffCeywcLeT~5xX%axD%IF^}HnWM~GT2LmNr&Wy z;?;$aTaWOAyj5ul5xd*xVX^=V3T@BJ{0Ey@P*8Ds??u^Ry&i}!D)hz2_h5Q%@W*b+ z<-e2v`L+LijNSiSsjT$a^4v3o@1PN%WJNv?#N$ILNxkc_Ja?43-lHD5MBLk*NJXEO ziR=W!woM;33IZB=?{(nlZDO4Y`OFCB3c@o7wltx{=AaJIg&2d&CX$%CW!dNnC)ay6 z)PKpg_y%mn*16^A?}o&=uBUF4mls+4*3Tc{E8PS4bagzGTgR?C>(6fU%RQg~#D00t z=9b7y#+CVM5(z!6W7?G&SLT9R_?SD*C3QKpj#|NVrR-o%gWn{B*FiethJ4ahsC1-Z z=9WpK5wgJ5`7w^WjUcxk?ORPnx3Wh+q2RUbiTS zIVlg8Ie|VLU#C!3eC&ZPNekm z*~W^MeZA#Yz>~LHR=3cj4)zDegRAi*=B@#u9j1X4X=T9prGyFcBB&X94vJ7=UgCN^ zJmg?BCkek6&wKAHDOI>Z?oZ^ID={So79C?2@m!qBY~+2az8KGgQF(-$!gL+yd_-bJ zoyNP(d9O)Mcsmi6D50O4dtRgz454U$9o>N#XSh<)jUG838NMa&HKbsI5a{79aX7S= z*F*)4b@3!o89j0=(r-)N6RDKFNtt*#dgN$?TXKFhj`JR{;tX$Y;+bw##Q+LT7cn=1 zzD3&RyO(zU^-J0*vqex3vKVC6m%nawuwE93=ra$|XPa0vN5*)VNo(u~7>OV+S*soC zs0wW<*q5_2nDdcH!=i`t>f4%>+%Ly^9M36Og?o& zG|~#(tO6$tk)=(jiut2mytK18H#RljKWZkndGC#@6#kO4E2fm39loGqa#nbR>!}LY zQ-Lhc;rT_SJWDX?vY`Of*gH3v^K!6sXXH!SIaU-$UJAsoJbevA>1cF1gE<>joG~G{ zsqGOaW_Sb_-oitoZ3W>%pJyjx)D4M2t9cC5kvMg=Y}FI_t8)<`@J32szG7GA`(w4v zL{4lf=WvS)A#-bFUjuz>JAgZCXf{$f*V3n1BSc;#+z-HJ?xH$T00qUw8ehqp?x9u6- zwr6zPo>6U^%42dj6b9o9Kh9N+LJT29u2zt zVT~I!G#7~Ogt4DvM>)M-NO-{bv7V+|VcaFjj7{JS*JWPfAl=lvQj^3>(F!2(eUkWK zG%Lwf2K~XQ#9N3lxWI-WcO`lpa>hJ-2SI_D0tQ-<)qbxCF0x%qnQM;ry!VfwJ>^=; zxH&W_7}6#>1LJaId8hY4?O>cUmN!s&@?&}Z#q)}=6_^P)72Sd8S9yyD25Pu*<{w{J z>x(r94zO)cu9qZX*c~r#9w=+4 zq+0oZNTrT&5^piX%b{q zf+nKjmb9FZ4v3DDlZ4$E`65(GFxQ2o7y}Ku%oKmjNO8SRA%&Vh$_S4sf3!^HYzbc_ zm2rpIKsxnW`rGap z9Uu2imRD*7(xR83^np>;2Sr=CkUMcwL^k)n7XTwm3g!&bVJ+?;_ZK6ahc!v74@MiM zFR~OLF9u*9q)0IUbkLv0@}2>tKMSPH08)Rafz;n=Amuv~$dM-k>CfUKe-|KCSXUtZ zS<3{jLhhe(KqDT8@^DkC~{{{gp^{;0|N`&&EdjTV@jXd7|&aM zh;`MohuQd*qI?@N&W|mf(l%Aoew*i+OAfS|ZJy^8bDN5Pu!W9Fl&;CgF@FFBTgjtp z62{@~Uudd%1Jyz`b%O88)VH~o7T9&+T0jZCe@YH-orjhzENXK2)8b=Kl^p(8@yTAp zD7c3Lqlc5>f@G14&}cy&0U#l2Q9lH0?LC+^m6f`8Jnz`U026Ck1x#Cf9Pr{a2|*R# z<|f$;j@#UHNF4g5`ZJYyHF26Qtpj3sxu}E)Lh8`pQJZejFI9Z(nHJ*Zi4s(Q@}8lq z0WI>#MqG748HbAf?W`x^ywvS-n+{QPJwlTDw)& zm|Nad9V%0%+$gwH>N55ui+ZTEI|v;BzbBVpc!w@bWOTeU8OurZG|S#n3>J*g*apd@ z_(cl}Ys|l^2aB?0+goT3l|6<%M9h6xGpgRSjR$3~%d5YM|1jW*3L&mdfn#$bAsF*0O~ zwCDhMhN^}3G_oxGlwh09?qQxwzLh%TyO;*lgR&orL=pBdhB+qPZT=JSXTUS&BNn6Z zAPS0pA$50zPvKpG2vlLW`P|{uu>P2`L{_-{4RKS9cxs-S*YYr>IsI*=%`u&o<&SEJ zK42(fdB39yQE&;UA=au7k=Lalu{;#M$SC+eDbzThP6mQbXA4AaqF;_3=6WLaIIkA8 zX0UfKt<@T*=TOPSV6)ZylD0Q}w2V~r`!1HhWYL9m8QMXsSx!Y%HCgnz*b+<^M44=sPSJHD zRDrQ@U=4}NE67g)+#VKi5aH7p1sJHYK-N@3QIp~=7BNpdS;Se>kyE{TGXrv#$e|Pf zu{=L@%3fQ2!ZV|L`J!Yu^3Ic(Sl-WsCnx<%sV|oIFp*pG>Iv*60<63__*ADDdEYf9 zgh?2gwq3w|B@ca<0CreDH1s%y>+_dG?Djq z??hH0RVgXgDRt!0W(}n;ZGKwoL5QAnr+Xt+#$C=TOoOL ztf9$v858O`TatSar7UYEZgGRJX_2F*;f@255$LVl&S|{^U4?kTt@zAir>O~Zio!_s zEW>z$jq$nc^T@0U+ikuQknR7KUSnczRZW%ZR5TH<91+ZU1+#kr!!}TM8d68|H4>{! zvW1>+Hhf1td4^(3ZP2*O&Asc=+_)?4L#8l z&2>jE6f?lRXca_dm{3?a}C%9R2CWM@0hl|S9%xV4!4L4u@D+ETXkwkKV|5S6m z4w){hB=c2e$-LeyFsP6y)sq@n^QpoR*Kle*`lusPx<;&;m{a#*pedGnRMrhHJdJZJ znZ63MV-oKu`YY&;q@eVWD>XZPjgKt{uUB9k#*sJTVa zhD6_ETewWAA-dMyS`2&lS_9(0?Pk^fWU{HIIEg+|w59|aIE?=_h5U57GludSJ+)sn zW4~7j@9IlB5(CT!E@pmca@fiZ$UeN!nar_rT7B3_x5U@(m0}KaM_cS8wuSS9(FLlw ztiFlSXrxrQp01K(D}=e|oHWT=8r_tQ-brMT`S^!`SaBYkkt=AXPGn9c2^uL|E&e#A z5eoE-q`+XyOw_%srjPcp{b#Cw?|mFCXcW0Uq)Y3g)G2+G6(x;Sky1&St#nbksHDt< zX!y;4(0n-xQz|J1KzK5hlr!04qLT6; zYov@-ax{G+%VAr5!cG48)HD6@2^VQ)l)a*i@+etcEAmMy2P`O~Y_OG4*8g0Gxt{8z zjKZy?d=8ddF!cc`{yB*do91TuT1{lXoSP#Obh?^VftZq(W4KvPz!&Y5$-OL3AKSCe zj{2SLx!LwqAuLeZDYuYVU`uJI90qq?&Ha#fDD9Med@Ajfkn*6NvQc@ivei`3PFX8G zDB3CChN%4|xb7Q95h%5|nnfd2DYiUN(nyDxv*}r?IZ?Oe#Wv9%%cz$AKZ$}$ zA}sp!D5z-0ptFvOo*c5&D>aoCsnjU5E7fW$GqswE#0BCHmxwEBDh&XTQd44~yRPne~dps%BCr(m)uK4B9-%?&#$C#a5c?B7IH_9u>Lbqt` zl|3PfLi?$?M9NX8&ZV8yS1y7dSL!P$W4M}!$);PWuXvYv+KTuRy_GpyePxcqBdunm zbXd5dVWHv4Rah)-oRg}sJo5rIa^|^;F_0|<+A7_vu)Odgv?Gd2T3UtW3G$frqOlOy z(vhJ=zTyVYb-Ixqn;X-$?o z(PX*Z)@0FSDrEJoaJZfISw2FsKuB+fK1*GOK8x5{BHGetc|y*D>qMUghquXO?M>;k z0EW_MIbCOg-9(>d@50*wJ2yNlQ=dfv{1XoMl_HB!$u!NK-}k6Nw3DQ zbXlwLHG+b;}bcNa_TOtWD znX#foSYkO8dgOluCm$|x!ws>672Y|uPvUII9P;+b8D5c8&%}S44({FD9$6EJ`?FAJ;P<%tJ*Iw-s^25( z_bc^#Sp9Zr`@r@YvH98`rMHk^z&pQ=c*Y9~kHw0}O~W0HO1h%0fUy0xI-8vm2fOq_ z*R$r8|I3or3V%@R+3vb0D)T6M`0|DxuBTf|*Kb%`sDxJAUD2C~SvwuJL#30i_*iTZ zZmr+9_MjbFnGS7PTMI*}(q5Pj{e8T8o(dbB4*RhkCc*=&%zNyxe*%~ebKN8NvFz~I zGs3SUd~Gdaa8+_^ModLUj^Aa3yEDQ?#9`Oql#;4XQ6<&iZHJ15e_e0=x9zaERM?f& zSU=GY+fLY8Z=udT#twZ_LJuGbk+kR7p}$c1&z7)L3FCZukR=_whdUo~cr4?qWQVf( zp00$4uBTt4!y~=Rx78Z0Z3cjA*&m`mY^3D9^)4j%Y2tYMaM*^#TqVf63$8T60Q~i8ovJ0c*BRHkQRQb*4OFQiz*>9l>rsV6SWNkh-5>iQ5^9-VzlCtM@6m@DdPCBj6D7gn=qsN`FnOCjakHs|$Y`-pJz{%@%Y z#XkN=_ek*t?W~JykHA`(SDmcD%T<8|J4tCyg$WefjM0wK^R6WO(-IhzzY-Cw7&gLWRG4;K`hww4;uZfnZ*oi?X~FJxm|Wt3wM*2A8^@sGdCrXOXdgX7+qa9oQ5+O#$XU4&g$^0d@|n zh3vX}-Jp1ael(-wERWM1T0<8!R<3l=x6TVa`kL8^K zAVCyW&QYJlj(3`ab=1+%CF(6H&J6ukKvgGkUSZh-N|Ql^kd~o9=r2tkVXC`xa?$tD z{v@n|^s(w6eGC^L`U-@YxwZ$VdmB-VwsqFmDc|Ed-=qI&zWHyUdeB>SZXc~{Ah*7; zs1^M*E0WLxO)bXc%%F9RHYnl@=U^x^P!kx9sE%t6qr{F#Bwt?2fp5OHm^X3$xnXCh z#y6UqalCyN%4vlW_4Y>qFu4c1kB(wY`-D$7uFdC^yS}&KbLOn)6uaK#lec49M^t)t z)W0h==mT<#LzDjF_Bv{@{mSJ{t!?(5>a(g!TUiGrWEt~Gw#6>PThfZ*k3G=?S=Ny_ zJ#CG_E1Wa3I_!~FBaqHbzL>z~R5@RIJg5hP!l`!BEaN;8EVwCQMXr}KY~;Bd_GB(d zZWP~)!{%3{7R6P;S@hGE3*9u9t)iY#P2O#A8CZfFb$R$y5miLj7Y14nLK zJfdKKHsq3u7ZzR$dR}rhkE587G1gfnWnxN*ZGA^xq#zXgmJ_YXqiw@=P*z0QmpDyT z`j~9s0Q7+O4#L1m&Y^0Ai|Es*-e+{09=&Th}XVwDCiM zKwZb;j3uVvLia8>lf9^vz&UuA`-nbjOFTX#s$4D%pp%HX$I3qFb#plp#jzIlbwgih|kvG4xX zsTW5@?@Y&S>=ehSNxuKqajP=p5)soYZ9_iAvY@c&c7;VNSCNVH8L;=U{U{{flq=e@ zt*91S$hw{*{Ce&}W_&i}NiwV(hT=0gLOj1DaR$}8b+rRCt2IA(Q^>suG0#%Aaoed) z%s%$^#jA46=l+Z)2xc&xoV{j2p;tzaIML8L(kI+EdL%cTFOs?neOv6Jkn#MZ(muuj z)ui8w-$C-osea=rL60r>%I^yPC-PJli5z7VYaHLD0UPT+C4=i$-2645?gl zaVG|iIZb9qA2l1Wt4I{&NbWAWeM|wh7~LF4VV}^P6x!$1#z?iXBSiuR%(XOysR;#}OMP{%R?bLITnJ z9Hucxe&?d>tuN-A-^3eri>8)lq>qSNM@bO5IIY_55KSy|~vn6f;^ou+Ng#!Ay zff}(~K16}OKKib89eyvM=eD-{3xT``-a@Q+?4bl|GanT(tm4PgQ`7?JIT4`=I8iL3 zrOw&A=&g!|3+8o0x&v6u)3>S_!LVt&m2n{Su7L`r_A34ob)Xz4mAX=6IA%V&; zkM4m%z`-MKU3!303K4w_f4PcSSsRV4rV4dRUm9IdoEsT#c->>%c|RN&E*f*DIH!y` zOPro#9NwR1MS7Uu?_ee?C6?40Om`#If%mFz;8JF#PItK-ojgfcbS7j;AHBhtF{Pbxda2>4W7f=)| zW%wD2y$L^8gNKD@O9}rxUrO|hiP!NOM4;xAwb?TNja}xSC3CE&IwHxqkwv?!u;WO!GJAJ?2^Duj*|)^zofPV#6#-2ll^xnHvA1fL$PzV?s5S8t{m|CzKsJR(gWQtG-ydZ;L1b!$j6i8FS%Xdan@GGMhM`v1$aM{rVu>Rb3Br-U@sBa!>9YP zuwAG#_;gFjAeFb(fu(i?&sM3D+Rjyh`{2=H7n2TdqF5Myx6H9Zd0=#ePu^G5=l;wY zl#IqKdWRAISZ{>6;ELEKcENYwlf7_FNbq9l!_eSBY|L-|hU0mYL%30;^1$-jB1PfO zEBZ(%Hz{>7EmX*bsR`7`jWJL{{hbl;9Bh~!s1dCtR07oB611KhI3vw}0*f}Xtg&KO zn&Bt)bnt8nBf`k#V9KsT8Kc^vYxp$;#!r)~0qd=og*Dv4!AET9X8hJ8j$%DBDpnIM z`z&4f;$)H7-4m9WM;10H^^{QUs58((7GxS#964lrJ}KQoP@u+?3HP_Z2zUo&Q`R>m zYCEsUgxm2W2}L8Fo$oain^Ju|fu$3D(;o%k)h%_o5%iqe8H)DQ`$6gFykm@qtaigh zd{yRKgIF1pSkxoRYBKZD+a#r00%;=mn{HF+ZX(_ly591 z=-vbYh-}g36LbJAgz{-(=B&tJP{9EPp*&lur{_xs~55Fp(ODMh9F*fXv~F z5S?d@FCoMB2UeF`)rCB7hg|;sq1e!isV#WuC46N97TLT)*)83ZPI~dL2BL=#%wMQ@ zMm9eISLI^G6@s;(+nI14^QuBtqdumgI|QhQ%(PKPvAC z5n&W&cbLCBq|_n{5|5epZjrS%UrM?=<;CbsqMsQhm06n#OflxRUb)R@NMEBJwO_Ph z;1n)qbhr$L*#SDbp~NhqgcH|bRpY45!tW<;3Ai3VtOsyuGJuT^S!t37<}+Cfjw3DO zWjq>`EV@;CAJ+E*>^;*K2Ue_+(YyfzLf{U$?Dgj@IZ|-q_wvz0f7tyj#vLub$hJ2o z%G}0b>CxVz-ZYPyiX~7~GJBf!u(fDVM!z~Re;i=U2Mi0s*MV@q>fn$VmmYpqs*mp+ zmG=|Ug{!5HD-~F1wnmQ}m?v8KW;r3|0Ge#uW>Ux|R7vaUZ!iyYpM_m%vS=l!$V~c` z+;7QA{LMUIN6gm|%T$C6(=%zgL>Y#~)K!XYkzwYaB*&R>l<8GZZ&=KY8orh7DlxU8L17rx9a1u&jsIUB&d;U3W$#g0U=`RDtU z;Aw|h1eufzy*tbcS4ukRJYkHq%}(K+Gi(X1E*lI&e~y4MP#< z$x6eNr$1_5MMB`M1cBt)j}Ll~j`1I9-Ul#eR+!($37pB2aY8pP+MqXjPm(2M91(NZ z@w~>jnBlF*F+BOf8CYxMl{x5s)$Pg2zU5(8g*Ut1yXUyK@pyK)2fta7UfilVwCB;R z@Je61--Q{c0?at=P-dL=I>CS)=BLPf~ znKGQatOfihW&4}55$rEOpb6-5J+OiK{PKxA=wR(9UUQcN}+?%b$3Th(kZ>VTJu-O+~cp3`4Zr^hM zX`yAIl3Voq&nO5@@kM76C16fT1YOT>Gt2+3sL(gKWQg&RV9rK_s#&i4c1s=5SijDS zrh~Z)0F+6qc@mf0kQibslXBCKl;$7o1lLOf5R2>Wis~c(pDc^!6j?p|nqev5%hEA| zQ%WgHt)R)G8^_Vzru4MjVN&NDnr6WPKZfirVB{mw!pe$*UwRY^TFIjQWRpx|$%LVd z(xW*GOT44zU?pb74$4;@hebZ#_mW1iHT+>)3j$fHmwJ8~LqG(!P<)LT@E{4xj#2D3Ag@JO2U(fh;T z@jN7PEk&^7;Fcedq2#-(mKoqTH`&pV1w0^5MaBkSDTmUVT=!%_7nyA#dGfoOMclhI z=~^o9c*`@+-@}a_$#X58g~zIz+L`0;J= z_pGMt98!AXH_4(0$Kl_f__6x_6=-}R(m26kH9tW3?ck&woqVLwQT7%JRgV%JpI9K+ z>J7x7EOGZF*)}e(sunbyy}XjK-ZZ**q|dSe+JKX~&uBC8px@Bk4GL`FOZM(?zG>t>?RZt+}s*Mfs zsi*SfxjcCjiCsVp{+!RI!^fqWo4Rl2rdPj4Gfl6?_p1T@q)mMdcC<@VuaKs$CtQt| zm?ZHjg9tKv>!5k|K#lCwn0)LxseO#xMV0^7v!YnEe-x@)k6=DM(`A;|mGamvj?C}t z>1rh5awqwxrR&T0iXEw~si6@$OtNUk6*A5TWd=6_oY)>sP0>Kj3k-9T~%gG|HyQm`BsRYVjZ@Zs_$)d%o$hLc=xE*HU^OUfd6DRO9 zAGlI$f;Zg9*EqUlkgItlvF4-C$#fh7GMUfejk+Ce0m4g<$@;mXMAkx4-DqQx71fPp z^4+vi7SBx84J}X7@>I39XUJ4B$aWyaA#csAa?yQ=c>~oTWUVkSP*wM$YLNm6K9?sw zZ@X6f3ZG}_73_;$(I4T*;u>qGESjPC7%#z5+XR2^A*|q)DGU6i=J98tCKcu@I9h-s z8uuBivib2dYCTTeLHAtn2~5LyBll3dzYM8R-%4X6$ACn<3`>i(#Uk_mR5|7&s$}>S z;vXg>EXcVkp6X_&l69?av93NF*fmVJnH*9~0RP@Bt|!}@K4eyLKW zYnXTHZGt~Z$Ba39pLEQxRmZ%|^8^9aF~iRx*b+TqJNpYdrr3I#U=x5?J))MDG^1j^ zwpMq^_u!HTu}xShS+$6-l%S~CG?4dS#*P+*i}X$w6@OFFUyjP)4~onfWn(ww)>IYx z-GX_PrQR(FrXuvRmQnXqUAOelbk#Lerg7S-_;0;8t9jKYavsj+>GzHVpvY}x?1Pfg zZV%S2Z-m=m>+3HWEn1KQ?IfUO`c_EtQXZsuP@Q6g7hXM#gdIR4R8xMd7Oxf=?s3 zI|M)997=g$Tss1R?h!nn7!g<|zDtLVr^7NZyk82#}ZvY^ki7BWz)&LNgJh|uj=b5>;4b9=yOM&&gw^SyV>Z^}OGS30H68Eb}o1{Y==cG+yfB%oZetN}P$LUC9eX9^$qps+SGJ z_y0`cvSIL-l*x4#_O`C4KjTIKOP6dFqYu-@_yvt5n!Nl)vTEFr$A*(*5z|V9xn( zv{yPC!;6JCEs+%)owcW5@U0dO+8lejFk}dTdeYnMych70rhD-f*t@`R_*KrNywd4{ z{ldnN(bM%5Ht|{z(I!rDJzqsTUorbVN?lv!mhn26@#H2C*%xlEWoJ8*?8g@jC3|h` zqC~#>!p2t5+877xJrQwkPV}J!dCzRWhABqohfFaOLzttq}G~O!09z~y309nwLIr}P~;h$x_JH;?iWG;SK#G75u&?Y;hfso zxfG*F8|-k3{#Q?f>;Q#4bo|v)IW$AK>l<}@e{5ePDeU2T3gC0R6{zHD?IN$GOQp|m z{^WYfdd&F6SCUB>@8*Zq$Wfx-h{=sGALlQN%Y2^C7M5b_h;5VSP2Ooi7O_^=S#WiP zGzP||uEOdN-XHB3sA-bS2p#K3cq{JA3ZG@D00js(@?bBNo|L6%*fxv+b1s?^`i;nL z;FV?Tm||%4j!MIU-A$3xC@>(gWeOHvc}y0)a~ULYlWS=fB_|%?e4h!~)=UA>;pZ|6 zoN-;B!BR{Y=ex z$0}jvK961RWX$~Wxk4VO7*)>>Ru|1$p#cr!VJk$)XjP0#r?h--D^=(UcK* z1`U4sbNQZslRdfbIR~=R#W!nlT=38#7TE0i(D-=hib7tWoj(%EybG!Ge1zBNpdC19 zu6I)k@)GP}dAVHgYW_QDv!&TO!%6y5L$S>7}X* z&O4zC)}3K>0kYX3_P)>9lB1e`s(I6gqBO#+M`^_Ffem29^@!LJeoErGsRjWEJ4Smp z?!8#|K==yX5Wu1N$dF%9@9;9eQMl}r>Xt---73`>x-(hyG(|v>;^Pm>DNvsI($AT+ z@oTKFO?(-f+jh}w`b8_u6GtG9piy0ae~brjo)h%KUIGWCImUuqAF5Ih7t6Sr!bXYc zYbBXO#v%^RDhtKn&S<`Q=saFkXYAo&S$WA#Xp6aqY?jQQchQKwo@^}?m`ai?YD4fA zs@V$i=lXdwx~FlZt&rmIYt&qPD@s5?k#OI{2DOl#si5loZ(wE)jC^%|?=&t4qVGY7 zH<)L$&IC%|YhVh^7G=jvUEJ4rDWg)AD{5}J<*z5ARW+@2I6PB%R8~q1 z)M=<0EID#VS820FH7^G2SYY*a@%{2%=cO>xprOZA=GL)SVXitX9>kMm#hW} z{Ot2uaAl?9k^tQaa1}(dHp&uG7Qp`~ex%!ie)zX!b z$Hx!))tl?_`B{QUV;BqoFZwI`lmZV2Otlw6#W#eVu`%Wr$ke6m4;V#?d6iF1!C&yl z3U=_t;i0SfY>kJmW-+1!Eq5&yy)7d$n}-W~28~LGQI~t@mIaL4 z|6EyQ)~eNRDmX_!6@`qd>3$S}1efW)b4Jqm{hE@T3thSUry$CPHlY@NV^$q?(as1c zDXZ)B{+;_ttZv=VU$MUG$nbiOY)cp-={bl-nj)CZq_P_F3U^uqMkPR~bUpzD9vs9< z7X9i1py4?2h)j#!<|hvdSz{77WfJ(oVJ3l>Nf2NXR5J+*Gbe!z5Tj(xf$6C^fSHX~ zF$HSQUa024D%G@y6^hFo@X8#h5X1-3A0$$6Ugm%-zS>$Sx40DIk36OjzpG^qi`mrA zOimZHmv?+z%?Cg%b%|cMNF(SpPp3WdAcI+?J;i3e_PEU)?HOTy^%Iq*%=}DwFeI!4 zyygMz37D^FPqq1i_DnW6Y0q@?S?!r^{zZE#%-?HIo%t*6Sztb>JVqnUGgoN847b^+ z{nBhRto_m*=4|a>CH^VeFXX|j)_x%yreFJozL*u--y;6=wVxU7Fx}d}NBsS@f4}&h z+J8X&M}I7>H%###)c%9we_#7M#J^wrMHR!`sr^pzw`jjCH0G1qUo8GdwcjoNhqYgs z3|p!FWfI=3{T1S0p#5I)&(VIdonlVc{%Y}062IQAsyCa)rpLKgz*F!l+d@p~j8r+s z^XzTGZXGg2hZGXBNr&`Km85V+CUv^3U@-|F(FxyCr|RQThc0+XhwRoNBM4ckL!Q$i zP?Casb;x5nq=JxzI^?H1#7juM4*9+gK~7yTTZcp>#A=?_EiVfh+vNPOK#X5Gt5_n> z_bzaJX+T59i@XF4Ym$r6KI`c#6o>S<{Ur0f!FaJtsZKux8e;N1bO6h{_ z?X5k-gO_KGUEaUfwXffWa`pzI2f?aVXRv&IaOsBdmV_%Xn9cb9`LrOI+)7yF zlPY6tu(d5Gat5OmcIo(h9Uu8DK+%I+1ge!J$_`&fdjcLxxH*6Ve8A)H?Tm!*GKc-W z3nOFjGE4oviz6dxoI9A@7TFc#v0kf^@Xzf0gH_v!{)FhBiKu+0{u}vy1Fo$B7kWRf zy7tb)voqa30Xo7Z83rjm2-4Ow^l;G-M(jo!8=e3jTy2lpJ6#6q#f*W%{@&}TWXm1`r)-+2!f}#+225b|+QkEmh@MbDoLODXGa^*cWQjv{B-udHmJ7&hQfk&kG+i zsifFXy29X=RyS&v38$52Wjb*)fa=Vhc^7YrcR?Rm@h*R8Kvr@jxk%X!5}Q|ben_kF zio9Jmv&3Q96V6vMu#6VMTyuOcBCxdX3-=*J&M8Xqi~!6LY7igF6`wC2M<%K?z(x&y z^jTzMOJ%9%;LDgS8Uhij(Yi#gX1Tu&8-6PhN+kb|#K|dIgG3;SUqx9YxJV^nDDrNk zz+}-HRmN7cNk@NmqokH-uZn)iioR7xKa+|+S4H1#MUNwzH%ZBMCgCEFIFq^}IyL;i zm28NV63gSIQ=}K{GCmTI*b96Q54OnO#}QSq8Wgm`Qw8edeT-T$=?guu z3f!~xq$y8U-LuUJC(lqr26a%;Pd{SmS<7Tb8||?(>DQ=we_9#bZyO0m4_#>b=ut$; z?RouvkA5vry`%EtFrPW+we5lny5W~03(IkuGZ4^ z#j?1=gPQIZi}(3E<;ofUDDxBY>Gk?C*6Vc;56(JVPZugZOxE--0;R3IjTA_3RkT(1 zKGwO~VRlZB?vWut)_)WDo>)V35D@Y*Ix0ur=GCk5M|yh>M~1e0r)PyvGw&j~(&Yu& z%sG;mt>WpFGhO_0==QkVJ|dL2-B8ish>-XPcFPobTh0!6n_iAJn2C2&U%<%UE+-sg zH~R-VMcF7H0nE2(6PRS?3Z^Nfflyk|YPTXGqg?$`EA?jTbec<+E zn&TPzF|Be%m*ZClZnn<;Cj_Ss-1MXF_L1^ql*fr%bE?>`$6pU2Rugz<0Q+mS)PCAs z&XoHQb}tfaj@nK!eW@b!5!>~2n>t1g=qk=k#dodZGF^prmJsGF0VGkA*Shy1T6*js z!)M`fhOxpHEHw9V!hspKLr2sh76)gXqha$+x>KK@&1Td${e+&-Cs@_dF>8NI$H;|l zZ@3n6()iy*%B72Z^CG4jA#-&I@?1ubIF>o3ndFyKAFo!oN*3)IDrn$Z8Y4|~%q$8; zS4Gy_WYL>QPLa1n7Db0wNoCA;5=%iTT`bfuP=^tM$$MI?6i|hfCKi*BnJ47~%sUhS zc*!s*(g~0Nq6_Ayf+BpXTtW9H`fka4^*35GKk@NuIfRmBuv(6S`rawi0IQVN`Z}ki z*gE5L`;B)v$CKA2S2r?P6V+>y>+gvSG{Xqv^f944B&!N+lZ|2fdT+SEyg|h(M*0Bn zNv^lvlU&bxk{b6o&U{zBCpqzC?@8{MB~aP#Nj|5)rrYmH9^=-dJQXey%b(*5$A3gaJ3lCS z;DCTD4@%1OKyqG-T8=w0z;^HmFi zT4!-WugD?N!5f5bO_vilv^zi7CvK5bIdPkJiX66O%UN67BT|kI;jc%qkeI#a9<24jtsq6%RZYi$o;gUv5qp8!CvOQ&uqpbO( z%^?D;AWnLbuz|e@V1-G=JY9gMDg3nxY`csA)YRnsa7&I%8Y|%kYB19F7~1wp0Bf2| z$Jif~fXWf)fKfW1YpF}Xpw1tletciDNK}ZT z9p7^;JtUQ!*gyMkr++-O;v@~MM!aRg{q3%BVBbR426_XXDsrdy`iVC4 z>ywvcq_6a8qmlQ3sz^>45qA9b2^lEkOnJT-{_9X|5qE$%W#pMC6vlbs2D>TEr%EO9 z>x=2X!So-8Cch+CbWt(y1u&0P947|JqES|KqZJ+b-WHy(=L5?@$?$ZpOL35PAAF5G zf0vRFFX0Y|>-#^H(0HgW!Ujc@>3?P7TyCS<7Pb8XfXT_Kks>SJbLwE{Cgw!4DER zw%Y=9uYhaGqHpTD^&2U-a1ImXB|=`kVca-Nt13i6kwdwrQPMNW&pqm-Mr79LBJ46( z&ZegLtbi@2o;LMnbzXx7H4MPCEd_zP?u!Gl1@6@LtU6@xkhfME?^GG4`D>O7F5iZ8 zt6~vu;kHN4GdCz{-AoqjG-a`P_x%qjiFLry*2Z8~E`6UDL83>q7w+J1t`&l?hbAy;la55kb3w|%oSGLWyJtMV<53UBhc9zU9M*y@J=dXD+C zgfzL4q5Nh=E;lwu4`hd(yyC%DeLh_lIfvzCnAwZr+2W8jLDF&Ycy}^U;9b7CB)fh8 z;N+pVbB?K@4#sYg1li&2LvM5N7O)Q9Y*_}J=IJSv@K%tZvv!HTx6D!C&2DVejNo{# zENI-#9p?8^MlXccmbX;O`;qc2pIZzda$PWX5kZug##%0ioAr@h~*()>0K@F9=_nsa8^cR2Yi&=P77-j4C<8 zMidh&-BaZn`_inCF=V*lP?hJUh5@y{rgt3&tKvU%9E4TboFk!;gCT>w@Cd?B7qYqX z%0umC7}aJ{Pd5L2i&{&{Zb4^-aC+*)=yQ!yFFRMkQuC-#m*_^fmBPwdZ7%4XQ^L)b znj#7x@A|36`@yUp=hD#C&!)s;Hq+Ejch z!u)||eHqI;Yl#d^ z-o(Y~E4LAv)k3i6UiCd1X9EcJ|Ns4es|Sjgv+DEbwA}WX{(jeT_gd~+%l)~UCuq0`;g^6YPoAIcciq9p?#q_D*>az>+(#_;LCalkxeF|J zw&hN;+{-O@gyj}nZm#8aEYbCTXt{4#?hBUtl;u8Rx%XM_Ld%_Pxf3k+8|-f|aP?n=vDWw}pS?orFN=lf)Sni)J_ZODC+;SINZk^>$vD}d5 zj<($MEO(IQIxY9BW(%Jz_chDiXt_^X?kdauf#rrRx7Kp2E!S(gBQ3X|9dEVM?w6F? zFI%=(C;aWdJxhhZ{Cn+A+OFM4U)Jsk8C<%YXXl(G-pVk+G0EX}_#6>O*fHC2r=#9+ z8@{_7QydcTDcM{Er~2eBv17a~WEV}@hS33;T-%-Fje4b(8paR=cO z9YKd5cQ|2F)UTv=5WB!pM`?2%Gx5*Vf&4~DClJjg?hbs@_`c1GA4v)FC%5VXHCy;8Qqsq#~IGvuSG$zv7=kZa4oU^lcq=3NFkfB#hs!M$xUxsaIv) zrcz6s#NMh#_e`tW|9kb_K|Rs}JwkSU(#L|y(!cVjzjdFcza@|UleBgG*Z4n|9=MG! z!94vZb3lTbN<|kw^7)*`dKr=vpR=K2{XtykKa0= zD|cK>ZBou0T{yot&bWQnxI5?0ye)j&opW!PQGeTvTjvNpoH%FJthy5-#!a*{`oiHE zHM2?3aHmRtJ1rIJHH)<LI9>WyUl>n`P{M^p&2ox2pMP~4Y z{cDf0P%HUw|4n3m%uso&7#+bf{U>uYtmvg(?kt5fa|oYCxspdyT%p|cl&E`H#{ECZ zXOD~2EM-e+LZ!c6t#)pQ;{sY$=J*DEBJRZ*?xYh!>=YwaOu0&H_lWd@;OLd~n80J# zknZ0T&a2Ae!EL)rwdZT$x|UEW-RMi**a6B&f#gQa&rVsW_G&B8NdL%RrJ6Oe zMo1ay726&?yS`bebb7|P)d+l>nxuuoJ?OvA^QSZKVAC4AOmQbVZsPwW;HZM%8wZTC zvTE*2uudS8vC`aVm^2fKO>?rsZKm6xdB7>w?3e>|vM$jaH-X;%v-_jF)=Mu5 z7R?~>ze*=%>G%m?oW{Pc^HRrON4!lv#)A7YpBkY1df$;Qr~7o>#|vI-sxy_cKf9NY%1ypD&d}lrs#MdJWQ>sU$8wLUQG*xLz3UjaN(k)Z8BH&-llVZb~m?l z3SD1U%E7_9wVqF7US7wStn@-HZek{kW2N^oPbN~tSm;QI*c%C-pi)&47vk$*=lf66 z3Zy=2y2fz>wf$%H1c3S~F!kTTBUDk~o<#a8Rm;DIe*+aZcjunh{V<7G!N+THC0_8fYszu-+XOgf!3g{ByR=2>6NyLXnuWW!ccAHg znXFZpv%eHq{>0U*RA#8GPqK;&2cpwS*|J`BtwTnkyZy4=Zs9$p|Lwob`b9C~KVN?| zy|THCuKdMSDY&n}C(!AAf?fX{pcQ)69n6v@z20VyqU>(F&ttdqUyYT_u1Z=uj&Th_ zS7eULNdCL}8W=&Ll$`tWD`RQbrn0J^e_!vvn|;%OMzHFdZp#zM|C16<)_>_aC^(gF zM_0L`-LjLF(>1+al9r2zTuJG{V{29K5~%I9VQ{6&hbV&)yqV z?}YhP~T&s7+Qldxs_AG9RQ|*^3G-70z1zWK6`bcd1vU zVl)j93=(>x(@OekMOg(K^xs6*4574s3o0uZEEMWnZGWooObXr=pVH;(HR0s((r%%0 zvXV?lQK+uzyTadnt`pah>uAjc{@G=BE!S?>*DWu-`gEV;WaUd6r6}Ew$B>bz+E7G~HdDD<~o^cl-f6iPbyUHu4DH(;7ZI>XjM){Mz zt7VRgY(iF#^n9I0xU5ZjeoN0uetU)UkzS;SGfv<@rBq$NaI-qkv=qHRS$ct9Qj4rg zw^~-g%+)~Xa(cDYwK!C(|9!sBy(M|O;$4~!_@F2D%?_9GI3%~Tdrai zLuz~XqC+xivV;T`X38ogdk>M-=s#QDtM@zUTz*oXxL%{!OSsKz$WCw~-)Zjf#P~G- zqU%fV2L(TPd{~SJ@?|%=U&j0V>lR5wOptJX} zf+ZrCoXgz%dcBv~ACj+os5lR~v%Ye{ZQ<(rJ8Nb&z+&H_ut)D(W@Vg$WM${%=Jn|5 z?A5zZe!(fO!oK}Z?O!zDwBmt-1`io}`Wfys&-#DZ`wpf9x)Yj{K?K2qt?vsdrvK7C_i@#z^Ens{Bxkj7SFDVn%UjXFL(4L<@Q zCvwP2ual|H%+hMpGmxT*OI7zrLsHW-G$;dXDz8Cfsa>RBGh3@hYW$!isgZ@UJ4J+q zq5`PcKO;RARU~Js<1#Zfsr^!hs1vd>lG75^X((zCsR60vH2i`Umy$e`FWxGW+z&|{81X|SXM~_$v}TEA4<m zEnb^1QARG#tljBaO&ZMw`XyAIpc#}LFUrykL_JMKJ881Dm_HafWS#OemE?FkWuazKE)_#C zS(k)gb`xks43y^~`PbV=%#;wPixboq76bg>Ru~Gh>B=t#8JhRAG?}Cvjs9?P5kjZx zG#RPMm}nS`GzMrIWTjBRPsJa5-m%Ex6S zW|44aY3oUzpKtxK$mwW8Ytxg{bfqg56+_FPh9BK{6dxD93q)Akk<2RR#w=MNUWurGUGDE z8|Bsl%m2SJ!b({fpDP=Mry@)r4oKIua^dOa!nx(b_$bOK|IBjXS>?iYvB%OqLW=k< zPu;Yvl$5g9dYBo*e;mUfXjPaoGZU3EFfh=RuI8S?U2t8sPgoCg3-*quXH*#6!Kl@| z`WCTCvA7t^)@I|ffCMvV78U>t{G;H17c8KD>0e#k%L)HpPJ1~f7(MfQ|HiVH{~61V zW@Z04_GbU&CFHQOe`O|BZo{`k&qe`yUe-TDn1^EYk&bj#Ll zpKafJ8KDZ;0OLhUwpK|G!=TU$*}r z-LSqsoxfrIx7!~a+%h$_CEd{rR|2kv>Q1=S#0S1!&@N z52-2r(W-O$Afs>>+_6z|ghq?|Q9-a>R$^8r9+Gy*O5qFdfHOLk)lQp{OwU3)#bv}N zQ3S3aO}~sR+%O9v8KpaPk9*RNQTlSRe3bi>&NRy z>v5n{jMtCP(L0-(;zT!Y+&COmJw5ed`iVH5)vecUL!q|iNfu9`rrs1f78bh-z`&<(WT!2+#`jQJt%00GU-Bk zXk4bQ9qzRUX~NRAS=!QgAxQ_f8l9pKI^v^XCp?&UW(*(C_5$VhKt7n;C@m5nGaIF&o^oNz zI{`&y^Z5mSHx*4`3lnC6GAP|B4)q)5Qz@AW`dF0H$Gxg4v*ju({(m1Av z3e_0yT6Ab%f*mV_il4y;*LkYXO3ADcZ(1&%GgqMPiZCjJi&U~m9c~}IOguR=6CSHs znlVdnOO%OwhDZx$aAVBFoiX2@DAQ8OEMFB*HZCX2mp9WSmUFaVj*+&^5q;noZfCI3 zTE|i!6~NgB2Y)5=sazjB=5xP;uV+z5-@0c)ebom$`MT*Md{qhIz7~;PedV(5)fCL0 z>+CDpz}E!%c^g3I1Pi7efbVfZ^Rp0dqhvNZOW2@XJWj*R+k)i|g!_4rvKM6eabg_D zkqr|pnN_%%mUK2?(J2D?kUXVDqHL5SkLT>nm|cPuv-7elurQcu1%0ciC4CDSPV%lw z=E_xMDy_XI=d57Pe#9~6-q4hJaE`vVTANtw(mpm}oSg}?n`!0A>x248AI`;sx#&QV z=FG+0jJbF@7)VDgUW&Se_2<0QrZZ+naoh|GHshc5$3VCVY$w=T{Q>4NNN#VVI2sr5t=nu0W2;FA zKP%>^tH@eYJIS2~xz(awZ@Uq}dVJZhY8O71uEyCQ5bZ1*2 zOKG8eUfAtE)|oREUn-Nz{{S>mDid-q#g)tFUfGmcbIweuRm4i;N)P==$JoVK13-mT zo~s#iJ!8Whb+%};lTjK+bNQ610VRZS_GZi;{c4Z?vk$lC$9mWrcuNOq<57+{H{@}g zl4D9gjM=IdOvO1c%K~$Qsa6m>X7@8?I{GV(Y^4b>-2Vs&2!oqn)naXm6w)tu@6qh5Q_htww)2 zw`i`KotdX*2j{DqZi$*DELO9~kJN^3j6DK1>?`!KQFxXOn}vKMk*5~&Rz^M*@>!J2 z=Uzj}9JmE)WwD7TwJ8s_;VRj%N?3aC%~9>@L*QpP|BeqP}IOBUzuPV1iov)U~5RS)Fh z=NQ(Vu#@;x5FlU5%j;nrC-f=5cE)nf7R))pnmI>WF=y;8&fzMpdo45O#Kkk(53Is0 zdAl(M#!M|!=HwlwcH%m#NpEUXtO?p!%^c%1lGc7^p*4>+3zO2&Ha9Eg23xzq)^4!1 zTZUj~evIZIy|t3HhR#?wJ*nM>LdNcbN+jK}e#+y7wbhcjVBg@^SAK298m6;hhO6+h z0!oM##tZS4ui-rv3e$YM51XAaWu^zsc)8Lv-bTjxIV)3U)ek#5zX}WUH}^VYKZE8| zdHlSyW%4+8>>U=T_&F6uwxK?8*P1e|cLcM9ZKO2}*_peWDYLvL%_PxYkS80kAF+(-Gho_;kIL^s}Il-Pzu%}bFeSxh(*tZLRWvt0vtTU7k*@^!U6iQ*tcaQr^)D~wx zQX9q7*eY7AmgjY^ZO)vzx$3f2L(q?(JA8klPqDwcVNG)LlID#S=5Y|#(e~(zP;^2^ zG@=93{>In}&?V9#*NWvrW+G%fAftkeML8MHQUO~9GZTy<{+y=^Kv{t}?@)QcOkcv- zVoP{oYda9x?y3;Z0VzNu%8wW=4+0pUzl zY*%PgU}cbxYdvJhzKNC`y9YW-x*$xvhZV=RQ<%mF*6o+#4q6-MGdE-AnEUJu_Mu1% zRtQi0_9(}V)^^&{UlmV#I^yYECzt0e6wJa;SxEa1*WQA)_qJlTk=D#M+|ppK71k9` zTaM*|_`QRnA?QP+IDT9k#|e9oHM2*b(;mcyTd{DQQ#$FeH&GnQu=f(iU@`uBz;jS8 zo$>g2Wya)jT)K*--?wHx&e*Up>sA=#Qb55ZTS3VxAdcy*jpKP;jN`;JP^@e{)C%jB3gg=z;~VSNOlf>$ zzpc)(C7?xAkE4<~Ca7@6v^R=(cfnbGvkAX{VhE!TVdKb(EVd@cMuP58-jD3qNB1kR zT*Rx-xH8v+Za53OBT+Nrsn7$6jX5gp^=Ks#CE5+~Zim#&j|v8yDB-Sp5d*D=GtF;s-S1ST==e?wkoV zj&o5m7hNaJ*=|O8-K&^F$IeVRu?TvGiF3rzm}7IBaKf3NY@x%MTpq{wOSw4cqHW5t z&p~1R1>J~G^x~M07$*KbZ?vDnr0c=D>X+go8>@}-IV-W()l*w|8LJ-YfVQfz{y4JS793j#nl05=E{-2dr|3M*m>SMbkAn=7EXt9`@q0JU3|6Ov zy*n0pD4+ISjx7bHBnoXN{!~lYKnxTA1E`f4CjJ?yE`|AZ+=)*&ioPm&cjF#cbwsEI2UFbtK`oCxUJxIEpElJI&C<%g6gI|#9kzi;||-f z!)MIcUaS+#b#`nq_A8uiim^JyQd=2Z*1ZGrO%mD~=*zK{p!-sJ2W^;99NoL2JchFh z&3WVeSSMc<&)@r4W1TdX=kK%oDYX1L>4CE=)(Q{Y7kXeV^T1l>5njPyd@jNn3FjiN zwH3}P)~p%bL!!NQ;rtmkOgIj#(rN?JXas zG=I>2vIp4{`HbTDelsq|y^=Yz-EUcJUMTHg!rbG}9d!2hc3`TRWxE&cZ^1A<7huf- zimX^$og-_4@ol}#yg-_}!hM<@iw))2FQBocFWPfaWhCf6-*)t&7|$SUl-AUYHAS2w(&^cVJWYIN5yqE`G|HRb zm1CDdx&dTU3zSRa18LCX<#fi&T7+{fn&fG0K;9IxREAMJ%AoNqFQ2o)8rRRF*o>a> z7+^!n?`_7cyy*D@>Mg=Lfi;}rZ0{L~IzV9qMZKi7NViv{!-oZAXM$Ss@{H$QyJ2k; z!^9VXR21gNfdhLf4rNmrrTO@0RoGLYk5T!QANt63{7#u{_RtaQ$( z`8k6<0{gCSUyk(xDO05U#&P_3z&?QcU;aw4a%=*m#Z&2Fc4ZisR1! zj;sJO4?xOP=t$!gh z`Rb0j=|0oNz~4inISy{P4@tnir8n+N!iDo%!!5Ya$6D6_e{VPfly47v;NtU8na*_I zi!*1WJ??w$nDb_=@BFCB0NP5mp-FSD0@chAU(G& ziz|@oA2yR?yFe?Y{CxjdGS^7-leal@^>V`ap>YU8yWG$&+#73!d&y&mH{;JE{J6xk7@S3%VFUi$!N-9|xy~rp*$dBTYMSHzV+)?$e~M>v`OI1??EQG$ zz#RDgGw1gvN&oIEIkpkBPt>1(Hi-GE$}m13_`r68ZDCjJ>F($Ycjy88mfGTrH5?oD zNm*GAG`8?Ng;ZCU4II;eE{Jsr`Urca)IZfXa;y>Po+#r7J{jaBjcF$(o^O~lCof^# zQ=fjBk23^F%44|+=SPr~hqJ`E+z)%&nBdInz&JI|7g(3nShv-ff9i}%#U6!j1r-d= zT8_q<&K%pGEZ_WPF`QXviW-- zX^K-E8-a8}zO5LCrRy6%4>3k`7$cZB;(RV0D_2hAJ|470EMJ)a;+(}87sr-?bHOt` zJezaT;T|(V0lS$nmqO}GJfp#SZxb#&f#L9c#KI5p{20e`L!7?^`^$9V*I(gTh1AYw zmthysc2N)aCf2O7_xp;t2n1r?bag$FuVc;deSZ z%VP-__c82W*cY8qxARP_4UKFAtKwO8#RMnxyCZY*x7&=fT_Jy32rIis5bWlFzQou) zWvz8@YTLYqPQ|KXzp9A3T$`ab=}EnqkJbkvAI3Ul`0-A^fqCIKFkz4Jug*Ug%ojV#phA0tw+Nfe*xqwM|Wu8*$l?a~oWix5EeE)sU0r9e}uO73}bzUtGZs|M|rg?C_so zT)__i`Nb9N@V~_`ZhICMr%MXLyKjNnI!#(8zQ&7UYzZIRE$#iRbe$%GJ_E}{Y(#Kq zRJ*W{#tA7Y><3PK1CIZYgt4V2!T5?OC_|%RYzU?KmumI|fvpEZdu*WqbUP;yF&n6FEg4mJ8G z#bw|lKutyz?#*6G#aDe9$^D=g-h?A9B6JDCacK$UGV&`$1ZmBeADOI6A+7Bgud$>U zejN?W)^L#`3WeJk=$|x4fw@#K+nrAD(fOiR zO)&!cJ)r!Z(i5^$G-#?uv~>s<#DCW)zIzC{G-@lo&lsTTABV5FLgUgBLjo9kR~ea- z3AMY&rQrR`dnTduouAN2%!ksDO-Oqrr12g6q^wrKc&MQm>2V3+noNAq7!PZ{3)`oa zh6A&sST$L%x9i`ZzRnxcDK2v$W1sLnYt-NDXTCh~!zPrkmU6dCNWjOCnRv^Ux2p8% z(+p))MzW5#0y`!2L_(MTWE4E=qcE{y^obR{YpWM6Ns&>k6F(A$;B&LoNPO#xS4}(N zEnFeZfyR)Aq9fBI%0%F|hX#0D1~10v@@M`j>6sdFI5k$5Nk^tjWiWP<+Dbv$JEeX@ zq%l}a$@@8F{S@K&$d1GqTQ2Gv$$xmq*iY0NA%m<8Jh_%eN3s^V&a!|k;rm)iqxX3$ zr>CZ3{Dvl{q1`R0hsEBmFWDEirRHR^Mv5-@kWdPw2!%=gZCIcVsi4)0Rmyr@U2(z29b<`SJw9v2&_9bac zUrhVsYg$PO=Bp4{K@E(HHf1rqB2nxpU!pLY0+Q(y;q;6lkX*yJ|K<46)N)}#$&}0Q z#k<)l8t9C1h0k_VB2W@mM;4;&mX@JOOvZ4@h>A-_8GQ-dNIGbwvRIt1TGQ-4J(LO@C)sZ_d1R_AjHks%nGCq><*QmV<)8LKG^ zMPf!ou|&Sq(&E?%gmq#ig2XUc4Aroa!b0(3=~&U)GXEq^{J`)yd`#?@rPE=sF@s4& zQu<&?hf$OxC_O$4(&Nfn6`G!w7^cIHmxnQn&%D!zh#6{{pkQ6stYn?27hA&ulJQ-) zE{lE42o`sw1-cwad~arDK}o{oo9)Y}}J0 z>n@^HNCK9t1_D{z*%uElfVn0QtD4MM# zjc7;hq=7$zA#4MML(|g-W@)Kqu+}_^;kOmWJ}pZ{?o7P(Nm;3&*_48Pv0Z#T)e@eZ zn54^Oiz$}O$gjM{Y4A-(dsT!cgT4-zcArpQZy}CGNlM%h(Zc62uB0`OU$`TLZGMtT zxP}}n@Nwwz5eN&bkW32kL(y)U1KrY+$vXn`BROGt<&PKGgN3mlnLbdH26Ny;m&~MO zoD8c9Col}nc4-O5UBmX9pp!FnVk>AjLQ(bk z7%~MSSQ3T0W@$2pl;!D7aWrbg9Bn8pXpWdDR^5cpAnG6tI{a$KQI-h*PH}0N3Q&eU z<43e8mMO@=dzj4PC{DEY10lE25y)G`s19tF{gV@;SVve4$2h4IrD%Avc}2We6sz=# zQV`9)5%)P!H@1`Jb-DKRz-cE9>k2kOe#GEg0wdc;u@T}LEefFA(PSxpmlWfugT(Ef zY~L=RJ8mYVS?kJ0;MkB7n5~J&>73Fdf&)XPO~zT#iF^r%Q44j#It4aVsgU(8UPFr$M+zZ}@;>1(V>l;R(! z#aC5yDuQ1J_)aE${liE(7D0SFe>@F!BE7o=b|U^P>gCrD{#1mah29H%=1 zVVEWZ{z`DfjfYjS7UFQ#pX?Wz96wMjd`c|In- z97@O)Q7@uaM5*v#F{~1?@EsxFb`d4HiDEcb#JVCrZXw7UM4T_8lutsv7%x0k!S^vO zg?v(azQ6eQc>)EDeM@=Yx0J8jOsJo1RQ|#n81yb0Z$E9YfGV;4SIJX;ZHTCMsEFP| z`BYZ<3vacsu+D-U-rwTu@p(&my(q6NU$_{~_Y=y^742I05A?@-T)ZB65ds!B7xaJA z^3|d~Qva4$t9<{e0*%WrpRc_B=L3x8@u`K;RbD=?oV?yhnz8cozDzV2J^Z-j{2%EbTTcED^>1pdPr2s4)L*O1 zjn7M_#yQH{Q~#FoVQ(pa{w?J_%gGyS|MK`$y`}ukw_G0%y`_BKTgv;2oekfHW6hh?gFpiTsn7RG(Bw`LGl(Vfpm(VM$KH^67sR zmhwqhUS5iquzdO-g{6EF{*iq7bSdBebtJn~07?6SLgeY3>IS0YA-*D@uTYpJh|VGS zvVcE(5K_y`fBfK+}@5Y>$zZ1{3gfohNz1a~9wb}Z;h?I3wQh;$w;Vh)hnG6qEHV?m_P z3{X7~Tr2S5r9jee1&Hcf1tPi6K-4CDM$f0815&yhj6(JyqVw86d#PS~3H7R9d;PO| z{ZEztdS_>X2JK$!J^Fu2!+*Y>*A4;k6#3dAp#JA|`xk2X|9nWq=85+|CQ5$!Sb+sP zHfKmqHPMtGUlEw4D+L_Aq!1nq81o%T-VE@(5c2z$Us$fDz$>oN&xJ@|5x2gfd^=Ho z8{|(cD_Z8kzF&58S?`x>C@p{FQa(plY$#nX^u3^FF&740@OoMP^!e`!pTI}TNFk>( zOlbdeoTEtCzg!NkT=<^~=B+foPb*)~znxBY_&r+~K6#D)orU<^Z~;}30{X5IFkfu9 zRJIhB#w8tZ<+h-`k*?s5LVjt_a}&d~$I^APgOE;pFkN9{SX`u-n;51&m9Cp1g1n;; zWCk(pB!+XvFr8WG>LG^Rg&^}3!yaPzaj;MxKE>cKgBYf>0$uaPa1|lQvc+&!F{~29 zYB7Aiy-;2?FCB(w@_^JgQh(%z3iaqkWa79n5$7jF`75@&AQvrS4-q3p3==U_#9$G9Mf4HTQ$)3h zDiM_;KJF&eb6><_5$}k2Q^X<>3q`yn;&~Czh*%)vQ4tS`Xb>@9#1$eg5>YCDwiuow z;#d*0MbwHID`J?4!6F8T=p&+9M3sne+hsJrrTN)dGQ5cSp9ppd-u3t3DDjfk4_XJP zPH8=$@gmK6QbDg3(u#i-)`hT$NQ(Z`S6IMb%)eH!L#~MB*Q@ej+GpsJka$`H-UIal z(fpkU+6p=Yx(9NAF+4$?L8+o)qK`O{L0d)eUi1`rx-}P@= z01KYvLzEDotg%DzCrUUBv>m)2cn?J1)8?15I#5wAA_916F$QR`55vH8>0`1d@T3^zXAnAM(&f7{CIX7fU%2s!Zc7cc)2gm z1Bk0J5N$v_VE||yc)1Tw^2HgL0zDB=xDIp)JmFrEm;2fzU!0%f244+dmxKpFD)7ZX z&otb-4=~{oZhd)P3+$N={UH+zoC6}8sJP3fDRdG3*^6Nc-crxx+R7oaJK#~*Kik72wu3O)?DMdS^@h(UrrvA{1uq(cEPeK2Er zR6bDh;Ym1eJlckM!Zn~R;PZi!uTH{l6EIHT?~+gtx&s-)%_2{DS>y>VCJHiy%|xCs zTI30JBCiKZzCHUCCA1H;w{JmHBA zjMasF0dU|(%tP>6pi4fM8SrZ0S0I`T1;87du-Abv2Er8p%O!c>IuMQ9eBh?7m=B0I z0Jm=wd7$K1lkk_F@G}WtmxNZk7`qR7!UdqC;PZeZ;1kmqGIIZyo(4X=GuiZeK;rp%_c>gBaf_S-4OY(QQ_gBm##1ozdT>>xnODXqTNq#LmJ%OHa zCOmcnUlTlGCde7Q9!UNthl7{on;9r1*NL9B>9z`30eUe!u6op;0X_c#)2;Z{s3x~20H_X!G8|zae81qTlkYgU2SfDp2~eXlE23;8NxtE$bb(6 zc7R{T-Neg$G?K5zB_MB%2SW1eSP8t`za#m6458 z;;VA(m|C!T0Wh&T+J$(z&qngy*a@v6&H=3hpAQ^b zN9YFW32}E0pYb&9#g>=qa4EVphEC+ ze~0Av@F$QS@`MiXZI}RF?$eO`9d?Gl!gYuzOa?6iFZXFE_h%^gNhtSYNWKgG-s6}v z`~VVm04c!}Zf^zuFDR3+08|8?@D}I{c)9;U@=f?HC>MDNAA$7X<$eRn|6mTNZjuR) z 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 Date: Thu, 13 Jun 2019 16:28:16 -0700 Subject: [PATCH 02/44] [GH Index] Fixed build --- build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index 23f6bbadc..39f56e4a7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -101,8 +101,8 @@ Invoke-BuildStep 'Set version metadata in AssemblyInfo.cs' { ` "$PSScriptRoot\src\StatusAggregator\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Validation.Symbols.Core\Properties\AssemblyInfo.g.cs", "$PSScriptRoot\src\Monitoring.RebootSearchInstance\Properties\AssemblyInfo.g.cs", - "$PSScriptRoot\src\Stats.CDNLogsSanitizer\Properties\AssemblyInfo.g.cs" - "$PSScriptRoot\src\NuGet.Jobs.GitHubIndexer\Properties\AssemblyInfo.g.cs", + "$PSScriptRoot\src\Stats.CDNLogsSanitizer\Properties\AssemblyInfo.g.cs", + "$PSScriptRoot\src\NuGet.Jobs.GitHubIndexer\Properties\AssemblyInfo.g.cs" $versionMetadata | ForEach-Object { Set-VersionInfo -Path $_ -Version $SimpleVersion -Branch $Branch -Commit $CommitSHA From ae298564b7389a1d95967a70ba5781b5d40f4381 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 12:31:19 -0700 Subject: [PATCH 03/44] Added License headers --- src/NuGet.Jobs.GitHubIndexer/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/Program.cs b/src/NuGet.Jobs.GitHubIndexer/Program.cs index a1308c9db..2581348f0 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Program.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Program.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace NuGet.Jobs.GitHubIndexer { From 388b9d709de61c83a724dcbf04b439d1d6774ed0 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 12:33:01 -0700 Subject: [PATCH 04/44] Changed Nuspec Id --- src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec index 6fa59e3ef..a1aeaa110 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec @@ -1,7 +1,7 @@ - NuGet.Jobs.GitHubIndexer + GitHubIndexer $version$ NuGet.Jobs.GitHubIndexer .NET Foundation From d1e407f52c3594a420139a272456c1923a4ddf3d Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 12:33:40 -0700 Subject: [PATCH 05/44] Changed Nuspec script include --- src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec index a1aeaa110..28025a627 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.nuspec @@ -11,10 +11,7 @@ - - - - + \ No newline at end of file From d0fcfe1086cee067480f94f8977aeb5f6e82a460 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 12:38:13 -0700 Subject: [PATCH 06/44] Added empty job --- src/NuGet.Jobs.GitHubIndexer/Job.cs | 32 +++++++++++++++++++ .../NuGet.Jobs.GitHubIndexer.csproj | 7 ++++ src/NuGet.Jobs.GitHubIndexer/Program.cs | 3 +- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/NuGet.Jobs.GitHubIndexer/Job.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs new file mode 100644 index 000000000..33b52f5e1 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Autofac; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class Job : JsonConfigurationJob + { + public Job() + { + } + + public override Task Run() + { + throw new System.NotImplementedException(); + } + + protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) + { + throw new System.NotImplementedException(); + } + + protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 63e84c9be..669529835 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -44,6 +44,7 @@ + @@ -62,6 +63,12 @@ all + + + {4b4b1efb-8f33-42e6-b79f-54e7f3293d31} + NuGet.Jobs.Common + + ..\..\build diff --git a/src/NuGet.Jobs.GitHubIndexer/Program.cs b/src/NuGet.Jobs.GitHubIndexer/Program.cs index 2581348f0..089be28bb 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Program.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Program.cs @@ -10,7 +10,8 @@ public class Program { public static void Main(string[] args) { - + var job = new Job(); + JobRunner.RunOnce(job, args).GetAwaiter().GetResult(); } } } From 6de40f35a79b6f1fc80b7379aa4771a3827f47b7 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 14:59:28 -0700 Subject: [PATCH 07/44] [GH Idx] Added Octokit and LibGit2Sharp dependencies --- .../NuGet.Jobs.GitHubIndexer.csproj | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 669529835..1b4b7c1b2 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -1,4 +1,4 @@ - + @@ -57,11 +57,17 @@ + + 0.26.0 + 0.3.0 runtime; build; native; contentfiles; analyzers all + + 0.32.0 + From 2d904d3a0d0e1bbf0402041629d17e543cb55b1e Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 14:59:46 -0700 Subject: [PATCH 08/44] [GH Idx] Add initial GHSearcher --- .../GitHubSearcher.cs | 82 +++++++++++++++++++ src/NuGet.Jobs.GitHubIndexer/Job.cs | 16 ++-- .../NuGet.Jobs.GitHubIndexer.csproj | 3 +- 3 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs new file mode 100644 index 000000000..1ac14ed5b --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs @@ -0,0 +1,82 @@ +using Octokit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class GitHubSearcher + { + private GitHubClient _client; + + public GitHubSearcher() + { + _client = new GitHubClient(new ProductHeaderValue("GitHubIndexer")); + } + + private async Task> GetResultsForPage(int currPage, int totalCount, int maxStarCount = -1, string lastRecordName = null) + { + const int MIN_STARS = 100; + if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) + { + Console.WriteLine("Waiting a minute to cooldown.."); + await Task.Delay(TimeSpan.FromSeconds(61)); + Console.WriteLine("Resuming query =D"); + } + + var request = new SearchRepositoriesRequest + { + Stars = maxStarCount == -1 ? Range.GreaterThan(MIN_STARS) : new Range(MIN_STARS, maxStarCount), + Language = Language.CSharp, + SortField = RepoSearchSort.Stars, + Order = SortDirection.Descending, + PerPage = 100, // Maximum is 100 :( + Page = currPage + }; + + List resultList = new List(); + + SearchRepositoryResult response = _client.Search.SearchRepo(request).GetAwaiter().GetResult(); + if (response.Items.Count > 0) + { + var toAdd = + lastRecordName == null ? + response.Items : + response.Items.Where(repo => repo.FullName != lastRecordName); + resultList.AddRange(toAdd); + + // Since there can only be 100 results per page, if the count is 100, it means we should query the next page + if (response.Items.Count == 100) + { + ++currPage; + + if (currPage <= 10) + { + //resultList.UnionWith(GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); + resultList.AddRange( await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); + } + else + { + // Since we need to grab more than 1000 results, let's pick up where the $currLast$ repository is and build a new query from there + // This will make us count from the $recursivePage$ parameter and not the currPage anymore + //resultList.UnionWith(GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount)); + resultList.AddRange(await GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount, response.Items[response.Items.Count - 1].FullName)); + } + } + } + + return resultList; + } + + ///

+ /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns the first 100 matches + /// + /// First 100 C# repos on GitHub that have more than 100 stars + public async Task> GetRepos() + { + var result = await GetResultsForPage(1, 0); + return result; + } + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index 33b52f5e1..d24863bc7 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -1,32 +1,36 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.IO; using System.Threading.Tasks; using Autofac; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { + private GitHubSearcher _gitHubSearcher; public Job() { + _gitHubSearcher = new GitHubSearcher(); } - public override Task Run() + public override async Task Run() { - throw new System.NotImplementedException(); + // Where the code will be :D + var repos = await _gitHubSearcher.GetRepos(); + File.WriteAllText("Repos.json", JsonConvert.SerializeObject(repos)); } - protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) + protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { - throw new System.NotImplementedException(); } - protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) + protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) { - throw new System.NotImplementedException(); } } } \ No newline at end of file diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 1b4b7c1b2..daa7bff57 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -1,4 +1,4 @@ - + @@ -44,6 +44,7 @@ + From ca96d25298dac57bd87a1afa10989a244fb2295b Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 16:43:49 -0700 Subject: [PATCH 09/44] [GH Idx] Add GitRepoSearcher --- .../{ => GitRepoSearchers}/GitHubSearcher.cs | 32 +++++++++--- .../GitRepoSearchers/IGitRepoSearcher.cs | 17 +++++++ .../GitReposSearcher.cs | 37 ++++++++++++++ src/NuGet.Jobs.GitHubIndexer/Job.cs | 6 +-- .../NuGet.Jobs.GitHubIndexer.csproj | 5 +- src/NuGet.Jobs.GitHubIndexer/Program.cs | 5 +- .../RepositoryInformation.cs | 49 +++++++++++++++++++ 7 files changed, 135 insertions(+), 16 deletions(-) rename src/NuGet.Jobs.GitHubIndexer/{ => GitRepoSearchers}/GitHubSearcher.cs (71%) create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs similarity index 71% rename from src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs rename to src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 1ac14ed5b..5bfb072f5 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -1,4 +1,7 @@ -using Octokit; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Octokit; using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +9,7 @@ namespace NuGet.Jobs.GitHubIndexer { - public class GitHubSearcher + public class GitHubSearcher : IGitRepoSearcher { private GitHubClient _client; @@ -22,7 +25,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo { Console.WriteLine("Waiting a minute to cooldown.."); await Task.Delay(TimeSpan.FromSeconds(61)); - Console.WriteLine("Resuming query =D"); + Console.WriteLine("Resuming search."); } var request = new SearchRepositoriesRequest @@ -54,7 +57,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo if (currPage <= 10) { //resultList.UnionWith(GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); - resultList.AddRange( await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); + resultList.AddRange(await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); } else { @@ -70,13 +73,26 @@ private async Task> GetResultsForPage(int currPage, int totalCo } /// - /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns the first 100 matches + /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns them. /// - /// First 100 C# repos on GitHub that have more than 100 stars - public async Task> GetRepos() + /// List of C# repos on GitHub that have more than 100 stars + public async Task> GetPopularRepositories() { + Console.WriteLine("Starting search on GitHub..."); var result = await GetResultsForPage(1, 0); - return result; + return result + .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) + .Select( + group => + { + var repo = group.First(); + return new RepositoryInformation( + string.Format("{0}/{1}", repo.Owner.Login, repo.Name), + repo.HtmlUrl, + repo.StargazersCount, + Array.Empty()); + }) + .ToList(); } } } diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs new file mode 100644 index 000000000..5127627a5 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace NuGet.Jobs.GitHubIndexer +{ + public interface IGitRepoSearcher + { + /// + /// Searches for all popular C# repos, orders them in Descending order and returns a list containing their basic information + /// + /// List of popular C# repositories + Task> GetPopularRepositories(); + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs new file mode 100644 index 000000000..5ccd481d7 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class GitReposSearcher + { + private IReadOnlyCollection _searchers; + public GitReposSearcher() + { + _searchers = new IGitRepoSearcher[] + { + new GitHubSearcher() + }; + } + + /// + /// Gets a list of popular C# Git repositories from multiple data sources + /// + /// List of popular C# Git repos + public async Task> GetPopularRepositories() + { + var resultList = new List(); + + // TODO: Make this run in parallel + foreach (var searcher in _searchers) + { + resultList.AddRange(await searcher.GetPopularRepositories()); + } + + return resultList; + } + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index d24863bc7..c837e870a 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -12,16 +12,16 @@ namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { - private GitHubSearcher _gitHubSearcher; + private GitReposSearcher _gitSearcher; public Job() { - _gitHubSearcher = new GitHubSearcher(); + _gitSearcher = new GitReposSearcher(); } public override async Task Run() { // Where the code will be :D - var repos = await _gitHubSearcher.GetRepos(); + var repos = await _gitSearcher.GetPopularRepositories(); File.WriteAllText("Repos.json", JsonConvert.SerializeObject(repos)); } diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index daa7bff57..51bb8d33b 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -44,10 +44,13 @@ - + + + + diff --git a/src/NuGet.Jobs.GitHubIndexer/Program.cs b/src/NuGet.Jobs.GitHubIndexer/Program.cs index 089be28bb..6b6c003c5 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Program.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Program.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace NuGet.Jobs.GitHubIndexer diff --git a/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs b/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs new file mode 100644 index 000000000..a475a925a --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class RepositoryInformation + { + public RepositoryInformation( + string id, + string url, + int stars, + IReadOnlyList dependencies) + { + if (stars < 0) + { + throw new IndexOutOfRangeException(string.Format("{0} cannot have a negative value!", nameof(stars))); + } + + Id = id ?? throw new ArgumentNullException(nameof(id)); + var idSplit = Id.Split('/'); + if (idSplit.Length == 2) + { + Owner = idSplit[0]; + Name = idSplit[1]; + } + else + { + throw new ArgumentException(string.Format("{0} has an invalid format! It should be \"owner/repositoryName\"!", nameof(Id))); + } + + Url = url ?? throw new ArgumentNullException(nameof(url)); + Stars = stars; + Dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies)); + } + + [JsonIgnore] + public string Name { get; } + [JsonIgnore] + public string Owner { get; } + public string Url { get; } + public int Stars { get; } + public string Id { get; } + public IReadOnlyList Dependencies { get; } + } +} From 2d346b5eb7dbb6146314839249217a535dcfe93d Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 14 Jun 2019 17:09:29 -0700 Subject: [PATCH 10/44] [GH Idx] Add dependency injection --- .../GitRepoSearchers/GitHubSearcher.cs | 6 +-- .../GitReposSearcher.cs | 37 ------------------- src/NuGet.Jobs.GitHubIndexer/Job.cs | 17 +++++---- .../NuGet.Jobs.GitHubIndexer.csproj | 1 - 4 files changed, 12 insertions(+), 49 deletions(-) delete mode 100644 src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 5bfb072f5..9a640a57a 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -11,11 +11,11 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { - private GitHubClient _client; + private IGitHubClient _client; - public GitHubSearcher() + public GitHubSearcher(IGitHubClient client) { - _client = new GitHubClient(new ProductHeaderValue("GitHubIndexer")); + _client = client ?? throw new ArgumentNullException(nameof(client)); } private async Task> GetResultsForPage(int currPage, int totalCount, int maxStarCount = -1, string lastRecordName = null) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs deleted file mode 100644 index 5ccd481d7..000000000 --- a/src/NuGet.Jobs.GitHubIndexer/GitReposSearcher.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace NuGet.Jobs.GitHubIndexer -{ - public class GitReposSearcher - { - private IReadOnlyCollection _searchers; - public GitReposSearcher() - { - _searchers = new IGitRepoSearcher[] - { - new GitHubSearcher() - }; - } - - /// - /// Gets a list of popular C# Git repositories from multiple data sources - /// - /// List of popular C# Git repos - public async Task> GetPopularRepositories() - { - var resultList = new List(); - - // TODO: Make this run in parallel - foreach (var searcher in _searchers) - { - resultList.AddRange(await searcher.GetPopularRepositories()); - } - - return resultList; - } - } -} diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index c837e870a..c3972ed91 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -7,26 +7,27 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using Octokit; namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { - private GitReposSearcher _gitSearcher; - public Job() - { - _gitSearcher = new GitReposSearcher(); - } - public override async Task Run() { - // Where the code will be :D - var repos = await _gitSearcher.GetPopularRepositories(); + var searcher = _serviceProvider.GetRequiredService(); + var repos = await searcher.GetPopularRepositories(); + File.WriteAllText("Repos.json", JsonConvert.SerializeObject(repos)); } protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { + services.AddTransient(); + services.AddSingleton(provider => + { + return new GitHubClient(new ProductHeaderValue("GitHubIndexer")); + }); } protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 51bb8d33b..9f12fae35 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -46,7 +46,6 @@ - From eab0c9c0e3f2fc216988995c08e05ed67458f70d Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 17 Jun 2019 15:01:07 -0700 Subject: [PATCH 11/44] [GH Idx] Add null check --- .../GitRepoSearchers/GitHubSearcher.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 9a640a57a..7a0c08902 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -41,7 +41,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo List resultList = new List(); SearchRepositoryResult response = _client.Search.SearchRepo(request).GetAwaiter().GetResult(); - if (response.Items.Count > 0) + if (response.Items != null && response.Items.Count > 0) { var toAdd = lastRecordName == null ? @@ -56,14 +56,12 @@ private async Task> GetResultsForPage(int currPage, int totalCo if (currPage <= 10) { - //resultList.UnionWith(GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); resultList.AddRange(await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); } else { // Since we need to grab more than 1000 results, let's pick up where the $currLast$ repository is and build a new query from there // This will make us count from the $recursivePage$ parameter and not the currPage anymore - //resultList.UnionWith(GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount)); resultList.AddRange(await GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount, response.Items[response.Items.Count - 1].FullName)); } } From d9a8eb23a788211d28a0fc5f57932830468bfe0a Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 17 Jun 2019 15:04:32 -0700 Subject: [PATCH 12/44] [GH Idx] Add tests --- NuGet.Jobs.sln | 7 + .../GitHubSearcherFacts.cs | 139 ++++++++++++++++++ .../NuGet.Jobs.GitHubIndexer.Tests.csproj | 68 +++++++++ .../Properties/AssemblyInfo.cs | 36 +++++ 4 files changed, 250 insertions(+) create mode 100644 tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs create mode 100644 tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj create mode 100644 tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs diff --git a/NuGet.Jobs.sln b/NuGet.Jobs.sln index a178305dd..12dc07e4f 100644 --- a/NuGet.Jobs.sln +++ b/NuGet.Jobs.sln @@ -149,6 +149,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monitoring.PackageLag.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Jobs.GitHubIndexer", "src\NuGet.Jobs.GitHubIndexer\NuGet.Jobs.GitHubIndexer.csproj", "{42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.Jobs.GitHubIndexer.Tests", "tests\NuGet.Jobs.GitHubIndexer.Tests\NuGet.Jobs.GitHubIndexer.Tests.csproj", "{4A64FEB4-198C-445B-835F-A5B68EFBFDA7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -393,6 +395,10 @@ Global {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Debug|Any CPU.Build.0 = Debug|Any CPU {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Release|Any CPU.ActiveCfg = Release|Any CPU {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643}.Release|Any CPU.Build.0 = Release|Any CPU + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -457,6 +463,7 @@ Global {19EC1E99-89A8-445A-8C22-C1B0CD8CC777} = {6A776396-02B1-475D-A104-26940ADB04AB} {D3F1711A-25AC-4EC9-9971-4F838BCD2A07} = {6A776396-02B1-475D-A104-26940ADB04AB} {42B1EB66-58F9-4D9A-8E23-FF12CBF5D643} = {FA5644B5-4F08-43F6-86B3-039374312A47} + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7} = {6A776396-02B1-475D-A104-26940ADB04AB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {284A7AC3-FB43-4F1F-9C9C-2AF0E1F46C2B} diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs new file mode 100644 index 000000000..c34d17137 --- /dev/null +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -0,0 +1,139 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Moq; +using Octokit; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace NuGet.Jobs.GitHubIndexer.Tests +{ + public class GitHubSearcherFacts + { + private static GitHubSearcher GetMockClient(SearchRepositoryResult searchResult = null) + { + var connection = new Mock(); + var dummyApiInfo = new ApiInfo( + new Dictionary(), + Array.Empty(), + Array.Empty(), + "", + new RateLimit(10, 10, 10)); + connection + .Setup(c => c.GetLastApiInfo()) + .Returns(dummyApiInfo); + + var mockSearch = new Mock(); + mockSearch + .Setup(s => s.SearchRepo(It.IsAny())) + .Returns(Task.FromResult(searchResult ?? new SearchRepositoryResult())); + + var mockClient = new Mock(); + mockClient.SetupGet(c => c.Connection).Returns(connection.Object); + var mockApiConnection = new ApiConnection(connection.Object); + mockClient.SetupGet(c => c.Activity).Returns(new ActivitiesClient(mockApiConnection)); + mockClient.SetupGet(c => c.Authorization).Returns(new AuthorizationsClient(mockApiConnection)); + mockClient.SetupGet(c => c.Enterprise).Returns(new EnterpriseClient(mockApiConnection)); + mockClient.SetupGet(c => c.Gist).Returns(new GistsClient(mockApiConnection)); + mockClient.SetupGet(c => c.Git).Returns(new GitDatabaseClient(mockApiConnection)); + mockClient.SetupGet(c => c.GitHubApps).Returns(new GitHubAppsClient(mockApiConnection)); + mockClient.SetupGet(c => c.Issue).Returns(new IssuesClient(mockApiConnection)); + mockClient.SetupGet(c => c.Migration).Returns(new MigrationClient(mockApiConnection)); + mockClient.SetupGet(c => c.Miscellaneous).Returns(new MiscellaneousClient(connection.Object)); + mockClient.SetupGet(c => c.Oauth).Returns(new OauthClient(connection.Object)); + mockClient.SetupGet(c => c.Organization).Returns(new OrganizationsClient(mockApiConnection)); + mockClient.SetupGet(c => c.PullRequest).Returns(new PullRequestsClient(mockApiConnection)); + mockClient.SetupGet(c => c.Repository).Returns(new RepositoriesClient(mockApiConnection)); + mockClient.SetupGet(c => c.User).Returns(new UsersClient(mockApiConnection)); + mockClient.SetupGet(c => c.Reaction).Returns(new ReactionsClient(mockApiConnection)); + mockClient.SetupGet(c => c.Check).Returns(new ChecksClient(mockApiConnection)); + mockClient.SetupGet(c => c.Search).Returns(mockSearch.Object); + + return new GitHubSearcher(mockClient.Object); + } + + private static Repository CreateRepository(string fullName) + { + var ownerName = fullName.Split('/')[0]; + var repoName = fullName.Split('/')[1]; + var owner = new User("", "", "", 0, "", DateTimeOffset.Now, DateTimeOffset.Now, 100, "", 10, 10, true, "", 0, 1, "", ownerName, ownerName, "", 0, null, 0, 0, 0, "", new RepositoryPermissions(), true, "", null); + return new Repository( + "url", + "htmlUrl", + "cloneUrl", + "gitUrl", + "sshUrl", + "svnUrl", + "mirrorUrl", + 1,// Id + "nodeId", + owner, + repoName, + fullName, + "description", + "homepage", + "csharp", + false, // Private + true, // Fork + 10, // Fork Count + 100, // Star Count + "master", // Default branch + 0, // Open issues count + null, // Pushed at + DateTimeOffset.Now, // Created At + DateTimeOffset.Now, // Updated At + new RepositoryPermissions(), + null, + null, + new LicenseMetadata(), + true, // Issues + true, // Wiki + true, // Downloads + true, // Pages + 10, // Subscriber count + 500, // Size + true, // Allow Rebase merge + true, // Allow Squash merge + true, // Allow Merge commit + false); // Archived? + } + + public class GetPopularRepositoriesMethod + { + [Fact] + public async Task GetZeroResult() + { + var res = await GetMockClient().GetPopularRepositories(); + Assert.Empty(res); + } + + [Fact] + public async Task GetMoreThanThousandResults() + { + var items = new List(); + const int totalCount = 4000; + for (int i = 0; i < totalCount; i++) + { + items.Add(CreateRepository("owner/Hello" + i)); + } + + var mockResult = new SearchRepositoryResult(totalCount, true, items); + var res = await GetMockClient(mockResult).GetPopularRepositories(); + Assert.Equal(items.Count, res.Count); + + int resIdx = 0; + foreach (var resItem in res) + { + Assert.Equal(items[resIdx].Name, resItem.Name); + Assert.Equal(items[resIdx].FullName, resItem.Id); + Assert.Equal(items[resIdx].StargazersCount, resItem.Stars); + Assert.Equal(items[resIdx].Owner.Login, resItem.Owner); + resIdx++; + } + } + } + + } +} diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj b/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj new file mode 100644 index 000000000..fd315caaf --- /dev/null +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj @@ -0,0 +1,68 @@ + + + + + + Debug + AnyCPU + {4A64FEB4-198C-445B-835F-A5B68EFBFDA7} + Library + Properties + NuGet.Jobs.GitHubIndexer.Tests + NuGet.Jobs.GitHubIndexer.Tests + v4.6.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + 4.7.145 + + + 2.3.1 + + + 2.3.1 + + + + + {42b1eb66-58f9-4d9a-8e23-ff12cbf5d643} + NuGet.Jobs.GitHubIndexer + + + + \ No newline at end of file diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..63078dc45 --- /dev/null +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GitHubIndexer.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("NuGet.Jobs.GitHubIndexer.Tests")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4a64feb4-198c-445b-835f-a5b68efbfda7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] From 1a5ccbaf1d968651d784060fc64b78e0396105be Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 17 Jun 2019 23:20:04 -0700 Subject: [PATCH 13/44] [GH Idx] Extracted constants --- .../GitRepoSearchers/GitHubSearcher.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 7a0c08902..7ec7ba93c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -11,6 +11,8 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { + public const int MIN_STARS = 100; + public const int RESULTS_PER_PAGE = 100; // Maximum is 100 :( private IGitHubClient _client; public GitHubSearcher(IGitHubClient client) @@ -20,7 +22,6 @@ public GitHubSearcher(IGitHubClient client) private async Task> GetResultsForPage(int currPage, int totalCount, int maxStarCount = -1, string lastRecordName = null) { - const int MIN_STARS = 100; if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) { Console.WriteLine("Waiting a minute to cooldown.."); @@ -34,7 +35,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo Language = Language.CSharp, SortField = RepoSearchSort.Stars, Order = SortDirection.Descending, - PerPage = 100, // Maximum is 100 :( + PerPage = RESULTS_PER_PAGE, Page = currPage }; From 515bdd99aa79f73a6163120d31df4b6c879fb1c6 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 17 Jun 2019 23:20:15 -0700 Subject: [PATCH 14/44] [GH Idx] Fixed tests --- .../GitHubSearcherFacts.cs | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index c34d17137..51d44b1b7 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -12,7 +12,7 @@ namespace NuGet.Jobs.GitHubIndexer.Tests { public class GitHubSearcherFacts { - private static GitHubSearcher GetMockClient(SearchRepositoryResult searchResult = null) + private static GitHubSearcher GetMockClient(Func> searchResultFunc = null) { var connection = new Mock(); var dummyApiInfo = new ApiInfo( @@ -26,10 +26,18 @@ private static GitHubSearcher GetMockClient(SearchRepositoryResult searchResult .Returns(dummyApiInfo); var mockSearch = new Mock(); - mockSearch - .Setup(s => s.SearchRepo(It.IsAny())) - .Returns(Task.FromResult(searchResult ?? new SearchRepositoryResult())); - + if (searchResultFunc == null) + { + mockSearch + .Setup(s => s.SearchRepo(It.IsAny())) + .Returns(Task.FromResult(new SearchRepositoryResult())); + } + else + { + mockSearch + .Setup(s => s.SearchRepo(It.IsAny())) + .Returns(searchResultFunc); + } var mockClient = new Mock(); mockClient.SetupGet(c => c.Connection).Returns(connection.Object); var mockApiConnection = new ApiConnection(connection.Object); @@ -54,7 +62,7 @@ private static GitHubSearcher GetMockClient(SearchRepositoryResult searchResult return new GitHubSearcher(mockClient.Object); } - private static Repository CreateRepository(string fullName) + private static Repository CreateRepository(string fullName, int starCount = 100) { var ownerName = fullName.Split('/')[0]; var repoName = fullName.Split('/')[1]; @@ -78,7 +86,7 @@ private static Repository CreateRepository(string fullName) false, // Private true, // Fork 10, // Fork Count - 100, // Star Count + starCount, // Star Count "master", // Default branch 0, // Open issues count null, // Pushed at @@ -112,15 +120,35 @@ public async Task GetZeroResult() [Fact] public async Task GetMoreThanThousandResults() { + // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); const int totalCount = 4000; + const int maxStars = (totalCount + GitHubSearcher.MIN_STARS); for (int i = 0; i < totalCount; i++) { - items.Add(CreateRepository("owner/Hello" + i)); + items.Add(CreateRepository("owner/Hello" + i, maxStars - i)); } - var mockResult = new SearchRepositoryResult(totalCount, true, items); - var res = await GetMockClient(mockResult).GetPopularRepositories(); + // Create a mock GitHub Search API that serves those results + Func> mockGitHubSearch = + req => + { + var isRange = req.Stars.ToString().Contains(".."); + var index = (req.Page - 1) * GitHubSearcher.RESULTS_PER_PAGE; + + // The user is asking for a min..max range of stars + if (isRange) + { + var str = req.Stars.ToString(); + var max = str.Substring(str.LastIndexOf('.') + 1); + index += maxStars - int.Parse(max); + } + var itemsCount = Math.Min(GitHubSearcher.RESULTS_PER_PAGE, items.Count - index); // To avoid overflowing + var subItems = items.GetRange(index, itemsCount); + return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == 100, subItems)); + }; + + var res = await GetMockClient(mockGitHubSearch).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); int resIdx = 0; From 8b5e2ca97c7b7c7d2b421559d078a250be842ec8 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz <50678153+mogah@users.noreply.github.com> Date: Tue, 18 Jun 2019 12:37:46 -0700 Subject: [PATCH 15/44] Update src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Loïc Sharma --- src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 7ec7ba93c..4b68a3ccd 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -13,7 +13,7 @@ public class GitHubSearcher : IGitRepoSearcher { public const int MIN_STARS = 100; public const int RESULTS_PER_PAGE = 100; // Maximum is 100 :( - private IGitHubClient _client; + private readonly IGitHubClient _client; public GitHubSearcher(IGitHubClient client) { From e3fc3a4d13cc9074613be43380d0596b29592dcb Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz <50678153+mogah@users.noreply.github.com> Date: Tue, 18 Jun 2019 12:38:21 -0700 Subject: [PATCH 16/44] Update src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Loïc Sharma --- src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 4b68a3ccd..71acdd637 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -39,7 +39,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo Page = currPage }; - List resultList = new List(); + var resultList = new List(); SearchRepositoryResult response = _client.Search.SearchRepo(request).GetAwaiter().GetResult(); if (response.Items != null && response.Items.Count > 0) From 6147012e0138218bbd08663d472750e8ec3eee0c Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Tue, 18 Jun 2019 14:43:00 -0700 Subject: [PATCH 17/44] [GH Idx] Removed duplicate class RepositoryInformation --- .../GitRepoSearchers/GitHubSearcher.cs | 3 +- .../GitRepoSearchers/IGitRepoSearcher.cs | 1 + .../NuGet.Jobs.GitHubIndexer.csproj | 4 +- .../RepositoryInformation.cs | 49 ------------------- .../Stats.CDNLogsSanitizer.csproj | 2 +- .../Validation.Common.Job.csproj | 2 +- .../GitHubSearcherFacts.cs | 4 +- 7 files changed, 10 insertions(+), 55 deletions(-) delete mode 100644 src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 71acdd637..a8a94f100 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -1,11 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Octokit; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using NuGetGallery; +using Octokit; namespace NuGet.Jobs.GitHubIndexer { diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs index 5127627a5..bde0ca8b0 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; +using NuGetGallery; namespace NuGet.Jobs.GitHubIndexer { diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 9f12fae35..d1f0b36d5 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -49,7 +49,6 @@ - @@ -71,6 +70,9 @@ 0.32.0 + + 4.4.5-dev-2768844 + diff --git a/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs b/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs deleted file mode 100644 index a475a925a..000000000 --- a/src/NuGet.Jobs.GitHubIndexer/RepositoryInformation.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Newtonsoft.Json; -using System; -using System.Collections.Generic; - -namespace NuGet.Jobs.GitHubIndexer -{ - public class RepositoryInformation - { - public RepositoryInformation( - string id, - string url, - int stars, - IReadOnlyList dependencies) - { - if (stars < 0) - { - throw new IndexOutOfRangeException(string.Format("{0} cannot have a negative value!", nameof(stars))); - } - - Id = id ?? throw new ArgumentNullException(nameof(id)); - var idSplit = Id.Split('/'); - if (idSplit.Length == 2) - { - Owner = idSplit[0]; - Name = idSplit[1]; - } - else - { - throw new ArgumentException(string.Format("{0} has an invalid format! It should be \"owner/repositoryName\"!", nameof(Id))); - } - - Url = url ?? throw new ArgumentNullException(nameof(url)); - Stars = stars; - Dependencies = dependencies ?? throw new ArgumentNullException(nameof(dependencies)); - } - - [JsonIgnore] - public string Name { get; } - [JsonIgnore] - public string Owner { get; } - public string Url { get; } - public int Stars { get; } - public string Id { get; } - public IReadOnlyList Dependencies { get; } - } -} diff --git a/src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj b/src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj index 864b05096..b3b956875 100644 --- a/src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj +++ b/src/Stats.CDNLogsSanitizer/Stats.CDNLogsSanitizer.csproj @@ -66,7 +66,7 @@ all - 4.4.5-dev-2756380 + 4.4.5-dev-2768844 diff --git a/src/Validation.Common.Job/Validation.Common.Job.csproj b/src/Validation.Common.Job/Validation.Common.Job.csproj index 1efb13a3b..e3207b41f 100644 --- a/src/Validation.Common.Job/Validation.Common.Job.csproj +++ b/src/Validation.Common.Job/Validation.Common.Job.csproj @@ -116,7 +116,7 @@ 2.50.0 - 4.4.5-dev-2756380 + 4.4.5-dev-2768844 2.5.0 diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 51d44b1b7..7bafac90d 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Moq; -using Octokit; using System; using System.Collections.Generic; using System.Threading.Tasks; +using Moq; +using Octokit; using Xunit; namespace NuGet.Jobs.GitHubIndexer.Tests From 736df94c82a8bcde2682f84bbcc64eccb77169a6 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Tue, 18 Jun 2019 15:10:22 -0700 Subject: [PATCH 18/44] [GH Idx] Refactored the code a bit --- .../GitRepoSearchers/GitHubSearcher.cs | 71 ++++++++++--------- .../GitRepoSearchers/IGitRepoSearcher.cs | 2 +- src/NuGet.Jobs.GitHubIndexer/Job.cs | 4 +- .../GitHubSearcherFacts.cs | 15 ++-- 4 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index a8a94f100..e30342e4a 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using NuGetGallery; using Octokit; @@ -12,60 +13,66 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { - public const int MIN_STARS = 100; - public const int RESULTS_PER_PAGE = 100; // Maximum is 100 :( + public const int MinStars = 100; + public const int ResultsPerPage = 100; // Maximum is 100 :( + public const int MaxGithubResultPerQuery = 1000; // The maximum number of results a query can return (1000 as of 6/18/2019) private readonly IGitHubClient _client; + private readonly ILogger _logger; + private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); - public GitHubSearcher(IGitHubClient client) + public GitHubSearcher(IGitHubClient client, ILogger logger) { _client = client ?? throw new ArgumentNullException(nameof(client)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - private async Task> GetResultsForPage(int currPage, int totalCount, int maxStarCount = -1, string lastRecordName = null) + private async Task> GetResultsForPage(int currPage, int totalCount, int? maxStarCount = null, string lastRecordName = null) { if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) { - Console.WriteLine("Waiting a minute to cooldown.."); - await Task.Delay(TimeSpan.FromSeconds(61)); - Console.WriteLine("Resuming search."); + _logger.LogInformation("Waiting a minute to cooldown."); + await Task.Delay(OneMinute); + _logger.LogInformation("Resuming search."); } var request = new SearchRepositoriesRequest { - Stars = maxStarCount == -1 ? Range.GreaterThan(MIN_STARS) : new Range(MIN_STARS, maxStarCount), + Stars = maxStarCount.HasValue ? Range.GreaterThan(MinStars) : new Range(MinStars, maxStarCount.Value), Language = Language.CSharp, SortField = RepoSearchSort.Stars, Order = SortDirection.Descending, - PerPage = RESULTS_PER_PAGE, + PerPage = ResultsPerPage, Page = currPage }; var resultList = new List(); SearchRepositoryResult response = _client.Search.SearchRepo(request).GetAwaiter().GetResult(); - if (response.Items != null && response.Items.Count > 0) + if (response.Items == null || !response.Items.Any()) { - var toAdd = - lastRecordName == null ? - response.Items : - response.Items.Where(repo => repo.FullName != lastRecordName); - resultList.AddRange(toAdd); + return resultList; + } - // Since there can only be 100 results per page, if the count is 100, it means we should query the next page - if (response.Items.Count == 100) - { - ++currPage; + var toAdd = + lastRecordName == null ? + response.Items : + response.Items.Where(repo => repo.FullName != lastRecordName); + resultList.AddRange(toAdd); + + // Since there can only be $RESULTS_PER_PAGE$ results per page, if the count is 100, it means we should query the next page + if (response.Items.Count == ResultsPerPage) + { + ++currPage; - if (currPage <= 10) - { - resultList.AddRange(await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); - } - else - { - // Since we need to grab more than 1000 results, let's pick up where the $currLast$ repository is and build a new query from there - // This will make us count from the $recursivePage$ parameter and not the currPage anymore - resultList.AddRange(await GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount, response.Items[response.Items.Count - 1].FullName)); - } + if (currPage <= Math.Ceiling(MaxGithubResultPerQuery / (double)ResultsPerPage)) + { + resultList.AddRange(await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); + } + else + { + // Since we need to grab more than 1000 results, let's pick up where the $currLast$ repository is and build a new query from there + // This will make us count from the $recursivePage$ parameter and not the currPage anymore + resultList.AddRange(await GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount, response.Items[response.Items.Count - 1].FullName)); } } @@ -76,9 +83,9 @@ private async Task> GetResultsForPage(int currPage, int totalCo /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns them. /// /// List of C# repos on GitHub that have more than 100 stars - public async Task> GetPopularRepositories() + public async Task> GetPopularRepositories() { - Console.WriteLine("Starting search on GitHub..."); + _logger.LogInformation("Starting search on GitHub..."); var result = await GetResultsForPage(1, 0); return result .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) @@ -87,7 +94,7 @@ public async Task> GetPopularReposito { var repo = group.First(); return new RepositoryInformation( - string.Format("{0}/{1}", repo.Owner.Login, repo.Name), + $"{repo.Owner.Login}/{repo.Name}", repo.HtmlUrl, repo.StargazersCount, Array.Empty()); diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs index bde0ca8b0..6093b0b62 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/IGitRepoSearcher.cs @@ -13,6 +13,6 @@ public interface IGitRepoSearcher /// Searches for all popular C# repos, orders them in Descending order and returns a list containing their basic information /// /// List of popular C# repositories - Task> GetPopularRepositories(); + Task> GetPopularRepositories(); } } diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index c3972ed91..f58a415b9 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -13,6 +13,8 @@ namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { + private const string GitHubIndexerName = "GitHubIndexer"; + public override async Task Run() { var searcher = _serviceProvider.GetRequiredService(); @@ -26,7 +28,7 @@ protected override void ConfigureJobServices(IServiceCollection services, IConfi services.AddTransient(); services.AddSingleton(provider => { - return new GitHubClient(new ProductHeaderValue("GitHubIndexer")); + return new GitHubClient(new ProductHeaderValue(GitHubIndexerName)); }); } diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 7bafac90d..a7b7740e9 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Moq; using Octokit; using Xunit; @@ -59,7 +60,7 @@ private static GitHubSearcher GetMockClient(Func c.Check).Returns(new ChecksClient(mockApiConnection)); mockClient.SetupGet(c => c.Search).Returns(mockSearch.Object); - return new GitHubSearcher(mockClient.Object); + return new GitHubSearcher(mockClient.Object, new Mock>().Object); } private static Repository CreateRepository(string fullName, int starCount = 100) @@ -123,7 +124,7 @@ public async Task GetMoreThanThousandResults() // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); const int totalCount = 4000; - const int maxStars = (totalCount + GitHubSearcher.MIN_STARS); + const int maxStars = (totalCount + GitHubSearcher.MinStars); for (int i = 0; i < totalCount; i++) { items.Add(CreateRepository("owner/Hello" + i, maxStars - i)); @@ -134,7 +135,7 @@ public async Task GetMoreThanThousandResults() req => { var isRange = req.Stars.ToString().Contains(".."); - var index = (req.Page - 1) * GitHubSearcher.RESULTS_PER_PAGE; + var index = (req.Page - 1) * GitHubSearcher.ResultsPerPage; // The user is asking for a min..max range of stars if (isRange) @@ -143,7 +144,7 @@ public async Task GetMoreThanThousandResults() var max = str.Substring(str.LastIndexOf('.') + 1); index += maxStars - int.Parse(max); } - var itemsCount = Math.Min(GitHubSearcher.RESULTS_PER_PAGE, items.Count - index); // To avoid overflowing + var itemsCount = Math.Min(GitHubSearcher.ResultsPerPage, items.Count - index); // To avoid overflowing var subItems = items.GetRange(index, itemsCount); return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == 100, subItems)); }; @@ -151,14 +152,14 @@ public async Task GetMoreThanThousandResults() var res = await GetMockClient(mockGitHubSearch).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); - int resIdx = 0; - foreach (var resItem in res) + + for (int resIdx = 0; resIdx < res.Count; resIdx++) { + var resItem = res[resIdx]; Assert.Equal(items[resIdx].Name, resItem.Name); Assert.Equal(items[resIdx].FullName, resItem.Id); Assert.Equal(items[resIdx].StargazersCount, resItem.Stars); Assert.Equal(items[resIdx].Owner.Login, resItem.Owner); - resIdx++; } } } From 228e82e3944bbe42e8d30c9b54aa4a6bcf2e1a91 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Tue, 18 Jun 2019 15:11:44 -0700 Subject: [PATCH 19/44] [GH Idx] Fix possible deadlock --- src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index e30342e4a..018fe3e52 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -47,7 +47,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo var resultList = new List(); - SearchRepositoryResult response = _client.Search.SearchRepo(request).GetAwaiter().GetResult(); + var response = await _client.Search.SearchRepo(request); if (response.Items == null || !response.Items.Any()) { return resultList; From ae4d8d27b76701b66456146d9a5a0d9092be5136 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Tue, 18 Jun 2019 17:27:26 -0700 Subject: [PATCH 20/44] [GH Idx] Add config section in the appsettings.json --- .../GitHubSearcherConfiguration.cs | 18 +++++++++++ .../GitRepoSearchers/GitHubSearcher.cs | 23 +++++++++++--- src/NuGet.Jobs.GitHubIndexer/Job.cs | 3 ++ .../NuGet.Jobs.GitHubIndexer.csproj | 1 + .../GitHubSearcherFacts.cs | 31 ++++++++++++++----- 5 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs new file mode 100644 index 000000000..5d088d7c7 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class GitHubSearcherConfiguration + { + public int MinStars { get; set; } = 100; + + public int ResultsPerPage { get; set; } = 100; + + public int MaxGithubResultPerQuery { get; set; } = 1000; + + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 018fe3e52..6117f63c5 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NuGetGallery; using Octokit; @@ -13,19 +14,31 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { - public const int MinStars = 100; - public const int ResultsPerPage = 100; // Maximum is 100 :( - public const int MaxGithubResultPerQuery = 1000; // The maximum number of results a query can return (1000 as of 6/18/2019) private readonly IGitHubClient _client; private readonly ILogger _logger; private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); + private readonly IOptionsSnapshot _configuration; - public GitHubSearcher(IGitHubClient client, ILogger logger) + public GitHubSearcher( + IGitHubClient client, + ILogger logger, + IOptionsSnapshot configuration) { _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + _logger.LogInformation( + $"GitHubSearcher created with params:\n" + + $"MinStars: {MinStars}\n" + + $"ResultsPerPage: {ResultsPerPage}\n" + + $"MaxGithubResultPerQuery: {MaxGithubResultPerQuery}\n"); } + public int MinStars => _configuration.Value.MinStars; + public int ResultsPerPage => _configuration.Value.ResultsPerPage; + public int MaxGithubResultPerQuery => _configuration.Value.MaxGithubResultPerQuery; + private async Task> GetResultsForPage(int currPage, int totalCount, int? maxStarCount = null, string lastRecordName = null) { if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) @@ -37,7 +50,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo var request = new SearchRepositoriesRequest { - Stars = maxStarCount.HasValue ? Range.GreaterThan(MinStars) : new Range(MinStars, maxStarCount.Value), + Stars = !maxStarCount.HasValue ? Range.GreaterThan(MinStars) : new Range(MinStars, maxStarCount.Value), Language = Language.CSharp, SortField = RepoSearchSort.Stars, Order = SortDirection.Descending, diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index f58a415b9..04dd37a89 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -14,6 +14,7 @@ namespace NuGet.Jobs.GitHubIndexer public class Job : JsonConfigurationJob { private const string GitHubIndexerName = "GitHubIndexer"; + private const string GitHubSearcherConfigurationSectionName = "GitHubSearcher"; public override async Task Run() { @@ -30,6 +31,8 @@ protected override void ConfigureJobServices(IServiceCollection services, IConfi { return new GitHubClient(new ProductHeaderValue(GitHubIndexerName)); }); + + services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); } protected override void ConfigureAutofacServices(ContainerBuilder containerBuilder) diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index d1f0b36d5..dd984eb14 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -44,6 +44,7 @@ + diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index a7b7740e9..d68f57469 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using Octokit; using Xunit; @@ -13,6 +14,8 @@ namespace NuGet.Jobs.GitHubIndexer.Tests { public class GitHubSearcherFacts { + private static readonly GitHubSearcherConfiguration _configuration = new GitHubSearcherConfiguration(); + private static GitHubSearcher GetMockClient(Func> searchResultFunc = null) { var connection = new Mock(); @@ -60,7 +63,14 @@ private static GitHubSearcher GetMockClient(Func c.Check).Returns(new ChecksClient(mockApiConnection)); mockClient.SetupGet(c => c.Search).Returns(mockSearch.Object); - return new GitHubSearcher(mockClient.Object, new Mock>().Object); + + var optionsSnapshot = new Mock>(); + optionsSnapshot + .Setup(x => x.Value) + .Returns( + () => _configuration); + + return new GitHubSearcher(mockClient.Object, new Mock>().Object, optionsSnapshot.Object); } private static Repository CreateRepository(string fullName, int starCount = 100) @@ -118,13 +128,18 @@ public async Task GetZeroResult() Assert.Empty(res); } - [Fact] - public async Task GetMoreThanThousandResults() + [Theory] + //[InlineData(4000, 10, 200, 2)] // This fails and causes a StackOverflowException (TODO: Fix it!) + [InlineData(4000, 10, 50, 50)] + public async Task GetMoreThanThousandResults(int totalCount, int minStars, int maxGithubResultPerQuery, int resultsPerPage) { + _configuration.ResultsPerPage = resultsPerPage; + _configuration.MinStars = minStars; + _configuration.MaxGithubResultPerQuery = maxGithubResultPerQuery; // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); - const int totalCount = 4000; - const int maxStars = (totalCount + GitHubSearcher.MinStars); + + int maxStars = (totalCount + _configuration.MinStars); for (int i = 0; i < totalCount; i++) { items.Add(CreateRepository("owner/Hello" + i, maxStars - i)); @@ -135,7 +150,7 @@ public async Task GetMoreThanThousandResults() req => { var isRange = req.Stars.ToString().Contains(".."); - var index = (req.Page - 1) * GitHubSearcher.ResultsPerPage; + var index = (req.Page - 1) * _configuration.ResultsPerPage; // The user is asking for a min..max range of stars if (isRange) @@ -144,7 +159,7 @@ public async Task GetMoreThanThousandResults() var max = str.Substring(str.LastIndexOf('.') + 1); index += maxStars - int.Parse(max); } - var itemsCount = Math.Min(GitHubSearcher.ResultsPerPage, items.Count - index); // To avoid overflowing + var itemsCount = Math.Min(_configuration.ResultsPerPage, items.Count - index); // To avoid overflowing var subItems = items.GetRange(index, itemsCount); return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == 100, subItems)); }; @@ -152,7 +167,7 @@ public async Task GetMoreThanThousandResults() var res = await GetMockClient(mockGitHubSearch).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); - + for (int resIdx = 0; resIdx < res.Count; resIdx++) { var resItem = res[resIdx]; From e4ef9824f9e2426758f082d223bc37052bc38604 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Wed, 19 Jun 2019 12:58:26 -0700 Subject: [PATCH 21/44] [GH Idx] GitHubSearcher is not recursive anymore! --- .../GitHubSearcherConfiguration.cs | 7 +- .../GitRepoSearchers/GitHubSearcher.cs | 96 +++++++++++-------- .../GitHubSearcherFacts.cs | 42 +++++--- 3 files changed, 86 insertions(+), 59 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs index 5d088d7c7..22f892b8a 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. namespace NuGet.Jobs.GitHubIndexer { diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 6117f63c5..9935f6599 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -29,17 +29,14 @@ public GitHubSearcher( _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _logger.LogInformation( - $"GitHubSearcher created with params:\n" + - $"MinStars: {MinStars}\n" + - $"ResultsPerPage: {ResultsPerPage}\n" + - $"MaxGithubResultPerQuery: {MaxGithubResultPerQuery}\n"); + $"GitHubSearcher created with params:\n" + GetConfigInfo()); } public int MinStars => _configuration.Value.MinStars; public int ResultsPerPage => _configuration.Value.ResultsPerPage; public int MaxGithubResultPerQuery => _configuration.Value.MaxGithubResultPerQuery; - private async Task> GetResultsForPage(int currPage, int totalCount, int? maxStarCount = null, string lastRecordName = null) + private async Task CheckThrottle() { if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) { @@ -47,45 +44,45 @@ private async Task> GetResultsForPage(int currPage, int totalCo await Task.Delay(OneMinute); _logger.LogInformation("Resuming search."); } + } - var request = new SearchRepositoriesRequest - { - Stars = !maxStarCount.HasValue ? Range.GreaterThan(MinStars) : new Range(MinStars, maxStarCount.Value), - Language = Language.CSharp, - SortField = RepoSearchSort.Stars, - Order = SortDirection.Descending, - PerPage = ResultsPerPage, - Page = currPage - }; - + private async Task> GetResultsFromGitHub() + { + var upperStarBound = int.MaxValue; var resultList = new List(); - - var response = await _client.Search.SearchRepo(request); - if (response.Items == null || !response.Items.Any()) + var lastPage = Math.Ceiling(MaxGithubResultPerQuery / (double)ResultsPerPage); + while (upperStarBound >= MinStars) { - return resultList; - } + var page = 0; + while (page < lastPage) + { + await CheckThrottle(); + var request = new SearchRepositoriesRequest + { + Stars = new Range(MinStars, upperStarBound), + Language = Language.CSharp, + SortField = RepoSearchSort.Stars, + Order = SortDirection.Descending, + PerPage = ResultsPerPage, + Page = page + 1 + }; - var toAdd = - lastRecordName == null ? - response.Items : - response.Items.Where(repo => repo.FullName != lastRecordName); - resultList.AddRange(toAdd); + var response = await _client.Search.SearchRepo(request); + if (response.Items == null || !response.Items.Any()) + { + return resultList; + } - // Since there can only be $RESULTS_PER_PAGE$ results per page, if the count is 100, it means we should query the next page - if (response.Items.Count == ResultsPerPage) - { - ++currPage; + // TODO: Block unwanted repos + resultList.AddRange(response.Items); + upperStarBound = response.Items.Last().StargazersCount; + page++; - if (currPage <= Math.Ceiling(MaxGithubResultPerQuery / (double)ResultsPerPage)) - { - resultList.AddRange(await GetResultsForPage(currPage, resultList.Count + totalCount, maxStarCount)); - } - else - { - // Since we need to grab more than 1000 results, let's pick up where the $currLast$ repository is and build a new query from there - // This will make us count from the $recursivePage$ parameter and not the currPage anymore - resultList.AddRange(await GetResultsForPage(1, resultList.Count + totalCount, response.Items[response.Items.Count - 1].StargazersCount, response.Items[response.Items.Count - 1].FullName)); + if (page >= lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) + { + _logger.LogWarning($"Last page results have the same star count! StarCount: {response.Items.First().StargazersCount}\n{GetConfigInfo()}"); // TODO + return resultList; + } } } @@ -99,7 +96,7 @@ private async Task> GetResultsForPage(int currPage, int totalCo public async Task> GetPopularRepositories() { _logger.LogInformation("Starting search on GitHub..."); - var result = await GetResultsForPage(1, 0); + var result = await GetResultsFromGitHub(); return result .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) .Select( @@ -112,7 +109,28 @@ public async Task> GetPopularRepositories() repo.StargazersCount, Array.Empty()); }) + .OrderByDescending(x => x.Stars) .ToList(); } + + private string GetConfigInfo() + { + return $"MinStars: {MinStars}\n" + + $"ResultsPerPage: {ResultsPerPage}\n" + + $"MaxGithubResultPerQuery: {MaxGithubResultPerQuery}\n"; + } + + private class ReposComparer : IEqualityComparer + { + public bool Equals(Repository x, Repository y) + { + return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(Repository obj) + { + return obj.HtmlUrl.ToLower().GetHashCode(); + } + } } } diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index d68f57469..691ec840a 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -63,7 +63,7 @@ private static GitHubSearcher GetMockClient(Func c.Check).Returns(new ChecksClient(mockApiConnection)); mockClient.SetupGet(c => c.Search).Returns(mockSearch.Object); - + var optionsSnapshot = new Mock>(); optionsSnapshot .Setup(x => x.Value) @@ -129,16 +129,18 @@ public async Task GetZeroResult() } [Theory] - //[InlineData(4000, 10, 200, 2)] // This fails and causes a StackOverflowException (TODO: Fix it!) - [InlineData(4000, 10, 50, 50)] + [InlineData(4000, 10, 200, 2)] // Tests huge number of pages + [InlineData(4000, 10, 50, 2)] // Tests huge number of API calls + [InlineData(30000, 10, 1000, 100)] // Tests huge number of results in real conditions public async Task GetMoreThanThousandResults(int totalCount, int minStars, int maxGithubResultPerQuery, int resultsPerPage) { _configuration.ResultsPerPage = resultsPerPage; _configuration.MinStars = minStars; _configuration.MaxGithubResultPerQuery = maxGithubResultPerQuery; + // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); - + int maxStars = (totalCount + _configuration.MinStars); for (int i = 0; i < totalCount; i++) { @@ -149,25 +151,35 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m Func> mockGitHubSearch = req => { - var isRange = req.Stars.ToString().Contains(".."); - var index = (req.Page - 1) * _configuration.ResultsPerPage; + // Stars are split as "min..max" + var starsStr = req.Stars.ToString(); + var min = int.Parse(starsStr.Substring(0, starsStr.IndexOf('.'))); + var max = int.Parse(starsStr.Substring(starsStr.LastIndexOf('.') + 1)); + int idxMax = -1, idxMin = items.Count; - // The user is asking for a min..max range of stars - if (isRange) + for (int i = 0; i < items.Count; i++) { - var str = req.Stars.ToString(); - var max = str.Substring(str.LastIndexOf('.') + 1); - index += maxStars - int.Parse(max); + var repo = items[i]; + if (repo.StargazersCount <= max && idxMax == -1) + { + idxMax = i; + } + + if (repo.StargazersCount <= min) + { + idxMin = i; + break; + } } - var itemsCount = Math.Min(_configuration.ResultsPerPage, items.Count - index); // To avoid overflowing - var subItems = items.GetRange(index, itemsCount); - return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == 100, subItems)); + + var itemsCount = Math.Min(_configuration.ResultsPerPage, idxMin - idxMax); // To avoid overflowing + var subItems = items.GetRange(idxMax, itemsCount); + return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == _configuration.ResultsPerPage, subItems)); }; var res = await GetMockClient(mockGitHubSearch).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); - for (int resIdx = 0; resIdx < res.Count; resIdx++) { var resItem = res[resIdx]; From a03ddfc0bc099172117de6cc88b65db01d300e8c Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Wed, 19 Jun 2019 13:02:05 -0700 Subject: [PATCH 22/44] [GH Idx] Removed redundant comparer --- .../GitRepoSearchers/GitHubSearcher.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 9935f6599..ff1613527 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -119,18 +119,5 @@ private string GetConfigInfo() $"ResultsPerPage: {ResultsPerPage}\n" + $"MaxGithubResultPerQuery: {MaxGithubResultPerQuery}\n"; } - - private class ReposComparer : IEqualityComparer - { - public bool Equals(Repository x, Repository y) - { - return string.Equals(x.FullName, y.FullName, StringComparison.OrdinalIgnoreCase); - } - - public int GetHashCode(Repository obj) - { - return obj.HtmlUrl.ToLower().GetHashCode(); - } - } } } From a82c0093b64b0264a4a8b411a2c8b339849bb713 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Wed, 19 Jun 2019 13:28:02 -0700 Subject: [PATCH 23/44] [GH Idx] Fix upperStarBound wrongly set on request --- .../GitRepoSearchers/GitHubSearcher.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index ff1613527..3a019a486 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -75,7 +75,6 @@ private async Task> GetResultsFromGitHub() // TODO: Block unwanted repos resultList.AddRange(response.Items); - upperStarBound = response.Items.Last().StargazersCount; page++; if (page >= lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) @@ -84,6 +83,8 @@ private async Task> GetResultsFromGitHub() return resultList; } } + + upperStarBound = resultList.Last().StargazersCount; } return resultList; From d44803ec17b189a6f8bea795be0c4482c06829ae Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Wed, 19 Jun 2019 18:48:29 -0700 Subject: [PATCH 24/44] [GH Idx] Fixed sleep time --- .../GitRepoSearchers/GitHubSearcher.cs | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 3a019a486..928d4e6dc 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -1,14 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NuGetGallery; using Octokit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace NuGet.Jobs.GitHubIndexer { @@ -18,6 +18,7 @@ public class GitHubSearcher : IGitRepoSearcher private readonly ILogger _logger; private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); private readonly IOptionsSnapshot _configuration; + private DateTimeOffset _throttleResetTime; public GitHubSearcher( IGitHubClient client, @@ -40,23 +41,64 @@ private async Task CheckThrottle() { if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) { - _logger.LogInformation("Waiting a minute to cooldown."); - await Task.Delay(OneMinute); + //var sleepTime = _client.GetLastApiInfo().RateLimit.Reset - DateTimeOffset.Now; + var sleepTime = _throttleResetTime - DateTimeOffset.Now; + _throttleResetTime = DateTimeOffset.Now; + _logger.LogInformation($"Waiting {sleepTime.TotalSeconds} seconds to cooldown."); + if (sleepTime.TotalSeconds > 0) + { + await Task.Delay(sleepTime); + } + _logger.LogInformation("Resuming search."); } } + private async Task SearchRepo(SearchRepositoriesRequest request) + { + _logger.LogInformation("Making request"); + + bool? error = null; + IApiResponse response = null; + while(!error.HasValue || error.Value) + { + try + { + response = await _client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); + error = false; + } + catch (RateLimitExceededException ex) + { + _logger.LogError("Exceeded GitHub RateLimit! Waiting 5 seconds before retrying."); + await Task.Delay(5_000); + } + } + + if (_throttleResetTime < DateTimeOffset.Now) + { + var headers = response.HttpResponse.Headers; + var ghTime = DateTime.ParseExact(headers["Date"], "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture).ToLocalTime(); + var timeToWait = DateTimeOffset.FromUnixTimeSeconds(long.Parse(headers["X-RateLimit-Reset"])).ToLocalTime() - ghTime; + _throttleResetTime = DateTimeOffset.Now + timeToWait; + } + + return response.Body; + } + private async Task> GetResultsFromGitHub() { + _throttleResetTime = DateTimeOffset.Now; var upperStarBound = int.MaxValue; var resultList = new List(); var lastPage = Math.Ceiling(MaxGithubResultPerQuery / (double)ResultsPerPage); + while (upperStarBound >= MinStars) { var page = 0; while (page < lastPage) { await CheckThrottle(); + var request = new SearchRepositoriesRequest { Stars = new Range(MinStars, upperStarBound), @@ -67,9 +109,11 @@ private async Task> GetResultsFromGitHub() Page = page + 1 }; - var response = await _client.Search.SearchRepo(request); + var response = await SearchRepo(request); + if (response.Items == null || !response.Items.Any()) { + _logger.LogWarning($"Search request didn't return any item. Page: {request.Page} {GetConfigInfo()}"); return resultList; } @@ -77,7 +121,7 @@ private async Task> GetResultsFromGitHub() resultList.AddRange(response.Items); page++; - if (page >= lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) + if (page == lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) { _logger.LogWarning($"Last page results have the same star count! StarCount: {response.Items.First().StargazersCount}\n{GetConfigInfo()}"); // TODO return resultList; From f23f800e02aaf410c0d0c52ccb7da9fb0f104d38 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 21 Jun 2019 10:39:32 -0700 Subject: [PATCH 25/44] [GH Idx] Fix typo --- src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs | 3 +-- .../GitRepoSearchers/GitHubSearcher.cs | 4 ++-- tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs index 22f892b8a..98e19b10c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs @@ -9,7 +9,6 @@ public class GitHubSearcherConfiguration public int ResultsPerPage { get; set; } = 100; - public int MaxGithubResultPerQuery { get; set; } = 1000; - + public int MaxGitHubResultPerQuery { get; set; } = 1000; } } diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 928d4e6dc..8c577abd6 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.Extensions.Logging; @@ -35,7 +35,7 @@ public GitHubSearcher( public int MinStars => _configuration.Value.MinStars; public int ResultsPerPage => _configuration.Value.ResultsPerPage; - public int MaxGithubResultPerQuery => _configuration.Value.MaxGithubResultPerQuery; + public int MaxGithubResultPerQuery => _configuration.Value.MaxGitHubResultPerQuery; private async Task CheckThrottle() { diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 691ec840a..d0248e2ab 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -136,7 +136,7 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m { _configuration.ResultsPerPage = resultsPerPage; _configuration.MinStars = minStars; - _configuration.MaxGithubResultPerQuery = maxGithubResultPerQuery; + _configuration.MaxGitHubResultPerQuery = maxGithubResultPerQuery; // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); From d34a3f44c4bf36182e0d1c253885c55750705c62 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 21 Jun 2019 12:54:28 -0700 Subject: [PATCH 26/44] [GH Idx] Made fields private --- .../GitRepoSearchers/GitHubSearcher.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs index 8c577abd6..b9567e5b1 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs @@ -28,14 +28,11 @@ public GitHubSearcher( _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - - _logger.LogInformation( - $"GitHubSearcher created with params:\n" + GetConfigInfo()); } - public int MinStars => _configuration.Value.MinStars; - public int ResultsPerPage => _configuration.Value.ResultsPerPage; - public int MaxGithubResultPerQuery => _configuration.Value.MaxGitHubResultPerQuery; + private int _minStars => _configuration.Value.MinStars; + private int _resultsPerPage => _configuration.Value.ResultsPerPage; + private int _maxGithubResultPerQuery => _configuration.Value.MaxGitHubResultPerQuery; private async Task CheckThrottle() { @@ -90,9 +87,9 @@ private async Task> GetResultsFromGitHub() _throttleResetTime = DateTimeOffset.Now; var upperStarBound = int.MaxValue; var resultList = new List(); - var lastPage = Math.Ceiling(MaxGithubResultPerQuery / (double)ResultsPerPage); + var lastPage = Math.Ceiling(_maxGithubResultPerQuery / (double)_resultsPerPage); - while (upperStarBound >= MinStars) + while (upperStarBound >= _minStars) { var page = 0; while (page < lastPage) @@ -101,11 +98,11 @@ private async Task> GetResultsFromGitHub() var request = new SearchRepositoriesRequest { - Stars = new Range(MinStars, upperStarBound), + Stars = new Range(_minStars, upperStarBound), Language = Language.CSharp, SortField = RepoSearchSort.Stars, Order = SortDirection.Descending, - PerPage = ResultsPerPage, + PerPage = _resultsPerPage, Page = page + 1 }; @@ -160,9 +157,9 @@ public async Task> GetPopularRepositories() private string GetConfigInfo() { - return $"MinStars: {MinStars}\n" + - $"ResultsPerPage: {ResultsPerPage}\n" + - $"MaxGithubResultPerQuery: {MaxGithubResultPerQuery}\n"; + return $"MinStars: {_minStars}\n" + + $"ResultsPerPage: {_resultsPerPage}\n" + + $"MaxGithubResultPerQuery: {_maxGithubResultPerQuery}\n"; } } } From c1c35dfc9d1945987c54dc3124be2f9c550da887 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 21 Jun 2019 13:13:38 -0700 Subject: [PATCH 27/44] [GH Idx] Changed UA --- src/NuGet.Jobs.GitHubIndexer/Job.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index 04dd37a89..f736b5c5b 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -13,7 +13,7 @@ namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { - private const string GitHubIndexerName = "GitHubIndexer"; + private const string GitHubIndexerUserAgent = "NuGet-NuGet.Jobs-GitHubIndexer"; private const string GitHubSearcherConfigurationSectionName = "GitHubSearcher"; public override async Task Run() @@ -29,7 +29,7 @@ protected override void ConfigureJobServices(IServiceCollection services, IConfi services.AddTransient(); services.AddSingleton(provider => { - return new GitHubClient(new ProductHeaderValue(GitHubIndexerName)); + return new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent)); }); services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); From 8a91ad7d0ce84665649372793ff62e6bb4164b6a Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 21 Jun 2019 13:18:19 -0700 Subject: [PATCH 28/44] [GH Idx] Made the configuration not static --- .../GitHubSearcherFacts.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index d0248e2ab..beacf8380 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -14,9 +14,7 @@ namespace NuGet.Jobs.GitHubIndexer.Tests { public class GitHubSearcherFacts { - private static readonly GitHubSearcherConfiguration _configuration = new GitHubSearcherConfiguration(); - - private static GitHubSearcher GetMockClient(Func> searchResultFunc = null) + private static GitHubSearcher GetMockClient(Func> searchResultFunc = null, GitHubSearcherConfiguration configuration = null) { var connection = new Mock(); var dummyApiInfo = new ApiInfo( @@ -68,7 +66,7 @@ private static GitHubSearcher GetMockClient(Func x.Value) .Returns( - () => _configuration); + () => configuration ?? new GitHubSearcherConfiguration()); return new GitHubSearcher(mockClient.Object, new Mock>().Object, optionsSnapshot.Object); } @@ -121,6 +119,8 @@ private static Repository CreateRepository(string fullName, int starCount = 100) public class GetPopularRepositoriesMethod { + private readonly GitHubSearcherConfiguration _configuration = new GitHubSearcherConfiguration(); + [Fact] public async Task GetZeroResult() { @@ -177,7 +177,7 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == _configuration.ResultsPerPage, subItems)); }; - var res = await GetMockClient(mockGitHubSearch).GetPopularRepositories(); + var res = await GetMockClient(mockGitHubSearch, _configuration).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); for (int resIdx = 0; resIdx < res.Count; resIdx++) From b5c38ba76fa08cfc526847e3d214fca7cc1c893a Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Fri, 21 Jun 2019 13:19:54 -0700 Subject: [PATCH 29/44] [GH Idx] Add ApiInfo doc in the tests --- .../NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index beacf8380..19f90a121 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -18,10 +18,10 @@ private static GitHubSearcher GetMockClient(Func(); var dummyApiInfo = new ApiInfo( - new Dictionary(), - Array.Empty(), - Array.Empty(), - "", + new Dictionary(), // links + Array.Empty(), // Oauth scopes + Array.Empty(), // accepted Oauth scopes + "", // Etag new RateLimit(10, 10, 10)); connection .Setup(c => c.GetLastApiInfo()) From 677a90059e782d168384034599444c0a62f5431b Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 13:34:49 -0700 Subject: [PATCH 30/44] [GH Idx] Refactor GH Search API requester --- .../GitHub/GitHubSearchApiRequester.cs | 22 +++ .../GitHub/GitHubSearchApiResponse.cs | 22 +++ .../{ => GitHub}/GitHubSearcher.cs | 28 +-- .../GitHub/IGitHubSearchApiRequester.cs | 13 ++ src/NuGet.Jobs.GitHubIndexer/Job.cs | 6 +- .../NuGet.Jobs.GitHubIndexer.csproj | 5 +- .../GitHubSearcherFacts.cs | 161 ++++++++++-------- 7 files changed, 164 insertions(+), 93 deletions(-) create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs rename src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/{ => GitHub}/GitHubSearcher.cs (88%) create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs new file mode 100644 index 000000000..b96ccf8c8 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Octokit; + + +namespace NuGet.Jobs.GitHubIndexer +{ + public class GitHubSearchApiRequester : IGitHubSearchApiRequester + { + public async Task GetResponse(IGitHubClient client, SearchRepositoriesRequest request) + { + var apiResponse = await client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); + return new GitHubSearchApiResponse( + apiResponse.Body, + DateTime.ParseExact(apiResponse.HttpResponse.Headers["Date"], "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture).ToLocalTime(), + DateTimeOffset.FromUnixTimeSeconds(long.Parse(apiResponse.HttpResponse.Headers["X-RateLimit-Reset"])).ToLocalTime()); + } + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs new file mode 100644 index 000000000..84c2151df --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Octokit; + +namespace NuGet.Jobs.GitHubIndexer +{ + public class GitHubSearchApiResponse + { + public GitHubSearchApiResponse(SearchRepositoryResult result, DateTimeOffset date, DateTimeOffset throttleResetTime) + { + Result = result; + Date = date; + ThrottleResetTime = throttleResetTime; + } + + public SearchRepositoryResult Result { get; } + public DateTimeOffset Date { get; } + public DateTimeOffset ThrottleResetTime { get; } + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs similarity index 88% rename from src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs rename to src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index b9567e5b1..cdecf45d6 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -1,14 +1,14 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NuGetGallery; -using Octokit; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NuGetGallery; +using Octokit; namespace NuGet.Jobs.GitHubIndexer { @@ -18,16 +18,20 @@ public class GitHubSearcher : IGitRepoSearcher private readonly ILogger _logger; private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); private readonly IOptionsSnapshot _configuration; + private readonly IGitHubSearchApiRequester _searchApiRequester; + private DateTimeOffset _throttleResetTime; public GitHubSearcher( IGitHubClient client, + IGitHubSearchApiRequester searchApiRequester, ILogger logger, IOptionsSnapshot configuration) { _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _searchApiRequester = searchApiRequester ?? throw new ArgumentNullException(nameof(searchApiRequester)); } private int _minStars => _configuration.Value.MinStars; @@ -53,15 +57,15 @@ private async Task CheckThrottle() private async Task SearchRepo(SearchRepositoriesRequest request) { - _logger.LogInformation("Making request"); + _logger.LogInformation($"Requesting page {request.Page} for stars {request.Stars}"); bool? error = null; - IApiResponse response = null; - while(!error.HasValue || error.Value) + GitHubSearchApiResponse response = null; + while (!error.HasValue || error.Value) { try { - response = await _client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); + response = await _searchApiRequester.GetResponse(_client, request); error = false; } catch (RateLimitExceededException ex) @@ -73,13 +77,11 @@ private async Task SearchRepo(SearchRepositoriesRequest if (_throttleResetTime < DateTimeOffset.Now) { - var headers = response.HttpResponse.Headers; - var ghTime = DateTime.ParseExact(headers["Date"], "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture).ToLocalTime(); - var timeToWait = DateTimeOffset.FromUnixTimeSeconds(long.Parse(headers["X-RateLimit-Reset"])).ToLocalTime() - ghTime; + var timeToWait = response.ThrottleResetTime - response.Date; _throttleResetTime = DateTimeOffset.Now + timeToWait; } - return response.Body; + return response.Result; } private async Task> GetResultsFromGitHub() diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs new file mode 100644 index 000000000..7cbac8650 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Octokit; + +namespace NuGet.Jobs.GitHubIndexer +{ + public interface IGitHubSearchApiRequester + { + Task GetResponse(IGitHubClient client, SearchRepositoriesRequest request); + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index f736b5c5b..3d0c4a3e9 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -27,10 +27,8 @@ public override async Task Run() protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { services.AddTransient(); - services.AddSingleton(provider => - { - return new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent)); - }); + services.AddSingleton(provider => new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent))); + services.AddSingleton(provider => new GitHubSearchApiRequester()); services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); } diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index dd984eb14..3e85da921 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -45,7 +45,10 @@ - + + + + diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 19f90a121..02f9255ff 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -16,105 +16,112 @@ public class GitHubSearcherFacts { private static GitHubSearcher GetMockClient(Func> searchResultFunc = null, GitHubSearcherConfiguration configuration = null) { - var connection = new Mock(); var dummyApiInfo = new ApiInfo( new Dictionary(), // links Array.Empty(), // Oauth scopes Array.Empty(), // accepted Oauth scopes "", // Etag new RateLimit(10, 10, 10)); + + var connection = new Mock(); connection .Setup(c => c.GetLastApiInfo()) .Returns(dummyApiInfo); - var mockSearch = new Mock(); - if (searchResultFunc == null) - { - mockSearch - .Setup(s => s.SearchRepo(It.IsAny())) - .Returns(Task.FromResult(new SearchRepositoryResult())); - } - else - { - mockSearch - .Setup(s => s.SearchRepo(It.IsAny())) - .Returns(searchResultFunc); - } + var mockSearchApiRequester = new Mock(); + mockSearchApiRequester + .Setup(r => r.GetResponse(It.IsAny(), It.IsAny())) + .Returns(async (IGitHubClient client, SearchRepositoriesRequest request) => + { + return new GitHubSearchApiResponse(searchResultFunc == null ? new SearchRepositoryResult() : await searchResultFunc(request), DateTimeOffset.Now, DateTimeOffset.Now); + }); + var mockClient = new Mock(); mockClient.SetupGet(c => c.Connection).Returns(connection.Object); var mockApiConnection = new ApiConnection(connection.Object); - mockClient.SetupGet(c => c.Activity).Returns(new ActivitiesClient(mockApiConnection)); - mockClient.SetupGet(c => c.Authorization).Returns(new AuthorizationsClient(mockApiConnection)); - mockClient.SetupGet(c => c.Enterprise).Returns(new EnterpriseClient(mockApiConnection)); - mockClient.SetupGet(c => c.Gist).Returns(new GistsClient(mockApiConnection)); - mockClient.SetupGet(c => c.Git).Returns(new GitDatabaseClient(mockApiConnection)); - mockClient.SetupGet(c => c.GitHubApps).Returns(new GitHubAppsClient(mockApiConnection)); - mockClient.SetupGet(c => c.Issue).Returns(new IssuesClient(mockApiConnection)); - mockClient.SetupGet(c => c.Migration).Returns(new MigrationClient(mockApiConnection)); - mockClient.SetupGet(c => c.Miscellaneous).Returns(new MiscellaneousClient(connection.Object)); - mockClient.SetupGet(c => c.Oauth).Returns(new OauthClient(connection.Object)); - mockClient.SetupGet(c => c.Organization).Returns(new OrganizationsClient(mockApiConnection)); - mockClient.SetupGet(c => c.PullRequest).Returns(new PullRequestsClient(mockApiConnection)); - mockClient.SetupGet(c => c.Repository).Returns(new RepositoriesClient(mockApiConnection)); - mockClient.SetupGet(c => c.User).Returns(new UsersClient(mockApiConnection)); - mockClient.SetupGet(c => c.Reaction).Returns(new ReactionsClient(mockApiConnection)); - mockClient.SetupGet(c => c.Check).Returns(new ChecksClient(mockApiConnection)); - mockClient.SetupGet(c => c.Search).Returns(mockSearch.Object); - - var optionsSnapshot = new Mock>(); optionsSnapshot .Setup(x => x.Value) .Returns( () => configuration ?? new GitHubSearcherConfiguration()); - return new GitHubSearcher(mockClient.Object, new Mock>().Object, optionsSnapshot.Object); + return new GitHubSearcher(mockClient.Object, mockSearchApiRequester.Object, new Mock>().Object, optionsSnapshot.Object); } private static Repository CreateRepository(string fullName, int starCount = 100) { var ownerName = fullName.Split('/')[0]; var repoName = fullName.Split('/')[1]; - var owner = new User("", "", "", 0, "", DateTimeOffset.Now, DateTimeOffset.Now, 100, "", 10, 10, true, "", 0, 1, "", ownerName, ownerName, "", 0, null, 0, 0, 0, "", new RepositoryPermissions(), true, "", null); + var owner = new User( + avatarUrl: "", + bio: "", + blog: "", + collaborators: 0, + company: "", + createdAt: DateTimeOffset.Now, + updatedAt: DateTimeOffset.Now, + diskUsage: 100, + email: "", + followers: 10, + following: 10, + hireable: true, + htmlUrl: "", + totalPrivateRepos: 0, + id: 1, + location: "", + login: ownerName, + name: ownerName, + nodeId: "", + ownedPrivateRepos: 0, + plan: null, + privateGists: 0, + publicGists: 0, + publicRepos: 0, + url: "", + permissions: new RepositoryPermissions(), + siteAdmin: true, + ldapDistinguishedName: "", + suspendedAt: null); + return new Repository( - "url", - "htmlUrl", - "cloneUrl", - "gitUrl", - "sshUrl", - "svnUrl", - "mirrorUrl", - 1,// Id - "nodeId", - owner, - repoName, - fullName, - "description", - "homepage", - "csharp", - false, // Private - true, // Fork - 10, // Fork Count - starCount, // Star Count - "master", // Default branch - 0, // Open issues count - null, // Pushed at - DateTimeOffset.Now, // Created At - DateTimeOffset.Now, // Updated At - new RepositoryPermissions(), - null, - null, - new LicenseMetadata(), - true, // Issues - true, // Wiki - true, // Downloads - true, // Pages - 10, // Subscriber count - 500, // Size - true, // Allow Rebase merge - true, // Allow Squash merge - true, // Allow Merge commit - false); // Archived? + url: "url", + htmlUrl: "htmlUrl", + cloneUrl: "cloneUrl", + gitUrl: "gitUrl", + sshUrl: "sshUrl", + svnUrl: "svnUrl", + mirrorUrl: "mirrorUrl", + id: 1, + nodeId: "nodeId", + owner: owner, + name: repoName, + fullName: fullName, + description: "description", + homepage: "homepage", + language: "csharp", + @private: false, + fork: true, + forksCount: 10, + stargazersCount: starCount, + defaultBranch: "master", + openIssuesCount: 0, + pushedAt: null, + createdAt: DateTimeOffset.Now, + updatedAt: DateTimeOffset.Now, + permissions: new RepositoryPermissions(), + parent: null, + source: null, + license: new LicenseMetadata(), + hasIssues: true, + hasWiki: true, + hasDownloads: true, + hasPages: true, + subscribersCount: 10, + size: 500, + allowRebaseMerge: true, + allowSquashMerge: true, + allowMergeCommit: true, + archived: false); } public class GetPopularRepositoriesMethod @@ -172,8 +179,12 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m } } - var itemsCount = Math.Min(_configuration.ResultsPerPage, idxMin - idxMax); // To avoid overflowing - var subItems = items.GetRange(idxMax, itemsCount); + var page = req.Page - 1; + var startId = idxMax + req.PerPage * page > idxMin ? idxMin : idxMax + req.PerPage * page; + + var itemsCount = Math.Min(_configuration.ResultsPerPage, idxMin - startId); // To avoid overflowing + var subItems = itemsCount == 0 ? new List() : items.GetRange(startId, itemsCount); + return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == _configuration.ResultsPerPage, subItems)); }; From 422f9c66f47c646ed481fa4e1aaa098787900a83 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 13:37:16 -0700 Subject: [PATCH 31/44] [GH Idx] Removed redundant import in csproj --- .../NuGet.Jobs.GitHubIndexer.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj b/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj index fd315caaf..b4c52624b 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/NuGet.Jobs.GitHubIndexer.Tests.csproj @@ -1,6 +1,5 @@  - Debug From 6096aa1019d12e35d7e20792f58b80b0a3770f26 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:15:03 -0700 Subject: [PATCH 32/44] [GH Idx] Add documentation to the configuration --- .../GitHubSearcherConfiguration.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs index 98e19b10c..e0771d642 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitHubSearcherConfiguration.cs @@ -5,10 +5,19 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcherConfiguration { + /// + /// Minimum number of stars that a GitHub Repo needs to have to be included in the indexing + /// public int MinStars { get; set; } = 100; + /// + /// The number of results that would be shown per page. This is currently limited to 100 (limit verified on 6/24/2019) + /// public int ResultsPerPage { get; set; } = 100; - public int MaxGitHubResultPerQuery { get; set; } = 1000; + /// + /// The limit of results that a single search query can show. This is currently limited to 1000 (limit verified on 6/24/2019) + /// + public int MaxGitHubResultsPerQuery { get; set; } = 1000; } } From 6d2731d187da218a37fc9f1e5b95c82b12157455 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:16:28 -0700 Subject: [PATCH 33/44] [GH Idx] Move the IGitHubClient to the GitHubSearchWrapper --- ...ApiRequester.cs => GitHubSearchWrapper.cs} | 20 ++++++++++--- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 28 +++++++++---------- .../GitHub/IGitHubSearchApiRequester.cs | 13 --------- .../GitHub/IGitHubSearchWrapper.cs | 24 ++++++++++++++++ src/NuGet.Jobs.GitHubIndexer/Job.cs | 2 +- .../GitHubSearcherFacts.cs | 4 +-- 6 files changed, 56 insertions(+), 35 deletions(-) rename src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/{GitHubSearchApiRequester.cs => GitHubSearchWrapper.cs} (50%) delete mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs create mode 100644 src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchWrapper.cs diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs similarity index 50% rename from src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs rename to src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs index b96ccf8c8..016e5705c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiRequester.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs @@ -5,14 +5,26 @@ using System.Threading.Tasks; using Octokit; - namespace NuGet.Jobs.GitHubIndexer { - public class GitHubSearchApiRequester : IGitHubSearchApiRequester + public class GitHubSearchWrapper : IGitHubSearchWrapper { - public async Task GetResponse(IGitHubClient client, SearchRepositoriesRequest request) + private readonly IGitHubClient _client; + + public GitHubSearchWrapper(IGitHubClient client) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + } + + public int? GetRemainingRequestCount() + { + var apiInfo = _client.GetLastApiInfo(); + return apiInfo == null ? (int?)null : apiInfo.RateLimit.Remaining; + } + + public async Task GetResponse(SearchRepositoriesRequest request) { - var apiResponse = await client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); + var apiResponse = await _client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); return new GitHubSearchApiResponse( apiResponse.Body, DateTime.ParseExact(apiResponse.HttpResponse.Headers["Date"], "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture).ToLocalTime(), diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index cdecf45d6..701b59569 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -14,21 +14,18 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { - private readonly IGitHubClient _client; private readonly ILogger _logger; private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); private readonly IOptionsSnapshot _configuration; - private readonly IGitHubSearchApiRequester _searchApiRequester; + private readonly IGitHubSearchWrapper _searchApiRequester; private DateTimeOffset _throttleResetTime; public GitHubSearcher( - IGitHubClient client, - IGitHubSearchApiRequester searchApiRequester, + IGitHubSearchWrapper searchApiRequester, ILogger logger, IOptionsSnapshot configuration) { - _client = client ?? throw new ArgumentNullException(nameof(client)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _searchApiRequester = searchApiRequester ?? throw new ArgumentNullException(nameof(searchApiRequester)); @@ -36,18 +33,17 @@ public GitHubSearcher( private int _minStars => _configuration.Value.MinStars; private int _resultsPerPage => _configuration.Value.ResultsPerPage; - private int _maxGithubResultPerQuery => _configuration.Value.MaxGitHubResultPerQuery; + private int _maxGithubResultPerQuery => _configuration.Value.MaxGitHubResultsPerQuery; private async Task CheckThrottle() { - if (_client.GetLastApiInfo() != null && _client.GetLastApiInfo().RateLimit.Remaining == 0) + if (_searchApiRequester.GetRemainingRequestCount() == 0) { - //var sleepTime = _client.GetLastApiInfo().RateLimit.Reset - DateTimeOffset.Now; var sleepTime = _throttleResetTime - DateTimeOffset.Now; _throttleResetTime = DateTimeOffset.Now; - _logger.LogInformation($"Waiting {sleepTime.TotalSeconds} seconds to cooldown."); if (sleepTime.TotalSeconds > 0) { + _logger.LogInformation($"Waiting {sleepTime.TotalSeconds} seconds to cooldown."); await Task.Delay(sleepTime); } @@ -57,7 +53,7 @@ private async Task CheckThrottle() private async Task SearchRepo(SearchRepositoriesRequest request) { - _logger.LogInformation($"Requesting page {request.Page} for stars {request.Stars}"); + _logger.LogInformation("Requesting page {Page} for stars {Stars}", request.Page, request.Stars); bool? error = null; GitHubSearchApiResponse response = null; @@ -65,10 +61,10 @@ private async Task SearchRepo(SearchRepositoriesRequest { try { - response = await _searchApiRequester.GetResponse(_client, request); + response = await _searchApiRequester.GetResponse(request); error = false; } - catch (RateLimitExceededException ex) + catch (RateLimitExceededException) { _logger.LogError("Exceeded GitHub RateLimit! Waiting 5 seconds before retrying."); await Task.Delay(5_000); @@ -116,13 +112,15 @@ private async Task> GetResultsFromGitHub() return resultList; } - // TODO: Block unwanted repos + // TODO: Block unwanted repos (https://github.com/NuGet/NuGetGallery/issues/7298) resultList.AddRange(response.Items); page++; if (page == lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) { - _logger.LogWarning($"Last page results have the same star count! StarCount: {response.Items.First().StargazersCount}\n{GetConfigInfo()}"); // TODO + // This may result in missing data since more the entire result set produced by a query has the same star count, we can't produce + // an "all the repos with the same star count but that are not these ones" query + _logger.LogWarning("Last page results have the same star count! This may result in missing data. StarCount: {Stars} {ConfigInfo}", response.Items.First().StargazersCount, GetConfigInfo()); return resultList; } } diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs deleted file mode 100644 index 7cbac8650..000000000 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchApiRequester.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Octokit; - -namespace NuGet.Jobs.GitHubIndexer -{ - public interface IGitHubSearchApiRequester - { - Task GetResponse(IGitHubClient client, SearchRepositoriesRequest request); - } -} diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchWrapper.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchWrapper.cs new file mode 100644 index 000000000..2900fa604 --- /dev/null +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/IGitHubSearchWrapper.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Octokit; + +namespace NuGet.Jobs.GitHubIndexer +{ + public interface IGitHubSearchWrapper + { + /// + /// Queries the GitHub Repo Search Api and returns its reponse + /// + /// Request to be made to the GitHub Repo Search Api + /// Parsed reponse of the GitHub Repo Search Api + Task GetResponse(SearchRepositoriesRequest request); + + /// + /// Returns the number of remaining requests before the search gets throttled + /// + /// Returns the number of remaining requests or null if no info is available (no request has been done yet) + int? GetRemainingRequestCount(); + } +} diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index 3d0c4a3e9..a4f70d63f 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -28,7 +28,7 @@ protected override void ConfigureJobServices(IServiceCollection services, IConfi { services.AddTransient(); services.AddSingleton(provider => new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent))); - services.AddSingleton(provider => new GitHubSearchApiRequester()); + services.AddSingleton(provider => new GitHubSearchWrapper()); services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); } diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 02f9255ff..168e6559e 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -28,7 +28,7 @@ private static GitHubSearcher GetMockClient(Func c.GetLastApiInfo()) .Returns(dummyApiInfo); - var mockSearchApiRequester = new Mock(); + var mockSearchApiRequester = new Mock(); mockSearchApiRequester .Setup(r => r.GetResponse(It.IsAny(), It.IsAny())) .Returns(async (IGitHubClient client, SearchRepositoriesRequest request) => @@ -143,7 +143,7 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m { _configuration.ResultsPerPage = resultsPerPage; _configuration.MinStars = minStars; - _configuration.MaxGitHubResultPerQuery = maxGithubResultPerQuery; + _configuration.MaxGitHubResultsPerQuery = maxGithubResultPerQuery; // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) var items = new List(); From b904dff0dc4171ebf8f6e96ba7d3e693e56c1972 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:18:04 -0700 Subject: [PATCH 34/44] [GH Idx] Remove redundant variable --- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index 701b59569..54a040013 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -15,7 +15,6 @@ namespace NuGet.Jobs.GitHubIndexer public class GitHubSearcher : IGitRepoSearcher { private readonly ILogger _logger; - private readonly TimeSpan OneMinute = TimeSpan.FromMinutes(1); private readonly IOptionsSnapshot _configuration; private readonly IGitHubSearchWrapper _searchApiRequester; From a41166757ffaf888f93f840ed62f73b5b6e94b08 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:20:56 -0700 Subject: [PATCH 35/44] [GH Idx] Trim tests Assembly info --- .../Properties/AssemblyInfo.cs | 37 +++---------------- 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs index 63078dc45..ff1360f3d 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/Properties/AssemblyInfo.cs @@ -1,36 +1,9 @@ -using System.Reflection; -using System.Runtime.CompilerServices; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. [assembly: AssemblyTitle("GitHubIndexer.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("NuGet.Jobs.GitHubIndexer.Tests")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4a64feb4-198c-445b-835f-a5b68efbfda7")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: Guid("4a64feb4-198c-445b-835f-a5b68efbfda7")] \ No newline at end of file From 46488162069797f8205f70eed4178328a01d8263 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:33:11 -0700 Subject: [PATCH 36/44] [GH Idx] Add checks to ensure the required info is in the GitHub response --- .../GitHub/GitHubSearchWrapper.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs index 016e5705c..c2128c809 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; +using System.IO; using System.Threading.Tasks; using Octokit; @@ -19,16 +21,28 @@ public GitHubSearchWrapper(IGitHubClient client) public int? GetRemainingRequestCount() { var apiInfo = _client.GetLastApiInfo(); - return apiInfo == null ? (int?)null : apiInfo.RateLimit.Remaining; + return apiInfo?.RateLimit.Remaining; } public async Task GetResponse(SearchRepositoriesRequest request) { var apiResponse = await _client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); + if(!apiResponse.HttpResponse.Headers.TryGetValue("Date", out var ghStrDate) + || !DateTime.TryParseExact(ghStrDate, "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture, DateTimeStyles.None, out var ghTime)) + { + throw new InvalidDataException("Date is required to compute the throttling time."); + } + + if(!apiResponse.HttpResponse.Headers.TryGetValue("X-RateLimit-Reset", out var ghStrResetLimit) + || !long.TryParse(ghStrResetLimit, out var ghResetTime)) + { + throw new InvalidDataException("X-RateLimit-Reset is required to compute the throttling time."); + } + return new GitHubSearchApiResponse( - apiResponse.Body, - DateTime.ParseExact(apiResponse.HttpResponse.Headers["Date"], "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", System.Globalization.CultureInfo.InvariantCulture).ToLocalTime(), - DateTimeOffset.FromUnixTimeSeconds(long.Parse(apiResponse.HttpResponse.Headers["X-RateLimit-Reset"])).ToLocalTime()); + apiResponse.Body, + ghTime.ToLocalTime(), + DateTimeOffset.FromUnixTimeSeconds(ghResetTime).ToLocalTime()); } } } From c9fbb29912fc8e35c5745cf7c6a4bc56a05bcaf4 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:39:51 -0700 Subject: [PATCH 37/44] [GH Idx] Moved public method before private methods --- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index 54a040013..295932e5c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -34,6 +34,30 @@ public GitHubSearcher( private int _resultsPerPage => _configuration.Value.ResultsPerPage; private int _maxGithubResultPerQuery => _configuration.Value.MaxGitHubResultsPerQuery; + /// + /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns them. + /// + /// List of C# repos on GitHub that have more than 100 stars + public async Task> GetPopularRepositories() + { + _logger.LogInformation("Starting search on GitHub..."); + var result = await GetResultsFromGitHub(); + return result + .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) + .Select( + group => + { + var repo = group.First(); + return new RepositoryInformation( + $"{repo.Owner.Login}/{repo.Name}", + repo.HtmlUrl, + repo.StargazersCount, + Array.Empty()); + }) + .OrderByDescending(x => x.Stars) + .ToList(); + } + private async Task CheckThrottle() { if (_searchApiRequester.GetRemainingRequestCount() == 0) @@ -107,7 +131,7 @@ private async Task> GetResultsFromGitHub() if (response.Items == null || !response.Items.Any()) { - _logger.LogWarning($"Search request didn't return any item. Page: {request.Page} {GetConfigInfo()}"); + _logger.LogWarning("Search request didn't return any item. Page: {Page} {ConfigInfo}", request.Page, GetConfigInfo()); return resultList; } @@ -130,30 +154,6 @@ private async Task> GetResultsFromGitHub() return resultList; } - /// - /// Searches for all the C# repos that have more than 100 stars on GitHub, orders them in Descending order and returns them. - /// - /// List of C# repos on GitHub that have more than 100 stars - public async Task> GetPopularRepositories() - { - _logger.LogInformation("Starting search on GitHub..."); - var result = await GetResultsFromGitHub(); - return result - .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) - .Select( - group => - { - var repo = group.First(); - return new RepositoryInformation( - $"{repo.Owner.Login}/{repo.Name}", - repo.HtmlUrl, - repo.StargazersCount, - Array.Empty()); - }) - .OrderByDescending(x => x.Stars) - .ToList(); - } - private string GetConfigInfo() { return $"MinStars: {_minStars}\n" + From c2e771daf7f42141d95632237964bcd28be932c4 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 14:44:23 -0700 Subject: [PATCH 38/44] [GH Idx] Extract retry time in a static variable --- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index 295932e5c..56f0f82c3 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -14,6 +14,8 @@ namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearcher : IGitRepoSearcher { + private static readonly TimeSpan LimitExceededRetryTime = TimeSpan.FromSeconds(5); + private readonly ILogger _logger; private readonly IOptionsSnapshot _configuration; private readonly IGitHubSearchWrapper _searchApiRequester; @@ -66,7 +68,7 @@ private async Task CheckThrottle() _throttleResetTime = DateTimeOffset.Now; if (sleepTime.TotalSeconds > 0) { - _logger.LogInformation($"Waiting {sleepTime.TotalSeconds} seconds to cooldown."); + _logger.LogInformation("Waiting {TotalSeconds} seconds to cooldown.", sleepTime.TotalSeconds); await Task.Delay(sleepTime); } @@ -89,8 +91,8 @@ private async Task SearchRepo(SearchRepositoriesRequest } catch (RateLimitExceededException) { - _logger.LogError("Exceeded GitHub RateLimit! Waiting 5 seconds before retrying."); - await Task.Delay(5_000); + _logger.LogError("Exceeded GitHub RateLimit! Waiting for {time} before retrying.", LimitExceededRetryTime); + await Task.Delay(LimitExceededRetryTime); } } From bbc09fbbb1bc19a427b24c55e77490b93d19a93c Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 15:49:15 -0700 Subject: [PATCH 39/44] [GH Idx] Add typecheck and fix tests --- .../GitHub/GitHubSearchApiResponse.cs | 8 +- .../GitHub/GitHubSearchWrapper.cs | 13 +- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 34 ++-- src/NuGet.Jobs.GitHubIndexer/Job.cs | 2 +- .../NuGet.Jobs.GitHubIndexer.csproj | 4 +- .../GitHubSearcherFacts.cs | 176 +++++------------- 6 files changed, 75 insertions(+), 162 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs index 84c2151df..fb56cc846 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs @@ -2,20 +2,22 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using NuGetGallery; using Octokit; namespace NuGet.Jobs.GitHubIndexer { public class GitHubSearchApiResponse { - public GitHubSearchApiResponse(SearchRepositoryResult result, DateTimeOffset date, DateTimeOffset throttleResetTime) + public GitHubSearchApiResponse(IReadOnlyList result, DateTimeOffset date, DateTimeOffset throttleResetTime) { - Result = result; + Result = result ?? throw new ArgumentNullException(nameof(result)); Date = date; ThrottleResetTime = throttleResetTime; } - public SearchRepositoryResult Result { get; } + public IReadOnlyList Result { get; } public DateTimeOffset Date { get; } public DateTimeOffset ThrottleResetTime { get; } } diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs index c2128c809..3c9e0f746 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs @@ -4,7 +4,9 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Threading.Tasks; +using NuGetGallery; using Octokit; namespace NuGet.Jobs.GitHubIndexer @@ -27,20 +29,25 @@ public GitHubSearchWrapper(IGitHubClient client) public async Task GetResponse(SearchRepositoriesRequest request) { var apiResponse = await _client.Connection.Get(ApiUrls.SearchRepositories(), request.Parameters, null); - if(!apiResponse.HttpResponse.Headers.TryGetValue("Date", out var ghStrDate) + if (!apiResponse.HttpResponse.Headers.TryGetValue("Date", out var ghStrDate) || !DateTime.TryParseExact(ghStrDate, "ddd',' dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture, DateTimeStyles.None, out var ghTime)) { throw new InvalidDataException("Date is required to compute the throttling time."); } - if(!apiResponse.HttpResponse.Headers.TryGetValue("X-RateLimit-Reset", out var ghStrResetLimit) + if (!apiResponse.HttpResponse.Headers.TryGetValue("X-RateLimit-Reset", out var ghStrResetLimit) || !long.TryParse(ghStrResetLimit, out var ghResetTime)) { throw new InvalidDataException("X-RateLimit-Reset is required to compute the throttling time."); } return new GitHubSearchApiResponse( - apiResponse.Body, + apiResponse.Body.Items + .Select(repo => new RepositoryInformation( + $"{repo.Owner.Login}/{repo.Name}", + repo.HtmlUrl, + repo.StargazersCount, + Array.Empty())).ToList(), ghTime.ToLocalTime(), DateTimeOffset.FromUnixTimeSeconds(ghResetTime).ToLocalTime()); } diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index 56f0f82c3..a7c4bd868 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -45,17 +45,8 @@ public async Task> GetPopularRepositories() _logger.LogInformation("Starting search on GitHub..."); var result = await GetResultsFromGitHub(); return result - .GroupBy(x => x.FullName) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) - .Select( - group => - { - var repo = group.First(); - return new RepositoryInformation( - $"{repo.Owner.Login}/{repo.Name}", - repo.HtmlUrl, - repo.StargazersCount, - Array.Empty()); - }) + .GroupBy(x => x.Id) // Used to remove duplicate repos (since the GH Search API may return a result that we already had in memory) + .Select(g => g.First()) .OrderByDescending(x => x.Stars) .ToList(); } @@ -76,7 +67,7 @@ private async Task CheckThrottle() } } - private async Task SearchRepo(SearchRepositoriesRequest request) + private async Task> SearchRepo(SearchRepositoriesRequest request) { _logger.LogInformation("Requesting page {Page} for stars {Stars}", request.Page, request.Stars); @@ -105,11 +96,11 @@ private async Task SearchRepo(SearchRepositoriesRequest return response.Result; } - private async Task> GetResultsFromGitHub() + private async Task> GetResultsFromGitHub() { _throttleResetTime = DateTimeOffset.Now; var upperStarBound = int.MaxValue; - var resultList = new List(); + var resultList = new List(); var lastPage = Math.Ceiling(_maxGithubResultPerQuery / (double)_resultsPerPage); while (upperStarBound >= _minStars) @@ -131,26 +122,29 @@ private async Task> GetResultsFromGitHub() var response = await SearchRepo(request); - if (response.Items == null || !response.Items.Any()) + if (response == null || !response.Any()) { _logger.LogWarning("Search request didn't return any item. Page: {Page} {ConfigInfo}", request.Page, GetConfigInfo()); return resultList; } // TODO: Block unwanted repos (https://github.com/NuGet/NuGetGallery/issues/7298) - resultList.AddRange(response.Items); + resultList.AddRange(response); page++; - if (page == lastPage && response.Items.First().StargazersCount == response.Items.Last().StargazersCount) + if (page == lastPage && response.First().Stars == response.Last().Stars) { // This may result in missing data since more the entire result set produced by a query has the same star count, we can't produce // an "all the repos with the same star count but that are not these ones" query - _logger.LogWarning("Last page results have the same star count! This may result in missing data. StarCount: {Stars} {ConfigInfo}", response.Items.First().StargazersCount, GetConfigInfo()); + _logger.LogWarning("Last page results have the same star count! This may result in missing data. StarCount: {Stars} {ConfigInfo}", + response.First().Stars, + GetConfigInfo()); + return resultList; } } - upperStarBound = resultList.Last().StargazersCount; + upperStarBound = resultList.Last().Stars; } return resultList; diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index a4f70d63f..e31f8949c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -28,7 +28,7 @@ protected override void ConfigureJobServices(IServiceCollection services, IConfi { services.AddTransient(); services.AddSingleton(provider => new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent))); - services.AddSingleton(provider => new GitHubSearchWrapper()); + services.AddSingleton(); services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); } diff --git a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj index 3e85da921..f39dedf22 100644 --- a/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj +++ b/src/NuGet.Jobs.GitHubIndexer/NuGet.Jobs.GitHubIndexer.csproj @@ -45,10 +45,10 @@ - + - + diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 168e6559e..d7dd5bc4c 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; +using NuGetGallery; using Octokit; using Xunit; @@ -14,114 +15,23 @@ namespace NuGet.Jobs.GitHubIndexer.Tests { public class GitHubSearcherFacts { - private static GitHubSearcher GetMockClient(Func> searchResultFunc = null, GitHubSearcherConfiguration configuration = null) + private static GitHubSearcher GetMockClient(Func>> searchResultFunc = null, GitHubSearcherConfiguration configuration = null) { - var dummyApiInfo = new ApiInfo( - new Dictionary(), // links - Array.Empty(), // Oauth scopes - Array.Empty(), // accepted Oauth scopes - "", // Etag - new RateLimit(10, 10, 10)); - - var connection = new Mock(); - connection - .Setup(c => c.GetLastApiInfo()) - .Returns(dummyApiInfo); - var mockSearchApiRequester = new Mock(); mockSearchApiRequester - .Setup(r => r.GetResponse(It.IsAny(), It.IsAny())) - .Returns(async (IGitHubClient client, SearchRepositoriesRequest request) => + .Setup(r => r.GetResponse(It.IsAny())) + .Returns(async (SearchRepositoriesRequest request) => { - return new GitHubSearchApiResponse(searchResultFunc == null ? new SearchRepositoryResult() : await searchResultFunc(request), DateTimeOffset.Now, DateTimeOffset.Now); + return new GitHubSearchApiResponse(searchResultFunc == null ? new List() : await searchResultFunc(request), DateTimeOffset.Now, DateTimeOffset.Now); }); - var mockClient = new Mock(); - mockClient.SetupGet(c => c.Connection).Returns(connection.Object); - var mockApiConnection = new ApiConnection(connection.Object); var optionsSnapshot = new Mock>(); optionsSnapshot .Setup(x => x.Value) .Returns( () => configuration ?? new GitHubSearcherConfiguration()); - return new GitHubSearcher(mockClient.Object, mockSearchApiRequester.Object, new Mock>().Object, optionsSnapshot.Object); - } - - private static Repository CreateRepository(string fullName, int starCount = 100) - { - var ownerName = fullName.Split('/')[0]; - var repoName = fullName.Split('/')[1]; - var owner = new User( - avatarUrl: "", - bio: "", - blog: "", - collaborators: 0, - company: "", - createdAt: DateTimeOffset.Now, - updatedAt: DateTimeOffset.Now, - diskUsage: 100, - email: "", - followers: 10, - following: 10, - hireable: true, - htmlUrl: "", - totalPrivateRepos: 0, - id: 1, - location: "", - login: ownerName, - name: ownerName, - nodeId: "", - ownedPrivateRepos: 0, - plan: null, - privateGists: 0, - publicGists: 0, - publicRepos: 0, - url: "", - permissions: new RepositoryPermissions(), - siteAdmin: true, - ldapDistinguishedName: "", - suspendedAt: null); - - return new Repository( - url: "url", - htmlUrl: "htmlUrl", - cloneUrl: "cloneUrl", - gitUrl: "gitUrl", - sshUrl: "sshUrl", - svnUrl: "svnUrl", - mirrorUrl: "mirrorUrl", - id: 1, - nodeId: "nodeId", - owner: owner, - name: repoName, - fullName: fullName, - description: "description", - homepage: "homepage", - language: "csharp", - @private: false, - fork: true, - forksCount: 10, - stargazersCount: starCount, - defaultBranch: "master", - openIssuesCount: 0, - pushedAt: null, - createdAt: DateTimeOffset.Now, - updatedAt: DateTimeOffset.Now, - permissions: new RepositoryPermissions(), - parent: null, - source: null, - license: new LicenseMetadata(), - hasIssues: true, - hasWiki: true, - hasDownloads: true, - hasPages: true, - subscribersCount: 10, - size: 500, - allowRebaseMerge: true, - allowSquashMerge: true, - allowMergeCommit: true, - archived: false); + return new GitHubSearcher(mockSearchApiRequester.Object, new Mock>().Object, optionsSnapshot.Object); } public class GetPopularRepositoriesMethod @@ -146,47 +56,47 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m _configuration.MaxGitHubResultsPerQuery = maxGithubResultPerQuery; // Generate ordered results by starCount (the min starCount has to be >= GitHubSearcher.MIN_STARS) - var items = new List(); + var items = new List(); int maxStars = (totalCount + _configuration.MinStars); for (int i = 0; i < totalCount; i++) { - items.Add(CreateRepository("owner/Hello" + i, maxStars - i)); + items.Add(new RepositoryInformation("owner/Hello" + i, "dummyUrl", maxStars - i, Array.Empty())); } // Create a mock GitHub Search API that serves those results - Func> mockGitHubSearch = - req => - { - // Stars are split as "min..max" - var starsStr = req.Stars.ToString(); - var min = int.Parse(starsStr.Substring(0, starsStr.IndexOf('.'))); - var max = int.Parse(starsStr.Substring(starsStr.LastIndexOf('.') + 1)); - int idxMax = -1, idxMin = items.Count; - - for (int i = 0; i < items.Count; i++) - { - var repo = items[i]; - if (repo.StargazersCount <= max && idxMax == -1) - { - idxMax = i; - } - - if (repo.StargazersCount <= min) - { - idxMin = i; - break; - } - } - - var page = req.Page - 1; - var startId = idxMax + req.PerPage * page > idxMin ? idxMin : idxMax + req.PerPage * page; - - var itemsCount = Math.Min(_configuration.ResultsPerPage, idxMin - startId); // To avoid overflowing - var subItems = itemsCount == 0 ? new List() : items.GetRange(startId, itemsCount); - - return Task.FromResult(new SearchRepositoryResult(totalCount, itemsCount == _configuration.ResultsPerPage, subItems)); - }; + Func >> mockGitHubSearch = + req => + { + //Stars are split as "min..max" + var starsStr = req.Stars.ToString(); + var min = int.Parse(starsStr.Substring(0, starsStr.IndexOf('.'))); + var max = int.Parse(starsStr.Substring(starsStr.LastIndexOf('.') + 1)); + int idxMax = -1, idxMin = items.Count; + + for (int i = 0; i < items.Count; i++) + { + var repo = items[i]; + if (repo.Stars <= max && idxMax == -1) + { + idxMax = i; + } + + if (repo.Stars <= min) + { + idxMin = i; + break; + } + } + + var page = req.Page - 1; + var startId = idxMax + req.PerPage * page > idxMin ? idxMin : idxMax + req.PerPage * page; + + var itemsCount = Math.Min(_configuration.ResultsPerPage, idxMin - startId); // To avoid overflowing + IReadOnlyList subItems = itemsCount == 0 ? new List() : items.GetRange(startId, itemsCount); + + return Task.FromResult(subItems); + }; var res = await GetMockClient(mockGitHubSearch, _configuration).GetPopularRepositories(); Assert.Equal(items.Count, res.Count); @@ -195,9 +105,9 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m { var resItem = res[resIdx]; Assert.Equal(items[resIdx].Name, resItem.Name); - Assert.Equal(items[resIdx].FullName, resItem.Id); - Assert.Equal(items[resIdx].StargazersCount, resItem.Stars); - Assert.Equal(items[resIdx].Owner.Login, resItem.Owner); + Assert.Equal(items[resIdx].Id, resItem.Id); + Assert.Equal(items[resIdx].Stars, resItem.Stars); + Assert.Equal(items[resIdx].Owner, resItem.Owner); } } } From 73c268a23053393a4864268d4f78d90aac84822d Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 15:50:00 -0700 Subject: [PATCH 40/44] [GH Idx] Remove redundant using --- .../GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs | 1 - src/NuGet.Jobs.GitHubIndexer/Job.cs | 2 +- tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs index fb56cc846..9b10f437c 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchApiResponse.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using NuGetGallery; -using Octokit; namespace NuGet.Jobs.GitHubIndexer { diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index e31f8949c..98ad745c3 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -16,7 +16,7 @@ public class Job : JsonConfigurationJob private const string GitHubIndexerUserAgent = "NuGet-NuGet.Jobs-GitHubIndexer"; private const string GitHubSearcherConfigurationSectionName = "GitHubSearcher"; - public override async Task Run() + public override async Task Run() { var searcher = _serviceProvider.GetRequiredService(); var repos = await searcher.GetPopularRepositories(); diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index d7dd5bc4c..007ca44ed 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -65,7 +65,7 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m } // Create a mock GitHub Search API that serves those results - Func >> mockGitHubSearch = + Func>> mockGitHubSearch = req => { //Stars are split as "min..max" From 63aca245d995207d8f4dcafa0c1789f4e27f6bff Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 16:05:22 -0700 Subject: [PATCH 41/44] [GH Idx] Nit space formatting --- .../GitRepoSearchers/GitHub/GitHubSearchWrapper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs index 3c9e0f746..f195d9952 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearchWrapper.cs @@ -44,10 +44,10 @@ public async Task GetResponse(SearchRepositoriesRequest return new GitHubSearchApiResponse( apiResponse.Body.Items .Select(repo => new RepositoryInformation( - $"{repo.Owner.Login}/{repo.Name}", - repo.HtmlUrl, - repo.StargazersCount, - Array.Empty())).ToList(), + $"{repo.Owner.Login}/{repo.Name}", + repo.HtmlUrl, + repo.StargazersCount, + Array.Empty())).ToList(), ghTime.ToLocalTime(), DateTimeOffset.FromUnixTimeSeconds(ghResetTime).ToLocalTime()); } From 07a2e4518e4c91b1dc12117e698c29167aedd50c Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 16:14:45 -0700 Subject: [PATCH 42/44] [GH Idx] Change UserAgent to use assembly name and version --- src/NuGet.Jobs.GitHubIndexer/Job.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/Job.cs b/src/NuGet.Jobs.GitHubIndexer/Job.cs index 98ad745c3..9cb364535 100644 --- a/src/NuGet.Jobs.GitHubIndexer/Job.cs +++ b/src/NuGet.Jobs.GitHubIndexer/Job.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Reflection; using System.Threading.Tasks; using Autofac; using Microsoft.Extensions.Configuration; @@ -13,7 +14,6 @@ namespace NuGet.Jobs.GitHubIndexer { public class Job : JsonConfigurationJob { - private const string GitHubIndexerUserAgent = "NuGet-NuGet.Jobs-GitHubIndexer"; private const string GitHubSearcherConfigurationSectionName = "GitHubSearcher"; public override async Task Run() @@ -26,8 +26,12 @@ public override async Task Run() protected override void ConfigureJobServices(IServiceCollection services, IConfigurationRoot configurationRoot) { + var assembly = Assembly.GetEntryAssembly(); + var assemblyName = assembly.GetName().Name; + var assemblyVersion = assembly.GetCustomAttribute()?.InformationalVersion ?? "0.0.0"; + services.AddTransient(); - services.AddSingleton(provider => new GitHubClient(new ProductHeaderValue(GitHubIndexerUserAgent))); + services.AddSingleton(provider => new GitHubClient(new ProductHeaderValue(assemblyName, assemblyVersion))); services.AddSingleton(); services.Configure(configurationRoot.GetSection(GitHubSearcherConfigurationSectionName)); From 8782195f2d4b1f5ac577ac3be1baff5be49bfad1 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 17:03:43 -0700 Subject: [PATCH 43/44] [GH Idx] Remove extra line --- tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs index 007ca44ed..a0b72855b 100644 --- a/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs +++ b/tests/NuGet.Jobs.GitHubIndexer.Tests/GitHubSearcherFacts.cs @@ -111,6 +111,5 @@ public async Task GetMoreThanThousandResults(int totalCount, int minStars, int m } } } - } } From a25c3a20e687b554a35e9b34f75471351509f0c1 Mon Sep 17 00:00:00 2001 From: Riad Mohamed Gahlouz Date: Mon, 24 Jun 2019 17:22:43 -0700 Subject: [PATCH 44/44] [GH Idx] Fix nit picks --- .../GitRepoSearchers/GitHub/GitHubSearcher.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs index a7c4bd868..b172d6153 100644 --- a/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs +++ b/src/NuGet.Jobs.GitHubIndexer/GitRepoSearchers/GitHub/GitHubSearcher.cs @@ -82,7 +82,7 @@ private async Task> SearchRepo(SearchReposi } catch (RateLimitExceededException) { - _logger.LogError("Exceeded GitHub RateLimit! Waiting for {time} before retrying.", LimitExceededRetryTime); + _logger.LogError("Exceeded GitHub RateLimit! Waiting for {LimitExceededRetryTime} before retrying.", LimitExceededRetryTime); await Task.Delay(LimitExceededRetryTime); } } @@ -134,8 +134,10 @@ private async Task> GetResultsFromGitHub() if (page == lastPage && response.First().Stars == response.Last().Stars) { - // This may result in missing data since more the entire result set produced by a query has the same star count, we can't produce - // an "all the repos with the same star count but that are not these ones" query + // GitHub throttles us after a certain number of results per query. + // We can only construct queries based on number of stars a repository has. + // As a result, if too many repositories have the same number of stars, + // we will lose data because we can't create another query that filters out the results that we have already seen with the same number of stars. _logger.LogWarning("Last page results have the same star count! This may result in missing data. StarCount: {Stars} {ConfigInfo}", response.First().Stars, GetConfigInfo());