From e3976d49688b6c6be8a874d0cde7681a5c9a388d Mon Sep 17 00:00:00 2001 From: MarsBarLee <46167686+MarsBarLee@users.noreply.github.com> Date: Wed, 8 Feb 2023 11:35:23 -0500 Subject: [PATCH 1/4] Add files --- ...ic-flavor-of-sql-for-python-programmers.md | 1218 +++++++++++++++++ ...2db9528ae260926e1258de124fe156ac24e5ed.png | Bin 0 -> 37004 bytes ...aaeda319436dc88038861ff8ee72bc5bdd140e.png | Bin 0 -> 8284 bytes ...7c33e991c43658e0b328f336af5eb6723531bc.png | Bin 0 -> 21326 bytes 4 files changed, 1218 insertions(+) create mode 100644 apps/labs/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers.md create mode 100644 apps/labs/public/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers/5d2db9528ae260926e1258de124fe156ac24e5ed.png create mode 100644 apps/labs/public/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers/7caaeda319436dc88038861ff8ee72bc5bdd140e.png create mode 100644 apps/labs/public/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers/d47c33e991c43658e0b328f336af5eb6723531bc.png diff --git a/apps/labs/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers.md b/apps/labs/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers.md new file mode 100644 index 000000000..a45589aa4 --- /dev/null +++ b/apps/labs/posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers.md @@ -0,0 +1,1218 @@ +--- +title: "Ibis: an idiomatic flavor of SQL for Python programmers" +author: tony-fast +published: June 26, 2020 +description: 'Ibis is an alternative approach using databases that relies on Python rather than SQL experience. This post focuses on writing SQL expressions in Python and how to compose queries visually using Ibis.' +category: [PyData ecosystem] +featuredImage: + src: /posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers/d47c33e991c43658e0b328f336af5eb6723531bc.png + alt: 'Excellent alt-text describing the featured image' +hero: + imageSrc: /posts/ibis-an-idiomatic-flavor-of-sql-for-python-programmers/d47c33e991c43658e0b328f336af5eb6723531bc.png + imageAlt: 'Excellent alt-text describing the hero image' +--- + + + +[Ibis](https://www.ibis-project.org/) is a mature open-source project +that has been in development for about 5 years; it currently has about +1350 stars on Github. It provides an interface to SQL for Python +programmers and bridges the gap between remote storage & execution +systems. These features provide authors the ability to: + +1. write backend-independent [SQL](https://en.wikipedia.org/wiki/SQL) + expressions in + [Python](https://en.wikipedia.org/wiki/Python_(programming_language)); +2. access different database connections (eg. + [SQLite](https://www.sqlite.org/index.html), + [OmniSci](https://www.omnisci.com/), + [Pandas](http://pandas.pydata.org/)); and +3. confirm visually their SQL queries with [directed acyclic graphs + (DAGs)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). + +Ibis is an alternative approach using databases that relies on Python +rather than SQL experience. Typically, users have to learn an entirely +new syntax or flavor of SQL to perform simple tasks. Now, those familiar +with Python can avoid a new learning curve by using Ibis for composing +and executing database queries using familiar Python syntaxes (i.e., +similar to Pandas and Dask). Ibis assists in formation of SQL +expressions by providing visual feedback about each Python object. This +post focuses on writing SQL expressions in Python and how to compose +queries visually using Ibis. We'll demonstrate this with a SQLite +database---in particular, [Sean Lahman's baseball +database](http://www.seanlahman.com/baseball-archive/statistics/). + +## Connecting to a database + +To get started, we'll need to establish a [database +connection](https://en.wikipedia.org/wiki/Database_connection). Ibis +makes it easy to create connections of different types. Let's go ahead +and do this now with the function +[`ibis.sqlite.connect`](https://docs.ibis-project.org/docs/api.html#sqlite-client) +(in this instance, the database used is a SQLite database): + +``` python +%matplotlib inline +import ibis +import pathlib, requests + +db_path = pathlib.Path.cwd() / 'lahmansbaseballdb.sqlite' + +if not db_path.exists(): # Downloads database if necessary + with open(db_path, 'wb') as f: + URL = 'https://github.com/WebucatorTraining/lahman-baseball-mysql/raw/master/lahmansbaseballdb.sqlite' + req = requests.get(URL) + f.write(req.content) + +client = ibis.sqlite.connect(db_path.name) # Opens SQLite database connection +``` + +The `client` object represents our connection to the database. It is +essential to use the appropriate Ibis connection---SQLite in this case +constructed through the [`ibis.sqlite` +namespace](https://docs.ibis-project.org/docs/api.html#sqlite-client)---for +the particular database. + +This [baseball +database](http://www.seanlahman.com/baseball-archive/statistics/) has 29 +distinct tables; we can see by running the following code: + +``` python +tables = client.list_tables() +print(f'This database has {len(tables)} tables.') +``` + + This database has 29 tables. + +## Selecting and visualizing tables + +Displaying the list `tables`, gives the names of all the tables which +include, among others, tables with identifiers + +``` {python} +[...'appearances'...'halloffame', 'homegames', 'leagues', 'managers',...] +``` + +Let's use the database connection to extract & examine dataframe +representations of the `halloffame` and `appearances` tables from the +baseball database. To do this, we can invoke the [`table` +method](https://docs.ibis-project.org/docs/generated/ibis.impala.api.ImpalaDatabase.table.html) +associated with the `client` object called with the appropriate names. + +``` python +halloffame = client.table('halloffame', database='base') +appearances = client.table('appearances', database='base') +``` + +At the moment, the objects objects `halloffame` and `appearances` just +constructed don't hold any data; instead, the objects are *expressions* +of type `TableExpr` that represent putative operations applied to the +data. The data itself is inert wherever it's actually located---in this +case, within the SQLite database. We can verify this by examining their +types or by using assertions like this: + +``` python +print(f'The object appearances has type {type(appearances).__name__}.') +assert isinstance(halloffame, ibis.expr.types.TableExpr), 'Wrong type for halloffame' +``` + + The object appearances has type TableExpr. + +We can examine the contents of these Ibis table expressions using the +[`TableExpr.limit`](https://docs.ibis-project.org/docs/generated/ibis.expr.api.TableExpr.limit.html) +or the `TableExpr.head` method (similar to the [Pandas `DataFrame.head` +method](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.head.html)). +That is, we can define an object `sample` that represents a sub-table +comprising the first few rows of the `halloffame` table: + +``` python +sample = halloffame.head() +print(f'The object sample is of type {type(sample).__name__}') +``` + + The object sample is of type TableExpr + +Remember, the object `sample` is a `TableExpr` object representing some +SQL query to extracts a sub-table from a larger table. We can view the +actual SQL query corresponding to `sample` by compiling it with the +[`compile` +method](https://docs.ibis-project.org/docs/generated/ibis.expr.api.Expr.compile.html) +and converting the result to a string: + +``` python +str(sample.compile()) +``` + + 'SELECT t0."ID", t0."playerID", t0.yearid, t0."votedBy", t0.ballots, t0.needed, t0.votes, t0.inducted, t0.category, t0.needed_note \nFROM base.halloffame AS t0\n LIMIT ? OFFSET ?' + +Another useful feature of Ibis is its ability to represent an SQL query +as a [DAG (Directed Acyclic +Graph)](https://en.wikipedia.org/wiki/Directed_acyclic_graph). For +instance, evaluating the object `sample` at the interactive command +prompt yields a visualization of a sequence of database operations: + +``` python +sample # This produces the image below in a suitably enabled shell +``` + + + +This image of a DAG is produced using [Graphviz](https://graphviz.org/); +those familiar with [Dask](https://dask.org/) may have used a similar +helpful feature to assemble [task +graphs](https://docs.dask.org/en/latest/graphviz.html). + +Finally, the actual sub-table corresponding to the expression sample can +be extracted using the [`execute` +method](https://docs.ibis-project.org/docs/generated/ibis.expr.api.Expr.execute.html) +(similar to +[`compute`](https://docs.dask.org/en/latest/api.html#dask.compute) in +[Dask](https://docs.dask.org)). The result returned by executing the +expression sample is a +[tidy](https://vita.had.co.nz/papers/tidy-data.pdf) [Pandas +`DataFrame`](https://pandas.pydata.org/docs/reference/frame.html) +object. + +``` python +result = sample.execute() +print(f'The type of result is {type(result).__name__}') +result # Leading 5 rows of halloffame table) +``` + + The type of result is DataFrame + +```{=html} +
| + | ID | +playerID | +yearid | +votedBy | +ballots | +needed | +votes | +inducted | +category | +needed_note | +
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | +1 | +cobbty01 | +1936 | +BBWAA | +226 | +170 | +222 | +Y | +Player | +None | +
| 1 | +2 | +ruthba01 | +1936 | +BBWAA | +226 | +170 | +215 | +Y | +Player | +None | +
| 2 | +3 | +wagneho01 | +1936 | +BBWAA | +226 | +170 | +215 | +Y | +Player | +None | +
| 3 | +4 | +mathech01 | +1936 | +BBWAA | +226 | +170 | +205 | +Y | +Player | +None | +
| 4 | +5 | +johnswa01 | +1936 | +BBWAA | +226 | +170 | +189 | +Y | +Player | +None | +
| + | ID | +yearID | +teamID | +team_ID | +lgID | +playerID | +G_all | +GS | +G_batting | +G_defense | +... | +G_2b | +G_3b | +G_ss | +G_lf | +G_cf | +G_rf | +G_of | +G_dh | +G_ph | +G_pr | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | +1 | +1871 | +TRO | +8 | +NA | +abercda01 | +1 | +1 | +1 | +1 | +... | +0 | +0 | +1 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 1 | +2 | +1871 | +RC1 | +7 | +NA | +addybo01 | +25 | +25 | +25 | +25 | +... | +22 | +0 | +3 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 2 | +3 | +1871 | +CL1 | +3 | +NA | +allisar01 | +29 | +29 | +29 | +29 | +... | +2 | +0 | +0 | +0 | +29 | +0 | +29 | +0 | +0 | +0 | +
| 3 | +4 | +1871 | +WS3 | +9 | +NA | +allisdo01 | +27 | +27 | +27 | +27 | +... | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 4 | +5 | +1871 | +RC1 | +7 | +NA | +ansonca01 | +25 | +25 | +25 | +25 | +... | +2 | +20 | +0 | +1 | +0 | +0 | +1 | +0 | +0 | +0 | +
5 rows × 23 columns
+| + | category | +count | +
|---|---|---|
| 0 | +Manager | +74 | +
| 1 | +Pioneer/Executive | +41 | +
| 2 | +Player | +4066 | +
| 3 | +Umpire | +10 | +
| + | ID | +playerID | +yearid | +votedBy | +ballots | +needed | +votes | +inducted | +category | +needed_note | +
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | +1 | +cobbty01 | +1936 | +BBWAA | +226.0 | +170.0 | +222.0 | +Y | +Player | +None | +
| 1 | +2 | +ruthba01 | +1936 | +BBWAA | +226.0 | +170.0 | +215.0 | +Y | +Player | +None | +
| 2 | +3 | +wagneho01 | +1936 | +BBWAA | +226.0 | +170.0 | +215.0 | +Y | +Player | +None | +
| 3 | +4 | +mathech01 | +1936 | +BBWAA | +226.0 | +170.0 | +205.0 | +Y | +Player | +None | +
| 4 | +5 | +johnswa01 | +1936 | +BBWAA | +226.0 | +170.0 | +189.0 | +Y | +Player | +None | +
| ... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +
| 4061 | +4187 | +lidgebr01 | +2018 | +BBWAA | +422.0 | +317.0 | +0.0 | +N | +Player | +None | +
| 4062 | +4188 | +millwke01 | +2018 | +BBWAA | +422.0 | +317.0 | +0.0 | +N | +Player | +None | +
| 4063 | +4189 | +zambrca01 | +2018 | +BBWAA | +422.0 | +317.0 | +0.0 | +N | +Player | +None | +
| 4064 | +4190 | +morrija02 | +2018 | +Veterans | +NaN | +NaN | +NaN | +Y | +Player | +None | +
| 4065 | +4191 | +trammal01 | +2018 | +Veterans | +NaN | +NaN | +NaN | +Y | +Player | +None | +
4066 rows × 10 columns
+| + | ID | +playerID | +yearid | +votedBy | +ballots | +needed | +votes | +inducted | +category | +needed_note | +... | +G_2b | +G_3b | +G_ss | +G_lf | +G_cf | +G_rf | +G_of | +G_dh | +G_ph | +G_pr | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | +2861 | +aaronha01 | +1982 | +BBWAA | +415 | +312 | +406 | +Y | +Player | +None | +... | +16 | +0 | +15 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 1 | +3744 | +abbotji01 | +2005 | +BBWAA | +516 | +387 | +13 | +N | +Player | +None | +... | +16 | +0 | +15 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 2 | +147 | +adamsba01 | +1937 | +BBWAA | +201 | +151 | +8 | +N | +Player | +None | +... | +16 | +0 | +15 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 3 | +260 | +adamsba01 | +1938 | +BBWAA | +262 | +197 | +11 | +N | +Player | +None | +... | +16 | +0 | +15 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
| 4 | +385 | +adamsba01 | +1939 | +BBWAA | +274 | +206 | +11 | +N | +Player | +None | +... | +16 | +0 | +15 | +0 | +0 | +0 | +0 | +0 | +0 | +0 | +
5 rows × 31 columns
+| + | + | count | +
|---|---|---|
| inducted | +yearID | ++ |
| N | +1936 | +105 | +
| 1937 | +106 | +|
| 1938 | +114 | +|
| 1939 | +99 | +|
| 1942 | +67 | +|
| ... | +... | +... | +
| Y | +2014 | +3 | +
| 2015 | +4 | +|
| 2016 | +2 | +|
| 2017 | +3 | +|
| 2018 | +6 | +
150 rows × 1 columns
+| + | count | +
|---|---|
| yearID | ++ |
| 1936 | +5 | +
| 1937 | +3 | +
| 1938 | +1 | +
| 1939 | +7 | +
| 1942 | +1 | +
| ... | +... | +
| 2014 | +3 | +
| 2015 | +4 | +
| 2016 | +2 | +
| 2017 | +3 | +
| 2018 | +6 | +
76 rows × 1 columns
+TXsS!QnXl-r1g`b(Fxw$#%!ZAkmtM}fl<h{%uQi-Rr
zEnd_lE^4KL!!pO)*ulX;+t}Ecey*r$j@yOjp4$`ul+L)=?d{+5=fFbLhmSk;R_4xD
z-aTnE{Bq-4APy0q*7*AcMk3j@fomBSZ`5=;iS}*PA+)4FY&{|s&ApGlW@T;@4bx^u
zLqkKx&2pUJqx0BD@u{O@W1(nrwlA-5U8Yn$bLNafqW#&kVL+|*Jw2Pio=NNoY{a~I
zbJXmt+vBHC>$^;I)DE0{$xJ*mcdE*E9eLpJQIbc_lljDn6Jxzq2SCduJg2R89(zdj
zp~!NSqqXVqp+o-p`SzJT9VKeIogY7PIaMzR>fV+Ows`8X!)ITSmbQ)#%f^j?&z>pG
zPJCdpdBBMluCAm+V5tM*-<_=ln6Ux!!{wj0S8AFo?PMQ(owz4qpFKN?AK%_SU^_SY
z(R*`o!TC1@7tA(PbT_{q1rXXwxS_)OJXHaL+>2c#xo1yiL5)0d-#>T-=Oh~=q=FOX9A0G*w@cCtoVsf;$P-a5`>gm6w
z#ONR}ldbo$_kyI;J%+=_(sg(37Z+#7awp|85Df?uMAV=-qKoXGN 3td~K
zss%ZTxbA{uk3vlP_UzkdEZx}Kn`$DzjlaTgRpDcIH~qOV$ShlO+<^}GAztU@=IV3q
z-n%!B&^vxSMP&)nfG%XO0^>y#kg(7(RA`+}%YzykKYdQ0IrHw>l{N9*`&fUq4|Q{A
zI37H>knrCX_ubr`k^f0?Kl)F_{oL#Sx8gqDvA1VatBT_-
QaO|Z<6iEjcaK;b||kHvgFANZ!{ZLOD( z5S4mV^2h>hLd}bd4C%&7Fy+BmOf3$*fovs+lJUg7;I$=T!QLEK;QT<42zF2q)F*K` z1(q&dng`>!%W%ilf#^%iz}Sms9W)tkGi}76i_A55n>ZA!v@C5H)?j2Wocu)&x^IvyLL4}*j$5hR4hFullBN3 z3m8h+F;wF}UcG}i&C%JJJ!~g3S4XF&g#M^=w7&u_R-ycRrDZNnrq`ZBG^L-N-!&u~ zq}0BQl5Qi}pufers%On$u%tyv?kvEW;zKfqX+T9a7sX(_A5{?IexV(BurCqU>DV zlrT658LdQoCy1(f|gtdCLBedg{3X}Hjxu!V+*K4bQyL>_=uv7 zSR}C|p{R+6ng~Ck5^J33H|N&LFk)x;L_`=qJJ2fYuk{A8^V^<}ukiRaVF A z0)O(oBeEp`D##+%hr74dbqzNP=sE8qF=2GEFw^i9h;E3e6OopVL<%256kas8S~%$e zm0_ZglJ&CD;1dU@L!DX^f3q>W#bPMa*m!w^aYT0`IA0Y70E;od>J-^D?T@XNfN;}6 zHHlT;J0I_kq|)~#IiKg8KDTZn@&f3%6XHH)qAXSWlmw;Kw$Iwfzmo_Qv`pT^B%K;O z#8U0`z44t--C8D=I1#9lqOj4{qEsO1E5-d>vgr1zaCIWrmI=G)f~zZuq1>mTA?ef_ zv|x(|O |+UVqDBJKtCUfVT6nWZ+Xm%15g%lan{rSwi}2bpj^xsi-M6GkxLeEp0zsgdHA z3x5Y0s2#VmvOs7x{;+$V14YUSN(c2i#q9;#x@N4~%FBG0@+c@9HzY|YkBWzVY5+wD z6Oi 2Xi#|JesAeY50izO!ndsUqL0t4$ehB z-riv+vT?F}w##~yoxKVG8UeG(A^|f5jl&54 Ql-A5)ecbjnF!kL8L|z-o+d)cp{ePKMz{O4ZLuk$FBn2;&Pz=%U7(3 z*FOQ!i} 3qWB*g8p2S48=z!I z&Si3{dU8B}z0eZLpDoYMpH6To{Dn5Scqcw1HIguzU9o zKs}ErITV%=j}Su?c>Q`JL~h8z;F8+0Ll=b-i9n(dc%&mU00wX|b@9@r*U;#ELOyO1 zS|ySo09ks$rK!x=uOR=|QDHGL+uDU9>MptuqE`{wvnK{`(+1%y$L;LYsX@3pmaj>& zZ?(0TyyZZO7+l| ;sp>7aaVxf#s^j|}JU@HWUszW5K3ZdguX=g+e0+Quf5L3UM8V%; z@5E;sx9N2Zy{S+PI$7K(0 &&2oYOWc_%DXly&Omg5jP2r9TN+>;xbzE{?vz23l2qe$PYTT zvdrgymDri<7KzBb3wfV%x5NN%Kh^rJ!d%Shz*C25*>sa4H!e{9dZK7#81)pteEA{< ze8&w4=E6*escJy;8T410o?FxmTqcT9fm{1(YG|w-9ZUwAOla0h@%fgsvnOnLE1?RM z0+1Q{5f)Q+)tx>YV}66H4giu}f&wyYKqtbD96BD%FNQmxjzB?6N0G?5!5sOj>v7px zL5uu0@>2jGktwMf;a}aEs>5EBG~pAfrKLrrK6Dhyf!CL!I9Ze$heI2XasU(F+(g!8 zrb(%tJ4x&<{-XLOE mev`hkG~{CBz_*>gdaJ z7jb`Xs&rPR$yipg03{%Olk^!17Y<1<2BLFjX53a@i@(E=0$8VjjtiBaIkEteFB6AW zVof#EUn?Y>JUPyZvy{z;-VybOx4W6!2twZxN$UYS4;zqb!~S>kr-8{91?b2m5r11s`2Dq1Hl8-z^G8_O+hT_d%zzS!umlesbr?W=o@R`E6r~O6CgSrF ze%!pdTz;#Bgs!cb+UOH}j9VEQ8RmRO&=~P=3RkabXgH|d0JS(9br(d3*4GbaOKCf~ zkUieKy+M|iJq+IiOmKT~%JgJ-JLcbq@vNr!Fh+;$MSX~73_jIU44%+vF!aM!xq_On zCCBV()~%|+si$<4j5a4iM8Z6jnxd5}RiGsg@y>BbHQ+y34}%_rtWL SQML#oZEu9kF4F*rSySWL=$wfgraS+}JA#B&%ZhRE& zpgeZgdaxb|4xpL@DJKX&JUXGCp59pyf0btb_OTU%Cou4}!k2+`+8HD%cYHdWqeFw+ zC`Cganb05RCU+L##%&(CJ9K=a26O}>1P=6uEA9?;BbCNkcl>;m{u?QzU5O;LgDiX0 zH%4*(N_T;j3@jWuvnWqOEyFw%DP} 7jpDGJHgz zD3jgv#EzzF;IspL(<)^Kv?_st%qNj`fyk#47;4g;3KL}xyCY73gP=YTMk*sPhG=fq z?D4o?TwGxOkB0Im9PP=?&s!#qe7^#hqDA&P!>~q`AgyIBuzV2Hn<}l2A16C{E&T#d zl#2>*s7)faFg`#3u)}DRY6G)Pl4?B+XejTB-z~%78H5=jblevU022X}YPMtqXdU{1 z=qy@tGIHdYF7yNl5yo?*w|YJNQfTI6cKKy)w7qL%7O*b?fp&V6uI0%g2jPy%G%LPN zLZy+7>D}-5?L%?a=u%+HSMWC0N=oWCj_yLT6J-wER z@e3pmlr}f7z$Tb=(R6xjiX2x}1R6lO`sn<(qZ<(T3GAmG`KfDjw{y3?X?tgY HCoO+M@F@I57-+IJ3*WV30(Dr6rcs4QXaD`X%5LxYb)3A_ZN3mzhJgNSV~)>L z+Y GoM0Xv%k=xDM!`7Oqhe`K5{IPC WT{CN}E*cjm;*(1$M2eE`IS~Fp~=lO|;GMKr)*8 zZ4BMrpLfiVmB72DMVY2PZtZst>2@73YLh6tnzlAW?c2wUE`!VM=-RxHO^$*0mBN9~ z1289kpPa115;Lem&LDZ`=-+%m%i-`_&cWe}tFI8poal{!YuA)6T*wRQc{1EAeRpX7 zkvROk1=t%0G{F$uknuzCcIYcNCT9s^@xsPQtZeWS#$EnBxBOZqktBiPg}ygz_CN-C z9{m5l=h_qe$z C1s2$GjVCh!em!%i7A%R5K6mD zR8DBT)?y6L#KeTO`;6rK3%yIJ65v})O`iF|Mz+#2y9A3zO;M3 #=GqD=QoN)iGwdL6lJN`}L6S9076c4-K8I+H{@)T81~QgafV znsq{GY%%3wpdkP@zbzGoB1{N8%cNu1b|Nt4J+oi9pbqR?4AS!HmUJn zg`OTWxLm%p$GsL=$1*G)*r%i{4S#R+g&w9-^>|iqR805|n{% z3gM80sJQ=*=ZLy(4@msr zjdn;GsR<^Pd+o294MYu1ErhS80RO|kBZ4Bd25JuXO}19!7Z9cS0I|TyO(Rl+EHgD6 zM5x?Xzqt!eVQ_30zH@3&b~;-SxC(ka?)qhIf`{~0iM9drjtZZC*le77841BTXN!N& z?LK`SG{~OcVuO}9 q^(W%Q zhJzdpR>GCXOaWFhaAl5a(roMrQ1%hWNHThbZH3>x&A)0M@rl{{SHciG^7kIkq6>lD zFB`zavd{7R(~I;ofsXef1xg|_qg&f6Q`2`wJJLlpKh=D0b{e+}ugr$_DI}w=phP!s zegFiCc>bW|VMbK3NVw%7AI+oej{lE~+5jyveM-&UT@F|10&r3=(|&{?&dtr%R)jo% zTwX;!;()SHOd_5uvG&zFoR+aTtZDN1uj6STgGd4=Bq3*syA1U;piwV~Z9od1H$f<( z!nLQqb8R;uj9WtG0pJNqwZt?Kx3@$ji6=_-H3Zu&ZEXZ1MYzZCW1%x>0&3bcH*=il z{0)qPoY#5IpE*bz2lS4Nj`=1j6QRJX1iUo_7knS+Weki+ty~}(2T?taOw{)!Xw^y3 zAy~5c0TY56CHNk98*EilBjB?4-p5~2OW>+d{|O=V0ZJI*DIZ*5 JM5>i6<{b~ zBf1Dg 1ZDytM$%wSO?g37z^d*G<-(ec8#jVVK+HB` z{HR#nEMr4MKZrtX$3qW<8o4K%up{SaEgbU1QHVifR{^e(4rDPa>$hCim$va@t*ZmK zEuL@et}>1RJaWxiq$EW^W@CAW*1HI}Wrv32;sJ~Q1l{go(=*shtm4Nd?$o+R1+@Cr z=14(njrDT@>39fMv4E287&xV_BE| sz69pNX>EYb0}AC0#axe$Kf`+_ew_<*5nL?P-(n@N1^BFxRhKM=i)A+afBA& zIPu?FPMUjAZu#Mn(9c-NRd|=vQrV~x$ZVp7B(MJkDowe+8U>Iq`i}C3h8ve{8q3R- z`e{>lEAVCzqV5kW3XmQ;IeHYJ<)ACS!+h5Rw!87KTo-$%3=;$|lw&=Rm5dzzGH}i$ zP!QafI7VggwnR1^mKNxbF2;pd2ve{|Z;81WU*vp$+C?Z^)@_@KJ;SFY1aWj?i#JaX zdP1EKWWl4o_*q0J-%G6*gQ7uY1FyUCv3RJ#mT$BM$7dlRWFfg&hnRU-Z?t)KqV<}) zn0{SbLj|2^_fwHCSI~cfgSq_Cb^AE0JzmvMWLj+ZI$E0f7JlBpO;Evti7tMQ&(^=x z`q5U7h0Oe<3&fN+8E*!HgV;kMNxyabwxB<}74pytEkz0_fqn}LdLS}Uc6a}^pb{7Z z3EF}YZZA+Hak$wbF`6rHWbdbcskK;acej!k#| WtHvg18JS;@U*SThSX_(~3rs>|&NC{qL1 z (l8ckFpLMbu5RzWxswSEGG? zMw9cXZLpv>Xrb-KMQ*zw$GV17T65lu(cj2nx}*%UF$fNuNW8A&oCy+4s|G9v`Ozgo zV+*P+At`B-!;OG|b(H?mqgu+!h-ws|vQhphazTc0uUX#V82iydxLg!Znwzhsi^Yho z=eIiz@ClV7IEO2ADPcGnfVzm=wFrB$|E=sT)H;T~j`Ad#MHAc5UeD8HKXwUrR1m2O z6+3$I9tbwj8)O!3R= f?9&YurFoXXBvV^kcj_QMDk_f&m=iQWn~ Ol>+-WTf_wddxbC *_6cWuX+ET4{B%3 zTd0DjlAJbp-(*fGM!J+CW!n$YXc?At{|?y-Aez}9W9t;~{y6aRE@gi7&^NLWjhNhY zU#RymEHnfx#XU8f3j|MlxG@u>a %e(*oEdj9@kBB|8KMZ+4&7-42f!1 zPCJc8r}@dyMu=EVO`}kVuacCKg8k*n)vG!4TO3v6B238n@?B&Axgp4r_=vTr3iKDs zgo|=^E-txS(;5aBKI|LY0}M;tyo8M9;VKYy_4=g4)wMKHhf&+q6!7>d;Vs248<%R_ zsMS_`dU_h6&@15mA_GCq&CZ?M4n`bur3cv|ME=~<)APOE@&@S?F(f8(=V?CV-cLu8 z1L*Eu3}}R?bpZv0k@ld(CT$@Q3S)$?ux~m<23(;lL`0n`(3NKytoVqoEyXLkiu#dd zvmn-LX<9C+;|Zb)s%uYuJiHy3e|CP**4MA7#sIIL-*ex76G#uO3C=?t73iz;yUsn; zq6gBr2tdF!fD)sf#OY0P=21Z#8gnE AHSLZrJweVqqI=1SN`SrDiDBAS?)`w6(OZ$IhZ&rB}c#8QqH>m?){N z46%>M>B+iRuM(lNNKH @Kj6rpGnyD9} z%5mot?Hg4BwDA#9vO#i2WDUI-;<1#%#FZTL$?Tq8m eMdkp2 zx)n!3jB#j|czlS{8R~02&x_X|R1XrE&K{mRoMXq1)0RwRNyvjlf$WC0YZbJs&G2-w?E06SNB C6r%;ttRp x~F;&*3dr9tw|LQ?0XiuXn8_ z2z*h0ToF2fO@Q)@UTC-4W02nP(e?lM^3O{z*pd+gN)eg>9vm?STq}`F13bJ8F$^SE z8HQqyW%=N9bpYEHos`@a)tB8>;y(X^5!uNZA3TL}lF#IubW68Ak?HXc-Z7VfKoG z+IiZglJVrAMP9c0Tr9rt&AAOQNqrdV+){H+hTCM4y5irzp=ro7PgJ7%PkVaq4>V3D z7r5^Bi@Ok+m9d^BA=GNIcx~Ug$uEbg<>? !+F6H2qB9yd9cN`VF;W z9zhb*C)*#=DQ}288h3zYgl?F$+gkE7RoXI#eV5~>m?}a$vxb* yw<4JgX+e56a(jT&kuC_q96n$Mi1-IZuHt-$2WA?A4KhL%&+V#) zV6S@_Gq5a3GfR^5F8}tx-3c&R@hyBGDC^ZwX %D}Q zWG6eK0VK``l)hw~06JU%6{xG+J)cdkR=k$axiG=3*h78H9V;k7Vk<=GPrn^DGD1c- z!qJYf24Kw9DC}(e#UgBg+k5A0L3K;qMDU8J55W7ko^nDEM#3=KfOQZcyfT= aHA+!GM=BV%`L%-|~=fW3QU zA)=z8$HycF?mgX4Vr>Ubz|^^%ZNIXjeypdzaada5&e{J^g{_U}2u?*1GN!Z~JyT>I zebE;R4H8$^`DZLEgzf~R7ZnkS!|7RzLlhA}GZ^E`*gvS-pIVQxSZy`cZ{OYlbu$RJ z#D-h@l95dW)t3g2JA(pi=4|l|q9Hec4uQ3dL=~cn(~gGkm5l8%;G=DdO7C$W?Cn*; z8gYVDP{w0c8r|;WzSn_%qSH76pQgz=pFN954z%3V_7VUax>!W&2oO9)fC90xSs1|W zO-dVfUpBU=UFUqMMx^s6t!e|Ii%MA%U4q9wWEY`@hoWmmOPrijieu|v9pm+XW8;m- z^wq3G8lYm4%)F85^cXM~2;V4~gjLUwjCtG5ctrc9ogtYUiWVd7o#CNFzBnz&KsEY} zHUOxg%Dou#3Un_kB^u*wYhy#)+3&i#=(5MVD59VV>p=?~^IF&6O?dK{s&{$}@eK}1a)Nir-Hzj M+o17}kHa>KtKsYzsu^o$pfkM+R$SaxF388=Ye0VkF&K)K=CbNG*Aqa#JovVJJ zDUNtF-nx79k)(j52F~Z|(`gwQm2eAC!b1GWAEHFnVp`(t!_%-=*wN$wm0yprcJQVu zI8n*o{!skJNwvZ5)TzuT1$Kdcev3e*{n#eUn4$KTbJ$Y92BXeYtn>_t8xsup#~N#c zJ@-sZP1l91KSEX>zHO3ZfN6&9*|saG{zczWv`u%v!#4qQ6d#Z%WoQxLqent%MYSlO zn+fh!Ai6{k4S4}^3t;|0eC<_c=lQRxBfc(f5*0LYV#A_BRFd4>+=v492eGqE;XugA zYnc=2GkYKTcmEGb)G=Iu3p4Mq#i=aPi3kk|Y3|l`LYyT3x4*qYvG)NlN`TKr+>}Qr ziYG>^iy(n{9|J;h`$9&h`kTVLidL(URQFDqq$2Y3^Q$db$iR?Xs5So`=H?DMW~UVu z9d(ET;M8*ci*3C1#+fJO`H&qu@Qx>hhN~y^eg3>j_<(r&9>kLmpL(Md*yJMpA@3}$ zoD=jDwWV$YQBBS6$F5n3w$o$36%~o}7&tb_K%mmvT4rnwOhhF$D$&>f^;cCjb~Fc6 zU%tq-CIhdR^6W4N7I?F+-#bPODbmu|&JZw>04X$i OT0Ok)ICl=T=Fpa69>19KV0Nj9-O}>lj zPqAQ8B+-)qf8^F%fFUD>NLXNeUPr68wk&5qCdN1K9fnL8f `La@AeS$hB2o`PC|7h;=rQ(77g z%x78Oj4HMnGawXu2?{V7i3RjqR{s1i3hSBN#mKy|#H^wVeYey;0v9GZ!4Sw4ieyQl z4}-6|_w(oOZZAwEj(hxASNkif7($AdmxG~MzIrvuH9!`^dWe&pot y#773oW---Ocrn!9 zILHUGoW2j~I26}G5slLh{=uE}3II3Moe$$MqFW1olk^V1ASl<@(^Eog13pd#-pP#u z@rb6-MST)}{W+$$AQY?_I81Bp^T?W;cUYVQbcFaF>OJw&ad7E}W1YglXY}a^nMBzy zUXZ<5^nDoeCw$lDB8L^g`m5hRQNzzJ%5Yd>{SU%6Hk%CU-9U{aFj{mvxijA8Y<9Hr z%>UCF=Tv{ lqlZiA|v)&Wy0iok0N0?-Q0Vj-o^oULQ3P?LTik_aHq54yYKqL&kXzGF63J)K| zF@TO)ceLik3r~C_wXRXpXXzo(ur;%?+6X5p8Fo{eK9!rTDSwS)%U*a^^ *Kw6Vsc0sGKzsp;^Zj&xWCoUB!c7blsbs$6?egs$rxK$F$Q;*L6H~E&@)i4 zqhA!waa)xUjqx)s-xw(*)t3Sc#CK2$y7w$(YeatLeOuoU%bLZ20M{v8+9Qb&ld7Ov z0CkMRp3FRacH3Yhzo{_;kQY+Wn&h3KBk(doXc7L7nT5Tm$$j7m3He?9-`Q4m=Xnmv z92hh2`<=J;T>-xe-gyBeWf%UsV=c)&XC ?TD&qvl*25&Ej?~nqX264x;fqF7vf%vb=Mx-e@9{>ZWPIUVVBte+%4FIFCr4hBdm=yaf!KNIQGp(rx1Zn|kOJpL1 zCL#H`B&-#5Xo4eTr$hz}e>k}X&?qAeMG03J{QgpkrVS8w;=Yg_eX*XqYd5?C6rug! z^*3Meyu-qUPnvklDFpVal@unho{osp+Zv$<)*Eg~U1$#@qNcL65OhEn3z;5}lP-E_ z-Z@ty#Ht$VVFD>b(oN>Wk~u8MOe2mKMCm&1m-|;OYC&J&WvPE9P`7j0(cNp!R3b7g z=pC4pN3hb`i`;iZ4tBI5CNz{bu2 BhRq{ay U8# zcaEDA8BPY(5V+V=tNJBa{H|VAd8T)9PW@pYZ2@WIVt-lrKhw)E=&{gmkO;PKE0wwL zTw(^-=S2FXSn%hVhqrGm!^P!`=nPyl_N)4b=o6!@WQ8H|TcutP%7+00=dyR$Wq4Xq>l5mA-duuY zrm^fT8!?0{UJSjDqO;kA8(sCrl-oNB^c;?j{ppgHC*5?(iDrF} Yra=s1Yo qs-9!{>NLpLljSnp1^X_tn2z##&A2 zer#Zkoc~t-EreUsU~?#2d)y$i=qp$3bA-}mxaMxr=uPEfy;@~(ZJB@%B9{Ket6r?h z)0ML05gHth3TRKhN0nu?RW%#t&lFg9q?!acWkowIeVr$7!fi<79fqD-AlB+mcP_L9 zh98v$1v(I-0qs`^mgvM_lMjUVwg}8|0*Ikiy+cZ>3FvJKx^Rf4RZvQjfi~~ozZX?= z!?{zkKxj7L=nMjo)f(N4wqg|@pB4zY*%faxBGM1V*8jdrZ_1jJ zl#rl8wgmcpg}H9FW&Q`kkyY8G(RvvJ5(^p{w1v0~;QtcJG3(NN%#`p#}z5MaTgNT7J zwli5WbMrJOT}S%I@9-RwsaLM)OKH!{zi=B8L}$I#e<>Oz;U8S@IZ-UGPwdnXN>w(t z4oY^-?SiXt !G4f!WV ZGS> z(%R%DMLFHHw&Dc@Qxuw^G&HrF7@{%;MhQq^VuY7(+st(4e9v=ke|+a#&m*`u`?A*W z_pbMSe`}rG9k6ZD`^(-(AP|f2+rRt o{(3iM3x4l=aLafv z<}dK^g2e5iDG0>EKhFNmxj&FO37_0_ sB5-awKsx8vVE5-KfcA^tTB2FA)gmCj6IQ>^;gh3@~eB$l<8xMzll3 zmih1ec|+|Sk8kFmAssFGVDBF=?|lF69Ll^sU#w~h`qPQs#RLAIL_Pu=uC9Im6VW%f zUmx4hUKxLJX@u*_Rqe>{c166m()P@IU#y%nf6I?I_*rbsg$F6dQtSN`V~RG@7pQph z@)?zr`I;@pO{802K0Cynhd_KQNbM)s!(&1W+aeHuUD)n`Kzwd<4g>#U_X!*wgyZfm z_;Yh@+(x55Agw7#*_5j5EMOWaksT*dW!JANC(am1D|mlvI&}JrTT8Mo2x#qWuR~mJ zUp>=m_Dtw5!{w|m3A9&4w0^wyZ4*uN@Ma j3rHXvwV_T)$bq>k9I=>nr3&89I0)Pk9tLj;ZR#o>T0Om zPWr)Uz_5e27B7rf@R$>$174!(8{f@UG}Ff}&R1@w$#sKihC*twDtSxtz-gH m2q0OWF-1b{@WH_8V|N4l!@#6`5PlbfHZ&bg{m8!f`ic%W2v~GMFcDI z#sULP4SWYX1mZ?(eNoUeiLcBL)tCl^2LY0Enr38PHI(N zVBEHhb^U|M^ADf*&td&<^?2VN?@B0=3$2AlOb~+!3^0P{!T@@-y&${`8hME9#`7)- zXcA-_ir!u|AD#%cXRBV;w)*1(u0sC!!z_&|#1{#N_jCn;(jiIeNntgAB0G(Di|0;F zt~ei#r@6=q))R%s!4_+5uk)tFl_g%(h=)_-XTo=7ssxeZxXg|PvQ#fuWdJVjmSQ+J zH27^3=IOgivFO8GL($mRTmi`+KYB(pfDgbYpF&doO%G2T=Kno4+|mSMwUZ^~g%UH@ zP@mGp!PD0K9#RH$Nes250?pObwr+E=#83==1d3{h=?T)W{@6I2g7|%F*N`~@rnx|B z2?QbYb|v@3XTvO4TenWc7RaYvWXoA$V2tW|bx7yIFc^u;+spI&uM5nR$ZpQPeeem- zs{FDY&Tu>&e&N0U%hdhGtX!Mg0%2MD7obL;V3Q)>B$;W(^d@h=vH8eT0rCao9mG0L z+8Yx7zwgZN-*Dsm_E2Cz3=QrBoUcKZ(1;Cr SR!sobd}z<`G=A$Ef+ ze0~vGF5F4TO0@7e*%`@C2gDW1|BSTY8>x81R8n3-MOpphRA+)w)1)ODzv1n4 zIIh284rj q`iroL>h)(gx4y+*>l^IRklsa_3+mxQ7y6G|8Wdt? zdWn$ST_!E)B+C^Ky1XT&Fqs*KYnVF!G5)@Z3#tCq>5COte)xhGWp%?*O4j4By`IE7 zG@^Z;|1S`y#VJ-#1)TX6l7C*=i)<_xn9=HCFIW2W_vJP)?)I;r#hq`V1_PuTlkTm? zta$t$%v^uuzKlOD0W~$Pn2njMWa#RQ?XzI)h9gcfY<2KWhv(^y_p6@nyb6Q5XwiR< z-bQj8{x=X0Z9-H&zDtQr*7QoQ#DsG6(Fj+%cGfKX3swHN=<7e2WmVT Jg_AmKIwfaVUb@7jkH&gecd_L4f1Tm~;hNl$1;876_yvgMuE$*_NV85p62S)i{1k7Ub`Eq z85ff$kH!`>D8Av9k&ibz5qYNGvMW^$Ir__jzF6LIC-VzNN1xp`FN(FjdP2{5x)hr> z+I(FgjILI?iQqw~K_8*OTm@}jJ$8 WlTRBDVXw*nu25wsF)VPp^k(0o7h{Y5Y;{aDwn$l~xf|-BwUFDIl-W z89v6wafRpgf6ZYw5AuZ4d!2n^o(?B++Wa-pN$d(v-lvv=K4Qp>Qj}`8RgMVO?t@tm zmQ;PRoRA(tF&yR!xcD+qGr$#8bj8Qa!t*c2H^%Hnd1%#T0c{U>Dz?sTmJe=Ye`geb z&mjMSh5HY{QX2;o=2BvJ%SwE*S$N!=!d7Jso~L+Uj&8qTD9K1$KGDqVRUXY-tlYn$ zm$p54dRbDvTzNd2UNZh1j+7zFwzal@++6FJo7!eFcHn%3J &zYr|1118uO$h@}) z08+mKaA^g MZq1??uD=F4M z$cnB*QM2j63*^CK5aQwoOEj5WLCvo4hPAS4q=HB4Fb{~*P lt_z=_`UcsBy z>#lLMOZx5a=GyS+V^8K`Izo*H NDXjXWn0)L8Lz1~wPjn<`NktumwgM(?~ zJXWrNqsCLlA2F70wO8Qz{dO$NIEd36oQlBe#;Y6LtAMvAGFACeAs^~vYjF68hlKR8 zEec00tsbU&SNvPS8?#!oFRh*6q&;Cb5O zKf=gZE+vpE4-F;@FY-4A_pAq-*T^CruNA@cutAc~=GK9eabP8Zf-W66GfN_6`OPWj zT4dl(( `RG@5t zL-b#st-tU=)K{=Ia0&uKLVDUNNBTn-!9{q~rBE8|UDd3~B#^T`S%vf?0u!Y@H)gW{ z&Sv2Z(wWL-UKE%+ks_AQ{=f4QQ%AtIdU+ w~H7mr(5VK@PVb+P=k z$|%f Ci~QEJG&n9TvmX*~*J+^or*?RzYNxC1=)e z`%?_?qelSUI%UVE7#OU-w!G8v;79B~zy;v9iqLz7yJ5S&VfjBR1!|GNxEdIjRE{J7 z;|=P`=z{CHs$z1BWG_IhDDaCONd|gYl_Pld ^= zu$3cuty9m-q0KdrIIS#INiEekSgIJ_I@0lV>XXeysQ#w(1M5iGb%~tJ?OJD!L1f3# z4f~<3hUepbRI@T;qGiZT1g7?baD^KPs{Bpq>d6#ngmlV5`LwV?ZhefDl*JIiG@z$u zSJ;&PnsPb`M4{*eP_oN(2bs?DStWDc#hu_?i;Dzv9GiRug9}lICzh*UqXSG&k?7KP z@l}EOO44*57*?!7fs;&d2Aj_$fQ3L0Jl?d*kuP$nxh2h>vXgirO2N{&;WJTifioRH zQde52SpsIPlr?}Et7H-v`c?oJ_bElPNS+~CAJZezRzcPA+<`RqjLtT&+zUzH3v#Gj z9AlN$8Sk5~Ifu+D)r@lmJGG%km5ZWKL&J-y?3of0}0JSTodneCDCIWrjWhhq0 zXEHtrdjNCxN&P97kB`g+pXniU7Rn>sBhv5`!;c>-O5E}Se{c>kH6inMCd1eX>DAHp zZ3mc50$LvFFh9h#F$E;LT6Ta$7fTl2sYKHx?@K}P9{BwwUlW0^4uCE96pDw_FI0Nz zjE%n2+SD3k)>%z)(u)C)N$o_PEJ64OYiH)EsaT1l$ZEL66{L42W#wz&e;%L;n562u z)5_65%lpbt>cvR=L$gR$<|%a}^1hnjo-15|jejhWqJ%~k<6%Er%@t6Cd&-f8Lb=dC zMpdw1CEg%Kx94k|F+SVw6RZ!vG?!biK|8_8M2dl;o(l623|0~;6HZ#V9!%g|AKeN+ zFrcOZ3J5fvOWN!!(_!?|yZ{xgzpS@j9uOYYVzoORm!UVtadB1Irl~xx!0I1kXn0wt zTKtee?1}3S&3S?@YZ~1eRETz*!Qo_7*Tz+{ZJ>{ztmR3Q;kvpS_Y-5n{| z2!{EV`t#!05+*3wX|iFdOPq|Gvhd)CZ5HCLLG9)NyU6q8*N4F4Yahj0U4{3Su= LP2wOcTolC`jCwN8ade?Fjp^I#x0y~qKqtK--OJ>w^j_A4OU8x{&?hE$ z%6!J#Nm;>)KTr0{iXAZlL4|pNrV5AeM?P8@uWCTQeAG(v%h$Bvo%|xEZ~8>P-VWN3 zj!vkh+aAScftm&u`;2A+Uh4@HW47Xp??iaCF$er$|C*7JYe>&QPHZ;~BZ+OpC{$ze zr`924+6t}ltY+?qz5Y~;mCKqE-mPEE@fKZ$GDT3pQZ1(l2?$a$R HMEZ@t(a$mu>}0*+Vt^S>hn|&|Lg