Y{zNiggVMru34Mxz(x@u=nR$zZ>%{ zl>6UzCbr2M@=7OOPS0I9)sY3Ta}wQXSIRcOT}+IUeFRQE z|9zqBCVF0)_M>PngOfh=u9S8YdgeBx1}|1~q@|$0&mqh81crRDyH<-zFju-3i5Y|F zdgEd@z&0vmQNgNuM66lToOb`3L#w-BansnB7=6U2hChF*Fm}sQTjDO8Zo+^Evyc EomSP-Jdpj8#PY1NBn~( z??l}MVP=Cry!cv=an{r>vYZLb8y#x+2IAiBQNd4x1D5J*!4=QI!^Ahc?C9Ne#Yj-S zzhD^;*u m1!LPS4@tkD~}Zm6ML{>h{k-sY{y- zt9K_Cdpq*fgk93+2xuxP$hqhWXSUngE}!6rQ}D99|06r4Zamem7R$=>V;g3&q)?oe zHE#m>Eg8nfkSBA;sQb+x0M3*mHuf{2mZ$Gum<3qxR`A9IE!m$A7OuLS`ek7@Oj~s3 z<vO{`w;Dt|t{_u`Mj7Qy@=h|+Nkafgza;PaS&Ae5F2{yuK9*FSSTy_Vlt z05qcdm(8vc33<(d+kTiib;X>SDTuNiDK1=?uYlbh#RJQ`((uPpqg1ySk |ADgLh*!2rPTPvD<;8LdcALK?E!68w>8*yae&kp{pn3BtaVIDtc69Y~e@ zAH*EOUa3QfAS}V5gid)e;3kYPE3YB(d(Fs0k=q=a!_j|+9@dx9bZNpVg|lo11V05? J4o>**-rw}+`quye delta 2764 zcmYk8c{J4D8^>qHSf(){OJ(e4j6M4jvddD5EXml4FoqdR8d)YJ`;yNf3fW0tkqojm zDoRG#N+h!H1}S9u>3h!i`};oU+ E+!w>U`Swt11=xxI2vh3cSYD0na ziH-Eqx!sw%<5QrH05m+u1cnxou?_sf_v7>XSD$&&pA;RG3s5{ipqbw&`MT3*J%XuB zL?~~uoY(BbZ?9|qIz5-2Oks(~BrE9z&XIG}sKcO2rh!Bai*)0!%JTD%sud543Ucn+ z6Q4c~=jcO-u|NZd+Ljxr&%1q6JojVbB1<3D&(HW>m{j7(AO~s;-#fpXh|d@fnXQrc znz%H{(a2E{?`eqh8gg#PU)VyBMl>G|*PWF{<4NgxzpbqMYCki`Q+10UbI#6dJCNN! zQ*1&PZ4PDybo-gIavPW0J!-wOB^JK~97~FQQP3GWb2%G%10}&j&Dnlnrhf85$vVwZ zSWZ&fZ7jkrHXx&u^;EPj>J&*W-V^MS#Mo3h^WgPimcmeJWQb1tiJ^1l74u~kId*Z~ zo~!2UQj7jwOHT15l@BY$G&cF0 SM##467OkqjHb%U;SR# zaeA0IA2C;5SFG9`F>!2Z+g#P>yOy$Cr!p83oNc7Rsf>55&h4YMCw`TVm06p)u@w`! zCUo?fRac8<`Qnh>OJ(Jrbo1qsG3@DtW3}HK4rXeE_!eDGpU(QwvJ1ax!Nd#Y0f9hl zhf5a}qjVK}0s;c5FaumHCjl%Qj65R38UA#Dtk 9Z+q&)>khR9 z1Yer_uT{9l@r8!IGeg(MA8IPUO6qU=T*b^ 6fcz5ls+ckTj4D# z!Q7B*w|cH8I1M=}q_&`Q#@%);N~i1Un4%jCCZTR%XzRwSP{hXbsh{0wC{LvlNthtS zW)!k;2uJV$jlaQ&YQ6Gs4SQW7;9^j8__}_xK%6U&{OV}d&r=# z8ONehT6b3OWtI9@pL8%WOM;Q1Z=8`|_A~__&M|T2$8DKo+Y;|TjC+?6=P6?_lQ|QnB%RnVR^S@_hYor>1Fm?bz_R|hwk5UO_}=O8M#UxG>y3Z#NaJC zK~G}ZOeFQ}`jm;#S@Dnn@zlwC+DC>9LKd@ejYIRUI_w$mjeHw&k8fgk{P>wnK`&cJ z4Q{qfw1=(k4Mkm9TbDV0E}y@)O!c>kEq&SHy)+W9Qfps&*MGxDJhbhZw4*ovTK-=h z`&aJ=WVS4rn+K{raWSlyQwS~S^1Ar8g;)fS9`$J!O0FXMVE1osyBw19A8_1He7`4j z2u{b};E+}1Nxt}G%1Fus1 ?>{Uaf7yluj-gso-iZ7TzD4# zJLP zf9GnL&>4m+01!d z_v?7m`ef{G`H#wZje0hoVkx5MaznD%r5j Sm5zWIH&S*r4(jP#^fMq684e7xAoviV* z%1H5gAlLe752*?+(DZRm;^(`k6UIe3>iDsd)rGC>L~1P&UtuUch&(WfCVIyAzWlaJ z{w0YU;l1C->{N!LdnH)asoNpiFdUj|+-8=jmNfTTQ+t1dc2^aTpOs 0(fLkLT&GtWj-pFKf{3RXpj+R))5`KNYgiE&9Hk$L5~*f~%SJ;i zPNmVeRl3Xc&$LX}DY7FJ-#KY}=C4Tt?Ry721xjFFHnWE1Ol}V*Sx&%L4TVZM zB_BAj!)A%PN z+!sTsmmNboyl+z)J;K;*jtpCa^jKPMu6EibM?kZD%48{XUltzC`!CBjY+;hEf8uXf zc{Js -SYj`FNEB`^n}_OO;Q`= z?cpYy z9*Ij*FL_%B{PMeImv@#~rP3%e8EZ$aLs%%kzL1{lp=i#r6;I+8nozVtNF{N?huFOo zR-HgP`4jdDY!lB5asax^`hUgS4x5p!wmPh&3A5HJ7Y^{QgG{S_9#p+tIZ4Nm);y8( zEPODJ#NwFH1)3GZ<5C)jXMq!0j5WYk{iR&sSlXi_oJJL mKm@b{aA8Mnogpcy7>6R!#^+%S_r3dYmfaCE?;^AaX#VVLZOcI67v zq1;REq8U}G9ZdFK0&Zs9Z`tpO iU$9@v<{Oc!q&Ko^ zh4%ScoK)wCi4b9oRUC{88%s0rfx@bgx^vS^w!4x>OfRo_4LUc`6u&|`{-T3bKgo3# zsUy`i3(h(DGxgD{8Qkz~IYlx*8=<49Rb!)Eqs(G__Xuh?%%cqq{i7>@9EQJ^$P>p7 zt!!lO2X;k-$#mQ+1 ePT *}+v;mjjj7gsC|$T#Pi!3WAAcu> z) @IoNMHe@0$CCK zJJpg@0Z#x#$%_y!k;4KS=t6M{{CRB5p)IvK6a%h*`X8RuNET6mTS^Cv1+1jRSx~4y GcmD&F^aq0g From 45a8d84957fd0f192b7fe8ae9a46ada1653db066 Mon Sep 17 00:00:00 2001 From: Sara-Jade-O Date: Mon, 13 Oct 2025 16:06:58 +0100 Subject: [PATCH 29/75] Update gptables/test/test_api/test_end_to_end.xlsx file --- gptables/test/test_api/test_end_to_end.xlsx | Bin 10624 -> 10618 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/gptables/test/test_api/test_end_to_end.xlsx b/gptables/test/test_api/test_end_to_end.xlsx index b8b81b2171a498ef745e15297a284b7bb804c09b..d3152d7ffedb4d0036be42081ed84d7051b4f7c3 100644 GIT binary patch delta 2203 zcmZ9Nc{mh$7sqFeHQOW+V}@c3CL-Gv2H6HFOEE %5EroA;#DlWvP%QM47nO zFoa=lmRrcaOln*rgID)?-uK?;J T#xXGT|D9mF~e-_nz8t(L1x?aSkVKPg7k{#DLm$?GZ9 z*F&SFPZ>JhzG!6%8LhB(i0r)J639sZCht@X(cX_Alz#cU`NDZet|y#otnT*!QNJ)@gI z4KgrmY`VNf_^_?2>%Wp~G5u!CrxD{urEkfI@h2Ik{}`w{uepl8)a#?ZSz@Lk|GJFX z f`qPsS|w)JydqFZL!c<#4&qJ+S9S|ew%Y|`jodT5 zEZo2|NgF<;wK^)xFxhi#=xb>^w-O=vo(wsd*O2F0+!k_~eq!o=^Up8&j)9dN+9%b< zJBoYmG|bW>Ki>8=^A6eXf{$;a)xBqPbF=1hqd6-dd5nWqs~jE|(hNEb{79XokkJbi z7iUU*ice+${#eglALLA&n1@-}HmybpdOc~vp8{B{gNdwtf|96_=MLGdEKY*hg#iEn z_mS%X9%8enMA-m<^F#w~X+pj-II?DpP|Nz9W6^8*JFcbjnmvwwjZUoxfiOVG#N}6e zKN>BSl%CK~0N7rEmg<}st>t62G1uc{o}yWQ5odM^7(RD5p0FSBv!Mg+)o(F*Oh7CO z6t}@llcbeA)}{kI-E{m(vnFd%hKUN|%g^(YV9i=DV%MK*lvgDYR^h|6^tV|?2DMU9 ztb`y;gQ<%$x>m;qGp^f+UTaT^&BC2%!(nRti?Rj^CSo7QDs=vsT|C_`bES}p=QLe- znQrNZ?D`-+gpCLB!ZpVcqFoP-;8%mDS0%sfa9}=p+Nv+T1HTNwkEuAAKf3U9wbj&a zwWq_Ks*HJ0WiKJP)HEy~6bM}YE9O*T;s~iZL{tTJd=9Bn5Vz@k+>+S~uC@4@j>~&y zBat3-ys~H#$*)-_?{QmmgW(sSf*{r^GEWOG$!lTs7FEW0_44LTQ`^4nta*j$n597@ zSEvZl)&~(q(Pt0hbS~* z`4VRt@}Iq?v*@A_#xP||%AqH?-$76stC|jRFE>VISLgRccY%zmKScz;b8{qYd>Vdw z;}sJ*SA02TVpP1<`nqc19~ZIr`P~=$=aqf@UK(%MnP8C(UhDPr97#U3cWz~M?661Y z4n1h9gI9@k =!bvN63g1)RaPeeNr+< z{PLBB<2Mit=-Fw_Xr5RjeR@TOo!4ju&Y=dX?0nM4P223pwzh^@vXK2G$BZj^ N+M(I !1Eo{0g4Dqk8p9 O#E*Ni`? zx(7PpE@WNxjB~L@8-;URR8LT8kt)U5&a`+2N*ezr7wEB{Q2(H)G5uzrUB4zoht473 zi}NS#ExUiOvKkbByEgbxC&HD&;PY{CNjKk~d^hqt!^uT$IQR!a_uFL9aKDI*#ZwtL zj|u%UM2iA5O=THnNSmR0NrK$6rkX8WIL};UsDA(T)$~cSkT16n8mqnGgO6z!clT-X zx+Rps65balB;;lfRqr;EZ-wtR&e^M?1coRUZ^$;qj_z9N)13$CX%$`s&-rXAQH^J% z NcXzmT^)4X#1U+ApqHQ2wKylhwho zPR?8X+?XP9lULxQow(eH?N~eZeP+tcjHXSVE#PgulwbJvCDO?A4DH(+V4XE%T<5H; z&6TaI%aSYL0{myDWc>i2sxW~nUOt96T0quE+#Ej|JZ^Ybs{p)z;5k=(0QMI_g&DUj zI+caHr#adT`@-|x8_zsp=wq)4up^T3r{7H7AC%1SY{KIw{XTJqGrunu?6oX8*$f6( zSVpyndIhDRvr5?2G%v1NrhK-4_!kh4QdY!xtBrXI!UprF@{ ?G?S_AY~J2n-mzq?<4BFb^gwFhiSHxFYA8?V+!uC{k;p=8X?|o(mWrSn9C#{FzNI-UZd0oS9+!*FrxB*M %3=$%#Y$|CRsgW0iPH9tG4V N+RICGa?AWW{||Eh{saI3 delta 2192 zcmV;B2ygfLQh-yi2L=g 9f}|wPzaJ&1nbG>NLmZOg^OC0M{N}Y6 1Qnz$Xy=^X#Idb9=$Va_28kz)GCgzX zf0{P7+5oEq)QdPvl11DzF6p4KG5@2Xnub?!ud00y(hVX81asKG?YOqI%xjKk{)LEv zrl7a!=59{oG7pu1b2!?OP5g^&mD+jdu`X!hQ{xZ0521-?LuzQ)Ubvs?_zig5Ih@+H zM=B+3c#@v`QBu;g*YJPjwd>HDN9jCSe`ME`RD0`G|K}V((*u{Cx$MGavuK$n>)CP= zjEB0R$9v{jnHzN^CKR@s`I|{M8BV40!CN0KC@Nv!epZKamB$A!gFhrMcs7Y=0bh*p z^)x=4#Ag9tj_@?O@FC@HA|;(JF`q0k5BzG(XYWc3DeG~{>|Kd?G|%p&!3ZvQe|BV_ zR|} 1da^y-OiL0o )2MnGc(STO6{fJTsn#p zv*Eb^=CK|Y>t8xLm(RWY3s6e~6$BIj000O8002J#g)(AX7y
k#@myvwa!_=G zeeyxPC{VJB?pyOi9=|YNH^x@YdoN_G{Tp=Ll@fxEzWVQxm!@&B8Cgb2j0t}P#a U%3fiH0 z>zQh~*y&(B43*17ZMdBm^<4UH8UsiGz`or=cfI1meDD3(f?DzND;G?k8ndT0`$kpG z?ODqz8*DVQSEhz>^J^n}zp^S@X>4Pkt;YUhm~xiA>Pkv;HmqMmw=FPxp~`4b$h~l* z3xnNjydI6NaBE!tcllpXO9KQH000080000#0Av{9ksbm70LBHA5e6B5G%k2;Y>kxL zj+-zLhVLu&9W38BU=zqez-m{kRaa`&Rn^0_0|u-G8`(~>efy40lA^>b^#bsC=KIGs zp2_dth8`WbHbTh)lSPaXmzAo8YzzGJ$J3`ZM#eH(vtCJF;6L2pyYl0Q{HXM+={UCt z6r?F|XYII+B2#sIU}m9zMlQkAC_OL>rfwrMYR+m84Shu8IEw}*B%Tzu`hPT3(};>c zs%k%QX(tiQduCz%rW2#VWnK&LxeX!Bn*!gH?E@h=Ds!Lum*7V;8zZ-qol>vPc&-Z^ zyU^$+_Q|i}MWdQGY~S1O>iC(9wzF_-sY5E&8~;XwaA%3pz~20S_b6)H6`0ZmU8f|? zXo^v_H&zY5Cl5LC6G=}by^v(HNLO(}!eBII_N#nkmX*0yN2GmfGcxyc$d oC6#^6OiE6XrKTe&hMoj8EbV??WD@5Gb2j3<>9OKQeU9 z;z@$eCFoU}b5P>l@jD6LxyK}sAh}LJ<_WHM5Y1|4)I!>Cqi-ar)uR3)BsZrL8(GW0 zGTjRB^}K<6@dD0IPxbG|R*ilHmusgil<3*&py_e#EV%e5jZ*e(fUae`p1&|{4XV^$ zx?X~3vq*0RbR_*;Plf(39E?i`F8>0v#SAkP1tS{#^(K@3ARd2ROT#b}hVKRcL&<%c zbVZq^?LcK$4g@EPcOg03g3Sj>wCle&{oDq-5^|Hg?|II94k=bgSx3KMozSX8X+ls0 zifJy?zC`cab-YB8^HgzKYXv20!J$=kds8rzGi~A3Y6I2_a8V#BmornMgZC!K*s%jh z>XP74LELF8sSkfwyT^vIZ@LG(AmkoP@RU Ma9wNB#fJ~}ZQo2E&ctha{zDgNBNy!E5TLUp@hfU1JCoLQh= z+p56lm$0~+I==}!-U;B3ZP2g0je`tzz@rc#AL35RNA@Q4yj`PeK^Av0d5ANzO|v{( zhV#|6yr@3`<=Euk#0fCuzWfB$AG1Ir-~tJVVpSLW0RR9qlPM)V8-+4rTo?iX05k;v z02lxO00000000000JecDlYu2G31k@Iksbm70LGKGC07B=lMN<30dkW{CP4wFlZ+-o z0p63&CP4u)lMp9C4OtWb0C;RKb98xZWpk6^CKQu$CmI3glZ_`p0?Hhd5g;3r>L)Y; z*&UM+ARCiAC^P}hlW-_d0sND}C^j1-8vOMp0RR9|0ssIJ00000000000002Cf$@_P SDLw&4lTj%n1`Z?u00019oB_!I From fe5cf728983ba8f248c3111535ea9728f0bc33d3 Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:15:55 +0100 Subject: [PATCH 30/75] 287 restructure docs (#297) * template restructure * restructure docs * run hooks * update changelog * add mkdocs-material for subheading support, add multi sheets example * run hooks --- docs/api.md | 54 ---- docs/{ => api}/cover.md | 2 +- docs/api/functions.md | 11 + docs/{ => api}/gptable.md | 8 +- docs/api/notes_sheet.md | 20 ++ docs/api/table_of_contents.md | 17 ++ docs/{ => api}/theme.md | 2 +- docs/{ => api}/wrappers.md | 2 +- docs/getting_started/elements.md | 3 + docs/{ => getting_started}/usage.md | 278 ++----------------- docs/how_to/additional_formatting.md | 112 ++++++++ docs/how_to/custom_theme.md | 117 ++++++++ docs/how_to/customisation.md | 4 + docs/how_to/in_r.md | 6 + docs/how_to/multiple_sheets.md | 58 ++++ docs/index.md | 16 +- docs/{ => info}/changelog.md | 1 + CONTRIBUTING.md => docs/info/contributing.md | 80 +++--- docs/{ => info}/contributors.md | 0 docs/info/examples.md | 1 + docs/{ => reference}/checklist.md | 0 docs/reference/glossary.md | 7 + docs/{imgs => static}/table_mapping.png | Bin mkdocs.yml | 46 ++- pyproject.toml | 3 +- 25 files changed, 467 insertions(+), 381 deletions(-) delete mode 100644 docs/api.md rename docs/{ => api}/cover.md (83%) create mode 100644 docs/api/functions.md rename docs/{ => api}/gptable.md (96%) create mode 100644 docs/api/notes_sheet.md create mode 100644 docs/api/table_of_contents.md rename docs/{ => api}/theme.md (98%) rename docs/{ => api}/wrappers.md (91%) create mode 100644 docs/getting_started/elements.md rename docs/{ => getting_started}/usage.md (60%) create mode 100644 docs/how_to/additional_formatting.md create mode 100644 docs/how_to/custom_theme.md create mode 100644 docs/how_to/customisation.md create mode 100644 docs/how_to/in_r.md create mode 100644 docs/how_to/multiple_sheets.md rename docs/{ => info}/changelog.md (99%) rename CONTRIBUTING.md => docs/info/contributing.md (97%) rename docs/{ => info}/contributors.md (100%) create mode 100644 docs/info/examples.md rename docs/{ => reference}/checklist.md (100%) create mode 100644 docs/reference/glossary.md rename docs/{imgs => static}/table_mapping.png (100%) diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index 731ebcf5..00000000 --- a/docs/api.md +++ /dev/null @@ -1,54 +0,0 @@ -# API functions - -#### NOTE -`auto_width` functionality is experimental - any feedback is welcome! -It currently does not account for alternative fonts, font sizes or font wrapping. - -## Table of contents - -By default, the API functions will add a table of contents sheet to your -Excel workbook. This will contain a single table with two columns. The first -column will contain the worksheet label and link for each worksheet in the -workbook. The second column will contain a description of the sheet contents. -By default, this is the title of the `GPTable` in that sheet. This -description can be customised by passing additional elements from the -`GPTable` into the `contentsheet_options` parameter. This parameter also -allows for customisation of the table of contents `title`, `subtitles`, -`table_name`, `instructions` and `column_names`. - -To customise the worksheet label, pass the new label into the -`contentsheet_label` parameter. This table of contents functionality can be -disabled by setting this parameter to `False`. - -See this in practice under [Example Usage](usage.md#example-usage). - -## Notes sheet - -A notes sheet will be generated if the API functions are provided with a -`notes_table`. The first column of the `notes_table` should contain a -meaningful reference for each note. This reference can then be used in the -worksheets - see the GPTable documentation for more details. When the notes -sheet is produced, this column will be replaced by the order the notes are -referenced in throughout the workbook. - -The second column should contain the text for each note. Optional additional -columns can be used for useful links, formatted as `"[display text](link)"`. - -The notes sheet can be customised using the `notesheet_options` parameter. -Values for the `title`, `table_name` and `instructions` can be provided -here. To customise the worksheet label, pass the new label into the -`notesheet_label` parameter. - -If a `notes_table` is not provided, the notes sheet will not be generated. - -See this in practice under [Example Usage](usage.md#example-usage). - -::: gptables.core.api.write_workbook - options: - heading: "write_workbook()" - - - -::: gptables.core.api.produce_workbook - options: - heading: "produce_workbook()" diff --git a/docs/cover.md b/docs/api/cover.md similarity index 83% rename from docs/cover.md rename to docs/api/cover.md index dac27d7c..ffcf69bf 100644 --- a/docs/cover.md +++ b/docs/api/cover.md @@ -8,7 +8,7 @@ This is important when applying additional formatting to other Worksheets by the ## Mapping -To include a cover sheet, map your text elements to the attributes of a `Cover` object and pass this object to the `cover` parameter of either [`produce_workbook()`](api.md#gptables.core.api.produce_workbook) or [`write_workbook()`](api.md#gptables.core.api.write_workbook). +To include a cover sheet, map your text elements to the attributes of a `Cover` object and pass this object to the `cover` parameter of either [`produce_workbook()`](functions.md#gptables.core.api.produce_workbook) or [`write_workbook()`](functions.md#gptables.core.api.write_workbook). Text attributes which take a list (most except for title) will write one element per cell vertically in the Worksheet. ## Formatting diff --git a/docs/api/functions.md b/docs/api/functions.md new file mode 100644 index 00000000..a19b5b5e --- /dev/null +++ b/docs/api/functions.md @@ -0,0 +1,11 @@ +# Functions + +::: gptables.core.api.write_workbook + options: + heading: "write_workbook()" + + + +::: gptables.core.api.produce_workbook + options: + heading: "produce_workbook()" diff --git a/docs/gptable.md b/docs/api/gptable.md similarity index 96% rename from docs/gptable.md rename to docs/api/gptable.md index 4db1dcaa..a204d87f 100644 --- a/docs/gptable.md +++ b/docs/api/gptable.md @@ -5,7 +5,7 @@ The `GPTable` Class is used to map your data and metadata to table elements. The supported table elements are represented like this in the output .xlsx file: - + ## Notes @@ -22,7 +22,7 @@ referenced as `"My table title $$Reference$$"`. References in text are replaced with numbers, in increasing order from the top- left corner of the first sheet containing a data table. -See this in practice under [Example Usage](usage.md#example-usage). +See this in practice in the Tutorial(../getting_started/usage.md#example-usage). #### NOTE Deprecated in v1.1.0: Ability to reference notes within @@ -61,7 +61,7 @@ formatting of that element specified in the [`Theme`](theme.md#gptables.core.the `["It is ", {"bold": True}, "inevitable"]` would give you “It is **inevitable**”. -See this in practice under [Example Usage](usage.md#example-usage). +Examples of additional formatting are found in [How-to: Additional formatting](../how_to/additional_formatting.md). #### NOTE Rich text is not currently supported if the cell also contains note @@ -211,7 +211,7 @@ or create a pull request. Otherwise, you will need to modify the underlying [`GPWorkbook`](wrappers.md#gptables.core.wrappers.GPWorkbook) or [`GPWorksheet`](wrappers.md#gptables.core.wrappers.GPWorksheet) objects before they are written to Excel. -See this in practice under [Example Usage](usage.md#example-usage). +Examples of additional formatting are found in [How-to: Additional formatting](../how_to/additional_formatting.md). ::: gptables.core.gptable.GPTable options: diff --git a/docs/api/notes_sheet.md b/docs/api/notes_sheet.md new file mode 100644 index 00000000..7424a8be --- /dev/null +++ b/docs/api/notes_sheet.md @@ -0,0 +1,20 @@ +## Notes sheet + +A notes sheet will be generated if the API functions are provided with a +`notes_table`. The first column of the `notes_table` should contain a +meaningful reference for each note. This reference can then be used in the +worksheets - see the GPTable documentation for more details. When the notes +sheet is produced, this column will be replaced by the order the notes are +referenced in throughout the workbook. + +The second column should contain the text for each note. Optional additional +columns can be used for useful links, formatted as `"[display text](link)"`. + +The notes sheet can be customised using the `notesheet_options` parameter. +Values for the `title`, `table_name` and `instructions` can be provided +here. To customise the worksheet label, pass the new label into the +`notesheet_label` parameter. + +If a `notes_table` is not provided, the notes sheet will not be generated. + +See this in practice in the [Tutorial](../getting_started/usage.md#penguins-notes-example). diff --git a/docs/api/table_of_contents.md b/docs/api/table_of_contents.md new file mode 100644 index 00000000..b2cc7ef0 --- /dev/null +++ b/docs/api/table_of_contents.md @@ -0,0 +1,17 @@ +## Table of contents + +By default, the API functions will add a table of contents sheet to your +Excel workbook. This will contain a single table with two columns. The first +column will contain the worksheet label and link for each worksheet in the +workbook. The second column will contain a description of the sheet contents. +By default, this is the title of the `GPTable` in that sheet. This +description can be customised by passing additional elements from the +`GPTable` into the `contentsheet_options` parameter. This parameter also +allows for customisation of the table of contents `title`, `subtitles`, +`table_name`, `instructions` and `column_names`. + +To customise the worksheet label, pass the new label into the +`contentsheet_label` parameter. This table of contents functionality can be +disabled by setting this parameter to `False`. + +See this in practice in the [Tutorial](../getting_started/usage.md#cover-page-example). diff --git a/docs/theme.md b/docs/api/theme.md similarity index 98% rename from docs/theme.md rename to docs/api/theme.md index 7bf4b777..054898b3 100644 --- a/docs/theme.md +++ b/docs/api/theme.md @@ -97,7 +97,7 @@ For minor adjustments to a theme, a deepcopy can be taken before using the `Theme` objects can altenatively be configured using dictionaries, with the same structure as the configuration files. -An example using a personalised theme YAML file can be found under [Example Usage](usage.md#example-usage). +An example using a personalised theme YAML file can be found under [How-to: Custom Theme](../how_to/custom_theme.md). ::: gptables.core.theme.Theme options: diff --git a/docs/wrappers.md b/docs/api/wrappers.md similarity index 91% rename from docs/wrappers.md rename to docs/api/wrappers.md index d352adf0..cbc05df6 100644 --- a/docs/wrappers.md +++ b/docs/api/wrappers.md @@ -1,7 +1,7 @@ # XlsxWriter wrappers These Classes are only likely used following use -of the [`produce_workbook()`](api.md#gptables.core.api.produce_workbook) API function, +of the [`produce_workbook()`](functions.md#gptables.core.api.produce_workbook) API function, which returns a [`GPWorkbook`](wrappers.md#gptables.core.wrappers.GPWorkbook) object. You may use these objects to carry out modification of any aspects of the diff --git a/docs/getting_started/elements.md b/docs/getting_started/elements.md new file mode 100644 index 00000000..965e99e9 --- /dev/null +++ b/docs/getting_started/elements.md @@ -0,0 +1,3 @@ +# Table elements + + diff --git a/docs/usage.md b/docs/getting_started/usage.md similarity index 60% rename from docs/usage.md rename to docs/getting_started/usage.md index 9f3a5980..da5051d1 100644 --- a/docs/usage.md +++ b/docs/getting_started/usage.md @@ -1,15 +1,20 @@ - +# Getting started with gptables -# Example Usage +## Installation +To install gptables, simply use: -This section demonstrates usage of the gptables API functions and core Classes. +``` +pip install gptables +``` -For source code and data used in these examples, please see the -[examples](https://github.com/best-practice-and-impact/gptables/tree/main/gptables/examples) directory of the package. +## Tutorial - +This section demonstrates usage of the gptables API functions and core Classes. + +For source code and data used in examples, please see the +[examples](https://github.com/ONSdigital/gptables/tree/main/gptables/examples) directory of the package. -## Penguins - Minimal Example +### Penguins - Minimal Example This example demonstrates use of the `gptables.write_workbook` function. This API function is designed for production of consistently structured and formatted tables. @@ -66,11 +71,9 @@ if __name__ == "__main__": print("Output written at: ", output_path) ``` - - -## Penguins - Minimal Example +### Penguins - Alternative Minimal Example -This example demonstrates another way to use the `gptables.write_workbook` function. +This example demonstrates another way to use the `gptables.write_workbook` function. This code is equivalent to that in the above example. ```python @@ -155,127 +158,7 @@ if __name__ == "__main__": print("Output written at: ", output_path) ``` - - -## Penguins - Theme Example - -This example demonstrates how to use a custom theme in the production of a workbook. - -Summary statistics from the penguins dataset are used to build a `gptables.GPTable` -object. Elements of metadata are provided to the corresponding parameters of the class. -Where you wish to provide no metadata in required parameters, use `None`. - -The theme parameter must take either a directory or a yaml file in the `gptables.write_workbook` function. -The yaml file used in this example can be found in the themes folder as ‘’penguins_test_theme.yaml’’. - -```python - -import pandas as pd - -import gptables as gpt - -# Read data -parent_dir = Path(__file__).parents[1] - -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") - -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet - -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "The Penguins Dataset" -penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] -penguins_scope = "Penguins" -penguins_source = "Palmer Station, Antarctica" - -kwargs = { - "table_name": penguins_table_name, - "title": penguins_title, - "subtitles": penguins_subtitles, - "scope": penguins_scope, - "source": penguins_source, -} -penguins_table = gpt.GPTable(table=penguins_data, **kwargs) - -penguins_sheets = {"Penguins": penguins_table} - -# Use write_workbook to win! -# Simply pass the filepath of the yaml file containing your theme to the GPTables Theme class and then to write_workbook -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_gptable.xlsx" - theme_path = str(Path(__file__).parent.parent / "themes/penguins_test_theme.yaml") - gpt.write_workbook( - filename=output_path, - sheets=penguins_sheets, - theme=gpt.Theme(theme_path), - contentsheet_options={"additional_elements": ["subtitles", "scope"]}, - ) - print("Output written at: ", output_path) -``` - -```python -import pandas as pd - -import gptables as gpt - -# Read data -parent_dir = Path(__file__).parents[1] - -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") - -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet - -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "The Penguins Dataset" -penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] -penguins_scope = "Penguins" -penguins_source = "Palmer Station, Antarctica" - -kwargs = { - "table_name": penguins_table_name, - "title": penguins_title, - "subtitles": penguins_subtitles, - "scope": penguins_scope, - "source": penguins_source, - "index_columns": { - 2: 0 - }, # The level 2 index from our Pandas dataframe is put in the first (zeroth with Python indexing) column of the spreadsheet -} - -# Define our GPTable -penguins_table = gpt.GPTable( - table=penguins_data, table_name="penguins_statistics", **kwargs -) - -penguins_sheets = {"Penguins": penguins_table} - -penguins_cover = gpt.Cover( - cover_label="Cover", - title="A Workbook containing two copies of the data", - intro=["This is some introductory information", "And some more"], - about=["Even more info about my data", "And a little more"], - contact=[ - "John Doe", - "Tel: 345345345", - "Email: [john.doe@snailmail.com](mailto:john.doe@snailmail.com)", - ], -) - -# Use write_workbook to win! -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_cover_gptable.xlsx" - gpt.write_workbook( - filename=output_path, - sheets=penguins_sheets, - cover=penguins_cover, - ) - print("Output written at: ", output_path) -``` - - - -## Penguins - Notes Example +### Penguins - Notes Example This example demonstrates how to include notes in a GPTable. Notes cannot be included in data cells but may appear either in column headers or in text such @@ -369,127 +252,11 @@ if __name__ == "__main__": print("Output written at: ", output_path) ``` - - -## Penguins - Additional Formatting Example - -This example demonstrates additional formatting that is not supported in -the `gptable.Theme`. - -Specific columns, rows and cells of the table elements (indexes, column headings and data) -can be formatted using the `gptable.GPTable(..., additional_formatting = ...)` parameter. -This parameter takes a list of dictionaries, allowing you to select as many rows, columns -or cells as you like. - -As with all formatting, supported arguments are desribed in the -[XlsxWriter documentation](https://xlsxwriter.readthedocs.io/format.html#format-methods-and-format-properties). - -Any formatting not possible through this means can be achieved using -`XlsxWriter` [Workbook](https://xlsxwriter.readthedocs.io/workbook.html) -and [Worksheet](https://xlsxwriter.readthedocs.io/worksheet.html) functionality. -A `gptable.GPWorkbook` object is returned when using the -`gptables.produce_workbook` API function. -The `GPWorkbook.worksheets()` function returns a list of `GPWorksheet` objects, -which can also be modified. - -```python -from pathlib import Path - -import pandas as pd - -import gptables as gpt - -# Read data and arrange -parent_dir = Path(__file__).parents[1] - -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") - -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet - -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "Penguins" - -# Individual words/phrases can have formatting applied without the use of the additional_formatting argument -penguins_subtitles = [ - "The first subtitle", - [{"bold": True}, "Just", " another subtitle"], -] -penguins_units = {key: "mm" for key in range(2, 5)} -penguins_scope = "Penguins" - -# Define additional formatting -# Columns can be referenced by name or number -# Rows may only be referenced by number -# Column and row numbers refer to the table elements, including indexes and column headings -penguins_additional_formatting = [ - { - "column": { - "columns": ["Species", "Island"], # str, int or list of either - "format": { - "align": "center", - "italic": True, - }, # The "Species" and "Island" columns are centre-aligned and made italic - } - }, - { - "column": {"columns": [3], "format": {"left": 1}} - }, # Gives the fourth column a left border - { - "row": { - "rows": -1, # Numbers only, but can refer to last row using -1 - "format": { - "bottom": 1, - "indent": 2, - }, # Give the last row a border at the bottom of each cell and indents two levels - } - }, -] - -kwargs = { - "table_name": penguins_table_name, - "title": penguins_title, - "subtitles": penguins_subtitles, - "units": penguins_units, - "scope": penguins_scope, - "source": None, - "additional_formatting": penguins_additional_formatting, -} - -# Define our GPTable -penguins_table = gpt.GPTable(table=penguins_data, **kwargs) - -# Use produce workbook to return GPWorkbook -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_additional_formatting_gptable.xlsx" - wb = gpt.produce_workbook(filename=output_path, sheets={"Penguins": penguins_table}) - - # Carry out additional modifications on the GPWorkbook or GPWorksheets - # This supports all `XlsxWriter` package functionality - ws = wb.worksheets()[0] - ws.set_row(0, 30) # Set the height of the first row - - # To format cells using the set_row or set_column functions we must use a workbook to create a format object - italic_format = wb.add_format({"italic": True}) - ws.set_column( - 2, 3, 10, italic_format - ) # Sets the width of the third and fourth column and makes them italic - - # Note that the first two arguments of set_column are the first and last columns (inclusive) you want to format as opposed - # to set_row which only affects a single row at a time (the first argument). - - # Finally use the close method to save the output - - wb.close() - print("Output written at: ", output_path) -``` - - - -## Labour market overview, UK: December 2020 - Real Survey Data Example +### Cover Page Example -This example demonstrates how to replicate the Labour Market overview accessible -example found at [https://analysisfunction.civilservice.gov.uk/policy-store/further-resources-for-releasing-statistics-in-spreadsheets/](https://analysisfunction.civilservice.gov.uk/policy-store/further-resources-for-releasing-statistics-in-spreadsheets/) +This example replicates the [Labour Market overview accessible +spreadsheet](https://analysisfunction.civilservice.gov.uk/policy-store/further-resources-for-releasing-statistics-in-spreadsheets/) example by the Analysis Function, based +on data from December 2020. ```python from pathlib import Path @@ -640,10 +407,3 @@ if __name__ == "__main__": ) print("Output written at: ", output_path) ``` - -## R Usage - -Use of `gptables` in R requires use of python via the [reticulate](https://rstudio.github.io/reticulate/) package. - -However we recommend use of the [aftables](https://github.com/best-practice-and-impact/aftables) -R package, maintained by the Presentation Champions Data Visualisation Tools subgroup. diff --git a/docs/how_to/additional_formatting.md b/docs/how_to/additional_formatting.md new file mode 100644 index 00000000..ae564760 --- /dev/null +++ b/docs/how_to/additional_formatting.md @@ -0,0 +1,112 @@ +## Penguins - Additional Formatting Example + +This example demonstrates additional formatting that is not supported in +the `gptable.Theme`. + +Specific columns, rows and cells of the table elements (indexes, column headings and data) +can be formatted using the `gptable.GPTable(..., additional_formatting = ...)` parameter. +This parameter takes a list of dictionaries, allowing you to select as many rows, columns +or cells as you like. + +As with all formatting, supported arguments are desribed in the +[XlsxWriter documentation](https://xlsxwriter.readthedocs.io/format.html#format-methods-and-format-properties). + +Any formatting not possible through this means can be achieved using +`XlsxWriter` [Workbook](https://xlsxwriter.readthedocs.io/workbook.html) +and [Worksheet](https://xlsxwriter.readthedocs.io/worksheet.html) functionality. +A `gptable.GPWorkbook` object is returned when using the +`gptables.produce_workbook` API function. +The `GPWorkbook.worksheets()` function returns a list of `GPWorksheet` objects, +which can also be modified. + +```python +from pathlib import Path + +import pandas as pd + +import gptables as gpt + +# Read data and arrange +parent_dir = Path(__file__).parents[1] + +penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") + +# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet + +# Define table elements +penguins_table_name = "penguins_statistics" +penguins_title = "Penguins" + +# Individual words/phrases can have formatting applied without the use of the additional_formatting argument +penguins_subtitles = [ + "The first subtitle", + [{"bold": True}, "Just", " another subtitle"], +] +penguins_units = {key: "mm" for key in range(2, 5)} +penguins_scope = "Penguins" + +# Define additional formatting +# Columns can be referenced by name or number +# Rows may only be referenced by number +# Column and row numbers refer to the table elements, including indexes and column headings +penguins_additional_formatting = [ + { + "column": { + "columns": ["Species", "Island"], # str, int or list of either + "format": { + "align": "center", + "italic": True, + }, # The "Species" and "Island" columns are centre-aligned and made italic + } + }, + { + "column": {"columns": [3], "format": {"left": 1}} + }, # Gives the fourth column a left border + { + "row": { + "rows": -1, # Numbers only, but can refer to last row using -1 + "format": { + "bottom": 1, + "indent": 2, + }, # Give the last row a border at the bottom of each cell and indents two levels + } + }, +] + +kwargs = { + "table_name": penguins_table_name, + "title": penguins_title, + "subtitles": penguins_subtitles, + "units": penguins_units, + "scope": penguins_scope, + "source": None, + "additional_formatting": penguins_additional_formatting, +} + +# Define our GPTable +penguins_table = gpt.GPTable(table=penguins_data, **kwargs) + +# Use produce workbook to return GPWorkbook +if __name__ == "__main__": + output_path = parent_dir / "python_penguins_additional_formatting_gptable.xlsx" + wb = gpt.produce_workbook(filename=output_path, sheets={"Penguins": penguins_table}) + + # Carry out additional modifications on the GPWorkbook or GPWorksheets + # This supports all `XlsxWriter` package functionality + ws = wb.worksheets()[0] + ws.set_row(0, 30) # Set the height of the first row + + # To format cells using the set_row or set_column functions we must use a workbook to create a format object + italic_format = wb.add_format({"italic": True}) + ws.set_column( + 2, 3, 10, italic_format + ) # Sets the width of the third and fourth column and makes them italic + + # Note that the first two arguments of set_column are the first and last columns (inclusive) you want to format as opposed + # to set_row which only affects a single row at a time (the first argument). + + # Finally use the close method to save the output + + wb.close() + print("Output written at: ", output_path) +``` diff --git a/docs/how_to/custom_theme.md b/docs/how_to/custom_theme.md new file mode 100644 index 00000000..00930b57 --- /dev/null +++ b/docs/how_to/custom_theme.md @@ -0,0 +1,117 @@ +## Penguins - Custom Theme Example + +This example demonstrates how to use a custom theme in the production of a workbook. + +Summary statistics from the penguins dataset are used to build a `gptables.GPTable` +object. Elements of metadata are provided to the corresponding parameters of the class. +Where you wish to provide no metadata in required parameters, use `None`. + +The theme parameter must take either a directory or a yaml file in the `gptables.write_workbook` function. +The yaml file used in this example can be found in the themes folder as ‘’penguins_test_theme.yaml’’. + +```python + +import pandas as pd + +import gptables as gpt + +# Read data +parent_dir = Path(__file__).parents[1] + +penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") + +# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet + +# Define table elements +penguins_table_name = "penguins_statistics" +penguins_title = "The Penguins Dataset" +penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] +penguins_scope = "Penguins" +penguins_source = "Palmer Station, Antarctica" + +kwargs = { + "table_name": penguins_table_name, + "title": penguins_title, + "subtitles": penguins_subtitles, + "scope": penguins_scope, + "source": penguins_source, +} +penguins_table = gpt.GPTable(table=penguins_data, **kwargs) + +penguins_sheets = {"Penguins": penguins_table} + +# Use write_workbook to win! +# Simply pass the filepath of the yaml file containing your theme to the GPTables Theme class and then to write_workbook +if __name__ == "__main__": + output_path = parent_dir / "python_penguins_gptable.xlsx" + theme_path = str(Path(__file__).parent.parent / "themes/penguins_test_theme.yaml") + gpt.write_workbook( + filename=output_path, + sheets=penguins_sheets, + theme=gpt.Theme(theme_path), + contentsheet_options={"additional_elements": ["subtitles", "scope"]}, + ) + print("Output written at: ", output_path) +``` + +```python +import pandas as pd + +import gptables as gpt + +# Read data +parent_dir = Path(__file__).parents[1] + +penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") + +# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet + +# Define table elements +penguins_table_name = "penguins_statistics" +penguins_title = "The Penguins Dataset" +penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] +penguins_scope = "Penguins" +penguins_source = "Palmer Station, Antarctica" + +kwargs = { + "table_name": penguins_table_name, + "title": penguins_title, + "subtitles": penguins_subtitles, + "scope": penguins_scope, + "source": penguins_source, + "index_columns": { + 2: 0 + }, # The level 2 index from our Pandas dataframe is put in the first (zeroth with Python indexing) column of the spreadsheet +} + +# Define our GPTable +penguins_table = gpt.GPTable( + table=penguins_data, table_name="penguins_statistics", **kwargs +) + +penguins_sheets = {"Penguins": penguins_table} + +penguins_cover = gpt.Cover( + cover_label="Cover", + title="A Workbook containing two copies of the data", + intro=["This is some introductory information", "And some more"], + about=["Even more info about my data", "And a little more"], + contact=[ + "John Doe", + "Tel: 345345345", + "Email: [john.doe@snailmail.com](mailto:john.doe@snailmail.com)", + ], +) + +# Use write_workbook to win! +if __name__ == "__main__": + output_path = parent_dir / "python_penguins_cover_gptable.xlsx" + gpt.write_workbook( + filename=output_path, + sheets=penguins_sheets, + cover=penguins_cover, + ) + print("Output written at: ", output_path) +``` + + diff --git a/docs/how_to/customisation.md b/docs/how_to/customisation.md new file mode 100644 index 00000000..166f523c --- /dev/null +++ b/docs/how_to/customisation.md @@ -0,0 +1,4 @@ +# Customisation with gptables + +There are limited customisation options with the gptables on account of aiming +to follow the Analysis Function Releasing statistics in spreadsheets [guidance](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/). diff --git a/docs/how_to/in_r.md b/docs/how_to/in_r.md new file mode 100644 index 00000000..157b5df0 --- /dev/null +++ b/docs/how_to/in_r.md @@ -0,0 +1,6 @@ +## R Usage + +Use of `gptables` in R requires use of python via the [reticulate](https://rstudio.github.io/reticulate/) package. + +However we recommend use of the [aftables](https://github.com/best-practice-and-impact/aftables) +R package, maintained by the Presentation Champions Data Visualisation Tools subgroup. diff --git a/docs/how_to/multiple_sheets.md b/docs/how_to/multiple_sheets.md new file mode 100644 index 00000000..47a08f11 --- /dev/null +++ b/docs/how_to/multiple_sheets.md @@ -0,0 +1,58 @@ +# Multiple sheets + +This example demonstrates how to create a workbook with multiple sheets. Note that it +will auto-generate a table of contents. + +``` +import gptables as gpt +import pandas as pd +import numpy as np +from pathlib import Path +from copy import deepcopy + +## Read data +parent_dir = Path(__file__).parents[1] + +penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") + +#Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet + +## Define table elements +penguins_table_name = "penguins_statistics" +penguins_title = "The Penguins Dataset" +penguins_subtitles = [ + "This is the first subtitle", + "Just another subtitle" + ] +penguins_scope = "Penguins" +penguins_source = "Palmer Station, Antarctica" + +kwargs = { + "table_name": penguins_table_name, + "title": penguins_title, + "subtitles": penguins_subtitles, + "scope": penguins_scope, + "source": penguins_source + } + +## Define our GPTable +penguins_table = gpt.GPTable(table=penguins_data, table_name="penguins_statistics", **kwargs) + +penguins_table_copy = deepcopy(penguins_table) +penguins_table_copy.set_title("A copy of the first sheet") +penguins_table_copy.set_table_name("penguins_statistics_copy") #All tables in a single workbook must have a unique name + +penguins_sheets = { + "Penguins": penguins_table, + "Copy of Penguins": penguins_table_copy +} + +## Use write_workbook to win! +if __name__ == "__main__": + output_path = parent_dir / "python_penguins_cover_gptable.xlsx" + gpt.write_workbook( + filename=output_path, + sheets=penguins_sheets + ) + print("Output written at: ", output_path) +``` diff --git a/docs/index.md b/docs/index.md index 0c5fed37..e0f4a498 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,27 +6,23 @@ either python or R (using [reticulate](https://rstudio.github.io/reticulate/)). You define the mapping from your data to elements of the table and `gptables` does the rest. -Table element mapping: - - - `gptables` uses the official [guidance on good practice spreadsheets](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/). It advocates a strong adherence to the guidance by restricting the range of possible operations. The default formatting theme `gptheme` accommodates many use cases. -However, the [`Theme`](theme.md#gptables.core.theme.Theme) Class allows development of custom themes, where alternative formatting is required. +However, the [`Theme`](api/theme.md#gptables.core.theme.Theme) Class allows development of custom themes, where alternative formatting is required. `gptables` is developed and maintained by the [Analysis Function](https://analysisfunction.civilservice.gov.uk/). It can be installed from [PyPI](https://pypi.org/project/gptables/) or [GitHub](https://github.com/best-practice-and-impact/gptables). The source code is maintained on GitHub. Users may also be interested in [aftables](https://best-practice-and-impact.github.io/aftables/), an R native equivalent to -`gptables`, and [csvcubed](https://gss-cogs.github.io/csvcubed-docs/external/), a package for turning data and metadata into +`gptables`, and [csvcubed](https://gss-cogs.github.io/csvcubed-docs/external/), a package for turning data and metadata intos machine-readable CSV-W files. ## 5 Simple Steps -1. You map your data to the elements of a [`GPTable`](gptable.md#gptables.core.gptable.GPTable). -2. You can define the format of each element with a custom [`Theme`](theme.md#gptables.core.theme.Theme), or simply use the default - gptheme. -3. Optionally design a [`Cover`](cover.md#gptables.core.cover.Cover) to provide information that relates to all of the tables in your Workbook. +1. You map your data to the elements of a [`GPTable`](api/gptable.md#gptables.core.gptable.GPTable). +2. You can define the format of each element with a custom [`Theme`](api/theme.md#gptables.core.theme.Theme), or simply use the default - gptheme. +3. Optionally design a [`Cover`](api/cover.md#gptables.core.cover.Cover) to provide information that relates to all of the tables in your Workbook. 4. Optionally upload a `notes_table` with information about any notes. -5. You [`write_workbook()`](api.md#gptables.core.api.write_workbook) to win. +5. You [`write_workbook()`](api/functions.md#gptables.core.api.write_workbook) to win. **Note**: This package is not intending to create perfectly accessible spreadsheets but will help with the bulk of the work needed. Users of this packages should refer back to the [main spreadsheet guidance](https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/) or the [spreadsheet accessibility checklist](https://analysisfunction.civilservice.gov.uk/policy-store/making-spreadsheets-accessible-a-brief-checklist-of-the-basics/) after using it to make sure nothing has been missed. Please email [Analysis.Function@ons.gov.uk](mailto:Analysis.Function@ons.gov.uk) if you use the package so we can monitor use and the outputs produced. diff --git a/docs/changelog.md b/docs/info/changelog.md similarity index 99% rename from docs/changelog.md rename to docs/info/changelog.md index a949d9bb..7cffe729 100644 --- a/docs/changelog.md +++ b/docs/info/changelog.md @@ -28,6 +28,7 @@ and this project tries its very best to adhere to * Updated README * Updated pull request template * Docs moved from readthedocs to mkdocs +* Docs are restructured ## Released (PyPI) diff --git a/CONTRIBUTING.md b/docs/info/contributing.md similarity index 97% rename from CONTRIBUTING.md rename to docs/info/contributing.md index 4a9dd039..253c7c63 100644 --- a/CONTRIBUTING.md +++ b/docs/info/contributing.md @@ -1,40 +1,40 @@ -# Contributing - -When contributing to this repository, please first discuss the change you wish -to make via issue, email, or any other method with the owners of this -repository before making a change. - -## Pull/merge request process - -1. Branch from the `dev` branch. If you are implementing a feature name it - `feature/name_of_feature`, if you are implementing a bugfix name it - `bug/issue_name`. If they are associated with a specific issue, you - may use the issue number in place of the name. -1. Update the README.rst and other documentation with details of major changes - to the interface, this includes new environment variables, useful file - locations and container parameters. -1. Once you are ready for review please open a pull/merge request to the - `dev` branch. -1. You may merge the Pull/Merge Request in once you have the sign-off of two - maintainers. -1. If you are merging `dev` to `master`, you must increment the version number - in the VERSION file to the new version that this Pull/Merge Request would - represent. The versioning scheme we use is [SemVer](http://semver.org/). - - -## Code style - -- We name variables using few nouns in lowercase, e.g. `mapping_names` - or `increment`. -- We name functions using verbs in lowercase, e.g. `map_variables_to_names` or - `change_values`. -- We use the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) - format for documenting features using docstrings. - -## Review process - -1. When we want to release the package we will request a formal review for any - non-minor changes. -2. The review process follows a similar process to ROpenSci. -3. Reviewers will be requested from associated communities. -4. Only once reviewers are satisfied, will the `dev` branch be released. +# Contributing + +When contributing to this repository, please first discuss the change you wish +to make via issue, email, or any other method with the owners of this +repository before making a change. + +## Pull/merge request process + +1. Branch from the `dev` branch. If you are implementing a feature name it + `feature/name_of_feature`, if you are implementing a bugfix name it + `bug/issue_name`. If they are associated with a specific issue, you + may use the issue number in place of the name. +1. Update the README.rst and other documentation with details of major changes + to the interface, this includes new environment variables, useful file + locations and container parameters. +1. Once you are ready for review please open a pull/merge request to the + `dev` branch. +1. You may merge the Pull/Merge Request in once you have the sign-off of two + maintainers. +1. If you are merging `dev` to `master`, you must increment the version number + in the VERSION file to the new version that this Pull/Merge Request would + represent. The versioning scheme we use is [SemVer](http://semver.org/). + + +## Code style + +- We name variables using few nouns in lowercase, e.g. `mapping_names` + or `increment`. +- We name functions using verbs in lowercase, e.g. `map_variables_to_names` or + `change_values`. +- We use the [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) + format for documenting features using docstrings. + +## Review process + +1. When we want to release the package we will request a formal review for any + non-minor changes. +2. The review process follows a similar process to ROpenSci. +3. Reviewers will be requested from associated communities. +4. Only once reviewers are satisfied, will the `dev` branch be released. diff --git a/docs/contributors.md b/docs/info/contributors.md similarity index 100% rename from docs/contributors.md rename to docs/info/contributors.md diff --git a/docs/info/examples.md b/docs/info/examples.md new file mode 100644 index 00000000..b942b1af --- /dev/null +++ b/docs/info/examples.md @@ -0,0 +1 @@ +# gptables in use diff --git a/docs/checklist.md b/docs/reference/checklist.md similarity index 100% rename from docs/checklist.md rename to docs/reference/checklist.md diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md new file mode 100644 index 00000000..c4771257 --- /dev/null +++ b/docs/reference/glossary.md @@ -0,0 +1,7 @@ +# Glossary of Terms + +## Cover + +## gptables + +## Theme diff --git a/docs/imgs/table_mapping.png b/docs/static/table_mapping.png similarity index 100% rename from docs/imgs/table_mapping.png rename to docs/static/table_mapping.png diff --git a/mkdocs.yml b/mkdocs.yml index 7e012a10..029449c3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,16 +1,38 @@ site_name: gptables +repo_url: https://github.com/ONSdigital/gptables + +theme: + name: material + features: + - toc.integrate + nav: - Home: index.md - - Navigation: - - API functions: api.md - - Example usage: usage.md - - GPTable: gptable.md - - Theme: theme.md - - Cover: cover.md - - XlsxWriter wrappers: wrappers.md - - Accessibility checklist: checklist.md - - Changelog: changelog.md - - Contributors: contributors.md + - Getting started: + - Tutorial: getting_started/usage.md + - Table elements: getting_started/elements.md + - How to: + - Multiple sheets: how_to/multiple_sheets.md + #- Customise sheets: how_to/customisation.md + - Add additional formatting: how_to/additional_formatting.md + - Add a custom theme: how_to/custom_theme.md + - Use in R: how_to/in_r.md + - API: + - Functions: api/functions.md + - Table of contents: api/table_of_contents.md + - Cover: api/cover.md + - Notes sheet: api/notes_sheet.md + - Theme: api/theme.md + - GPTable: api/gptable.md + - XlsxWriter wrappers: api/wrappers.md + - Reference: + - Accessibility checklist: reference/checklist.md + #- Glossary: reference/glossary.md + - About GPTables: + - Contributing guidance: info/contributing.md + - Changelog: info/changelog.md + - Contributors: info/contributors.md + # - gptables in use: info/examples.md plugins: - mkdocstrings: @@ -25,3 +47,7 @@ plugins: filters: - "!^__" # Hide methods like __init__() - "!^_" # Hide non-public facing methods + +markdown_extensions: +- toc: + toc_depth: 1-3 diff --git a/pyproject.toml b/pyproject.toml index 81c4a2d1..ea880950 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ docs = [ "mkdocs-material", "mkdocs-git-revision-date-localized-plugin", "mkdocs-print-site-plugin", - "mkdocstrings-python" + "mkdocstrings-python", + "mkdocs-material" ] dev = [ "coverage", From ddc8a147418f48d3681883363829a5030afeb65d Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:03:52 +0100 Subject: [PATCH 31/75] Delete closed_issue_log.md --- closed_issue_log.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 closed_issue_log.md diff --git a/closed_issue_log.md b/closed_issue_log.md deleted file mode 100644 index 95b148ed..00000000 --- a/closed_issue_log.md +++ /dev/null @@ -1,29 +0,0 @@ -# Closed Issue Log - -This log documents feature requests that we do not currently intend to offer in gptables. -This is due to the request being non-compliant with Analysis Function [spreadsheet accessibility guidance][guidance], -or otherwise being out of scope of the package, designed for implementing this guidance. -These issues may be re-opened in future if guidance changes - please contribute to the discussion by heading to -the GitHub issue. - -## [180][issue-180]: Multiple tables in a single spreadsheet (05/08/2025) -Section 13 of the guidance, 'Worksheets with multiple tables', dissuades this. - -## [265][issue-265]: Rounding decimals (02/09/2025) -A user requested the ability to round a column to a given number of decimal places. This issue was closed because it can be handled by [pandas][pandas-decimal-rounding]. - -## [136][issue-136]: Add hyperlinks to Theme format elements (05/09/2025) -Section 7 of the guidance, under 'Making hyperlinks accessible' says links should be underlined and in a colour of suitable -contrast. Further advice was sought from the Presentation Champions group, with the feedback that users expect links to be blue. - -## [104][issue-104]: Support table formatting as one unit (05/09/2025) -Interpreted as requesting the ability to add a line above and below a table. Section 7 of the guidance, under -'Other pointers for formatting that should be followed in terms of best practice', mentions that gridlines and borders -should be avoided in favour of keeping things simple. This is also on advice from the Presentation Champions. - -[guidance]: https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/ -[issue-104]: https://github.com/best-practice-and-impact/gptables/issues/104 -[issue-136]: https://github.com/best-practice-and-impact/gptables/issues/136 -[issue-180]: https://github.com/best-practice-and-impact/gptables/issues/180 -[issue-265]: https://github.com/best-practice-and-impact/gptables/issues/265 -[pandas-decimal-rounding]: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.round.html From b37b65023273fe3bf75e7f3687dd78d99dc43d6d Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:06:14 +0100 Subject: [PATCH 32/75] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491e..65e50eb2 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -6,6 +6,7 @@ labels: enhancement assignees: '' --- +It would help the development team for the requester to supply the following information: **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] @@ -13,6 +14,9 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate **Describe the solution you'd like** A clear and concise description of what you want to happen. +**Please confirm you've checked our [won't fix][https://github.com/ONSdigital/gptables/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22wontfix%20%3Acry%3A%22] issues**: +- [ ] + **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. From 7720f7a0dfcc46e35620cdff4d9ef804f4ed14de Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:06:42 +0100 Subject: [PATCH 33/75] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 65e50eb2..f7326fb9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -14,7 +14,7 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate **Describe the solution you'd like** A clear and concise description of what you want to happen. -**Please confirm you've checked our [won't fix][https://github.com/ONSdigital/gptables/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22wontfix%20%3Acry%3A%22] issues**: +**Please confirm you've checked our [won't fix](https://github.com/ONSdigital/gptables/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22wontfix%20%3Acry%3A%22) issues**: - [ ] **Describe alternatives you've considered** From d3c3323bbf7c97211e38a01977ff01d35ff44545 Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:07:22 +0100 Subject: [PATCH 34/75] Update feature_request.md --- .github/ISSUE_TEMPLATE/feature_request.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f7326fb9..77e46c05 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,17 +8,17 @@ assignees: '' --- It would help the development team for the requester to supply the following information: -**Is your feature request related to a problem? Please describe.** +**Is your feature request related to a problem? Please describe:** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -**Describe the solution you'd like** +**Describe the solution you'd like:** A clear and concise description of what you want to happen. -**Please confirm you've checked our [won't fix](https://github.com/ONSdigital/gptables/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22wontfix%20%3Acry%3A%22) issues**: -- [ ] +**Please confirm you've checked our [won't fix](https://github.com/ONSdigital/gptables/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22wontfix%20%3Acry%3A%22) issues** +Yes/no -**Describe alternatives you've considered** +**Describe alternatives you've considered:** A clear and concise description of any alternative solutions or features you've considered. -**Additional context** +**Additional context:** Add any other context or screenshots about the feature request here. From a4341e3a01f0a7b7b8189e6670fe17de22940325 Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:10:58 +0100 Subject: [PATCH 35/75] Update pull_request_template.md --- .github/pull_request_template.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 51ccd506..54d0ff58 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,3 +1,4 @@ + ### Proposed Changes - @@ -12,6 +13,7 @@ Please indicate items that aren't necessary and why, with comments around incomp - [ ] Version number has been incremented, according to [SemVer][semver] - [ ] Changelog has been updated, listing changes to this version. Use the [keep a changelog][changelog] format - [ ] New features are tested +- [ ] New features follow the Analysis Function Releasing statistics in spreadsheets [guidance][guidance] - [ ] New features are documented using the [numpydoc][numpy-docstrings] docstring format - [ ] Other relevant package documentation is updated - [ ] For new functionality, examples are included in the docs or a [feature request][feature-request] has @@ -19,5 +21,6 @@ been made for it/them. [changelog]: [https://keepachangelog.com/en/1.0.0/] [feature-request]: [https://github.com/best-practice-and-impact/gptables/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=] +[guidance]: https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/ [numpy-docstrings]: [https://numpydoc.readthedocs.io/en/latest/format.html] [semver]: [https://semver.org/spec/v2.0.0.html] From a6abb7f98875e26b02442c42d95c8b8966292a3a Mon Sep 17 00:00:00 2001 From: Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:20:01 +0100 Subject: [PATCH 36/75] update changelog --- docs/changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.md b/docs/changelog.md index a949d9bb..0cac99ff 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -18,7 +18,6 @@ and this project tries its very best to adhere to **Added** * Merged pull request templates to create new one -* Added a ‘closed issue log’ * Requirements and installation steps to README * Pre-commit hooks From 5ccc3fa36a5bdbc0edaa496c01bbff273cd4530b Mon Sep 17 00:00:00 2001 From: Sara-Jade-O Date: Tue, 14 Oct 2025 13:09:44 +0100 Subject: [PATCH 37/75] =?UTF-8?q?Add=20exception=20catching=20and=20exampl?= =?UTF-8?q?e=20of=20note=20entry=20with=20no=20link=20in=20peng=E2=80=A6?= =?UTF-8?q?=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add exception catching and example of note entry with no link in penguins_notes.py * Run pre-commits --------- Co-authored-by: daviel9 --- gptables/examples/penguins_notes.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gptables/examples/penguins_notes.py b/gptables/examples/penguins_notes.py index 9722bbae..6f82fe15 100644 --- a/gptables/examples/penguins_notes.py +++ b/gptables/examples/penguins_notes.py @@ -40,6 +40,7 @@ penguins_table_notes = { "species": "$$noteaboutx$$", 2: "$$noteaboutz$$", + "Comments": "$$noteaboutw$$", } # Columns can be referenced either by index or by name penguins_units = { 2: "mm", @@ -67,20 +68,30 @@ # Notesheet - Note that the ordering of each list only matters with respect to the other lists in the "notes" dictionary. # GPTables will use the "Note reference" list to ensure the "Note text" is assigned correctly +# All lists must be the same length. If a note has no link, use an empty string or None for that entry. notes = { - "Note reference": ["noteaboutz", "noteaboutx", "noteabouty"], + "Note reference": ["noteaboutz", "noteaboutx", "noteabouty", "noteaboutw"], "Note text": [ "This is a note about z linking to google.", "This is a note about x linking to duckduckgo.", "This is a note about y linking to the ONS website.", + "This is a note about w with no link.", ], "Useful link": [ "[google](https://www.google.com)", "[duckduckgo](https://duckduckgo.com/)", "[ONS](https://www.ons.gov.uk)", + None, ], } -penguins_notes_table = pd.DataFrame.from_dict(notes) + +try: + penguins_notes_table = pd.DataFrame.from_dict(notes) +except ValueError as e: + raise ValueError( + "Error creating notes table. Check that all lists in 'notes' are the same length." + "If a note has no link, use an empty string or None for that entry." + ) from e # Use write_workbook to win! if __name__ == "__main__": From 6340c2b7bf668d1d4522b351f91b1e10b5c7a368 Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Thu, 16 Oct 2025 09:27:31 +0100 Subject: [PATCH 38/75] set up examples page, add collapsible code segment docs add ons (#303) --- docs/info/examples.md | 1 - docs/reference/examples.md | 143 +++++++++++++++++++++++++++++++++++++ mkdocs.yml | 6 +- pyproject.toml | 3 +- 4 files changed, 149 insertions(+), 4 deletions(-) delete mode 100644 docs/info/examples.md create mode 100644 docs/reference/examples.md diff --git a/docs/info/examples.md b/docs/info/examples.md deleted file mode 100644 index b942b1af..00000000 --- a/docs/info/examples.md +++ /dev/null @@ -1 +0,0 @@ -# gptables in use diff --git a/docs/reference/examples.md b/docs/reference/examples.md new file mode 100644 index 00000000..869bca31 --- /dev/null +++ b/docs/reference/examples.md @@ -0,0 +1,143 @@ +# gptables in use + +This page provides examples of gptables in use with real data processes. + +If you'd like your work featured here, reach out to us at ASAP@ons.gov.uk! + +## Labour market overview tables +This example replicates the Labour Market overview [accessible +spreadsheet](https://analysisfunction.civilservice.gov.uk/policy-store/further-resources-for-releasing-statistics-in-spreadsheets/) example by the Analysis Function, based +on data from December 2020. + +??? "Labour market overview tables code" + ```python + + from pathlib import Path + import pandas as pd + import gptables as gpt + + # Read data and arrange + parent_dir = Path(__file__).parent + + labour_market_data = pd.read_csv(parent_dir / "survey_data.csv") + labour_market_data.dropna( + axis=0, how="all", inplace=True + ) # Remove empty rows in the data + labour_market_data.dropna( + axis=1, how="all", inplace=True + ) # Remove columns rows in the data + col_names = [ + "Time period and dataset code row", + "Number of people", + "Economically active", + "Employment level", + "Unemployment level", + "Economically inactive", + "Economically active rate", + "Employment rate", + "Unemployment rate", + "Economically inactive rate", + ] + labour_market_data.columns = col_names + + + # Define table elements + table_name = "Labour_market_overview_accessibility_example_Nov21" + title = "Number and percentage of population aged 16 and over in each labour market activity group, UK, seasonally adjusted" + subtitles = [ + "This worksheet contains one table. Some cells refer to notes which can be found on the notes worksheet." + ] + units = { + 1: "thousands", + 2: "thousands", + 3: "thousands", + 4: "thousands", + 5: "thousands", + 6: "%", + 7: "%", + 8: "%", + 9: "%", + } + table_notes = { + 2: "$$note 1$$", + 3: "$$note 2$$", + 4: "$$note 2$$", + 5: "$$note 3$$", + 7: "$$note 4$$", + 8: "$$note 4$$", + 9: "$$note 4$$", + } + scope = "Labour Market" + source = "Source: Office for National Statistics" + index = {2: 0} # Column 0 is a level 2 index + additional_formatting = [ + { + "row": { + "rows": [1], + "format": {"bold": True, "font_size": 14}, + } + } + ] + + # Define our GPTable + survey_table = gpt.GPTable(table=labour_market_data, **kwargs) + + sheets = {"sheet 1a": survey_table} + + cover = gpt.Cover( + cover_label="Cover", + title="Labour market overview data tables, UK, December 2020 (accessibility example)", + intro=[ + "This spreadsheet contains a selection of the data tables published alongside the Office for National Statistics' Labour market overview for December 2020. We have edited these data tables and the accompanying cover sheet, table of contents and notes worksheet to meet the legal accessibility regulations. It is intended to be an example of an accessible spreadsheet. The data tables and accompanying information have not been quality assured. Please see the original statistical release if you are looking for accurate data.", + "[Labour market overview, UK: December 2020](https://www.ons.gov.uk/employmentandlabourmarket/peopleinwork/employmentandemployeetypes/bulletins/uklabourmarket/december2020)", + ], + about=[ + [{"bold": True, "font_size": 14}, "Publication dates"], + "This data tables in this spreadsheet were originally published at 7:00am 15 December 2020", + "The next publication was published at 7:00am 26 January 2021.", + [{"bold": True, "font_size": 14}, "Note on weighting methodology"], + "Due to the coronavirus (COVID19) pandemic, all face to face interviewing for the Labour Force Survey was suspended and replaced with telephone interviewing. This change in mode for first interviews has changed the non-response bias of the survey, affecting interviews from March 2020 onwards. All data included in this spreadsheet have now been updated and are based on latest weighting methodology.", + "More information about the impact of COVID19 on the Labour Force Survey", + "Dataset identifier codes", + "The four-character identification codes appearing in the tables are the ONS' references for the data series.", + [{"bold": True, "font_size": 14}, "Comparing quarterly changes"], + "When comparing quarterly changes ONS recommends comparing with the previous non-overlapping three-month average time period, for example, compare Apr to Jun with Jan to Mar, not with Mar to May.", + [{"bold": True, "font_size": 14}, "Units, notes and no data"], + "Some cells in the tables refer to notes which can be found in the notes worksheet. Note markers are presented in square brackets, for example: [note 1].", + "Some cells have no data, when this is the case the words 'no data' are presented in square brackets, for example: '[no data]'. An explanation of why there is no data is given in the notes worksheet, see the column headings for which notes you should refer to.", + "Some column headings give units, when this is the case the units are presented in round brackets to differentiate them from note markers.", + [ + {"bold": True, "font_size": 14}, + "Historic publication dates for labour market statistics", + " ", + ], + "The monthly labour market statistics release was first published in April 1998. Prior to April 1998 there was no integrated monthly release and the Labour Force Survey estimates were published separately, on different dates, from other labour market statistics. From April 2018 the usual publication day for the release was changed from Wednesday to Tuesday.", + [{"bold": True, "font_size": 14}, "More labour market data"], + "Other labour market datasets are available on the ONS website.", + "Labour market statistics time series dataset on the ONS website.", + ], + contact=[ + "Tel: 01633455400", + "Email: [labour.market@ons.gov.uk](mailto:labour.market@ons.gov.uk)", + ], + ) + + # Notesheet + notes_table = pd.read_csv(parent_dir / "survey_data_notes.csv") + notes_table.dropna(axis=0, how="all", inplace=True) # Remove empty rows in the data + notes_table.dropna(axis=1, how="all", inplace=True) # Remove columns rows in the data + notes_table.columns = ["Note reference", "Note text"] + + output_path = parent_dir / "python_survey_data_gptable.xlsx" + gpt.write_workbook( + filename=output_path, + sheets=sheets, + cover=cover, + notes_table=notes_table, + contentsheet_options={"additional_elements": ["subtitles", "scope"]}, + auto_width=True, + gridlines="show_all", + cover_gridlines=True, + ) + print("Output written at: ", output_path) + ``` diff --git a/mkdocs.yml b/mkdocs.yml index 029449c3..91977fd8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,12 +27,12 @@ nav: - XlsxWriter wrappers: api/wrappers.md - Reference: - Accessibility checklist: reference/checklist.md + - Examples: reference/examples.md #- Glossary: reference/glossary.md - About GPTables: - Contributing guidance: info/contributing.md - Changelog: info/changelog.md - Contributors: info/contributors.md - # - gptables in use: info/examples.md plugins: - mkdocstrings: @@ -50,4 +50,6 @@ plugins: markdown_extensions: - toc: - toc_depth: 1-3 + toc_depth: 1-2 +- pymdownx.details +- pymdownx.superfences \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ea880950..33360ee9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ docs = [ "mkdocs-git-revision-date-localized-plugin", "mkdocs-print-site-plugin", "mkdocstrings-python", - "mkdocs-material" + "mkdocs-material", + "pymdown-extensions" ] dev = [ "coverage", From cd5f46cdb49e321cfebc320cdbc9b4960205559f Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:01:15 +0100 Subject: [PATCH 39/75] Fix conflict (#318) * Add issue 265 to closed issues * Update closed_issue_log.md * Update closed_issue_log.md * Bump pypa/gh-action-pypi-publish in /.github/workflows (#279) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.13.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update closed_issue_log.md * Update closed_issue_log.md * Update closed_issue_log.md * run pre-commits --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 91977fd8..715511f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,4 +52,4 @@ markdown_extensions: - toc: toc_depth: 1-2 - pymdownx.details -- pymdownx.superfences \ No newline at end of file +- pymdownx.superfences From 458302921d2ee504963934ae3ef42b4bef286c4b Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:24:42 +0100 Subject: [PATCH 40/75] Fix conflict (#320) * Add issue 265 to closed issues * Update closed_issue_log.md * Update closed_issue_log.md * Bump pypa/gh-action-pypi-publish in /.github/workflows (#279) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.13.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update closed_issue_log.md * Update closed_issue_log.md * Update closed_issue_log.md --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e26ec4..9f46b1ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,3 +136,4 @@ jobs: python -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.13.0 From c4682e5893164ace0d16f874570195d24d72c4a9 Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:36:16 +0100 Subject: [PATCH 41/75] Fix conflict (#321) * Add issue 265 to closed issues * Update closed_issue_log.md * Update closed_issue_log.md * Bump pypa/gh-action-pypi-publish in /.github/workflows (#279) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.13.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update closed_issue_log.md * Update closed_issue_log.md * Update closed_issue_log.md --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f46b1ba..33e26ec4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,4 +136,3 @@ jobs: python -m build - name: Publish uses: pypa/gh-action-pypi-publish@v1.13.0 - uses: pypa/gh-action-pypi-publish@v1.13.0 From 38dd0908380d61a2f528cfbe51afc1af1e53d78a Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:55:19 +0100 Subject: [PATCH 42/75] Fix conflict (#322) * Add issue 265 to closed issues * Update closed_issue_log.md * Update closed_issue_log.md * Bump pypa/gh-action-pypi-publish in /.github/workflows (#279) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.12.3 to 1.13.0. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.12.3...v1.13.0) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update closed_issue_log.md * Update closed_issue_log.md * Update closed_issue_log.md --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e26ec4..83e04fbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,4 +135,4 @@ jobs: run: | python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.12.3 \ No newline at end of file From 00f2d46ec3283a47a88e2019c787721af7e87920 Mon Sep 17 00:00:00 2001 From: Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:08:34 +0100 Subject: [PATCH 43/75] revert action version, use hooks --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e26ec4..71d32d9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,4 +135,4 @@ jobs: run: | python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.13.0 + uses: pypa/gh-action-pypi-publish@v1.12.3 From 0eb38a8afa85db30bc287fa58864b192f1e7562d Mon Sep 17 00:00:00 2001 From: Giselle Rosetta <135234996+gisellerosetta@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:14:55 +0100 Subject: [PATCH 44/75] add basic example, update commentary, add picture (#325) * add basic example, update commentary, add picture * run hooks --- docs/api/notes_sheet.md | 2 +- docs/api/table_of_contents.md | 2 +- .../getting_started/{usage.md => tutorial.md} | 190 +++++++----------- .../getting_started_before_and_after.png | Bin 0 -> 399824 bytes gptables/examples/penguins_minimal.py | 57 ------ gptables/examples/starting_out.py | 28 +++ mkdocs.yml | 2 +- 7 files changed, 107 insertions(+), 174 deletions(-) rename docs/getting_started/{usage.md => tutorial.md} (69%) create mode 100644 docs/static/getting_started_before_and_after.png delete mode 100644 gptables/examples/penguins_minimal.py create mode 100644 gptables/examples/starting_out.py diff --git a/docs/api/notes_sheet.md b/docs/api/notes_sheet.md index 7424a8be..a555c83c 100644 --- a/docs/api/notes_sheet.md +++ b/docs/api/notes_sheet.md @@ -17,4 +17,4 @@ here. To customise the worksheet label, pass the new label into the If a `notes_table` is not provided, the notes sheet will not be generated. -See this in practice in the [Tutorial](../getting_started/usage.md#penguins-notes-example). +See this in practice in the [Tutorial](../getting_started/tutorial.md#penguins-notes-example). diff --git a/docs/api/table_of_contents.md b/docs/api/table_of_contents.md index b2cc7ef0..f1f596b7 100644 --- a/docs/api/table_of_contents.md +++ b/docs/api/table_of_contents.md @@ -14,4 +14,4 @@ To customise the worksheet label, pass the new label into the `contentsheet_label` parameter. This table of contents functionality can be disabled by setting this parameter to `False`. -See this in practice in the [Tutorial](../getting_started/usage.md#cover-page-example). +See this in practice in the [Tutorial](../getting_started/tutorial.md#cover-page-example). diff --git a/docs/getting_started/usage.md b/docs/getting_started/tutorial.md similarity index 69% rename from docs/getting_started/usage.md rename to docs/getting_started/tutorial.md index da5051d1..058a4095 100644 --- a/docs/getting_started/usage.md +++ b/docs/getting_started/tutorial.md @@ -1,4 +1,4 @@ -# Getting started with gptables +# Getting started with `gptables` ## Installation To install gptables, simply use: @@ -9,154 +9,116 @@ pip install gptables ## Tutorial -This section demonstrates usage of the gptables API functions and core Classes. +gptables helps produce consistently structured and formatted tables. -For source code and data used in examples, please see the -[examples](https://github.com/ONSdigital/gptables/tree/main/gptables/examples) directory of the package. +This section demonstrates basic use of the gptables with the Penguins dataset. More indepth information can be found in the how-tos and API docs. -### Penguins - Minimal Example +The tutorial code can be run from the +[examples](https://github.com/ONSdigital/gptables/tree/main/gptables/examples) folder. -This example demonstrates use of the `gptables.write_workbook` function. -This API function is designed for production of consistently structured and formatted tables. +### Starting out -Summary statistics from the penguins dataset are used to build a `gptables.GPTable` -object. Elements of metadata are provided to the corresponding parameters of the class. -Where you wish to provide no metadata in required parameters, use `None`. +This example looks at how to produce a basic gptables spreadsheet of data from the Palmer Penguins dataset. +The data is first read in for presentation. Next, information about the data is supplied to +gptables. Then this information is used by gptables to produce the spreadsheet object. Finally, we +write out the spreadsheet. + +First, import `gptables` alongside any other necessary packages so that the data can be read in. Any +additional preparation like cleaning can be done here, and the output should be a `pandas.DataFrame`. -Table formatting can be defined as a `gptable.Theme`, which is passed to the API functions -using the `theme` parameter. Or you can rely on our default - gptheme. ```python from pathlib import Path - import pandas as pd - import gptables as gpt -# Read data -parent_dir = Path(__file__).parents[1] - -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") +penguins_data = pd.read_csv("penguins.csv") +``` -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet +Then construct the `GPTable`, defining some main elements. These will be displayed in the resulting +spreadsheet. -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "The Penguins Dataset" -penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] -penguins_scope = "Penguins" -penguins_source = "Palmer Station, Antarctica" - -# Define our GPTable +```python penguins_table = gpt.GPTable( - table=penguins_data, - table_name=penguins_table_name, - title=penguins_title, - subtitles=penguins_subtitles, - scope=penguins_scope, - source=penguins_source, + table = penguins_data, + table_name = "penguins_statistics", + title = "The Palmer Penguins Dataset", + subtitles = ["This is the first subtitle", + "This is another subtitle"], + scope = "Penguins", + source = "Palmer Station, Antarctica", ) - -# Every table must be associated to a sheet name for writing -penguins_sheets = {"Penguins": penguins_table} - -# Use write_workbook to win! -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_gptable.xlsx" - gpt.write_workbook( - filename=output_path, - sheets=penguins_sheets, - contentsheet_options={"additional_elements": ["subtitles", "scope"]}, - ) - print("Output written at: ", output_path) ``` -### Penguins - Alternative Minimal Example - -This example demonstrates another way to use the `gptables.write_workbook` function. -This code is equivalent to that in the above example. +If preferred, this can alternatively be done using a dictionary of keyword arguments: ```python -parent_dir = Path(__file__).parents[1] - -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") - -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet - -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "The Penguins Dataset" -penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] -penguins_scope = "Penguins" -penguins_source = "Palmer Station, Antarctica" - -# Use kwargs to pass these to the appropriate parameters kwargs = { - "table_name": penguins_table_name, - "title": penguins_title, - "subtitles": penguins_subtitles, - "scope": penguins_scope, - "source": penguins_source, + "table_name": "penguins_statistics", + "title": "The Palmer Penguins Dataset", + "subtitles": ["This is the first subtitle", + "This is another subtitle"], + "scope": "Penguins", + "source": "Palmer Station, Antarctica", } -penguins_table = gpt.GPTable(table=penguins_data, **kwargs) +penguins_table = gpt.GPTable(table = penguins_data, **kwargs) +``` -# Every table must be associated to a sheet name for writing -penguins_sheets = {"Penguins": penguins_table} +Each table should be associated to a sheet name for writing. Collate the sheets with their names in +a dictionary: -# Use write_workbook to win! -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_gptable.xlsx" - gpt.write_workbook( - filename=output_path, - sheets=penguins_sheets, - contentsheet_options={"additional_elements": ["subtitles", "scope"]}, - ) - print("Output written at: ", output_path) +```python +penguins_sheets = {"Penguins": penguins_table} ``` +Finally, use `gptables.write_workbook()` to create and write out the workbook with the output path, +the sheets, and any additional elements. + ```python -# Read data -parent_dir = Path(__file__).parents[1] +gpt.write_workbook( + filename="python_penguins_gptable.xlsx", + sheets=penguins_sheets, + contentsheet_options={"additional_elements": ["subtitles", "scope"]}, +) +``` -penguins_data = pd.read_csv(parent_dir / "test/data/penguins.csv") +The result with a subset of the data is shown below, where `gptables` has created a Table +of contents and a sheet with the Penguins dataset. The sheet shows the specified title and +subtitles presented in a minimal style in text with a legible font and size. -# Any data processing could go here as long as you end with a Pandas dataframe that you want to write in a spreadsheet + -# Define table elements -penguins_table_name = "penguins_statistics" -penguins_title = "The Penguins Dataset" -penguins_subtitles = ["This is the first subtitle", "Just another subtitle"] -penguins_scope = "Penguins" -penguins_source = "Palmer Station, Antarctica" +The code is combined below in an extendable tab. -kwargs = { - "table_name": penguins_table_name, - "title": penguins_title, - "subtitles": penguins_subtitles, - "scope": penguins_scope, - "source": penguins_source, -} +??? "Starting out" + ```python + from pathlib import Path + import pandas as pd + import gptables as gpt -# Define our GPTable -penguins_table = gpt.GPTable( - table=penguins_data, table_name="penguins_statistics", **kwargs -) + penguins_data = pd.read_csv("penguins.csv") -penguins_table_copy = deepcopy(penguins_table) -penguins_table_copy.set_title("A copy of the first sheet") -penguins_table_copy.set_table_name( - "penguins_statistics_copy" -) # All tables in a single workbook must have a unique name + penguins_table = gpt.GPTable( + table = penguins_data, + table_name = "penguins_statistics", + title = "The Palmer Penguins Dataset", + subtitles = ["This is the first subtitle", + "This is another subtitle"], + scope = "Penguins", + source = "Palmer Station, Antarctica", + ) -penguins_sheets = {"Penguins": penguins_table, "Copy of Penguins": penguins_table_copy} + penguins_table = gpt.GPTable(table = penguins_data, **kwargs) -# Use write_workbook to win! -if __name__ == "__main__": - output_path = parent_dir / "python_penguins_cover_gptable.xlsx" - gpt.write_workbook(filename=output_path, sheets=penguins_sheets) - print("Output written at: ", output_path) -``` + penguins_sheets = {"Penguins": penguins_table} + + gpt.write_workbook( + filename="python_penguins_gptable.xlsx", + sheets=penguins_sheets, + contentsheet_options={"additional_elements": ["subtitles", "scope"]}, + ) + ``` ### Penguins - Notes Example diff --git a/docs/static/getting_started_before_and_after.png b/docs/static/getting_started_before_and_after.png new file mode 100644 index 0000000000000000000000000000000000000000..86aaf647b075b5da47ccfc288cfa873d1c068b52 GIT binary patch literal 399824 zcmZs?V{|RS7BzYh+qP}n&d!N#+t!I~pJ2zfofF&kNwQ X}w=y-j`HR8o!dwzKbJS(#&~>eUtvph9ZS{+-3fHA?Vx? z@C$q)_|sUw%j9vZLwPET+j* 0!STc7|C33FtrnT+|5cDp?M{AxEh-qU7Ft f{z~%wDcQCu zkuX X>SGU;l&Cu&(3qM^ONlfv}#UjTM_8Yw8X!0^wc+5Y7q z0yQEvrnam70;=b#O93*6Cmy1 T+5fMiD`n%AZclvj5YM~I?N9q8qOu_Z0)p1IHbo5$DJdx^1Ox=o;i2}n zg<-gsX^9isv14E1eC{kK!g>pYiSc~1tu_rUZCQD_Qg;j5*_kR=N9*oUF3Po*ePlh$ zNfVnzW4b~*%eB4eNkl^f>(0&&=-?oK{#Z+e0EGoUQb$ME5rv1QBnu18^N2~i(AmJZ z4xDIy_x!v{M~PsL+LGBbycIud)4^3K>cu`rc>y rQ2=>wd70_QX*W{K9E8y_#{MofPg`u{`eB*jQ;A&4wk=KWFY8o0PjvGtMcRqG9Zgl^%V0q-`$=Py2vj*hs zQb`EoJ(Xry;xhS%PQyqI zx*2Eu)CQ9(udQ&PsOf&OTCig8`TXEbNJx;`%T7p`g^5WPXA^h;bMK0Tyd;o1?!JF( zvlMKS@c#4GnHoIdzg(@09tPlHp(jVwWMKH7u;H;B1a}HD?xH2a3!BSAbF `C#GyEf`ef|x1c|ktBs{3u5UKRDRyx=aI^}4vM$aW0!Q6)?h z^MF|kGDgN}ugC2$aU?v*IHBs+bL8qv)W8!kXA5SP2K#v>40{18)lzXHSJAckscc^5 zU%!OJ#Rs=9N_j9xp4Q =E~E0>^EbXm{FAl?x3yUi{O1o)BmOEarS!u6vVIMh)7*$mqt+hT zZi}-+vF`Sk3u>&}boWn-y*(0CYu~*6S( Fa^vC|Y{huvp)= z^PJ2GIETcVCaOZZKh+o^6Qwgz)oEKJ{)DYTP?W4;2owqN330|Cq>J`T0f@$XN%E|n zR5a;|5s?7=YJpxb&ob&U6sP!h;;j5=Ep}hKcHrlhD|mn%g5;^QY@Vx&s16OgKrgb# z+G2bvo}`b+4`_LHC73pH_P%TToiUObub1z!1b)F!ZAuxLBp@VzIRa>EkouBlTEBj0 zylel~B(QgRYv}z^M>4o1<1AVwQCW$qs;Ojo-`!!EetBkUxN7!8hv-TB#f0)E&qUlV z$sn`Yy8rn<6UUQ3thAp=To#!i=avVlfPlalf5ehaqbYB}LwK&cq s}mW#>d&w9jQ&db2*xY%)vdg%O{H>$dTgTwMWdm3*JacrCVRl! z3|g1HG*9Pe%O$$hO4+{^r%Vv8&`4}$rt=Ft%>~SHPA~d9d7E$D1_;iFQ_3e%oqo^v z?)fOrrn||HS}0YD>aik61;uo6*%g>Do)fArVhO` ZM!J`CPE(A5#1L4`c1OgQnFklE!>}MEm2vGg|L-ysph5vytElSZP bHI^kMj4JWpVyyJL&5}hZmbI8u%`!2WYsdxF->aJUy|1*!dz&=ypsbe@ewBFb zHlLGK6p9e@V06WHf__%&HU}QviMU|ob n_RM8_MKPzId(6S&OBe!VF>T0WEOcI0r`*$Ki zsVF(oc5SmoAPaCrm*9?3ZH$9wH^qzI94l|am8LC2IZu@8xG$-S@K^G4dN2Gk0ONlQ zpw_#6yl6`zsIKQp&bjS#>qshyG8bXhB%wWEdwC`a%*n2!6_k@xf`Md>x<)t^DPgLK znu W4__e!M(>T7`#7{=P#RB5!F!>k1DPh9Zf?}9Typ}J`4q>2`;20 zXs-~B#JL=awXIata;hZNy5p`tn&z|0yf=NE=}Rc~9bViaYZH^83T6)Su_NoV7RbLA*<8+DKKzQ4}L* z<#-ER+}wL0ItEH~r`^Dy4nzcosj4};mRRM(-&!NRQ~8qq`BO5!=xGa=scLac<+SFZ zbhyN@&m|#I(qjetuA)L1o#L%!G>}C(Bb)Vq7}OONK!8Z6T?N?!cIaQEXO0doRurh} zS$OygYgGB 7?xGi0BY)7G+Y-!X|C{`=*Q8RC;eT z_Iw7rJNjDghmK$&Y;``Iwi1aF0{!Kc?CR<43HZIU!=OAD>7*eCtkET*l*4bqDS92b zsv_KyAd&Cq!Sk1;eTB=pN%~dOT0g2N5V32Xl7NjdZGv*B_a+pq<{Wz4?{B=<;7vCv z7~-c3DaP^7i|St>{&6*?Su|l7v2w%j_ZkKswQVeHQT2!lR?Ompg^Gt)Yn~dr8d{X1 zsGil2*^Yu0{7kWwtqdZ61z{}4XcyTxR+ePce=1AUU*PrPzZenv(Ci!eYyvI;Th;UR z{XIYZbAkmrr9`8k6{bDU!%=XQa8y^AU;;kR495VWt}T9Sp6)RQftYL!n3a=}R?SYc z!Hi{TMTK(p%b3g!B6*U&b*~^1QW{2TnArHZC{GJKS{jCt%XNupAtQpDnVd-2Bn7D( z!WLZXt{Xx7tJazak1=!h>(SA+ur*Jc(}L{MzA $o?}YPT^g_jF*s8<~y8)Jm%d zh@qTpA=8@gO`&fJDqyv{u!D(kb$%r7G}d2x;hq1bO>Yq}m)e4*HJOJRO!?{XBTSa= zuI+>(^x@=pNe6AUfFE4z3r;oe61J=-1Nl7;9sP>-`WiyDPQyRYSztv}s~A6`-F)_` zeOIi*uvQ`p>r7O;lj2brIt`?xPKwuVY{z}#{m`K1X1 S>Onsqt7A;p0e+20=7U; z=jwPqlLwVpPEPB`ys_o&XJc_k?nreIY(h6KBqpTFWZ<30ccsF^s}M+2a#uwE1-u{O z>{2TbHGfo_&fMJ8ikQp;&P5Jo?RG{>B-t5G2-qV_2->l?L-7y>2=ny-Ne$gwN*3@U zRQolw( GHu++PIa`UnJq)`&qZCq+yHR@YI@NM C(!L*|Do&=xiehpAPd ztD|c=W p$!Id!>*vENXXm%yx$iWtI;2N+g3|Z(o(Rl zys*Rf)Sw!dGjO-z3qg01b)o+g3UQ;nr|~e3mwFyvFtl)U%BLlYF{d_iThD^}fRvpq zt(sj)i?d(&HdGM+QT5%J7B6@WCmS549 JB5U$6rn?i3^(zn7A$v^vV}Q(Kn8-5=b7QgIOmApt zI50G1#57XnqRj;UK%+t_L3tWCS~yZvG%hi;++eP7=f(e3o7K)!=F*8~PtLctqE5|f zc 1Cm{ncg7Cq|{W2uVTiNhz(gy{eLga!}cmC;5&7BKT^=r$`YT^+wlS*EtP|P z3aj|t0Es% 9RB_N9 ~d=ZFRj7@D(bu$3!ybi~aqgmfxl!oW?XA9hu)>?v-)u zj~ALONXh4XDTb@++NJB4p5C`5lJLUdaf{j3+S=OaX%V3- 0Z5{xVmMj_DkfV+LA21noSdA-(?%^%WKgwO z_>}~U;8U_GzZe=cG_*#(? t9V-8-}~+;4{P1`^&$JF1rxQ Q_bdH(i5x&oojYB?Kq1BcsqppggJ2@$qq$#BT<&6<%(G<*BIM6P9QR z&*UKZ!ym^s<@k$WZO=xdWxa)*Yyw=k;q8`<&VA88q3h(qETV~oP;I|J=562?w=$D& zK_V0 N1Fw@k<9fD&f`T8f z+n;kqvT0G>v#IgCv(l96)fMe4#P_08+i9^qWTKe7AQwhzRO&B+NBEd5KK`b4WmXK# z6f&ig!EB`6kPWBIqEz_&jEa&)8@S@Y5Z^ph$+>%e;s|(9Z#HB#^Bdp#3CW#|CfP(J zL<;oJW~B5Xf|_CbdcU2l|NKcCsbZ0HDZ#(5iAYP2T5<|oh!jMQ&1Mf>`h%}3qv7FA zlqwb+Emkax3;rP6r$khCadT6Xni)h`O`>0p991O3B^DzNN(7VkTsespcWf+dpp^_{ zaFf!ek%TBZuSM>YSpRCu61w)`=<|-xdCfxqsJ^_RyWEd31eqTyz4L+JeA#z=|MNDT z2DtrAd8!7;vL9e%gyfJhC}v9c#+wF~cu0 z9)`0XjK4fr<`wR}%{%#7_&x)}C9ty`G=cBR@rED%y_$#HG!_0otH>G2FH0g`<)%N> z9hLmhu{8vNtg&g^t0OmZmm}t1-KM`&)2EoDEhx*@<$qVdB}m8?;wY1K(ftKqAytgQ zD}lDPJNjWd!Gh*fcwPvt!C94ZI47scwjK)NT6AWt2n+#t*et `kKK*XTK|4>E~Aa3d>9`+qa*L^|8;_L-r0_PV|J^Le3NT zqcS+`$%_ySU1F{*C&JnS8q;0h8d(BzIc%>Tlay&cJVz|~>szYIh9`~Y64Frm?VbG% zAKvseJKR_^z~Mb0!1nfz^KGU#luBv&Cgy;C 9nmJ+l}M->iQ2uo_I=W}8c zL_T6;ZfY3A4@-c4lnUr_B}mhI2pB2KOeTzlc<|OZ*GyIw(i?3FK>w`qH}al3S8cX^ zEy=f_AP0B1C-pJ0KATcf!#$ +aujkI1}M+7PwA z1Wc0q?RaO5Mj$EFjT^u1CKIkJneB6S5ia81b~pbP!{@?Le#Xs$t`Dz0J`N)>qd#YY zz-fU$_FU@NZ0|vHe`O8jo)B8kL=LHo2{bO}IayC%u)PCRPPItb7l>_4F-cK}!sgin zUG=ak&6)6mEYchC+fPI|7__8RniEFch`D&Pe1Y|Yg_Pc(UVd85gpctYN6MTx E6*mM*!UkHiInwp(pCm7swPN$y2m)(1r;UKPyTuiKaaQ^u1r3K!VMB( S3i@GpHznGn0F4>z1!46XCQ6V=KsbWuRcPvYM19<6xx z^8izSdNb0d!BrdWwq(tQ!<*qqc|56_zl)6NM1+{v-msEJYPF{QX64aVGEuNImh5m? zEYbYD%}hoElW Y`s{y}d~6M^+i5WFnz=6J}Sa{y4w8u4D_6`;NyO zZkOO(?&U!RV{t8U9#a46d9yDXhA2OzyS>42@lO>FtSWbf?+L&_knKdaK+>hZp0rkx z>mzQ19#X<4onBkaTe-je?7d?lfU7M8a*1Up$UURZf*W!dH9Ts2JP;_j7jr$olBBqr zUovS; s z;sJIY8Mh&OReAexQK5&lsZg&_VYrtb_UY+VUz&Dy%<4rDM)(egCSt2L^4?pibUw54 zaT>+!C%L7!MyCy`X3_nEdP;o4ANiOEmmMi1&B=s2x-n}u%0=O%yx4xl( J?rQ{NH`lx5Hu|T-e#F#qp?ybpvNW0~0&!UZxYQ&aNHWFUMBlwdy z#dfV8{uAy$t~xA2eXq-H6zF9AVmDSh4E5xI4zuEYGISxjxjis3pCnRtymT2GZFt3I zPT7krJD5Lgbeb_wtw6G-tRTM=CY#UYSEuO$`!7Yckfj+9u7W|6!s7Zs5@a>kYyg9T z5<`@D4c$bg)eQBe^hi!s`-zOz0RMC1nu>zc9Zs_tA;u0NFpnbAiS4aVBr;OAv~pex zpAmMg-HN6HB)tuvoPwrIGFODD*E1n0t_eLzZpy}HFC@N~Ur~{t*S+N>x-o~=U^z&F z(wNZswAxas(G )=H%$BMu&3(Vdij4FO+hl?FSnp$sRzw0HW06>uEmc94cH>lJgPT2ET!9Q9GWynR zf?a;ma=V`uQi*b-QU3&)^QnM1=T>d>ng^2ro(&}mQm-h;YgS5@M^;_DC+QjPOE9$B zXHB?BoRe{(RH& zy`m@c W zn7l2hs6lzA&l5R`Y~(c+(UbQg%B`s>Ua>;OaS>5ldaXjp5VK~H7a6XoD4(@D6~|}D z6RXo7z9DjzRMsv473eIS4`IWL=%1-J5_;h>cHPtYie{CTBlVzq+D{aaX}O9JxO#{q z?5w)$@0JVfj4WoEB}rS-!bC-um9f$kO8rs7E^ZV@M{mMP=CM=+sq5+4+AbF&;GlVv zjTiiC#BIvgBUlui< jLzjp5O-oqz=4YLKRN`l2kfC4y#SE8d4U@u5`rC^G`EvU-$$vlBTp6^ zIDn03Uvu`2! pL2Int)| R_y&?&DZH)jp~umx!>XnctDCK2sm_VyMSk z{RiLK$ZAW%rMdl3m9lx@2GQwX1T)`Pe%MbPc_lk`Ra424md5?H-T)gtuCR0?&USYM z(W%f1dY*~P9=d-}-#t{$NGWtB#Y%CHJ#k*(P9%S4hGY#;aTFc3U4)K9yK*{Gd9gNh zXHNEQr`7r0x+%fnK*7QGuR|N9z7HhET%+Nyj(2k3uGVvcKmLZIL9jI&njqAuV~*4s zMp|ou?UVK}7N=&W%eck&Ou^xCKaMOYz&VLrX+J@9+JIoW**b+ryy_bqOuA~`JYH?X zH$* ^C!#$r(a5oJ5>Iq>wB}_bFuTbzbg9bCunjTcjOeZjjn%!Rf-_tgReV6Zg zm;2G( nKpK9Z zjy&6+?a7ntyQTGyM09LY85ovJYIXMSPQ_E{gY`|?R5Ltt&AMO=ZaBM~Z@q-Ltd{Fv z44fN0nXTp&; H}Mm#!+`iH86!?G`Mcx=^t4iGv-6A??_=9cf` zMjqNL{) ) @xyGf8(x$fLud zQ&)8%)uirbxy|P*DrOTWUkp%|-pF(E(sTSNx( XRvdrJ zBo)+>8|M41C@QB<0dI@1->3bNGIf6Ek;I70y63{2ioAZKq;Xc#;4p{Sey 8L)uC z4VJgSjSNf5s<)B1f&KSH?cjEFI=0f~hq|A$*?Mtvadi90s*tQHaw3pQuSs|EqDK?6 z)rk6_k x?lc&1!?!PtzR! z){pY7R_zjW&OW-aU7}M?JhGd}3hb3$1 S9j)L3!0pm<~qJXmfKaV?9$q+ zIR!=|ML(gCysCm7Hqf}J FG3qSAc@a;kq Vl4PNOt#=jHr1vchLox#aIN-Z+f$WZjC4YMoWwxSSk*?H%j~C9TEYoRPWNL==~5 z1&MPBr979}tkdCLmurpH8)dM2{;0@jRfPVT)8r42+d^T;q#1mB6wb^U;ObwEJQa-* zwJJ2bW3j_Gn1@;jh&AGVZQg%?yy&Z7iZs KuY2lPysM0YF)oa-!mPl7s1w Ix%$|w#iD_%GpO+_co-Yytgp7SMRDCw*ZpmG__xl-iA8vHpjF^|) z?RzF=2Itet={KilV*uvCU)5DC>m4jd2as)bQL*Szobk>Z$?UudOGZYVw&c_mV%$ML zc76%XOCfs%yP0WRP7Tn%gfez#^;MF~u4MlS0^kprF6)TbaBN{&MXvU^a 1@j3X}ppGeHwm)l;~9h*`~9YVm&kG9qv%TguH|Gd;W}6F4iqX3<+3YqEPr c8lvn;JN$Ktr zW5|6b7D0z_d3xndu8+1T_ZW@u^azqyai6IoVA0%aH2gC+lLJoOVY8+Syju&r+u%UT zykl=DaH+&fD78%QH%xa^@TiBSweT)lYCqY`E!zMX`hMw;oDd3_-#pS%vzE`MobR93 z^cY cbRF4JYTcI1-|8ts|4=lc z60{BG6MK!ibmuSwVS@O#w)6tAY)nr;3eWs6;ZP-kPxpaHa@o_>Z2T1RFoOQC%z{<2 zLfYyhso?so`@^P@XVHrpmAZ8S`84O2%Y;p6I-R0ed=^N9nST3G5x|?xw`Ut^xxBI6 z{@*)ktc}V@v4FnsZjL`vP^+sM0$Z1n?_b@0n2i(SL-4l*F8Tm=W $n)RG ub1(jm zUMJlBfz2mS+L-c(xAx2N%C^NM_K^B=8GYsNl49@Z&sppdgnrwhQ@tsnZ(Wr-bxBrL ze7_b&LWnRj!Dkt(D=3v$u2XmuXOHBu%AHo*AKX!BPnj0eVEURe?%rT%SiooBjDCzi znCu=M$TizU<+B8DfX^b?PA3dlG~37)j+tP*Smge@`p#juL3vghDyoga@_cJQT0TPs zDaxxtan+D~{elMiHp`tPqlg?AOF&)QKS&uE5Y%oUBPb?pd0uu}E-vZlM{qwB3r9FZ zU_NZjP!>dWDr-mBFB`Wh68u=!Y|vt0&)q+h2+#2b{@>_tZ^n5=h!wYQZlefSrJKL9 zH2slPRJiO^AMp`Nj>ntkme1o)fscQh3=wdDhkN*!b^K9vc^{1i{DC{E-wnCS=}PhN zDyKsEEL20Z&coMqrhBukS9OmjM_L4p<_6-Pul9$JPOK&DId+du`fD?*|27E31Z4!0 z9i2$Yhxa4RB;{n2h$&430=SWu4r%Og&?cr#Wx3+DgTzf!LPsjX^vT=>K3IS-<}dFp zTf2*gIc8GYk|^`HWs6woC
F-iZXBncrAkVNkbxgD~|xbdjpy(H3Bx z3~Guee`k7}2RZsYTtjwVuT?~MU&^?shL90bBeNji)L~s)2}Ud0M;p(zF1IZ-1WcfK zf84%E%01orUYwyz9mH})%02XzHRmE6k=;Ce2gZCROB43Uxrfm;+l7mgjua#}Wp^cr z2s15 @Ze}b~tcoxf{hXpG5Nn78`(y(0*kzhF9hoTt;_TQKF51!KmjS?J)$fTPZP@mB~ zp^lvB8GBeG6o&j6OnhnYA)AqXiK0OCnX8JqW&_64tOpqhe6M{})u$ID5A3+3RFme9 z5IY;sI%8o;0{genmrk!lga{>Oxy fx6V8k`l~9KnC6|Le z6cI7JdsGZ#|Neehk$7+Sjm#E!dcRS2eoUOZ+_<+KfPev6#x*rG27C6f>Gzv0a<4b6 zLs2Z+sh1 @kAaT^5ueK%T#y}&R54$@gXk!j(r6_T~L7j*UX3yk7kcq z=!MW~_LO7Oq^*Q;JulyZelN_V_PBE5Rv7zD@Fi~LALvrPb)q$8y9QY&pJdj{zkf z=P`&+paN!{P0k;~6e&t5rjkN&6&}}Bap`ZP8e3lP=n{fXqzLUOC=F4yP}nP)Sx0Oj zz}5$dRp1@O9{R)+@5hyv7|oSh1t=V!RJdI&K}~0Rz`sDQch0}h_DyL(OxtFSR0p=B z8*fOM`SWMg*>?@4A8sGK?kgr+pF^MeL38RcCv))E{JS>KF%ZH<6lvET8o%_LINU3w z+X4-S*@@Ve6AP4t_ci*`t$T#~xySC@N{gs(`DSZFH1%q#Ktp=7a$k37dgo@1UwZ$+ z4CcqW?{gM`6y>V*qo)pGH`hs9ys7Q)?FH_*3yiN^qXygL6ldpUWo?Kg#L3gGtB$ZM zboOEbefwGww?Xv}b{3XdQ|9I+C{;l}bBMFCHws{j%0^X=*^*80DFhY=X7YYQ-MeBu z^g)}u3{+|N=KQeF@igZH`m*G>OA3lmG8W|f*N^{lGbvr9xh`YYoinQt$8&O-FOb)l z=zbAUrieMr=F~rIGQT9>5FPGrBpoXS`h41p4iq#NKnP!vLpN**5J2FZoPZJE^-IRL z?yY$#Ug~c6Lcv^#Z7xXU1p5`roA9u| zrWl&io#s+NAP^}` mhOxYl$k{8^m`7TjFo9`;Sg<=%qqnw4v&>``@ttB_at<3sGB?GN$h z2DLz T?r7gP%b{a5D&%*2T;I_A!jB`2O0OT z`TND0R&!u7%P|)a;2+z=xP0vqe80POxb?5$(v0=ALc!zZWeW>7Kb`rRmV+!gvYr__ ztS>B2U2ixZu=4~NJolJLd-P9J#!Zc;O!8&~AozEqW@~p;l5ju5T<_qJ{dGikG_REl z7;OZfqsv(2KkV?M1I`q*QsenKYklRY81*Dh0yfHlD vim%nava0Sjkn|DypT2feBuXo-5|a9 z_hU1GATDqn!I8IOHOigDwVb(Iz1a!7coHQEeWMSzKcdfH>qitODKDjXz+=Dbcx}Qr zBEpk#RkD&+_XIL2a8?8@YQVS+@xVmFGP~i8h0A&)As;FH3hUKzJtumL)-+K+4%u4U zBixD+-dsaL!IN2!hwA(wUUztiw`~4*bRs`3;H!vfx6`#eeAT-EZi3@;efub4yKTZr zC-RsTakdp@fJk3wt#*5g$eO|7P48(6Kh{XdodU&ov`|3UwGNJgGMmFeAkZhwuBewK zOrfXaHOaIV29CT^jN4z06$?zWuHb#GKg;r2bL=NI#^St%G?UMspncSdt*A9^=*b$m z>~LbLP~qZ{$v^$wkVH8(bs>Vm5A9NAzTK`$_v8n$4W)^eJW>{FHa1>Q?E-&LZ_SXG zJ(ChYLZIXR%=E^eCB8YMsFEluF+_|pjW<3rOt@c1DamSkjZvQ%`-{q^FC=KlmDTDH z3gY2d;x(K|B#0O3;vg_cSk_Sjg`6O}^=xb$3e$^Vs~b~YpBGJE@_v>l? j2i?)#2vH;l&yuw#QiU>3K%o8VQc>65eoug57Cou?`FFL zjV)TB<~Tn`D;T>*Bn2yrH0E2eMQ)SX{F*U8HQ}w3Na+biW+8Z+aKehKbk9wUFI`+3 zM>ZLg{rE*k0DMA9a%f>=S;y-woLiETo;b$0mu%7N5_d#)izLm%m_`@~wJ@Zfj+yCq z7r33blYGtNinPKsKScP&XJUF9!drB*d>xXs&&KYkcA$Thc&Lixlkn3V`#=r% pQCle5Gxw~yQAa3Z7~E$AHc^yTWwRbv(_GX0SfHz zw`>`&%}tstr$b*Ng=S_t_?afxM>M(jFOS1YCU{Dh%LQhQ>PgNDz=ns)7mAvkT5$7@ zgwD`F@+Rn|1o3-yNvyWXx6y{j3zrO%jagrrE<27_wK$E_w>Uh$$1$7T{k-EcTwgwj zw{&`dI(X^0_*-RyDZ4zNRFM{Iu_r1nqlPWbtsyHaB)Qd_Jpt^ 2n zJHhL7y^7mNt6+ZWCag4==|rbArL7K?3)Qd3wqZq?RF7XF!I;He;|AAr;FWNpo$Rvx zcqEnM`Nl!QztD%w1pQqqpWeS>XndYfACr=}VW?4hM4k-?ECKeM?KkegrG3h>t@{0EaAPsW|% zVSST{p#IU#E8GhAB5Gwu`ab3a*i2C#gnvWGq?MLkNyCzH2ZEgU{otq+#H5rZ#kQO0 z4oLONKq(!u`l1UB5UZ>5@tFJmQ9P9B&RP9LAplk$ u^QpJpKg&rv#G `iJT aNR zsz!+ooyi?ye+l&&s^MX@z5|in0bHVUIrwbd7G}Kii1XgkZGWS_6pvET2Co2I8?#i> z)R$ngt)PkiOIG_fUTaWFOZh4Vejc_p9R~E7YQzTK{4MG&v|ASq7-~1poF&6UTK!30 z!n6`mqq+cPu5 zJ0orZOoVP(iA|&?@jW7nF=XFT2*DGWv~?n&;{I-L#^tpL4CNZ8L4RuVr037hBsW+; z({*Rn#Ro}_&Empg#q4szyIQ#RJ)_%3R uJ2QVMbA-pf;5J%f~SPnSTZAonGvnrZRn+lg9A1jv#0?Z4E>S4T; z%UlNy-C4^ZG=N?e(yve5ZfL=YlWvdzI=;H-7+pjsk<-Mc>helHc+dOrM@@@(k(=T& zdAK{aJhRkX;X;!7pDBdh6(XJd%=d~rV$y`TyMQz-ac3}-u%s8p6ih1LimGX8o7h+9%Hr( zVtjrXuhTiv?(epJE#+FK_m~Dtz&mcus9c}sZhdlI^(Qz8WiU&ne@gi&Y1PXaT^-=( z@#=b5t!PhU>3cYt=6Mx+dNf}_|2<&mi6c2F9Z8`HKnc 3GJkBbKdJQ&}Lj;U4ZJN+qlp2u4*yIA5$5Bo)TTNb@L#Oxz`Tb+95Wm-#|H zy|osTmED(`yhO#FBsTdAI2Fn6?Ra$ayHs%R`CoQgo2iDvpKG9mR9B0iN}*k|dhvcU z>tdb1K8)$Th`BW_7aCA3=E;Y|-$pUK5$6 M8tT>P)WVv_ z8smFUq7Ffd;Z1s9gk7nD8)A%T=sygtee*!O;pj=&siR6F<=xpC__dCN_``sv3CGac zI+8mpx%5krYknveug{fd#n`U*{H;bh^Xj96?-LZH<7z4>!Uu5Fn*#r4?Q9Sd9})ED zFT& *O1>61>&YpS*i8O5cSYur7?*r$vZ;dP^KKjpdZbH -Z0i%d7 zKplV(?20Ya6Kl1^_>%o@37+qZWO#IsW @C#ii2e;nYS$s}g=Jk&( zg@qlFAM0YfW`LbxOV+28Ol-Q1M#3(c2c?dA-y?PxHT>Pv2;kLfn*Qy6vi&NCerUFZ zMl#zP_gwX|kk_l_YBy)nRd?FlRzqEy%yAb6NX&9itk_4S!}(QLeW!pP6`@iuW9{GB zZg3l4X0R@ zQ`U;( pswC!_bA55uaRmK=)>_JfE%4`mKMf~fm~ zrrbl#{aAB7sQipQ!CM02KCjnc6!ICP(e@NKQ`YvZuDxzF=RexSxU5YW_L89WnfB)+ z B zyzN`BbWnyS((saWxcxnC&HnR2P)LWKq17Xs?Ff53q>H!_CEJUDLANf!ZK&h;iUe0i z3%-hK%Co24opnWE-}wwpYjksU#1x(h30802Ycizp5lS5>rjXMRaHAz!upv%X&mfh* z<@)!RS18(5)y({K_sbu~p}76^eaFVG9V@S=_VC7$sR-j^OFVF%)OLSdB?1|r{Mgge z$ko<=uzDWH^u<$}69=Wfcmg`oLSYC`S|yLvu0s3KB}tKy$sTSt+S7fP-G*-yf}^7~ z=wvccc {wFo^7FXY=g z+`mxVu!Pz>?=AmJ(PFvGsfilO+gb0VRdd2Ad3X3>_5JXuBwoCTz3C!Z>6*E{{R5%F zUX6VBD#MR6`*f~b&KoCM<|Qkrla)AYFYnsxG{y|7lfy%xywO+ wx2d%oz^m4yhht&|=#!6ww^T=h>q(8ZcGph{5m#ce>|4vA o~17<$c-BxVBg(2fRVQ>|ssazeb4m z_$T1i?ARhR41xQ>z0%y<{H3dHgm;_%5FH&I oh zh!f_J8gA}I7@D076XIkQpIu)??f?cakUrga2Ab#i{np#*ZY^D~a7L&T+(#OMn*EDN z_}f_?dbYzosYaFg-oR1nJ+2!0_zX&9)OOsC8Lkd zVqb!5?%7|A2QzNxOJ{{S#sys@GE{QxS1y%&vn%%Hxv6zz-W@8B*Vlpyy4(@wMD1#P z!19Bk#n{{zxse%U`Amd2-K{I)(2QuZQp7)lNv3FkIdMe`86^Qe=A M}ScvND_rAWq$~OmYjiE8{&cci-MiFtMV%K>`th)aW28kx~ zhBywBPG98`k^60RKRpXMy&*hY$PzV(k&kQ(iNMo{bijQr=7dy2FhgE>bb@e)g%Cv_ zWroHIdUe=0-E=^fcLDVMAEwT+yOK8A+T9)7wr$%^cHHUMcCur2Y} 6NQ`x%gno&iWUpWUObJoW!R9 z*xC%|LS~;B-)fgzE_DZf!0YxvKhh>ZG2i%; rnslj>O6_s`DnDtzU|83=>@ku?= zjXR#q1+=Z$G`){It6o?f7`CC}WQ^~`ZM~n8dWoe@ocTxmPES!JZcBM8{KFC=8Q60v z#DDh>n$$aMx`1zIA AFO;5$y3IYb9&&1JuMWJPW zur27hk(89FEyA#G_6&Jf&ejN>FPM>#R%!OFeb{4X6r~`ijGI(iJfG$Fjt0Ew<oc zhP+lfTYn9XXjuzC#b@r|&AP$3nDl(G^Dl)fy3knx)+k{lDcW7v236#lp#D{E7?tb$ zhOc6KK7CWLK>RAKR9$B ?_FKe0uI$= z8*R5S^j!!-jS5z=uoU0()uO)ZdN}M`T())MRU=wu1q)(OT#%8p1T~A<9iHla7ExT7 zDcazdOF)uB!52g)#!38AtZx5AZEuXt6Qs)^;CqVKqL)_wjReW3Urp#p2iD$H>a&h) zRbLpsMrV?f;P2d#&wu;u1CPqNQ)HR^MOw7C?Tu#>Sdy_n04|>|&}L&GRW^$D$QAq8 zUz2BEluHaLF)(Nb{RS*2X4yPm;EWsVe_J8MP)MH*9S?o_TkvSUVk&l1Hcd>UKm5D0 zro=c83h>w$LP LsZui@#Y2si0sLn=Vwp)p;k0}WugK6!Pu*dGe?0O zsR32p-YFEHPjY-aztZ5v K7cpgbc!K2# @`nzilhlxp19wB*wa|8_PtGzA}@@LK$o7DofCrqeH z2t&*Fs;TK>VZs9Huuyv?Zj$ax>fzF~1Izc|{nX`u`&fWXVibLKp0;3$UmKl4Y)7IV zn%NFcf!9tLfC6PYcn87aMXk;cQOAbL(v5%ASzdmo5osmw%xv3!vD6*b&o;ZN!0UuN zJ^BT?0#!?5;!cDdz|wn0v|8$ss*<&$_%G }1?YzERR|L?7Z`SoL A#sCW+<>X! z64Eqy+_5{j^0I0PDwF+FygZwte>MzoiON)cV<^)uWx5dI=uWdR>DU7A(D&-f8TuiC zip;&;1*V)Z>H8RYF4vUw3s@eE02fEW0|cvpk7)W=2X;SBoxaekPps4o+66)p@gT^5 zf0~EOstT!lu#**$ML_EG4yUrJmbkJb1TRX0y)?GSEj~Vl`lO+ ?v4HK|TZ@(QQCTfUzJ3vTlJ!5}6^jon|WR)YLbJE>BNbcF)?NM{<_%3kt&PU1#{ z?>UT8xp*8+_&f=Ufun#VeeCYOow$M4pZiUnh{}Su6U0zEYKt(Mhsx!%g|2r$r_yd} z?O4n(U#Q0?^NRpT06Xvy$uRDWrPVB=My;yd{V@Po`cDM~nqUl4KM_NBIiHqpDk-DW zF}UR5_fOH#k9|^?_?%s`Rgi04sbjn`WQSx-O*MXUDm$F}EeM9RYRJZZ=X%XPy*@u@ zxjSu4;bH tkt2Qp`Y%9*f(MV?cNtgQqL zg}PZWqO`|HV%(&mm{_3bZynu()?9ui6+(8+Pz`Bj!An5F1pERzhSIN}(Nln6DPioL z?^~im5}GMkN|dKg4(`lTv}_bF1%E2o%d$G5VQ+fXR3er}RadCYMc5;I$>NZA&ezg| z@SP)PcwUeRccW#C#8SindMuAqdZI|W)KxZrb&pMc6B{r<$V6FNTPGL(*k1%MoS9Wo zzM?(j=*y|NM&IY2n8uPi^H9SKOquJ{kqzF2fsR$1Ql&%X)z#8uGTD(as6`r@b858G z;MYA}A@aed%BS19baE4Bs2lG#T@u9F`tl}ScD;{-AhQweHbT39wDJxNR=B>gY20Rj zK51QEUf$c;@t57#CWO|_(Ekl!SPLh%H#E1RL4I6TSkc#KQWgTbj6qVybo+zLu7|{D zM3CM_(mJA=5}k)zB;JEVZ>pRH W z(L$^#6r{^`N<%kR+^W*}-JhgYS*mU+{F+%=<9yPBE@G51=?ZSsg;iO_5ZP6-lPcB= z`9jptlEal&<2`s&WAUd-p(+~giCC2dRwJomenA#~27Fq;5STg~EUg4#;LQ_IQbU7r zt+HSbv8u_yzFzo|_B=cbBug5;dVg%;~3Rmv8KCur66<6qb zCB3x4)NSwa?j*7z^2^eaiU1H-C%cBSa~hN##0F&tImrM2>|owFW5+o=nef~-2LO(z zIqj~71n=VCKLZeafG7CdXh;PGvEp~djcYogkNTv|Hdp@7E%BVzX0O8U_7WqsTl)he zdMzm-ZI94G-CQ9;Rw361NvS8h4_MY7pQh{_f9>q-mh?V*Bo$7zw!ANO)mr? zx-55YzP_<%47n)6e& $~&ki#r}fJi6p2dZQzGaJbdbN5deP&JT19_?CQkephon7*rSVO`!z@M4G|@r47X* zcg26GscH^YwQN 4f7$}VGPdnsy(+vE{EB7AAx|II?MuC$E~Rom)|pREd1}3J zbEntEI?jmpUPA3jH!2TchQlqKPRU<*QX!9W1KRR~nuL5Nr$Vav?>77_&JIm{=6K@( z*}zv_9i6GQaLbYC!_ab}J_^N~`wK7!0Xf#sukTLxQh>lj#7iNu-$79iO+}YFqX9%% zb)a>sr#%s*jvp7 3qx=X8OSs?g&es )Y8_V(GoBQih<_W#q-_GANCVb>PpQWc`gCTcyJ*c-_s@ 0!Gy&S>HZ&A*IxCDJmgzAocd6$0uEj{ zM)r~V1Q&_5=)?KsPZ?_7(qq}c8H~hwiS7jq^L#qe@1Vy5&%TPtL;4uIm>dazWa Y!!%ta=bl3DhT _vwYb1QhO+k_!eV7XD+jn{Y z81gc2Hwh$XdeQtG3fVI}JNpl(Hig02Vb~uG-A0no3jmZhH$8A>5&f^cMsUcM!4R1Z z%caQV{qcT%_b{}1a9%2wCGNX_AJHxgdwVxBIce*9pscAL%CA?-vk$YdXgY|Vx@^Er z0%%Z{GnK$;((*_cE;r~U-=?7 A74RlaZ&x9kJt|_SlWXfl#3jMY6g$~po%+pH_YnAlXD{LsXbwXibBg(3l zvuz)^p|B+csPom90x|jBj{5vJZe*7Rjq3%K9|w~kh8L$Jp`z<)7#Z^c*67<);BDH* z48ann!x!kTv?JLxbmAYC4Qm=6+)cee-y@=Qpo^NinsW(T00P1yI!4wCBfFkQ@gnIZ z+c3RbK0N~?=3|G)2U@ep sL1#RoIYx{|Dn$0E*NGT-!~8v9!I zUHo`3c;u?It!a6EVZjMHr{q{&k@Rs|-12BG+Dl7YEoYXH&HwOJ9=EdA+_k}9l+iX! ziIHOP2Rp91`#Y{W2NIuj6^6el@|fRkM0c0_w{oz#-}bKKua+CqTBTJJ|JMmUKPBKW)3xxovWVz_4k=>*;u1vbI4h=_m9OsZw3{ zCse6nS8+{$D=f)Ea2DJtXSu4UB&kUUWWl$YT8|yJb>2T}oR9pe+Y;cQAEIsk;7_k3 znkM^@rX24k9LCzNDfne)>VwG4p48yF)FJ9uG=)FE_qFi^t?%8D)HoO7H-nS<@Q;{n z3%6g+*gQ@LbfF%11I(%Ce1!&_yY2O=dcxjGGD&DW)`FWi-KU#dH*0G9XSg5lLeP v*INX-yg_cEWHjZ6XxYwM?Y{cCW<`As$J-kCip9g}@Qa2UObYqP#0U zBP0}&CsI#@wta`fYX0Tpx?O>O5S+RTN9)J~RtT=q&uPQMcU^E`M^a@7By!g=U&-`( zQC4X_gPd`%oBoZqia=LQd-x8)#VWfOtMkucC9ev^`bhM8jk >(U1EGrL_>qt#AlA$0_EBLz%$43HyntYA0lN(Q@}c7jBJj3~R1FB=lx)S$+d zJ9m$S(&@gYfH{!2iK)Ljm6yH-1^#?@v?901aOl>;>Z>61;|~e2jolJ!{ps>Z_?*&} z$}D@o_M@gB^potj4&s6BRTWfk4RL@*w9?jZztG+dT2rR5<66QC2W5S4RR{P*sj#)q z$me}=PWx36-^34o1YnbCQL&kdlcX4hRD{(CK%uy-o>0Mugw8K*Y?^M@k~z03YV!DO zJWN+0R)1If5wt52olmlX*nN6! zcVgXbskN9THEI~ELhR)4FAr*Lq#u*mpSvGP2YZ1kOnX}-H>(0E;bhIXi ;}ckGAH+{_MC>!*~o0b^ck;7w^}DrSF}VOcG;l?q5AS{|@E0{lRX=HYQpce#WcX zCPxzY4?=O?ZcdN0G+v2KH;b$^lh^I!hWtl4pk; +Nf{UFsv0<9?w# zVqGc2iyS)I1Je3|6jpDH5#TQt0XmUI=OvWOIGR@+B9GXmmwI9<$wG>q0x7Y>(WdWj zg(o&+1|LiAjfjRD?-P;U+&_oDNNQr+Bkf3f;k!>5?b-_HT$6EZ^cu2%j}z^F$mCwn zO6_^P2>FC>xBL6xm>$cj$c! bW>+W ~W@_jXQwc8faapJ^P z DX}U^bk4@|YGb zJa(0#FN&0fL!VnU+VGS^y{NP(PQyK@Kin&PjX=&!uj+a729*`BGCX=McF7Mq_37GV zItMIiiDMCrZBas#yu?IFCuzw4nBY8|hd!*6Ft9mgBJgc=q$Y3H4{e~nAZ)+~issLp z0qRQ??Ud>EW%l_Bj)E3|Mv{(?!?JROADBijf{C{GSkFcqT)@lFMA1e*_T+X>&T;|$ zFxe6QC9%T8jk5OUDcN(AUyD=F9n+nw*Gtg)`FVtE`n9Fa?>7EzzpaRJCkvAx!n{U$ zuexg#9lBuQ7!HnAFG 2c0tN1sD6@H=cWeN0h2LCn(#=WLVXrGgCrA!c&WHAtw~@e!n |qD0^YNbxxEFMbHhNfovs#fOQeB 6_PUZ5%0KdU z{iy$9GzVWW$G+0Wn~gs7CAU%zbL9e>oITmM0(a>>jy*<|@Ej-f=^UXpZ*n*<`(5I% zJeqf}UxLG&I>P@sOBtzGRx_z5SBC>~{Y>r0P)reGl(iL`EPzq8v&^tmx1Akb;)RRe z^WakENTQ^ojvQQhs)yLxWi(SJia^?n>h5s98a&DVUV?Ku20o9*BXnlm(^1~EP*o-S z*9X}C#ns?iKp^(QQfNUx&VmKJY#`m )Nz{m|~`uNkX*cOdpe zLrv^%d(P!p^>06j#FDtOXk~0Z_#J;QJIXvA?np;Xiz;V%`XOFaE+(_{vzqI7Ii{Pj zb)tHUH$!+czG4~96WY~|d#a&s&X80+-*l`Tuka_zDKkIUV9|hmzFP%31(Zs^w2@Tj zpQ hAF2O4_2FI>YnXG?+RK#-7|pLzd_;(j1Ej+hDm9v2v3pk_wJ z@>O}$`8qb bGs{d`$-o?D3BqO*i%|i@iS;~Unk5n zF7t1*j|Z^z1o5)nihA*8ohOa%)L-Q(s>oJ<;*F{2FGz`;?)p2T%*1N=$Xg#{Qg^ nc>SvDFH|BX3L4gUAJf6xG@TQ{M<3sWW^--^CaK7r*@$iWMQ|s^FWXorMV)&3 zs8^@Q>aTDlJ>U2veQ$r3!r|-JBBB+aBKegd$OC>5o4}`UMN#kYV0qhkX}4?tkev0b z0SSlW5CSBast+k-4S3o6{Egy(!G_SJk=^Q_xVd6G987gTp~%Op>6z|CxEX`aJ%>*J zdf r~d4|36W68{}dwYJ!tn0iFh&z*G3IleIMhz;fSwuxR`&hiOQg%}8Vp)2N zjC?LW_53VRIx#7ooEjv(n7$dSB)wpYF-)2XpZ3#}a=a*~B1m_^IN$D$%=m4$@sitd zTtaJnwFPdK5duA_1 5N5)_&&WpC-FppM7qTOY! PSu!!x;Rv*--+xr4W>M2@)a z52c~5MA2H`_?ANv1ZKRn2pE(1gH?FXqX%o8e#db%VhKM|C!+ zA!sJ$$lS8Y93_lJVSpC|hOuFa9V_rc-v1i3^M{p=7ijjYY;0{hq!rc^z9&V*$8(-x zV<3irC5cv`JpuXL{4(l_y9jP)R+-7+sh0T`HDTs7x =fpI} zXJ6Lp@uSzR$F@Z6ae(XO53rfGqP%LUu!_lKFpEhu$8fhqY8ZO%9viGPBJHc+=CLG| zD~31=oDa7`PFfCy7oX|j`J47n2BXm>_bgu5fpMk5=D}@NH#tEudoLa2JoC1SKms|x zSN{jc7i{p^vIzx4#fb3l%R+y<$l49ZwkznxQw-^l5pA^YT@$Q+v^}2g+@Gz(5gAkB z6|z~>Ru3{;Rg7|{347|AUwWM?hqzUnzc9hcls6*dA5QNChEms93hQeFc5|rjKl6jy zmm8d>u>OoT94%3a`$`FjGL@1|d>A6uttJ;_MI~VkI1%S( 6oFqJ(ljvHO%MO<(N;_|THp!>o6NCyt-OQA+@O#K<$a4Bj zA8l6^i33#Y)e=bObVT$Cy)JPsiBJIV3FQ;(NrN-mM5g;<8cNw%a%x7f$y#Wy6*eg! z+{qZDn3DYL5vls_3BGoN6JNfVUUPK8^R{`N)PQ4im_U2K2rS91Xg5dno 5X_mh` CCJ$zgIGwPmRxS z5X3PsCcj!V#(ZMsi;w$F-07@8b}n6P;^mUgY+_TqWBlsK-`Z=J)C~{o;SakbLNd-< zohk<)B(XAVk=<1X1cP|_3!=TX5|iMGHdy%Revq|U4JZH|=uPGyEh4O<%ccUq@sttJ z$ZhpXO$$tlAqfqLAqfniO~;rTmr!wS`tfb9yBp0d#&w!ARfI7DFY#lv4Y)#ph$KGC zh^(@BpK}wm<)|