From 606d189787e3c073cb1e643a6e32e4ee8ff2fa08 Mon Sep 17 00:00:00 2001 From: Pablo Esteban Date: Fri, 11 Jul 2025 14:32:20 +0200 Subject: [PATCH] - The get_undeclared_template_variables method now analyzes the original template, regardless of whether it has been rendered. - Added optional context parameter to return only variables not present in the provided context. - Added test tests/get_undeclared_variables.py: - Verifies behavior before rendering (all variables) - Verifies after rendering with incomplete context (only missing variables) - Verifies after rendering with complete context (empty set) - Verifies compatibility with custom Jinja2 environment - All tests use asserts and are ready for CI integration. Closes #585 --- docs/index.rst | 4 +- docxtpl/template.py | 32 ++-- tests/get_undeclared_variables.py | 151 ++++++++++++++++++ tests/templates/get_undeclared_variables.docx | Bin 0 -> 18305 bytes 4 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 tests/get_undeclared_variables.py create mode 100644 tests/templates/get_undeclared_variables.docx diff --git a/docs/index.rst b/docs/index.rst index 292218d..c4bbec4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -406,9 +406,9 @@ In order to get the missing variables after rendering use :: tpl=DocxTemplate('your_template.docx') tpl.render(context_dict) - set_of_variables = tpl.get_undeclared_template_variables() + set_of_variables = tpl.get_undeclared_template_variables(context=context_dict) -**IMPORTANT** : You may use the method before rendering to get a set of keys you need, e.g. to be prompted to a user or written in a file for manual processing. +**IMPORTANT** : If `context` is not passed, you will get a set with all keys you need, e.g. to be prompted to a user or written in a file for manual processing. Multiple rendering ------------------ diff --git a/docxtpl/template.py b/docxtpl/template.py index 95b4a2d..2e12d53 100644 --- a/docxtpl/template.py +++ b/docxtpl/template.py @@ -887,20 +887,34 @@ class DocxTemplate(object): self.is_saved = True def get_undeclared_template_variables( - self, jinja_env: Optional[Environment] = None + self, jinja_env: Optional[Environment] = None, context: Optional[Dict[str, Any]] = None ) -> Set[str]: - self.init_docx(reload=False) - xml = self.get_xml() + # Create a temporary document to analyze the template without affecting the current state + temp_doc = Document(self.template_file) + + # Get XML from the temporary document + xml = self.xml_to_string(temp_doc._element.body) xml = self.patch_xml(xml) + + # Add headers and footers for uri in [self.HEADER_URI, self.FOOTER_URI]: - for relKey, part in self.get_headers_footers(uri): - _xml = self.get_part_xml(part) - xml += self.patch_xml(_xml) + for relKey, val in temp_doc._part.rels.items(): + if (val.reltype == uri) and (val.target_part.blob): + _xml = self.xml_to_string(parse_xml(val.target_part.blob)) + xml += self.patch_xml(_xml) + if jinja_env: env = jinja_env else: env = Environment() + parse_content = env.parse(xml) - return meta.find_undeclared_variables(parse_content) - - undeclared_template_variables = property(get_undeclared_template_variables) + all_variables = meta.find_undeclared_variables(parse_content) + + # If context is provided, return only variables that are not in the context + if context is not None: + provided_variables = set(context.keys()) + return all_variables - provided_variables + + # If no context provided, return all variables (original behavior) + return all_variables \ No newline at end of file diff --git a/tests/get_undeclared_variables.py b/tests/get_undeclared_variables.py new file mode 100644 index 0000000..9f165c2 --- /dev/null +++ b/tests/get_undeclared_variables.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Test for get_undeclared_template_variables method + +This test demonstrates the correct behavior of get_undeclared_template_variables: +1. Before rendering - finds all template variables +2. After rendering with incomplete context - finds missing variables +3. After rendering with complete context - returns empty set +""" + +from docxtpl import DocxTemplate + +def test_before_render(): + """Test that get_undeclared_template_variables finds all variables before rendering""" + print("=== Test 1: Before render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + undeclared = tpl.get_undeclared_template_variables() + print(f"Variables found: {undeclared}") + + # Should find all variables + expected_vars = { + 'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field', + 'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author' + } + + if undeclared == expected_vars: + print("PASS: Found all expected variables before render") + else: + print(f"FAIL: Expected {expected_vars}, got {undeclared}") + + return undeclared == expected_vars + +def test_after_incomplete_render(): + """Test that get_undeclared_template_variables finds missing variables after incomplete render""" + print("\n=== Test 2: After incomplete render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + + # Provide only some variables (missing several) + context = { + 'name': 'John Doe', + 'age': 25, + 'email': 'john@example.com', + 'is_student': True, + 'skills': ['Python', 'Django'], + 'company_name': 'Test Corp', + 'author': 'Test Author' + } + + tpl.render(context) + undeclared = tpl.get_undeclared_template_variables(context=context) + print(f"Missing variables: {undeclared}") + + # Should find missing variables + expected_missing = { + 'has_degree', 'degree_field', 'projects', 'page_number', 'generation_date' + } + + if undeclared == expected_missing: + print("PASS: Found missing variables after incomplete render") + else: + print(f"FAIL: Expected missing {expected_missing}, got {undeclared}") + + return undeclared == expected_missing + +def test_after_complete_render(): + """Test that get_undeclared_template_variables returns empty set after complete render""" + print("\n=== Test 3: After complete render ===") + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + + # Provide all variables + context = { + 'name': 'John Doe', + 'age': 25, + 'email': 'john@example.com', + 'is_student': True, + 'has_degree': True, + 'degree_field': 'Computer Science', + 'skills': ['Python', 'Django', 'JavaScript'], + 'projects': [ + {'name': 'Project A', 'year': 2023, 'description': 'A great project'}, + {'name': 'Project B', 'year': 2024, 'description': 'Another great project'} + ], + 'company_name': 'Test Corp', + 'page_number': 1, + 'generation_date': '2024-01-15', + 'author': 'Test Author' + } + + tpl.render(context) + undeclared = tpl.get_undeclared_template_variables(context=context) + print(f"Undeclared variables: {undeclared}") + + # Should return empty set + if undeclared == set(): + print("PASS: No undeclared variables after complete render") + else: + print(f"FAIL: Expected empty set, got {undeclared}") + + return undeclared == set() + +def test_with_custom_jinja_env(): + """Test that get_undeclared_template_variables works with custom Jinja environment""" + print("\n=== Test 4: With custom Jinja environment ===") + from jinja2 import Environment + + tpl = DocxTemplate('templates/get_undeclared_variables.docx') + custom_env = Environment() + + undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env) + print(f"Variables found with custom env: {undeclared}") + + # Should find all variables + expected_vars = { + 'name', 'age', 'email', 'is_student', 'has_degree', 'degree_field', + 'skills', 'projects', 'company_name', 'page_number', 'generation_date', 'author' + } + + if undeclared == expected_vars: + print("PASS: Custom Jinja environment works correctly") + else: + print(f"FAIL: Expected {expected_vars}, got {undeclared}") + + return undeclared == expected_vars + +if __name__ == "__main__": + print("Testing get_undeclared_template_variables method...") + print("=" * 50) + + # Run all tests + test1_passed = test_before_render() + test2_passed = test_after_incomplete_render() + test3_passed = test_after_complete_render() + test4_passed = test_with_custom_jinja_env() + + print("\n" + "=" * 50) + print("SUMMARY:") + print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}") + print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}") + print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}") + print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}") + + all_passed = test1_passed and test2_passed and test3_passed and test4_passed + + if all_passed: + print("ALL TESTS PASSED!") + else: + print("SOME TESTS FAILED!") + + print("\nNote: This test demonstrates that get_undeclared_template_variables") + print("now correctly analyzes the original template, not the rendered document.") \ No newline at end of file diff --git a/tests/templates/get_undeclared_variables.docx b/tests/templates/get_undeclared_variables.docx new file mode 100644 index 0000000000000000000000000000000000000000..e112a90deba19eca57d2caeecea1c0dece36d0ad GIT binary patch literal 18305 zcmeIaWpEu?vIZz-W@cuVEVP)JnVFfHnVDG@Sj=p(EM{hAS`Rc z;~P7oXrL$};Xkm%meGJRqik(O>Z<|~;#Y!__QNj1ZwMSIgvBa>1(U~~6$mf(^oxDJAa$MqdP`3|fLtE4g{0lR95F-bGMTr;<`8&a#1XSf`jnR&_Zz zZb(4*FLcl(G$}w?-@l4}0kVOHSS&5`Zr=rr2k{1AnaGB)O!K`Lx&GOKOj3^)J;3Wy z*mw~HQ0QT>#N|q~M}YFUeFtf=TYsOQ`nvKM?&eCu2~Sn_AsJaiB!v zEYs=iFXQqNTl*mDS{!4p2ab1l%vU8%avFIUc%mYg2WND%&cpux$68e+3x(>!xj`VbY$p7Q(|6-EWzQvwqUmdlX-A&@-|8=PB|JFsg4>ki3Jw_7?w8PgS<~AbiZE;ZB9fvIcG>x zUzlUBy!epj!uiLSDVUA8yEXyqfWbD=_Ij*DcXYa zjt0w$fu1xg-k=f+>#Ae=#G|mgbRayK1~MNqoc_hjL<>pd{V?JHlaK9^P`yES+ZH=Z zoB!s&^5fiY+`ShJ005K&008lGP+V;tjOdMQ4V|q&o!cK??JP~(v0MxxC1jq4C^WkChq9jJP(>^Lu z3Q*`rw)u(k9K%lq(b*RC9gJ%Y9QP;(EF;#~K`hvS;l7n5Cf$nDkgn$xrW5^%ULAZ= zcUO`1Q8o!i-;4oviCVdP~L942&#`3&pKmxZPZSunDo`qN$SLHx;a6ta5 zs0rQ}=0km~aK!T!MF;vM^%54iBIU!B-=VTfIQ%mKjTuL`blGMM`gI16Y5f$FgeMWh z>%K7DiG=gZ|8npXt+9{}llHY0P>lQ*8-ARMvLZi3V;^zXe0ul8nOs*_Yk`^}o*DXJ z1uMrm$aG(W47uH_Rg)z}d5$FnvOk`TJ8H(+H4MJEcf2M@$_LF)Tx!&Pb}o*lu~{QC zvs52A$-aA7TgqY5SBAwY+zP7SBRC4W+BzktKW{5~J5Zn=RZFn$Dap?Rbn2>R&qSU{ z161&aZJw4R-bg(;i<>$C*&4`F?7ruNs~+@f$dmBIKu_r|C`=d)VAkEv6ZjoI{&8GL364{B&B zkD73=Ej!^mqp432MX(H>=R_YxJ`4}?6~w5jKa`RRCW0^j@J`)xglUC+e(sLs*M4;0 z%CM9&mu-Ycskhda61zSJh5jkvxj36f*xfEzQ&C1JO*1l)%mHu_Tn@Wu-(I10Cv1;h zAD-J`7nnAVuZ9WAm)th6RUK|dJ3oFw^N&D~$smNGk~eR9mM?V9My#%w+{>)iWqqsX z1mNNp#sgt7PBUcdwRn(~f>7@7pvmoYh?B>HNn=PKS4`tGpm>k(WfWwH-I8>zl|w=? z#B2}j=S!GhN0dwX$*1!~XrO}{xqp9_&vT~hujaz%x84>S>kJETSEuzMIV8nTTq*Rb z$O2zNFZ62^hj9p-GnRsUH1x9Eo#6 zSc%I(jsFn-YyA7l4pM+!p&k&utgaR)h%krbX?zNXhh#%*J0bPx^_P2F62J92-5v7siuPA3H3ueu*P(}{3#9z z+zyN0ob8VR5Xc`)Q1Ba-bRMb9cO{m$9vAZ$4h!C^&MzvLmS+x6)o8DzL_u6g$5pE$ zmS(4mc41!xoHn?DK^h=3jy~LP^SEvJ_EmF}pKh9hB)_20q@w@2c*sE?+H}t+4TLB@ z%i+FMzz&?ZX*2Q7%Pn<${S2r-KF-uMZ8I5A-l@?PiZAkE!z?QUlmry+^cCiK@1_Sr zgTnQUvP#sGKX|dlR-A-{Ekl}&x%FNiwazZRczeTf)>{_>Dp2W4v{*ZAm=Y$Zp!nj) z774mrARGDZSj|enqnj+K6&sjnv=C%$zEyu&m}2Xknx^{ZXPRGRb0bE1U@URzCA~Oa z2Gxx!?H3Vwrd!x^)pRqyIU+j=o~@7+$TwoyT2{2jadfmA;2#G6&G|UmDVoxi~85p$K5PE z%4fhDc5J5&c7$`u_#>}|xH>-?E(?hauiMOstvc6}D*r2)qj#SlcTsl-KXrQeE&17b zChvd9C0`&v_HxBxqVQwhG$2s^w7;Cc_LV>Cq?lUt$~Ch-lEcy(!djgW!re}!h5ZFw z^PvdwC0RGy7mSf>J;Q3FoO1=l%_nvD!%nJ0)- z*>fu}32SCpyrU<|Y(41eO?!h6dIRyMb@Bn}(GE}hEI)iZoV8t1xsOh)0g8|jI~K%> z*Y3BZip~8%GK=S08GW#Jn8^CUt#bW`S*?5-xEVnlkegwp^LhqvqCb%#Vh-*K6 zgKW2_pJ?9_#n-w1G^GDDvo_&~n!V4I!wUug0O`|u{xmZaTU#d^TPI`3KjNynlvSHT zhR?W4b;b4TTIoR?Fu!zp`SN!U)y8)CEi7cYYyPH@?ej%2silM}KOJkjfZ8`-yh*pg zwT*1CZJe68FNN5P-Pc;+iv0;$7wi|8y0P`c&P4$%{-A|!yE(N^mra}9>v+3KjKb+Z zW~O1Nc8Nu?*%c#(0K}5?f%aiX;`I`WPrw*~)G`hr0*R3!A-FdyG_)9kiszttzk2x1 zSlb=3M5REu%^3h^0Vnb2UmgFr5*0+#gDv2tNCTMRf_deo!im-ZGE|0V$px8Fxf6>t zFQU%WlpzN7p0U89JWDJvFWRstdIO%|DDhx04)|KEJAj8ku_4Jjs|Tn2o`T-WX<5?T zfRQ%)0EuRixlu-i|MA`y@EUAy(jKX4bTd!g(6>I5zV}{y-(Nw8p1A8Tos-Rwl zvD2>6Wmra%Rz7O^bxYtSUOs)lqYBQ5N*dk-&Eatj5L{u&UGxH8s?j&ONgFo=+Ai!# z`b81DMb(fyMv=^1IHghQKXk7Y+9aEAHVlaAYhf2u}~vwts4{u@Ee9o}>%5 z3@<|!O=(QT_8$7sGqB~Bs3E6c5l%3KfMuvVhI=R}aZraX27|R_rR2COcMZ4Wp*Xp1yTw}KluY6nfPwsggY-IBz1sTp2B zq9FYMIATcrjV|gK8#|t_-%p^4N#1f*+E-9rA4F5Wc$Pa+J+9o@axdm*g;DIcH@>_q z?i%fLuIS-yOWC!#$SSozWLE25z!B~vqK8<3BWQ(!i!az1M8P1=kmSRhVy7UHHiV7C zBEp1o$Uq8%=M%Bqc~zTzAD_}6gO7HT~fn> zA!9Z29S8JGnZJRXxlFhCpt4?Nf}yR6eNsMCQ_=9rKJw%#Fh`6oh=ug6R@eNo1=0*1 z>oA_~_?9zJ3MR$n=Qov;Dm<>H3?sC1)|Ay?sxhC%M3s`*6m#df&=WhD1z6yPV8B^% zUIS0fXLvj;G{T^TOKjA*v;}fXW-ei{R*x{(|Abk~t(Q(9%~LPMIc6RS=4N2ZnAAQ= zdLm2eusky}Ii0hT;9Y|+3#_L6BNv_U?TeufGEII0GZOE4i`*kEa3p~@?(o;rFu%9Z zKr4LVM7&5JPoHv;tYwiTt|pSU%zNUgFF^++fMwtMp|JC{j_QtCt|Q%%f^Mc_c=$^n z3|nDd!8d6=wj)@ZT-DzyQFz4)(bJB2Xbu^gk5^_e4%3fgxL3a~vt0$)?gvbc@;3B? zu)!zVSy5;SDqf(C^7yv-+lL`p`@(^am`bNeMjDPrt zy%b5sRR)Bi%j{#zJ)EY-yrtP%?uTDP|7_zn*5F zIK_ze{a6=!H9`#88AZS#ju{AHPLWeWB+L*q6Xh64b~s=mHK0-RzaM%oCn^ze^--)3m|DV<1V3C z4_d-b!^p7@PmAKOEUedgWcGv7fQYC8JsE}3B{c#(*t@i@c_whJt3??th|LlDVgg|q zqg>j&BBP&M#>xtnBbhyEe-SU9DRm{#APnk3a)2_~>2&_ugduL5-71-T~+P8nKaeZ7<}4 z^7LMRi#{<$_#2Nwj-FDGGGD!vgVq!ns@>1~dpSBC)iQPon*ErRt{9hg%VkSX8_ob_ zlvf|*nQCOmTs_F_tl-}B0O;UD6cqj^PC0*>Cg6Lha4Xwo;$@>F8eyJHv;U=BkO-J$P?cO` z-+%)Zk#GkezZ0DHK8RTNs&z@S6D3sXf6y?n~=PG}Dtrm|IDuX??O@lU&+5HisPw z^dox$9{vZFkfgw|CRMtTDPfm2l0Rnb#^`D6L(F54p$L}n!?}vPTWWUcL-d_Ks;taV zYajHlctzbG&mSKhx^-XV@tJPOC*OLNO%_QM5_SJ8xnAa(+^9au%Al;#rYb(oLSoJMEVo%_+r(=6YuI;ubHj zuam6Z|3}a4QHz_GbwN5XO_Z3r%U1VZiciQs)#QAx>j8Jx4r2 zSvG$@WNaGLwplSWw;+TuRYI&>U_Q)-DLF+G+02%JL2Nm0SIf4{<}lQiX{XB2A(R@$ zO5eo5b!^sRS*#K#p5p`9%x7ZfB*+(J>oU+R z_98~s%@AHhPdQ-?0dIy~NLm;q>`n$z$%VpoO&vbVy^4q0ifZ#P(CPHrma?Oz^9UdBb6bQI*vUN(;n*@&$yE3vQ^ePA^PBF zMBNd%g~h)X!@CvCN&@fVK%JSezJN_+;OqxVlh|&4k8{u)|O!=M)((K zJ8Q~p)K*%^V50sUagDj-y6@Q>I0M|-mk-mIJFQ{2>>8S>4X@v|bB>B@8~`&_V;5FL zN{orlBa68YGY*hPWPKZB8nKt*vi8$#LKjT$s*QxpMoObmvbIaHWARD)EXdOM){;xd1%l|sM{aZn)riSdMFZq zP%DN#L-z&Rb{nnm>iCmce9#lGrxgGqf*gi}n|k@^V?!H{<(O@X;9!R&y^is3ArjKgFetG91r{xGF}4QEnP^1Rl2zBD00(SuE5}@}j!T&K_3@P%md_XSdrb)7jb-^YD1CK%j*z zzWipClH%hR>xNEF@PAOq*q-OP<+cKBrI?X5OYe^c{zHP$R1lS+ns z;%n!B^U8$Obc&+-c*_0qU(*^29_W^Z&)MQh0ssKie>p8DGh=IG`ak8Lp6g6qHWHg1 zr5pVjw|@;ccIbXdX9xB@)4DE`MxC5gbUDSeVIc}kd?OmtKOxJ55@lvaGQSy5!aUDE zMCVx_4Qb_8P-$iqOKxDB|)?-~OMtFVMwbgi}N<(OP`I|f2N$S*1O3C@Z$A7MnMc389y`xi9} ze=Sa?92!4iCJjXlCU>$2vBIs^BE1O05L}o($vj*U3q~f@9&Ng?W&Q~ghWll2Yb)Y zwuH?H7_vOHLjl_oQ|In?66VFaz+p@McPA(HGo^rbED6-ot$Ve_<(CVcplsk0__S@< zjS9>ChqtNhckHiCs*G1yNxUj-i%Yh*ZOXva{KtmQz?%-8E`?TGtQ6k2>{rP;6$f`P z9bmyAL$lHix)qCWpdO&|UEy&gfS#%45Q+RQQqBwP?MDqt8fr+0dXd{%Pc9 zK%z*C5+M6|k?sOpYk-hZl&S0ARjEdLDq1#93Tfe75yr$zwTZi?iyr}a4LA^`4@2fo z_6X>8J2*UFU(c>>O=WYkc-}$Jb_r0>T6r+Q-CGy=eBK@&bp5)0-nQ>%ZO`0thPt1h zeyVnVT-%-m^PjGoR_E|}KCP?zzTeIP*)npB^?`E;;H(LhZox(G0nsAo5qffk^F!F{ z6rh6R49bnDP4OUN9cem><$*4Wl;B=6rqfyVoYor#Uda!e0BQQM_YDK!$w{mvsV_Py zjwd(&pn*F}tXIc#1^nXV6DOA12QVN8z$#J-q27OCXti_) z5R0+zMAYLAvx1;0&D0}EoWYdOgCpKOW(`u@4ZDi725c>1b<%#+FErW-7%ojp6oOgZ zm+lvz%ortx*9tFeM(oaFO5C@lB_<0VCO3smJYmh$yu6m@i9Fh2?EC_lE=raF$=yi! zKv?W-@bkx*_XjqRThX};l+gM0xxc*JlZM2+8GLKRH zIIP)Vxy4Wzab6^Lq#+}_z&!-06LfCilg3VZA4h;y=7FXnvyq1&8G9kE&WM>sk;2$q zNn>KmII*F(W+Pt66a@AB!M$5HbDytm-B8ikHP>)nb4q;pTeLN^Uw;|_bQ)m4-e!tT zaoF~|l#n$!EnEGl^q6#JYkrH$tq=!qDbTa+r>XT1%yGMHy(n?5B8_@P*TXjJEIUzP z@5Xl59RE=F3fogR@Yh*jO?4js==#3gluKWc{?p>1E~7Tm%g=(0TJM&1>Q|kpWq8xZ zs}uvFsDaEB?_x20s;BQ-YIa+GunM}gI0ZiXEM|)z{~HVjYOWz7UNA%aY|E9)YtgC_Ic%Qnt7p>)5W^Tsbr6Js9WRH1iu7t zY^2r^DI{a)@Rhn793&58^dANK`tqtL&2>|{(AC3zkYAk{b*hS@j9NzG@TmEUe@R~I zw3Ieu&T2K8s_O{acVft5lQ?XfBn$`ZQ^3dGT|nPE6FYw6mrXZF6sdYA^r}i2*03R{ zT?Q(3m`tuv7>6K}HYM_98yTXAe{LC7iUYBq#S9P5-z2RkiHfA#(}$WBJbosoK>!93 zWm>qgA+R5;+MA!l;!|*deb}wfdLQM&lqNGHX}UF%{*ppf8`DLu#(E@`zza20swZZ0 z2A334z92E`XE-)6nqfHR(3`DpPpeJaRB#POv04yELa3ER@nn?qzCtB+4Y;?xvzBQHebHZeKViL$G?c1tR4N;)k`J4e%kuIcFcKICj5a9!C|g$ z1jZtwHdtf9QlY&kfPj6SilsRl5#1ZBQnZSbAjP@Z#=2vzq|XrZNn5>YB5W(8{B{0^ zp6QbY(>O?*Yq~Zb1cPDAF}G7imBdhm3zSXg3boQGYO`E*(`c|tqXykwIcf9iJ)U@B z7z35^Z0pOfA>MTQjg8ayX!ghHy16c<0{Qq8&X%&^JM7m4(~e%*QzHhsUAUeZCV21a zO$zoEpL}_nJLqJab{#XPSC{-#SBrI@X2mM@=U5!Og5G46R>f=2y8zu2Cn(fS<=fL} zfvO$%Dv2uPOJ$8%w5AklT_5`K3itE(&oJ^o=Q6zU&}bV$006My{x+B4Xzb)Ah zQpIMIn93jCdTt7A1c}MkCD7~zr*~Nr1;Vwk6V>** z0^a_Szlo|8Jda+QVmfM#jQKYXv6WW=RPKQZOlFGAKK)9SNQP7QOK_% zk|MzWrtcNwhL|pxbwf+!@S@DtDqaM;2o6)7`QVIc@G7iUhJmcE)KJDjeJTX}2aoNg zT&K&al*-~1urz6c^Q4(VhUxWN8v;@>%$on)#a9=XO4kE=t-7aL$zN9cWKH8lrY3;- z-To=}+7KJd1EleobF8<0{)^|>0OjI))X^*qgXJVq%V6n@0X_cO9VuagmX1i4f#NY# zCHV4fGX)^Uv;mGW9rm!c_xm9zO@MEpRrA~W43*bbfh40W@fAqMHLFr68c+wfHILFq zm<{rf$~u~R0!IR7zuLLqfq)h9c|MfBA+Fxs>8>OIbw9iHI#M~akcT)Y+NxG~==q8J zbF9scAgDn}Ns&;CZ{{cr_K5Vk@^iJv`vHF#v(I?NKV4O?8 z!o&OQ0jx2fy`nli+v_HP^0x(8zT}Ntnb2U+rYcDgUP8mPzUpUAbCV;n<;h`7in&q^ zObkB75WrkUz^^nb?8(Z7q%gS!F}Y_zRe^R}TAsYUv}xJQqU(sNwGObCB?XqJYKe|G zD^7!1Bl1O|eP7TesNd$0m>>*v-^&&i*_CI=%@rKnu)_;6jUR?Ttgj!oI+8{|j5A`8 zNGM6uA^32gG2(55t)3*Tm*>C|mgdf+__frLBo$hwT7tsW(UeA*BN4$Vd6RO29j(MG zZN7b#)bo{KXt-PgcjX|Xxlxxw#Xv(d1lbURq*zRHQ$W$l%D@U09HLd1v5GDxyke> z5INTjHpg;aaJm#ipK~j*B3~|I8~L2!JH#X+gf@(rN>IX?+&lw6Qyq(?1qeVXCKF>@T5wa){1!1F ziSNhZjrH{=;L`WCEi90pdpt;L;sLIkYYEy7X;h5F_0TidcC&+H9l9=y6fx5H$K(T^ zd(*=%yzkr32YCAWSH=VNJb;9=eIoA7%)_T&`|mzz6HfugnhFOFTwalU-HcAb=ujUW9@Imc!i+_{o z%lhJ6zl2|}_90hwADCc6(x>V*sA8XUJbugBnCbHK)txwz8yZiijs|5%WnLAv4WazC z8C{(0ehzV>?v)6+@_I|_!;Blr#NGO#IIMr8i|#t7G@SD__*)1cm5Xt=GFLf|YMTP~ zi$Tg-@{8O3&Yg6pgN5p^o%&@~W#R10+S`HgU`B47?x+FWpQ_&?3_X4lpls7cf1kU% zseXzWc;53~Wf<7haHMT+mZD9iK`BwNN^wM;ha(4X1FN0lzdd@aTzhPo(W+CXvnB+~ zd(`Z`03KATQOv(A3F|qb$n8|^8Z6Az!sXaxHvX2KyQy6&P(n4DbD5paG(Umeg(FYj`gcJrv1e12TLh_BY)+j zPPp^-;Oi#oQhssOh|fg&g;zkIHH4x!N`HLQu!ZVsRO?Eciz8Zj-t>1UtjqGeU2RmB zWo3TycB&&MRZz^dQW`Om_!H5wry2~umEc`DVyq!THkKGcNi&>Kr6~?L(mZ>dn3zHP zM@Fon?GlJAtq?l;$}ca&+vn#k>)t{5Hm<2#p{wh_kJ~QbUVLj8j~Ah5sZXL_SmXe^011Jiyts=sUK&LNX z+#?Vxe7tinA`g9?2uN?4Rm|x{Jsd&D`$-C~hC<0L(34alo?a;I<;A(rob!uhFDXVE zyxcT^zfMC6(#vt=Vz|Z)36WhlTnq_?Dpcj>uoW$w!0rSTTJcMVMMC^Q=mQb~)7$F-gwm|QH3v~6Z!U}i4^x!tJpa2hOmNp+S2uzEA0bCpMO<@V1V zmp^t)rbC)ntA-7ccTNjq`rcdHE7VRrDFRRJi7${6lX0Vr97}pxC6~2FQ*OSRHfHLd zE=JKFDu~4P(hBFQ5XwiNMn@iLHHoS8VLJgwOK73u#dlC~6ZtE<^^xB5!zSJVoYRhb z9hk6=V+&ixd4%7G3{)6=<{=R<|QB@oA z(}IgCUS(1MX-;9&t{Ae<7C)6+dA9DQSIpK3=O;McG~veHHyW=hx+h{mar%*N9hyTL zWRvd=fYaR-^toLSyfyn<&}}j4VrFihZw9?WI8)IC8yh}u_-muZIVL9|`G6fs?sVuz zX>7=k&AN}bmn^6qa*11e@kbW)VKahYXA%fbOrQx-VR3T|iX;I=@T_hhd$EcmGhtxo zocjYe6ko^%I`kur8IK62QX(SZsooKyLSQz!)u^0w!X%WMN%R^U^qVr?NBg$(FA8DA zm<0K%91?w51hoyN!P&jiY5>{2!t|Du)&uW#10vV`uzhN=?Uc|xQR``tCp618+>}x^ zcjR?1uSRkQ5^JSQE7HCLN`9?FqX8VrkQ=NJPLS8B?IrHe3l#P445Bl3Y(yTtz% zHayIrrzC~JK!x?0d4YpPCHZa39?kWH@RcY6W4~9*0E)1Hl=;g)VVXb4I;EC^iR_z# zMU76vU?k^aGEnhhcS1c{m4Nj+X-mN-j2>FVIpInt+zcp`(0G#sc;Vh~iNq1mPJLJW zGHd%|xX3Lm6F}jx%DwNU@yAeu+kVyht=|@U((kagD!W{tRBTYo00AAcxz)7R=&6<4pZjpGE3}ZZO~R0 zH{wsfBomjx@xtc|Dk;3u4VX@Bt++k<+}5l+@Widu@E#h1jAhzr7Yp}ber%60+*8P@)J*#eMDD8Q|#IdZEO$1PIFzf5PE#-9EzURmn@Wjs-K>6d-AV>HQ$iL2+ceJ`>c|f8P;)g~R$P0!doDyK& z9RU3Qf_ zf`44=4{SzeFmTZSZ=ChJxvhK~UOivGDNnZS*+aLpG4%SSr&;9}Y+mPEe|+t_OMx`c5&*BYMWj8Y zL2EkQW{|`V`^?fs+TJq9H8k(jlss&lW>D?IpTF`^=5Yacn-~VtLTQuQrab zNVo`N-`i?(#Hd-W?m3ZS0g+QrI;=eM#imJ#d^~z8m4{tG7i%4Yf;XGN&0t;5EM)!j zu69i%rIlWw=cE0KvDh^o6rwX}8~;bNej6N5$te3%+`~`UeEwGZ`eK!T2~zg?mSqLx zzX@P2Bl6F|`23doQzT?stXX{v|2Hur&T7*%C-E<_SMqsPi-La>zuknl99h5?vK%{f z$^qc|!_G#XRZjgjKKW}_5qIJ>Iv!|4>NB2m@(VonPhE4XdX#)lK2__IZa*DQZAz*x z087B}@XG>=bSmpgOQfawzB3y|eb4qLifIfN6lMR-g9+g|}gbHiMWkoVmNh7{lHTvKPso&y|t`-bZdqe6>n9 zN4nzsWCVM!)8CuVp*^&V8_Cz8I(06C2`%G@vp8^jO>ii1w)Zscg6gVrnPAS6o)yXO znrJD@_`GxGT;r{?d`rvuIHy0Jie6VnBUaJ!{ z%gf4+mId&VnM}#H%2TYyKht%+V|Ifb-%s>YT`({cy(Xl|S(Xn@HQgc$+Z*UAf=awi zdti0*qaqeGlpoc%sUTetd|ZC)xtTerHE`x*HeZZW`eH`-5D_+;4REnp^!a`nHz7f& z*78>SWI14)Q5KGij z$7XXOt^u4B+s}dCpb+3puNTF#0A5x9g1)^ZEvKxhDSbt~NhKs-20*g#VLwxI}CFX?qdDQ0qI>^3ZIPDLutZdTkcgTGd~bq^>SzhlXWy7{k7(?A=Zm zl8svNfQ#6~x;V70#ax6<<9Y5?;l80sPLXI=c!GSg0DL2JeQ2Y9qF!q&h58T+$_<&5mR z6)n^LOxx5nSA>N~E2ma^tBCmV#QYBVG*8v%iNtrJoV&gze=S+vZ*-jLwNgeuylCL= z^R{MP)cDM=Ae6|L&vCiD)+D$*`jZ`lXaeBsS|7zzeQ#lHmECFWZT6TW)pSG-Z0&!} zW|gjOP0$s-W%{Vde8|C-6o$v=n!8S}xjH-EhD*+H)~GJrF$HZJ&4z!AZdKK^ zN>>aCdFNv}gnTtyb(UtA%jTaUtaP5)ZkUvL8Mm){jVjR-u5>!}ed`_H?yc)6424RW zEznxkONXMpM%A2Q!N(9znN;!som&(iE_r1~>&+46>sM-$l*FiNU&pFCH!o(o2F=A> zwpyC580pGz$+uNtQ6x4HljSH2Vq1 zD0CAEk8RaDGwg1<*FImw6`YHXYD$nZzrvXuiDEH5Ntx7>a|!GG11`kAX53|!OaNEe zTejx)jqf?CS4O4Kt0pnIyAJ>T&w}&c`@}Hu*~%Y2D}A*-kD~l*vcuKbK=E&Lx@XDb zHmeLULzlq!a3Pn`d>I_yri;GHIbkrs^sVst(Tvo}iL;Q9$~@l|T9MnQ;GJl@EDsb> zeE5*Uy=>FeQ6M~O{&VdAyG;;ZEHt+TuL zMVemVbeRJenPpP9!B8Kba7WNowGg=gu`Y6`2gvttPgxNOkDWGk<>y)K2oV<9g;GTC z(ws3K5Tj5?@;?zzRqd=aJ`dZ43r#g4HS)}4#7P@HSi7o+baWawGDN#_2Et9TDJXnXjSU-u zf+XHT4ZNnt4}C9t7Rz4s^TB$JUh*<&hO5Pbn!8)-%*r;9?178uXGvtcQ_k4R6VTFj zZT2dnRxnAF7mDm9yH1-S3$L(mg!G-vBk0NL7FHJUohZ(Y8uKoX>w;N_vjEN#`$vwe zg@F5|Vw_(p=rZRKfqn@!5~_N7eHqD^ICB z7lCm0cMucnyxO$}U19}+o86aG8NNsI?>_%^=Ktx;tOxkBigEsP8T@aRy*5rt`UY0U ze^hR*CI0+0lh1N#&}Vp{hgym!MA0yu25A_lS>bpu0BLzNL|za$Yjt1U1P>v9^v1SI z0LenGoWOLhx!qaeh@&wWyDEny8it6pg0`)bw8G}aw{P0qsfhhDW4|01-EM9+ z^>4|+!}HX`B;)eN*4YD$QxW1o(Av!V3B_y)Y7Z7MwIl~`N}KYumIhD$&^D_a18#vH zQLVuRKNA%1E04vQj0L@|((Ht)@^)BZ8V=Aj3Z><-r7`_7)gxlaAytXV)V2LdN<{+c z&VRtpx}wN^=k6s;*a5D|`NKT0N9k@bR&x;BM=3+mZ#XjJ`ywF^H#5=!B4%lX+Hjt) z;B|TR`Sl|$@NAak&n&}9QTy^d4MSQ{C-!7$JBb$T(_F5enwFEMTcI!kNnrh1J<%)oIl|DNtq~l#WU3 z$=)4~WZyH!Cp*S@+D>{oe|||OhWB+X+FiR4Q**l=%SS7Ei$&W6uX*{eCezj>jvxC+ z&%x(W#Lu3C&qf0|2U|NwdP7@>KP~376ZQWZ4L%>bQ3-!*H1M`4-7ltqMXE*>6GQP| zYQ2|(3AbKW6!^#ypDC#V2+yuQeDv_*8F*6FCe1}Cuy+})i`Rox+i$3ir1Z>MFEXVG zr>IIvu4gc&4m<1M=B7xROoq0@&kFSeGZ2trI~bK+#x9ur5_B2c1q*}uwoDtcf>>%v4pT(k9#KcKK0BRK|01w zl<(Ezt8V3c%O&|D9_*F*ia2E~r$O8R=Y5yx6y!ki!?q2TY~s&~WmxJV1Fx)zSr+8_ zYZ7$wRs?$MOwYsSkh?bj4-OM%LI?YX%NE9?I>rpJlH~kY6V=4ZB5fLTnwOoJXU4OA z_W!yZ5RnMu==y2Jqn{5yguh#{zMb74kG=mh;?H6G+_Ds;Wk1`AJBhFH%Qw{9U;zbn z$XJ3nu1Fhe9FtnCg))T1zn*RGDyT%X#3#O4b$1+su;FKqY%mI)pK<0+MsN#KXU{aL zA5PAXj|k@skW?ZKs%ss3Mm>e#} z2=-}dTZS*nbUB+LYoZKb_QeLN+>?`bd`Q2P?39jUl*@q#_Y`1PfR&@UUB>X)Mh7SU1@5k$I~zgMdUVCJ6Pe&@6T zi3V-=B2&xJNfM9G@%Xd5-YgmcJn3k2-fRwfPYLCkx#R=AKP(_5bOGd#=FO8q7zs)k zn-QMs;BbgDaKEb4jvAq%!LUc2-TLY^=kBn}_|2{UJ(<#`TY-e2*&723R|WqBIi(`s zBnBWmNqQuyN!j`)e&?en%_H%