From 13b3c58ec884866f1a36ce064b1c19eface307c2 Mon Sep 17 00:00:00 2001 From: gilles Date: Sat, 6 Dec 2025 12:28:55 +0100 Subject: [PATCH] fisrt --- .dockerignore | 36 +++ .gitignore | 37 +++ CLAUDE.md | 122 +++++++++ Capture d’écran du 2025-12-06 04-55-12.png | Bin 0 -> 167433 bytes Dockerfile | 55 ++++ Makefile | 79 ++++++ README.md | 270 ++++++++++++++++++- STRUCTURE.md | 260 ++++++++++++++++++ architecture-technique.md | 17 ++ backend/app/__init__.py | 1 + backend/app/core/__init__.py | 1 + backend/app/core/config.py | 111 ++++++++ backend/app/core/database.py | 47 ++++ backend/app/main.py | 187 +++++++++++++ backend/app/models/__init__.py | 6 + backend/app/models/ip.py | 82 ++++++ backend/app/routers/__init__.py | 8 + backend/app/routers/ips.py | 216 +++++++++++++++ backend/app/routers/scan.py | 201 ++++++++++++++ backend/app/routers/websocket.py | 35 +++ backend/app/services/__init__.py | 7 + backend/app/services/network.py | 295 +++++++++++++++++++++ backend/app/services/scheduler.py | 103 +++++++ backend/app/services/websocket.py | 125 +++++++++ backend/requirements.txt | 16 ++ config.yaml | 89 +++++++ consigne-design_webui.md | 27 ++ consigne-parametrage.md | 21 ++ docker-compose.yml | 40 +++ frontend/index.html | 13 + frontend/package.json | 23 ++ frontend/postcss.config.js | 6 + frontend/src/App.vue | 48 ++++ frontend/src/assets/main.css | 147 ++++++++++ frontend/src/components/AppHeader.vue | 68 +++++ frontend/src/components/IPCell.vue | 87 ++++++ frontend/src/components/IPDetails.vue | 207 +++++++++++++++ frontend/src/components/IPGrid.vue | 79 ++++++ frontend/src/components/IPGridTree.vue | 129 +++++++++ frontend/src/components/NewDetections.vue | 119 +++++++++ frontend/src/main.js | 10 + frontend/src/stores/ipStore.js | 230 ++++++++++++++++ frontend/tailwind.config.js | 26 ++ frontend/vite.config.js | 25 ++ guidelines-css.md | 17 ++ modele-donnees.md | 28 ++ prompt-claude-code.md | 24 ++ pytest.ini | 20 ++ start.sh | 69 +++++ tests-backend.md | 14 + tests/__init__.py | 1 + tests/test_api.py | 123 +++++++++ tests/test_models.py | 134 ++++++++++ tests/test_network.py | 98 +++++++ tests/test_scheduler.py | 76 ++++++ workflow-scan.md | 14 + 56 files changed, 4328 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Capture d’écran du 2025-12-06 04-55-12.png create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 STRUCTURE.md create mode 100644 architecture-technique.md create mode 100644 backend/app/__init__.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/database.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/ip.py create mode 100644 backend/app/routers/__init__.py create mode 100644 backend/app/routers/ips.py create mode 100644 backend/app/routers/scan.py create mode 100644 backend/app/routers/websocket.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/network.py create mode 100644 backend/app/services/scheduler.py create mode 100644 backend/app/services/websocket.py create mode 100644 backend/requirements.txt create mode 100644 config.yaml create mode 100644 consigne-design_webui.md create mode 100644 consigne-parametrage.md create mode 100644 docker-compose.yml create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/assets/main.css create mode 100644 frontend/src/components/AppHeader.vue create mode 100644 frontend/src/components/IPCell.vue create mode 100644 frontend/src/components/IPDetails.vue create mode 100644 frontend/src/components/IPGrid.vue create mode 100644 frontend/src/components/IPGridTree.vue create mode 100644 frontend/src/components/NewDetections.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/stores/ipStore.js create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/vite.config.js create mode 100644 guidelines-css.md create mode 100644 modele-donnees.md create mode 100644 prompt-claude-code.md create mode 100644 pytest.ini create mode 100644 start.sh create mode 100644 tests-backend.md create mode 100644 tests/__init__.py create mode 100644 tests/test_api.py create mode 100644 tests/test_models.py create mode 100644 tests/test_network.py create mode 100644 tests/test_scheduler.py create mode 100644 workflow-scan.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..67637eb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Node modules +frontend/node_modules +frontend/dist + +# Python +backend/__pycache__ +backend/**/__pycache__ +backend/**/*.pyc +backend/**/*.pyo +backend/**/*.pyd +backend/.pytest_cache +backend/**/.pytest_cache + +# Données et logs +data/ +logs/ +*.sqlite +*.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Git +.git/ +.gitignore + +# Documentation +*.md +!README.md + +# Divers +.env +.DS_Store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d0e7fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +backend/**/__pycache__/ +.pytest_cache/ + +# Node +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ + +# Environnement +.env +.venv +env/ +venv/ + +# Données +data/ +logs/ +*.sqlite +*.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store + +# Build +build/ +dist/ +*.egg-info/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dbd45d6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,122 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +IPWatch is a network scanner web application that visualizes IP addresses, their states (online/offline), open ports, and historical data. The project consists of: + +- **Backend**: FastAPI + SQLAlchemy + APScheduler for network scanning +- **Frontend**: Vue 3 + Vite + Tailwind with Monokai dark theme +- **Deployment**: Docker containerization with volumes for config and database + +## Key Specification Files +Speek in french and comment in french +The project has detailed specifications that MUST be followed when implementing features: + +- [prompt-claude-code.md](prompt-claude-code.md) - Overall project objectives and deliverables +- [architecture-technique.md](architecture-technique.md) - Technical architecture (backend modules, frontend structure, Docker setup) +- [modele-donnees.md](modele-donnees.md) - SQLite database schema (ip and ip_history tables with required indexes) +- [workflow-scan.md](workflow-scan.md) - 10-step scan pipeline from YAML config to WebSocket push +- [consigne-parametrage.md](consigne-parametrage.md) - Complete YAML configuration structure with all sections (app, network, ip_classes, scan, ports, locations, hosts, history, ui, colors, network_advanced, filters, database) +- [consigne-design_webui.md](consigne-design_webui.md) - UI layout (3-column design), interaction patterns, visual states +- [guidelines-css.md](guidelines-css.md) - Monokai color palette, IP cell styling rules (solid border for online, dashed for offline, animated halo for ping) +- [tests-backend.md](tests-backend.md) - Required unit and integration tests + +## Architecture Principles + +### Backend Structure +- FastAPI application with separate modules for network operations (ping, ARP, port scanning) +- SQLAlchemy models matching the schema in [modele-donnees.md](modele-donnees.md) +- APScheduler for periodic network scans +- WebSocket endpoint for real-time push notifications +- REST APIs for: IP management, scan operations, configuration, historical data + +### Frontend Structure +- Vue 3 with Composition API +- Pinia for global state management +- WebSocket client for real-time updates +- 3-column layout: left (IP details), center (IP grid + legend), right (new detections) +- Monokai dark theme with specific color codes from [guidelines-css.md](guidelines-css.md) + +### Data Flow +1. YAML configuration loads network CIDR and scan parameters +2. Scheduled scan generates IP list, performs ping (parallel), ARP lookup, port scanning +3. Results classified and stored in SQLite +4. New/changed IPs trigger WebSocket push to frontend +5. UI updates grid with appropriate visual states + +## Database Schema + +### ip table (PRIMARY) +- `ip` (PK): IP address +- `name`, `known` (bool), `location`, `host`: metadata +- `first_seen`, `last_seen`: timestamps +- `last_status`: current online/offline state +- `mac`, `vendor`, `hostname`: network info +- `open_ports`: JSON array + +### ip_history table +- `id` (PK) +- `ip` (FK to ip.ip) +- `timestamp`, `status`, `open_ports` (JSON) +- **Required index**: timestamp for efficient historical queries + +### Important Indexes +- Index on `ip.last_status` for filtering +- Index on `ip_history.timestamp` for 24h history retrieval + +## Visual Design Rules + +### IP Cell States +- **Online + Known**: Green (#A6E22E) with solid border +- **Online + Unknown**: Cyan (#66D9EF) with solid border +- **Offline**: Dashed border + configurable transparency +- **Ping in progress**: Animated halo using CSS keyframes +- **Free IP**: Distinct color from occupied states + +### Theme Colors (Monokai) +- Background: `#272822` +- Text: `#F8F8F2` +- Accents: `#A6E22E` (green), `#F92672` (pink), `#66D9EF` (cyan) + +## Configuration System + +The application is driven by a YAML configuration file ([consigne-parametrage.md](consigne-parametrage.md)) with these sections: +- `network`: CIDR, gateway, DNS +- `ip_classes`: Define known IPs with metadata +- `scan`: Intervals, parallelization settings +- `ports`: Port scan ranges +- `locations`, `hosts`: Categorical data +- `history`: Retention period +- `ui`: Display preferences, transparency +- `colors`: Custom color mapping +- `network_advanced`: ARP, timeout settings +- `filters`: Default filter states +- `database`: SQLite path + +## Testing Requirements + +When implementing backend features, ensure tests cover ([tests-backend.md](tests-backend.md)): +- Network module unit tests: `test_ping()`, `test_port_scan()`, `test_classification()` +- SQLAlchemy models: `test_sqlalchemy_models()` +- API endpoints: `test_api_get_ip()`, `test_api_update_ip()` +- Scheduler: `test_scheduler()` +- Integration: Full network scan simulation, WebSocket notification flow + +## Docker Setup + +The application should run as a single Docker service: +- Combined backend + frontend container +- Volume mount for `config.yaml` +- Volume mount for `db.sqlite` +- Exposed ports for web access and WebSocket + +## Implementation Notes + +- **Parallelization**: Ping operations must be parallelized for performance +- **Real-time updates**: WebSocket is critical for live UI updates during scans +- **MAC vendor lookup**: Use ARP data to populate vendor information +- **Port scanning**: Respect intervals defined in YAML to avoid network overload +- **Classification logic**: Follow the 10-step workflow in [workflow-scan.md](workflow-scan.md) +- **Responsive design**: Grid layout must be fluid with collapsible columns diff --git a/Capture d’écran du 2025-12-06 04-55-12.png b/Capture d’écran du 2025-12-06 04-55-12.png new file mode 100644 index 0000000000000000000000000000000000000000..bc0e48b1dd468221d804d1b1a16cced54434949c GIT binary patch literal 167433 zcmeEtg|*U;PoZvYXAW{Uwqv1S$_uA_&|Aw*9y-`+eRx08( zgKDAdv7Po3ylyeI=c8=e*pV6iOE-)4%ljrpTF->cVT{&9x=H@{8V&-|xBm__iM|%N z|GgOu9L9M4-yu)`-U8?O-$O;hm?P-(e+MplBGJFrNy=ZwDE@1lAKYVx{9oheH2Acf z{~FWhv89Rs?>-^_|I(l7^XRMXLBK%ubD||sizh@`>(3w3v_SgH!N4%Nc$snpDKaKd zQ^3UqUvIs_rKY5io8v%>15uV+qe*a>99kRWa}k{(89^AS(OM8?CtN9rmJ`MfR85v!&{<_wsEPLLh>F1*s#b1Y7xAcOnP8mI^8!D*msD7i{S5o@; zE%HcvA8w46Us*X$5UxBY>8L#EOX1YuEgn4@HISK)uF#I1t5IE^>uo4VT?uXW`*kR= zWG})y$8KTWqG3al&TeNXKO=EFZ_F|ir8VB%K7|dkO1Gf9ana|^NsXDwv)-5qz|x-? zck5p5upmo4nJ4hm%ba{io~oQ#OFDN8z6knQ=vgGH6rVyAn{a|$6!n8oUa*f@t!#&-!WsGB7hwSE+RXX~YSdH>?KjPB?H!cOtic0GZ_U#?<8Lr)Q zWbjO`ZRx4iV9AB$zK35bZWcr_(kzr(?!-d~M}p$}%k`5)Ht(-zjNe9I9)CAj8E2a2tI{LTm`Wrd$jaPCJD%7(<)KPblJL#2|w93uSz4d@1&nw3!J0 zLlu0~MGqD;A~LB@Ox_g1h}x&XSrH}&lJUYZ_gc3p6lO#bc!PFUt&v7I|c zibGs+9mZ?7-{DAL34Uu#fvYmDm^auqoA;?R$vpe4i1K)wN_mNwZFM@>pt$H7>vwNO zXFu3^EL)p@@Md3V$20G!B^m0G^QS5dx)w89dSoX&T}VIqdYm5nIj+9UOE(u?Y+Y6- zB}2hMud&?Rd^xxz}|AXLPlDKJ54 zsW=z`wbUZ2^feAD1rCPTJQrf1A7YR=@Ukk6F`9BHKgOmY!~kBzr*NJ8hhbky4n_7& zKitKOgqmxZoLXgv(H)N{>dNVQiw9rM%UYF+b2CxOg{rZnsc@F@EHxYlhhmZx$S2## zDk^>&gi7^CB(I-(k!l9?R_cTqdb;aXGr6AE|4CNaAvl4(zOgA^PPVx$i!ORJ z`i;H&t>_OcwZhCnS<;bzrjg>ii@O@l5SQH+n?b%x4F1NQyC91XGcCk<{3c_J>O~NF ztx+(TyHY&_qPnom^H`##nGq%WVSAPq9CFkoV6IOS6jN13Rn>UNhaBVDW01Ir3~N4@ z;r)|7Y9N*PnyD76ZUw8WgTqpvrUsIoyxL$fzz02qs(SfvCsVT&8X3|DL0J=&y)Eb8V8 z!y5;+Z%yC*zsVmLA2c66PaqkTi0~t#aPg6NNjY-Fxj9zT8hBpGAYoMZ4A*#`C&p2q z;=n`h`L>K=PZLw5OIwf0DA}{&7{`tq*}>XBEgD6d!P_DIDK=_*o<)s2-`z-K+>+3f zURB3u;9F-6%Xxu^w!KU#9B(cq-qG~y$wJSgSdX1`H)H|?5rVC_hK`+nQl;VcNPxJQ#) zAgIeb-iJ?MgTD3P&UdZmaA_OSC=Jz25J;tB8W1oDFxcj6@eLDQI^G*RyT9LMMBB4^ z_0Ea@Pl@2ehm?vBP6NTBqD$oCr&)8SSq9I1V>M#jkuser+8N9%W_? zE!HED@RmzW9A(j+4)s?CD_9g3Yl_|9L@Vb>(6}5q@d#vjzTn`rl}It4ca_f>;@=6I zbY^s&p|YnFTHbqrz1X9O~0pXl;7sMeal|QGE&!T@30`3 z0=-H~KlXw2GI$2x$4>Cc@r-Vdw1ui3)-RVX(-l>{y8-E|<$W6vD1}I9R+tm3&+>JH zD{w`ag`l=sRXS&`3XPmy=@4->Zg2gS39^h>i|bs7t#WiIVdaWIwg6~_9<44W#8tvR zi%t6&(?w=DuQ~hv-M2riZ^qMkl)SiGQ6O#jHDy$UgB-4#RDCjj zP+A?w9D7+=FRpS;JT-32TYMuH-kt8WKF-$h9{Jsn1kZtPh76{tBr){*>3IA^34&FP z-wGVdHgwz6oXr}gt!`F+!DfUb26t`-Y`lKIs?5?((~UYCrd+JD&+41U-PM%iVqIYx z#?w7WTD5L8Zihc(?r$&2cIdbqWwq1fC9>pP4;GM84g7@n=N*FU>Rdd8va7aQAO$5Q zsLPEG0|S`0)yBQVFCMcrNA$jC9nw34r??kY1sDmh ztN#!TbD%}iSLXZ$X7tl*l$FJZc&=xn$js_2Xz964Ykafz7K~2~u}Eccj?^-J z(XdunX!fH*4Z9xpL+0#--Bg`6qnY_^ga@T4Ax3^v6RNF7Wfuw5ub4*l^Vs2{V%h!YAAI zjUco<)=!t>89Er-yT6kiJqF+pA z-+B+{a-(4?AGVHg>&l*!Ila+vxSW_>*IvDP#_muWEH&QB3O zmt6|7w#OH@0sp3AI$Tp#HL60FC*QL2K^Y&Z-gZ7Hj+)7^1F?ndY=T6F=4&$1;-Z4% z;x0QV;Bw8IfP|+!+}!yeH&J{xn#wvrN8V{J+WF@rjB%^JeS-Q9ksG?ij|ggUEfWWP>q)8n=m zBIFL8^8Zbcx&b=+Z+7DzLAUt?GU3}i1sE2){p@tOHV1Uc*5T7UtUnORIj_{FJpCV0 za#>0jh6w`ezQ<4GW`u;|?btDa(_40H<1Gg8bP=o>6p+UGz-o+4NcrC?5=gd6kI)u< z-j`Osc2QWb=3Rv-*#6YsWFeN za#$k+iPhT>l^Av!N9p40H`8&lyy7QLpf)~U)%&$W&T{;}ORFI2Hh!opww?b*-kb7D z8)zut&QAjDsQdljtK9#-Q}aTLU9GS_XPFy@u9MkT`dFMfQd*ieD8SR%pJ`J6UoTbg zVRNAa_2uzk#!zPd8Ycw{v)OVa?UX!gVhxC%PIfEpKQ>E0Cusj&rP`#3u;JHG((j+z zv!z6F)Mz0gNL$Uif7{%Ut3&h%n04!=dP5cBa{LKJa#5E<+Lvlaz20968V6DA7=zo6k|@0+~ogjV4}Nl$-UZ z&q&B_x74&7nhL`A{0uWYQfs`#lvTAdbn}K2gQrII&hG!rT^vmziy8|D(w`rFrAaD~ z8>Xb|*aFEobMbD5q@B5r8$e(JF5~Hz{d-f~n8W~;z*6AcKd%!v#f zRLe8)z|FLr4C*DQatU#%jh#a7@BU8gAU!uXrhaR7WF$)m?&9M3NMDz*XuP0vZ)%J?&z=X-lDSo1sTga-d{l8bt^*o#N zI0#P#mXSHCN`qw!1a@-kOV7j2v z+_;2sE3d=PDY`&uBs{%|#UEL!|Hz}8%!%X|rYsNSN zqKJb}aY*?6cqvzL=2R0$A|^C)nWG^Xlo_Mt)^cJ)7B6ur(_(v0cXtzBj|^y|krccO zkN1fWo&{6?Fz;JD?9;Ju8DvXY)|WegP!7}?*rcs&^zF;T^R)z88PBvpnkSqGY)d0@ ziG#mdD+h^}q}#Y$s|2AU2TgI0A7SC&pe^YK!hZq|NO*`B1VQQnBluiS@ouNq8mUw{Jqf-gnqM!3^9 zRH9Z$7dk8dHxn{>)-x#{Za+y*JldvQNw2TN*ssgbzWljHQ3jv$HI`Blaw(?!($&@D z+qOEp8@nH|+})iqfy8zuRvgD&8(#~yzQOMg>J7urlZxbN+M2ul_~^?!ct6ZeV)<)V~KArTyL0Dw*--M3+Br>4nHxv0g32qLFuotf(IejP|?ALYD2H!q1mjCR<*%b5Dt> z&%YV{>H5=qZ&idm{#2(qFf9@>GzHadrA+xf2QlKpJgsK3#x_3)FqsG|{lyx}U*&JtMXlb(6*Le&`V|vq@{?F# z_9?JO5WEBQQR_W+drVRSv^eVU;V3poKeqq+!;6Fz1_vrF&Cg{M9jM4to4%WuwCZ3wq;Ac>@7S49Ee4=XKbAmwkW$%Ku;F%jBn z&yV|2*@-xVzWvwpt6b{=4`hkYO`QZ(G+*;KU+=bHNvW8u;0quhSZv}DgOWO_pLoIN z%}?BXR3P?d48Wz;>WeK#a z$Cj!PZ6sSU%0(f*zx~12C3v;+oW?2>pnwtGT3SR( zq8x>owC8ZG|HcqS8%y8n{X3;t<9CYWWVNarF7fFm)SO(*9fL*`K~;WqeV!ZzddJbG zV2eyeZP}+>mhVA3nx__6SBK92VfdRuig{sCk>n7E%{(Mbd`CY(r+z=zs~$sE!e}Bh z?aHpZO^f%eP*z?JZoR3MroVpb+RMk1bA&(5SV+g5@n63qa0O^&*;^5&!e<-oWQ0!E z)qwnxClL|>48Hs7xw6*S&ABZoGkrMfSD_gw0H0VVVe<=dhq3oU#1raX$vi`cykr^BtUkPP^mukYH@{ydmsD1a$j-8q?TLXx3!mE z@xpXua}XU6zGMUNGZtufx`13---n=vgtxc5O=v9ZR#@cbqmLd|jkQ0d@JfWAHYl!q z4Xu2vYR|$VEcrj}{j_^=ybpSs`RCrVUCv`~>vF14?%mbBh-^X^(AO%D>0^(Bc<+Nm zFUE`2+w3m>oeOa}Id(3EAb@FUa3kQ+bvO)y%d$U-Q3TYsocA#|u7+)HN?iH+J&jL5 z{7}>5C%UN$Z&qhP2BBNFyvo%}B7b$j1*SHu2=-8WcSChf&r6Ui+h8Q&K|+B885Ey(NzN zN$~iF7vKfLrh`T^UtOPY1Q(`4dR>?3u`dQdiMnT$L;?^{E{wNEiU==zbRE5Ui7k7g zHgim}DhEAj6s}j(XUoUp-ujT^|BW6qA*CtDxd08T55s|1EA|PECYd)rT%J1+*3b{$$jlgT$7ge+8>GMZ^iXUvfvxB_=UOrY+Dovuf53KTf3 zojGoSc}*TWgonmg!27eX07wP_Tl{1=*7hL^$ZT$?r%!3EH=UaiK(2=@e3GFtf%C8s zCd8#iQM`6CG+vglN|0pUvDJi{aXK<+_Lrq zy90!MIT$&ZFo}7Par{hl_4I;cMSTz?P+g9^ut2%)IiD8Vz3ILxUXR7t0#Q!AG8+!W zSL^Ivn2wNgwA73TI+-aaT4;1oT6kdT^ub7B(f<4`ieINDLXjY}c)~2|GCW3JQ=6i(-gx_jYFYz4=vj zZcbU?o+Ezc`tS}#;DDR$y}i~1J9_Mbo*K;z zxO%KVWT*hTf_|%8uAeZ~bJcboS|Kv*28XpM%WOX!R~S#gAhbJ%R2OKZx?PU~Cio-< zN={5L%3}ChR0PfT7l{Cry^XzMvfxQe3|X(MDS!(SH~1BefS*#8WZ2osE|q(M+o6}N zoLo0VUr<>~>*MRy?8Lq+$%%)G zhK69d!7daBK34RagU4PgEHJPa0QgjZgcH2D4N7sgd<-b7Y$wX`;UNuBL5i=nMS0-A zkj|X`_Uwr{``%=S0UZ>dkK*C|@Pqn;VJUIn{39<~0ZRvZj{hk_FdOpJ*!W6_Ba}=! zabSG9LRTqswgLqp$Tpo9!15RbVxHp+^JUkuQvc|qFD?zufWCy^wdV_#j$bx~UkhKm zD`5c}H^{h`wR`f+!g#qs!Rc|K`#g?~}j(I|V^elXgoFh;~EfxG%d{x+!BKNF6ePHdo z--`r)pJAJByU(z>jubydYS4OywIKy#*$b}=GLXUyo5KmrpBI4k#r-u2kDX3rR8+dvTscjR&D_Anq3%hSFYhw=MVZEZ2*ffgXeZnM z84V4M)f5FutNU?ZT_qhH2t5+L>Y!3I9$c%~Cm%X*==(xR=$zr{QEanW|#Y+e?dALk@n_CgzzrU)@=`sj^2?aQ;Vwqd;s)h z!S0;=*Fm3b(nF0X0sx)p>Qb_iil4t9;`LD=(;t^Bi4Q}+i3yYr7e_yhzLyzaajl{%jszWP3&(>ELWrQ|r#d>@y!g2>M= zSiL^a@A0^zdAN@W2A>Z359B*-l|FY^X|bAiXo5DBeOx(jghs{af3%Mkp?~wH8&IgK z9icj7+1v;y*eMG8eF^78i5_HMaBzzRmEYTsGsyifbRl%ESt_M!a!{@BF5$k3cN797 zZD)xFXQ78XXSft*-KeA_b^3@vBR15WN6`(F$qAb7bn6NXJHuy?!M=1Ik%35h_)B+mKctKvh?HHcu$KqdY~uJ{U8 zg%8PO5LGtw?A(crQxe_# zZ&e|GVX4-g*X8WHCs0UxqQV5)$wUS*Y4JF316?@7SkH^bTp1*QEzjHG*Du$6fOIx%evVAbIWWFuD0`o0xk{#zs9>=^`!K4EV+b_ z>s=jBR8=|^u)2WXlZ2YW9&?}=C5fw!Y07w7<9BA=| z#Yk9z{)_dX}<&m4F3A4Bs!OcogA%@B~W zdhwLn1JMZ>OnPs)ul~%qKj|d$64V1#I9ocRuRk?f&^+BgLMD;c_cM$Bq1SgW2eb3Na~aU6*SxkweD=Ruxd+uKI-5K@4TEz~=cs4E0P ztfyQsTfR!ps#~Qa>(2vZm`foH_|SF52NL?#6?4kfnM8ogTi>*~^jiQ#avn}WbDYq$ zx!RYsv{XV3j5rHY2Xp%e4&0QYE6f6Qbz5lLv|(xaDJn&s=vVyIW>U;>34$*M>yi{ZOvxHuY+m90`8p|(^2S0^D`iG~i`+GFD+R_1wrTLf64~IC+*g-;G zsyA8-Mrlsm#*6qMua;imod_Zty%qexH5!27pd@FxqKHTr?-!Il>iZ znpNy)PqRC{XS@27{PfnLGZz49gnSs*Gd;w!jpd(X!@GRU>q+{^xqV9_)O6DRC?9Nr$%C}Y?M<LyLU4 zBj|j*;B0VMj%vSk0&paJXU<9-ZdR4UIPuz}vL&S`H7|xiu&o5XQ%?^ zA8CI-v<*32(;g4|hsk{#%4dtarN&CnT9-p-2_$h7*bM>@1*KoqWa(A6ILjvB&k6S} zvaEavToEaF?zVjJE2iKcx9|ipmWbSOshNe^%EFyYf9Bv9@!7ydRsD&}8d&_&?6lSyVJ<5n~0Fc~KmI~LZ{(vwc0K9dpq zJXI(Bxv`+_dv?mJqLK+~**u%~!`kIG6k= zdtFy_shXS%Q1sM3yLONro9d@Ha{w0x1aFo6=Moe&{7A#Y-((f##BJo!+lGf_733tH znmtz0Hddq%@VeBQ-sLPq~7++@b8q`zu${nD0gkj}qNBeWtUwK*f^u0e!5=QUy^YkcoBFDNqF$B)C#3?^erz=H zWE1*WG96{-U4dvwi5S>TK`j@UPiWW<;9Dd22}LGA$=M#if1bfp0VLjLc6)YUls2Q% zR62&EP-#km#iBkJm-ljG!2f6XQa!8(rVv5c+gW_7kU%|Da~d)QJq>0q18{q(`nL!` zr&<3!ZK|xU6_9G+18?YaD#w&AUrc~jgM-skUJAH8jWx6v3DkN#l0rUEzoP*HBH+fO zlt|Vbkc*91u5T#oZK`3UhWnDdC_zY|tHGU$wE))b`G7+G;p| z>|Qa68co2()NrDdU5kCrdF5coqQ1iA=QQag)YR3k_G`4vxV&n{GNx<%GuN~kS5kp; zdq?TQc%gcRA6#N6T(vjvXIu+xv+Z5W)beZus9Q6#EQBP&3VZ#I%8k^x30jZ`>#!7n ze+_h~bEASWC3uUu$0KHPONqJkL%!N5g9(UR&-#UFIbW|=Xah+)zYGvXO$SASJMNJO zTi&QE;!BD5%O{^6hvi7F`30T!oW%lS5O}M>pAvCGq?osj%)#m_K5={DpR=jqY{vv^ zcLE6y-q*J>T6B5aXm#3h_{6FGLTL@D!{*z9N_8Pj2abAXlq6^4hr<$I=oa(rwCP6Y z&>GKzSC&KyPdq1NhMOG*Z9h?mTD$BwdjS>(KfZIJwUFK1qAx&=bzYL_%R!W4 z_yj+6v)@al#yk>lhp!Xfo((xZ&uL*vP$727uBH=f_rg3K$jC_o+?2svygSo=Kx_Ux{6ShVsmkg!yVX`K_V_)oniHE7zdAZK2C!{$zS<)v6&e z$=_7VuC%a%UqNlMh?vH?Na$s6H=mZvl%>VlK=Ked*?d2vpUSfGajm_vuZ75*dc+&A zpf&BUSNzwHLOJKQ-}@+#@F-Gm{PM-GM)L;PK5)?E5>^Uv@OU?eF)dHInRH=r5F)4r z`mRoU*KVS*x3C75tmqefq-g?y-F-jO(ymcLb@3K%R13yCJr`>zk6>Se8I4^ZbIO zuht8`HJ#qPi%YCM{rPKeCjQl%!`JURzXc-NSimK~$T4N{+(zBFeBWK0;0|#I6KD>(w3@J-cVP*X6 zy!4{LxPk(|`5<)x5bB!Ndqz)gY@v!?85f5uco%x$AV_rrwPYZog68vzv8}#AG+7*( zD1<1MH!?Aa6x97uk#qirfpKQeKJb@__#jj{{yfKoLNWNdf``6nU3X1T*4oN z`QXrg$2VHq11l7lT8@W-Rc{UNo*s%Z;GLf=0LRp-_^V0PdoH>(bczsv`qFkqe7IS( z%zNs$d8FfsQMT!_+o4R~vaId)!ix*DSk+G`;jp-HEFUW;$5gWHLzD zJ`{Yq**2}o;5&zv$7e~kE$e6ph>@31X$VY_SWeH)&2 zTpnt^vb~?bdWiLJ9w?#)_m5Ngb={D5nk?N3>O&KR2tGnjh!&dNF=;2;ySWwP)jqCV zt0^CdMxk-C#yTZ1fAoA*Qem$kUQOe{5TCv0X{PdSk6j`fXZMpAMGOpqI7+rWz8v4b z>|->{M)0RtsdzZ5YF^!i=|NlSJ22Y?S3-6~VF3yBj?5$&y;ALlz0Nre zGSv>9wtDCC3Ljx1wWs}=%T+dKkIe&eI9Kztx~FG`tfs4mtW5)OUA_hLCg&-~>~;@_ zD$pasdQ_C(yc>zfET4&aEru`(sVz5WSp}t{TgxBmN-{iOWM@tB+5C*F9nAAhj65Bq z8^=Ly=^PYZj+Hu&ao_BhTXC#1YPHqNIEW`)gh{pWyP`T>um+tuUv&j3nRIpKd7Sps zRRI>Xs5Fhnm-(ppu_%Cczrz^rs`k2OU@UI4jf+Zp<^5kY03T2aE`!Fjl3|qTV3go? zWaUJdle}K*4_3r1r8S;W#HRJhiHUDVkI%-PiD@rl z;0EPN&uTQ~Pr8g4_7c-AO0P$`KCWf7<%8Kq~*(hUasc& zeY4Hfl?pc$dGh}-7*k!aLwZ*g)VZnt@CPVZHsvRcq~k%J2Cq5t3g0! zyRJUe%cTiLn?PWr{mn~b%?XS7CKhG~v4Sx;4~G%+g=X6VTfOCjcq{M@Zkym1!@)xH zDmP23n^NZDFY+%ZrBsHHEO1F42};dn9b$zZ(qpRhZd;Y+t;33tv5H}=nHsd(K< z29yqwz|pUYhlUAfZ|xA@33(h844{_ASE`Tp1a+@uiz6vf3EfE};HNavI8gr)faa7K z*1yd5YNT=_U)aP&$(%kwue1AzIp8Y<%5YfeQ`h zYKw%6ZyiZ41a*GTaB%jIUwL7i zTe5JIri1rAgD?9Ge12WN<8T<9v@FlYXUk+9 z-yzsAyQgpp%sUPUn2yy`^@XG^Ya_TVBG~e8mM6TWVySx%1_$2|^sp$@ja-AqRisNK zoiV5_l5uq!|I|o-ZMbE(A$F-1Eht6CK}7_}d!RT$IzM{-S6k(vQtbD))ompMtLvpON7g@U7&-`%#kLRo8 zJE=ueb!T7pHc8U4465{Rp5azRMFOG^I_S!)!}}EoK|nMHYdjR1%<8=%*5SRGCv)36-8iGwb^|6q_}SXWeKhuB ze)G;gc5}A!#zgnX0peIef;CtoQn`LUj8J7gFErnDqiwlS+MTmmn>KGUn_>-doMqtY zQK0G=bng@m6rFE7CQkh&H@K1t>HCeDc+%T5s0VG6XBaT3!9c;X>v~!1@G<&~{!Np0 zpB`Y8jO}ptSHkB<)5Q1^{3nUnW*ptEi#C!+>>R(-Wvf3zOUt9Ba~v|=``_Pfdp7#8cu$I=V=rq8>%*JIE z7i}|JY6_bhmz7iK&;Gfhj1NWBQ4t!DYbqBTieO(8vQ9B4>JjK}P%Ix$+^<@;PosK{ zU_OZHm>s1{2OBt>3U9{Y2=*=GFkTBBYbw%QTnGQ#H9Fs0uBq+Fo=iN4W(BG284tgbsgbF@=?=Y#54}kZy@?jM-6x&|*LIe~ zuTPKvit#0>$xZ7+Lk7%^&oK~jl%r)R(Ewj`5MYHh-kCT&xrNqZTM5}L)+nTTKGS56 zcWlw_=#Jz}zfq>+yesvTcS0`(k9KwVnwQax1Zm3kx=MCPi!paE8_IO*pKRUJ=Bw)1 zwXxT;-oah65cN)YVn)L>7f!*P(-gTZ;n^xX}uR^&eyQ`eDyNlNhUvkH*@V8@0UjP&IQYes_ z|4bhWGWrvMcICP#sO1)%xM7Q|)c+fE=48~c@@;gh-@^NYpN)JrPG}$q;T1o8H@wf) zN{9*+m|+s@VW75Jc%cHFIKJT8pWO|3Nt;c?4UxI6<||l~^6+))EOE5tYK51a@IyaV z=tTAYtXpSq zkXF@i*ZC%$S;6&C<29pga;1$ODwkGDffvaPB!JeC6|fmiot*tyiJeG+X>R2`P}*MW z2x8w36+9@{YJ1~7D|EN-zI&lQQC4N{FA;RjhjNXE36D=)H7FEc^Obv?{c|jqzFMzC zcM;6u{hqe~$yrt<OqzWJmRCBbE-J7>d$+&-_q{;#qJX~`Nwk8`<{fJ6sn%&-yE77@~kL5 zwicOkrK*#p;>Lig|0iRF{db4`J)|3c=W`X^@=6GKZ!;X1*=(BwsEv+$n@Oq2 zEKC%?943tIDQAS>o((f({LMBu6wwm{z)` z`p4lI^{}FtPPmL*Fu5Aln^Liqiv8U0%hC&NHOG!V^b8Dr4HX%uD^YK>+P#n(J}eH| z;JgHU52)T9ZW%9|QTP^i_GnAwzntnaK!im_KipwBsNw>k!cR4kX!^nKLI|2ORQ?^` zL6P*cSk(|WWS;0UU;CcS0|uj1!NXr!{uu$OkcPH9u=YJj9_e!kaCf4&Pc)GpE0GhN z?|YSn_Okst7(5*wE35XM%z-$b*S47a32+CXRM0}^S){FAy?nGI^u*!x?7QaaeCVV; z^t+m^(E-h9mC20Rn896?t)7lN@_<1b;g;U4S*``C=0SpLw-~MvaW0j0Iaj9BY)PF2^1zS|^&Xj(7Bx)t$Py8`Nsc zrmL^bJuH{lUNQXKAWqe9eR<>3hMn`khAeo=>>6FB31y1nyF~Kge55R2)@Xo*7HOQj z0eB63*YxpKirrNYH^i%m7Wjbsu4PmIuK5OGz6sr$o4xcBrq%JFd)Lw<9KQkn&Q?0< z@nKRw3Q1)Xlxx#}P%;`WU#8m6ha#L76!pM*fPPNc2$dZg+@W z3i5EB4*<|DyJ*JBMLqp{h-f~?&R+p?^ zRZx&p8790$#hA*X>n^lJcgkc~e1+FuBa43PYevIcZYh0ts<3?Pc!-58Xsp_xtF?HT zvWI11H+pIVAd7NT`kKLV+Qf}h51>LFQ7(4skw?#gzlab!A6fXzjGC^PVmJd4SXK#% zs=(8K>k9G>JD$R19&NxAoo#@Sf8r6vViK{*UN(@yx2UMX1?p*-l-%#jqND3>1!)0-ZPe@dT~SVIH+wsn8d*{vsY zA(Q#K8D*!NJNj{{o&DSp&9K|4&a-4U6Bm#?#fFXfkz~I0$47gK-$$%(iD2Qf|X+ z^$K)^PhvOLOn(2YzVbd(pKlUK%lbGP!pRP8q&tO0%4FnN=qW_tD!oAaY(sqe@IwA-&MxH9YdX8J7Q^m)gZVlG`5j0n}dpTOo zzgeIBZDH80mxI*#ea?LmF9q_bWdR!-ZaTL^bjvw0U`k@yb+O>ZAa!F0w4*-2a775C z9mpyyR8GY;6zhTbmXGl1BL(8lIb7yj)K9_VT(|$v3y}5FML*W78~Qfx_0PcLC=1P8 z-3<1tKVggov>fU4grpvO1K3xJS$WR_|hdInnzyGv{OA34GFvYB!xxXgA1GA31xgT^D4=M4ITuw`+-$EK?mGJ! zxYAOSy#!IVqs6dl_ncdD1VEJL5pa1W-~(9FCkA^$&H!(`(nsk*h{s*{W4`;bxK^td z&DZyi$Admz==TbLP^e3EOng4&CPI(L$ugH$JvT}AJ3MQW$aY&j+r``)uJotxl!zDhdMOM{LVjJyg1QEQJ| zWW|M>+3z%s*796cQ0moxncbQ8eb{a3LYa|y8-BL=`ynUKNk>@d+7V%%j}jk41ZK@O zwjV09S{9!Bu17L37*b&`wtSFBc&S}n(uicITFg6P!-)*!yr@ljgv`0&-Ot@C}i2wM<<(l?frnao2NdfQ4 zgfPL3bN$co1e34pFfXuf2Z+ZFNlr$el)GQ`nj%dA!%%oaifWW3|0Q~%rUpRq7yc~F&Z;aWJpQz zb8_E5=%$rxYC6`v^4s8CsDtg4l|SvkzIY^7wcP1?tYM|KwBD{k@R&9 zKQ(rc{YnS?=vd>@(7Pk0^&p~`rjHw#5J$Tjna%WW!_ArF%Kq(Fm!>P_t71QWHkjV0 z3BGtY=etR)DOyHi-)!`v~^$0fwIiT_NUM$3VD<1)io1xh@B6e6xq z)@LbdKvJI`Iuy+oCir>YOuB?l%14tc)q$hei)qJfU~%V9^)2$ttpei(-F9p-$eFLGFFOgM^cl|#!U1NA%4Yy31#!1uIXzVn$ZCj0P z`)zF7wr$(CZJT%Zd!G9{Iyq-y)~uPeiN%b_GK5GX(?Cl~A+oOaH*tqw8pg}=^)4E- zu0QY*1Q)&`?kUEV?R-)#8eH`_CEw3ERg!s8h$q+$6$e~{!(3mo`t87?2!%@mh44YJ z%TH8%piDP20O>0y*ba{b9R0}+0W%?Orv-XKZa<7mtsvU){^b5=>e5-5Y8Mx50Lhz{ zu164QQjWt|+4W^TASl>6{Rit2;Er$crQUByEw}2XnJaZ7DlzOX!;&Az1l8IuJpSSQ zH3P#b)tkMny~CAHH($oTlH%kwM=#36y%bqeQ3V2xXW7Fx!$Ru_Z7Ey#$m9C(>mkg&DXq}yZ6|k= z&R{lbNdy<2xXheNi}xOT{Q1MKe3@;9Ig%X9;yJ*Tn+z4P6@M^qE|&q&(@t0P8&9N0 z3I?&!LLkb!y;=2eLH&{&!MP8G`^$IiFF&{Ufd!QSxnjqPtPG!9y!^pZMCQNeky2>^ z)cV5Wojgb>e^v3JAx63oEY|n+B>6lEi}p9^d(GCzt9Lr4iZ#e9FGgZ>t)fHKT*)%3 z-#bBLd4tR)YxsUsuLwL}C#3tRmJSgXT15!7_}6%alWo|V&UCgBWmyZjUl{bbjNTp? zV=Pkk5oH-Oh^)HTxW;teps*?;*z>jQ)*aW70KRGfea zeEv<^_t>|$O*?n$sv;HP%?G`_Allm>C+I>QoD=YZCPaA>iopEJ8!GYGHmJR5LzudF z5s}n&9@!TF(7OGzTnutjPoP-12Op0CRdd(v{o`4XzfrQ`cz zNn>|+!&~Zg2|W#TS)WRH<79ux{1c5vqd#njdGDSNo(VWY2*=5U?@lnu1T3l zfU3<*Q&L;Y0KKAli>MSL*pQGotqKk)H{Eq=2G{HuQsDrG_gY}7nh2cQGKSe)H~Z4r}P8J z3Y2lyW|nMduo2+*F5`akYYTJQyuMVBJ?|%Jb$02~RM%$4Wk_pi+$`?kn97^r zXhww-(jTN`PcGK7;HFCHJ(Y;(W=L9&m(G?gFhpvlPhJdpfIB6UR^FR+#4@s~*xp=X z@n@9eaC?2ISg0&j!B$mbA8RmVU$_(I_OekTxylVcU`{(DL)&EjM0zaAmZ6N9shVJ! zFJ5si#$RCOIOpl%^_BWr*Fa;9A*v3pO-}G(0^xm`~T{Xd2242>uOt9Iv)4Sxn24Spz7(?dxq90a38+1>G04vHTpNmEsF zGC2q#9&{5Nl?)CpGbZNbWMx6E56u_$@PU1Jc(`N%_B3xuXC`POn#!=z>SNp&vw{K> zmFFr$TrUcY@$hK7U2~$o2(2ReE8KpulA$qLok`&NM_-Rd<;KZAbJ#+Tmm8vwqMYgi z#eXr{k<|yoL+gkm;kI=)6UmiBP#h1K=vNhJ^X$t^`i@Hm=e(=|dxY8y7brEMWl>4ut zVF%(o`x;Au_TGpk4_A1&lwL*+zngo5hQ#p7RY+tPZ876~ou2D9sEn>AdFqFLw#C3R z-cie|Ep`yCCtdNJYAR#vnH`>RhVfa{Y)-+kOCBxj# z;olCOL=aAD| zU)!=}NR_8IOny}%;n|j*Anyej7>0_N^Bg>@SB@5;A_0Y)<*O0RUIgxET#WE= zgu?1~Ub7onDoKLG%#8e@2@5JLVS{udS_8%kdpuE+vBPPqEniP>Kl9SOJqhF#tOpmy z$r?l9nfxUe&+z5k9=H=s3avqFIw225dPLA`aH>Y+IZmJy1gvxY&j~|}b>K*Y@#K5w ziXf9-ScO}b3_%nH*N9XDC*jgpQqr*?dJQk!s)7wrb1u|I~DnUc{G^Eo&FOV z3lz>8UueE8?HrY&2=w@@DYHJ@kR|LpIo}+Qs;XJ0RX; z$wI~&gNFs9d>2C1YSv8qBsto-sjybA?VHxrDG{{gCv2GC>fL^T?QgNL#4ZV)Za(U> zvifM0RCJCds<&_8-+5j7zci7Jct zmyGesQ~~hi_3=CzZTH6gLs_<8n^vS=%#>Ykk(rrzboP>kpH@ObTz^Job^m>7cTYDq zB_%{bG$c<^*!$TG4Gm2tzOLqDtp|>^sdPM}E3tkaWiSGFc&s6^6>C^wo6^@fj!MN@zA+=z@;E@uBEx7RKdB3igioMgHr|rFL?y= zCE1Q#oj7Vpo-nGvQ7}!Jo)Uxnv0A=2yODv@NwDrDs5+FaQ=x){==C=*$p>vimGz3G zpNyMme|#yom9OgMzfhnNg(X@$TC!ln8gQb4H=w~AJ~O|ACf&lTSI>zuMbFe9s+}rd z_PpcL2!UZbQ9k54`N^s^w#AteU9`<*RY#yo7qF}|XbgrhtI>xL8(A31TTFu9%)z}YU$t=4Yt6WV5=nFs+H9Z$&h z!(7dZP1+pJKv|4#Mx~;G>Y=b;sHmLxw^)6jxKU7Jz12OpW@I=a;b}eqK5|JDaMz%C zs)Qxw_b0-}cCn0pO@-Iuj*;&ymV-}@TDy&JGMS5=4X|S*t6j`v-09jZKOE zL`Dwi=uj-d0sBuNr+a!BgOL$dN=oWWc@u#lrMq0CmE)6>J3`XXVH4TU$43MGT-S$FaEL$=xGy!f z3}wla*qG18@rqoF1O=|xT4A%oWMiZD9nWJnXWHV)hqyUcWc)}0vv98jkW~ceJmDp2 zwiXwGtzs^g$cgAtM~MryFRFIV>I8A4m%ZR!!YbiE{QC?fAFGxG>@7UiJ^RBLQ_2O6Hsdi9%b0x}dZUzplw0DT~9p<1 z=rz#X_kx{7%v8aEY14^}!AM#-S4SJslTI1u{bcza9qs)3@FdK^m&2lt^4_{5wn9`S z3<3`*({Lw0dvkcb_j+;d6vv`CaSvAeHgH-1QAH9F!LWDwf^RB8JY)!^u_yNEzvJfagP=cCh)C?GS(fHd1uJgq=kTLJp4+FyGI3+81HneSN!?yQ8-R!@iybPaW4*np zpm-kGn+TnuPtVIw8JPZXLQZ`R8^y#@Ap6~hUm*r|RxZLyjOh~dCd)%GGQy0{jJ{e{ z_>W1!q$eZQTTI=B$dOB!{3K1Xkhu$hE&94oSE4Ma@F4>Za0ysi!u7PV+;CAce7yh! z?o4|Nog~_qjI%RKL|T1!Pfv>I&J(-KrD2RyJyOWNYylLjwN{SL)}t_9R!j^uDCmZ5 z#lDQ|Gk2qnwxm0#O+@9!YfNV1;AY1fJqt@tfemN8gm$kCJ`uz12i3MhN&Kc)pj;O} zvSqmyJp&6V5s}}`H>zV=T5HG4S!@FXMeD8B!AfoHubJ88+{vl8`?$_b5kN%lU@ZGF z6e$=ShTe<8QVyB0QHq3GVD~p^#Blfk21)1Z&4L*hSIcyD9TFjGP^nTO;_d0G+O!7_ z3E5w@ds7tn4SAxEdFBhL#H7$mEI>)Ct*sj#9Tmut`(t6Dx*DWtO#bZBo^R0pL`!q9 z!yG}Mm6=21)ZR5PTKNlt^zn<5_3n#ilY%iuJd8V8Ua7iMwG94v7+$TTs_oAb{6DNw zP48lKQ>l`hf{2SX)m`9;jCO8$lQ|xIGheap_p+82lbJ$ZTz0SuU$vU2-YMEnQ`{l# zwaWSuV{R2rZuHwBD4aU+1+!DU^(KSg; zIhvw~Gkfn|a-AhR1D{+G5SsaYc)M{#>W!LguzbIk@_f=FK*rZdwU9^?Mo(t-eXB7)x$G{di4gYUPhI-_t{XL#+_aH|ff%<81nNxXjS?DU!%>XbpkPGk z#__EFkFh7moy$16e`6f7w7C{_te^fferHAzokQ3z=q-|vXeHxbE{ESG`)&Pch; zIa{sAC?z!>9O6B=P=+2ehT85H*FS-+3Ts9~UHW401VpZAK4 zTanW5K5rxCP^I$yVpZyj$M5EGX1zN$7763Q4!_pH-gI+4sV_(XkR6Z`WOIH%hHuXf z)Qvwsvvzj&fMyVbz_IHKX=99eA=V7M$y6qP=l~p#uLWCn- zr1-t*ZWhtvMpEg&o1>AvC3Sr`9FGSdD=gS?ozMN6j*gFgBO++bnY1Z-@i#xl_(>gL zsS>HTLZwKF=;{B$pC#3~Og(npcY@?AU}n?S=VnCzoJwIFf<19W;n**>A08Rur%c#6 z)xZ=KB&yWukRydp`h+DW?Hrt)#R^;RBGQ0SoW+;)+Jg}@;wZLf7!ivSC7lzVntt^I z!(?AMX^-p>x8FrZ;RwXgR2m$fkJ-h4FP8j+Um(2mL=rb5cbaHvED}W8C0`}e!>1iX2_VghM0|Sp0RhN z5!y?$N`I|p#-A6xGfF*GN#F1&4yT1JK=Nr3=5#zF19NMSQ1!~@@LG6B7#6t?Cj8Pq zR8-ysnup~4Nw3(Zulh$$f2a3wVc!S1le>iC>x&gJ6;+t>FI;dZ`xIK-w25B1t-zMG z)ZkQQ_oa_-oMjr+s|zAbP|8CU2n*?JHI(_-Bi|}?`5mNF7qKP_1qFp9Sxea(`>J|~ zoime%8Dmr`Xb;k6I&YK&CcQPv|ExC>D~~9gRO&uNzc13do^!zYQrhL(W#dC(&I3Kaeg5&QGx^}lPeQEHnYLl z;Q|gvb=v$q%I>c2&dJHZ5E1WzuoMa=<_R1w3toW0U)8Wh9L~n9>bcakc1dAY_b1G8 zI$obRd}wObN-kJ5E4~Vqy8#m-D1@#kn=V%PXdfgMnenkwQ!c zTV;$YM4PmCspFT%T8Z*(P?EClb9>@Kj>4hU;=beOi-{EC@BI-p*PzMk==1PO6J!8HPdm17sGVhNY~ZBJqsdOLj{ru3pkTea}$MGUFgoi zyju`Y?#}(A24$hK?>J*_vo$AE<;!c*_5FgPg`%=s2j@J@DM3^HSJL<|SNr!7sJR8$ zAbns6wr@8)ky@|9O02~jwEBBj$e!lD#g(#W4Ti=>p%COCt&Pc1nU(B!E-dH^FCH>I zn}t8RtWa1NtaLmGoz^+%J<5_yoyF@kkgrQvGr>7E!<+@o)SSq*HQpy6k!D4Ree3V1BaOfZ<_* zP*`q9D4L%x5{CAxw$A=Y(C8ZN(Wdqrx!zQ`FT%g1g-;A)OxCl~j$FA@0YyTa{7Tpn zY-2p#<0)5TK}SYb+_z_k2_V$P|2d7z%IYGM&es3;qX#&r3Fz(Nl}d5+bvrKxuQ_;r zfPo2eVSChgt!X6ErS-)XA+~#>hDj8R`q(I!E^e1q6(M3{PYlAR#y4{~ZDKXBbF^+B zEMSoxg}!v1VJstab9*^k>P}^>u||*WH!D{{@F~o$%}+izv&Bo0$jG3+Jf_*a32P8*pkQEbwSQD;|CIY@rrfSYBU{rAo3+#bGw_5FGx-;P@A z+QNG!%r6fX7Tt#U*RM=FgIAvz)u;d00^F^IU#-;Jfg>>;Fle_v($s4wP(s2=QhBE{ zU^i$?q^~H3L_`$2Kdc^~VRt?~gJz2Gkp9lbRIP$e6Z)8QJf4oAP&_)|U7WHS)0lSP z3lF8W79&OycmhWq9X2~$fB|@}IGLH{VbrjLaVj?9KhWZKzTbkhb9#L}-noC_$ks^juQR49 zk@tY6wrG4A4z)h@7j~}sP07`1qqmuG3(kG?MU#r%?ZTx;yrd)sOc%kmKZEQInuA@$ zR>3^qW~`{A3JKFAr6DInz9dvfXa?X!Yv&L*^9LDxf{L=$3nI0$KCM{t$_)C?X&Szh zRk;-=Hli)4Rp+s%5NDx&F(xBzBtNA6# zf`cKoAIAIL$@+)Ywaq)SW@NBQ!?OB9LEMtfIr~S2DM6pwRw5n^`cG$r7SlDI|28XB z9NuocCpXl8m7Xmb!%pey>}=!nhC<*$G;K-RuR1j1;{S5J0C{cP3>Kg@kTzwlt)2Vs zl+kf$V4x8)z|WJrRO#e5GGBZ=m0kfqJS>IeMW;~@71Oj#PT@BbF3=NR>*5+fz7k{D zbEgoCn6)00BOrQ%x3g6wGSP~Gfl+HYLJb_#>dzBJ2o84T!~T*1;#f+0qPmtcsp`q> zL{>!wEdoe#4;zd(1f6`@tzYY1K0#|ul?P|Lx)u1axz=$ge@td09sS>v>*C1q9cy(mh(Siz~gCi z^SAC1USH<6)q3;Ik^~JNcdP7|H!hpqJ3&I3lELZJT{4dWJ8&+6>_^>FI8 zySqF1?l!m5X2Q8tsngH5_0!Ai1Hl^~CLnkE294GD5RJ)lo{1NvuqX+8eE7Y(_D35` zBuM{YXN8qAr4uHTn!#?P)17&2+?Eg6>RkmAa z&5}pswMlQ^RSmBr`dH$m{_ZnBpl(9(@&fDp_+*umVp5lWeT7I))6_RK6j-wUxQ*)4 z9~gzhrvH0>PCakV44jUh+NuPG;DfO>1G9%lPyP}m!nhGkMr$Mx&%pb zk@e%|@XRcpyr-iYB@3>9EPS)=@fcy0nd>V7N`-ut(_~b4@TyU; z9*FOnBa?~UoPj5S5F!JuQ@#`f>g<^7p2X*z*+oo7YD0bi9U5HM2rAEFouU*}Npq-IY%#Vfw{uPi ztW<42s*3+;r%eR$BF+CDEU!(K)MUZM6Htk~&K1yZ>SU^552g_n359SoZ*?v*sG~S% z;|VmO3jnjfjJ!DBVNB8{rI=_^(zFZK2qkP z>1=aeV=LgI#%&f@vs?2r%TN>Gv)Wt=QzfL)ZU9!8De6WmjC)&k%Sye#T4qd2!un^L z;2Qr+aTKi$K9zJ)IZT;QN*WIS4& zut?|i39YYhg3xd?0ypW6L_d*@UV2n67}Yxr#}5vf5qhH2V)M((lkB9mVV)lu34lfK zEmqINb9aCBd>r`=parKhW&z(p^85D|#Q>GNxGmGjoyUwO{=}})DaEed$;rQm6Phyx znz||#KXykCc-5N!Kul%v3MXQdyKXVt+m(Fp2JDk}K2I~-^5hAv#KipuMpk|S0nGC9 zF#t!Hr$i}VwotA`pS3siXrbfr^#ib%y>HiR|I4H=JyO7c{eHh^;xoc^e|39WFnF*r zGZPSmaFvsnC*Jc}ww2btG;(0i^K#EwUNHuoi|HLf4%&Rv03s%;inmHIE6o z1JY!*9v-B7qvKGQBhv-=lR&om8c%rI**h1AOt`1>IDMb~9z7_V9$r^Rg@Cu7qd?-) z`D_kDsr)0Z6K6nGS2jKm!@J~covomBe=EiLNlX_EP*J<-t|opf*~Yqacb zc)`Ak9NcAjyhtf(;y)b!w3|t)sjkpGcD-d_$=rk{WF$fim<=YyP0TK{>G^E13Xb3s z6w%<4;J(}CVJ49VjTNgYM8MIke-=~@Ll>nKX`L>p`-k;lyC+3;>@U_FNgDr_k3GVX|<0p z>;6ml#oa;_JF>0N$=;z+@yF^Iq%z0`d0stDB`0eO>oF^16g(yzQX7}(wP~Xtzljym z`EDxA036toQ$^)YBD!CC$ZzlgIzjOW!=VuR+*X~=vY8kv^`@?-;!1-#_2pz$y}>~9 zTn^nRaV6*bgC+O{VJG-?$Q@-1a)-tclbBZJ>w>x^xJCcR1L;#({tSbbK~KOA$4|z4 z&{Nl!yEdPD;ALoNTI-)rvW&9l_ktcVBT>O@(UVZDPv&B%mQHoX-oo?KI-yKH#}^r3 zH!frG=z$)*~t1tx~uIt`?kGwAr;uSwm4rP zNhXc!kHo5{vpb~YDDT8~wh!6A=uTP6Q#zR~j-x9oIuH`!*Xi{!1pc@j{ha=@PN};0 zv_e%~=dgV;CklZ7*%k|YQtw+Dr&E^xi3&=9;V;=gjI5I+F|=o&nVE_fBX+!^k2aq0 zda*iW;P~#ixBZtJNFX>cu)g7i7-PP>4y|>F>lZN!#Ypmx5Vng9<)_?wU*opaR{ z6GMNyKB8p~s*4#S1Uws{fxX&?bt==0<2_kZjp60&X1&uZ1AsSrW@Z^*EmGch5Wu$; zNZ4q4rzWSGeIq?fwBHfH0Nz$Wlt7FGec(_hSszbLL4kkxcB`BF=cj5xY_>|$CxJi? zQ(?NG;_5wbuw+DLaV=vrVj+EVSaV5=nzF4`8vgKbUc01Zs*6ShKm>!FlvFj_zeDcZ zYh%4kQJ@Tq1$PL~6HWLNka7AsybZ==C^ut^NaP;2WyPQ&m@_|UN)JsgAcWoGC9f!n zR;a{K%H`hL9BNyXv@7)O?g~Q81-dT+Dhw$t1!C+Uk7!Ie^9%F^SzDyF4obj0EHmxG zN2`c5?MONp=v)qeL8w7Goj<6SZppo3Yx|$+P`pNhyTV!pRoagz0tyldIn|8<^scvJO5rXd3u!cWsP-0H#}bEGRbf3|tNEgW^-#sJlSTSi!zc`3RCRv}%0+X3rJ?WqV|v zeZ9ii7~GGrz?$>srdkI8pN2Evnzv?p_+a``ZBN z@pACX+WJ!X@=5Pm8w7ydO2y{@>@#oxB4FKi-;}-4DKP@_57H z(<7>1?kf<+$Srh`8VpJ6pErvS8#2^7ShNDI>_!#PS{UEDbTRnsa9}Z+hAI(~&|Z=_ z9KDYpMo`hSHWB}>CX129zdJa>2UsR4%`X|LHzUxx!*avNbLH{Oj!?`lnx4VIM6X|1 z1|$ZBmRtPA$y?x!yWHpy-y_{D*J(?m zH|W~$#n&@5%&)UJDtxj5kp?+foG4S%(5SP$5mBkym^g1Kvnx3sJm2y$+-cVZ#bEtB zd;0uUsa1f*(`of`8MLL=>WOW!2!6ML&_!?kPJBqCCK><}=^uc*x60W`Y-Uzcu33lS zWX)%_G%3B_S)q=}Y43^;K<(yM6^LPk?^xJrAgR3lgM)>@wPqxob`qa7pnwgJk7pZ8 z?<{cB42YVS8WYCGWoVNE#0~_wbL9^eN4 zVPq$3cn=wwSC1+^g>^6`cJ<`7McR0$n2dx`0ACj?uV5szfnecB7K;Xu`BFgJkTEZC z4WToTG}O*H)=5Yr>WF_d#inRK+nC?DFuM6Zd;!H(%1z} z4Tp@P+)%Hpf}}XH^W~U2&AYbYN@33F2#IX_ReV>>5Uo>@SnciOgBRk;53FH?z8cS# z9bKVV@t|dOGOW@V^ZT3|aX!?T_Id`(DTz1&Y@d6XdMl?;=T9gmFc%KKtKy&U9~V&g za!e;{@c1~TWh){JcNyfhWs-0+21o}%hNLQc&wJy)x}%lDoe%=jr418sXB8*)_OgUO z-C*5I1`Nzv9P}n0$nprGfVj$3#R8*DpfwmG$CAc;_v(d4Gb$40gE|?Lb7sQ?w5^Zy zj!9*CXncM;GY7Pd&D)J-sV|_arsgkhAJ~)q&Lo74b{a`B<8+~ZqR;iUpe6Lq#PRjyN3{t6Yu&tsl90q0vv z8AV!KV7Ki3_TWK?XK{K4&{bhlv_nHh@Bppy6ZZoLV6rSNE#*s;UP>N!cAnIloT2Gw zghlIQGq`$1!M?eDw%ej`auY?QFm{p=3LrN`dZ3Q&3NyMr^C7>>l_-s8a3|F4b&+Np zxOi$b?i`GvC#NPGSnG+4h;Y0KGBeka$>8J~(;!r8p|Qjx5i;D;uQgTV0SnSXnKas^Azhe0X9-_BQS5IH#^Q(5l#Wo5H~7gcM0fkdmt2F5pCIl=UFJFvd) z+svj{*X&>-TRW)_ZEdX#Qr%kg>S%7q5T~{&hzya`mZ<7ms>aM&1T;SX}8PGjuGGoiAhO+Vwa^X7b{Vs(WLVm z?#BsW$^vx#c=`=kMr&|Un>hvEtp?xW{=O+^3do^xV!*0?qG8HN*btd`c-z#CRflcm zBC16Y=`nVKC}BcOTn4}Ks|C^?-t~T?vC(pl1f(GYF6F{Y@N;q*myhj#VL_eQp+ivN z(#GS1{Ms5i0ZC1OBFK+8RtVz6&vc#NJ z;78^gF=F||{bmw_QWd2N6*JEQemt;it02Vf&EjrHUB)6>izD7-$vIU|J9x4_kR?*C zQlv3r(5AQ8sO9F2HkyjF!f*)KOBj+7p&9BZ^E$vjDsNQObt$78pq+AQ)GV4E3#{^N zRvb++qVi;6PqnD(^9AF~EV&sH-(n!YJ2j#)pRDVnGsp5lwama2lWf(U`}bh}~AnH-Tzut~#Gi#Ost) z^Z659Ocxwyr5NY}XS6gj)pBE6ls&oG3fjjJX!{(c3#@cwvMbYtr31nJZ4U<}e&{>{ zqGLU+m|vhC&5s1h+lswL41w+{aOPcc$IWY&>>NLxLIQvZuqv+Uk2V@0M<5}H-<0?K zNdlCBYX`G=NYtVwen4Nq&z}Yjv#Pa!r@o?&!%@%rWe2S384G>JGPf)4kJ*1VjV=U- zgy`QLD&w%DC=9dak?&1$DN&+MWpY#8w#*ep9e$OIOibcp!*G8A%Tt~&OSa5R9mmsI zY#{nCx4I@``hKNDbIKT7iwuUL&v@QFEI(!W6N<^o8l1$cTduT+{s7%Do35q++?Ml}+=lv3eFUqxVC#08G0kBP^ z;rQ`iY$PNp-W_YGSf)@$IXK){qceHEldl?~QSH?J=jrE2H(3;C@lADf_sztzAr1|f zTpdk8=i`Q#y6OWTRAr&_;=~N$G?emP35RVuHOBZlJe*T`*lEo^ZX^hm<-Ps2Fpt(3 zdSFLd#swTp2z^D@t`HDeM9#0WD=EfETJ(pVlruB>r=; zaz3oEA+Uy+{J$j|JlNCHHpfE6J2OCVJ5;v}>)Sf|!|Hg(o+s^x&-h2t8@N;C0oTcr zGiqC;&*TE6a8w0B(}qd6%#ZY}$$_@si{6O)G5W$YdfKh6@fyi*tjtUWXviKLyDwvP z0EeU?RJ0$b(B|8SQ!yE`xn&03;StP2jxewFo8cMCR)yt=EX9cu`;Mls zoR^VSDedbDa%3@LW`KkUz7&4DNSQW2)R0T5blcD7gH8=+K2tTMw*VW!@s5wFlG7m} zT%jBk#dAvy**?{hw)+fy!Zj-?lu>j+rFo)#8dizYnD0_S(Gy#BxGc@E^CUjrI0Eu zQ#VOZ&mBG~$9soda0bUs(LGX{-@ktopV!--UVMEsJLxqm*BFHe3{0IZxo5g)_*UIm zLji=+5EDzJv>EF&FX^?a_f9)LLTKj(lY7PD57Q~jKDVwIY1%L1=%9&^0n|XjYEF{x2 zKZw-a;Ad002-nh!e(N-1g4gOW^MRDS06G9@eP3Y%sx*ERZyoAxzYyOH7c{B9b9^ueYv z0}Vq$L*yHk;J5EiAYLDhdf6ruv)he^#^@XB;D91pADG9~arQ;+HOjLqrTa?e%a<|9 z+cqfbuvtJUZop@bNG#87G&Dk;4lwU-~>cV}yE80x9uzM)qQ z_Vx^YKBmg=bd%ALpjJN#F*GU4Q8*v~D#+3sZM%1 z4rRjLiuI;0$E6P}`&E33A?nP;IetNKPpJXB?6*|uz3vx^b#yaWb1RjAOCASyH>(<8 zZ6nr&)1gUoB;xve2_MD;8c>-U z>?(IbJ&FC5BU6z1uh0A9ZuWO}5@pX}L^xZ1a#}nh5EGupXIHix;l94vkUZbB=4K+Y z94fyQf6zwis&}Vg!-6{<4o=?N)TNX?`LQ|jvoZF{O{D^L0P$rP!lTb0{nT+E!to(f z)PB|CG0=;!7z{Yqf@Pl)yA7ayCmy`G>fg@#JAg{mh_w?huH<|=UYOrXm@|o~j5#t; z*`tPu?Z4)JTmB5ugunIo$zocwxRlp=oyHpVAo+#y2Y*EVW=UTC<_SikWu#bV?I3=F z+h5wVm;uOFo`W*m&=EVKWjzdatPo&X?EL zEfJ<$Vlz5L)D=S;g>hs7J{U;e3j3dMh9F8VKl;li$q%h8eqjuQkeld4I7j#xZhU^` z#u+0Ea=Jg$>e(!_az!Gt>@l;DPfh>mNpEIIIzwoLI)!_%!q@~3v|n27&NpmHjemq&;HDRBH&t%yi3e`rR*Bh8N(NxU|!KozUi(aFg{PS3f(9}Y&2Z?48m z#aPTPc#tGey577c2KwSEf^aZp2!Bv=zgQ`9&(N~aoOwmac7%*CtA#Du_;YKYwjtzd z@0Gl_=0Mzt3nwvyHLt!*Pnh6^Ils`?3$U8cXv>uekF~`g*iCnpjOQC_)}h^gQMYIx zVQQkDn*#18A5ehpg0kcGAv#JehZdw#oi*cr3JK2i;HA7~C@cY8e4sP=7x; zM~1@<)@t-}3llfI5)JZgMoOdkX1WFW;3?|yRA6|>Nm+mitN&d&d^}0-`I`6tT7aQuyiQ>GBLYMF2jf(o zF<`=vacGkxBK%^3NGKldeEsJ$<#m5kU|!BuHB3%3qv7^i+R# zi{>unyIq9L7cWP`Lqdi^Fkm$!6zVO20t#$7rfB(v5-=XTtm>QTKmRMwVBgZD4W3Tu z05v&KagW!G;mNATi3Ix7RRGCGtiw@(5d&kMKX&G)4|xLvGJRc%50?5RrL2AJHN0$C z;+L=?1ABw{(%wKA`R-moTZHM1f6pyx#;=oVOi?iG5&E`o$)LRoJD_r$C$S(6s8_Y^CL`a7J1b|0jMANv+Iy zF)nGzmLZJUmOmg$oyQrxe_#$lrzdu*HuKl*8=>Bs6Rvmc@-@jzW_DqDPz`ox|DMk8r{h8w4+F6iIIuCw&v7H^D?vHqzYZE@Hd!>QyZbyoalrxtH%jA@eT{chETk;B}H7=2X3es*&PIzpS4BxhD|!IZ0Q){qwC_nJ89Gh z$HeBQBqa_|Ly1!`oj$k41A4+7romWFad&sDpdA#e--2U4a4AxOnVFyfBSsQkSS}pK zCQ(3pyIu*`WDVP7@J@F_-fk-=TJidnkYf6BG(!OhO*8R2MH&;3b)|=1(S4&aqK)vLD$#b{j>=+Li z5PTCL^EmL0ZI+kTqfjoB6((~@bsyR$ZpP}^oxBpWCr9fUvYuK-131s1*{Rk)ddG)j zLbxX#sPyqQBjJ9|V;-vm$kr}bcf;_3XUCzKi1m-!kQ_r@9H8}Gzpm3gvjS40j%`?$ zsSJ=X=%-s@*SV@{$}38CK`34q&&Cb3ts-XKi^(ljuR^6R;q~oiJNNg)zS%3g{K%IE zq6GSs0Bg9qu2l5T-3c(2YA{9XWn1t^C5ExtUYg(yr6U4DUYoW6ho(jQn@tY+jo@y- zbr1LGKJ+5MUF!_BW3;Q?h>!iNbS_soe4jPdtwc-bu@<8sb~dH%3s-^-aIi#?V2IE& zE(yIjjQ(LorqKwED=(Mw^dQ#zbhgmen=+KO-Rrd`7yAr^B1Eic%o*~%e!oq9P;*c;FjVwL1KpsbMWMbgq$CII zW6k=|x&PN=7W$>Fezu9Q>=r7Qk6YBXBH%*eum@{(ogkvwgKv|S{y7g?|CAmW+52C1 z+%CsetL`78-QHg9UC>a#!zDkG0}U8@V$vV+6eXWBsn?Ljv|^J>jC?c}vxY;twSyy( ze{iWfn@Y1@ZK*eX5oLzPZzxrl4uO0c5})#kZ!8hcl?f<_k;K00FPo zZfb0XABaW+0)V{LT4NK7I{#bOCZlGp!cW$SU-c-+tsFLfuqi2+?UCr#6)&>aIa}fu zbEEYZTmkTqJcRV>%URt33gh-lLb5;9#1nT*SZz(6c0!(TLm|I5sw`?nTV!UuhTITC|WKMxkC=(g3>df(N^b zF{*UuP5N$-K+XM|5*od&P>+CIO&eSUVOQAd^8$qhx9RVmf! zjog+O)CHET-ryidKM4=Yd<)QX0eVx?_Z`~DTT9L)+hl<6i{_m3jtHPP+vY&vprH=d zE8x>xd32Rn+xPVFco2y0-(7as&VCAb-YPod;nS3!9W^1A2nE4I4lqUl@_@nFddjV; z>Yoe10|Nuv6fL|p4k&??IH<~&J0^$`8%-4d>LR1h%Wq3Td2abGYT@3}11F3rwFew} z0QvXMq|ZyssnQnmuheJ$n7Q@&1_;v8sb0 zW5|}>m73E6ftZO}y&#~yfyVxEkHTDbe%kRJK)?|}t+@)&N!Rt(*IyrOt>Mfw6 z{GzULL=XW9Ns;avkZw>wy1PN??ruqG8A`gOyE~L-2qmPuyQJ&8{NC^VfB&_d#VlDf zJkNdZy=R}j_dVwl`RM>HnlVqJ)yF@t;__CSxQD}Ok;I2xZ6)?Bmn6L-UQC{U}grsXZmle@SFRV4zFAI z^Ol&U2W8E(f-O`b^^UXCfEd(AU3U$UQP6y|@N87QDy{)PI0u}FAIzdJ!h|~hJ;j1q zXw@2HmzS%-y@8wb$zkt<$b8YqWX+y`@dNQ)+4))W`bMvJVWUi*G)Z494b2TKKmGQYt{} zHx&HO@G7XF^N&T;KYI9728x7EiUU(F(hzCNQ|>JvyofF2sYCIM3GRNT)|w9=Ik_l@ zaTq)@jKN9BymKx+gvMV`aevR^^$pwfgQM!*{+xf@JwYD+;~&y>+~=sxgZ8D?F)`XU zE_GakxU~Dq-?u|^83>Ycs!(hV+*E1$A7^xaB6`O>)RO(OD#x2ejET`ZlCLs0trqV! z5-8U;^dyF0MT*)&&%<^V=0tKl43xO{e{&~(+Ps3N;zg_%eegm;9<|fXRqU^F^$Jr> zj51Y!w_?*Oc6N5AlUP{2u3GpyR5jzwZ$I574QGR}vjdxON^jziy>nnKH2muqwE8T~ zuR(|bUkr`zadh@=#i(&&4-Z4uc%t`a7m}AqYxrBdq6QDrTbD++Y|R!g(=UBoUENF< zw|sIw8tBV){yB)nG1j)2=6yc7YZtq6tFP=94E30h9=Fxf+|Og;7O)w|K!~fK6JINz znHGh0&>H8>^43y+T}N*U5>Gn9OK|!%_~o2@!YWm|=dGH8nIE0)st>*C zYE0m;MT|P!8MkY3_NtK^So7H(HgXZITW&DONiUEe7u@L-n0v5cxM7d}a|h#GQyb?n z5*RL=x{IESboDQ9YTXhZA9gC;ONp`mUM(gr&iZfub*Vu^xRM4&g_11|8X?!lIXBv6 z74cky{ajI`CUSKRJ2FNL2W*y`(e&po8PQ0J%`E-ZWf;o%*O=1>E>OcT`SvhvT<9>&*>2ZX zO^raPwY0XBS-G(gr49FYKzL2s8{_AK#lM+R%|V%?(8CN@sl$%JUNl5L^)Y7rN;vw2 z+B41XoaK#LPBE#{Z78YKlPBLz)U4%*Ca2h|&?_qSU^M~<0SwktwGk3R5w{iA(7Da$ z*Kd$t!~7B}#sxC@Hocf1wejZ2GN)f`v>dyCm4r}HR5_Z6Xv(sdjndEEcPD)-G}O*a zSF&hXeu+eX&tP|{KyNYZ)ExMLVu{Wv5xkPerDdvG!Pd1Es+IoigWLZ6FMr`Jr3QT; zW#!pwm%Uy~LLD$ZBhI@MMSai5GgvOUZM5+}SXhz}{;kM%%XAvL|C9>Op`vpg6nW$q z1dPMP?oSoHGsve(_2t!FH!Vor{^WQ7Z%ra-#g?lL5gHUnHu|M)h)6QZP;8eRDSz$2 z@QNR8VV;4M)bCm5!dTh^z5fw~u3VbEBvhk!mou`0uR2D@BZ@Dy`IRzmHIaH#dO$w^ zbcY`XdT0e(-tJj?F&&G*U$z2iv66=Z+t-S-mOmgeSTTh1S_@tf*4BeIb=-WHuu=PV z8y_a=NLB$`4o}|1*Bd%+SK7hx_h{#JhQDf0q0$FQwY5+mNIT13q~nkVRTzGTU#qy5 z37ljdU70qVk*VsElyTDw!I|YON4;&FbM45(fc5)ijK*D zbPZmH)I(l}v=2{5WU?h8F;y@2o&lOVc-DavniOK%mop*Vow|QM8 zHJE|{DN(A2^23y$inPuxRZWS6oc+x*GJY;#=a1Ftydz0DfmC$dK#KaJ6;)Ng!qm=& z(|zvW!%kfwZ8rOBxpmw(4oNE7+3PJw8noBv%iI|aqt$zNNQL{Cit=TupKXFUsY`ya zF?t8Qy~a=Y@UBfmo+Xb>rg+v25h>x7_4+%B>!06SH>hFKFSHmshWzfTXRlu<6l-P- zURouFXe+va4z;!o<5|K+wkzx4LiA&|wzl6z^%tFC5dj^U?ri@no#U7r$901rAHW4eDc>1k2vA{}^+$DZ4K zZL-+#QKHX&P0npBPX>dp^@iM{FWT|$iuGcD<|n8WJ6&aj8j5~!$uA#^t2!BAS(R0c zf;SnS!%$cpUaYNzgD9p}r46AA4=*m(goro&=?%#799*!$nOw+Jswgg=>8&|xC@U04 zu#*yjnix0iC=a|Bm@~D9JXTjn(-Tn1oFYt|?v+HY!AV^poS&A(`M?_sOWBBM;qjUz z4<#&=?;j~_Y)~igQui_PD!sE|)0puUQ~ECR*Ms)9F_1jLC5_u3>NaJiy~P@L_OIfW z=*TKyi?iy?HkEBh$kaoqB7@Iv$ppcYp(b5|xuPkfqA0sOX?v8LCw}Q$mZDm#@iIFEw;IxltY^mkLWX|Gh46T(^}Sgz5Jb=*Ar5rQ}#&qjvW|sXQ()N z7R(&O_lxt6S!=}O0`d~$dYc}n#IQ3G!L8Z6rlGU!rasZB;miF8%=tcotvSk2@(|OM z@YExvaTpQIX))3!)5#*mUOk#h%6WS_>dW)dTrn6I74>A7HpRj;Xzx2bXV<2j_b@`` z=vdu&Gx)jnb%Bd&)6a?A^~-Q zga<*a=&FxOq8S1y0;%@ss@D&?8WdWS=o%nEvW0 zLBcSyBY0cGz_)u-ZuOGlX;0ZL;1Y~Of|!O7Wpz^c-JKAYXuLD(|NR=&Nj?84E}mqm zk}A}h@?mDHX+KF6{j9DDUHj=cx$Dn z)q;}dHCS1AQc=-eBzZStiK}+s3Qly5{AD84Y_P=z-3iyPPB>6d&#Z#^4YICJ`YI&( zeIqptZZTUHY!l}od<#0b<8>yiV(4L|0>M5KAdxLzbr!DFqvVhT|7iR3jT&~QnfSO+ z3yVWm)ivGYkwd66Y#A``9*)~rZGnNIvi{LouIKA*=A{~2JW;%Q*0xD4dpt={9EnoMm?@h!J^+j z^Bmr%(3XnVDPGnw5fW1Pc))>#?o`o&nm?bi>4T9sD64@v+*c;fPGcZ+$f2Kv(@@%O z%aP9*4QpdNu-TjIz5DJB%Fg(XQ0spEK+D90TQL6KKRau8yLn^~SLTi1*12p7TOf^G z%IzeK4PnMPz*-(FH~pVEF`2LxuTVo07XEl~ap5DHn(IudR$E%wWn^As~xv&QR~>m5{>nFZ)Wvu~r0U6Gc*dMBr2z03iKQZl;Kb3Y&(ao+#l?-_ zM9@-KEF&)rN9-|l()r`D=GqSpqS`ZspewXu^bjYB#!|akV^FGpEtaH%Mwj|C>>aw3 zH^%V#;Njy|S*-Etf4}Ep)L=&Izxr>Ou2%O#FRaa8!^D{(5#PzL>*wiFZTY2=E87*Uz zbShf@h|+yXcsL3@GxLw>va~{`!bD6}89F>2glfw!Je&xs63xKKD~pz2}a5 zD`bM9ITZN;5e^sJ=$EW|hwb0b(24mv-Ijb_n=8SGkH?0N$4Y#DsPfBuPx)7+$b3>w zUh2-RR|RKAmXPUxMkkkxfLOIAH7wIcybk@p|NHV`IpPp|zrMNGi7yyS^l3NY#4o}d zo_W57sZui<0jAs2pZo8J8ICg3X;z`BNk~j;*h@Hu|Mwes;V5C0thTNqR})9O z%r9KDrun}Y0Dc^keh`5lSQC2JwB^%VQh+Wca$3Sp6AE4LIPf_?>T?pv5R$38yvXZE zDK5?_WC$-MV?-&=Asb)|&&gpBPo^m84-cX!A(QDpG#U;oG4AG$H))8320`d0vCNyJ z%lF)sg?Cb_%69m+`Iz-2##m>!`Q(R#H^C&gFi$}RQ$F7U?4-KjEiA0uf=5q24IN27 z6kBcz^dKiD7Vyc=-D`wJ+T;#Bj&?+{?TK0LhsRpR{t>QYV82^6p0K8l`Ts_gM~q5} z0>VMmBKWoP_~q`T!VBU{1m&~jZ=NwH@*F?;S-Gr+`PS4#5>2QEZj0S%_N&cS%gF|M z5%iRBF1vLvtSDO^hq%H6a{Bu-q&f_m0*VX8y;R6QMUu;)R6~;QQ;npmm5vBtHK08G z{+u|p&GFj$wq0Sg#F*jj9qf^DyE)tbHwS)7e(qk1m%-W6UuSSzNicA;D`UFm%)@%7 zbaIi7mnAJxV3~s*z_H1mkBEn;2Zul0frp4EJc5ql^bYr}UvO}vE^~}CXtwCE-cJr9 zs=Tmo`-#s3#tJEVj%i1Ozn1CO|Mv)m54uciunF-MWSxo7poD;%vD%%qlT=m~%*i!+ zP{FnmK3`K(qz}h3S7Hzw=KzI5!LbC4Cjf9+Zewhmw!Hka4YYYg)Nbt%v$?r`F~pY3 zl}`1)vu#ySL8*aHzSxI>Q(H8UK7M?_rKFp zJazipW@4K}td+Px!dI=tD7F1bvr`hQy7W69L%3mMM}M`U_D{(}u4;;020SV)5j8Z} zTeYfBRfW?50hM!sm2>;k2g1|VuBBXrT&ZXgJ-ZLKI-u`j%R=<{4#$FrvDQz+DmZbX z{=!1@oSYnsrEJt@TTR>bP2(C9n7F>L1s*W!RvfqJjTg#^BTZT-uapcG3ermb?{>h0 z9Tp*zWmZwMGv&uprd`Kar|o;Z>58{TD)8XG_9gRwD(&74b)RNQ8Fckg(?5uE46ZXdv7T9wPRctelLt*vPv zBJ_Fd`ggMc2={IXX8Ui4dfE;O!w?V=+4t{c8aHQkUZSA9=-hCOO^uV=t;&UH^TaIq z-naiP<3mMwe8D^{=ITl_Gn2eOTi2%Q)8QM~wAl=*CMx`pn~Scsn2RluSQuK1Ss2|M z-m~XRfA+1}vR@=wiKLx2-xLYEX!y;uh{(d8a0|n{l%KJxVw{1L1HM0@#$DDjiHf)p z{5-MT%~jmLW4X6wYCj7fh#ufAg9mUydGr z5*H_fB+ZO$a9y(xzg~D2<$N~Dg2bWUcAFt2P{Ev}Bc%7y0`H|fBIVbVx!N(K&E4qv z=&f1M;V`L{rUJT--#v^iw|JDYgFf~G>X4p5<8y49XmyTQbWuq5|WoP>>^TG%AsjhqZ$&I!tP_Lma!SjsEZHuiP zjXs~H)9-Vsrdt`F!>wlVtxL!wx>Qsn#ICO?-x15*-=Gbj4yR6?ohOl2S&VD5v5kp| zzxj7uF@k0XdqoVnM4UqGWV?UsR`DoE(>LfA40N5AHSWoDiQ!5!uk`J2yNQ z&c#ev{Frs*Ar=oZe?nCNe6i$+g57kr+aWrSgqoH+H77@esHX=92j^RAs;H^y3tnEW zRq|#c5fKs4Je7iiE&RCh<*#2|VUb^HAOdM*UMyT*m={@OhDNLXqoYm=(Sm>e2+~is z4vI=a<;q$hFyAG|7A{S zf3UU|n9}nj3=DlG0i{AgIh|=Xp)~M4dB&)IM}IgD|C7Gj?TGo}aG@vWz4yTPD!1{5 zB{I*8eTh^qV)ykGDNvt&=vOA8EfR51Pqpk7_o@j8>wGPDW`Aeem@`(!C(f|;- z@%Y}oQ4n~5Gw|k03MEaIQ&oKxBXj{jDtPwtZ0qrj`N>_O+|(^x@6-N05IAX>Yc~2& zD{CP-nsF~BS-Msb@6e-&il1m9n#pd?;fY z^>BFG=AR8$Ez6qK&=s~8db7HvrP{xHy(?N#tg{}Zl|H##w?}K^q1-5SN7B)cgVA%= z>f?75^tTO@GQgdaGAHwqT_c!oYZ~g8@$wIK1I+Ts<6TNVq3MVa4U9zP(X)l`PpTa-Ea>XVjQ~>iSR!J!+DCji~j;y;o)%h;xr|Ih?(7%1OTyJi=-G`k~ zRhacf~>I`h-%aYL(Xy31ay_=$bHpvXH;JF29uKAo+ zt!ML7o}_Nn3hC>v38!W3rHum2d57(Scm~yTdayVbxVr^qISocXH`URzL^IhKulV(| zc0rd#*Tu8XRhqF>iZ}`1zw=;W?_p(rlXP~5{QOBC6cX~9G_zkz@K#AzcWAMVU#tEf z255ZM*^6ccFjdJKBPL6;^UlBzdiJl;(bVtWCB1w9=>M+c&VPPh8?#iAtdJVu!+M*g zAwjpL?k8fnUXS*W$nU+FSn%ETR=|e{Gm7gWJFA;WK|mr~JruEiGkIFKFEHBu_iW#U z4x1uHhA!W=3IG^kycRO)ecltgztF(gIfjBzhmkrw0<6!FYAi#E&76F# z&0JV4r9@mp!hqLN>mbOdzGY@!W~PuyLuEgHq-uyx{n+skI7D}X9vYao4~fnrHrY7s z&}_STS8ucMQOV(8NQ)WsB}1Po$*P()s{DJrNs)@hE&IdD2=njrLCfD1Q@HKKBJj$d z^$!XR5ZbkNW=7zDOG-M+Y({#>JKD7PFBSx!?hyUppdVDO2g%xK$ z`vWmBMEAp7J@Nf+42j^--IDKmYQ=jB_A_JuA9t7Le<8tE6@(Hk7wJQiC!y%X%V!Ep zCQI!LL_*%*N=xG&n~=<&U{;YPL>&~bl$4bv+A?5)9xiz0()G?70Lxb@cg0rSqhQy` z8YjE`QqkBCDbx&4Q}oZDSFD%nlL^L;u18zBzhvcOlaTxjRLT5dRA5WQ>-deZyrodQ z(HZTnK-z!`$%{{yhX?a)o8fRWszQ^cLs!X-jR;w$jO-w`5peArmXipEEH7hr9y@8@ zOi^sM-@Z-Os`JC5#+-1*5d%ehYMPAK>455UC8jRqZ2n&1J!d*l&VWL=1ipuvfHyqi zUDI1Bs67u+sX<5hQO9i>HD~{jB7m_Y&d|72`yREa@g^0FoE#%KI=cR?afQW=KGJq7 zN=m^_tOuW*97A{83!RCwIsY;pF1CmAy^kSSU$Rn)R0@RNwcnAfti(d@tzO3n2oQvJ zy;e+qfzjkI6=wz~lDqxtfww|db@5IXB!@^P_ z^$ZvCL0Y!wY1|FZUggywdx^>Vr^@^IL}8n6-eJxh9Kyv@^aw$3`{qSMFgoSsdh}`k zJM1}wf|#n6bZiOmNvp|>6#SW<*%QCx8JD3Dl6?xg-+Ovdgr4SNN~(q6~e%s*sG6% z;$~Z{9%Gs(X^fZ%->?aG?(W993yTK>$4wCh_RMSs$6vFUh|3L{Qoz_K#dUwK)#zvY z-iN^b0Bhl@rQB=)8vF%Uo(vPNw`(16C?jogcGQ$>L0Z1*qrL*%w>G~-x}MYN_0Wa? z`d)<%7=N#)Corz-eZC%m}esD@8v#$PPeB%UL*nDWNV{A10B}b$ngZ3m$WI?#>^hfpFbh~Ej7>BBF&f}6D zAB~>H7kjIUZRDpOt+sx~Pc~WdzDIV__hR-}Mh5K7@{zKpYCk1A&BM*m<-4Qah{X^-F{gwzlXMEtzPe1n#yU@E-)}%NO{c z#pN;}$R;bPK-fdjiGMcE(R{Acex_MV8n)luF^S5EcX|ZN4gHAayXuJ`I~b>$%2uE z=aA&2_4QuajMJCJmd~;p3%@ojy3=?5&$unE23{^LcU`ujM8zgW=Nml!d=_752-G6GaHK!AW2J>aHtgkOg4T;~kyE_3@H8H4h?*doy_wS7#B0?aH^Uh~_ z)7HHxdf;9UN&Es|R)bFBfixWeOwcq$0Fu~mc-QpKR zF3}5gA~5OL6>%u3y#W$pC-n;p>gIcvD0^%mG3C;Jb@v?ZDTuW-qrp>3GO-Q6{B9Bg zLAsziKj8jqIvmLnlLG6Bs3FC0e+#HQ`(5kB*67E!)7oS-D*7U34Sg4*PQ4ju9N~DI z1gXCF#cLH_On@A~ReUp{?u1^C*J{ZC`;!AtxW~pu zXW7kPXDA`p=6+4+r>Zy1+gz#XBYK{{bGWKb z?+q_E4re}DEI6D}_o`C$fr|RCm>QGyY$NmUoC$N)o`4=h&IBbW6}c1}dG_Z?8qqn| z^spEOV<1yKUGQe5<%R)Vt;IMi*%~T$oMRz!kruU-;lH{DOkx{ZXZehwXm`5XVjp;H@V4YS zsailAg9Fv>U-n-CRs<3L91$bt>ko~ihs_6W1D}t+!ulVKi%_^1ooO-N-v~IIAb-Kj z({(0;KmrO2w?9Ns@C42@84&^%FSW_vc2PfeV>n|~4mNS3+ ztk;B0+g$ijiJJGn*BMY&Lk&N@R3S#^f5ghjyyt3|XImj1cw+ezVv~}(u-QpjF1^ww zlkk|dkrXNXv|BVh1X1vXXMzwym34K4@|)mZqM;#?anrWU8W$RLki#)2T{J{DXxlsm z^FzTK)x9(sC{+rVLbhmVx#>{sn2j+g=PPZ_sogxmg-$#lTE0g`^OqXJku%$O$^N16 z;*Ua-2=Up+DNcQ_4T7gMLsOGyW#zT=-eRf=?4i(AtJ!tW#@24O zF|l-LJdT_m0!jN4IAZqamq(g_$FB<8-^(lWoDa_yGjHFfS1B$Qc6k|*$ z5tt%FeFhYilC2`^giS<^K8A-nWeaQOeW~KMF z;aD_r+1PCf7#$2bzYv=_t}2^7@n4h_GYxL|jOvggWlJ3`_3fE6Yiln4Du)M@6);m~ z>yAVgn$arfp2*lw8{yO-KPsJnprFj%nJ6N3IFSM&IF>S0vwa`q^vtSUM3)Rx^&P*P zt;s@j0w@-VtCdK=apmp0Kdqd%ceq2v#}M)jb4|-}u3qk0LS8;L4h~S4vagd75eXCD z@=g{G9V4)fj^3IHYnO4Q?wo3;Pg~bJ21;q?{o9wj@+` z&XxFjuR?5+&)259xJVs90DJbP(?LQxxs~}#eHTA1OXjL7hy04grR>j+*oVnin4tbX zfCQb4^C_;2+`M0mPmwhF}0*nS4_u`lFosVj`Y4Z4^ zLT4;zez)1bkO;LBlQ}j%r*PN^V{8Aeiicemx|kZ$p6b$GFz!$O+aY=f}F6hZ?I2y zAMIGH7~|OZ_|@?}_mHm+a2M9@D1gK`23*V(Cj^fXln04y*^sWTzDrQ3hOoOIBhK?a z_ySdGC(*o0bGpfiG47DS=*1og&;bU1*BR)<&G6#;&m(%?9$R~~-K0JLps&xYl}!7b zK}JrZPLmD-`8XA2rdPs`Y3_QlqxGE*Rlz%Y&0>Rb8V2AVWfvjVi39SFAGNs-Yo@@g zAqF+^HTVa!P&SvMs}7)&0*U6IdN~O&lPo3*0o#%QNDu&@V~F^iK`KcEg8j&}4lT>x zNfCm;un8I^(*d1)0cwO;zRDKKGeuMP2CExdY#tMF7xaA^5Tk(G2mYJqKbB0NQ+>D} zg?tWGC%`+9iZS;XbamD32T@Q~*2|y2rSrPjU+%%rV4)TLX9p8K$O`$S!68sboQ>_t%Mu zuI?UMM_AdV{b;vEnxul!!7T1Zv{HlZb7kd7tBogB9!iZ0FgxnIX6Rzu=;yI@0str< zP;e=pBfV{(0oSuWJY@zHe9<$!G6J_bs>7B?*4{;DUagsp_llvxV!uHG?X0&J*ko5} zI_yZLwK@hX&-S_$iUhe5uTw_FvF`c)WF|utD6=0T+IiBsqZDb{vBTF}sH zaYY2to%i{OQFn0C*;T1V<68ZnMqPcx`0W117(g~Y@&8_z`yZhu;!(}XBYGpNIT01B z_f1X=@nS%K=7s3o@TcUH!`MLR>a;{_Wy`ftp$uH|V{$TxNVIw5`!nzEXro|&u`4{= zVjK^o1L`w%;!r9GUP-1&NFO|J%UdS)Qan^RB=keUytNcAn*o=p7)lC?@UQ)JhK6hI z55W!hv+En14@Ll#F#4i$;zb0v^uCugG$aO?0|4m^?;FkzBlWVo2t-t0!-ZMg$fg}- z?OJotb&P@2uUYrrJf%09IMrgI`8gV)+GCA%*4BQ32ryD2TsS-L&Uf9}0|w)RlD{1! zL8-%+64=CcobT4)@|2YR0Ky3C?c0zZL$N8&XcEDFv(HKdQE$0()%GqxMNS7y* zNu+rF?yRN`3%Mz4CLHC4eW$$jEu0bMwJ;-ZQ9=Ec|M_&{{N?$b7$Gr+lZd zGv|hhA)gwY-=tY@&AQV!{>EWt!`DW2QY185kJojd&S}Xj3fvAGL%lT_=Q)eQ`+L6o zQ4-~Bwp^c3W^h@j3k{hkyN?;HFz!-=VX)uln^ineQT-)9-(F#h!9YjPwrbFS%0&Q< z{=D`=G#f}cF8~AH^!O$DmEe(x(Dl^;TMSayt4Z62vz@-9)188&6!kZA^|tFpW}EgW z{Yu`a1N?cVbOIm6dhE{&Q=jTLtOR5ZJC;lWvl_5MlQ=D%37Ol>RDUrYRyLn#q<{Td z36w8Nbpa5lusC@1y;mk_`{OT&-~J!3>(vr5uy1~#V+V*`%GDLGp@vMOhXPrl%}6x4jakrzA#>*>n;cr02Dj!6Bsk!-IRA)xj+ zEO+XwLmuOpbhxgx=CmMq+6{-DU!Hs2m-jJg*KU3(Qed&&S6koQV6mP5s9k3}3K**I zmuK9*Mm@c~XY>0FGl28Mrk&qEoRYq@ZELjrH_Tx>pO((waC)Kh4pf(O1mayHE~GdC z;8%g0o3CH#;7+9>v%x_@j*A}n4g-0)$mr;w4^nW4JPx$bNrl{{w-iJuAOi4WO8qbx zuyDLot5ej}>`WZqyPqAb7U_%DYx4Xz3{YUKkC)rY!~B$1wed6NqzL*;LFuj#xuFZd zfmzShhdsZ&taj|d;^_srH;y23EzL2LBv7lwwQP1$}I+-QSf;Dz1mqtCv1S z!`8YTA%DyGF-b_2q&=LQ|5&x6bjffsvbRbqpPOx&Pu0gYwK?`cElD3$!=hQ)%7F}g z&lKegl)Yh{TKHv`u)qHrM( zd9pZf$mdec^>DtoKQ>pU4%Dj7RmwGOz1XeEBiVO7njIO4qcv9zOMHB~lI5R1t^ZcB zeK(rRYs~)$@VV}SjwcI_2?Sm58snc}u=lF4eA=;Jp(fxsK_dIRNQGPIem$M7j2}QC z4Lu)rtEuw!C~p1Z5=Lg`%?O1hv)z9M=ChFC$&$oGK7A-vd6O%IocyzFs%L|NYy?oE zoTiAUpbo32Vv^9HNy~G_$BVs~nQ9L~&^hIT@f`&60ZK(dDRyiG%ow9)neBa9 zKf6GDc=mi0wUMpMNUzdk{6<h?y+55Owb(w~ z>H>@M`$@kOsOOa$5dms>8+Qk5*n>qw;%P4+p%Qf~?dWKf;eP7peLpGTD}7F4c1}%S zL;^~P8Ut{)zt9q)UN#pOuUZkW3qIU!+fZXuGKI2I4XfBlvFdY#oYO7&oY9Mp5dU1K z$8Z`hTmjD-bf(9P_&l@S^xgwUxcw32$3qp)an{Ch<=M@s?vH^k8*dvgeQ)#2%b5yBX@^^V(mho=#bRQ zORonbmp^p{32@UE3}uN9pr!t9a>4*vDIs_1XILT{9Kb3kcc@2-05{Z>4V(6v);L`q zKLh&u6D>AfV|GhFu;$pLS!+%bf&aMa7W)z%nVJ!1#cW$Q+jx@CKhtJK*l~BdKp#bp zc}ED*V$1vz7zzUR!K_Oumr2M1Nzkz1Yu6@SBAGJnELd^1=Q*2tg}&*bS3uLome(3* zhPzuZuFIIf_%D%5Gu-6$ivz@0pfw75{v#)6 z9Z37OblEApx{5X3Y9VOX&YB8h$+z#CNxt_lp0;-n<4x4ajx#<-CnxjdF+4FPo=x`T zB$MD?xs>ON8#Pa6+xGhxV_CwK9Uqc}g@5Y0Uf1E^^Iz@sRlseJn~~*K5WuQUB0LV0 zB|s59GJe_!rQE)(LIxasau`auWp-g8@_6@!?2DLax%y3}o)lH8Y@{2+xOLyd0p5=euP-ySss52t3Z`@}#4W zjYT_x5D)gtt^U_tgX_(EcDXv2`lZ^}_%)eck^iuZj&kPz*9*``!0GuaDUwJ`(wN%`xwMOzv~>nSUwwF+^SJnqJIA_CXwET z$8Neu*ZmK!rYhA4;5z1Ap*^{B@nF?%7D8K|&Ezl9aa<0Z5!T*&z))%ap-L zP;&0pBG5!CU*o7I*>TC}(5V;QSi-Cm+BpfGGP%(k)Zq?IM z7^*VuYeMY4#yQ$QYo)#bvsQaua+wM~knQYP_%g}p;{bd*Q)d<0v{bG~`$V>bqHDDA z1Ofh)evkU6XJ>$J$d#Lu_de-Cnq(^n3#?~V$&`mzy{DAeFR7od>)#|cnXH=X3-q@D znd%HiqLSJ#HrW%seG)4tu!={y^6%Yt8%goTv8)5|WQ-J3{ zYX5>v-(!a!1qGEskVZfNS|YLZQCL{WY00N-?BZ!LycbRSI?|rm0^`;(`CCvSY){AQ|z%>3VTwHyjwMr0S_@SGPr({e3^XN*ALQpU+^D}hOWnRyH=spW|v4rUW26m#zuDl*x)`fq$`-F0f74wIZTQ!JOE^) zP%7KT71yUM`fOBaaHR_g7?=Q=sR?(nKPzu<&jK*Z=PSe{2e1LA+2(y&Y0tV5-1Edi z%-|XrkZZJ@hyt!OKo^HfOuhx+w#wFM z#&S%o0``9f1qY|j3^F%4b5xf;g~?JpM!C1#Tj7iqv_OT3#ENs%oBHEIoI0t0*v&!3 z6Mur8pHs|$gmFmCz!0KdHtX1vz-Ef!zx5JUh{}D%LM>A0OsM-4E0%s7IPzK;dOuYCab4?!3FxJ+~)Z zmqo_kT<&yqA2SXj*HQcZd%!Obcf|;ef@J>$KzH>W_QHWDZpepO4RZK~VN#4~%miU< z_=o4HGGJ)Fe*G!}nD*=IIZNUewtzs<@7_n0&IhwF4!e=TLE}s2E=R+Iw|*Mmo>->4 zASA%jp;kN`5daS`Htp#07)<;LB0X&rTL)8M4+EH#HZ!9OL>~r4;}6m$^|D^u^x$i+ ztOi7?YicAVC0Da|6xdBpd7L)5nVG*V`n)&>i4uGxC0?k?b}o~169@1We1Fs#miJ=3 zI(qUMfYTX33VO<{Cb`OoO(k5G>;bBllt=EE@Jp>^>0BCBD-c0^2oAU@-}(g->KanNGtN5J^|23>Y;! zL5dhMa494L=9I@yFfEVczlpnCL?E$}hIak@TShqSrD*MK*cnd%SYXCe3-&e)G&D*; z+X4D7!_)C?Dpy7B^v?1Pp)D;9&0lM?sy7Z~gW8JoJJF-RfmRBK)6>7Iu^in;BZUPH zXnoRNpDE?)tW2D)cfl47(f^=p4KJaBF`g}|5t7Cq07SCX2uoI0hl!FYe*iXFy}LIe zn|l?lK^SDcZR#5usHKwXuinSs9JOWFb*7r3+n=)|1L;MnW_u9-Ve_c-%1pBhWku_q zYQm@vuzmxP9S9wNYJ;pN3JHK=62brY_$3zhuKnpcGLBnmWSv#wIAEaC-g{Ghnt#mn z+I+2YvMfBs=`1Izepf@zWjh}Uh=QlbHM)4Oq?T~qao-OX08Ea13God|eyEu&2R@Mi zE06EZij_*N(rXe_ywu5%0vB`1H^BCptiEp1r!sum;&W?yb~azzWDU!{KkSL>mo$1! z%fsh5jB2_Wbaswn|Sp+)oU13nOP3yTre$`{)WB|FDOEQN>*j1zaD=B z6o~gA!%#W%YmPOUX?R%tx?k_^USC6u(? zc*JT{{i?Epj$xts-0)Yo1t1+x#v*F2z`?&kn|DAnw|){I0pIlu^R<4xj;;&Y09LqR zcQHTlZshMud%&B`fyI;L(R;SK5_tQEBg%AGr^^-gs14dcbw?{WbZNqK+8>Uto=d3hjt>BA$LDlE4Wpi9uq?sQT69t=laXAcY4Sb;|!ab=i#9W6w3%aWRGLPRd+3jVl=kCzj2!tmeR^ z05{Wj!Bki*T{$=GI#q7t{V=2Zp>M~r+V9a8ko1udSV8=HR{p9pU!^8QbAB+xG8Zq| z6uAGTX67cp&|;eU`Z6~)O(tm&nLkwKS2uPtUCZ}cjrcG4Uy1t#0B^B)iQUOm+7qgfZW}v_3JkE&1 zZO=D_77ZPzwE)@i2MNE(Hyy$Q`-%M_mH_q`(ZhjyDk+4Sz!JS=2T#YwTkN-+N2f}_ znH5WkWC7A?AkI4crp?m<1J-!6oBDKe*6*PyYhDOre<=_sog7#2i=Jmu(aMh95nlZ_V8-mmPgup<^ zeqjr2h8{H^KPmdJP%`JL6dVGR$ly_9^&1xZH*2&4{==qQT^Nh&ZElYm7Msb8<&Duw zQ<1_`E7?VEHJ&az%eg@RU;a7eITY6`gxL>`%qf(FI%}(Ib#+mKmZ$4$3X7~T$1}(2 zIwwNCmUBa}yq;A8nu)pjiosOyn&JKZKk8t_;M$|j=eykFj~;2eDUWL*U!;t zB|S2eB>`5^_~ix-s?}kO3s3+Gka5hmJ@8NWDwdvC8uj6Ure(>(q9JF?wX|Tt(UP<* z*`1sMjT<0X$CDn7s`XLCjThUl&1nE6A%q5{Tspmr zfDCn|v&F;?+rAuWIW!tQBu;aCpr)dtI_beDJ>J}_*_miUqGWDOixKjUFtU&UC7M^E zvAJ5c$M{?}#aAQ+7H>T~JgB%|iMr~UTi$A`BEj!%F>%iDa3Fa&ur)Jg_j z-z@<33JCOhYJ0A&cO4j!h*9^q_RY9r*B(u+=YJU+bwdb823~bB()NrIH*X)rpE;X6Fa_6(mNg5&!3+GpeYDqN|oWYVNUMa_Dl_FRBA05KVuxf9jP3s zJ1s0MJYC8KAbO+SDNlzc)?9;aj?XPK$TJfL6O{>p9$hsT7+d18>g`#MQxNK(XIgeh zR#T=t`z!>gN7CePfm8kCM;tIji>k&S#l)Su{tr!G0aVrZwT((iBM8!6(jnbQcQ+{A z-6qkC_66K=G|-iviHUG2tCclMoP$0(NzHvBmXo znNr{}7)m|r&J#l=ESxYuKDzn0p6b4H zN4usifH>7KfLXuE?$hqnimXi8FZa0>sp%4C*nMLQv5-ww8beeMz;mXa4!Z9-js>1$ z#W^@Rby`ebhlCsa{O(I;esvz7Z%z0LF>5%MXGIdg^I+a&CxfGb!AqV+HzdH93O;PQ zHMlnD{IFpFRUBh&Yf!cT zTk^SS0T8mAK%CNJQa{z=>e;{Sf(dj`IXV4%n)0yAxntcHlSsaKA;1#x)d~-k+4f^$ z6AW7Bj(w;~jYffi8rkQHKNGxcoT87u#~o6Q+YG(^G$)U~t~$Du(=(?as*vNDh~=F# zU!$>K>Pi4CXJa59n*>K%PL2rh8ozEYD@Qa6A8s)zK?{dN7uR?bzw(ZE#}7c5-DBAc z&j=OYepSFTAOVyQBe~H~bsY_vZ2n6?fe6JCe%R{pH2#pxyjAaY`Z5fiNEXN+pPd^( zDb^0_kM_Hb{H%NPCkd0?zn`<{Kbv6yXu&QvJeyCq+r0L6=hAqN^+Bm8YW6jug$;1u z08`$|52wv*d3z4Ra7#hMwdnbz;Nt4C^REM+Z(bJ!Ko7t%!CYUU;*S(Q&4+m3&d)vH zBruuloX$xLX9f6GI54TDQ1ARgD-4#QNxyTzKVXD%6b^7^dK1W~61tBI(QiNM{T8xz8^>K(@H-XA^;%BQZOB zki>IY8X$@{AV~n&M<-laiwxVw0R2HI?abcS*T-e^2kmD}1MyZ;Mjf~wG*XurNNQIo z=3(TYh6j zeG1&QXFz6vv}5kS&X>HPHEuxC0^p^`vFq_m&3^m+RFd1lpX{KJu*rN=e77YPJz^o3 z2myz-uqr!8$hVx1TTZ7{Hi@tr%X+8o@gfp|A);E5@ln&G?fL=Z^`AjPV9CZXF+Ri6 z?U!M5CcVFY2K|bNirRj--gYX;B0dMmI}SK_h}pCRqN1WA;vzQyz+ZW zk!F;jf{I=CCk!yM&OCj~c9zNV&&;ophmjF9qcovLIe(jsexMBmf08|9vtWN8E_OM; zWDz^whXI;*(Qmr=$?{lmqp+-%_kLsqa)FpnbMZ#6Osyy?7&(|eB1#7p(#-%o{O@8lEsF95#6T%lHdp8NX;HZLK(?Pz&!}>^lzPL9bEzx zUcKp^1y5>%1k&}{?_VU@xxgck{`D)PpvbEEtq>=Cwo>?v4Gj-n=J!8lTGa;YTxM`! z%m7vP(V!cNA3idb@IQ@5mxpTew3Lxpg!gF|G zFR!dj1%5Tn%9&m(wnW=8_xB1VkN4S_HDzI8v0kO^T;tj|7N+`_4K4mUf`q;ZUHAxfvEIgpqFOj_D`e@rXG4maxYChdqtnrr zumJa8k#{t=PBllapI{=GsG|-sKv2pS?C(Kn1RaiKqT3DZ4hP#P3N!@M5P!O({WJ1Z zR>K?Rk1BH$U|be2Z5&O2_c#Kkww$T%T>4B+tF?HYwsS@gU7jp8nd*W%2O!G5xx%}6RDIoDU z|2OVqaz8CZVSXcbW)}adx&o^oLO@jLQAd-2MRa=X{v41q7z5#RXf*W3>w<{O;onQu zH?m4{o4M|lyr1H^t13{`1Ji$pU|!~f$G@(W`|0jV(QdvyMn`_9G2iWjW9e!DSu zPK#wbjXEMuhfWF!P0i(ieaZN{H#oIykD1B@Pot%hv-OTEN==$%NMKt74{f#f2V<0= zFrz>cPZ;J|^YO)QQs~uqGGLX?RN}7?AdFExxqLE_QT_U= z|C{pR1lSem>G_w49_^RZnty`0_hJtkuc^X5(DucFc|p%HMA&8*Tmcz9wk zlu3mLZ_1}CV?~QFUz_4;{nlQ4T=U9{vFzuZ|mv3LWRzZuG3F@RPFOukAcLxb&!oi)){* z$vqz);F1WFk|n32iUr6lVC>Caa;I{OeQ)2ME%+Qg7QlZ5FQRap%Y9^JW%ZrcfRp(K zMISFo-_oTZKR+S7HkN?KfAge)gX%c?q=fy~dwo?+`{3$)=eD(X_Z=UWfru4XlI7D$ zTRP~f_2cb9gJrt&jn01w9el)!(^IGiy_tIcXXQZ2zf?yXIAi5z5uVh8Zix#ASC(?! zj`=Du2e6?F4SWev^mO7OjcHg##V5>yef0MlWxp*=X@NqHsP?~7K*Qua+ke-PtP2a1 z8XUM1EcX#{=x63KziP;oUG|-f9ztUsw*+@f4q3@O3}4#(?J|gkX)*;9{)|n1jg+ZhKQGS_9}Oy)@I;+CEWca7ajOc=TG2oT%Y?I}XE< zzy?fBi9k9PPA#PR`FBtiq>)U`?*q@H`4US~+D7XbF=g)MmlQUKuf58+l7KZ$c-yy) zQ(HEst($NdaP~fm$uzl|CvT3Bknjo55%UpJs`b{9K*=zct7$4$S_1qyNH;;-;J|~E zirVe-4`2!=UwC z{@s8esa2;3{*JNx@o$J8!U1XE(3&?vrKKO%F=LFH3D!(et>QL~oUsv0FgKguv-@T9 zgIwQS-w=tx0<8Du71v5GJzT7U6{=W&nN7Pxx-vlXWG5fO)E_^tMgnl8^4w| zz##KRXJi|Sxl`>PD2{6Gfu%mn+I6JkZ*3kMI)5#^5Y^PsnDND|E=WuLAbFQ={k)z8 z|GdF&QmlLMj285@wX*t)1>b$;2i*!6R6TLg84U~?H?i=V34btFu+nSVSE!ZF=@TJ8 z$%ua|i5=1n_pSz}W4tQ!P*@SAXkGvip3e6+K`tOOqPboBEsZgOL7cYA!I&{DzGWV| zeY^K)BPpu2i4-!c4J7qIu=RKi)kzV%u72RaMoj1>%ZV zP5VbS8ZIg7=gB|(>7pvgc_9oe(7es;uJx&sLgT0ohd=GE>e<owjqGd<$wC(61|# z_%9B+5auq_-xFAu6aVcxH--@{jP?0$#5R%XkvP!i!OX#BT7CaWY}#0h{yKCCiAGks z#buH{%0%Bx-Ry2XqYv_vb{IUXzV&QeC|DETe`5%il}9TWI8h`|ZmulO9RuL383qkn zy=ET=9p)ij{~*XLZD$?$XG?!2HUq?jk*fN9bWA; z*R%{Qjdef#4L(MISN#vk(7A=w!8WylfELK$O^CI`Z3ArgIQc?0;{vt+X>@!4*lIG^6r=)^q8l zIsUMUJ=GWUIl%^y%8=~$rt>AVTDbEiZ&K_lK4;3!ubWWUQvyEyK@&m-EgOHYpM{7k z?T=CHV&rdOrVC+6IgGMc5RY6ph+wQEFKuQi*t$Cqq35Oqb&cY|LL0NAM8GS_fHNXK!4w9Y+RD%Q zI}b1j?!88xRgm&gpU{8wxJ){E-&dV&J0+qI5JI!-qF{<34mC+) zk;M0p)C|XsMbpopHuTjmpf8vsII!QfyQ3E3mldCq-Juo=2A|tU%_$0#9?Om1kn8CS z)C^Zlr|=qWURBBV+OOAqa~^h*S?w_OyfZapWCeF5!WMn6Mi}BqguZlt5-pu8Y!ngL zy+B-Oj32u?D4g5JhRarxmc?j+s(A8iRy#z^`yGwNy)KweD!Oy{|5|_#{9OV++Vp?W zY1_@v*q1aa4mb<5ndwyH`5L3-v{k;*)6)EUkMarsN3{tnU2{d{;S2-4!`xW@5JKkZ zm`7EVXimjX_b$a`#?6`X&6(28Hjj#mX8Mq&nviNJ z|6P(LQyoLm;kdfThx&D`c>mnSB|-P53VMeJVHYb_I%VR{qvNShY^{E&M%)&->-@%V zPUo#Q6?t%=WSj1bG8&rh7-)oqJSeK)u_RxJVPQ*7!fX~;D3M908WM-^+Wl)D>SvO!jT(+!?_qoHKE4FLcLnl+ z2Df`&xt=UnJ~V^Nkh>PRgT>CAgQ}50_w2XO)njH`shsFI9<0Sh1uoMt@F6Apiye0Q zgSoH{dbBZsak4Z=c6R%l9Wt0?Sii5LsxxwA^QqfVX}^ZoBjL+tWVqo>TfuNXY>zqb zY_wK`rJ4LaT)}sIfuX~{iPi#B&9Ti+!Z}T0*Ih?_v>}sTA--;xxGG(#pZqNKF4yTd zLf+L85=s`(bNpL8-%h&5+Fi>3tA8lV(vPM)Yhv+yNPXwRh5G+aZLrV(4L#Z{VvO;t z&c(mm(7*6`#ht1$NwA}WWf%QE7_Q<$EmG;)eBtFc^1rE!2Ma(JCcaGhoqV0EsF~2< z4Zl5Wo2SAq)da3WO~ zf}W1-FfDKnN)+mxIYR2Z+adeeSr z@_1O_n=Y7K<9i1Q*y=!<*u79nb;ulU+H`xGm3>o?2Bl@(EFl;q zeuS58XiJjkzHEGGY$Ro0sWh`ycd?dFBxc#?ex+(7x88NDBp1a|ShT@u8pea9HJSn{ z4&QY*_VcMP8z(<5XBB@+rUgkte2NwSZ%h0+yd0N9Px)9HhWJT8W)e4t1~qAN^Vh6n zHukT|44ye8KgWOgkBT;0ptjM+x6Grhm4wsm;BV$zj}|o|)cfBzNemy)74u)}3=8Z3 zkbhmC89D5md%3=MQo;V~!<$M^)|jhrwfW8h>uIaL<0<+4)7?I=JWazO1)w;~S3NuN zx6vXKXaUQ_0I4%J-FZLeovOCD*~K*)-i`mGn5c)MY2@LunY^R5t%R0_nI^5a zR$UoNLjiduN>qi3rrj=&xn#GE@+m{*d#$?wUCo1m%HL5jFMq%PT@ef_nD<&B)k`vg zeV9W_v%cd!3Z7(l)o&eKf_+VTf+UxQzqwwKT&^rBPk$dYH)cgGC-9ATX!8j;YhF%9 z7hp-ca!yV(Gds5$d8wwaDau!GzJb$7coM+W*Q}V$Tj69qnZYMZ%bvvS+lf3e^;S3k2xVbXZ^MCC&%HIo4i+xd)@1BS^7IxBk?Iv1o zgCFW_9DC*7#Kq;nucU2hj5JDL@=$6*cw+WzTLT4!;^*dZvO}_)S|cZ4mRIQ3z!m4= ziBUrpp0TIw26s;?JLe*FgvR&LPahs`hVqMz)nZPW*AvB5W_g|==fmJ$H4fY#E3V0h zpydnid)+W}|BUHVy6ihN#8Py(Eg$&e%7+tu=0aq&Bt!4ti-9B?=l&R!X0z_kTz0Y+dn=>{! zkfq;*;Wh|V@wXy(l$h56iL{D(Sj2DSL`BH~`SbbqKUv@&YQBlqK)W<0dhS%yZ`9|i zNz1jW@kj)g#7&Z0%V9&d8uNWxdtEJaz$VH=8R6WS#cXBMo-ec{)ia3$~EeD2*4 z+twv4pQ?V*?_#<8OzdJKyy&1Wz>#z-_)5K)*4B!WW}c}>#gnTxVq9CQYv!FHty$u& zVV-0AH~5;n%EWtdlE?#{iz})}MGX}8cKTt*-wBpKCc&Pc7Vo*Q?=HpELU)+ES`PfF zma4^{GNVjwBj41D85{DbH-G_hxNCDiR;<=@e-S~inW0`9r{3X%rYy+!1&{gP$ywcF z+GtDYtTDt6A&2PX4V?MVcRH3MUOaggpv9(uh~XFol#>DVEc1nz7?#)iV9?DS5*!vD z=~#VXc$pDZSZY{@WQ&Wc!sF>a<#@ea3yRzBEf!pIr3Un-{#V+lh2D?OXuzDD89W(L z)6u;RtVcO4{LNU~DPJBTgU6Bu@UE_wZT?n|!@-~3wI&>}1n_jgT30W$us`Z7MMWp% zHb40}uy||50Xx;nwWQaFymUL*s~?Mr`R`?aOkDPn@xsM?d;3EC}2u;AHO zEFEjUFZ%)4_iHqi?UD56mv>-Px|p4vsl~`m{@=G9J!J+h{Pqh=zrZ>)@+7088lKwd z9!R@*(4I3y`m425p;2zM`BhY+kosF{#*9E&#hxZb+D>AoCd1mi0tlaDzX_ddDb!_D zQ%`JhdL({Z@V+(q^!yweL>dnu!Qm@=qMMt-1n>ai;o&(Bl0B^!t{gyGQGW{a6Yf8y zXAD;|62u>#-d2vg9vKK0&`)+gbhL9FDCYP+nlA^~(rZ;((nJq#N- z@o94*mwo;6#awlnie{*Gn2^K@9ny))_&_B49gt3j|>>s|#dq$J@Ksv1_e7w_Oguhf&s%EWY%X(c= z$|LIDqog)+68z`!+SH?})KlSAPOQDu#?S}Y?sB-7*PvbPrR2s{?6F@Sd6=QQNB0!QLHCf?~_7W zaTGHlOjBu-4d(j580dABo?~WV{xXlA!*-6P)O`WtPmN*jmoMZ1o);orsS{I|OJ(_` z?}*`^Ig%^no8j0+@+;L?u|#!aJCu+UUMvsTji{*&EPlGi^P=N5*qQg8h4%Jy=euWO zeEj%*dnj36+w)ZLTUJ&t&(qykKrKujr{g1|MmO7-N&#A40OCl=-^$?%Rd97^(is7v zAwf#l9#k9+F$?U98IWExj?-)7zy?$4Qx-~aJaFEGt^qGUhntfzixGdIJ)KwdXSR2z zn8-zXKlFI2ltcmp0VogLTsZOlF(_}}pPiPjG+1>rfsN^*Hh9KnQINQaLiNPvr5F!< zeqLKKH8PsRA_{y^dK>rYRvfzUp5Y)=Lk}g_-f*BV#!a}Tji|cv&-!xI%njA!*V?v{ zQ?mX~C;6F!)uzKI*_nfZ-D%hax+w#Y*L& zGA7)6zJ*JRs<{2NbM|9wQlqHm<_!R%QTyuY&b|Jd&AedBmGQUvvtiH%s}Zj>V1vNc z&iY^M@82$e;_XM;TR5}B(2y!BTn=_9GCtVGcJJ^lrIRCH1U}NRWQhy|KOM?}vXTQIZ%<%Xe01IMD4QvKm6eyy1vCk4-7wz4U2y_f4bAAruuA#d3BY0Mc`p zVJ0}08UqC28N>XU!n_YNIq)qiDS1|DhIO1_WFPN$pP8eADdK)?->D;|?*v2Wqlkt7 z%Z-=r>h6<=ma7VdX@>`j0%TpM-tTigqdAGda(qPvF<_{aMFOH-q3h*Z9e7mB??ypI zGhMtN0?MoLjrt$9bI?m{GAt6qP{6H#q|4W6XswXwlKiz-?Kz|H$AoV`vX7^!_Sh3p z)6g_CAiWH+GnLWPL;2L^yga=dz+%J;bym<&1qic0cEj-4^`&T{+p}yV{!UW$W8%kN zafc~DO_XVrKl3$bY21ID$)@*Fcs4;6QvxWOAa=UOGOu4?W%Xz6Qxs@WV_y3ND`mI2 zuH6$nGkIUX4u`gmd5LSzyo?eIe1ZedHXYb-*Of>-9pd9^#78sS0W#>ef5kRq`=DaXqlJd=~j^L3w@Q(27j0-y4qcZEe5=fO%-G?g^Qx1=Qh zCFn7#xWu!|OS*K<^=iZ#D2d?~gj)SzR`0cI!l>oAY?m`wJ2MpeqBMcP9*_u9?ZV#x zAz}X-!`#5CWdZtw(p+5$^Kat>YzW3(T`QgHV z&yw?Mb*EmUozIW0OTl;qRbpi|z0?<1PkO##t6aMaJFrTpx4mVW&v%wqTPWawO~TkK z*tq0j>|x30bRcSF$-s>G!q)I^*zbTyj4ahK*>c(fcHXWVw z)==NeJFw}S^i`OdTi_A4{a(9rx%mAe{w^d~f{iein#R`yiY1vK_qOs|Vxj`bryJnd z8kWxg{T%cVnj@{6ra&uvyJK6xsAVx-(iIs&KMKx3mrqX)%Y=Lvt$Xm90vmL>ig`Yl zdiR3n!3+L_pWA15FD?Quo)^V)>>XkVP>vk0XM6UuIsG&R!smkNU2A7c{Sgo@%rNM8 zx)uMde_bG z@>02|w3z*%xU}{*i_7jt7_d5b7P_>*>}W@<+Dm+ksFDAmF!yzD2KU$4QoLvT>YDz< z^3F-)6PqRH)T5?IO)_Ap3v_4RO-%h~vuGA0C6fE2nt@YrbvW9k(0@=li+2cWx=eNS z$2o3Z{ebUfRx8F(^}^z-pZDI4Szu?^hulkmR3CJrovS;~~8B+-fX6f^pbr#FsRknT z`%>POg(gPtI~7egev(9`c%{S%fD>K%xcPK&vV_yiK{ygOG{fnmz>+~j!~s<$mGl)p zn0Xver7r_n$`-3@DohewN%{9*3z4Q~6dI-Vel7I)AE=o?cj9t&U|srxMg-%>S9)(P zMKA*fJ*4~hl|6!?3Ndo9|J3Rl4?d9aRBCxTgEAVQ)u?S>67-{m!o=Z5wU$6O0ibsx z+3z~-b~uX1%}Jmf;^3@Zl2KzUAqB-TIc*8)`L>c}KiakL)Y`8*ug?=3VAJ^qIvrQ& z7^tKQ=+AU=9|e~j*_Oz5M!|jl8-ODv0%-bcckgfWjU{zvrv_?jiS*hmQQ^KKvd}x9 zI{yW!**17IW^3NDLqO{%P$wKNa68{#yaqP3c?h#|V(xbeIR-!Q4z<5dvv(tdYU zC*Fkw>d({~q1}I%6a@v0pMMNx@wrA#7D_p-_2_D7(y+4!4X3iWtZ!l;wN`RIQ@pJG zoytlS_0@LWf#--h;M9-tyTbvl8&NsAHwg*m-lm&Y!VfpqCbiyRl~h#lFU1;I&s4m6 z%c!+>9eOlGCx%sSM1`oS$r~_orSJ!0C=Jf+N-dVc-r;@+4vbfcwWb!E0|oS(zqMY* zP&n9p2%$fKj_@0nlW*yXUi{&HkOSHPd@H53YSo4Kl2SkR zdJEa>+aVV0lN6uJqI&i?>E?620243~NtwGmpD;kgwUJKKSoRh>vWouR^$NEQC(2_b z2|l5%kWszM&AaDSaNTdMM2=~`l&qg&sFqRaUwkH(rNyQwPPg?o0{d;|ke-fi)6vTx)9e8X@6aRE3uappM_Z@yWJF#)nS;LMfl>rJgdf zg`K@vf*0JTVWuMrva(?3+T*bbrk8W5{|X?ZrM7EYPR^tbUr$^;Xeg>>xn?CxLh-Q6 zyks^7GGzyucg9rlFq{8gk@$u@Z_L;So7wGgkpRyH?P581AHf@X>3C9;r$@sa3~5zd zjz$iUSwQns7zKHa|LC zW`Y1CBaRrBPV*;jb=r8(6cPVhYMidSU|#`|pBu`8#s#qiV1vIp6!^?V2y>p|1p=DT zd4m(rMz2#xCZl^I&~!~(Fi~()RjWvZ(MMhEPUi~y=bFxqRo|cddXkYbXqv>gSd`J`nd=CfF~t-1ro82Hs)J6evw$TU<~L+vNL+VkPmrm5TLM zz`fIcQcC$So;F$Q@#>#e?2AtqOH0`c(%Tl(u9IjGLw@Z8k~%3Fc;!E~F-6myo$`** zE1oZ&vrHCeLD_w?lkEx!r1leN9}^gs$UGOWyZ;p{EOvEFn37fM#1Nr}m#mzEST&c*iTd)eRUO*B-A` z8%7D1IdSgJ_!l=@p0~tca;L|8T_|LGR3C6dFjLquX*@Q3(CVs6!x@6EtK=J+_gWR= z_q*z$DX7SGEfAfN9HeRBaRCJ#FAkZul|TTk2=3i`duL#KSDH#FHfNt~M)*B!v0i<3 zw(PsnJ!5w3*^M|orPkuME9mS@7a1*}y4l5Px!w%aG}<)=)O>si&xLU#IK6^?gm7@`s2|Q zMhUm#!U)?z@qF zDq#=@E=BdP%5OYeWTw0u6>KK(BhftvSqFfWRn)yEguM^K%iPRqbmLqNa|4 z9P%4!q5(OCQTQwG>&(DoaZ>|_Y}k0@d;#w7hC;PZu*7UcG$!B61Y>zolkcN>CRc^O@B+s z#R^GSW42$h)bpTqtkMnBA)Z~ zNyCi>E{>F;IRinZ(ZcZl{(io()M?+@%ZmYM3r|n!gpPO!75~(b$b&;gb}*83_+1Pw z_@Bs7nQM44@>Y|#Kt47&!n7<(A@qWPLKAhMqk2}rAu*bSJG?h)XjrFX z9|o@r;n=-%c1SYN)C5B7Zo4rGpo&{uGytU;*u}_&gbp(rj2F?sFM(R+#;U`Xw9Tw{ ze&EWQkem)|g5Ecu7pQI4f_0zuM7j{ewzziKEZy&hWQvg7=M`wT(i!_8Fv zoP{*?pS8S?&^>i3OSWZClWDdX2UU+e``X?7*Klc|7*?S56G#C{+7gAwFtHG0Xtk%X zdCv_y@RUu~K4JCr^uV@Cj~8jzfIxjD`j^zpiQ{t8EC4h8u+^FQvQq{E zSm2HN41#xvEBl_?VuG`E^kySn^mpr^I|15<~d#7Q{S# z8msN9119sP|8}Ahp*35grPHdl0j5e|C(_bFeAd=3mT!P}>TM$*zAEnS5p*l z0XF6)kA%iu96d=>(4kaX=y9lXb=X)=qNZ-Q;_l`~gF*67TH#P9)5KzotLaM}z5$+o zlxbg7=!O58_wAK`TqI6c-seIKLD+>T$YC3)Z4C5aTqqt6`BS+PQB{QomS3R=jIgk~ z7wAm$jGrhp%CP)L#bll;Z}{_aHu&o+{QtiefZ#7|?<~M&PFf6?FDdKi03-gX;Q;A3 zM@mgr%0zOJz_B9gv$fk~CSh}V<&O0~*c>A*XzhjQ_63zLw|SOZgrcx7r$plkcbk=nu`=+E;tq0tB>-b4J#0)Ta!93((ouC$U8w?FCO0i;VjGzz@bwm>x6A0Qmb?`{eXcneqq0+Rt-X3T;L zdJfN3r4gBgzmSn3s*xM#X?E@E=;PzV;U2O!59WY`&n|~~q=2ClLdD%HOw;-_-AicV z%RA!D412_o2;_0>Br?A;%wnoxwxi+6-wjautBL~P8xRZC89wf8L!Erf$m!Z2ul?2# zWXOOts&z4k%6!!+6$hvGto1=6J8D^R7TUwJuD<#V5d22O^QdX&GyZ&w>&vNH-$Awf&_4rBdg^Mi2X4wFK>~IDWYid`wsvD+Hacg z$^!`rpRqM+jiSNh*nRk|esM69`t_@PJ6kg=Xw=O>@W6%tx_1~F$>**WB zR+;+tvwyxI_av@aeSiSAjXHJ?5-EXfc}cBNO1rA340GW0@Mc^G=8F+@ZHEf>?5 zJ94*A>2gdr!rTGyT_QOQ@#Fanq~f~iKs^7IPo&9iJ}$vAaOl;No+&ppF()Ccfc{UJ zZaC1%I>XKoKB{j1m|KBvOger`@i^3#PE36Bx5*Rf+VeQnSQM^W-(3{zV<&870AYyR z>hJegNFGVwzl&x9wE5k;UIE9BUVzAYmuVXVya|+Y861cA&=Z$WR`JMVPkgVez*DnY z@t|FM;~Gz9w%%!9l<5&h*PziI(oEW5N$6bvjkOeOu;9 zD9Fe?KqBng(LMu^J!taW@^dcWyI9(Iv1YP^d8DL44|^i+RuDHRZf2&N<7Y!&U$6L% z-ERUtK1gTk6v!DYM(AZ8<<|}Grbf|+pk7|ihP#3|fhb?bvdKQ(&Bi(eTmbkJsQyV# zPo2j`D05`w8;ixNRU5f!3kOFnxhV^{o?G<3XsAxTiE6$(D5buE0WxlGTQBb@L9mV2 zZ>Pj099_1+ZO!aT`lyJ+2)hne?1#Hwov^ajd{w+=0WOv=?z?B0XUA zjC6S0u|(-KnE0W3bu@3Zy0*JHXc|LuPKwwOG*g+%*Q8&KC;VVWy+8XP>*i+urpCZz z0TN5?tI{=^_DEH#L;~GO2+4AhHTPNX#Rkc{pY@trh_aOiPEEt-JA5P8sx5wF#gKP*Q#y> zs8_Un>f)<}421xVT5Jgg1tI<0*(^IVU5nX~!7AO37|;i+^EWUtFg(-wY3g+G-dSa| zG&Cneb&r2))N?aF>;uN@C+ysEz5zyNsW+Xd)O6Hg?ISI}{m2b`pPuzVs(pLy8}ZBl zPprvQ<4_P`P~@rV^WZvPU(Q|DO3756Dkr~Zz!zZkl^Dw^LTyX8BGAP$|s}!*p z`iIK3PZWLd{2ooqED&vsdGvWZ=J6r8nSnqatijWVURGJqWXsJEw%@GF z0PAZ%pn2;WA`n)eC=pFxJ99Fud7!hlrXx%GwYK~X29i?ZezeADcGHE~*rFopNq`sG z^eJqee9kPY2E6Y5ai;Of^|j^t*YPFNJv@S8SC zeU!MG8f9v&L6OoD;&YN8-~~WHi&SBxm4m7#E zuC!wtoq&e{+r$LuZG79ZIwOxebw? zSmJLvMHdSWYps%(#S%R?)&qzmp}pETbp%8xj%0e@W7p-h5TPiet!De-8oz&_e8`Bv zh+UO&p|qGkf8X)NCK^d?#J@{oM(@8G4U#W#m7$_98q5iT3g$$99pEVFs@CI9h3``U z3mZU1Rf=uUJ0BbnJ4p9LK>^6$k+ZmXpg)^7*)c!`d@ui%C#8uvBD!wBZEx!TQy~w$ zKlf+T@PJ7^)KJag_l{8(AsF;r8Q&h$@~pSnn$o}GH^A%{f5YkZN-=%y1w~-1AS=cm z&s{hfu?cG8*4`5{BZoZRc=0h9E(MHWX~Onz%!j{2J6SN_5EJ)Y<8-zzBl5NqcJt*86W2=MEn`RCU{PzqHkRo<>37%{SK zFTsDDWdB;(^!?e*pap#4-XKOGzKwrTx^cVOdajcym>iT?zi%&X(?sT$bbj1i^8t2Q z9GJ#+pT-G+d2ndC<4CGyYZ$M=Z(cn9aDKhKnG+cD{*+I{J;4QnNMOdmTK(M=Lna}i zfA;$^4r9OpC4H&n3 z#Ra`0?J)n&%R^*L#3{Ihq^aRzHm_)nZ4>|6d6p(lZ(2tGWM#{FKv2uDUj(ZokUU(8 z?7EqZd~s9yTOfI0wxF67|1@IpP;iAy`M135NB&o9v?AQ4rMNN#`*~_k^n4A;T=J6) zTKHu1%7rH~Ci1BO_Wfp5@9E@N?W6GT3e)&#*}#e{1Rb&upp{voovUv^+GblT47##g zpoV3_;|wPMG(q%xbg;wR@zgP{&Rmc`@R2FVqsh-Rl^_U;)FCbus>{l6Aa@_=YSFM{BI~0Abk5ju}w6~qT)R{pyvH|R4Y=h z+lT>LbaSv3!9B;uKDHAe1lJ|Ak||yivR?^*pcH9fN=nX__3<$xCj}KGP&S1DV0~*y z^EEPZtNDHYhsTARsewO4l?ib=*iF-NpHEhiJRK&Rgt+_IC?eZMHL}R?!#-%`OQ)EKyr|f;;?0Kj7OlwUC0O-T6*? zQzQLE48Y9P!1k6DjJz)mL2mWn&lmBneP@-bRAHbvF&{rVRLIm~L-73koy9P$Y)9@L zniRlj_&m9DzJ4t*bT)8y@8)`k_GJ}sjsX}G-E9UMyw6t<&LIE6?&;FKUG#OK;Y7C`W4Ee`FIF{h;7l6+v z0-ir_diTa;#24ZDaNsy74k%+8iUF_?x5It(mFLp%-|=N;?wc&|xKD1WSBm*mn_>NE z(*NW&asE%E?&QbE=M%?j zKjOj%bTr`yD~*>a+M;M`d6^imo97s}rSVLwzx&tl%JGA(aT&XWrM98P_VRsI0OV7* z&s~rCDU>RT^68C!w`RbIROC-9Ke3weSiKW|ELNExLzGD z4MwcJLhb$1N_c@t6};+z92->;K_3ZJBrE6aJ|u`8-2|qLTwGbFI2IsKRL*4mZHaPr zZ;sHy;LuQe&6fchg4_~|t=U2GEg^HM7q(&eNbXIq!@PcR64OVmPqM*rcO8soK3SY^ zcSToGQBj2iC|{m5D{Gkn$L9c`DWj^Wo~K$E??GEIbhiPTm&L!OOSwbN%p6J)a);My ztf@J5j*b^adA{^@T;8ujD0Tr;nj6~@_E^qxM7r$6IjA03<|RQk(BHJv|2#5LqoUZc=7k{MGsw4 zRD}!WE{b`GL^(e8;Bv40n7?vw?^LrLuI+l9i!f*51pm}Xi^0fHo|qZmdxX$dgx7~e z)8A0chZ^^8#305ji!*t8GQ2OHC_fAy)|9`@WXbwWufo9FJh7p9cRAGjdMHNk#~#PQ zR+G2$j=Bz$g+8KKHU^)-egz(rxOmTmEI$V6V)Vh;0|HX`)Y+H^P_HJEVpfI;9)wmTnM`?(XjHZt3o> zcl&?W^MU1Ben4l>zVEBf>pb@9KsIeqzkz)%f(BT(B1Ny2NKe0q&mWY~5E_4fI zs*$mK1mT-A2)V)K4n?DnEn^z9U_jVei+9&4Hm`XRc^ks1 z=_(^u@H@pr`N!D1IDG*^TV>Z^Wf0@T!P(hP@((w!H3w4$Bq=nvCM6m98$xk^WfLe# zuKnuES*4${-lYdqN!4$Dp9mxP^(*MI)@Cw@N&=%fE1c?S;3+2Nt^Zq{={dmD#rAz) zn@jM?BpQxAi=HV`b{c+y#jv@`jUUQ-s~UsS^sK%J>_BQk934wm_yhx^)FzJPcc;1r zzHbR!N(gPehRl76sOCsedzqt_TBBbyR}79 zIAqNTdZ=DI6a}7=ha+@d#^=XZL8xKaPglw{&Unb;f;Mhkz>V^0GmiM!Tr(S^$|P6w zaqx%n(s77_)J{rXhdnn>mzKn9h~&Dp3#q%*-!Z;(@LWY4cTBou+(9vWxoU>~SG55{ z);N=@-?WY((kvbi7B7c;qr5xf(f8qD(K!kfRnNIl67Ekv-+DI$3N#Onu4& zHpo6Hmbzh9&Bhx85(GeT?)`de+Hm!6gzN`PN)AX@0`PByag& zO2oHrLH=rnPR8T^J3C*{_bCg3^^(Uk|1bmuiw~j$(~Y?d0*&Exu&XnmbGb&zf+e9! zp0`hcz>$|Ru!6Jn&r0`U`jF%g`ZO?WoHc7)YC>0b0KZEt5Ij~vrU-!Gk!;P1dM>?3 zp@nnGbDrw1r7;6aQ!S9;2a{G4&!|95QWwn^uHb^ix8bAhU`fz^EMy$qpINpl_UmP%|_T>N8fx+#Vd=y5+Zwrs!pzBc6koHA%F-cPQr{kNOzbjIY~&YQSItP$?0pshKa=?s8;h{h9gjGYxSRI2KZJ za(hvbGP9XGl6|*CdJR8vQHkHhUxAEJv|J_vLkSCzs58vudn=r0-m|!jJ4z>g?fKUL zz2h^214N2@?FsCw@j`;GoioZc?w$5hL_wcz<(YJ9QbJ<)AUVJW_a+PEp}TFA(1toT z9v4bg<^RWW9nVupY==_`MVxgV;1l<7NJht%W~%XiRux4_2hU$^%n2jaoin&IoS8RH z$dsP_IBWY$EpW%vk1fuj#_cWo@!`;Mu=u~Ixg+~>XI4-gXAL#5#uvJDrp3erF}c_Y z<9Rr~5u3N?{t}}Oq9%j;D8-b*#$=oa0&h5@N?=V)VSSI_Kctqu1H_j9Mq+H!d+L-U z>OeyN_Q4PCr=>ey^yQgo#`u?OU8 z1<+z@;T58c%~8kMHm>GSA75T<&Du}^Vzvrx$hZ~)7%F7|A3n)%!^P=awd(@@LLQ9=xx@01+s zbt&=DuTwim-Hnd~q55-G)D#P54~vz@0pK?om-fB0*{NiDuZsr2SE-{Teyp8I6oJJo zi=|^L3=3`l!(%y10#(R_8b&qnNv0*d<*b$IZk+`QBKQQ@xjqLzQO!g>L3`VDoDy`h z>j>Q4;}%s^n5-?UNi6`b;d(x&Jit$rv(Nj@%>5(q6SuAx&ta)<*KH45|Ga`YA=Fin z)jHAsOfndRY^mAmsu%1P27A_lT~QJ0kHVY&W4wimvnuzPz)#qhx&7f+@llxbf*h{) zVfoTj9Y!Yp=~PZLs(Nrrua{)j4@xvR=hy?;>*4$hh8=e0xR9aZB~qi05th?%lf|KM0||-paqdz5rv8 zn?B5;QjCk|qVjU%k;KTNa*N@r6_6+%Xt`(J2m@HaVRQN6Cv3onL#Pnh=tv!#{={Zf zq*XNl0!T!vb0Xs6)X(yQ!g&AKB)^fx6%?To{+wp|r-Cc190=o{QFi8wB(5L#oLB|m zM4pi*=sVHf&Jq{qZp^l9VV<(}#y|kiQ%vwM60{vTN5f zrNJ{6X`S|-g#);H&0t2bb&SSbEDPogiM2*evAMQYJqHfvLE}mkAwi z`fA@^6^Tq)zY!y?jOiHgXR$)`iV>f>p}XmSU<9MlMR>r_D_!xUHM+<|&)ua3fg42A z`A@wJk=YDuYyc!kxN-%}+U3NRR7S8vm9?yoIF#??fVhnHekFV(%um+}{y4ntu zT$o?DSg-yJ6>+u}&C~HD&>)x6U+e9E(W~r(*onm2WH9;-3dd%xH<(akMJIP0zhgiT zu#@~pF3l4Q3H{AcPY9BQS3E&KIUvA6@V!Hth)~@L#DV)H5NGzoC>+WxY$X2gT9CDc zde4?!XXjoRKk9IQECUWi#FZO&%4i8 zB0qsbPJVfe$Sjd)(*AaZ*-}Eeh;Bt%K~N>=s9@caqnSC@YeTidi~_uT*v|c53wTGV z-nIoC;y|^5rZv-8i~guXie6sA+2xhJ_4>VGZIm@x4Y@ z3S?+EScVXj@GwjB4du1zJ|VNeff8l;O-<-%U~0gvbP_){(jS`c?EQ}Rp7XjyVcqcR zXBLl9uV&EgQ7{*g@pmVa7Cl4*gb)~~nHKUw%K*wu+^z%DwVTvMbpGO>684+Qs;k8o z?O&yP{d;b-LXukkPYVF2gpIEjJ62m=Z8&aB&&ORl#Xk98KrM@3Wsp81`m7^=v9z~5 zi978#b5Y`G##FJC2J?a8O0=~}x4o1BIK;Au+P*uMxXizSzdp_5gO=HI)Jby+(Zz65 z@jrIi1RKVn&xAY+C313z^?Ib>2MJ{2L+MrEvdq*jONn@+1Bdw+s}V3@(-TTBkS%6=-r&+I& z`UzKBf*VSl+4?5FGMlKo(V=Kjm#*CFpr7yq&a1_1YG@~)s!Jy^q^#PU#h#Mc805y5 zK`7xFi2#S!fVE1CoR1x(Y{{Ll-6LytGkU?9R-;9{o^8>8>HsH^_1CU$5XRBFo z!97yeJvH|vJ6Rrv(w|yyAf(9Wo?*6c$`OhS(RF_Y3*AT@@{y=K&fg@*&x(u+r)B0^ zWG-z>mx@lgT@)?FKdy#_Q4Eo#+WGRPj(Of(_z4CCO%`vaOS$RCM}djCKg`}qVFJt0_`MfkdwVvIt;mB1>SdgF9nR? zD;*n9%*#N3pMK2**ujZFji$U(l6u`ZH(0kcHHGHM;8Z#eee{01w6tlz6_gja)lo0k z?AFltUOjBahdqfrasFWNN%Rv}!WRA9MM7~vnydoCP&OIg7vqA6n9b?gG>%!KgjKI= z`-H|A)gPbtf4^b9ycvE42&h8X*CzMza9DVUKsPKB=xdPzV!D7 z7bxwA`1ufNzHDDrZnqcujUAjmcLc>qW5(Vk^Y}3M8<_d-OekRc{hFcVI2IMxfj1o> z1I4GO>lkbGA6w_Qw?~DAvHhouHTQ!%{hyRruTAD)t>jY!pMQ0=GH@w$=gS>W)n;&Y z!n+IJp};lc0FM>!B(IEBlpD`^)TR(PA4kA7uB@Qyx+qGv53w%jXFvYhvZBIyPsMh?nKA zX>V8I@)OFEgoZnb<}ma#>f}Z zpuZ%k3XS6mhucCq&-JKEQ~m_*ldyFFodgCCOne@O;AvSxC+NhVvMo)UEckQ!Q-6e2 za2%Hce{^?dp}ShHhxEdoF6q$V-%QC6#c}5nAeJ)#tW{ z{Rowzah93S`|fOwCL5xSn>9N~dTs8vTjedJc|PxcRf=EaX5a#Ue){Nr6Ot)xBnsW7 zh!)%~Apa%!Hi#E5or{b))GQ0+UY)R_GYg3*DfB%%%W=?=RwXJJ4B znXk;$7wlVitV9WuRfcJ5rXgE96tARa{!Q5mH+lW#ESYO4Vc)b074RQ9V2qprEcuk{ z3=b!a;ISGa1&Z}qEv<^+1&WF>GIdTmS_tX;F9;!FX*A4sO(K6>d9m_vLJX9+>((+O zvAjT;}z&9A?9`7Sf7H&_lX9zF%K7q5R0s!>207v$L?VF`I6T@jLR<4^HOS6}lfS=r8E@8Hz~IQFZ!Q|J}!h z)oO8Yaep|>p}2&8XndTUo-b{6mETw^yRwq|(7N6P;HO65@g#0X2}J_2AoF#>h?O$b z0GINpZ`NO>2I14s%)ry_Id%&s26o6zL@q^j3f!j_RPjUD;{Mu4G?p`3W;ejBicd6`oqDDcvf4CeVFkQn?>&i5=Yd*@| zk>tffbOrZCufKHJqE>tRV@rYdS&Lf4Z*hYkR|JH*V)sFXo+cl>m^QLT;l0e0>+mUC zU!h)Gr!eufR)hQU2zj>Fk}e9!Mh%tJc=mlBc(c--=)xFsxpiH!u=Rgdi$@YnWO|%I z<5{g~4-+@&u9R0XaB*ejEYyD|LMt35WQj-1z&c#XuBXbByoXNGQbv8sEV-_6YY^$t{H&m7< zGnG;OgnzjkF3B=*7^a%!YlB3O$TRWf8GQ4TuQA*-&OGx#r*OTLh#S0f%r*nzNDLBBCyZ{8<}B>AV?d&OS*X&_x7sR5m_4 z^EtOpP7B;Z5KA|V@=w*qY8kO{ho3fz_q*DYDjrpM&3%JusHeQ1Ll4w5%n}DAbWng~1-J$tUNbz1WRoko?pN;tnv4EVhLN3}ovT+D{i`GT zcj-nZUm6Vg$*F4Tb~>n{M-KL^&r5Vj&`2!*We|d>=Geam978|M=L<%NP)x&vehbjA z6t@1S!^9QIfqjbL<;-PyWB~=oJ~tCjhA8Fj{XtBu?ef`+bs|gk@_yiKf2cq|9G^8n zufJU8=Fq4tc=N6f?N$HZXVC{ydMzW4sMIXWDHH_*7n!eSa%f)8Y7!+7jj_h#<_kE(x%AIgiMcnEpW~7%g?P- zsWhEXt)z|}*N~%0HkS5m2`BDK_Q!sDZi*|_ZHWb_EdH4C{LsFDw{$@*zn@p{9*--t0`CE>FYM0EzPeV$U#O@m*Mmm{{?4AJPp4 zf)DY3(sWF0Z*NsIpq>>X)6q&r5M?TYO6(U^(|v6?py&tz-o@@MG_G~K&xkmq;%dY? z_Pe)4KThag&Z>nRCiHDiZtx(c`%q{H28s+Ou7=g9Ef^u>8crv@ zOi}T1JKw1{uOw_%1Z>Fum(u93BR6A%kn{Cdyu>t#gpGldE9K!-!Xm^^lMy>@_+i)K zzYh2*=&?}12fSSd`y@nbec|ukv6T6w;RA79xLL0gv2363z%@oe7o%u!({T13)X{-U zvhFZ{|1P@SEt&eD@V%G;_%i8pJ+DdRHFm*4jA|f6 z%CoxLtSB(vf%}UGF%t9|3yULh&!mzXEbtN}m3%qwN*p76e7Qz-eXZMv3ud$nZ3s>5 zD!N_JEbdGDi(KOzbp8nK%`R8NpC;uT#eH5x6tI~ke07CmX zuyJm>*-0Kjz!5b%YL%MGdlxb|!lmNrY>bxlx0eZ9!O$q^M4gVi6wXDTD+p!XNzNE) z?8q@=o)Aj(V^5aBX4$uh>aE`Xg!Ir?MPTwdoVIwkYE=vQu~HsO^rTlMq~&odV>W7s zW1MPB5p-JLh<(jHB#(=tMLrt}+j~w!+zon@E=uriN(XE!J0LpepXu~T3 zw^Bq&nsrowLy=*Hg!qE*Zll zw!=Z-K5OW>^yW2EZM`lre{R+`UXT8rdav)Jq{Fwul0N#p>yG)66mcr}y0P=f$Xiua zSHN7KhL11l4I0M&t%6A`mP*h4^6;!p(;aOdv7)Zl)**}oq#ZcAbdd4^#?O~Dnx;qj z!{lg0qMwgx_c(s0>|KT@nb7M4@{hfj*&_fYGnylI&3#+@!`@>xg8+1Ojp!F8*7WB= z7Js}KH@zi`&@?+v*v&Uct4=a(7I1F;^9?B4_QesTSXZFhGO&uQ%{6P07bZWZ?pa0Q zvq62_MMmRPyk5o?ij){KXH6ZBI*nEn@4fgViIIW=>*N=8e7?}_S!)4*O<%8sd66xN zv5gSMWAq|O#OlDmz%^^Xt)TCyo%OKNoy*J10Rc*O$2Br$W}rjm&kWC)kU{=+_`r@E zkHdmU=>5f7dIWPZJOeylJ!Vew`J>1JT7Se&)oAjRiXjZPy}e^fY^?XoZFT`2(-|=# zJA)=m8qM1!nUfPoj9r+k=zlkLk@kLfZOAzlE5T}b_QX>M4yPTq=RYKRgQLH7_|-cz znYT=oC7C}~S;NVBWKvsqSTdjFpE@v}=c(`7ybF%wO`my$D$)kz2imu+5qYc@0ic#V zK)B|%{~iR$>F5><-mGb&j4+?zp~f-sEaUe=Ck5Q2je%tAq9a50Gv4AE(~ z?p#plAVkLtwgzD%7>kh^dWAXX^k{!@olQaJaub|E+W(quJ0!mvxH1lA3P@en1ma?2 z+kpgl-R)ToFpS@T@4Iqmv*w_6rtuVHW^Q{>z+q0E05~L~U@+?diOUg_<*1obOhfwz z>01V!7umFoRD1&hkgs~SY*GJqDC*|}*9}x|JA%kaWFYPpuWdZv97>RF(yU4?%LBf- zuA^4e#BN!5_9Pg}Y1&CR5=rIwqb9PWsrVAvTK zQ|+91a{EN{+GM7Q;l*NO?7pxteXB4}%3;Z_?FzI`Wg9ZP!t^S1t=AEDs`c1gCkbry1FT` zv7zc{UX#oI^X06`)q+t~0|>shMi-l4DE{jkSpajKUCU_65g!4(H;{@v)z-G)bnD) zrX;S@8yEqKn26ywH9Zo!PG&o6(0uvU_kPlNu0ouR}3Y*O?kmhivFZ$XpiiTYpy(B;JidRHat8B zJePLp{Il!gMOCZ!UpF0on~Gk(sUYExUg*A%3iDaue{ghl*CH)jix20FVM8ftYsfas zc_$PI3980YX|asbPJprPg1{_vL)7_I;(Xdo}?H3nFSu-`xR zv2r5nkILvw=y(?rk08Q)W+Fnl1e%oB-Ph4IZr4H1BS1%$jE06QF-wq(D_SZpztU_< z$HJl*Y6jb*e~JdURs5->ba~%^UmHBK3p8E!)>~}Dnqm>1kyF#s7MraRzj!^_FPraQ z0$BSM-MllJ)WJ?RBMTWg6$<2_j6Ai0Mp!clR3lLNa#Q|H3;Li<<39_)gEHw1_3gi()7rhBYiD3-`_-gZR6)Ny0g{`dsSmrom~ z1ZA0Qc4k8XoSRJ7BQ@~|JbxgWTk}a7+934_eS3ukB;gHfmuOsT4$Mh!XUmIsPHVq` zj^CH?eW#^gj=-!FDw*rvv_8T5FesPez#PyH3qbWqM67>Fh|2~1ZJLhzHVlh!Y%S7K z=k&%R%#uSn=l~wnHUuUw3Rr}ASJu)RpBhLX6@ev<3g`t=H2Vp!cYtS3;(bsq-^Mn2 zr5)8UGbfT9T4BOsL1pJ+8NemDLLC#k@kg(8=#v z@|Dg5%(P{UjPHvR;cAO_imQ#w4hl%l#2=DlnuUdKZL|68GWwJX+VBa)x8)ZtYY4TZ zChhh;Nta)UbT$uOy14yV_H=e;i?6Nf*AxD zSCKro6^+LKI-JzexQfgAevg4v28-)((-M@Jb}&yWDlQlFlO9C$+D z9(+(4)< z&Uk2lk_=p#g!>I{vLF+bmK*LfXyCPsgI z+jkmXZz1)nSj;K~nl3GJ6%-SNhA?aH-2OH^KLwLy^DhdOj3d4VIakMjn>waAl#RR# zOguIsffMgO=3LoYl{Y0TJuWRSf!rk`7Z&?VwLLjCN1D;f)G_lIiK~Jr>G>YnoA{(( z>b`3waD&3h+N$P2J$HUFK;Htv*+V5yB^<1kEENt_D3D=8x%Xr(mn>y(4-b{3?3|+u4pR&(qY)m|IJhW6R9z}zfp-+)Yo57)<;q=)$DF<68bUO6&#nPkN4d6 z?kke+c2{9E;Ba$bpYCwf1SkhWuCAEO|WN z9$`y$nj((h&zXSN0+>`&+qOL#{udpg=>}7ErCRqokiULn`B+H_rbdAm<%+#OVgVWY zw|FVU)KlGHO+&2-qnf)W#c%0;_y4P^h{$G-Iy9h_9$2E!!~=_*zm+tm`fjV$k_Nw! zGQlot`<@yRl!8Zvv-2_$y3h8NK5510F*TaAoEk_pn&sz z#okM>mqgAS{+GC$oGPm_sYCxZj7&)Xbbp^2)W+ptJv=JN7y8oS@0x-gVxTwlkE!$n22kDNN-W zZKF{rI2eE5rt@sYzs?rdd$%udVb?B&3_X)Z&xN$ac%W2mBuc4p*M6mICv1!Qust~@ zj*{%OOw?*nml6L_eDbEdm3C{Sff%Mci)^8V2Q$o<9zX5m(7*O*<+pV;pX2OuD|*l{ zeJwiN(vke->zJTgv(d^=bk6wxetW|}QQEcaC!HU3@ff@`zPQ)YuPM*2pRva3N3t4P zBh{wjlWpZTWgDy>jekYSE-wB#Csm&t1n!;gQrFPjk!2zl}sX2Y6u{6m)*UNSG-=<}UtGhe>M4f@d zi^aE`rCP9M$p{rz#>xS7QRzPmr=<~&aY?yCP8gbP&@NogmLO(eBsmFX;~U`xg`jyX zaZBm(Uto0v7GVd+H#{!KHFoDOED*d@c>~LV3D_~h|E}kq9z065Q=`zQDf$ynqeZPd zQG@UeV+TRoH~0ErD-l*2Bobf_VQAM#MwHW%@oIT7m{T)6oDlgtiuZgBkAykf~iVrDJsK!M8>DVtn5iy_X`R`d9xtM9v{J;=E z8HvDm9&nVXlHReKXWsa+xFzvprA(tCY+tX*fi4IP0JzNn=b5p;V%HBlarfL$2XU@I zsDcap?|-QB?(=*56-JE#!{%J#JD~k_y;}4ByYe?4@cyU0olDP`>%t2k&W{1m;)p>|-}9^8O3-ZSIRrN_>7RtX-4uu5T&9 zmo$e_?I2^5UAxKJTzrr-h1+Mb)|VHfQtc|*Jd@^hMy4Z5@rW%zdLn7=rf%d_P}W!c zJKvSM6oANPuPpb&yzI%g4H#vZid#q0B^MI9D`(>K`GXHw&l{s)GGcSc#Mr_mm5(OG z*ds*M`8vjt#@N3~!Y3YcIzkJei7W3q4_w2*m=ORviGy#A*rP1*Vrsh#%kTp5F0fz` zVswrRPEJlo^ERLop%`)aM#kB6%{+o&(?BQ{YlXE<`hQvgUL<{tKSKXBFeN?Zv=@wM zC&P>06a56GnXKODNF!O9&ff#dFjQ0c?rE4_$M7G#%GT_GUFkuf7U^wv%GU@4j46b?d0F4EO-D>S&c!PJISC@Ol*Ds+F_sACkHrQcHFXVeVpk-f77!gpL^Z zK_#KRs0h7h(wbSj8|x#O?$mLFxh-ErI!6ScnVEGgk0H!0lIP zB)>gLDNok%JLIU?LGW!;uKn%r>mplN4nXNO8n9WsV2vQ*3tbZOkrV5iUvy}bdHQjL zk6X2`_q-heVu~Q1pMRd3{sc@q z|BM{pJLa555~F^m>(i5SpdUl>U_4-4k}bU+T~vpxi`La;1mip69xqhDL`4NQ)QW|T zIK*0s$7YZdKn$rG>mz>$W#h9dDbOF~!E(fpEB6!dre@E~_U%tcZHD1tBU3iT7_(C< z7^y5o;M=xdeq0%@uQc#>_*rdGFEfSJLxE(VL48G3tu(1n&uMH*y zLvG4!_ji7N{wv;MMf*$R+`c01V5?gPi|slye8VG9V$?saW_sNAM;&U%( zqYv6{1i=UX#0%+YThK9)#%5cNK47zDU-TZVetzwV2v<5irZgNB$b_UjLyS+Lg(&eNU1g{tkBS) z(LerI#!?^W{BYp55naejA+Ye@mhBj&5LMV!i(5UvCk_7_>;M~W3wch)T2X;1AyLQW zvO8J^mYkSi228kV_-Eu_0vnW+VY~fED?pv>Pmqn0hPBlwpP$%?-3X#SccF!q%Mf9~ z+sWI9u$j&`0P|e;+q02>PbYguK^QUkaX|$Yip6o!EQIZmD!85%I{Tnqj@ZMZ$?{GEkj)pi1m%(&N#hD+cL&Y|cLRL{ zq`8T5tX6QC+ODV_t`Us-BP#Q=i@%BnMQ&-}>IM$PN$FZ+1E=)g31tO71#NK{j{WzI zdMC{5y|<8!jW>g#El2x44wu-3j2_DSjoz?bL`5~&+yOZ%dU}uGafO%DMmA^u9z}QTuCq=S2{5C7v)mC=BH))%-9!%BNd}qtwVg&h%__~E%hHN3}#S@ zBWxXdbh~1#VP;ASM0#G0zH|!J;*d~lbDXjg{`s@;X{I6;+$Y^0>8&T{k@L&yEJi?M zcp^wul`OVm8A}pCK|%^SY9u~?5%2jf=y|Tz{=8=dG}pH5Ldx<8-3|jgYV6h(h}dsw zVUUPmIuEKar<^ht^t@w{kJYQqdo0%7U-5xU&nE&;Ad~Ky6sGeAzu;GzhVQ~k@1T+g zi+L9G;~J?AYnu+lhm>B3fC=P2`YlS}k_d-6eT;EIcXWO_@}%xugtcLUS(c=8*YT%1dczTM zWTb&CM<%yNG5-4-QU0feG32qFOzmcH+#jnC1gdQAbBn0H$-1N!?HlnN9=9cC>? zJgygTL6LZ(w0OpYlS(H%=04+s?+!pT0ndf?@i}@s=I*ZU?d}@AXDqkX8nafH_9i-3 z5*xQ2Fu5yp#ub_-B&6bFW1Cp5HKzdeVZN*&cVC{=zu~2DiDISej?T4rk}AB$`K(7F zE&kWoui4Mgp|~(qYScd_?vPp6q~2=5v|92=a<(p4Ir@Ket!_BJvvn#{ig!qF+~I34 z19d(RPyXcHFt&`cHggC8x!h@-Sz)39E>)`1VE@+;7HU7THpP>>eCgb+P8%!;2J!6T zX?+~Y0Xnp}hVo3E#4Mho)>P(5xrm7gKGaeSit#yyO2vQXaj^&C6YIO3{?W#Rl~bPY zrvnkh9k>bDD}}Tx!xc>J1S~yZJ)c7<^$H98MbrJ{nmo+=^(-}d9ME;w_sLooA+L9y zf=oBo6fGWh&iM~8q_5}5?eBXkQ9N4=QM`NX{ZVPXz?aH@{qSnFZs=|S)D<*+ZO<2P zE6l7W?>)(_@9e-J{p-WAfKNB>0(hL61sS79B5HeMVti-dTnGdjW}l5qNFe-P4xH0B z_j9KR;uhTfmnZ4*woPey{#^~vyUXvM4{yhSmGUbJKarXfz&q2oTV6^^mY8IDI6DT) z-!j~0XTuy*;b>AT$v<)5wb*LmDo%gidzDyG2bC2t6;pyuEYy90mdQ-RIaE}tJ^y*@ zwZ-tTJ#PjQAt!=ww2&-OeG-o#p-&3x&1?qN#3mAzR`-Q-IyJViB}xugup(Q=jx4@p zPiZf{urrnf7>4H=_2Z~w+})i}s@fmgj*!?0hI;Xu=|50vi7Bkz>_qRTV^;wqEo&rL zTI6_0)p?Fa!$;VClX92K1}v2`6WV}k*hpwmz70^BPC6N*e9qwCdbMYVTK1)Gm;G7(7ltmTJ{u`Iep#0dTG`%k#}KjjX+3 zTOnwqD+k11*gVc8AU2S>Md|)!$C{XpjUaGJ;**l5XQ_>3W;yuL;4X$c-qArwR+*>Wdx9gG8s{MwF=hM{p*0-y;ojq&=Jl+xw z#;sQuz5G8DP>#92U7lQ=WbWH^6#mgNaB&1}pXOPg2HoJ|9SF3;z!qri`>eT5+V?1GYtN>$Ij z8u$2EOYTKtEj2Zi#j96E{vwD{FmpQOPjhYfG~*uB(yjwIPswULJ&XIQ5ED<(5NW$# z!T2!tGaa4ja<&Ak>!9M%k%jAan#`fXd`x8K_f$|F`dTj1TvteIcRT?| znyorzwuQ9az?Zq9RH4yRU7fSIM?_1j=!+LNbdR%=qsUQcCYTQyp{yIDkAnv7#x*|K zwXRVg6RevoRf^s)kc`%b695r+Fhd`U$cWt5Z8L-iS|zzr0+GkIZXw$WywTUp&8aC0 zuSxp4H?YGE)_b7?Z$v*-T)5ML2eftX*0kfob+M4b{Noh)-v0LX{9mGMs$I z8%?=oL|wc08TGtS2d>}U`W8|e5GZX|Wiba{{_wTH8&Fw_`d}lV`1TJJ2n^6GRtVS{k3o5-`U;Wc4%4x z7E~DMR19G6P{plWv~Yol{hzDKmkFEr#nrc2&BJew{obaqlDD@Ut4_z*L_c0qPFU@M z{6(G$sC?XamAzliRB#v{0{0qIc(f8P-3i?u+6j5?dTUbL7pnj!#J?c!`O-yLs$~*Z z-FCa&f#+~-#?*4enaGqmP>daJ*Q^83rK5lZ3y8Iidtsn|7wIHNS%LVv?ANz%eG(tO(vP$_+RXSJ>{`xw+4jC>S@ay8A1&4tQR93g)E*+O1-It5CB&+pSziR8ePut2qfYd`-QBhJ!iP_=mYG`=0dwYng zXPX=kVQNuRVX3~1L61Eeprlvc3qDtEcn7mM%xXI7?g#FZ?B6bAowo{ZyhZR@;bjVp zinxyl=&X9JvRBx2x8ygyGSCD?{R#MGf=t8~3&NzN0tYzWD2B6Yu{rX$9S!k0a!c#U zpFG=XJ!{o0E5BchSKpmU^j}as>9D0dozG)3*ivY&f1ZHT`ftn`8b;d)N|fstF;%fTnL^`Z9f#UkTX&K} zMv96K6K*xbOvSseO@)HTKc!1-mOh+sd4V31ngPgpJWuMf-xhtE3hl-*2h*_r_WTdZ z`n|9-p^^9~NHidYDll6@W$CPy!^iSaJk#yUZ)cbT32E(jc6Q=^mRs_Io_nl8XY~5s zR*U(J_F40-!#K}7LJ$gRPbMY}CPHvwNl{LYC;bmoFW6)-ah6ZBS40aA%Jsv>8`1OX z>}+QlmrnQL{!1dbM*`&5VGayN9CZiwts!fv*sU+!Mo^xYOML7eggjRVDm)jz54kg# zTGt`P#lxCaR^;y<`zBD*4OSs0oS_9(*-%9trCcZOE_?>^IgH?4Gnbz|-rncBAm7lu{VCP8_Q0Rz^cPJPi8F99++? z_6w?}{*|8z&!1HZ6}9se<{2eFr$Y6x7?P-~v$>2bw7za=a37z9CkwuE=PL-6e{ay& z)g7W|7V7ahZ;x>W%N3mUHB654MvjL1#~8z#GUJZxF8ulHh;&;2>K1y=yT8bn`yDYXJ9yXZ+I_2&kkD7bFxq`IhAGopg8rLCQ#Qu*X+dS z=IKCoAiFpxqC?0XmP7#tWiHg_AmHbR7w>nJG5**qAnyV3rDS4b|JhcsYu7oK_A7?6vCq$k*Y(9) zLZnZwyBpEhcWDz_cFj;ABiYXLjclxDaUBV-nf_kZ-NPXCnbFFlc3bl$5_H+Xir{gWR|7Ea~JJ)NXYE~43FKyhlo@{JaYiU?)PvDuC{0}%AB+C;mZ z^U3-HweMyR>iWM{RbF%R>tw9+G3Lee#WcNZ$AR40;lLJw)zLN}uEMXRv@_!?vXtwm+DqZpUQLpFvx^$YJdC+ug^6awLFSalb|q&X#gw&@9~b4|lE{f_>Jj`f54GYo9l!85`q^T5+ge8}Mq z)WAl7rs?QnIda3@K`#tPH;9ss>ZB(LHA02|go7Zlaj~LubwT))usKKkV6|kNJMLi=>S;qh_a$CVgj`rwYHf42P~MpJO|P|9F0Wd zv)Hlt`DR5D&MPZTIh_K@1_K+Y256!$T|R$D#9UXFzNJ*i-q|DDhmhmLXkViQs0^}M z*nfy>Z&P%r23Bmk=juK!SWz`*AK#arAuHXFX{X}E|2;|b&&J0iAG31N4J#@@M~*=_ z4j_aE{$jRc5_txy7@Cb%i^(`Cj7sJceJb37>&o1aS#%yp0@2e^TsnsZDhd%xe4G0{ z4IgFr--yyv+^7RIr$9(Y^s~=PdWAF>RhzuqlKR`-fq;&8AK*^7`{Vc&_1~UQH85xg z1PWbGwF~F+iso@_1qoeG$EfUFY?qqHr#aB*+s;L?xJ;dN^dD$j(|c~;*y-@gvJjQ zX!#_)DlHUd)WDIwNseC1uOekbB`t7o&*?+U9bA6r#;(qtl|Y zVs2xT)711_?PYe0Y<4^3p-|frSxoS8WPwVr5;5TTHQ?B#JQ7*$EM3G&Mgs^pwYj32H>qBQ|kV3O_|%|D&l42nQl4~h^0OC-c{z7$AF-0#n1oSo%=mZey& zHfD>f$>@&&I=wN_s^Sk{vSFN`>L48g(j|g) zcY{btcXxM#ba!{Z!~MPgwPY#YX?miDB4%R*h?>7r^>jFM^Ycq;IcVSiNzivsC`2 z|4Y)5)35VDuHl4f^0l@`C@#!GW4kQeOPIlt@BhNlJCPdcvj6^#2AUPDoN+N*TWbJg z|IAO+q1Ktsv(C-RXu@e;LZU-}8xWM6K6YX~u7S|d&>JlE!ZVvin*!BJ?pE`Yb3o0w|1S<%fL?7;HeC+n)AB`$AHjAtF&` zx1l7{w@~7N{msNvLRU9~a^v7|+xANAXwe-U*UDOP+O{!|WM^4rMwkzs{t0iMx#KZ# zKVC@7a4Krn(e5}TCK8yMnf)%ypD#1XO^3%gL#rr$!VhW2W!!ryzlLMNmyvg z5!Ht-fmLjF`RtI@c3EE&z`I8~mo&JUwIITr;b+DGO}NPC%-51hbdj&sr|QIX=J(4G ztBoM&$S$nl&0=T~>v7bWqkj7y2?%9;gzv`@yN+0BFFn-kMJmtm-^=954-&);ieh#l zd4qqsLgD^Q_&wV17aUZ8nHztFw-q~i+&BmfU{?+{G|jG&5N zm;Vf9C39}%-fC4(;ZW@X2IsU$cs#J`JOL#7Oqvh=viv?0CIZ8@-6`U_y50+_nESw% zzc$&U>s2s`A>!Ch-OyBFl4a2(<45iGKQsWzeCG25V1IOG_AYvjE!pLfui)eRuK{06 zYROE!LK5+(FuskWR|PYkfW{iSKZ9_-GnckV>3-8b$!WLJsdoCWVEA;C@TsbTG4Ss) zxHY+mpt0CFm~rC)1idwx`6xsv5=Qvc@;R7ds*0-Yu`!g7jhXSyc_{E^=Z(7`6|q3b z%x4uWG4TjhELEXcY$FZf$aM}toGf2IAyus_GTbKcdy!ZKIMDhG!8j9*76r1tyVl#j9vvSD!%ajA^w zrsw~8@i77$OI0ia+el0(!Wc_TEY{daj786QoC^fcL{enYC>jd&7i7}Z|NhfHgn{_# zXo*p;n}=)f zPO{T#LbBaIXCsrXWqTgrm9Y71r@KnCs`0G;;k!6tH7&W-?B!m%X`xA~);@s}?~;sd z+T3H4IlibY6DBn`x6ZIA2GVQwDhtx@OBbHL?e$fgJ`P)r8!zP@lrfiLciJF8Ob>L^ zXzU(@ei4jonBHHi#T7+)HjGL~;?ap^XB?C=X$ig;w*HN2xI5fyczR4)-@=WIQ~T&4 zM4C@f>l-Y2TiRnTOW4WW!Yr7SStGuFRkNgt56MLRS2Z0lv1@tGCoQ>YawhY;$5krO zBlKQhlY8)JP!_X{p0pRE?00-|S+FnG76aK2ij1B=-M=VgXv#3=Lg}T@g=Qsne)n+v zjy=kBh{=xS=_H_P{}{$A@24MsH@fL08aM5*fTTJ)@CjRIdXx=WXL>+QL}L0JaJECe zy~*f+n}K0VOREzf2>~JDY?T8f2xl0q3A=B5MoKOH*h`tCyRV{Jf&7rwtGvQez!e9~ zCD`&XO!ua_9~Ra9b>OsNT$)3FhoN^08~TmzbVtWoy)hL34)8G`8Tr%Hrde-Jjz$rX zedSJpY#*otS*>OY{%>`^u^{aOUU2bOpnQhyDQldS9m|_6VP-FAgzp@=kYpd*S_eBg zr5)v}LEWk1XDU9~fargVkU>))0xC#}OgfD)4ylGC;b3s=3vm!yQ}&nHY94MrH8^z7 zaQmx10}d4$ia+?wa^}tl+v>D|f!$Hw-D|)y!DnntF7F?<_m{X96v``=xY>UYkvCq5 zz!4#gB35>|sh3SEl!@1Uz8k23uxWL-x-{zN0mj2CR2%C(zI&VB6FOS=S=?|ix{L}0UTA$zikYQ@ShFvT-;7*J6CMdd6Fllrm`zqY`CIN zYuo^(wWUuZ@9W$Y*XCa!!n1h*e-uvha%%XXC{i$-k9~`Wq4;eLbf1HKWd^3* zC(6)8v*-7%uuZMD>BegcH1*IBoCEOU{NIK38@okQtiDYszusg`K*&{V~M^; zr$)&eoC%RgW0^J$ZN~3?{Xhbe7*7>)f%;RW`b8l-4*EDk z&SJoNii2YsGGnu6LG?dN1pY`8W}L4G5KuLDyez(PMA^5!9!5%1D(_mIQEu)RpMT2h z*s-Ui&jy&fNvgYk-yYE9l8IXNsZ_I{_*mUC8V&Re3>cUo3FYUn+Sk+-3rmslZD>L) zW+b0Qgk522M&DJH0{b_hJw>f`>XeD2EE&6sgW}Z)UlQ$6TpK2mlBl@G#{wn z&W{A2^+ziHumA%CNPc!+o_@VCrBvo`1wkKg7{Jc*^%1fc0bFfGm5I0QBVYhkv0x%C zB!nKP<3S8sWDKvdvBCRWHR|7PPk%GNOX(rL4Fq232z5o{ISPU#sWZEmiAn_dWGb~$%CtXt#pxIQVEvF4tweePfYMN<2& zTT2lOoB1nfBvYH`K{#SF#r&h{^n6=~RMcn%GdzhsO<*P(((9YcBn9W?$GOOS!(v^^ z`Ky+-XCBm?%usq!(Vy`Rf7Ew_+*=Q?{Y5rL6TowxYm_bI_EyuXVZ{yzhoe+#iEWQt z{lD#+Zn2z=x~eyOT%cHH{r>%~dgnOSb*v$!E$ z30_n9>8Jl}-}VCvCz>*!@r{?TR`nMj0L^mFtT6!dYNLvm1i-sV&(1<4u+`-r?L!Bi z3c|vx)y<85Qm%5FZExkl4#26N69K;}Sa=lZU}B7;n&Lw)7tCxG7JCRw3v&h{gm%+SgKe4X|(l8a!eC2 zV?v@X%7NHeLSnBvr*%K-(Q$kd)f3x)@Ndje7ed3vZha|<{`+cC<_*PfYAut^t8ODs z!ZDnlLn^0t0{Z>YuHXAoT!;C$Tvuy${_x(c4Qqfrl^x$pQZzUbC*{?jwCn}iu5Gfw zaujei)NCJL!2;{v7kynedbX1e9UWbnrn-OR9+F|W{T_y0{1{oy$t#{LZmARoJn2Vlz|=3kG!G@SX+;`B)Agm4+ih zWE&bGVfSKV{jm!l1qD?C332|b;^}^1%Es-&Hs0JsPp56yizdE!^S+G!to5!KvKNQX zMI=UjUvEy9-znU9i~K7nh?;{Vbbj8VSyyhM4%wGQpJuNSWh3#1ngZ@uT)E)Vz0y(etx%Bt@#*F6C?Fx=40&wKs-- z??tZ^FZ}lsIMxw?-gdmw-?KEP?;1COKt2O9$u?A z)jOV4dv_OCsL_~=tLx$q)>1Q%1(2M0Fu-p!5L*WC>Ad~%$n$ZBm#T?BfmForto>@~ zdanHAYviWLyTf-uMdo1c9vUC7t`xcV`DQLMQi<#6K4`Yo@a>X(%0NSH`HOhSodn|B zv)W6+>d`Ef!q!+KBp_M)Z;2(SIh3cLiIWskgko{X$4(p#MS zXNRzrr$^uwwwZF(v>AMv@+(XM>G)#=SN&D(cHNMn;o<2!CaK6@0im5c_R$FDy9;-G zhmD@}H#fC>rz4V=2XpRi&DB&OJ5x$NA2!-I0vjhjL-Zs-3cVmMJr+$0bwiVl8C-J_ z(}{oO?!n1$%XHJv^)K3qab%+m3a*sL8fCy>=Hm%TfKcPX-Hn0x6KoJLxiP!7h}aiz+nY{jTd&h_JiliA`Z!I& zBTeXszul-WV!Mi$mX*_V6 z19=z!&C6+B2%5;WZ7rDnh^f=#5O7-cMUd|R$1a$W3=!N^&lQF$>!ks(Yg;$$UOyzg z@&VLaDr>a7SR+URr!~jf=HR7gXxZEg>4hs1|Ak+fx%VtvTdZ^S+VZETc9OEPMJ2(X z3*~bx=PupuS32z4H_OUdf!05M=Gv}0jpqUgQ67+(U-4s$35{1uw~MH#W=jkU{C+1* zPQGT7ut5^$zyCK21Uc8Wxx@YFG{`m`$DyFiDRG@fVTJU^s^X(apf^8VDkmHn8m@g^ z%*1Ali}LZoAQ!uX@B$bmiKB>;y%HfnN~)u)E6bdq{`=AT_9zCde}4YF<6Yy-fupYL z1wN=a(q?9uU^(qD)w#VL4;L5g3U0&^$PT%twNn|A7qw4*L@<{hO7Zb>PI8 zA12BBLey=>gUI(_$Mvs@SWVJpWWrx&C+N23&mTd*buXj@a`f|HGr^(?Xazdsv> z6%xE2K%$JPC&0yy3DrnF@8!(~zTAFcFxu?rEV{~O4Bk$|uqSSP}_ zge@r;5A7#pSLv0mzH55&KqMDmlrb>CV2lGn*kiptTmvDTi>*M9HV|zsuu(ZpmHy$w z8g+w}UqY~J_`O~9`757YDh$+DlpQJEEe{f6Vxvv&CS(uiqYWfg77L$tc!0w#Z&N}> z$G#D2lRXXC^h|1=&o7pS42&$3;_}-JQ-UDeVTSL{(fkDM%sz-uQ4|P4lY!$aAJ0DT zpBPmB`#OKk#$l=HKfXirJ!Lx{4@A%A)0dN}Ot#ff>;85PD=*cww3sdPp4KH9_J(yG z@j)>D3nQlZoa7`WDY*)ejB~=oR;0Ht@z}0Eyw*`&1B4=z5IGR;?7+W} zl+PFON70!n?;gN8Yn2!WMfKyf18_dSK7Qa-FIz9hEHaTJ=b^&!YG}}I{+h3kx4GVF z>u83hA;cN{A#LTQP$k;VvucEkQCc{F+z<)OC^ig8q9wE8FJcH$_A36EZ#c5W;KkI0 zI`nBe!chFOj1wI?ZFlNC@#wmgR^)psMRKpv_Orh8 zWQ)+Lm%X@Y#vR3xKC?N4ss-u~*6G8yON^O6V!K9XX^o+ltA#Bh6Z z8j~2a{RS6lJu8grbhZD;=AY>xzQa2Q z4|DblO{qJ%h7{YXU(W%%`kN952TN2r1*AF`02RA!W=a`d^i~1ACTi*lIn`EhYlsn z3keCx=llT4pD^II5Er;ze!8EtUaUZo8wVeg8iye{sC#XtkFMDGvKRuI8ZeC8oyhCP zRA_2RFmZ&AQ6GUZn60b!OxAt_w=ITTQdJ8xQeySj&a|`7hSd^hlW}Rs-S5hMZbM$S z_I?^0@CB|R6Mo~7PSQUt=QziUbo|3(x7wLjiNY&;-PlcX3kYhuIukNT1Vjpkbp&a- zv*#~{dG?4_*D%4%O!4QPy(^!3;b;2%?18eGcrM0$!C%3_=d7~{eZVu2K0k1JFHgxx zd73@4Kd(AJci#0Wt7gHfDTSxDkF|fTub?Oklx)7sZJob*)relczM`*Dh=l9Mhxtei z#g&v0nHtU+2a~D|E_RJUfbO_45{u{k<6H10lVGVb%>V zD5V2Q@gN?)5gc#76*WDOg(JJx>Yuj$2v4#VG%UCdz+C;k*bpontmA?4u-HI!1wpr8 z0|Vse+lxs{oA)4Uv1#j+bej2~eyO)tAh6RcTj+;MgDw-3z!Ti~A9n=2{eQJ7LGZdL zEzcf678XQm+T6+8+jxK6?_ND!ZJr4|iF>E>yyktqS z&<-uvuf%zpwb+2#j5#2O&4qODwH`RGoH!lVEI2g+fDjc}CWduuA6_`!?|J79XAt@= zeKp7HEUpV3-!PJ(B*B6KD+uGn4}Bw{s7RWSAn|qK76~+r&rgN>JphvgCRsNoouB`p z*FHA+dB<1c$@#e;#N*)>!q76_0*t~P=g&mNW}SCBsp&2%j!C63@&KdjZYfH64QOF#`jwh8_>g&g6zv_9%(cReqN5o-ug^Ao<%}_e5Dp9V z{?iSa`16xFu!@&QOa1Owiv!AQdfAq95F4yL)BCewGCyq(6bY2LP_^L!zBV976@vQF zUSD?9z{d+7eZM{DO^aUVsaarbx?d&tNVy=Not1c)YdpIwcrP`Oc0~yByvNaq1yzh3 za`r~g?J)!G9xHk58t$XWVFLP>-#xuUr#Y_*cfi`7xHLYlt@nqX_iX3XK6%^9;t3K| z&xwI}A7}f0IKU|Vxz(kpVzkR6D$#ATlimJx;+l=h{T`>*nfspo>AO|!1&I}*Etp0W z8}!E_cXU&;+Rq@{AK7wzgFba^%I(3NHQ4V?qy#ZGCC@fLw*0H4lb%!np1ooW923!a zf1Ve~E9f=kVUB6G${iClPz+ieBh^bkkqGRu1s}Oy=}hk|W^rZ#kG=bQ?LYE26U|FJ z4!gDw$mC$Rj4*L%;s@qO#+kdcm?nzveLmP8El1U=w)zcv+w((rlDD{@EtX$Ev{&Z8 zF=ak^wc2mbU_aX&l>v7|9{XK!XqW!j__(yGhxT3C-1bN*U7E)!2d|^WD9_9BiOEyR&*g4->?fX|x3B1QkqJ5>Zg*FRdBJ@ld&pZxBH+Cs zi>DWvlC5=BZaPy^`t)$VqnB=)xCF^^C>Y0QHR3XbzdBpa)!G@!phM+96}(LyPkRS- z6EP5GE!0XI(mV6qO3QxxkV+gS=vu5YYm(;9n4f>TaL)A5Rt9OhOSD=TTHRmVI$eP; zj!I*y_+y66{sme|ktDVLJU-3un(oHDvY5OFTH>~_OroenMa~}0Xg>z_*OcN6#lz^i7zFAJ!N8Jbn#w3MHl8 zReo{xv*Md(+d+2D=UU))yrNR}Ep#kXj4(5Ezv1>&8NBz0nytVj0^hcq#!1nBB!-j3eSal13S=ymN1{1Jh%FRL_q@*119(stu>mImX%BD5p+H`xqTW)*tBZ4B}kDS&g z_4r|O)DWYmNB+kL6hcSXI9(2;ui8EjT3NF3Ab>O9BPvr_)lKxx}IS^UQN*b z)v6AZ1BuKae+SefA9^~s<3;nB$Du^Y=cnU`58&r+gb|(HpZ|7*mn;u25&HX^=J@7J zmLW~gFz64kR5N5sZFb8sN!GyaW@JPJo%pe~J$LoeBmM*#0U>}UP#U@!A>AG~^mPf{ z>)Q}<%LHv25R~c``wu2fk7t`R(c@_SoI^BY6&IcSK*rxshxR5k%yX^gMl`_7MEs-| zp*&k^x*gK;=65g)EX{m^%)e*y=3x+2=Ku-2mzoP7tirtX@bPW)*>hs0QaZ&qi&(+Wm*@h1yv1NXLO zv?(vD=LT2E{XeP195q2i)?Nb9%DoRCD{JVr&Xa= z;c04-J5-#UNwDyjir_fCdUd1i+5QHc#?JZL=2geq=RzBrnW<@1gLR#YQ20$sOA*ry zxXI^}jlJ&aNz@Aihye6@5&y-Lf0zxT*DcL(ytmb-3f{2H1?nTJUB~*)Z&L4b;mGep z@n0DG0V@BOC+|fycKmkcnyXa8ex0iC!}6(a1}ZUk;3dgaRJS_My2P7;(sX?f3%-6x zR}T*lZ!{t9!fd{`@30h+ao=*RcRKc->(lYP$oH(dE85Hddxg5>*$%#xH-PEu!Q7zE z(}O~7KZ)~K(b}bV{dJXewR=OSSBmIjVm}A&Pqd(q*Onu5`~NCKICax(v^OS@V|_MgNzfw$xHsy#z+DZvGTfKp_sO@cq9g9Zb2vQPD)AHqU5`QP(!gF7Zdvp96Fh-K9y1)bJTMAdjvS2J^<<*%yFvK z+;5K}g^~=y47ggab+Ka~nj9pKy)5VE)>wzb6YsNH>)gA@+~WZkwSISKUpXuSTH6vLSOhZT$sIw8ax7;;WdT<+jm4LF~F64wi^$esxq3=%kima`m9 zE-0v$JDrv($~uun0TJ%V2XJ^O?31r5T)V8j@(pul57NZS`v*1)F)&!-1!?0ze}*MRzxGak^sa@>3yeUrptNhO*B=R@G{tl)RM zh=heVWv>tH`{Z9u6>Ih0;A+eMc%AnMUVjG(PypQbC|%F)>-U;(jXN-!cGr;Cl5U3Xb`a0qQix!rebhluz{s; zYn~UVT!WmlVMr@6wr1#3uEM$O$>19ulv!`|k{T$;gl705ZoYRFzXSkV=?&_OkPy)x zBKDUM{K;76s5*zasN@L@u}Vet_|^;&Fm^s8*J1EZ7hkzRRG<^OSasF?)l~BF6YsAI zPE{$Kkzb$5X~+NpRkw{6<^$RrGsK94+MlQTSlh;*$J=EwFcWP%!+R&JNAQcg1N66P z0$&AD`n~h4i($}$rkfs0swsnAluu0lv5{6uGLnhDR6P4LKnJjw(@OFY^r_ezdf(KW zh_9C=TrY~<1_Jzlh3fBj_q-}9drIKfiYXPjJ-@?odqLdYTERY$$me^1&&IYO6U~qc z9lW(`bsiqeKC`?(Ka;fkb|qfd2Ph9{7-Zb-w7t)6Y7^q3@lunb0}%3g>Moj4LtJFQ zCwW7eU7p}ul>E`KtM|7T3MFl?C3ntJ6us^($rJq@n%voS95xma>De~Q<-X3qMSF9m z-jeP8eLpe!-$FoL^YtQNPM~_*$mN~>89f|TMeF~#06WZ4|2mrgIGp`_rlAQ*(r&on zPz?tY7bWIQ z`q9v-UJ6+X;X!;TgS&za*&uE6nYZLPot1zcA^d9?B0^SiAeRpH`wYttNo22wNn#Ie zj%!dhcGXjN@@3F60UahkO+JGxJD^kgj3{I7!wU=|jl$!+F@sm~WeAVx<8dLo2VO63 zZ>ovGuS@A=#iB62JZ}7;=(u^R37E&H^bE?ZeVr;&TIw$g&W{bXVMF>Mr{zJ#SoMO# zT$`(a7OYYZUsF@@vzMkByg>~CUqn9?;}A!cZ5drml>vLESSE(r_7h)X0iXh6irz$MXzL}FMYpcnJ+zUXTMNH%&PHb)utW3#^=B#8Svd1 zgy3CawcUB-VBb!2LDCKes`esG9@cOT)1wZ=?KHZ2oL-Sfu+xyhs{&Jk#G(vyp2S13 z=>Z4!!JyCgW*$=`4#d@S*gMKpT>3Bn)qR3OA_fyUmK@Z_e+5cYFdcnKIKsrhh@ePl zYkR59nDca(*#;iOk)Kpu|Mwa7$?UQ)#ZCrw0*AnPAUq~~Pp=Z}S%k^5q6Yk}3bYK2-HPK-Pp`utZd=XfWR&DW}*v5e%}N01wIk)JPu9kAwgSKEUO)r;*@41~{v z^bdPvxr}X5gQB#T;4_+PKv+32-c5V?UW>1h@zsOz=T8Zn`nPR5 zs-O&VWuwCa+Oi|M>0t0b(RJ`5BsNxZV-aoR;a?9lzj2sCnz#Bs(~pAvIVc7UM)(r3 zb*ev?{ZVPX`((q18>4u5+pX3f_*zG3BC967pNUX-YluGqpb9?=Gz&k2=rWEZ%T4=mR$o zd0v#pM`6+HIQF|X>~C=deoXJtmurKnX~w|*g>{hnzp4u#aruY!10MCHXd85ZhR_Hj z0TxSIs41}K(GML9XZ@`EU9)fF{-=z5oN zS7^A(Vdj1wZK;iC>Xo-y;Zstos>&icGnjPDjSS0I6m&WY<;wb{@w#dt59GqdmWNn) zPW(OE1Y)S=9;kx=FptM1>u(l^nK%k0ejyTxIkd2zyR__s zt*9`nGU(uG_@oy2Q9axs$q~t4FHGqX!W_UJ`LsGqXZ0`tjbVI%)J%DNbTm=@s1+h}CMrZ0~^CzF~*6jI4k z6|abS9XD2k{6{X&FDL7suCL+1B;4unXLUTY2a-%2Bi9h3Po*x=l=w+*X;q+_vawM? zpoMBnz85sTmJiPg*?d9$FY?-_RSTzY$y`;&;ni+}sRM18w{n17^e)Y7fdm8=2v4)q ziVC%9+XOFdW3E8C~x(|X~ZGK$LAh1{|C4(ktv7CSO5Xs3>LlFSd1g8 zOqB1E`eo^v02oSxUYc~adci73qa5dadzSidS8!xScm#iOp=Mht3S@$C;x z#*n>#zkEFyKCM|wPVEL5R!;EPjQM~aLR>|Uau)iwZxz@&@Sp)Y6hbPf(+AmnTi&~S^cn!Q{aT%|a0Q zd3&)pTVaYBNikZiCkU{*{AErx9{7lXFb|Z7Xtijov%JlbyE1JGOiA4#v^t#1SFN-mCmK5xy-zJCtsL`&S z)E&x&@3Pf@5&~Dv=HNq2t>+MQp)LjsJLXcI%Z2Sn3UBr6#wF{fgbq|e*^KXLXLua@ z;+mBL#9rbd3R%uUQx@vYb#0cD*EFlcJRvoQa^VM^i^$H5fm6R%7;H#sS*3%qqfd)f z5SSZy65D^Qun$)k3K3n7@7?g*!$hmoU05GV|8tcYIsGwyw!N^$*Cp`Cr>J=OVTujt z%$)du&I~nili`f7T%DeRdRo7u=MJCkT6%LJwfCs`K~`Mc<&(n+`XqhAw&O(!#cTvU zqDus!b?Vy%ojL}94XcbVlhKylP|LiJ1Dsn(5Y2peD0RVh%`Rxp3DJ{f&(L=e#Gc7X z*H*KMN4V(DZ6wfD^7%nCit>aS9me0Q2@LJIO|9h=Qn~qozOxyJHc;F|c5AU8uR}nf zqu|r^Phx;>YB_gD%TxIM;&t3l2Z+w&u!tX${y;p7#lH#!FUT-oQ~)M5tMjQ5aP|kO zqkPxPKY;W+N`oQ- z&)0s_C^ZoKG*`=?BacHYgdjj3uRnoe=!cp!PGscTkZQK{9pIR_m+ySXcZ~K8zpI&j z;X)@0C40O##Cf~q(dW2h=6}`~%s1&a`UQJ#B0rhL_4i&{Cf)3hf7htz zsJYxwAL6bSyb5zko|vGTcbVhY7FZ`pCE^Su=B@sWe@aD*5wKN7adIC^t>P#~p*s?J z-+UF(_=UReY(1nNCnzR99uG^5Qh?E$Du?}};o;GuN=n2S7#IK&>a+@@;^BE+ z&rcu~30&XEKz6027N_lUi!K2>PsqeX*Ygl(E0lp={TN;%3_3wTp8Xz=OpplSJRite z*x#)rpg(szM079&O_77AWSk;<7cO8*w(FK&Y39zuxp1x*8MWs{0cd-^(@)k)c@C^7 z?|7^X{qplGOy^P$v^Z6{XPAR~>KK5+mVH{gz3u1Q7vD;rrnIclF_l+eFP57qtgfxq zu7&b~U_urcaF}`I64~@;O7!`z=D(rm)uW0j`dkjwTuf(4?sgr!X1sv9qAO|d{XP4ivhdL zL<)@HVVPzR+}ovM^+(Pqs>Z>hlsWl6L(5h@&l2}L96!|NpE9p?0I5-D@w!<7=7IpQy(A2gk} z5zmTA)0=R3#Vr~B+h`CU#kr+lp%mH4Ln8V@=?mwe%kaD5Cd^%5VD+6Hhn(g4B*R^3 zdZt7w(W;aq&kx{di6jn_Xbq0#Oqtr9*m}UnIszuGoPD#Jp9u31Hevs}qi`t~x)Voy zCM?Lf`Vg*qPwsAh)@qN~6H{&r#fX}jk>933yikbi)1BD?siq4=$jmlbMhO}%|4BMr z2SCDYwO##!#cV^U``u-U#Vi?Mk;!>@?9`YhF$9u|6s`1SOk+NC_%=Mx9$kySUE3sj zro6pF-HaGjfUG2p(qHd|Nt+YU8j4FxBNFr2zh`HU0skKWQsQyjQ3T;I7~G!kXuBR% zK03go5cO19&Z(Cf1w8S}jmHam3QX&Hx>MxqA&M!!WNlXgSEpsoiXS2HU4H-J17gx( zSGv%Tc+hZW&-IqkE0x)r2_Rz4;zE_q*dZ@VH3!jHCJhQN-KldXUT__j_+hp(BW(^R z4HADB_y|D4JgsU0KvR(^G%O?xYAn>CUf9_<-l8iQ3|}es|Fbq9HyDv;ZpxJjU$)?r zsIJ=cODas)9p;%YtsuaX|4(=fC7XB|P%Dfl^Rwq2=iJ%M+z74~1!F-JXLwSQ&LKxH zZdI3uiw;caV@*Iq416CR5h5qilQYA-eLSfDlKaz0?A(d#LY9gcnLP>3mtuqwjR1r~*R5!kJI?i|#{fi7VA5>qj?yyeo<@&#-i< zEdsqxnODU3V1uW}gR#s8!~$M+Fu{0vw((z*#D~8~*6ImG7|U z2(!MW%J`nTAEOSv&fgHMyQI84v@;MJgmL+&^1Ggb3bAtG!u@oA_MAPThy_TsL76lF z14;nA>ay=gx{TniW|ITp1&{r9OE5m9uVz0#e^+fYjN(s8s!CpJAcfSt`_+P%X%E7E z(Fa4n1uN8~_u0$pzr(9IBlTb^9G#)p7gj#ZtHez3mq)MOQ)XLL9@X<@9#BHgLNm*B zm89LGpWIs$*|c}MsRx|-YQol|?xw9GUsnl*=H1S4n2seg>bNMWsJHAD2crg54f!-b zei`q&aO1gg#lVchbGf*Xy}dqQ-J@mUFmPpK@sx1!sPzkuBK7R_0P9Y+G3MCeC{3B6 z0HJrtp!ty)q7DeC$aeRkM(d$ zpA)*>6zE&2&AgD0tpWIHfG83lMm@NEK0a)8oULZN_zL!6&tZQi+#5wb{#OG+C70MSt{^A} zc7xMNCvNR?(Lbmdhlv+(=h3Nmq~zsI8qJdUwidz(B+z8`I#+~1P!tFlCBD9J02cR@ zU7&o8N&<2vf?&S2c-|dULoENXf#zXocd|gE#)fp5?@YGxsmYK9soi=c_g9_6-dMG@ zp1lrJrS4i+NbSy_qz?<8UwQ_cqmKRKeioRXb$^^@9Y)fM=5DE0FIa+*L&lRS!@?B) z8Vq8nsETp7exf%o)ra0)G5~dUw1$CP{i~x;E+e9xKCL3z?3sgvoQhh&A5Ir4Q^SXY1^@1w&Mng z5KfKRV#@p^sPF(CYP*(j0(1;6yY+CrfqH$jwfCYLuPR@XpRPrGsjLN$&%t!6fJ^bZ zhy1`_Hz_||t7g z{Ivc3j^|hR8WbS;4eTdkfKHSE2=QatGQ&!DoJcA0@iU113Vjk01)HMO z^T|WAiXQ#v-fTHFFyOITs8gD|gJVyTrv?TAcC}x}|H$Pzfc9Tvx=1I1#qc#w&`*tW zs4Z}01~urjRnrYL)$LS@fbHu?VPWCtT`=m@JvUoyoKO1zPmveiR z91vlzhdtk$Mb1{?>k@A*mWgNVwv8Z{Z5_7MauP~KNY$0d#mrGqJ-=r>0aIwLYq7gX zXRe@87hr~h9#20@!xj$|P6)A$B~%NRmZ^UcMGz-XBtNqxbBW{SJ`Y;+l9>#=sW$Nw z1aaq=*#HuR?6%tO&)0@TkQZ&H0FqYcX|t46o=8wE)V~jxv)*>C&hE z4^PSvD#&%TO}6Y%*5_Hr?luDod>=9)6c|5FX$^KR*TwmzMs=`nAQ@ zPzn+;&xCb+u39mP0^bSTTUA_@^{<5GksOk17N z@JNtFrWFk=X9(2xQw*x-6jiVR70}6M31Ny6Xg5)3H8dzF|2(he;%Y^npG13-GiJgj}F+rNZYK)kdm)CH!nFOw6KQR1B2@Xc;3daA@ zy++E!6b1@C#GvC%ETc|ePXsw2pOe5ygc2e2S|fFAB`8L)tVPN&=GnVy#5y0=Zv6^j zSc2s6#aRjcLXeChkRC*8HW6olK(E;$!Zwz-6@FNiX2qo(mcz^QXRl#OOort>61ksg zAqI*J^Mbp!L;T-A#@yM|4|4EwaHP(}mysxW6jJS-@S9PJbuID*_*Gv~G?`%T7b^Mu z8yZ3K{3Rv3*Jm#x%MY9wcIIm(V(vnMWWKa`$v7Wegk9@zl0l!Ud{6~byS!ffD21b3 zHvL*1F6tSevC~Kl2zCs;xQUQTN5h z*@#q~BoWQvDSt%t4BFh*;R%2G{63dK^isD_PCWV-?R&p^sjD1lh*y%HQrRr}!*b)a zM!jS9)18C4no@Y0gg>PIxL;v>w#*n7RFHJ*C2$jinU~;qlu?$_^Zm&<$k4V|(H!C$2}0xc=?lMSH;{{Qo=`KNo6tZVo0r?73gB`1h1)Z?^8+)q*!A zDZ}8vNSRSDhE?qb4wZbeC z>4gAs@M)2ZRAkL&qIrG;6cZD(_MmDmp3BxC!PtOlK(yFcpQ`CDzrp556lI1w zU3-qPB0i8wicC8=?$5NvOB~X3P{~XH9#r+>O)}W{1Cf+(0Y4f0u-#+;Mp9BXGriuasqCMaDDJ4$qJ%lU2!ro1oxz ziI|gZ-+ieZuptCu|HEmP!v!U>om}y8)ok zW@izaT~gA~$;AvCP_&mFOUz~CMV;!DIdI9%%?;w6d!A;Qfkm&bTr-#w(ko9pfU>ju z)Bt*PAUhcX5|r5*Tf-hGt86P*p|D{WIQUZX@CEQQK+aC2(ZUjPn{^9&Tnyx#X>-PK0R`%YK zRkBx-5h5#l&+M7KXUT|cvNs{JLN;01n+Vxk$lm;p=X?GBd9I$zRrtK`_qoq`o!5BC zaA_%B!qWbAxyfRz=4<|o`w9A$ZERRTm&GI^lJWPi4cuzqSd6QuL!itn z5&9*D=UEmM%6NMVYH4YGKBnv4Z=Q?;pTD3{+yoUbUT!%$Nh###A7R*TJM77UA+OSA zJU+y(NwKFMvG1VTZq62_4N_|&TEcgo!nEZvn9TTU*duouUfz48?Uy*i;AKzM(>9yU z-$37W{EGwAZ;9pQqx-UohEkblKFZ1~?+-gZt~!pFZ6)m=(Kpi~-Nu~N=ngQ4)B1*9 zAj~>`v$gEdCxY;EtvH#;*w6Zu>0x4&^%ZjO)FfKt$rTPaj-&+VFeUoZ>vKj5PX)OP zp4af=ru1Xtg+r1!fM@>x`;B&m_q5{6r)>DsBBHERFj}RTpUHirJX1Y{-Lv}x+lCL` z6+}GI)_+>PqaFx}>)cX-zG_~dNbi~h73P-V5_R@^1o>D8Q0!V?va)T(6Ad-NJJ;$$!Sl-@UgL1+o+TL*~XH4s=kr_ z&2L)fS2-%V^n;`3#Z>nK=0s-gg9mYI1vxj9j?Y|1muAFO*L0DX9V<1lIlA?g2i_UAxo666pF7zwJQ^yxJzd(x9g;gyBuu9X znWsl?@>fkM&+VD)8@=YEpX|v7BEqF&AD|rtUdl0CzbMIHB?T+C>&3 z!aZ1T_MS?Un#l|;NmQj|@z*UOPwBkBdqMLK$+_R_@}jBi=Cy6pitYZv{^`QSqn0Ac2=kDZ0~LEsi0QVeK!_2DH6M`6VGjIBvo)#8uww7kc{9eZF?=Kk2tO1MH)sH zLr-G9qD+^)$AEt4+L~*BaM7yP4Q#sc4#vk-W9@C z^IyjhBVL6t^K+)kn@fe0!HeKp`6B;;vIUNhbfWZjEeTRMX3OV8L1FK65=WxTr~5{s(bR@Kb^PrKEa&Mfyn?dz+ zFZUvzo@GxdPf0BPgRNTd^1C-7R%E(&bqwN3S;1lD^)yRx4l64*jxuZEdTA=xi&_8s zw+Y=<_YB=J)6u`%E)0NVn@IpIA78jJeD7rvx8CO)RW;TF^Kwm@E3MnTxOLKQ z@AUEH6^%}?@fJoNr#OyOzUCj`iC!1A$;q2urju25$;x2;>q8%dwf^M-t{N$K)6I^x zv)EISZ0!K^pTKdu^rL^H@@_&ox3vNM4gSSdl0?@USjTcQ_!}}V@R=c#L_S8x zuC8B{*MlpFFQxy6gosFOwdw)gyTP}ag;uq_#p>2ye(OE`p(pUKs$sWBhq&*%Frv^y zM1#j(2E$w&jXE$UhK*Z+e&bVjp&}KyL4=ScaS%-Y3LJ@2jp@%l7IYxz-RXblBkj(c zlO44UtGTz9xZ`UF;f_11HM)XTho;|ph8 z`YNuV%<3>~=pku56ux2FOj?i?A^*xr|08jM^{XcOXrH6%S3`$NO016v$`pJMQ-SJu zeMT}`sU>@>Wjh#*VBoH>iVS-jN=v)oTHlLx`>`fe1ky|4b4$*#kfzDP{9 z!ekR``}sb@n@3YgA@K$+y9xg&hdPpNGa5V-IUuqhqyi*Spop zwDD0y$BN@^r*I1!-AaV0BPk*~m!4p+*X_8WoOtk))&4U6yVpPSq_U__jZ^GXk7#DPu5%Xafjpf`h&?`oubF(XBVg z{ALy?Ahz+2zv>a>22SqV&dx?nnVCxQ;1X{kQ$A1>ffsh_XIbJh<4$4_+^a2AJp_GI zVYan)?E<}(QMqr3p=ylGcXxx8+#22)@`!CRem7GBA2XqBaweskTJJa5^xb~?f)Z+8 z+7|kybFVcuNvGr(-xY+Sb3b+Y;2o)^Dcf@Ef1Wjf``w(J;><_J!QP1IC8i5nt_e3u5H58XBw0x*c3cW;_9kIk#=1uu`q#vN z>SntqRM=>_owlSutuYS>!94(E`Cd0h&_K2S6U%!~MYx^0!o-dVn)k}wS?^Ofi5 z%e6Naa129H@QwN{M~`7}vDxpSTBH5?gMukFaksR~Wl)&u4a==Y5p+tHiBpO4X?3H! zsn>_wZnQ9K(DZYT@&%kqCO5Td*=Y>=3^5OK z=qG=0{4>+=rp9{cfsA5kvhUgGJ2>!K+)*C2jia%c5Gt^|&;vJN*;k}#$&nK8<_U9Z z8b1tOX@8$gp0*DdPNY@ae{^^nF!f?0(-UUC?1FCt_0RvYf3Cd4z{4v4y0eneYnI1y zCWS4{Zta3**LXBO<4#(^LzUd&t)rNvJ=ifC{49GcGJkoHZa#IG#iW)#_$-@wL7pag zmEqY%yO7uzLS&$$`L+)#(^`ln>oVIP*`1L_(NgyAJBEx}L=@bJ6-g#$lqL3!AczY| zonZkbzgC5($A*s-{?S(cNS3h%Y{v&daP^oC=gY{uT81x^NGLfnw2B?J9aHq_=he>Q zZk~H)L|IQ83qq7MV{*C;d=cCvJAPtza{whFhB?QpsL8ECf0RIRlxP1Sa*#Vm;^5be zup=RCo>m&&3^1%HM$wA{U#Lj_+wIsT@(CqBMe$c5-XkrDDI?>K@L>$rdD}Jj_TvB$ z&k9xwWV-Ly<(YAZd}ngnuBy;HAM~=BJVa8QioJt?;dp?(-!U;^emtGs_AJmSD+MBwcvF+)@}{2*|+;b`*(qus?WpjaATCKc;<;o`u<2Nx4> z2yMSi@-G#ATo?{*Q}gFEa|c!>^qzYIa(b|-_dMPdpRjFoziFJfq^AC4>^TN7FD-&+ zD6ZwfJC0BcTeDg-#HLU~lU3sP5Pyvvok5X6fboB(|sxcbe1VxFOC>U^0@(Sp5C zFW>k4yJ@c^8zo2L)8=c^Tv{YQ_t&o;axi2CTdFkK4)q8c0(~?V3jP311^Qu!N#5O?v?;G5Rzm$r9 zld`Nkjvjj{bnzYVFJk$0{upvTe1HQFWUO$o_ol*acJGzi8wB5-ewz_z9LXe8bpLO` zm=d^zFDPrO`EgGq@1TX=_YXxrlV!!u)wNVCUu7lA zRG4Er4MVrk+N{%#z>?7J(5AD#h#V6hk<~)02a7KMZoo01yVbpc+3o?-&wXgyPn5da zVOQeAXdw`08bR%&<3C+wGg^_JkRzoz545fYHU4Ap;VceZc(Uz?+SBxHbVfJzsk!nFBE?% z`mxFBok4rDpawWAS>D(EeYc*ut>nD651zJwq*r@;iunjUvl`kC*vcAMS_K~Cx?L?{rv5xkW3l>9lb(1lxIBH&jSOr zfpf_4?jTYi)C~VlGFg(=7Doz!^ane0LPYK*{zW3HOa_DSa~a(lEvGImWxl+U7pk+m>1|MXNF`&9~G zrwNxakEFvWtJ{QS8bb1uH`2gS;v~=cP_*2{kL|&0DbKgr8?q>{dBpUG;31gdT%1bNH`?-@!q-kx~kkM4+~?F%@qWZ%f8R+<9cDDc147|29XxHU_DKzCupMc>5z z5^7Ktn3sCxTXUD2a^OMJ^{T*BCAD0}>`CTm5_A7Vjgomu!=)bIPK7JfBN?DkT2i4&p0HMYqnW+nk8twZsz z?w3YJTs{|A$gkH1-uN;VuMG1ZCbzHSXGgh=Tg#^2jgf!PHtxTvTxKWatEo*>&gro3 zEKsSfSLI=i)HwSbyeUvnJZffIFgRJziD~hz&<(-zm!GJ$0R!W_Z?>A8&_=XMa}LBd z2L5N)XgMs!-)&q(bj2$W35hA^FK8sW=VzE|tj831q_=hyxJg+s*KbIxXAfDmep6`f zcebmr4Q{(~Zxvf!&q1%+Zp|&cJp3}F1K-D+2A|h-vh1<@?p3LW)ul;xjxhpwq`<;G zk_MG`YqZKew6~85la;`Nr;cRUoL6Aw93gRvKGxOGpjiQXx(@U2bkQ&V$qI8czLfDf znrhoW5tgNWv_g$;BQjKLUU`9GAC-S29`TnqZ{R#MjYguv(eTciHkpcl)e9$8|h_~80I~BrgjU>`yVjHE=vln zzEJY?F^~n^=PBCFM!pz{lTg99NdbkiRYFwbk&CLm2TFxKvDw?6@7r_x1F3AO6E|8V zjpuYGTEU&$&-0(erc`{&@bmcKvL*7h3Lj`68q}Wie6|f|Y#`PQ;bab_B?+JRrzNSd z*)YPUnu*l6d!9d$GrF;dMQRd`S}gfdy{N;4i_V&7@~edA$I!B=P*5DQ_a|2!CIEn$ zxVL!^6iPKc=kB4DCO39M81JXE*~gzpg=@!@{{2vE%*^1mAN5LrN0REB(#PD<`x_?o z;bDaZ1-2-P5Va8bC$K&f`SWBT->(nl5)aPDEN-04^!yTYL~M#(G2@bwTKt4PQ=aX5 z4%vN+iy3+Qd-pQIuK9}Qp!tmb;$-*fmo4^Qa^R`FNGK5^bJ*a0@lwa0U&eiLZdJ2J*dwmM`YjH@-O$9699o(x&==<@tk z21nD1x-S$_adACM3UV)E=+#Sf8T;grtNck%08KlW8ww!n$?nL66YGP~Kg~X# zl$NEglDA(H^aW{Xe|c@!bhf(szBJeHa5x94(Ib|b$OFPuAN39p@_8(l4?F*zOiHmM z6Yo9bBgI5LKc;XWK?0I8FmdDJ;>yl5RvLtj@d5qhovL;>$0qZ-W;{+MZa=gQrTi>D z(v>(M)#5DKBR$?3ys54js<(VyN3fOro_BT^^om(ElLLA=G(SzaE=qeD5_CH{NFq&6 zcBpD}*$zF01k$h@XW#0VvR@tgYN!#V^oO=BWahRf5~@+dhe&%EW_Z z`y2`D*CQ_EtgOG%n4gdoX`z#Q5N@Ab9i*biOoyCI_Bo)-Jjh}U#eWp-=7PHKm=WF7 z5>c!osF|ZJxR9BUhzfF*S7Ucn7UujW_Sfm$qN1W+z&aS#PkAc&JHBs_B^+W?QhJAT zw7`&0Oo@(^gm-@akTrg$9&Y-M|@?%&M3TIji2kLl|& zqY6UhTvuV8HBwbfvlV2pjl(A?Yo_@X2{b=m|vH_;*I-Vs+x#jeI|>V zzLyIWkRl*js(*F+rkJ~J2DQ9~5mCFUaCRAvRCW&Q3k{>zSdUZO-h`VLe-wjqyWJA2 zNEhd8#u)ak59OwUdu!FLGr~mX>UcX3n4~hp`7$}Tq7TXK(6z(Ea`*1?sY;+khHfe; z{<&KyE9y}5kML=9jehIY#)QPOm>{7i;hsy@ z&hfF9N#WbBJ-IHg1YaWr*_+F7|JF-wR&iWUG2TnU9DTTKGcb}k=+=d~TXhQ|V{{X3 zMJKrqKfUJ;_Vc#iee{A|{X#OfJ4)Z}ru!7`tp$Ulc-uCPgm%MKd*B0o>Xde4VAcr`-<4WS&QeWbuc}-g} z84#fsM2jma-I0($`TIxb0K@N17BjQN4dstI;r9iV{Ml0sIkKU7?p}O5*<9v{`1tAg zBvv743Lyj?TF2NRHv4JvJYpm>JVb16$bVDa!HjsZ>*0?y1Ak9{#2*aJH*x^J*8Izs zy|WHREkKEOf2yRbE3uH* zagJdQPjUh)H z6CXeJx!R+LT}!~UVbQ5e#G+8o@C-!pcSJw1f{*bZYisb%b~vS_WLE&0Oq7Ih)13f| zQ52?+EWJe6^wP%y*^m2|sCcBV*W{Ql7W?yLUo)#tzBM>Hps3KB{UD_=rS>a(*8WR|V?$15hku4wqvbwgwT?w84JKH=nN zDUquK*e+_wc#;KPuWpM8@=?<0&pQPceG2CEsz;MA^(2bMjmkAYgoiUs;fveIvBW2m z8W|adM0|f;-tO+F179<`om=d#1%w0|sT&90s^_y3SxI%poT%(yv0cT{lWE07ne3kY z;Q7*Oml3tBNn}#`R#a4}K@7FDEURH&l$GhNqO8zm0tT(>XWetoGoOvn!o^!8P#3%x zba%e($|n$XsX$Lo^+^<>Fb2|pi*fU|Eh0pW?^;QaB$IP)h;eMRFDnHLCzml@=T-Tr<&b` zg2t}kbDW#~Vl3dp_`|2zcyECoexh!4UxX4Q)Gbk)N*DpW=qSRU2SLtA`Ma&l|XGJP(}9`n)NS zI-`?E?W8+yjwP*8gHG&?DXt$q}PJh_p{K4v^q2q(yGUoD1NnDI8w-6^B#%4_`a4dLg~O zw9~92X10)3LGTPQDT&NxO7$LfFS0LuF{_~9!{5Ju6*WF%7o2ws*YXv_7#9vz{>SMI zkk3D`vbCu4eS=-BzD0ukX~JsHIwvmg(}d%myCCRWCFx?*(%RZd^v!o?{|KJ+y=PL} zD*W*F`)R3U6Ho;=(UV3EG{xQ~`QbFixkdy9hDf^lDehB>-uQ29wf@VVUj5S-7> zsBb64(8eJQQG{iERoBELAm|i25teTYvSjiJ`x%~sm^mI~M`-lE<@v?&wzyun)v#5S zRE%oAKW$ox>#mAcd&0TInb&>LJi%u95@Wokx`897=_n9P03tqQdU)ank~Iy*W;r5| z>m&I#>Kkmf57!rx6A+-i9oHu9u?i%zw0s4eHR}zZ0gwNEM}*les*>E?Tn@J#UGS_D z)BD7{hY{e7SX0N6^D88kuU1kj=HGwz@&225!*a;}QZI@Ri!vY3j`k_~Q_GhfN~05S z6Qt=!PA~o3KEBJ%!pVt1xV#qlP1{h~NNiDhce)^2qlhjQez2&o!^6W_B{H8*14vyS z^%ZPSaL1K;=^YD^ze*ZfbDnSnqzJKP46?e}z9X>rY@R<3cVulIZtF)w+^ zS#1-hcfEdC{442MTUibG@0&Q@$ue|&GS7PPKgJZ@*f`Qg}q-B)a0+UEY$xir||u8g#w=AdVI*z z>&K|0d&lKcJs-;5`sPPzG%pmm8QW40agD;KQ3*@|WJ*emF|jJ(?QDyHNy#4vw=e2) zl7J~<^W`^$%(2M#)bL_2Xqq|kyo@jjsxk{wNL13*rQ9r34cYxu{6&2ng+%uU5M6HR z>KhZKG-qdLE?SLN=%pt&s(TpRz!k=-i%)@)i2fjuLrIDlPM1s~PL}vO5$6kyJl&%7 zn31T*lKKkD%FM7Ana`(LTXLL`j|-B-8nyT(m=;bkp{%Mpj1x5b`p1gvf~%Lt($W%v zqS~j{Z2wrc(bpVYmobl@Zv8EZBTV?Wk?%BFZ*Jbp8Ow~0Y+^PY7a;~nc z(g`}$72SQGCT8R+@-}X%A!i4|own8LwUPAQcZ6RtzVdw^K~;~utfcly9POmSYK042 z-)>#Ft}L%Ea29)tY|S=h31Bg=@Wa=$@!e&^OqnYJb1zq6AqYG1Z$A+DXb7BRu63AOO@VE*Vv?S2nM?e))RLt9uN4?LOk=>~_aAJKHdS7(^0bY~v5 zr=IaOr!PW3ZWEQrg6Vh7$}djmoe_e!si`*lesAzWUxwb50M7?z=4|ocf$KNG30wP8 zZai7*l;WqOt<4Q`tt7xOD7yvZfTlEBui>{t<|L$UtrU3hYYU6VD(+{N_{G(l&%je8 ze*4$#^=@`JUDJDCO9XFPi@LrAH8fBTY&fw&7qt}9xqG~3Id{zx6H5WHDuapr!!r(R z?=hkN&L&+k&v_q^A@HSyH_T9ik@>At?$*RjTW*L#R6>SMmM;Pl8dlD$>0{_xnRCf7 zQ5_wfHplv26phEk$4j;aVTFW-f-{jRKP6Gsi-=&BURW?ozG(YgXcgZ2ExF(9{A4$h zUK&t54(D~nS&ucU&`@S!<9D5bu@ zOhDq;JihQ)D!#ElN$f;`!;GvD?iY!{2;_{ux5;32m$O>;{9Ze2X2WwFD# zcp2ZzsNeQd7eu(i`ksF;NL}}cEUE-b8n0db($_M)!j!7@21W^}5$C0TEiB#TLip=S zK}LqgY>@@GpuSz^eUZ`u9&XRW^%fEX@0JFS1CTu8BtCUw#K6E9-_?ZclY*l=G9qFX z7|tu8g)#+NH3(+%N3KvYNx^yOkHVL0CjY`h{x#(3)k(sU|WuUzm~h?(&53vxu!Qv&j8%xI}D=UpO($tnY(YG2)S(lqWa&q23 zLfv8+`#qdiJ59{ZIgj`1e^>Pu>D>yDjmq2YSJa;D9skupfQ*6R)Zr4Ok0h+iM`Y{} zcg3IT5cR}pX|m}}#ZBVGoIO+Vbmh{70dbjG|J_P$DxV8%pvg zBX3|J%EJ}0m!30hJla@mSfs7eQ*x`v5{sIgJZ0Y9 zj_Y2ENXeVn>K^RHvQO01v8dA4_c>C7k_pc2gYGMzG?-5Vf%UaC{F3Ig-CPqPA)%tC zW*1PwChJ_Re>i+qFMT8rx(1J&O0 ze5fFFL5n-*z1>(qM;1sBfyuIr{%V zsp+R5^NC!2OU79p$WdBi^gj8MK-4D)Q&-|~PyM>HK3WWz#xC!0Mn=Ut#euk*G`3r= z^7rE4T$vKNhsRTR$Y<3)$GdEqI3-agauvCIPsev}p89#UL3s^UogGg5I)r5rFDkdlo+oFqQr6 zTUwUW1-n(B3p5RV7rKvrI$b}*w-5)OF*2qrRnJ9s7>TN>Rcf;u@A#xb<;s&!LU<0N z?Hvf5coB@9W-mY`c>2$s__`tV#&?gKYH+hBQl#%5zpb((GqEfS#DyF7uKJN(w3E(y z``iu9K7D2BeX4KLu1*+~0*T0GtqCe}|Sp6f@b`}Pf&Ufw>GgZ=$p z_5-jTwqFP&xz#;LhLcMGV}=eo6A1s1OBW;yZW9H_*Xn8}B3NNvnMgz5!!+-bM!Ira}(ct@LV4)MIay?^uNKRnAcW@0!h79m96q%$?H$F(#o zSZNM&E5Z{f{XnbzYRsz2f`=Sz)-a{JzW_oQnBD3P#NU?NHS+TAWcfW%C^QeQIgXD% z;XZA}yM3&kG>x(9#Z#P)%eQ8q=LemgOquS2sXne6zrOAXd{I^Vl=kfV$X6{{cx<;W z$}#kPD*^Y(!x>+&A?Q@w9NN{xrA!*G1liXyadBdfGR#TAI9U@E4`CAJl2Fmq z^l1N3S!pH}(+)ntx3t)esVWl5oM_00udo_7AliRoITgnED&^qZI<{u1B-mm&n+WE! z+v5GFz8Xl>3ZuD}P6)ss3hg#o?T}V5s@B>V`AlM4{jthgqa0YxH=L$gV9H1>9Rp{g z&;1SXK3-So{H}H*c{9k*orx`608uSS*_O6Sn<1Yr z5N5@1uUq^Do;L5p>`}}f1wJArHH)D#qfqm@qv>9*&ozt4#OWl{zO{l!~wR#Qw zri|6IuIz87D)V(toYrTTD4`D{{3Lms^Kg4wo{^>W%Q#ndG#6$>FYxNrt|sejOxWSF zD|n^gK}a6E2>blfO+$FMNuFKC0Wg>@FGs5r<;-T2k1$9un-ZZ(9I1QtY}a7R6&4vw z4~Z}2(WmMnde`GnNI{*RpR!Xs?znKgxJ zxIWC&J@^=7e>e9p*u#_h@R*53M!`;ei*FlHo2zMXQ%}w7*N%!(RC>MO%n#}_$@r~R z)3FU_P|cu%ju|fX+VjmKCBi_Ezdew~XN(PzWvdN9qjrA_LZ}$H@(<3pO1Z7Z9-?BA z2ei!dKC$}!jX}FO9t2jf6=(;J{l;WD9Z0lSAX(sf=>)g+FBZtMYQ8>yY~Z^w{y@9J z3=6)B70Oy7pD!LCIcEYdHltkPb08Rh-tcTp2lBBaXy?uYpL8znjU7uB9oCIi+|l)D z$^YLBX&6>YaaRks*_iU}uZnBM`vs8!+FR;sbWC5J(0RBUG#FKjLSPJnf7Q16%2y_I z`||R@4L)3!?!yfy>Z?JD)^DFbYSua9g=R-N9il@GfBUFM&Y?W*iGg2;pA0Wq#44rX zc#kRc;z$U_8}vIVrNWIre#3NP-ahU& z*7ZD)6k2}se^Cp6Cz}G#4n)j*6NFWCbwNzHnqaG=vf%rSl0h@uxNK_6tiwQw5DrFr|C70} zelu}3wJ^z2MktK+iQMMck%If^XgJi%*B7^WVgO16j9aZ7O&F}Lrr4S7&fX?!XjfTv z{3_PD6=Bt^rF+4Je+TlkRzdT%0$DIWY^%A!mE(Aq{lM;4RzRmq!+4(a~<}$rikPS=`r#1e8r-~d z-kb^lOQ)3xBPvrqcm`?iUkoC6A1>Ia7wfK`RPRmFxh=A-4I<6g+ugkM^`++1!NU%I zzn1P3a6#=Jx@A}MKC6xGB&8LmG6)`=tg?O^0BFNoXpJ&N9H#{@sbLT4QTiqGqVg({Vvsrr^#c z!{x4MXjtKX);8oI?dtD0gVHo^5z; zKqnl*D*`^nE6|IS{)rX9CJ)b(d07aZ1_OAx6pRE1LuUm=$MvtKZQVxRU16)uK2H1P z%5GjUoq;B5DKbpjrhF9l)h!K$Dr^`Fg|Fmb$&tv<>=W(trgjJ8W`0q9e z+WIsZ{3DuUKkd1B#x9azp5eiNfAHOv*V8gTyU!Q9bIrlv3KUnAk^9qjj-jn6_wF3%v7#>0cC?JIG@k}I9 zv*yU#wVQ#@5ynT~t2+~%C!j(T(JEgYlo_ne%?+5W;m0;Lp*g8bWp=Gk>-b0ls-#7N zDfSU>s;+O|y8Y14By&Lhzm-@)SSobJ50zcorjHk7~G^o17=_h0ySF76jIDJ#nj<K!o6n|8kp} z&TD@4Jbc9VC@nMSWZ9@Ki1$;nNh&j(!d~_{hxO3KQuV_#Id2L|%W%5XtWIJyDV6%4O0GXTk0Tr~RY>V;B_nJL2^ zA6ry;Lf5)1Ij)?SB>H&*BBS9a^e*$r(IA$NSF;o{Km%OG|r@eD%RV9~Uw@wT5aUr?5!GP!LqQ9xflWtob@<0jA zEpkRQp%jqXQa6uEOa#mj{cWR{E8_gaM_(B8GX(^x2OW61ThlUS65p5I&3m+lp!(uA z0TW&iXJdUW3o|giefj%=uEi8~R+17!RvmwfW#2@xj)a&IKJ1R~%6Rw}{;qNgeu5bG zhj_CvP8_QzL$%j0bn%N%1~E}VPKbvPLAn*s3gz3o8Mm3QxJ?0d7mq|Pv0U`v1cW~% zFbj|()oLn6Kj3sWVXH_doTX5j&^ILUK4DzD5qhWQ zKEGTBA70Kd;ilCXr`4CkO1ak#4TDQcNu3A#(*rwW;aU|U3u$g5bxNOVeZ${qTE{x7#jQggvB2iCmO@ z?1f{hC0RO}GrlqO>6&PHZNe(eC*0m=^mM4}L2}3~DqZ+8F&;4Kmj(I7vmO;M=)a-{ z_4TzAIOn@8_<<>#J#vJEK3*3q%4nGd=>7l2lv5@k7&v65(}FiIIsW3% z*UmHQhyXSo>q!H0c&K})bUNdu$3Fq_Rc_|x4*OXk)1P4lW}T#mLE2x3yN!8Lr)tNZ zz~t2vNTMfmLha*^%kYRFmf?_hB(8@gny54pu~x)t!1D~aXn2m{i;6E*p5775OTsJc z4kVSki=2;ly_)Cy>J$l#)k@5Qhbap-->H zc<_&wiwk>f-IqLENpOFi8>%Fy+YSrMFy}JKa=H15E2xmdH}l7>EJt#mVqkOwR0I2a zm|h~rwN;m#;^;%}u{Xq}*pl_js{G6u--Kut5L(AxUe1uy0!p5DZ#)&2mw5}oKEyK zVly%{^4By-UsIKd0U zpXJqbHP!j){id*eu?+iZZYH*ZXZ7Gt4reIS&!*?QPUp81@YzCe>6K;PLjraMzXULX zmz=wT+oQUOneF?*@mG!0y-vGAq5l$BJ&=Ot_uLa*f7QdM0O&e66|QaU;V3FAADM9T zR^bt3_*^{SP(l z?#AxQNFIr*HuDRW%z;pwvm6GG94a#KewonsNbmUQcXzT>t0G=5`4S?qu*!aal}kD2 z0bdHp3ZBm$6p6#6@(QG4t z9q3TxcCJiQAQLTS|9@|x|9oaZgGzxO4|&>+mnp4n8<;4fVp6$2eUFMoS0zgKqlic` z9lVj~`1rs#rR~xq1ExlK@BpfT{ao$tyK>5>j)~(%2{hH1?^}MHCbg zNVwaqFIGzNYBJsMFtL8x-kmRBu(Y5X`Ko_aFn5}F8E0&({W948 zc;Zn|y!@Gk1k4LKkGK>hx7<55wcE@|$)BQrzFX$I(PW2)YS#p$aKZv_)#r)~NOpyO7lR^x zo%K#l%gNSu#*mQwFf@O}5hf9N2+YuIzNXQKXzhL3R}8+|{23&SjRPp}*;oBC{JOxR zp81amYcUyw3qtTx_*%M0J3GfYOnnv;O?2j!&x$11D5%~ne0yB71dZV3{sE%!)v}P^ z6gXGR+{~-oJ`a>RAFZ4vwV$_Hsvr^?bdP@AhMat2U zx!x6g=%p1F?cM1_u9{AnATMp>SK|{nu1GveFnL0tVVI8*W-v|y25VoQ1wbck)sHj7 zi2HT+CJ6`t3c4LEVB%9a_ihUTGz;mz^z6y7xo=!lB4r^nMr+LZFEFKvP z^V`PmU|_lwC3j_IwaoMYbtIXm&4i0;!mT6CO-Rnx7JDh9j{QDMMPZ@$^_gy+^L7W> zwg9C@VmaiHGAek6zDGMov1o#54k#FqM$0QOXZy!9r;vHK@j?K6S?@|+K!K^*33^6q ze;t@l--l4@I^eh3)Gq=Auiu0|VuLSyec9OMJ@KuTvJ9dYa>|B^IF0yTUQ}zba8_E+ zMT-X?qoH#l*tl8;EN6VMkdIXY1$T8piUU`C5h{B zzRRR+)kuY1aDb;xexd&7C@E4SRK3K7`ll_}Or&>C#DHurkmbU!t8tp0^xPyV>9XDs zD`&T1>So%42T|XERG5w0mjN#An~T3~j^9`H2_?YIB;nVaDPYGwX3a5d;}LM%XmOkK z#k=42&ybZFdfezJ`^#}B(26W}+CU{LIc?2y&sU;$DfM#a2`NDiVq5ESaaNrWM@Vz>8?4ZwBV!EEM&6+Ela^9Gjm&ueuL1pWiWBT14P?e=RbqCz<3&&C3QWq062%KtnR$;d2mD;#LujBMfb^!8XFl|Q);ca&v;C< z>GB*ZW1=FFkp(Bh3jP*{^9lRX8FF75|H+)=lkV;>-bWps;J9@<3QLT4ur>qiF!5@@ z52RBTRr)k+FqjFR|Jw%inF8VcY}XG_@$%_6l!9dmzjwXm#k?QDu3Fb-4L+HE91+#s zK3+=Bq{b-{P>2EX=EgOl*Q0eem!tSmqQMug|?#1W&0JZ_M38r{j;kU zTji#kbf~6tZ(s-dcl2xd)Az%Br`cZ5O%z}uZH&;>y;pKc5w4Wjx4I;M?IJ)GKAIez zkwB5zuapkv$b=GEnKQ^0g67|MfgEE#xI@KTk}qmEUs6wFc*>yh&9Q^4>D1dc{8&~> z%Iwe7r%$svkPtFbphQ4?&0qTS2)!V-<;QwGjVV-X5I&pR{N86bPaS@zq3;#xG3WjB zuhyu-W)094{%b74YC<7ZjoWSK@7u#-9lmd_{W1vHG{RkwQ+m6QexqHzR7bHh4}Fru zZX(5hb5C_Tz^Jf>t+{E*=P&ca@ngN9dO!Po+Lg+Ep0-qc2zEINllN8tv$up&Szy0D zbVrvB4IwEh81XXXVJP_!D*XMCk>qmgiSKuP9!aN$GX=P>ur0b~QL&VKWZ291{H z_c-8e#v=Oyy!WL(awS6@nucWEe+pei5PK0K9Xpq)R>VCC;lYCn-e0H6N`z-@pL9R^ zA|-VP2??xf&XaW%)dEYKE^8z^1>zy1$zgb?H}QJE0=!oc5$@jGui4y8v6#JFmeYW3 zl|pz5BwnwTf!OM&kc&Z}lU}tg(bg}uF;9@#M^}0npOKKTl<}nF=g&RZoN%rE zA-ld5NK4l+@XF|YsXy2ncmVQ2!7l=k+(F!k8uMm`%M8HrV1OX5udOi%GJFW#zDO#93W9q!QC8WAi!2!Do5um_fN=feLE&d< zI(D^jtzvdHQ}vP}tz{oe|J`H1Rb`OqJCi9yP&v)GJ;yBxun4U3u01Mx3_Bs#cRQr? zhQAe2{zD3^vTw+62mPPhPtAMnZcSLT(|(fV4&gz|>@;G_FT{ZbD1S^E z1s<)vdiADjGGCvbY3-qbIxWI~IuMgOmuJB%em4tqY|>mgw#_(@jUmk*R*dVA!!;my zU_Pkd+Jg`Y>u2^W9H{KY}2FDc$>>S-LbgqSf0iyfd z2OKfpez2fpCcZ660X)>dF1m>FvBsj*L}S?ZB$Y5Lh%d9PYEo$jG=GE-q2_ zcT47vI<4v9AVLm2P1Kx{vRD{H&@Da1w^bE_o65BBK_DFtm4K_!pE^H;nzbe-DH$2V z3O3>3Lmpbhde^Cxpbp~Wf=($jdZo`8WdsZ6FvqC{p#cF)GNJ)5O&&NXU|f-uklze;pAACRb$}<#ZfWW6?(gvX=0AhZ$c%9ByXT&>_gZVOrM7Gwx>>7qWq(J4 z23n2c78kA3NW&(Qh@O(XxcKQ*45~8IyIMI7CU$UeR9Z$Ea7q9uPF&XG1ofE+0wW;L zscm1L789!Z)?~*g=JmP75%wLI+}|fY{vm$rvcIoKKi>B|o-wGVzAP&x6{2USqL?@W z($3ilBT^^KL^LH0GED}ia|`Mypl72Ptk0POnSK;V*gg!kp(%J%fZ zw>jrRYaBI*UYG-3Z|=e4ggFfLGI9a{i6rgz>&?le zBgKwFh{^^;Y`x&;quTLhmb$y|l**vI|9DLG3y8h|K`jNeJMLD~w8q493MJDAKyH0} z$X{M@eU)+V&hNb21BVs~EWI2YB2U}zYz=F0PXR71>t@O@BA+^(AF*dK`imWvh#Nea zA=xjN>`#B{Ain~qS{LU5e}J} zbe{^}jC`2YOFpw%q*fg9(fr(w>yA6q1s=pOK~-8`xDNy z<*+8_dNR){tj{XcY8!Bdpxfn#7J;Uy2WZKcSbA=#YHA>`;X5{7m0@=V$kj^$?|hFr z+xW>(7TKh?VZf@!e)}IRW?ZZV2utN8WLD87#>-TKf}PyyU^;9JgnEBHV>Je+_;Y0! z1@@wy^XBW0ID>aB^f*w|r}R>?gx=29!S)_4lz%*=od$-uY{my3aQWfOi&` zmHniX-k6!L`1nvJESM#t*rp^if2U|+DmE@4Xx@R+)+lR{Lnws@6t7|o7v(K6kpA+d ztcg8$@A2?rS47Oa%fnUR-S+ROEq;=&4I%}`mC$n7N9%y4m8{Q==I6FG@8KONOcmdA zI2rp>%G2qLUK_$XGhJW7FE+B2p+Bb^CN$Io@yx5hY;d)>#CG#%Rg=T^sJGC~wGNnK zQn7X^5zfdid=}4DnKCCm4f(+Sn0mS&x;#jUp-uuXNB`x4_NaL^fN4g584&L5*zAl} zV1WaQ9&AJ4n0QGJh57+1^;s_WxJD-QC7L~_+~_>g}^DTmZx9<8|0cDd4FQNHoVR7g!Or9@oyj-1ZRlfGTzg(03@0#nIWnI zG(kroCb{k57H)6m!4C|YSx4r->*NG1*>j#b=wrmI0TX`Lv+z5+`?WnIDG+0crqr5^ z*W0U9a-dah*OA=-CvFYBw>wwB#+3AP(}n{})9PEG^BMp<4p6l}r@`HxdsQ_cawnsm}D;hAv1K)QL#(-rxgmP>N>sf-aWV9V^Vd{V53_$6N;9HtOy5**# zVy%+MtQX}!wB(D#1tr?4*-Dkx@>A_4dI?_xwd=;s7HJ9RL0^olZtA&eO)n5JQBko9 zw{^IPD_y%hZhhf@I%BxHa$67(J~{bnqp2CxaFk9|!hEMeBXnh+H}493$Bm*EG!Gtq z9xnK)0h~QZa$)*G3j{9Cj0zkLi^C-TmbL_9iH1ljZ|F(LJ^Mb_bc|WrJ z;{}Hl1s+O+KNJLk$5IPJA2zn!Tvi!T8vm`r#F2|XoA<5Hl?0*V;E4BRc{QJ1TIpit z>pm!A2gJr2dS5?U0Z(JHNh=Cosg8aNZNc5wFTp6DTex;hnAwVATruK1PAokm(!+%3 z=oh?Qbt{#8r)>wmq`P(|W6jHC@B>u4$q2-huqVftf$H72xX=x|Zuur3Sm2AOJ$Of` zil6Wks;S{f3#@|d7%;T*zWa1TaX4g1Ay(pvW!mC!F_u@YetG1 zObQ^hI-?p~wcueOBkuFJzXCI4*k6GQspI@m!Z@&}0aEX!ikMwzU(e%zkIH9QNOkj_ zK=bSVuT|T8i89TwDGhkS*>8_NW~R1fau?|dKHJOkiyojc3;5G>tyPCS5A(scp#^ffSs9?6zZX^G(GA%5maK)qs!ykcQqmu(U+h~FUuyR}mE+F4bL@|mE zhsHl(AcZZ#%74&I;uou|h5!mU?IHfM9iKjI#M=&wmWDo$j=Tz*Xuo%VqUPcGz-u3_ z-?2L^K5AJaV)s^MGm=7;S#rG^=UteogY{ieak@SZGk89ZzVw69xs5+C)B&LY_#pU; z_^79|XQM!uruRBvOb6U_NTt11A?_L_EvEA5`>I=(a7^PJI~@hKXDR~o?R?uDYn>=3 zRu*`wliC8L6~|PC;*`b?AOP^N&>MypO9H~AKrK3 zRZ*&Kh<*Z+^TY!D^&t8ik^gR2Ma&0q8}OFF4_V^le>vQ;G^3Ja;U6fGEUHE9ZZTuI zHA@{Sfc})SxbvA5o4_0U_!`*7VOYNoL>5zY#CbdaT1^a!^m0NktrH-t=)0DP@b9P# zg`kD=b~Le&n;PLyEb0e%&s?IfM~Jl8kP`SI@i5Rh#qVVAn8guEq~Pbx0?|!jIIZG` zM-9~d8-WZ~@!0v*g|uhZC+*D>#DcHju{{6(ix3WI99v>r;0^xU;4#@3tYUprX7Wbh z-2vLFHX65r;M^Y}2UFpzf(^WHaNvs+L$1%pD{T3Fsd9cvG~p{VC(ac_*SXCg;T43i zN3;Dx&F7ZN{83RRC^n3yt#Hj7eV@(rG*g6EF0jiV}V4eY3-Vxxc9Cxv|43RwGGX ztKf(dVWdP&H=FX|@JicmV=k&GgJnrf_GlBh0OZu*+vEL+ewvs6`DB{uA%A};<$+o$ z4_67#K<8qXm&IgsR|9#xcM9is9PPm$qfPwvc^Kll!o46KiAARr7OSKWm(nmuw2alCsOHcO(?LDf1CnK{pS3ci6Eoz29YnO%E-HPwK6T$rnR<&ol-vA$=>YdXt@3}5AXM2EB`Y=n z*+2s4dw?lwSr@&N==afyQcp4v2cQM81t#3rVCDzZWl-{wn4g{A27wYeSpc2`!Yo(i zjU8YE_Q@XiRsuH~qFR6;70*^9)sZ+IHmETD?{5a#uc+LZ5V8eAnFLPboM}dQyZW&n z0;$G(S(KsGzMIdEdA`Aa*tkQe@?9OB=uN9DB#ixVr*aglS7G>GM~z8DLzr|3;hrav zRc(n3g-R%TaFL9MbRwuq3U^;nhZLT}GwDo>7HExj;WwK!;+%RfzH6^+ft|eo;chL- zFZ9hx5s=B>IVJSqr_kED9SYY+VcaIp=VeE5eT85w7GcCPP<*hEYHWs6+6f`Pad@An z7PdF<``1yMoTwVRR6yjhvIQrSxlnNTZE^_HhjfsBSYiYKA|S*LZXg>*c6NSVPv(;T z0onC{%OqD7_unX^e}`o%*~_`n9#8Kd=P#8rHs0jw147n2+uz@5RGGd6pmj;KLOhq# z-crihFp(%=oo7FAXx5yva)16L1BPpTTEW!plGo8|Qg3~1@o7pBudLPRa?lMXEZnya zX6k{)+kCdNM!0QaM}HtbVt=tI?#IBB@!2M-MyW~Mh5cHAoqPuDC&#Im>suMuyI!lK zF55)C{|2jLp*~pD2xntrJCgVd@!KpB!QkDZ>P3Xg+Qfwj7Vs6zP+V;tV6!ZveGVv` zmV)R^!=5D-t+UQX@VC|fsl1>c>$P1?zDjANk;BXWHY&K1(RS~&4Lx|M?ZrGfHh=oBAu6Lp7zXo~K2?ec2T9U>UAtG>KnML!-3D)xF+RO%#`i zkLAfKWgTM3`d3^;1U9d3;%-)G1Yd%sR?@4%FhoyYHo8+U-r}5 z&v#paL__A5XD1)LXcPS4O*INu`W2AMY(-sW>G%wg(>2w;r*TBw1|?8;6G_F>z5PNg ziJ4Vu!zomQS;`#jXiQ~6glK=H zLH*a2gz0)vBD?Dn#h0U7GAb#|5rGH%g9975axha3mW;?Rv{<%()q`jD_(LfOh8a>& z0+v^q&k~UNL;cO44RoO|M0kloG=1V?xu@ALRpmLEo0t0UT2W}Rd(j=>RbzoN|+i# z?=N*6a!-Ms#p<>yq^CcPxrJDBWAcj+GDQuCcG3#TJ(v7TQkO%5rLp5G z`Gyj5yN6GSc8f4$)Sm1TEPKu}i>-fV>j$OWx094KG*b7gCRVaZD1czV*9(!+_FYi_ z1DLz(OO>S>tD0`jD@BW3Aoyx}Jly)b+3$_fW$MkB`>`g0r+XFNG?qx=M^4-{r&p*E z&I0HEbYMQ&PhBN)d%Z%vs9AWp5=b_1VSk?TdiMEui^4n>-@WA656wpYXdj$mZEQcwA`>9Vc0#JNUT)Q6 z8W)6;UcGca(hI?u-qvU&Ddp4gAAkGapS$K?s$%GVr2%{n-DiI4uzVOyIfe3tf&i30 zXN&0tuFSogm86(Pg z&nWaV1NAo?+Wn8KWzR4kUA@7l$JJMF-_AAs>#$Y+r2673ZiJlj<8jav5b)|!62BRn zn;RTp73>`nzO_x2$5RI*4I)f75cFfH!72cI>*h7@SE2r$S-LM@PjD(?b6LEaJ9CT! zQo>l*_~zRAWhE;#3=Nei0=bbZ<$(pd&qP|%?M=-Q)d$2b-H`mTTzGDZ40k z`PSp1`DEEDnESfoME2^-O`>!lfqKot~xLrAo@ zy>DG$I+?f*PyxIw{b*`iFuZuzSk^1U@1np0bkee(o|q0 ziOWYXZ1k!RGfk$>tTq;+y$^f$lyCb7?NnNEJgu zBj<5!!@@+}B*ho}GiDP;qT1DwY?c6veJ~i~$j`rY@V%W5mPBs3^J+om=geDTUfS>a z{+x3O*7>`!?teeH#i$Z2l2OcIXWj@~1@FHfBaC-9HEvk^f&o?TOr_Qg21Td7Ue_)^ z&^<-7LP#aGP2$*6n`saz&0K#p9byY6@a^viRunGcZ{SeD)bGwei#`3<-|gCUeSrYu zeDttd7&-!-)*E~V*~%(P@3kx=_wK|{;!z;i!|+%`osRaM{@$c^TF+`jseH8K4121u8uA8z7G*Bt0_w*0r6YTG`6OPaK zVvUV&EzAoS*N*m*HKe<5s$ve1ZEUSGA0EC9wETJV&Z z_VnY%;FNr=2#<*sBXaVi(_CAkF!qV+t&i^?T;_P^Es@%Q1*sz0U4cksfs83|y_cYLq|m zwWoXeCOKU?ySo6) zonmsoBw<0-oQaU#w^MzXQYTdCOC zZf{Q^_CPC?P=;tdwgi? zV^wA2KO@N@`;}vl_k^;{E)-*a7~#Q@IgRL2KjMzy=w8Y!Ua} zZ6EC-bSs9SNP+oETy_FN7*gqRfn3(QD+-8K@3yvHhZxDkUecBz@P{_%;PG-s*mR*@ zs=DTq?A`GKRKa1q->mZAu@T}cKkM1b7`+7#xGcrNs17!#vewfV0=mop+KAbYUX4sj zhejr$LpwRMHUetEXZHSNj-AS5kdboZD!~}ZQw_G&ulHYy+Dl9^q9$_>7p_7FCKJl+ z3rVQGDh|i0m81EWL+A@DqH!TG`uJi3@@XD6U&_u1+RmDMJXG5L@$lU;CKl9}DEVZ# zEZ^?YTr@@7P`Z$5g;2-;D057V@Zox9nzn9lwmfz`E?>oSBuGrVkxE}Rie#(au)3Cf zEb#9UfyT_&C2k=(PBt#T-)5-Tq+F4wRc7PI+ou{SvHnNDW85xFAlKqpIwg%chO$_c zH6*;2p>~lB(zo7V$y83{=Ufw4H`lWit?s57RBysuDg|H<5=iaM6E&4fSBkA*urJ4t zUbeIenAUwzV4;F4)9lTTa!7^@_9bw}eV=#N41SgaKX=t$<3>x?N_RJ3onr{r6|Vm% z8sfiFV&;zFma9^yFmg5j{5iLw!5%mdOn=^*SJxHU2)4Aes;hGBttCaURt+1U=C;%7 zE~C>$m`WIl3c*K$(D7L?P}9nFhwqx(P;}Igz<=aH*HDkLNW>=L`qn|!Lq)$x`?GXG zW~e5}kD%lb!{!w0x)SNBpr-#j>eVo1%D?|<0nU+)Ge|TK`1oS2ktH#gG^fP0U%2Ie z{Fkg8h%r4O-ScXOr~3;_zM7)_rtDkw>DAdb%#lHc^GI}5g`bi#!YBK}FJ|-3nOGTn zrH2`G5nduhba2WFOfXicxgpue;i1kgEe5-O<~pR`_@2u7FGgoa=L8q=$xn%^L!-6 zWOOP+EhbN`GLK~l(a|77Gql9q%LvYb!Rw{5JPdIQJ#CKUm~+yv8WvA0iZ5@n&b=t;I1KnaHkfKO#C`d5PXM zKA8#D58alF%?Et_#)jOqH;V$Cf-hw({xvzlNRXnGyLGn%YW$)wpo}Mwy%^+ znPV)3n$^r5D=EfdC=Rmn3^iX0p6*B$h>-unb<8}!w64o=X(3#iVHuFF+Vtq{>l7O_ zw=6R|PdtHRO$-9pNaETWwOWybkd2>p-d~y9C+YHXCeaXlQb^teVGxTaJI2S6J$5H7 zD7uvJH)^C=njGx6Cx<+gHZKm-mB0CmgD9r215J@B3 zdDy394^eHclc6Hm&aSpv(o(_6y~>JLnI|_RJJ-hihx9n+rLZsbIJ53Lz0#%IBZl_x z=Yx0|n(JxeL1gqdRwASsm-EHprBHF67)^_hv66k~V*;S0%V3aflHfvm27qW$j_85t zTQYhe$gZU}!vl_gkd>GZ3mUF-xEn#tDF1PN_^PM2R6CLXO0l?DLRq`1bG+3Uj_PM# zl2*Rq?km&L-)*(9KE=HO2H7=19?O{)hGI_P62hgnpCwyMao@%Wa%VGtGZv4n_7-!R z6UMK1kbWVEo^`M&_rMB~p$mAEeW8;$mYyAqF=pZ8<7kSSTu~TZOYJ9l!6w@=KaXT< zJK}}|Eq?BY%|C4J?dxdRFPW(i#cxLrj-F8uLBxj?f9y--hk%XtkJR?igA6e zLnxRCfJj|G=zuTy7vQTxc{)omTA%#u@^S#=9oVxP8yhvAHj)K7-T^t=^Vav51anG^ ziBX?@@MX9v)7fBOK^o_GVDgU4o8h;~`ft`O{Z}KudsyitoQS#hp)3;#4k<*OW_-I& z;9e%0pkK;|r8}G{ieFjUIIv86&n1oC!tVsVAPM+d)Z52hm%nD&h<+c{VOoXS@50YN zf3u@dW_D4Gv+c2o!B+XcTT{i~QI_3_5)+ypB=&u64PpM?ZOh_;e)yLM^ zNmSX+U~whXm6eh{cHyk{FSG!RK?v68{3gOC*+>NyvzY$&>t}LE%v#!ar_nlIjMAl0KUV`9B2-1NeI#e=36o7BrUUxJkeHgN~ik zfA&p9g+KFwoDC+UJX-JJwj4~_JuG;RkPuw-Dy`()H*128XVEq}v*y((+?Yma1XK5) zi9Zz_HI*vj$|kw>F%?BbiAq$s8@S<^Hm@<}MrF4u4?Mq5A$8^sjAJ84{+F& zGJ}aZ{$?P*g>h>Uk`5i9V`9k8%tWl~4V&2M*l?hx(w>{svaqtrDlG*>oU)~5512}P zJ6)zJCu247`AUxbZ%T~vW9^F)s#sv=^iL|70F;8c0^;mJ?(m|+T__eauib|qgq!4U ze&#k5eG6lK%dCm4_RABLOkzY2@H|`c8o~SKv~!~YPho-I@NiLH$EuS+gZ$pTp@N=P zB=;-En+o2gmOIW_Y{87q&bH@A=$+1$Jc0TA^rg}R`p}N^9Vp-7^+Y1yo=9`x zp{^2=#j6G>NDh~d*+-6*d-rkUub3>45v9Va9y4yucuPoPMoi0i5NR;F%`WBmwgRE@ z>*v=aTEXdqsRlH1WA2Ti!mnSPR%Ncr_og2Bbd#l}Lmyo!jzT6~E6>W%*eDxb|A?#& zShd;wVBVIQvJg06qQ@&$ooRJr&y=qQC7@(CQXSH0bs5`$0lJ5Y1;;D0o!#BJoO2|d z8O<6?z7q9v5MOn6P~y{fLYw*r?ejH3g6_TA+qZX@=_mVoQ}6rOL8KXpo#S8tm~guj<+i_vEbW|EOjWFt_E5bv8kc*F9r$wlmRyz=F0PKIL_uj`My zdMXr4JCA3hp6y+1jB(?8k3qLxxqCfXs102LOn(+Ry@Pc_p?q)9yt%u(PLEeVx2`QL zm`M%{d|T2)*~dpY0y+?sNF694JdmAOeC+HykaIiLNAK;@eXl2xqiLgpNZ>AUh$^De zn`WJt1R?BC=nJ20VZ+WV!V%w?J6ZzG1^~=fk9_Xn(jE{|{kl2jUcd^s-CQt#S66lT z4SS|x*CncZJ<@jjsaLRq^XBawX68ew+_i9dC*r}Z^PjeSQgEE4a*1P;@F2}xV=@e< zi669(VIAbfPLvHMb0v(AJHR9-6JC!+EE}x#uwE7_z{nTG^~V)Bg7s8&{5S5{?aN>y zdy3xuslxs0G!Y{F?NN%=C!`I~;hxMPSPx9f1%D_%-=r{n;m_&hgP-?1MC)Jf@o~jp zMgdNtGXf8Fx4Kp%3yYT%S1D3zQ<4mfB}PS3@_288u$mTQ1kmIr$cd3LkY6GNdDf~a zDfPovp}ok=%8EajuY2y4mNZY_0;iG@q9dYr-jw6Oa_F9FWytJDqCu!74MFr7iBbLo z6*%&}8_D($1!=u^f5)>LN?2y#n2ex`3=A;h^LSIjfPs{&Xk>z>)1$3)!^?d?-rH^Q zhfsn`3?XtCI8bdPQ(pMV*5Q90z2qh&)o$M=BMw=0RBX1xL{%umx0BWWrB4AO(C}Zi zwI28ciuJ)D28jOr)DZrIG2sL>b|*o?iy$q=g&FNPOZbzeDYW z%&iw}1USQU?>AfpTFJ6+Y>|AmlIGhekSll$QSdDFnA*B!;prvwVMzFNNwRSG`3b9& zZIlyrf|G5?pElATBGniBord#Jnhv{*Q#KS?rm(Qka5(iQ)w{=i!Dl(c{l4OkiM5lq zj}li{Y(6Yd_?v!4)?L;U8#*+QkB<{(@#3k0Whi;k0dSc5yoTb&5?baG+=P;~Sr#`+ zF_fi={pbQFbj+o{4^BtN$E)Wb@sldQ-1Nc>Ci10JRw5@Sdw6X2y}`*Nk2l2BP~%w_ zm3Tpgoww5eLsgfJXe*HtJAp(HeaAJ;;%*3D|G6pFdsO;(TjRVZ@pv;5)Uch+2n2>$ z3b)d2hJm*7b+9hg{R#tNdx$m`B$eh(|KdLqW?X;ANEq(qD)r6Ip&bR46hW!Brshzo za9f)F8f{)no8^>4y2mQ4mR8Cb7pl(8s1vGYtP(bkxRN3^PD9;zXTB60HiR%jSy8dC zuKW=;j#lAAECg&eI3NizqHgJnJ6&s|KEBc7I=K%GH7I`qrwm0cJudCqd^O@tWB16U z;vI#HK$nUe7e3gxs4_BXCCgpLU1a*loys(~nKZO7oZYA=r;vfn>zZZE{3-56Ft4gA zt!~-l2UVm%{QEb8p}^OvvX`UQ(Di3^vB7wQq1neftcAbf)5Ido&TEA%vBm+Bq zDZ%Q8xxUVMM3udQpWt-g{?^31@x6+Up5Ey6`$O=7zfIi6_@gfVCc>*pe1Q9@8UVxxv^RXR(Rn(*wEF)i{4thTVEqfrX#}|QLe>kWzHV61#tN(Q_ z0N|6V7~l`?NJHNJ?z1qKa4F}iki7hh7iKw|)`x~ubSD-cBr=H4Px>qyLCZ$NWGV2h zo6~OOSZ>RDzG|oy(ylns%8<$$oCsCZ8s(s$J z1-VQ1!(B4x4`Lpqny!c?f~=eFeMABzExiX!6&}Uc#C^5jqMD3w(-W~dMBB+{e2yHv zohG>6GZ@48cTkXet*5U)F*25bT*HT-6n$pO1J2qLt$^#`X|&buCJ;c6A3Ao_In8V0 zw0fkI1B+gvqCQmVBZ`O#=CaJpDbq;_sco40!S+C*lClxjhItFwVusf@`eRK&m|>;v2Ntuc-a#(@w{x@g z6n8%aF*mGS4;Pm9wqhyQ(g{n7#~+6`NjJ=c2&sN%xdz6haYNky5p}?s_I?onM{>jA zobBe*nsgu zvC@umn0`Yt)PH~2+G`d|K`NTsIK>y^;NiNKwi|_(y4!@aj`OlgE#&45&+hOB4{ACH zFF?u)18NpPuEsf#1(&T5UVAF}KYP^)E- z2mVVMfs+Fz`*lesBPbBeWm-Mys(js(pw$G)^-QzXHuld8kbK$2D0RIXGz9^B;S>OT za0@+IQ^!K=AANK>QsK}{EreSUb#0sXEi*_$y(pU|TfeYQMC)*eMy2`Nen#!eL4Z)7 zt!OZBU#drA6WpRhiQkx=*37TvH{>1Jkb0dX-Tw>Bz=)6l9t1|yJ`K2_sLzSg?CJ&v z?av#`{+Ytvfx0LR(kr+YZ=yExm@oHwT(^b z{Uwj^EH!iQdOz*qR`tY)!C~|JTO8<@){eoqG*XhGzC{;g&We@h46S@q<~Hwk$uuR} zo7vsaeh&Xc6*)06#kHuKjVn)sOUxks(ic-$91%oXAJ$9Sgg3z%0|~*?b4zY|e1tbk zKjRX*lX@}?oF|d-L<)M}!WxYqh=2|?E|8uwK>%y)T(RwW-!pn0q2N7dCMNyjgBt8J zeYs1Z19$((0%K_@fmXktZdf(T_rSQ7SHt+1KZfbe(68G^ZBQqscez_>8cUGs@Ho(# z=F)dIEWZmeqR8+j1}&c%B*MqhV<~0v$+&=SWg0;5M-%E~?G52h!x zyErYkVcU%oboja1&Bee z<==0O%`(5I7Nmdq8X3v0>*Hnb(3YIvAJ0rQ>YP@LL4~lNFi(OdE1IQWo*^SiAc)J& z1@~0^wmOnc%M)D%3Ko)QLlA(z0EpkIt0Q7AW7Rfaq25Df>0~4V`;weEGQ^llViE=1 zHwP%ct!p*3lsM^!k>KAekcx+a2hZC$mDdP8?k8l~hg#53a=a)13uHN_Lo4EZCS~y? zaHEHPD1r)I;aN+lAq?mbH=kg>|4;``Dw=KHHsu)umA;)j5d8gRCuoe&7+!u-?CAGz zNaoedlGUZ29jvoBY$4+4DE`=CpCoG+M`Wq7cjd3}%@=gqyqWiowi`0u2ppqhiyO?> z4#BxF$4rkWBpR4rf9;<{(b8Ma+b@G)6dhz9AKPc9m>+nF`Q&E0t;Wz3kotsp-yw69 zI8@PM=YJonxzAVol|VHKeA$Kt zE5}S?my{sXz9EN-#uP;rwqwUByO;s&GA-WT$L$}$xb=#IHxs*Ikz)DVIq;Q(s9^W4N3VFM^QTG1**yJW`{ zD%FM{?6u-&VuO-qBM6(dZb*tYU&pH(^9}G{QB#`;3Z2~`H|*{nO)M<*#Zs4FG^L3p zOX>08gxHI}P<%!9l5QSb7K$6gcHE&Xdk-^MICdmPjlZ9Mj8?1F13dB?E*r_%EhjZd zo1e<97i%qkJAbdIu*yg$@Dr;o32%HvG!6;{T>ZT{5$KM^_xh~wig`YNO*WWH5o=Rv zk*EwK?c;u&7#Ehk5+33u zNuw>Pqx+at+~*+gJ!My83C`r_uEJtka9PV!`zKQn_wTEt{apwq64Y_?o1oC$AN7&m zEk+RCa!6{IogtYKeJ82@UHMPD41s*=#B^Q%*CeGI#cg>LcF~9BegDq(?=wlSQKqK2 z?k}Gnd%(WE35=N5D|!$6%>~tJF3fo%a_b~b5#ANgfcBprK(kZo#NnP=;u3qkmyF~x z1gMW%pYH#>v^WSp#q{LrS29mjcvfs5xA$efQ0MM1*i(Dcjbeo=VxpM~?8fgH8U&8= z_ALo494o(#GX=hWpXQl_j`G|G4@?~{3~vq*B|889G%Wn2tU47qf3V{FnsqDHk=1l4 zS*`h|M+UAoK{7*ea!JM|Kfvg)r!YCVx=(iJ2)8O+|A|tTj+%oC`;Y9PM)kJS<82)+ zJ#*>5^rok-wWy`IiZN@$Co#&FE5;p)xFXlU&%Yk7WD1+EjFL0%h@bvwL7GIn8>)_n zx0kDNM9_L~ndy@Vq6ix!Et2^fbj6grsFmJ&+kQ}eW{~`-3UqB-q7`N$yoO;oeY}CQ z#IFL#J>xmCpiRKJdGtQP*Ubd*VsZJpgh)^=Jo?m~1_=xHQ77mOh0yxE zjOq;=QU9IHRdRjkqZb=>8yT22y&`Q|BjdkCPl~-WMeqGe8jiUz zDQc(k`b#tF)fTF%E^o^CM8TS#HknM#P6>be-x~naf)mb-dsjksuuuF)rhceYG1C=G zEYc-rY)})IYvy|$IcaiqO=CIO`~T>F$4p|^Q28(*Ig9!pL&-MG6KZTZ3DoV3^|$kQ zhc2FD=J>}KJ4x@7zj9q4xHh-p^cSJvTf~_Eo;0K!6v_L9rKSO+nu5HNK7b!6Z*%ZQ zOFPBF;`3^f8EHX9)*H|T1Q*B1CRLJg<@DH%-gj!+U5k2O&1W3{8F)IZY3VxImM@yD zuv&)BzPof_omzgIXzuv6Ou+@c?5GP-jLjKtYugaqb}6-FZiCNQd+-O-Ok+CJM4J30 z7EWVdrU6y8snN;F=n5Vw%gONI!N2nrZ-=_ZHvt;?Szn7d$-p^zK))|3(ZD$|B{h8T zS2Yo6RD=3HszRkG7_}t+MA)-*tOWT^I;CVuwkX+DWHG9Lg!698SY53|*@T$7CQZK^ zeSF*;#N!^!4K{eAkG^@~@u!zPE-pRX_BOqhP{^sf)wM_@=S9?GvF zi^Ya?4UHO%Hc4VEzZL4uuzT)|-?Uxdpc0fO%mrp6vK!gv&wJ}G0DZad;4hD*RPSYh z=5n0|8K_i=so|nL2K6YAsRjEd#tig@i={|d8-9;C*h(?$Q9KItcNH%yZaC-p7G*&5 z#9p&cYL$;<3aPRm+}XY?+g1=6<%}2}z7!Hh3wlo=P7MD7Hq(|BtEe|j=yrnCZ~z#U ze_ZKSQv15<*cC37{c5ZhJ;ugHZ2qn=F;+B};<3pj(KNe|Ar^uaa*o+Cj^NVd2lpm1 zZ2rIiNJlwzSuYZ$@duGFm4CiL!M$pE@ovfB;>*-2XTrPaCeFA?TE}>>8Ko1vXm1TP zerGI^J!BoQohL7y;|_!!x=%qJFq|jBZRN!T@8wjC1Z5Z7Mn+TwlQ8MYH9I-SwAQfP z8=kx@s$2_J)(!QXoqO&XKY8UQCFT>%c)b#OCA&zjJpV4Y)>a|>TQFz$ggD0phaKM{ zUFdkhm{qxre`oW>G^1DzU0cIJeE7`X?rykvvLt3Gr2rIuwsV)Z863yQiAngrW>pUY z4lqNrh?X^M%&2w~pxM6^#Y7u)zBoZ zM<$t)Y)F`?5}aTn=yR3HJd0UT*^oX?eKs1CZ6Ut&AFfXvOMo@!;d@Kw49K8vm8w^* z2z19do1->ey|kw4QtE-reY}k z3^M447J52s8-cRG4t=0Ps@2tPi^VB!=xTrFV51t5@N=erDz{ES!!~PGxj7~ zu58H1Q8YXo3id1Vgff6V&o4Fj54 z7qcg6SV+2xrn{RsEi&K0Fpw)3MUXyJ=Fis!@%zb{8D&k)04CDf{VB+y&reL8t}$x# zb+V5m>Jwxy-q?yyW?RzW1{0j5B(0n~$|ebYj$QnH za&;dGX5}yR-g&FV85)DGnLV}+5&CK6uNp@c^)~%#$t$k*avHXl``pQMV_kP;cslM_ zY{SnwgnCw=P7ul|D9yIryLJLhvnZOMmb5+RTD%+9Q$>?NudBy^nObkLK1P(^fxhe}T!R@LuA`!XO#PGJKc05Lq5WE^nvy*E zt*9w|PlV@DIlk8tS8M+D6&f+84{WQis7zOQ7kRV_oFZmAfoAAn^qP{XJLl-Oy}^eG zPM{0DGS>!GASeq~!pl>8v$5JbV(WBGF3cDMn`KM03qLvROaVpoEQ}bX0qK{2!@A_)!dDDD5*(^Wrw(OKOvpM+S@IykjjlK;xi%~SD6OQaLfxQ_ zAMtQAqHyM*+s1YE<9|TH{_U+t*TtKkYe}-{-Y)@ z%X6l@Z;;cyGkOlAyGv&H7v7IwddoE`?$-pDmaw9hKd7h(0?SIhhf5Z3m%Rf}RxP8S za|VLaJA&uI7dq029xTW0bGAoD8zJJ_jc8Hb(t;rsU&w1kgJU)%#*9BV`37JLp|7{d z`3-xW>|_}8r=N!8+5R%S?AJZPzMsob1xWh!7;_ygI@3NNdp*9@3_6^N+I3uBsl zXkCL!G79O)UonE;)R%uXR$W@}pQxOxgU^wO3Twfacm`Sxf4~;E0WYFFB$wAsi}LzBr)ku`S6p7V8zNQ=Fciy4DB%#}l5ACe z*IuicT%*;tpb6;<|H1+D;MA=wFYMeNZBt%U^v$aL=9O^s3Ene%1~#^yeLjSfxf0gX zdq)MpO*fJ%G{spM6f-9kV|9v!6-c^!y&Oz&#m57-3phAbUlY+Cj}~3QNOQ@jH*1_=CBf@)i>%L&I|=B}0jcVw|v4cvk5Rcjnbzr`7i) zD0}?jgm?-t^s?<}HG2JiIDhsr-tL0h@e(r$Ttakx4#WZTH;O>eFimFEBmeLA=*hpb zGBK)GCM<&je}9eeXG;H(y2FB)uO7EkLK2A~@*C0!FJmSS>bdn1$-%#^UauR=*qPJQ z<7{Z;LxCODkQ|?df<6WxX^=sf1qB4$YGLr6zZoZd#2CcG0|Ncm7<}w0s-nGbJO=ur1>U-_w6g0PiD!?F6mm2-0E|O-M;dH zR{Tcf_sGN5L*B@vdFi|GUxC?kF=-4xwnp*~)6vE{-+Pw1T@k%Isfdexvh^`@l-N61 zXowIY|Gho7mdI}OrsN|t(YSY>@~2P9;V`rH1~$`k7${^v8y|?}K>-VN@JtHO{v6Q# zp@=YF4fZ+L-~6usH2fFK3gZo%u`2eDv85&M?F-276$fg^yDL+OX1?jE@n(jb;m2+i zn7SbhEU0z78|u~)4CltDVO>!30$L?E$vDi8cW||PZ`3?RK%>GipDQw+U)Y5&79o@ur@-d}WG6@RZS7hgc z6U2vw#y37+gqa65xSSqk8M(OW8E*8lp~35F@ZS0eU7+LXo!WeEyh-?SG&k^9vxzy! z8NqMy@ZY$tu3t7C&{wnXO?wHg+aLy_VIp~{JcBHt&$(?LqQ&40g8m;(R~gsj8?Hf= zl2W=wknYiqbclq4bjj%M8X(=$jP8(!s$GMFI;X9Ec2!>N)go7lQ`4f^<3h;sCs|)X6cGZz($dHkHEZ^me2#JW z8cJ6?7=TzL3;Ih8fPpQicSipN31Pf&P%;+81S_^w5fGRM+9tAcbG?TJj6SY}O9O3S zFARYGUC+ZA^hNsS-Rx{UKIL$u7;I^F`F#b;QGb60+}pDO?uDy?)PL2+hKzIG$2_U#I+ z`%Cp#YMc)ZjR`~7Jh%&0o}E|eK2MlFcjsL|GZP?YJasmU-F0QDMJ4mlx-?)r@~%^l znzU{a52;Hkrj$ykEeO|G>*b#=6`FrRVH{U3n|5aM2SVy(jcP7A$QaEU3x>S zI_*c%Z*}`L)8dY#p=&R~yKwQE_H)Ki09qWB-SCXUmJkr^$WNrx=PO7)gBHa)9d?~- zW2GN&2vE@&f{=9)R(u@u@k<9zcTtX0>o&^~09`_43F4;}#wapbJM= zSN8@fJ>UmNhnwVAeCOI)C+E6KNEhMFW4ilSElqZ=Z`}NSg$u6#{NUd0uMC+st409+ zus`}dhA^>q@6$XuG+};U4Ro>tPD=m&uKwokgT~m{Z2u#hNcSAYMY!Itybb7si}6 zv)3FH@og48dPn)eXR@>^76J^D$=ysxj@;%}D;e;{xEM9g26czQ&wpiF;4`~dj(V0n z!%k!9+*Avc(<#pwI6Ez_pj>fj8r-sm6GfE^*MHi7&sqVbVZh?#II1TtuKogR?IL=X z?FHi@k}0F!N&QEHLP`i@HSN&Kujl5(BAy#Ny$?s}>}?jz^CYeYYKK6gRA9+f%X@53 zsp~?hVtqoJA^)D!n3paqIL(XGr7+I!-A}^R=U8UkKEGtcID8{};(I-yx6*VT!Rdz5 z^k2)SEYXPexW}G%WS~Tj*gK+^T?~WY8}5%b6ydU%j@4tJuCU+f0IiA2SMImIO-<=S zXD7m?T8$C+lwK(Sk3ksa!lNi3Q+P7BJ$UGzhY0dGbBrcMoo0W-kmfS;_u;(EbWFr` zhD?!!J0>iLGqGG-I=CHIE|%~1r_`5(uN*erLjOeK)C#LsYQ_;9)+8!nvTv)Had!^jx~T-ZD7Yqyj#6+= zYWuRkQ8PbV5>4co0g(Ge$CAHWC(}PXA+ugx=wVscAqi|$nH-Zm@$GcIRQDb*hd%+u zpVb$RN}QtgYWw&uaS35$X|iLUX=&G0(3nwXlwv23%Zp@4$Kz*Ce2S_%P(lQdm~LRJn5Dj^P1yKhuZE7Ig12zfRP)N3Q{zcO{KyyW z`9jA1tJ@O-{Lvc6Z5tUsdLB~0s3<*ACVGZ!qYE=KR>H%`$b*Fj4i=M-u{6J6(S=zx zc;j5VR@V8D0fL&Pwr2Ama?kEtnjaub+9M8EV~TGl6(zq1{llDHOR<7*0F+Ps$*`79 zODmz;5Q6qsH$`qwq;jJ(r3f5*Z)m(P`@j!E4g9$lZ(|jKE;*oPi4

0Hh)tMXgmVLzWGc{P1UL!j=BFdUU4Je^yQu09;_Q+S*` zaQV?H30BMyP7SwdBeu2Y2+P4i3%)(+A-siAKK*R@&B}NAW$5OrzLK8|a8|i0Z8~)k z_mDfa&JKgQE29@NhMIey-nYWTnCz#Jb8cM60uhS>8+-AQzN+T4f>ouiUL2AX47^%nvzfEvdLj( zxuC@Nc5C7lo#VkmE}HZYZ>)pqH1`iEpHgAH>7?$DalD8#J`a$GqrG`PyMC+k=krKK zBL0kfo0f2fT)Okl`*3&LOO!OHzV}#*&jz$gY5hQnH}@BhP1~jhADPm>G}FauIc#8a zD?)4iY0=8`IXhs^V7BF^kdmeC=R^n|T zahVxDBIi-UiJCHGel!DV4!4`7Ue~%Lb++3x=R?L15yTI7X9N7Jov({2c(HtQhXlf- z^*us|>UdaNc4;wmw_P=KI;L$K0|8t7!tU%t5`cj>!YBs(fX~(CzGn!rV-$Z7sG==p z*=t|BhMUL8l~4Ig!cS3Q0h6Oj*qOfQ4Pr{c=~eraR93FPgc5&XBa9qo87(aBs7o%j zxtZp)n~%Wky@@ZNt$m6|z`6drt6??6^X7h#x7yM8ngbU%MA6Cnq*zo!yj&PWqbgqw zX27I*1Eu;!^>1GJ$pAnl!GG8cy-Py?XNC&e&9EK3wWu|P8O#J@h^@PC5j^m{dL>(h^pZzO%cz78}V_*58Fw_^n_cC;`!nweQ<+-_Z z5`W_g;h4KAX&o%O*I=?v#5nMWdowj_ul*W@hE+?^KLy>YtgM2Yq-3s(3{J=baS6CK ztp~7)S}wcWii$PAo@l$d)k3=+2#VvCihh1;b-KRjm8ZZXxw&EFzN?jNo06nOLbmxMrE|W_%W1zBpOa3rMbp`S)PTgoYLqAaj;6nw!e!2!%=77X!g}_jqczL@VCFDjnQ{#8UAR6;)XwuC z{dmo{cTjDqYXR1v~)t=c|#Tmtatpg@=AtF^T&ueK)`E|NZ%y0NY zkjE;kw25S#%QLb+*?uS64gIx!!yheQ|mJIEmLqI%fYJJyXQV8)6A=vC+*6mWDZ0l@xZR^NO zK|_E;g-CswAfBJ*3?|1$`(2N3s1UFpc5J+C*bE$wBV9-5W~JR#h(W!_!(+|^vHOcI zJ!NV2Csr;zlV|PNk3or11oE%LW4<>W7qnNJZ_`4&j)zZC9u-MJ{IgTjK2M*NL@p$4 z8df}51usd{ymmrcZaNemPAPq|>l({pZL$H6&1B_z*ONfVqJp2`(|&EF+UGkXcNxu_ zxk_m((iMSr@3?{ZnqASqI_i$`{8|uoJb*tA8({@lHTYr$C~&-7Ick26PxKfAW-{0y z5M!<80z>ct)UbN~wwI+|S-|L}>!KTVZ7PqUBcI*97jSF*dvK8ERWB)CBJ5>x?yyK| z)E4-tX%+dql;OkEy&AuV6SEO)y+iv;_o!*+U{C^6<9;tGF}FMC^6lesaR$2|%l?## zhyx94W^^1t*Y@?eZ6LS(4!;K<|KTLVlL+L@2#CrbT(;NqQzJj8G@83Qx{yoF#y4De8!{767&sj1sGf|U-+6_5Rk4{bn4eXm-Ocd8lOpVRXq5s)H!hX3D=z07} zbZc+F@p($y^N4x;<#-Jolq)z$bNziV3j|@%ZnbcCsRMRXzxUvjt+8^=L{dPNb1TtIv9l;3L_SJi; zY(_?GfV?wY)D7*^wA)kqv$CE}R1WfVSsiZw-gqH?C(sJYqS-cf>oYgY@D<$d4!8MjS7+1gMk>j; z9^d&sn;y5VFBA<0Mn4b1WwjTZ?Qj zn!55=20HQCqmzxh46@EE#&^o#oOq-5+-NZD@tO|xMU=ogM%#6;sNJcqD~H9JBnb+} ziZ8B`!9)NW289TjuMTY&{&t1}fPU80*Nsh1Nss^WLzv*^$%%hIzBH7cq_0$v>;KBO zMI7g41Q9RR@VOZY7b?vDTeBB`y{-dVy0ta4&#|z;RGIP2%!w=0{A{Rdn4d&20W8JF zY$f9P`k6?`=p{r%-%4pu`jcsD4o0M5u1xjus0`@&a=P(xOhJy9i`_k1m`K?C?ZA?E zQYvdx$WW1{+3nR`rzGNgZICUWio2$R_@*lQabnmn(`rNgSd#$)>zHlAkEoTmMMl&X zo~FDX1SLY|a-4T@yQJy6zVYcn+G7XcvKA7gMjXi;4&RBKW+1`EJ;bO5pXP~uAF*%f z>pwU6*IA)BCbbQ$vx#(!SBPq);cw*gKW`OIKex;r9ET=lu4xxSB6aKMLxb*qTV=88 zelDNKpg{1Hgrc{%-wR~8Kf2p^UsQ1A?W6thjk2P<-QhSD*HH7O?n1#feX6>)BMTlT zyOSHbhw~#m3_m};^ff6-%8oaxDE2(rDTNV>m=QI4MO`VP_@vvTxIXWTU@aYrL9Wa6 z&vV%xe;)SQ4BToYRlOr%)<^xr(*?szN{4it-tL8(o`-8g6pVFmK>2FBdin#1kw`O+ zdq+Z#d({ii2NFV@meQ5Q#gur-=iAd^DEmmc{+AQT86^urb<^wI!W%eB!rNpz(>>N! z--8!j)?0a(H!@upd&*JByNl3p8!qE>tuqL~7#W5!*V2m7T~{Y>jh!iTP0q^+tk z>7^tbLA^vz9)CYDLwT`%KZ*>Wy?UM8biE|xzMbA<{rtFBVy;b!Y!NY4YHMy7tm`Vp z(33`}|NT1JJK{6setzL*JTrCxd<%DN>r%Quj^+po+5+#9kp6XX&d!a9ST%;7Eos|w zI6Sx^=%sM@Z&P}NBmr!cPTs;=mS<3mK(8CmHF_ysiDY0IsA|txN@S>lYiR}1Mt=-(4%+>yO-GOIe z7r~OL^0iS-5Z_rd?EFu~@mik0Xa_drS+plJ+vJg?W{Ru|CPOUhKsFx*?KYQzBu;O& zBKwjKo>k3XHDBCmZN28_Y-Dae`u!SLI?{Bszo(R?lx3-+mNp&hUa&h!@Th@_t?fpq zVwliThcJF_g`+|>>@i9xHm?1WMN6Z6qujD0-}_LDp#5CQUS2gHFPH9<(L+1~-22u< zolUbd>kYNrT9jbeuyM^+J~4>z3J#Jh9IvU+9jJ0$Ho1Du_N9$3TDbSrH1pGWmUyXN zQ#}4_f|I%N{9?~q4ra(UIqh*JN+c4>*;)i~L?&loG?^#!v-kO+Sn`{*8|(Vf*{NPS zAl^>DUd}W>6^G+>vB&yM$l6$q!<3^22{?sI)g4193iDQ4!=4i{r?$ zON5XZg;P7N|9>w)v#Ing>}##5%Lf8Dy{KZ%X>2#lkf*77$o>A4miF|TyP@%5V&bIG z^?{n#>P2K2?f(ot9;@~v+{OfJI#n$P#@P46qRjw)SfLZe1ll6TzVBcF{+QTR=5w92X zF>Cs8|I}Bq+T%Au&s*>{$2OtsuLNZ}@CTFEzU&QWja{OH*ll(qgkGzjPGG^)M_<2B zNXqh~Q?q(8TF`;*WPVSA`{No)wx?qdg41__+m7RB6yJKnp=_okLlaAr*Z!{d}pV`EI3B+A6#I^&<5CY~W!tR>927BFFggF6@$`{g8)N zFYU*=o(!?`Y!y zU$|P^?Wd#WBLN4Mb3llTn*_!#FX{8$Jg1mlX1raSJfKsxy!KGJkznR17OLG0;$Jz4 zW`~ls|D=QXbo$32(hokSZ;Z!*MVNI+sCD(rl-h$dd zZ5Q#OfF1d8u?>4CBs#Mwrt&42;}&olPVfK z0iV(3yxg7=^`F0?!xnyKKTiw*Wx%!XSYeRkJR@~ETN|$(^{bR7x=eu!Jc@_G#TNE^ zsU=OJq->;M;ie$ujJ9oxC;Rik z(&G<55l>bydD(5wzN<51D43%+k_GCNUuObYG~$V%K%dhEJkeacIdS=U+6Uiu+tWQ` zavItcyBz znDe|$lT>$m@KrTGV2K?&2-{$O%}2w#G7zk3XU*lx45^aDlacdiK#6_!E-TgO6lssQ zq5REw80^VL@2KnSdft~m%yaA9)?40j9YJjejq2GBn>+4J+pn*rDQzQG=aVvF;kNDF z+tClql*{`#NkK#K&B!7VB;a7xLtri2qHrnx$XX4Kv1*U;e+j9+FwU(A+hq3a3GQ!-QJ!A z>KxiIzgEWk_CvP!Q~@|&yDf$zEJVrPe2lMd#Wwf6e>60)P$wNV&+jylu5IDB50ZV0 z;yy54y0XC3F5@Txx-1rmSKWAg0o5U#^*V+kApq~Z37n7l@qxCScx=to!p%t8!qrVW z*A(w+BZVJU!*l0+!)cJE`-)%t_5SS|XB?&p*a~xQx@fPVE2l;^LY(9lfvdDDzy{ z+<$+dkP#YOODw1@D65XF!(2G#!vgfdWOvV`G+bWDWgcRB zF-37!Ss#9~Q!Z+-?Nx^pNz_$W535z!$nyzb1(BB6nD@)M4VZ{p)_P5$QU&}jK5UBS?Ne)8ekv8OR< zt&yS&1=ILuWwk}XbsVJrCC-Z0KUL}S-ahanBFz4+EzzwZp=ono%TqgDsyTvhdb;di zIL=4ZH&T}@ya=_e*sK9N*t5Y0%`Z(s43(#T6qI@D3hdMwi&XOic**MYf#QzcOO)G* z&$qbbP^BO5431*m>tv?bO4M0RPqL^UAv!CBop1~^u$7()dp4^HU9uq?UEYCWGS0`4 zGCgiM_f{1Z8`AH*L+M5;J6r5nR$MN5P#2l~y;m~gA5BM}ahngbjf$+Y&nJ-}A- zfdBN~sPJ05&)f)2dX~#VdfK0RE-+annU~lHnS8;66D=LXkY9;fyk76@<}LbFFeOnD1DqDux^Iu1-TgFGJ%RCPQEdNigU0pqM}?J zX7_fWZ?u67YxwEL37Hq0k>^Djc@V&hYX`5SySk6@)Bi(6EeztR=1W}B>URtEPv&{) zXKOwG5tZtfBtDIoJU9ra_Cu*rQKD7(UX&G0U7`MxS6}Z$X^;IEIWV%Aj0`DM(K#OK zq~Ym@p-eJ2H^pIQ_v{JglOL^xw8lfN(BxO=_#4nWy~Q<92>QU&Z#&%;V4 zRsW6XN4-F)d66q7h)hPOL&1XE${u9J zs^I8&e(&LSe|M=09t;0t_9Azb=!G@Fb--~WZ5+X{Xo}&7h8oeEd}1%L$JK^qwp)cu z7H+I2hpfCV`gvE9B>?9=xE`^{ixN#m!OB$aD=pQRdLVG#6ufc)gF^j@CvnsDT0K4( z*Aovz$UU$2?d2Kl$nClmHW}@5=121#a#v((GS(Xnak^CiI>eGRyMD!dyZBBOLnOsb zr{=P>HD{Q;vads@iP9ew7LBF{qe5naV$SjlOXC5=>rJhCwM<$2YEMPb#i_S9nSANY2@7(2v#H zHJ|TqkN=6XgbZa(vukgaqI7Ck^7cFNW*+1&&b$_`f20cD9ZPDDh@B^qZT?nv>&vOt}L{-z`W@#GNEEau7hr0LGYiLKSSbayLCi6G7!kJ6L)Y3d`b|Azh#5oM-Wml7RwSL%9l=Tg6bYuS;d}9n|W2n1rF7Ga9sX1r0W*t)HtlZA41OP!`pux z?$Y7$kYTL@Y?Op51}PSVTk#B(RJ8QnUGDQd{$Y6suaP@@oqh4*>$qtoU~`(S62EDT zhYC@37nqlo_2QKBzB>$l2?8nJM&|Ko<532D9iJUbwP{EMK6NmM;qwzA?CDcGT|$`i zsSq~I{#1u>lLjZ59Y<39rLp3T)He*8owX_>p1`S_QK#xuue~aaH@N9byo}6c3@opY zr~7?z!LPbM@PLu_1=|YTC+`LCf732^;7{v@wH=MogSuB5Sa*Yv$DEzrcJa#hvvaPy zE^`#7dz^)J*qWSlg{GO7wj1-W+0C^qo$wAu#gjYUN)Ch(vxxhKR()FYRdGj_Z_&FU zTRrwaM%noTyY_zgLl?sOIJlJ~qW1B#vSh`pgv{Eyv?B5Z3-M=vK;!O{-ZP@?%a8AYZ+ zd63tYlXhtvAS0ti&kDfN%#cXYjWbh_$wEVMYI|J3(o&wN;axl7fcn*eK=HrQ;TS+} z`_}ZD_7bW#m6BIsq5;?;k(MyJFV=}rGqm#8fXWVsm$^$A^1b(H|bgCI=no% zB=8sATa|tnaGabgJal)?J)=n93}&P3zh1ne0F?nv1TC7X#r{$35?#Wc+Aohz(s!{EMqykColz7^1P&y6rBZtV9;q+dtDQ97LQ;T zI@McsW>Cz(r}#KyTx}}Nz@(W&{BZ(3wRQT1Il`P;&qS%5bq%2^FMaov1U~SOi}VQM z%7XeYfs3t+SS!_1A5iSa3Zu!OlKk`=3Kj3^CZ8Nsuu{PPO8d8sc@sneWU)>fyk=a~ zuA;mCJ{?Rbn;!JAuG29-8}xOz1{?HEw^i@*VBAG%20JngbeO$LzZ3&Xt{4N#W`l31 zJ()5qAG)1#l)tLY#20u+kz2YMKRQg|dahRIo?mgr*a?AF%#srKB@wO|PL?xb4OY@V zLQOQ97rTI>8aGLse0F>Gaatfz)Mp>XOGT6@;bf!qSB`1W#rB(jkp|h9 z`sBK@&3oC96k|uxsvJZ6glo7gbF#`)4h;qTClW^Gn|0AA%oNy>V!>uBW_fN4UR08F zBZE#o)@FXrAIS!0j4ye1WY`OM{r2`c*ko)NUm7U-<*ePbxXL)BgT({slW~x-e(Ypo z3;fx!OCLK!Fy?Bz>e~Jz2|E;c5CjFS`FY{Jvsb-rDE3JELY<*O_=M48AbZ0`R@j+r z*DSE4PBt?l(mR~(Nc_W#P+Uaf=P^y6tKU=>S=}tqBjLTT;$Y3hdrUfo{VzDZx{?R< z6A;ElIL58W+nlKxt4IUptDD^lKu>$Y!x2?T7*JLfrz8sYSp4Bd_^+hnxfOv_5EBTC zmYywgeKK%*5id$3*j|_n7XE_DhJ!wbjBqnRJeFN-Es&Tb3Z)z!Bzjk6pF0}J>Qw$( zblGpnd2L%@G*HQ@-2dG)P=~a=z)-z)@dzI@L*myR&e#@__4p&(+<^i62$iE@dvpgv zXUUo#k_)$G4XUrSA1A{8tfAd(fDs>7Vwpe;rrB_sr<1zdaH?Nw7AvwTYWyvJsmHDo zZz#mB+r~%UJR&{pWZoGF5}Di*daohuE*;tV6=1XDyrcZnucRCmJk7P?FFcF#g^{Cr z6yb{p7{J@6${9Vtc!lUmc0e@L2^_)Mie+su_rZ5CP3>riYQ9L}80epv+Tk6FBbtJI z+HK34Un|vAnUm92>449EGp5wy~p|3z$%poy$YB6WXS+RF4{rq|)F#9g3KsPvF2fM=l z=OhOo8ZQDSQsF93K4Q_otFCk7WU+x%&~QtHq!@xwHThFRYYi{taylZCm2SQ=WTs3e z`Kzn+%5nQ5RtJ(eGO!KqjN2HjU2n0QwSUfTY0{grCw5r~RD4*P$9V3RYKV-~{tnRK z4LHZIuX4{|f`7V!r9;01M;So>Y0%<0 zsNe9A`pUECy4uIn-5LY|nTbas8Q~0PZ7Jhq$Qj9w*FEB#ev>~p_QTJ~Ypmn^w61?{fu|FL!u z(}6cs3>mCp6R@A6oD-jKHp@k)=;#celBnnjH#xH4fPI_>A&K1BUI|V1D4cua(>X#Y zwj)_Kk|7tD&Q2T|4~BiR?7aCwGb76*{}=6o^VqS1d*2t3$F zo?$2Xf-Ia`*cM00;EJV>yPQoa5nR?G*VlmtByqty1fQ8;+g$M<{(d4Z+Kbnud@0fK za>om$cX*{jA45FqqDa_$=1BRBX*&qpK}Q)&yshj)5?Mgc6PSjAVTfF4GZb23q?q{*I}XyMzPxZMpTOqF^b6BH{`xoLR1 z)z9*<{cGiKk35ijhX`ywEX=s84qII<4`nZes_-S6 z`UC#65`xy@%%Kc+LPmk#8vRQ(O7elW60!eDt0XVacwM-u+a{BINQ%nIVFFa_2D;f) zp3vs_7-Pmxop_y zqx{isLs9nmG5%o9)q}iREz<~2jJ<-H8s@Oi-Xyk_?)lAmo7b~+A5)2AuLk85`P0Wn zhKNk+>Jv2eWe6?pFSy0rC0`J^^N0TMg6d1qcS51ge!;e>^;Z#|U_20rnE`C_;R>4j zGhGUZvMTvL&4avJq04rXZ>Qr$z?l z5w62;d%Z`I@`4kQi{W?nD8AY#(*9_Fh!9B(xM!<9ulaxjnji{$wEmL9^T_X^fq8O$$*I4QR3!`S_UEC5xFFBxvxqxb**ZztGWtPhDQ1oP z=T2>xFZ=CB1cpbtuKrk)A^yqoAL=S5?Y=8y%e(w*8D61a(U*@Qavt3paeR99>1L$@3|k(g^jXb;tA+AW?Vt@A>^3z6d1m|W^V_Zy<@ME zY_VZ;>>*93C@U47NeMn8`03a_y&o;<25g0bI#X2DdsqD1 z^I%8*XlEOV?Fd+TG6w-er6!A_3VsnM{2EnvL$()gt?@5#@sy`L!89$n|LdsxyYDmS z)-;voP2GUhMgvnAWFelz@Y??H(;Admp2Rl{GsN?3i;Y3BFR>`Lpu^ z>FVx1qXlMei${DaZC`1}22*a+ZNhOtbp?WwH}$7xh~LKn?b4=08up+&wEH-UR+Oi? z#?ps{aDm2tf6itQc?6!3B zknsND%k64)0z4uw(1ez=%XjB*_aY1L8@OEx%ukies@aqyYlQt)QkB{T4SVWb_vTQc zufol*PSXcFvw6kXA>HuZcWQJN+ZTYzQp&z;%rJ(#21QC<3q{ruK1KU^ro(@BL0(6} z`kw`V_}!-|tWU%dU*67tyPO_=^IdCXR(((UxaUNzJa2Q!*3bj(s40@#ZV!2!`&q7O zYmlb;ZC;wEe(FD|Pl~N+c!26I}f8(lw0jA+X-UVtHD8Hs3tqEt7>oAG`7MdneS_S-kI4KW>_x|U?tu)N8h)T2 zY)M(8FOIbs}1cf)o+(1fwkf6;uKDP7sR9xF^XwS)98x2b z^){ zL2n@{@zf@?Mo2jbsj8&I0(~r=h+~6%#nOt|;R~H~yI$KARj)JO@64R6&zb7VEbc7w z67?a_mwG21c!K&^Ax+cw&a?)T`_Cm$AylZL^UT`%T(=PfLB-lR{wr z>q7b2SdkMbYJaVsrPrFw@9*`a|5qq`vZ13zVIBx1FAR+HqyZLYuOKJPCiie?-y|aD z$^PTSPd9lJZHs)nE7f z{lB_Gyw~?P&H=sfv2sfP?@3QLBCY5QHuZ-}Wzk18GZXK9Lgw8#j{cmw4dxb;Hj^)n zm^-yOQxbjiKCUNnw-CluQv$=G<9}J9Qon6Pma^pXWDY}kzoY#5y*5mjo91yBXAfo{ zjjz4(CzgA~xt>A%mJk0UDq?<}3J(YrqQoj22_Q;c5U89}XMMjYZ)rS~h9yBB=`Sx5 z9{0OFs3A`b)G)_G&1W%;MI;Y9FUfc(tHeLclE)o9&V3vjcJEXW_qE<9MjXXExc9Hj zXv;dCiXOSZviL|{$U&#iC_k`jNCUp>!#rXZF{O@u0Bn*#uxtG;vbXksPx2W} zwas^)|L+AjBh9p_JMMfNXHYGy|IFW?U0KOM@@cFZN>jxT{L@!fkr=>;#qj}N8VF!a z=+>+PsMco{e?7)gkD818lb%PhX%pwLc`FdrsCD>WLHKyIO)HdGJK~h1trf3vG`GIv zTu3bDEEY{Y&^eGZ^&h05%mC`zy8Zzqf-08iZHvfRJ?oc@%lV%GU&i}ZPTl?FJ3v~2 zW~=)JL_V3z0P8X0-w%$%>T@ltuDg|+x4{mWeOv4#5M#}V9|YQX2_R;u^aJqUH$4Us zF#yzNp=k7BjP&P40Qs@TegnBM_5dY5Z`|Rfst5B zB=ea!yRrWT`!kq03upoFJ=x`4LVaGH45gX{V61=!xTN9su{w&2soE|RlL@VyOz`T{FuuRV12-u0{#OnXH)Mni*TRX1L92ivIZ)6WqPa$fLTL$g27BPtmAxK3Mq+ zy;JEclMiycU7ra1Tx-6~)~JS|;nL#U(&5AVHXUyu**WHMz^|5RczX-`PQI%bC-FsB z3Ga+xEG28+dxudFkVzS8+cpfOxxe46y4$XLAs}N@d~`hj(_PQ@OQX$^MV?_7TB%9Q zE&|!sV$!y?umG?8=~$JGpi2D&R>z;Vw~Lp^eh1)=0XWLjBdB5$PUe9z$XOpJa@ebx zY*;f(-(`s9_D4s*O;4Frhob$ltbniWM}H1{7Ye0^`uR=ir-nz z2F|Pguk^fu4npy9oqVkF?g6coUSzo((#AE|0tmSry)ZN7)rg`~cv15Aw{Ja6YHV3R z#=sMCNsd=pl$mR1DpIS$oK>RykEf;6vw=5fu@oCeKp@#Wd^dw-@OTHnJbndS!%QEb)D=LrvDeu1GMo)){#aB!} zvNGvE?YqvMrHV;kfe5qgcDv*|J^Vu5k9iFHwBF#HO#d$XA4V6YMu<$8_T?r`jLe;# zHsn1Hvhj=f(beUM(N$4=DR9Dzw4z}dH*E%#{4{pAUui{}Kh+pBIY`ov|J>-Vwtqx^ zz?d60Kqrn^4rKk#4nbDi>3q4G&-y*fO&?3`-9zDPB3<8iO$SQ+^c5Xi;NG9vU1nAo zu4Iv;HMJk`;L4--?*$`)OxhPwB{6G#1|G(4@-I-TeZP=d69-kq?DmO5holG7il`eed#`eNK5|@Bkg$U?i3$t7=cUL?=dly z4;n@6C#vBXJ$r}#b7zO23t3I4yp|GK%?am8=ocD{JVKtoNC2Cx{stu2kLKe-CuFA4 z|0>^3hj6^if@OS4$>nej`)aG@v*2-yU^ywIrA({#$Iws|u&U0P$Tt>{I8-VG=?Jn{ zVjV`k865x02i$!`xIvuE54!{diobMnIxv_Mp$Gt>YciEYP~|ZMWxa_)JW`RLx~?>_ zp0aS$8qLN#napVf7CuesdEwVB;#h+IK5x!gS^Sp?;e>?B; zujLAW_xkL~q$5y4Acw}Zo|xn+Cub_jvJh;jwc?P)F9WUJHraon`y3kh;Xa=*Y#Lj1 z&5g+zXa>-{_ug1py99{Et1TwOy6e&h0@bQLGGHqm61el_|Evedu(Phg=35j%6l!6) zjyi8E1tpFJkn=%Q(69J(w1eq2pf$yal>fMq7A`fGR@S z%YSlvHX?ddP+g6bkEhRnualCJQWhzNTIue6@ej4uG!0!5MTdu1TP$cE=hgC7-nlCuv1N!&PhXKlX7S#tsvLhdjC zt@~&tB13T-x^^i;(H6{YsZ|=Werx)S7GS#B8$oPpf&obQfrXjb_nVV92tv1RRUe{- zp=RJ0p@64U=*%E?DF9=B=ZVV*TU2KG{O$gz+0kv~Nf2?yym0X*y1FfLy+IvD6X5nN z9YywUXCHxYlAc$F=);q14=Y?QRnV5Ftjy9*H`;uR7ZA`oc@%OlzFjBsS*su)A*cGu zN+M$mS`!^q6{845g;txjV$Bjy*0^pE=wJw}dNd?x!vZ!_d zKs;8Di2&2mcI-Gf*BuFAe zOZC9nc-3|~dO%U})aNV1YO&_0p8QV(i7Zoj>ViK6>;wZE z?J(5%9DV4T+~4F}mUe&0R%tf+V!q30>)dcT!hGBXT$t`sE%dJgLv9W@5h8vEJcMD< z>(A(cKQ`Y4n!0ou1ij=6D8iTL=ZosJe=GbQds3EDqpCvD-ePLc%~|j1`ZX}}+KLcC zE|@|2blt<*3>X+OhMp{OiJQp^pW#&*vfwF1pPR>gIP|>WFdOj%SV|Hz2u9@FKX5!1 z8a@jFDiSoCzq`6hTYxIGuFrk-=BOCX4Il;as(eu|0TctjjnY(xCKDBBrvb6(ktQIr zG8U@xsp9YR(w39z7>|w&z8tqmc5ageNu$_9l(9DnW_aA zz5n!S@SuLVL&tfLCGE7Qg41dS6=+|nUveOi7IMfuT)jLQQy4z)-DgwN&=|&sxN#sM zV^j&#{L_=4s$u6t&m8_J*oK#T3BW@m)dl0J@G&lICgHk#oWL^1h{{za=vVyl4#QV{ zSjF?08E@r_ejZ)i?%Gw!w6YXk9&uDvn;BI^`|>@-hL~o-OybUP^`MVbl2J3 zZ$=KY`J%bC&XIsj#OJtyT^|DETu`V3cB+4C1fdt2eo8Ly^xj{hN%2r-1QnMXAd6)y zudw+vW_(Z}INPjl>zS!s=ReIGu3DB6>0fkx45Xv*38}R#Syzw?!ES!jCBtE|7j@8j z{d7>hEFM9^U9k3)uRv%&nb5x0qdBZaVz}ILyH%oNf6I2$%iBK80n31&nJ6@z4{850 zlBNR=E_L{1mHPI`U9|IDh~j^$`Uni3Tg6UqJk+X3sA3T}s= zVSJEoXk*FO9*FNj5^L?!npwpw6NgsDz+jL^)_J`ND|Nob4!eWX+9pQH(tJ+n+w;Ex z*YBUI*P+*Tcg>93Yg|dXtocvSLop4{Y}o}~X2#d=7L*o(fXg(% z+_e}7uEb5q>QsmE^UtSZ)`AQG@^c?;s2^gHxL#JZLsA%x|Kk#r+q_cxVxpzGb^A^> zr%_c{^^9K}!t_*lcNxwQ(=|p{MJ)L8Hz+Kh{8N&I)d>zD|JZL&=p%(_q-JKvs~_r7 zd=aeaK8cgg_3y6|F6luGx+1QK&61OwNgGWPiQq`m|DJ4}#!h=96ADX@A+8-H01}mU zCx=QWE3C~BfL?aa&I@no15zs;j2wvy-;6#*V{^*N-UG6vJ53Qz9LM5Rk57D!(aFmC zkgVz`h-q@}apcPtj-+HyOAxX)(@=dho&971I_W&$nW<(~+IV1j66Tb|>nhHoIt7hh zKO3AsPEDo#SzSFiI4D1Pyl+c8>&vdSDgY2FtId$@BApr4Vf#EoFmYQ&(ZOU*e%jKYrio9i?iDXf15ZH7w%2Oi4V9pe>_V1;5N7F| z^{Q1pDl3i>>hEWD#oA;=j~fL7xYX>2t26Y<)Ew~_EpF<%n%M>F`XzGI=O&=0(-ro8 z0_^Y`qjC|DZuWjuNy?9-c&$5X9^ws~A^1?Yw$-87R##(VSPKYQLELcmr&DH^K(xDp z(3Lh<0tWP5*U#X{iL}ug`H=weS6m^{`g`UGYhYYux5o~D=l z9W^S+1b_#HUR?k1v6f+Gu)}};+zR983PU)!cNgAnWTg%+Ri*FF^DEg7LgJ#}%0JOh zDu*nw<)%oSqmydP-b-QUnMokJh^apj4E5dWXZYIp3BK$|w}@jdbr zOuJ+Cv=w*lUUUQQxaVX^|H=1kNZba)O|Yuax;nq6!2YWNCqFFF644K?*Bp17?aqb< zS4;l9oh-h!F~1LT*P`RE429>}6VQ3E;f*_vDqodmU}I|P!?bDP4=`2N9sSMhjWno8 zgXsP7`H^pjmnk3ha(&EIKuA#A%uHR(gdj{(Wd7D|B*xW*9O0Q+F+Z2R?3*=gH3`-L9QFR1FBY*1X;{Kkj|ste_kcM=(z<##`o`~VN5ct@*Codj4ncbs3(8S)7!u6+wcjEmfi$jI9H<y{t7>tYw~}!j6K`u2U0i`lvGJRzk&Sm5w^K} zF!?H{F;Ef^7Tv~B&M;zn(@2nMD}so=D?eQ-)DkLs7jfG=HHy!}B7PGHpD$`$q#o&US{@jcqSuGj z2S%G_4t|6a&&w9q+Tqo2GznwreXhrNCw3&3E!f)Ha?a3Sx#T_LN*H`LneLE-2@^^- z;E`y1O+efIb5(qJY(O!gx0h8;H(4r>Jk|}*XUQzuh*Fl%p}sqM5=41}nC4E3Hn67U zjqy_V{gLLSu?4l4&8K&)TYqm*2-@HB->jv_gVFgvUQRu;6cZPJQ5L}NzSPiFSlau& zq9R6yL0u?;If0P+CM+u8!}Fke%u?row1>f&FM#JI-KWEnj zc^EkUGOB|{R_P8JR|Ob-tt)a=M&)U-o8*tbN^vEV%BfKk$f5{Js) zwqX;(L?QUI7Z-1PXi}(-yT_Zd;%7r3rI3c_tbSZHi=?poPAn5-_e*8-UpkNb(;)Rv zME#s!88-z51pA_p-OG>6reIRC9#~z0>7JpwGt-D_PrLBQ#r6%olcogjHstBfvW}+j z%fpX`)Ex3w^Yo{AaBK57g(-jwL z*Hy;p9(K<|{*LJAtq=4h!pIVl_d0{K;#3aR@JOGN`_EgRuU2w86VL2QX$PdvFY^fA z+&VBMGt1n;RFrr5EJ|jV#h=l6Hq$-HfjL`^FDDL)9e>rz(z2tXqPDZA`qga0VNgUr;;+=!jPv^0Ac@K%Cw!J|H$t?jPp}~QHP9R40+*JsClj=1Q z1qdh5CC^87Y;MzmN^X*dluL0vEz@zxtm|X@gMC`OR;9S_Hbq7Mz@y&esCj6D+V=OP z(Zcj?!G+=uoRo}C;mkp`A0gGgaj}1JRum_{Wb57OQIu$=0sP1BYq7=Fbvz-aX|jVx zBC`22^lPzwG*x98Q{Tf04|j_Zd=8@(f+ok9gA`$Q(wpEzKt-3V&aJrNEUIJil9=P` z7b=Ux0ZkmJpf7 z8m?j=Bl>%3WD;c@RT{W;yQn7t<0xd3Hd2PpQ1N*7lp~Ji`iTXjS2k(5Oqwc-x>S0m z)2Y~KkwM>tAQz`uS*-VR-%H#{t)Qf!Pi^>CpAFEc$6nO~pE>V)a9-%;(GC#|&hq>1 z>jjbXvt)pggxI9{wobfw^WaC-2JS~}k|y^YP0YR;Ft+f~t*u7tY3;IA(o6eZu&iYi%{^V76^iD2)~&`AmGa&?g; z%5H@KZ^v&0Edj#aRR=?vOsX7?fk9}Ys0jF+$jj~;k`jF}MFvAMx*s*rP`7!ldyx?8 z#lENoJoNyBpaFf(qOld%j5X%Qz&6yuSz1%cj~mqQK6t1jklO{+H;VQoZZxC!{e2ys zStYtvMCT{#iaaVC{ygNyu%)*Yc-Rd*C61T0rHy}X8!B`u(PvbUq%T5Ouj$+Ej2`P& zj<~3>XhPm(TQ5#t5OS!^=dRbl$xX|LK7~vcraXOg9cFv$sf4)Ri+ari{@)cZpTTM2 zDG-6kJYhY7h#Jaj_DT7SP?vx@4|+|KO=*YNOV>+Ld3@d(3+aqgro>U&)Uw2fbB7+p zT&z?^hf_~8+O(20ble!RVO?Fz?o%$Og2smxNIvsl(q7{b_494_?3@?8s^7XT%`)6> z*!qJ)@e7~b5qz0 zSVTE;xPO};&4C#X4u4;VmRAf@6RO4r^Atw<`}r##x$o-v;&PJJxaub=_tcc9_P=ues>LCwm@fR@cepy>l;d;ZO@WpeVfPoAjJ$G+Vj zP;s2jr0hM5!jZAD-|fvck$`(fZjm1X!QT>u1~?HFe!t`{s3OFlDQ`*&#sytS6~%KJ zbV_<7BeWu&19q`AKC|Bdar>})dI)p+r;P{j;31_jZ`T`k>EacbevdMymMqjSpS~keI}yOEu$Jsm4|b!gHSXW( zl*y(`7!gkO7F7X(73Y%(BNm!=+ogZeikVu&qvsfAxXd~&qP zOyl~O70<_}-^h4pGJR-p@RhF>bI_~;q5yk1tqja-|9Ub3XuOOv6ORH1NCgB0c93UV z`T-lL<%PPEjc+&5-#k{nDY%b}RFkQwoa>DN7;!V}=RF)fjD<`UNqaKqVV36|X-Wcn zW$pS9A<()Q=}YP<6%sgeT=fKd=g zjH~~(3d==Id2i8zlm=xIS_}n9U`Yas ze@Z8qg(h+U?^ikG1JJUOJI4+@AQP*dZx}WCmDd{#drIXkAL;b}gVBij;PehB&@Gst2QUBtm$r4K&iaP)4X@teYwvXm|Q?HUMgg1RO}wtQ}%F~ z*O4WUuI83{>l3?%MRDEyReD{XuX!cK7q7U`nenj3jE1RxSS!CwbS%OyLwl` zPj(3Z!Lachh6l+=mo3R^G`&sW*U%?^FVIa(OP6R|eleWihe5B6g?c7N1$zT?$f?a)<2JhCF>tZ}KdD-Wl>? zCjy!Xgk8(DV7_YEcdi_0CMt7vulC?=o+c7-)S{Qqv}+36jK)ooSOO-Fc#me2y75!x z)lqDcamSw5=Gb11eckj*kaz=I^OJ@W{3!avD^El2Tz>P5_54hZ1{-PX;i684rvO1Xgk}kj~a>dzL{!Kxoc4TNV`{1@B!xYd=r( z6nS5zYkAkmO2SB4V?9Z8x`NsOL|!o5{)cDG`S8YCv9_EzBlU*C67}f`|z8!$RK5(6}^lOJV-1 z#lrg4;sw=fjjti-n-~yMY?W*ef&F6ZH`jK+~KTYS>DhHIf9Wzf`#dFqHcbwg451?qB;URKSHZ3 zudmHR?ido>M|G5D@?FbTHJHtOoTo{5h5ahcEp%91TxsAUisl+O4UNLH1U0@y%?~Xf zHs^o6C_1S`B~@zDiL7vE8yD z$AzoOl9NNA0rfc@Y~ee zzm+_50{`O;OfQ?Mid)46KxN+by7GD?DD-CSa2o?Rk-7GUy??Gf_ba3tmhJnWZZqwOVE;mCOGFZY&`X<{W( zI%MpaZ8b?#9(Vme0!gpcVS5?hHl)})o?LS>vRz5@=tU(9BUAEL*2OWOe{<2PWJJzVK*Y{PTPA{wRLV1QS*7Cyj`IFa$hkx^>(`M_1kUi!HCLk^u0S{2M3F?;;4} zzm-jBsLFgY|KFDw?4_r8GMk!O4m5-~2F>N0J`o8QFh%^67r^^E)Rwn8D0aeAHoiIP z0O}raXz@o_$2&~ne|jHFu7x`IU#E_<Qxz+y7UlwOlkz1J4z~E;0O_Qza+(JI(Y%P0}am}v$ zOL*Xtz8eq8Z*0{5BEah+Fy|OqXv=50a_E{g70BVVk)HSW&v_%fjCbuH64+AJRK6S^ zAF!r8cfHSLP4h%*1GZF}j_{e?v zY2NZsP(VP}&F`X}hBR!XkV&b7Vq>CG^53Y8F~z@ju!?rYgP9;=78 z=)i7mjWz}PGby27)o1>1L+9XSta7%!fP_R=b$u06s+!V$9&5#+`uaL%C9WR*g6wR; z_oNpy4apL9Us@gYL@`6o7T|4fiC9uJ?cS5}&x{>LVor9eOA>xN3toK52R9I=Si7z2 zC@E}j>0c4GR?4_|MLx^rey8K*QDJ7Kbi=?#>;A;ga#R-`8xO3lQSF8Adnk_Y3OHS~Eg&?s%rg!&Z9mEdt)( z=RbZTkx^_k^NDHtbOigSJOHAVf6>~d#m>nI(U;!%?91NCR=S=#92&;YVP?_(v8;?w z5?4XLHt;P-X-I!_B14BGPDkwDqJ&jLTBVVZkwSm0(ebpqI&tme-7H8`G&)$`>Q5s_ZWF48$}CnW`jkBg$GXfm$1-tFtXlS;NUc zet)3+@@Ke0*3;9|-g)cKnjqqliFP5ac-=xD>r1D|GxLBYuq=8GV};PSYI7^PXbz8JMThzq;7BM>T!oK z{eUE9rWBY#XeIp=pB%SfJZ68*WNC{#Xy*Wsh1xYggGUDpo| zPVa!hV$_H(CR1Q;P_>3ac`m1#A!Lcpz6j|d@&7}BkC4O%KEpJ&ASaN?W0Dg51tj zPgX@6_lD911w1(CZo#KxIjw8xe6}@Byr-YrJ34-eJ@qUJ4m`T*G;>mSjKuG&(;>XkMi;)x#!qB*mPEHXdq{1rv2v83LdkefmGDg&)F7sEtSdn-;id63DX*#TWBj%c6mOcKt}Rp!oRl=Mky} zpM?r_rWpTux(`yK64%kTXSTgAQ;MZ461tr4D^q$>mKl+)e5_LXb9+0~kvz;L zgB-lezjuT?I?Hz~pL;m7?i&Ao%Rv`hzQjY9d$Ow$)cc;DNAIV48I8(Dp_M^)3EVBw zm~MFa^f6_2sW7{Le?PbNw_R;9^C_(2IAyG0)mWu_(C*iiv39Jgm!D0=%xq!}CV7ZY zr5JsZy4?DWRj4BnE?!T=f?-NSiK=qE?Rsxym9BD)$7PDtZa0E#mG_2`YsuDqNVbLr zRLK{XzK1F*GF@y>Zn#lVb~%u%ryHkvF!(n)6E?K^lZq<&&)A=$r5tNPc+(4TxcnJB z*m>)f6Y_w^)6;1%*dLdP?%g{%V=c@{yc?^QTe&nrUFo&4VOUk9E4cb|Xw@)#*lxUP z!-V1-RCuc79TcKaMwn}C*GGo{M^#Hp%L#0=>8a%~a(}5q-*auRD{HvuT>+={m253` zK(_Pq^9{hL%81c|oE-U855sr}dyH#CJx_nC8I$WHhxp67gHxeS*>0!Ls~)1r^ou}6 zi1z0@g>K(2mqNO#I4La~jKUjgxr{oZxVoxN7hXNdT=#}mAepNnMtW=!4fg40CLZEC zS0YKn8d4V9Zyen2Ku=5`U3SMh+$xd$*;ZRSMXNI-72f|E+v^6qCf~m9UgKte(&&|W z6+x*_?2foU&FHN9)8B`lYy7h@W1#G=54q}VzSq+?eqvFT?kYdr8m9wY-CeQImiSwI z^vp{fakwvB!PBqra}3Zmm`)uGq*4!}^>BWzgKhqF2(kdDL-wmB)Q<$%_Lw}1!*&Qa zb$$9>jP^Xqj18@LEKMCYXRb`nTlka`E$DAERy;d~k$W#p6n8!>{zVtwc34^=bDHT6 z!6s|e2S-Jest2dguF{*RxEOn=J?oI$y=A=p*?0kGy5hq}I(o_6Vt&)YD;`uCShkZb5O#L_e*36&U>3E8P{?rwUy2G6&gKWVNuh= zb!3z`D$_D$%H(ft*-TxOw|A{&IZ6)|a8m(eG_p|w`aE^erbtaX#4S_<6s4wMFlv{7mCWjvjlN|`eCfzfQ; zySYzjtgtzSJG!|UcXIlL9`2xjX!z)Nl{OVU_3+9HWfU}Ru(Q)HRfMl-ZjJ%`#V#6D zmw#*lD6O{1&y0JV6TVk#6u)VHwonba)r@C%bZK|#BU$2`XZ7Z2IF|_Q{yRr!d+~-y zje=K~1P&q3RPXYAGMkx8d!xl&{gVUeNFR%?9X?^i@<1jKls zHN+%X;t0kw8XO)Tw+L&!gBw|zo+VPSOfYuetoEm`d&p0vpq523>+SJG4hwHo*lPRP zoYU||`e!uSTg=!6FJ#F6iyGiUtK3`l?l^6LSt(cRJ;*O`EF?eleA(d2;~?d~_#v(P zE%Aa^;uYKkrBKc<4P7?tiyt8cGJF?844zKh8NM2q=@s0g4a@UKO3&2fN@UF6{2vfk Bpw|EZ literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4981b05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Dockerfile multi-stage pour IPWatch +# Backend FastAPI + Frontend Vue 3 + +# Stage 1: Build frontend Vue +FROM node:20-alpine AS frontend-build + +WORKDIR /frontend + +# Copier package.json et installer dépendances +COPY frontend/package*.json ./ +RUN npm install + +# Copier le code source et builder +COPY frontend/ ./ +RUN npm run build + + +# Stage 2: Image finale avec backend + frontend statique +FROM python:3.11-slim + +# Variables d'environnement +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +# Installer les outils réseau nécessaires +RUN apt-get update && apt-get install -y \ + iputils-ping \ + net-tools \ + tcpdump \ + && rm -rf /var/lib/apt/lists/* + +# Créer le répertoire de travail +WORKDIR /app + +# Copier et installer les dépendances Python +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copier le code backend +COPY backend/ ./backend/ + +# Copier le frontend buildé depuis le stage 1 +COPY --from=frontend-build /frontend/dist ./frontend/dist + +# Créer les dossiers pour volumes +RUN mkdir -p /app/data + +# Copier config.yaml par défaut (sera écrasé par le volume) +COPY config.yaml /app/config.yaml + +# Exposer le port +EXPOSE 8080 + +# Commande de démarrage +CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..737907e --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +# Makefile pour IPWatch + +.PHONY: help build up down logs restart clean test install-backend install-frontend dev + +help: ## Afficher l'aide + @echo "IPWatch - Commandes disponibles:" + @echo "" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +# Docker +build: ## Construire l'image Docker + docker-compose build + +up: ## Démarrer les conteneurs + docker-compose up -d + @echo "IPWatch démarré sur http://localhost:8000" + +down: ## Arrêter les conteneurs + docker-compose down + +logs: ## Afficher les logs + docker-compose logs -f + +restart: ## Redémarrer les conteneurs + docker-compose restart + +clean: ## Nettoyer conteneurs, images et volumes + docker-compose down -v + rm -rf data/*.sqlite logs/* + +# Développement +install-backend: ## Installer dépendances backend + cd backend && pip install -r requirements.txt + +install-frontend: ## Installer dépendances frontend + cd frontend && npm install + +dev-backend: ## Lancer le backend en dev + cd backend && python -m backend.app.main + +dev-frontend: ## Lancer le frontend en dev + cd frontend && npm run dev + +dev: ## Lancer backend + frontend en dev (tmux requis) + @echo "Lancement backend et frontend..." + @tmux new-session -d -s ipwatch 'cd backend && python -m backend.app.main' + @tmux split-window -h 'cd frontend && npm run dev' + @tmux attach-session -t ipwatch + +# Tests +test: ## Exécuter les tests backend + cd backend && pytest -v + +test-coverage: ## Tests avec couverture + cd backend && pytest --cov=app --cov-report=html + +# Utilitaires +init: ## Initialiser le projet (install + build) + make install-backend + make install-frontend + make build + +setup-config: ## Créer config.yaml depuis template (si absent) + @if [ ! -f config.yaml ]; then \ + echo "Création de config.yaml..."; \ + cp config.yaml.example config.yaml 2>/dev/null || echo "config.yaml déjà présent"; \ + else \ + echo "config.yaml existe déjà"; \ + fi + +db-backup: ## Sauvegarder la base de données + @mkdir -p backups + @cp data/db.sqlite backups/db_$$(date +%Y%m%d_%H%M%S).sqlite + @echo "Sauvegarde créée dans backups/" + +db-reset: ## Réinitialiser la base de données + @echo "⚠️ Suppression de la base de données..." + rm -f data/db.sqlite + @echo "Base de données supprimée. Elle sera recréée au prochain démarrage." diff --git a/README.md b/README.md index 3ca65a7..620c5c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,270 @@ -# LANMap +# IPWatch - Scanner Réseau Temps Réel +IPWatch est une application web de scan réseau qui visualise en temps réel l'état des adresses IP, leurs ports ouverts, et l'historique des détections sur votre réseau local. + +## Fonctionnalités + +- 🔍 **Scan réseau automatique** : Ping, ARP lookup, et scan de ports périodiques +- 📊 **Visualisation temps réel** : Interface web avec mise à jour WebSocket +- 🎨 **Thème Monokai** : Interface sombre avec codes couleurs intuitifs +- 📝 **Gestion des IP** : Nommage, classification (connue/inconnue), métadonnées +- 📈 **Historique 24h** : Suivi de l'évolution de l'état du réseau +- 🔔 **Détection automatique** : Notification des nouvelles IP sur le réseau +- 🐳 **Déploiement Docker** : Configuration simple avec docker-compose + +## Technologies + +### Backend +- **FastAPI** - API REST et WebSocket +- **SQLAlchemy** - ORM pour SQLite +- **APScheduler** - Tâches planifiées +- **Scapy** - Scan ARP et réseau + +### Frontend +- **Vue 3** - Framework UI avec Composition API +- **Pinia** - State management +- **Tailwind CSS** - Styles avec palette Monokai +- **Vite** - Build tool + +### Infrastructure +- **Docker** - Conteneurisation +- **SQLite** - Base de données +- **WebSocket** - Communication temps réel + +## Installation + +### Avec Docker (recommandé) + +1. **Cloner le repository** +```bash +git clone +cd ipwatch +``` + +2. **Configurer le réseau** + +Éditer `config.yaml` et ajuster le CIDR de votre réseau : +```yaml +network: + cidr: "192.168.1.0/24" # Adapter à votre réseau +``` + +3. **Lancer avec docker-compose** +```bash +docker-compose up -d +``` + +4. **Accéder à l'interface** + +Ouvrir votre navigateur : `http://localhost:8080` + +### Installation manuelle (développement) + +#### Backend + +```bash +cd backend +pip install -r requirements.txt +python -m backend.app.main +``` + +#### Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +L'API sera accessible sur `http://localhost:8080` +Le frontend sur `http://localhost:3000` + +## Configuration + +Le fichier `config.yaml` permet de configurer : + +- **Réseau** : CIDR, gateway, DNS +- **IPs connues** : Liste des appareils avec noms et emplacements +- **Scan** : Intervalles ping/ports, parallélisation +- **Ports** : Ports à scanner +- **Historique** : Durée de rétention +- **Interface** : Transparence, couleurs +- **Base de données** : Chemin SQLite + +Exemple : +```yaml +network: + cidr: "192.168.1.0/24" + +scan: + ping_interval: 60 # Scan ping toutes les 60s + port_scan_interval: 300 # Scan ports toutes les 5min + parallel_pings: 50 # 50 pings simultanés max + +ports: + ranges: + - "22" # SSH + - "80" # HTTP + - "443" # HTTPS + - "3389" # RDP + +ip_classes: + "192.168.1.1": + name: "Box Internet" + location: "Entrée" + host: "Routeur" +``` + +## Interface utilisateur + +L'interface est organisée en 3 colonnes : + +### Colonne gauche - Détails IP +- Informations détaillées de l'IP sélectionnée +- Formulaire d'édition (nom, localisation, type d'hôte) +- Informations réseau (MAC, vendor, hostname, ports ouverts) + +### Colonne centrale - Grille d'IP +- Vue d'ensemble de toutes les IP du réseau +- Codes couleurs selon l'état : + - 🟢 **Vert** : En ligne + connue + - 🔵 **Cyan** : En ligne + inconnue + - 🔴 **Rose** : Hors ligne + connue (bordure pointillée) + - 🟣 **Violet** : Hors ligne + inconnue (bordure pointillée) + - ⚪ **Gris** : IP libre +- Filtres : En ligne, Hors ligne, Connues, Inconnues, Libres +- Légende interactive + +### Colonne droite - Nouvelles détections +- Liste des IP récemment découvertes +- Tri par ordre chronologique +- Indicateur temps relatif + +## API REST + +### Endpoints IPs + +- `GET /api/ips/` - Liste toutes les IPs (avec filtres optionnels) +- `GET /api/ips/{ip}` - Détails d'une IP +- `PUT /api/ips/{ip}` - Mettre à jour une IP +- `DELETE /api/ips/{ip}` - Supprimer une IP +- `GET /api/ips/{ip}/history` - Historique d'une IP +- `GET /api/ips/stats/summary` - Statistiques globales + +### Endpoints Scan + +- `POST /api/scan/start` - Lancer un scan immédiat +- `POST /api/scan/cleanup-history` - Nettoyer l'historique ancien + +### WebSocket + +- `WS /ws` - Connexion WebSocket pour notifications temps réel + +Messages WebSocket : +- `scan_start` - Début de scan +- `scan_complete` - Fin de scan avec statistiques +- `ip_update` - Changement d'état d'une IP +- `new_ip` - Nouvelle IP détectée + +## Tests + +Exécuter les tests backend : + +```bash +cd backend +pytest +``` + +Tests disponibles : +- `test_network.py` - Tests modules réseau (ping, ARP, port scan) +- `test_models.py` - Tests modèles SQLAlchemy +- `test_api.py` - Tests endpoints API +- `test_scheduler.py` - Tests scheduler APScheduler + +## Architecture + +``` +ipwatch/ +├── backend/ +│ ├── app/ +│ │ ├── core/ # Configuration, database +│ │ ├── models/ # Modèles SQLAlchemy +│ │ ├── routers/ # Endpoints API +│ │ ├── services/ # Services réseau, scheduler, WebSocket +│ │ └── main.py # Application FastAPI +│ └── requirements.txt +├── frontend/ +│ ├── src/ +│ │ ├── assets/ # CSS Monokai +│ │ ├── components/ # Composants Vue +│ │ ├── stores/ # Pinia stores +│ │ └── main.js +│ └── package.json +├── tests/ # Tests backend +├── config.yaml # Configuration +├── docker-compose.yml +└── Dockerfile + +``` + +## Workflow de scan + +Le scan réseau suit ce workflow (10 étapes) : + +1. Charger configuration YAML +2. Générer liste IP du CIDR +3. Ping (parallélisé) +4. ARP + MAC vendor lookup +5. Port scan selon intervalle +6. Classification état (online/offline) +7. Mise à jour SQLite +8. Détection nouvelles IP +9. Push WebSocket vers clients +10. Mise à jour UI temps réel + +## Sécurité + +⚠️ **Attention** : IPWatch nécessite des privilèges réseau élevés (ping, ARP). + +Le conteneur Docker utilise : +- `network_mode: host` - Accès au réseau local +- `privileged: true` - Privilèges pour scan réseau +- `cap_add: NET_ADMIN, NET_RAW` - Capacités réseau + +**N'exposez pas cette application sur internet** - Usage réseau local uniquement. + +## Volumes Docker + +Trois volumes sont montés : +- `./config.yaml` - Configuration (lecture seule) +- `./data/` - Base de données SQLite +- `./logs/` - Logs applicatifs + +## Dépannage + +### Le scan ne détecte aucune IP + +1. Vérifier le CIDR dans `config.yaml` +2. Vérifier que Docker a accès au réseau (`network_mode: host`) +3. Vérifier les logs : `docker logs ipwatch` + +### WebSocket déconnecté + +- Vérifier que le port 8080 est accessible +- Vérifier les logs du navigateur (F12 → Console) +- Le WebSocket se reconnecte automatiquement après 5s + +### Erreur de permissions réseau + +Le conteneur nécessite `privileged: true` pour : +- Envoi de paquets ICMP (ping) +- Scan ARP +- Capture de paquets réseau + +## Licence + +MIT + +## Auteur + +Développé avec Claude Code selon les spécifications IPWatch. diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..85486e5 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,260 @@ +# Structure du Projet IPWatch + +## Vue d'ensemble + +``` +ipwatch/ +├── backend/ # Backend FastAPI +│ ├── app/ +│ │ ├── core/ # Configuration et database +│ │ │ ├── config.py # Gestionnaire config YAML +│ │ │ └── database.py # Setup SQLAlchemy +│ │ ├── models/ # Modèles SQLAlchemy +│ │ │ └── ip.py # Tables IP et IPHistory +│ │ ├── routers/ # Endpoints API REST +│ │ │ ├── ips.py # CRUD IPs + historique +│ │ │ ├── scan.py # Contrôle scans +│ │ │ └── websocket.py # Endpoint WebSocket +│ │ ├── services/ # Services métier +│ │ │ ├── network.py # Scanner réseau (ping, ARP, ports) +│ │ │ ├── scheduler.py # APScheduler pour tâches périodiques +│ │ │ └── websocket.py # Gestionnaire WebSocket +│ │ └── main.py # Application FastAPI principale +│ └── requirements.txt # Dépendances Python +│ +├── frontend/ # Frontend Vue 3 +│ ├── src/ +│ │ ├── assets/ +│ │ │ └── main.css # Styles Monokai + animations +│ │ ├── components/ +│ │ │ ├── AppHeader.vue # Header avec stats et contrôles +│ │ │ ├── IPCell.vue # Cellule IP dans la grille +│ │ │ ├── IPDetails.vue # Détails IP (colonne gauche) +│ │ │ ├── IPGrid.vue # Grille d'IP (colonne centrale) +│ │ │ └── NewDetections.vue # Nouvelles IP (colonne droite) +│ │ ├── stores/ +│ │ │ └── ipStore.js # Store Pinia + WebSocket client +│ │ ├── App.vue # Layout 3 colonnes +│ │ └── main.js # Point d'entrée +│ ├── package.json # Dépendances Node +│ ├── vite.config.js # Configuration Vite +│ ├── tailwind.config.js # Configuration Tailwind (Monokai) +│ └── index.html # HTML principal +│ +├── tests/ # Tests backend +│ ├── test_network.py # Tests modules réseau +│ ├── test_models.py # Tests modèles SQLAlchemy +│ ├── test_api.py # Tests endpoints API +│ └── test_scheduler.py # Tests APScheduler +│ +├── config.yaml # Configuration principale +├── docker-compose.yml # Orchestration Docker +├── Dockerfile # Image multi-stage +├── Makefile # Commandes utiles +├── start.sh # Script démarrage rapide +├── pytest.ini # Configuration pytest +├── .gitignore # Exclusions Git +├── .dockerignore # Exclusions Docker +├── README.md # Documentation +├── CLAUDE.md # Guide pour Claude Code +└── STRUCTURE.md # Ce fichier +``` + +## Flux de données + +### 1. Scan réseau (backend) + +``` +APScheduler (scheduler.py) + ↓ déclenche périodiquement +NetworkScanner (network.py) + ↓ effectue scan complet + ├─→ Ping parallélisé + ├─→ ARP lookup + MAC vendor + └─→ Port scan + ↓ résultats +SQLAlchemy (models/ip.py) + ↓ enregistre dans +SQLite (data/db.sqlite) + ↓ notifie via +WebSocket Manager (services/websocket.py) + ↓ broadcast vers +Clients WebSocket (frontend) +``` + +### 2. Interface utilisateur (frontend) + +``` +App.vue (layout 3 colonnes) + ├─→ IPDetails.vue (gauche) + ├─→ IPGrid.vue (centre) + │ └─→ IPCell.vue (x254) + └─→ NewDetections.vue (droite) + ↓ tous utilisent +Pinia Store (ipStore.js) + ↓ communique avec + ├─→ API REST (/api/ips/*) + └─→ WebSocket (/ws) +``` + +### 3. Workflow complet d'un scan + +``` +1. Scheduler déclenche scan +2. NetworkScanner génère liste IP (CIDR) +3. Ping parallélisé (50 simultanés) +4. ARP lookup pour MAC/vendor +5. Port scan (ports configurés) +6. Classification état (online/offline) +7. Mise à jour base de données +8. Détection nouvelles IP +9. Push WebSocket vers clients +10. Mise à jour UI temps réel +``` + +## Composants clés + +### Backend + +| Fichier | Responsabilité | Lignes | +|---------|---------------|--------| +| `services/network.py` | Scan réseau (ping, ARP, ports) | ~300 | +| `services/scheduler.py` | Tâches planifiées | ~100 | +| `services/websocket.py` | Gestionnaire WebSocket | ~150 | +| `routers/ips.py` | API CRUD IPs | ~200 | +| `routers/scan.py` | API contrôle scan | ~150 | +| `models/ip.py` | Modèles SQLAlchemy | ~100 | +| `core/config.py` | Gestion config YAML | ~150 | +| `main.py` | Application FastAPI | ~150 | + +### Frontend + +| Fichier | Responsabilité | Lignes | +|---------|---------------|--------| +| `stores/ipStore.js` | State management + WebSocket | ~250 | +| `components/IPGrid.vue` | Grille IP + filtres | ~100 | +| `components/IPDetails.vue` | Détails + édition IP | ~200 | +| `components/IPCell.vue` | Cellule IP individuelle | ~80 | +| `components/NewDetections.vue` | Liste nouvelles IP | ~120 | +| `assets/main.css` | Styles Monokai | ~150 | + +## Points d'entrée + +### Développement + +**Backend** : +```bash +cd backend +python -m backend.app.main +# ou +make dev-backend +``` + +**Frontend** : +```bash +cd frontend +npm run dev +# ou +make dev-frontend +``` + +### Production (Docker) + +```bash +docker-compose up -d +# ou +./start.sh +# ou +make up +``` + +## Configuration requise + +### Backend +- Python 3.11+ +- Privilèges réseau (ping, ARP) +- Accès au réseau local + +### Frontend +- Node.js 20+ +- npm + +### Docker +- Docker 20+ +- docker-compose 2+ + +## Ports utilisés + +- **8080** : API backend + frontend buildé (production) +- **3000** : Frontend dev (développement) + +## Volumes Docker + +- `./config.yaml` → `/app/config.yaml` (ro) +- `./data/` → `/app/data/` +- `./logs/` → `/app/logs/` + +## Base de données + +**SQLite** : `data/db.sqlite` + +Tables : +- `ip` : Table principale des IP (14 colonnes) +- `ip_history` : Historique des états (5 colonnes) + +Index : +- `ip.last_status` +- `ip.known` +- `ip_history.timestamp` +- `ip_history.ip` + +## Tests + +Lancer les tests : +```bash +pytest +# ou +make test +``` + +Couverture : +```bash +pytest --cov=backend.app --cov-report=html +# ou +make test-coverage +``` + +## Commandes utiles + +Voir toutes les commandes : +```bash +make help +``` + +Principales commandes : +- `make build` - Construire l'image +- `make up` - Démarrer +- `make down` - Arrêter +- `make logs` - Voir les logs +- `make test` - Tests +- `make clean` - Nettoyer +- `make db-backup` - Sauvegarder DB +- `make db-reset` - Réinitialiser DB + +## Dépendances principales + +### Backend (Python) +- fastapi 0.109.0 +- uvicorn 0.27.0 +- sqlalchemy 2.0.25 +- pydantic 2.5.3 +- apscheduler 3.10.4 +- scapy 2.5.0 +- pytest 7.4.4 + +### Frontend (JavaScript) +- vue 3.4.15 +- pinia 2.1.7 +- axios 1.6.5 +- vite 5.0.11 +- tailwindcss 3.4.1 diff --git a/architecture-technique.md b/architecture-technique.md new file mode 100644 index 0000000..6e39658 --- /dev/null +++ b/architecture-technique.md @@ -0,0 +1,17 @@ +# architecture-technique.md + +## Backend +- FastAPI + SQLAlchemy + APScheduler +- Modules réseau : ping, arp, port scan +- WebSocket pour push temps réel +- APIs REST pour : IP, scan, paramètres, historique + +## Frontend +- Vue 3 + Vite + Tailwind +- State global (Pinia) +- WebSocket client + +## Docker +- service web (backend + frontend) +- volume config.yaml +- volume db.sqlite diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..f0dd6dd --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +# IPWatch Backend Application diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..f340f00 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1 @@ +# Core configuration modules diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..17e372a --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,111 @@ +""" +Configuration management pour IPWatch +Charge et valide le fichier config.yaml +""" +import yaml +from pathlib import Path +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field + + +class AppConfig(BaseModel): + """Configuration de l'application""" + name: str = "IPWatch" + version: str = "1.0.0" + debug: bool = False + + +class NetworkConfig(BaseModel): + """Configuration réseau""" + cidr: str + gateway: Optional[str] = None + dns: Optional[List[str]] = None + + +class ScanConfig(BaseModel): + """Configuration des scans""" + ping_interval: int = 60 # secondes + port_scan_interval: int = 300 # secondes + parallel_pings: int = 50 + timeout: float = 1.0 + + +class PortsConfig(BaseModel): + """Configuration des ports à scanner""" + ranges: List[str] = ["22", "80", "443", "3389", "8080"] + + +class HistoryConfig(BaseModel): + """Configuration de l'historique""" + retention_hours: int = 24 + + +class UIConfig(BaseModel): + """Configuration UI""" + offline_transparency: float = 0.5 + show_mac: bool = True + show_vendor: bool = True + + +class ColorsConfig(BaseModel): + """Configuration des couleurs""" + free: str = "#75715E" + online_known: str = "#A6E22E" + online_unknown: str = "#66D9EF" + offline_known: str = "#F92672" + offline_unknown: str = "#AE81FF" + + +class DatabaseConfig(BaseModel): + """Configuration base de données""" + path: str = "./data/db.sqlite" + + +class IPWatchConfig(BaseModel): + """Configuration complète IPWatch""" + app: AppConfig = Field(default_factory=AppConfig) + network: NetworkConfig + ip_classes: Dict[str, Any] = Field(default_factory=dict) + scan: ScanConfig = Field(default_factory=ScanConfig) + ports: PortsConfig = Field(default_factory=PortsConfig) + locations: List[str] = Field(default_factory=list) + hosts: List[str] = Field(default_factory=list) + history: HistoryConfig = Field(default_factory=HistoryConfig) + ui: UIConfig = Field(default_factory=UIConfig) + colors: ColorsConfig = Field(default_factory=ColorsConfig) + database: DatabaseConfig = Field(default_factory=DatabaseConfig) + + +class ConfigManager: + """Gestionnaire de configuration singleton""" + _instance: Optional['ConfigManager'] = None + _config: Optional[IPWatchConfig] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def load_config(self, config_path: str = "./config.yaml") -> IPWatchConfig: + """Charge la configuration depuis le fichier YAML""" + path = Path(config_path) + + if not path.exists(): + raise FileNotFoundError(f"Fichier de configuration non trouvé: {config_path}") + + with open(path, 'r', encoding='utf-8') as f: + yaml_data = yaml.safe_load(f) + + self._config = IPWatchConfig(**yaml_data) + return self._config + + @property + def config(self) -> IPWatchConfig: + """Retourne la configuration actuelle""" + if self._config is None: + raise RuntimeError("Configuration non chargée. Appelez load_config() d'abord.") + return self._config + + +# Instance globale +config_manager = ConfigManager() diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..605585b --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,47 @@ +""" +Configuration de la base de données SQLAlchemy +""" +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from pathlib import Path + +# Base pour les modèles SQLAlchemy +Base = declarative_base() + +# Engine et session +engine = None +SessionLocal = None + + +def init_database(db_path: str = "./data/db.sqlite"): + """Initialise la connexion à la base de données""" + global engine, SessionLocal + + # Créer le dossier data si nécessaire + Path(db_path).parent.mkdir(parents=True, exist_ok=True) + + # Créer l'engine SQLite + database_url = f"sqlite:///{db_path}" + engine = create_engine( + database_url, + connect_args={"check_same_thread": False}, + echo=False + ) + + # Créer la session factory + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + # Créer les tables + Base.metadata.create_all(bind=engine) + + return engine + + +def get_db(): + """Dependency pour obtenir une session DB""" + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..d15c4c2 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,187 @@ +""" +Application FastAPI principale pour IPWatch +Point d'entrée du backend +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +from contextlib import asynccontextmanager +from pathlib import Path + +from backend.app.core.config import config_manager +from backend.app.core.database import init_database, get_db +from backend.app.routers import ips_router, scan_router, websocket_router +from backend.app.services.scheduler import scan_scheduler +from backend.app.routers.scan import perform_scan + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """ + Gestionnaire du cycle de vie de l'application + Initialise et nettoie les ressources + """ + # Startup + print("=== Démarrage IPWatch ===") + + # 1. Charger la configuration + try: + config = config_manager.load_config("./config.yaml") + print(f"✓ Configuration chargée: {config.network.cidr}") + except Exception as e: + print(f"✗ Erreur chargement config: {e}") + raise + + # 2. Initialiser la base de données + try: + init_database(config.database.path) + print(f"✓ Base de données initialisée: {config.database.path}") + except Exception as e: + print(f"✗ Erreur initialisation DB: {e}") + raise + + # 3. Démarrer le scheduler + try: + scan_scheduler.start() + + # Créer une session DB pour les scans planifiés + from backend.app.core.database import SessionLocal + + async def scheduled_scan(): + """Wrapper pour scan planifié avec DB session""" + db = SessionLocal() + try: + await perform_scan(db) + finally: + db.close() + + # Configurer les tâches périodiques + scan_scheduler.add_ping_scan_job( + scheduled_scan, + interval_seconds=config.scan.ping_interval + ) + + scan_scheduler.add_port_scan_job( + scheduled_scan, + interval_seconds=config.scan.port_scan_interval + ) + + # Tâche de nettoyage historique + async def cleanup_history(): + """Nettoie l'historique ancien""" + from backend.app.models.ip import IPHistory + from datetime import datetime, timedelta + + db = SessionLocal() + try: + cutoff = datetime.utcnow() - timedelta(hours=config.history.retention_hours) + deleted = db.query(IPHistory).filter(IPHistory.timestamp < cutoff).delete() + db.commit() + print(f"Nettoyage historique: {deleted} entrées supprimées") + finally: + db.close() + + scan_scheduler.add_cleanup_job(cleanup_history, interval_hours=1) + + print("✓ Scheduler démarré") + except Exception as e: + print(f"✗ Erreur démarrage scheduler: {e}") + + print("=== IPWatch prêt ===\n") + + yield + + # Shutdown + print("\n=== Arrêt IPWatch ===") + scan_scheduler.stop() + print("✓ Scheduler arrêté") + + +# Créer l'application FastAPI +app = FastAPI( + title="IPWatch API", + description="API backend pour IPWatch - Scanner réseau temps réel", + version="1.0.0", + lifespan=lifespan +) + +# Configuration CORS pour le frontend +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # À restreindre en production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Enregistrer les routers API +app.include_router(ips_router) +app.include_router(scan_router) +app.include_router(websocket_router) + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "scheduler": scan_scheduler.is_running + } + + +# Servir les fichiers statiques du frontend +frontend_dist = Path(__file__).parent.parent.parent / "frontend" / "dist" + +if frontend_dist.exists(): + # Monter les assets statiques + app.mount("/assets", StaticFiles(directory=str(frontend_dist / "assets")), name="assets") + + # Route racine pour servir index.html + @app.get("/") + async def serve_frontend(): + """Servir le frontend Vue""" + index_file = frontend_dist / "index.html" + if index_file.exists(): + return FileResponse(index_file) + return { + "name": "IPWatch API", + "version": "1.0.0", + "status": "running", + "error": "Frontend non trouvé" + } + + # Catch-all pour le routing Vue (SPA) + @app.get("/{full_path:path}") + async def catch_all(full_path: str): + """Catch-all pour le routing Vue Router""" + # Ne pas intercepter les routes API + if full_path.startswith("api/") or full_path.startswith("ws"): + return {"error": "Not found"} + + # Servir index.html pour toutes les autres routes + index_file = frontend_dist / "index.html" + if index_file.exists(): + return FileResponse(index_file) + return {"error": "Frontend non trouvé"} +else: + @app.get("/") + async def root(): + """Endpoint racine (mode développement sans frontend)""" + return { + "name": "IPWatch API", + "version": "1.0.0", + "status": "running", + "note": "Frontend non buildé - utilisez le mode dev" + } + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "backend.app.main:app", + host="0.0.0.0", + port=8080, + reload=True + ) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..4df00b5 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,6 @@ +""" +Modèles SQLAlchemy pour IPWatch +""" +from .ip import IP, IPHistory + +__all__ = ["IP", "IPHistory"] diff --git a/backend/app/models/ip.py b/backend/app/models/ip.py new file mode 100644 index 0000000..e325984 --- /dev/null +++ b/backend/app/models/ip.py @@ -0,0 +1,82 @@ +""" +Modèles de données pour les adresses IP et leur historique +Basé sur modele-donnees.md +""" +from sqlalchemy import Column, String, Boolean, DateTime, Integer, ForeignKey, Index, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +from backend.app.core.database import Base + + +class IP(Base): + """ + Table principale des adresses IP + Stocke les informations actuelles et les métadonnées de chaque IP + """ + __tablename__ = "ip" + + # Clé primaire + ip = Column(String, primary_key=True, index=True) + + # Métadonnées + name = Column(String, nullable=True) # Nom donné à l'IP + known = Column(Boolean, default=False, index=True) # IP connue ou inconnue + location = Column(String, nullable=True) # Localisation (ex: "Bureau", "Serveur") + host = Column(String, nullable=True) # Type d'hôte (ex: "PC", "Imprimante") + + # Timestamps + first_seen = Column(DateTime, default=datetime.utcnow) # Première détection + last_seen = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Dernière vue + + # État réseau + last_status = Column(String, index=True) # "online", "offline", "unknown" + + # Informations réseau + mac = Column(String, nullable=True) # Adresse MAC + vendor = Column(String, nullable=True) # Fabricant (lookup MAC) + hostname = Column(String, nullable=True) # Nom d'hôte réseau + + # Ports ouverts (stocké en JSON) + open_ports = Column(JSON, default=list) # Liste des ports ouverts + + # Relation avec l'historique + history = relationship("IPHistory", back_populates="ip_ref", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + +class IPHistory(Base): + """ + Table d'historique des états d'IP + Stocke l'évolution dans le temps (24h par défaut) + """ + __tablename__ = "ip_history" + + # Clé primaire auto-incrémentée + id = Column(Integer, primary_key=True, autoincrement=True) + + # Foreign key vers la table IP + ip = Column(String, ForeignKey("ip.ip", ondelete="CASCADE"), nullable=False, index=True) + + # Timestamp de l'enregistrement + timestamp = Column(DateTime, default=datetime.utcnow, index=True, nullable=False) + + # État à ce moment + status = Column(String, nullable=False) # "online", "offline" + + # Ports ouverts à ce moment (JSON) + open_ports = Column(JSON, default=list) + + # Relation inverse vers IP + ip_ref = relationship("IP", back_populates="history") + + def __repr__(self): + return f"" + + +# Index recommandés (déjà définis dans les colonnes avec index=True) +# Index supplémentaires si nécessaire +Index('idx_ip_last_status', IP.last_status) +Index('idx_ip_history_timestamp', IPHistory.timestamp) +Index('idx_ip_history_ip', IPHistory.ip) diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..a4083fe --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1,8 @@ +""" +Routers API pour IPWatch +""" +from .ips import router as ips_router +from .scan import router as scan_router +from .websocket import router as websocket_router + +__all__ = ["ips_router", "scan_router", "websocket_router"] diff --git a/backend/app/routers/ips.py b/backend/app/routers/ips.py new file mode 100644 index 0000000..3c8269f --- /dev/null +++ b/backend/app/routers/ips.py @@ -0,0 +1,216 @@ +""" +Endpoints API pour la gestion des IPs +""" +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import desc +from typing import List, Optional +from datetime import datetime, timedelta + +from backend.app.core.database import get_db +from backend.app.models.ip import IP, IPHistory +from pydantic import BaseModel + +router = APIRouter(prefix="/api/ips", tags=["IPs"]) + + +# Schémas Pydantic pour validation +class IPUpdate(BaseModel): + """Schéma pour mise à jour d'IP""" + name: Optional[str] = None + known: Optional[bool] = None + location: Optional[str] = None + host: Optional[str] = None + + +class IPResponse(BaseModel): + """Schéma de réponse IP""" + ip: str + name: Optional[str] + known: bool + location: Optional[str] + host: Optional[str] + first_seen: Optional[datetime] + last_seen: Optional[datetime] + last_status: Optional[str] + mac: Optional[str] + vendor: Optional[str] + hostname: Optional[str] + open_ports: List[int] + + class Config: + from_attributes = True + + +class IPHistoryResponse(BaseModel): + """Schéma de réponse historique""" + id: int + ip: str + timestamp: datetime + status: str + open_ports: List[int] + + class Config: + from_attributes = True + + +@router.get("/", response_model=List[IPResponse]) +async def get_all_ips( + status: Optional[str] = None, + known: Optional[bool] = None, + db: Session = Depends(get_db) +): + """ + Récupère toutes les IPs avec filtres optionnels + + Args: + status: Filtrer par statut (online/offline) + known: Filtrer par IPs connues/inconnues + db: Session de base de données + + Returns: + Liste des IPs + """ + query = db.query(IP) + + if status: + query = query.filter(IP.last_status == status) + + if known is not None: + query = query.filter(IP.known == known) + + ips = query.all() + return ips + + +@router.get("/{ip_address}", response_model=IPResponse) +async def get_ip(ip_address: str, db: Session = Depends(get_db)): + """ + Récupère les détails d'une IP spécifique + + Args: + ip_address: Adresse IP + db: Session de base de données + + Returns: + Détails de l'IP + """ + ip = db.query(IP).filter(IP.ip == ip_address).first() + + if not ip: + raise HTTPException(status_code=404, detail="IP non trouvée") + + return ip + + +@router.put("/{ip_address}", response_model=IPResponse) +async def update_ip( + ip_address: str, + ip_update: IPUpdate, + db: Session = Depends(get_db) +): + """ + Met à jour les informations d'une IP + + Args: + ip_address: Adresse IP + ip_update: Données à mettre à jour + db: Session de base de données + + Returns: + IP mise à jour + """ + ip = db.query(IP).filter(IP.ip == ip_address).first() + + if not ip: + raise HTTPException(status_code=404, detail="IP non trouvée") + + # Mettre à jour les champs fournis + update_data = ip_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(ip, field, value) + + db.commit() + db.refresh(ip) + + return ip + + +@router.delete("/{ip_address}") +async def delete_ip(ip_address: str, db: Session = Depends(get_db)): + """ + Supprime une IP (et son historique) + + Args: + ip_address: Adresse IP + db: Session de base de données + + Returns: + Message de confirmation + """ + ip = db.query(IP).filter(IP.ip == ip_address).first() + + if not ip: + raise HTTPException(status_code=404, detail="IP non trouvée") + + db.delete(ip) + db.commit() + + return {"message": f"IP {ip_address} supprimée"} + + +@router.get("/{ip_address}/history", response_model=List[IPHistoryResponse]) +async def get_ip_history( + ip_address: str, + hours: int = 24, + db: Session = Depends(get_db) +): + """ + Récupère l'historique d'une IP + + Args: + ip_address: Adresse IP + hours: Nombre d'heures d'historique (défaut: 24h) + db: Session de base de données + + Returns: + Liste des événements historiques + """ + # Vérifier que l'IP existe + ip = db.query(IP).filter(IP.ip == ip_address).first() + if not ip: + raise HTTPException(status_code=404, detail="IP non trouvée") + + # Calculer la date limite + since = datetime.utcnow() - timedelta(hours=hours) + + # Récupérer l'historique + history = db.query(IPHistory).filter( + IPHistory.ip == ip_address, + IPHistory.timestamp >= since + ).order_by(desc(IPHistory.timestamp)).all() + + return history + + +@router.get("/stats/summary") +async def get_stats(db: Session = Depends(get_db)): + """ + Récupère les statistiques globales du réseau + + Returns: + Statistiques (total, online, offline, known, unknown) + """ + total = db.query(IP).count() + online = db.query(IP).filter(IP.last_status == "online").count() + offline = db.query(IP).filter(IP.last_status == "offline").count() + known = db.query(IP).filter(IP.known == True).count() + unknown = db.query(IP).filter(IP.known == False).count() + + return { + "total": total, + "online": online, + "offline": offline, + "known": known, + "unknown": unknown + } diff --git a/backend/app/routers/scan.py b/backend/app/routers/scan.py new file mode 100644 index 0000000..193b33a --- /dev/null +++ b/backend/app/routers/scan.py @@ -0,0 +1,201 @@ +""" +Endpoints API pour le contrôle des scans réseau +""" +from fastapi import APIRouter, Depends, BackgroundTasks +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from typing import Dict, Any + +from backend.app.core.database import get_db +from backend.app.core.config import config_manager +from backend.app.models.ip import IP, IPHistory +from backend.app.services.network import NetworkScanner +from backend.app.services.websocket import ws_manager + +router = APIRouter(prefix="/api/scan", tags=["Scan"]) + + +async def perform_scan(db: Session): + """ + Effectue un scan complet du réseau + Fonction asynchrone pour background task + + Args: + db: Session de base de données + """ + try: + print(f"[{datetime.now()}] Début du scan réseau...") + + # Notifier début du scan + try: + await ws_manager.broadcast_scan_start() + except Exception as e: + print(f"Erreur broadcast start (ignorée): {e}") + + # Récupérer la config + config = config_manager.config + print(f"[{datetime.now()}] Config chargée: {config.network.cidr}") + + # Initialiser le scanner + scanner = NetworkScanner( + cidr=config.network.cidr, + timeout=config.scan.timeout + ) + + # Convertir les ports en liste d'entiers + port_list = [] + for port_range in config.ports.ranges: + if '-' in port_range: + start, end = map(int, port_range.split('-')) + port_list.extend(range(start, end + 1)) + else: + port_list.append(int(port_range)) + + print(f"[{datetime.now()}] Ports à scanner: {port_list}") + + # Récupérer les IPs connues + known_ips = config.ip_classes + print(f"[{datetime.now()}] IPs connues: {len(known_ips)}") + + # Lancer le scan + print(f"[{datetime.now()}] Lancement du scan (parallélisme: {config.scan.parallel_pings})...") + scan_results = await scanner.full_scan( + known_ips=known_ips, + port_list=port_list, + max_concurrent=config.scan.parallel_pings + ) + print(f"[{datetime.now()}] Scan terminé: {len(scan_results)} IPs trouvées") + + # Mettre à jour la base de données + stats = { + "total": 0, + "online": 0, + "offline": 0, + "new": 0, + "updated": 0 + } + + for ip_address, ip_data in scan_results.items(): + stats["total"] += 1 + + if ip_data["last_status"] == "online": + stats["online"] += 1 + else: + stats["offline"] += 1 + + # Vérifier si l'IP existe déjà + existing_ip = db.query(IP).filter(IP.ip == ip_address).first() + + if existing_ip: + # Mettre à jour l'IP existante + old_status = existing_ip.last_status + + existing_ip.last_status = ip_data["last_status"] + if ip_data["last_seen"]: + existing_ip.last_seen = ip_data["last_seen"] + existing_ip.mac = ip_data.get("mac") or existing_ip.mac + existing_ip.vendor = ip_data.get("vendor") or existing_ip.vendor + existing_ip.hostname = ip_data.get("hostname") or existing_ip.hostname + existing_ip.open_ports = ip_data.get("open_ports", []) + + # Si l'état a changé, notifier via WebSocket + if old_status != ip_data["last_status"]: + await ws_manager.broadcast_ip_update({ + "ip": ip_address, + "old_status": old_status, + "new_status": ip_data["last_status"] + }) + + stats["updated"] += 1 + + else: + # Créer une nouvelle IP + new_ip = IP( + ip=ip_address, + name=ip_data.get("name"), + known=ip_data.get("known", False), + location=ip_data.get("location"), + host=ip_data.get("host"), + first_seen=datetime.utcnow(), + last_seen=ip_data.get("last_seen") or datetime.utcnow(), + last_status=ip_data["last_status"], + mac=ip_data.get("mac"), + vendor=ip_data.get("vendor"), + hostname=ip_data.get("hostname"), + open_ports=ip_data.get("open_ports", []) + ) + db.add(new_ip) + + # Notifier nouvelle IP + await ws_manager.broadcast_new_ip({ + "ip": ip_address, + "status": ip_data["last_status"], + "known": ip_data.get("known", False) + }) + + stats["new"] += 1 + + # Ajouter à l'historique + history_entry = IPHistory( + ip=ip_address, + timestamp=datetime.utcnow(), + status=ip_data["last_status"], + open_ports=ip_data.get("open_ports", []) + ) + db.add(history_entry) + + # Commit les changements + db.commit() + + # Notifier fin du scan avec stats + await ws_manager.broadcast_scan_complete(stats) + + print(f"[{datetime.now()}] Scan terminé: {stats}") + + except Exception as e: + print(f"Erreur lors du scan: {e}") + db.rollback() + + +@router.post("/start") +async def start_scan(background_tasks: BackgroundTasks, db: Session = Depends(get_db)): + """ + Déclenche un scan réseau immédiat + + Returns: + Message de confirmation + """ + # Lancer le scan en arrière-plan + background_tasks.add_task(perform_scan, db) + + return { + "message": "Scan réseau démarré", + "timestamp": datetime.utcnow() + } + + +@router.post("/cleanup-history") +async def cleanup_history(hours: int = 24, db: Session = Depends(get_db)): + """ + Nettoie l'historique plus ancien que X heures + + Args: + hours: Nombre d'heures à conserver (défaut: 24h) + db: Session de base de données + + Returns: + Nombre d'entrées supprimées + """ + cutoff_date = datetime.utcnow() - timedelta(hours=hours) + + deleted = db.query(IPHistory).filter( + IPHistory.timestamp < cutoff_date + ).delete() + + db.commit() + + return { + "message": f"Historique nettoyé", + "deleted_entries": deleted, + "older_than_hours": hours + } diff --git a/backend/app/routers/websocket.py b/backend/app/routers/websocket.py new file mode 100644 index 0000000..7c6ee50 --- /dev/null +++ b/backend/app/routers/websocket.py @@ -0,0 +1,35 @@ +""" +Endpoint WebSocket pour notifications temps réel +""" +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from backend.app.services.websocket import ws_manager + +router = APIRouter(tags=["WebSocket"]) + + +@router.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + """ + Endpoint WebSocket pour notifications temps réel + + Args: + websocket: Connexion WebSocket + """ + await ws_manager.connect(websocket) + + try: + # Boucle de réception (keep-alive) + while True: + # Recevoir des messages du client (heartbeat) + data = await websocket.receive_text() + + # On peut gérer des commandes du client ici si nécessaire + # Pour l'instant, on fait juste un echo pour keep-alive + if data == "ping": + await ws_manager.send_personal_message("pong", websocket) + + except WebSocketDisconnect: + ws_manager.disconnect(websocket) + except Exception as e: + print(f"Erreur WebSocket: {e}") + ws_manager.disconnect(websocket) diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..58d3f97 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,7 @@ +""" +Services réseau pour IPWatch +""" +from .network import NetworkScanner +from .scheduler import ScanScheduler + +__all__ = ["NetworkScanner", "ScanScheduler"] diff --git a/backend/app/services/network.py b/backend/app/services/network.py new file mode 100644 index 0000000..b3ddf43 --- /dev/null +++ b/backend/app/services/network.py @@ -0,0 +1,295 @@ +""" +Modules réseau pour scan d'IP, ping, ARP et port scan +Implémente le workflow de scan selon workflow-scan.md +""" +import asyncio +import ipaddress +import platform +import subprocess +import socket +from typing import List, Dict, Optional, Tuple +from datetime import datetime +import re + +# Scapy pour ARP +try: + from scapy.all import ARP, Ether, srp + SCAPY_AVAILABLE = True +except ImportError: + SCAPY_AVAILABLE = False + + +class NetworkScanner: + """Scanner réseau principal""" + + def __init__(self, cidr: str, timeout: float = 1.0): + """ + Initialise le scanner réseau + + Args: + cidr: Réseau CIDR (ex: "192.168.1.0/24") + timeout: Timeout pour ping et connexions (secondes) + """ + self.cidr = cidr + self.timeout = timeout + self.network = ipaddress.ip_network(cidr, strict=False) + + def generate_ip_list(self) -> List[str]: + """ + Génère la liste complète d'IP depuis le CIDR + + Returns: + Liste des adresses IP en string + """ + return [str(ip) for ip in self.network.hosts()] + + async def ping(self, ip: str) -> bool: + """ + Ping une adresse IP (async) + + Args: + ip: Adresse IP à pinger + + Returns: + True si l'IP répond, False sinon + """ + # Détection de l'OS pour la commande ping + param = '-n' if platform.system().lower() == 'windows' else '-c' + timeout_param = '-w' if platform.system().lower() == 'windows' else '-W' + + command = ['ping', param, '1', timeout_param, str(int(self.timeout * 1000) if platform.system().lower() == 'windows' else str(int(self.timeout))), ip] + + try: + # Exécuter le ping de manière asynchrone + process = await asyncio.create_subprocess_exec( + *command, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL + ) + await asyncio.wait_for(process.wait(), timeout=self.timeout + 1) + return process.returncode == 0 + except (asyncio.TimeoutError, Exception): + return False + + async def ping_parallel(self, ip_list: List[str], max_concurrent: int = 50) -> Dict[str, bool]: + """ + Ping multiple IPs en parallèle + + Args: + ip_list: Liste des IPs à pinger + max_concurrent: Nombre maximum de pings simultanés + + Returns: + Dictionnaire {ip: online_status} + """ + results = {} + semaphore = asyncio.Semaphore(max_concurrent) + + async def ping_with_semaphore(ip: str): + async with semaphore: + results[ip] = await self.ping(ip) + + # Lancer tous les pings en parallèle avec limite + await asyncio.gather(*[ping_with_semaphore(ip) for ip in ip_list]) + + return results + + def get_arp_table(self) -> Dict[str, Tuple[str, str]]: + """ + Récupère la table ARP du système + + Returns: + Dictionnaire {ip: (mac, vendor)} + """ + arp_data = {} + + if SCAPY_AVAILABLE: + try: + # Utiliser Scapy pour ARP scan + answered, _ = srp( + Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst=self.cidr), + timeout=2, + verbose=False + ) + + for sent, received in answered: + ip = received.psrc + mac = received.hwsrc + vendor = self._get_mac_vendor(mac) + arp_data[ip] = (mac, vendor) + except Exception as e: + print(f"Erreur ARP scan avec Scapy: {e}") + else: + # Fallback: parser la table ARP système + try: + if platform.system().lower() == 'windows': + output = subprocess.check_output(['arp', '-a'], text=True) + pattern = r'(\d+\.\d+\.\d+\.\d+)\s+([0-9a-fA-F-:]+)' + else: + output = subprocess.check_output(['arp', '-n'], text=True) + pattern = r'(\d+\.\d+\.\d+\.\d+)\s+\w+\s+([0-9a-fA-F:]+)' + + matches = re.findall(pattern, output) + for ip, mac in matches: + if ip in [str(h) for h in self.network.hosts()]: + vendor = self._get_mac_vendor(mac) + arp_data[ip] = (mac, vendor) + except Exception as e: + print(f"Erreur lecture table ARP: {e}") + + return arp_data + + def _get_mac_vendor(self, mac: str) -> str: + """ + Lookup du fabricant depuis l'adresse MAC + Simplifié pour l'instant - peut être étendu avec une vraie DB OUI + + Args: + mac: Adresse MAC + + Returns: + Nom du fabricant ou "Unknown" + """ + # TODO: Implémenter lookup OUI complet + # Pour l'instant, retourne un placeholder + mac_prefix = mac[:8].upper().replace(':', '').replace('-', '') + + # Mini DB des fabricants courants + vendors = { + "00:0C:29": "VMware", + "00:50:56": "VMware", + "08:00:27": "VirtualBox", + "DC:A6:32": "Raspberry Pi", + "B8:27:EB": "Raspberry Pi", + } + + for prefix, vendor in vendors.items(): + if mac.upper().startswith(prefix.replace(':', '')): + return vendor + + return "Unknown" + + async def scan_ports(self, ip: str, ports: List[int]) -> List[int]: + """ + Scan des ports TCP sur une IP + + Args: + ip: Adresse IP cible + ports: Liste des ports à scanner + + Returns: + Liste des ports ouverts + """ + open_ports = [] + + async def check_port(port: int) -> Optional[int]: + try: + # Tentative de connexion TCP + reader, writer = await asyncio.wait_for( + asyncio.open_connection(ip, port), + timeout=self.timeout + ) + writer.close() + await writer.wait_closed() + return port + except: + return None + + # Scanner tous les ports en parallèle + results = await asyncio.gather(*[check_port(p) for p in ports]) + open_ports = [p for p in results if p is not None] + + return open_ports + + def get_hostname(self, ip: str) -> Optional[str]: + """ + Résolution DNS inversée pour obtenir le hostname + + Args: + ip: Adresse IP + + Returns: + Hostname ou None + """ + try: + hostname, _, _ = socket.gethostbyaddr(ip) + return hostname + except: + return None + + def classify_ip_status(self, is_online: bool, is_known: bool) -> str: + """ + Classification de l'état d'une IP + + Args: + is_online: IP en ligne + is_known: IP connue dans la config + + Returns: + État: "online", "offline" + """ + return "online" if is_online else "offline" + + async def full_scan(self, known_ips: Dict[str, Dict], port_list: List[int], max_concurrent: int = 50) -> Dict[str, Dict]: + """ + Scan complet du réseau selon workflow-scan.md + + Args: + known_ips: Dictionnaire des IPs connues depuis config + port_list: Liste des ports à scanner + max_concurrent: Pings simultanés max + + Returns: + Dictionnaire des résultats de scan pour chaque IP + """ + results = {} + + # 1. Générer liste IP du CIDR + ip_list = self.generate_ip_list() + + # 2. Ping parallélisé + ping_results = await self.ping_parallel(ip_list, max_concurrent) + + # 3. ARP + MAC vendor + arp_table = self.get_arp_table() + + # 4. Pour chaque IP + for ip in ip_list: + is_online = ping_results.get(ip, False) + is_known = ip in known_ips + + ip_data = { + "ip": ip, + "known": is_known, + "last_status": self.classify_ip_status(is_online, is_known), + "last_seen": datetime.utcnow() if is_online else None, + "mac": None, + "vendor": None, + "hostname": None, + "open_ports": [], + } + + # Ajouter infos connues + if is_known: + ip_data.update(known_ips[ip]) + + # Infos ARP + if ip in arp_table: + mac, vendor = arp_table[ip] + ip_data["mac"] = mac + ip_data["vendor"] = vendor + + # Hostname + if is_online: + hostname = self.get_hostname(ip) + if hostname: + ip_data["hostname"] = hostname + + # 5. Port scan (uniquement si online) + if is_online and port_list: + open_ports = await self.scan_ports(ip, port_list) + ip_data["open_ports"] = open_ports + + results[ip] = ip_data + + return results diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py new file mode 100644 index 0000000..855ee76 --- /dev/null +++ b/backend/app/services/scheduler.py @@ -0,0 +1,103 @@ +""" +Scheduler APScheduler pour les scans réseau périodiques +""" +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.interval import IntervalTrigger +from datetime import datetime, timedelta +from typing import Optional, Callable +import asyncio + + +class ScanScheduler: + """Gestionnaire de tâches planifiées pour les scans""" + + def __init__(self): + """Initialise le scheduler""" + self.scheduler = AsyncIOScheduler() + self.is_running = False + + def start(self): + """Démarre le scheduler""" + if not self.is_running: + self.scheduler.start() + self.is_running = True + print(f"[{datetime.now()}] Scheduler démarré") + + def stop(self): + """Arrête le scheduler""" + if self.is_running: + self.scheduler.shutdown() + self.is_running = False + print(f"[{datetime.now()}] Scheduler arrêté") + + def add_ping_scan_job(self, scan_function: Callable, interval_seconds: int = 60): + """ + Ajoute une tâche de scan ping périodique + + Args: + scan_function: Fonction async à exécuter + interval_seconds: Intervalle en secondes + """ + self.scheduler.add_job( + scan_function, + trigger=IntervalTrigger(seconds=interval_seconds), + id='ping_scan', + name='Scan Ping périodique', + replace_existing=True + ) + print(f"Tâche ping_scan configurée: toutes les {interval_seconds}s") + + def add_port_scan_job(self, scan_function: Callable, interval_seconds: int = 300): + """ + Ajoute une tâche de scan de ports périodique + + Args: + scan_function: Fonction async à exécuter + interval_seconds: Intervalle en secondes + """ + self.scheduler.add_job( + scan_function, + trigger=IntervalTrigger(seconds=interval_seconds), + id='port_scan', + name='Scan ports périodique', + replace_existing=True + ) + print(f"Tâche port_scan configurée: toutes les {interval_seconds}s") + + def add_cleanup_job(self, cleanup_function: Callable, interval_hours: int = 1): + """ + Ajoute une tâche de nettoyage de l'historique + + Args: + cleanup_function: Fonction async de nettoyage + interval_hours: Intervalle en heures + """ + self.scheduler.add_job( + cleanup_function, + trigger=IntervalTrigger(hours=interval_hours), + id='history_cleanup', + name='Nettoyage historique', + replace_existing=True + ) + print(f"Tâche cleanup configurée: toutes les {interval_hours}h") + + def remove_job(self, job_id: str): + """ + Supprime une tâche planifiée + + Args: + job_id: ID de la tâche + """ + try: + self.scheduler.remove_job(job_id) + print(f"Tâche {job_id} supprimée") + except Exception as e: + print(f"Erreur suppression tâche {job_id}: {e}") + + def get_jobs(self): + """Retourne la liste des tâches planifiées""" + return self.scheduler.get_jobs() + + +# Instance globale du scheduler +scan_scheduler = ScanScheduler() diff --git a/backend/app/services/websocket.py b/backend/app/services/websocket.py new file mode 100644 index 0000000..25ea2cb --- /dev/null +++ b/backend/app/services/websocket.py @@ -0,0 +1,125 @@ +""" +Gestionnaire WebSocket pour notifications temps réel +""" +from fastapi import WebSocket +from typing import List, Dict, Any +import json +from datetime import datetime + + +class WebSocketManager: + """Gestionnaire de connexions WebSocket""" + + def __init__(self): + """Initialise le gestionnaire""" + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + """ + Accepte une nouvelle connexion WebSocket + + Args: + websocket: Instance WebSocket + """ + await websocket.accept() + self.active_connections.append(websocket) + print(f"[{datetime.now()}] Nouvelle connexion WebSocket. Total: {len(self.active_connections)}") + + def disconnect(self, websocket: WebSocket): + """ + Déconnecte un client WebSocket + + Args: + websocket: Instance WebSocket à déconnecter + """ + if websocket in self.active_connections: + self.active_connections.remove(websocket) + print(f"[{datetime.now()}] Déconnexion WebSocket. Total: {len(self.active_connections)}") + + async def send_personal_message(self, message: str, websocket: WebSocket): + """ + Envoie un message à un client spécifique + + Args: + message: Message à envoyer + websocket: Client destinataire + """ + try: + await websocket.send_text(message) + except Exception as e: + print(f"Erreur envoi message personnel: {e}") + + async def broadcast(self, message: Dict[str, Any]): + """ + Diffuse un message à tous les clients connectés + + Args: + message: Dictionnaire du message (sera converti en JSON) + """ + # Ajouter un timestamp + message["timestamp"] = datetime.utcnow().isoformat() + + json_message = json.dumps(message) + + # Liste des connexions à supprimer (déconnectées) + disconnected = [] + + for connection in self.active_connections: + try: + await connection.send_text(json_message) + except Exception as e: + print(f"Erreur broadcast: {e}") + disconnected.append(connection) + + # Nettoyer les connexions mortes + for conn in disconnected: + self.disconnect(conn) + + async def broadcast_scan_start(self): + """Notifie le début d'un scan""" + await self.broadcast({ + "type": "scan_start", + "message": "Scan réseau démarré" + }) + + async def broadcast_scan_complete(self, stats: Dict[str, int]): + """ + Notifie la fin d'un scan avec statistiques + + Args: + stats: Statistiques du scan (total, online, offline, etc.) + """ + await self.broadcast({ + "type": "scan_complete", + "message": "Scan réseau terminé", + "stats": stats + }) + + async def broadcast_ip_update(self, ip_data: Dict[str, Any]): + """ + Notifie un changement d'état d'IP + + Args: + ip_data: Données de l'IP mise à jour + """ + await self.broadcast({ + "type": "ip_update", + "data": ip_data + }) + + async def broadcast_new_ip(self, ip_data: Dict[str, Any]): + """ + Notifie la détection d'une nouvelle IP + + Args: + ip_data: Données de la nouvelle IP + """ + await self.broadcast({ + "type": "new_ip", + "data": ip_data, + "message": f"Nouvelle IP détectée: {ip_data.get('ip')}" + }) + + +# Instance globale du gestionnaire WebSocket +ws_manager = WebSocketManager() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..204a37c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,16 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +sqlalchemy==2.0.25 +pydantic==2.5.3 +pydantic-settings==2.1.0 +python-multipart==0.0.6 +websockets==12.0 +apscheduler==3.10.4 +pyyaml==6.0.1 +asyncio==3.4.3 +aiosqlite==0.19.0 +python-nmap==0.7.1 +scapy==2.5.0 +pytest==7.4.4 +pytest-asyncio==0.23.3 +httpx==0.26.0 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..2b11070 --- /dev/null +++ b/config.yaml @@ -0,0 +1,89 @@ +# Configuration IPWatch +# Basé sur consigne-parametrage.md + +app: + name: "IPWatch" + version: "1.0.0" + debug: true + +network: + cidr: "10.0.0.0/22" + gateway: "10.0.0.1" + dns: + - "8.8.8.8" + - "8.8.4.4" + +# Sous-réseaux organisés en sections +subnets: + - name: "static_vm" + cidr: "10.0.0.0/24" + start: "10.0.0.1" + end: "10.0.0.255" + description: "Machines virtuelles statiques" + - name: "dhcp" + cidr: "10.0.1.0/24" + start: "10.0.1.1" + end: "10.0.1locations.255" + description: "DHCP" + - name: "iot" + cidr: "10.0.2.0/24" + start: "10.0.2.1" + end: "10.0.2.255" + description: "IoT" + +# IPs connues avec métadonnées +ip_classes: + "10.0.0.1": + name: "Gateway" + location: "Réseau" + host: "Routeur" + +scan: + ping_interval: 600 # Intervalle scan ping (secondes) + port_scan_interval: 1200 # Intervalle scan ports (secondes) + parallel_pings: 100 # Nombre de pings simultanés + timeout: 1.0 # Timeout réseau (secondes) + +ports: + ranges: + - "22" # SSH + - "80" # HTTP + - "443" # HTTPS + - "3389" # RDP + - "8080" # HTTP alternatif + - "3306" # MySQL + - "5432" # PostgreSQL + +locations: + - "Bureau" + - "Salon" + - "Comble" + - "Bureau RdC" + +# la localisation est herité de l'host il faudrait adapter config en consequence +hosts: + - "physique" + - "elitedesk" + - "m710Q" + - "HP Proliant" + - "pve MSI" + - "HP Proxmox" + + +history: + retention_hours: 24 # Conserver 24h d'historique + +ui: + offline_transparency: 0.5 # Transparence des IPs offline + show_mac: true + show_vendor: true + +colors: + free: "#75715E" # IP libre (gris Monokai) + online_known: "#A6E22E" # En ligne + connue (vert) + online_unknown: "#66D9EF" # En ligne + inconnue (cyan) + offline_known: "#F92672" # Hors ligne + connue (rose/rouge) + offline_unknown: "#AE81FF" # Hors ligne + inconnue (violet) + +database: + path: "./data/db.sqlite" diff --git a/consigne-design_webui.md b/consigne-design_webui.md new file mode 100644 index 0000000..d6c30d2 --- /dev/null +++ b/consigne-design_webui.md @@ -0,0 +1,27 @@ +# consigne-design_webui.md + +## Thème +Monokai dark, contrastes forts, bordures arrondies. + +## Layout général +3 colonnes : +- gauche : détail IP +- centre : grille d’IP + légende + classes +- droite : nouvelles détections + +## États des IP +Couleurs, bordure pleine/hors ligne, halo ping en cours. + +## Composants +- Header +- Volet gauche +- Grille IP +- Volet droit +- Onglet paramètres + +## Interactions +- sélection case IP +- clic nouvelle IP +- filtres à cocher +- animation ping +- transparence offline diff --git a/consigne-parametrage.md b/consigne-parametrage.md new file mode 100644 index 0000000..3666fea --- /dev/null +++ b/consigne-parametrage.md @@ -0,0 +1,21 @@ +# consigne-parametrage.md + +Ce document décrit toutes les règles du fichier YAML. + +## Sections +- app +- network +- ip_classes +- scan +- ports +- locations +- hosts +- history +- ui +- colors +- network_advanced +- filters +- database + +## Exemple complet +(… full YAML spec as defined previously …) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8f8a17e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + ipwatch: + build: . + container_name: ipwatch + restart: unless-stopped + + # Réseau host pour accès complet au réseau local + network_mode: host + + # Privilèges pour scan réseau (ping, ARP) + privileged: true + cap_add: + - NET_ADMIN + - NET_RAW + + volumes: + # Volume pour la configuration + - ./config.yaml:/app/config.yaml:ro + + # Volume pour la base de données + - ./data:/app/data + + # Volume pour les logs (optionnel) + - ./logs:/app/logs + + environment: + - TZ=Europe/Paris + + # Healthcheck + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +# Créer les volumes nommés si nécessaire +volumes: + ipwatch-data: + ipwatch-logs: diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..afb17b7 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + IPWatch - Scanner Réseau + + +

+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..aa2e93a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "ipwatch-frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.15", + "pinia": "^2.1.7", + "axios": "^1.6.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.3", + "vite": "^5.0.11", + "tailwindcss": "^3.4.1", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.33" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..8dedbc9 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000..8b93bb3 --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1,147 @@ +/* Styles principaux IPWatch - Thème Monokai */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Variables CSS Monokai */ +:root { + --monokai-bg: #272822; + --monokai-text: #F8F8F2; + --monokai-comment: #75715E; + --monokai-green: #A6E22E; + --monokai-pink: #F92672; + --monokai-cyan: #66D9EF; + --monokai-purple: #AE81FF; + --monokai-yellow: #E6DB74; + --monokai-orange: #FD971F; +} + +/* Base */ +body { + margin: 0; + padding: 0; + background-color: var(--monokai-bg); + color: var(--monokai-text); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Animation halo ping */ +@keyframes ping-pulse { + 0% { + box-shadow: 0 0 0 0 rgba(102, 217, 239, 0.7); + } + 50% { + box-shadow: 0 0 20px 10px rgba(102, 217, 239, 0.3); + } + 100% { + box-shadow: 0 0 0 0 rgba(102, 217, 239, 0); + } +} + +.ping-animation { + animation: ping-pulse 1.5s ease-in-out infinite; +} + +/* Cases IP compactes - Version minimale */ +.ip-cell-compact { + @apply rounded cursor-pointer transition-all duration-200 relative; + border: 2px solid; + width: 50px; + height: 50px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: 14px; +} + +/* Cases IP - États selon guidelines-css.md */ +.ip-cell { + @apply rounded-lg p-3 cursor-pointer transition-all duration-200; + border: 2px solid; + min-height: 80px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/* IP libre */ +.ip-cell.free, +.ip-cell-compact.free { + background-color: rgba(117, 113, 94, 0.2); + border-color: var(--monokai-comment); + color: var(--monokai-comment); +} + +/* IP en ligne + connue (vert) */ +.ip-cell.online-known, +.ip-cell-compact.online-known { + background-color: rgba(166, 226, 46, 0.15); + border-color: var(--monokai-green); + border-style: solid; + color: var(--monokai-text); +} + +.ip-cell.online-known:hover, +.ip-cell-compact.online-known:hover { + background-color: rgba(166, 226, 46, 0.25); +} + +/* IP en ligne + inconnue (cyan) */ +.ip-cell.online-unknown, +.ip-cell-compact.online-unknown { + background-color: rgba(102, 217, 239, 0.15); + border-color: var(--monokai-cyan); + border-style: solid; + color: var(--monokai-text); +} + +.ip-cell.online-unknown:hover, +.ip-cell-compact.online-unknown:hover { + background-color: rgba(102, 217, 239, 0.25); +} + +/* IP hors ligne + connue (rose) */ +.ip-cell.offline-known, +.ip-cell-compact.offline-known { + background-color: rgba(249, 38, 114, 0.1); + border-color: var(--monokai-pink); + border-style: dashed; + color: var(--monokai-text); + opacity: 0.5; +} + +/* IP hors ligne + inconnue (violet) */ +.ip-cell.offline-unknown, +.ip-cell-compact.offline-unknown { + background-color: rgba(174, 129, 255, 0.1); + border-color: var(--monokai-purple); + border-style: dashed; + color: var(--monokai-text); + opacity: 0.5; +} + +/* Sélection */ +.ip-cell.selected { + box-shadow: 0 0 20px rgba(230, 219, 116, 0.5); + border-color: var(--monokai-yellow); +} + +/* Scrollbar custom Monokai */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: #1e1f1c; +} + +::-webkit-scrollbar-thumb { + background: var(--monokai-comment); + border-radius: 5px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--monokai-cyan); +} diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue new file mode 100644 index 0000000..65ac69c --- /dev/null +++ b/frontend/src/components/AppHeader.vue @@ -0,0 +1,68 @@ + + + diff --git a/frontend/src/components/IPCell.vue b/frontend/src/components/IPCell.vue new file mode 100644 index 0000000..2bd282a --- /dev/null +++ b/frontend/src/components/IPCell.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/IPDetails.vue b/frontend/src/components/IPDetails.vue new file mode 100644 index 0000000..b7031a1 --- /dev/null +++ b/frontend/src/components/IPDetails.vue @@ -0,0 +1,207 @@ + + + diff --git a/frontend/src/components/IPGrid.vue b/frontend/src/components/IPGrid.vue new file mode 100644 index 0000000..6509575 --- /dev/null +++ b/frontend/src/components/IPGrid.vue @@ -0,0 +1,79 @@ + + + diff --git a/frontend/src/components/IPGridTree.vue b/frontend/src/components/IPGridTree.vue new file mode 100644 index 0000000..4510f0a --- /dev/null +++ b/frontend/src/components/IPGridTree.vue @@ -0,0 +1,129 @@ + + + diff --git a/frontend/src/components/NewDetections.vue b/frontend/src/components/NewDetections.vue new file mode 100644 index 0000000..41bbffc --- /dev/null +++ b/frontend/src/components/NewDetections.vue @@ -0,0 +1,119 @@ + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..414797f --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,10 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import './assets/main.css' + +const app = createApp(App) +const pinia = createPinia() + +app.use(pinia) +app.mount('#app') diff --git a/frontend/src/stores/ipStore.js b/frontend/src/stores/ipStore.js new file mode 100644 index 0000000..20e9771 --- /dev/null +++ b/frontend/src/stores/ipStore.js @@ -0,0 +1,230 @@ +/** + * Store Pinia pour la gestion des IPs + */ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import axios from 'axios' + +export const useIPStore = defineStore('ip', () => { + // État + const ips = ref([]) + const selectedIP = ref(null) + const loading = ref(false) + const error = ref(null) + const stats = ref({ + total: 0, + online: 0, + offline: 0, + known: 0, + unknown: 0 + }) + + // Filtres + const filters = ref({ + showOnline: true, + showOffline: true, + showKnown: true, + showUnknown: true, + showFree: true + }) + + // WebSocket + const ws = ref(null) + const wsConnected = ref(false) + + // Computed + const filteredIPs = computed(() => { + return ips.value.filter(ip => { + // Filtrer par statut + if (ip.last_status === 'online' && !filters.value.showOnline) return false + if (ip.last_status === 'offline' && !filters.value.showOffline) return false + + // Filtrer par connu/inconnu + if (ip.known && !filters.value.showKnown) return false + if (!ip.known && !filters.value.showUnknown) return false + + // Filtrer IP libres (pas de last_status) + if (!ip.last_status && !filters.value.showFree) return false + + return true + }) + }) + + // Actions + async function fetchIPs() { + loading.value = true + error.value = null + + try { + const response = await axios.get('/api/ips/') + ips.value = response.data + await fetchStats() + } catch (err) { + error.value = err.message + console.error('Erreur chargement IPs:', err) + } finally { + loading.value = false + } + } + + async function fetchStats() { + try { + const response = await axios.get('/api/ips/stats/summary') + stats.value = response.data + } catch (err) { + console.error('Erreur chargement stats:', err) + } + } + + async function updateIP(ipAddress, data) { + try { + const response = await axios.put(`/api/ips/${ipAddress}`, data) + + // Mettre à jour dans le store + const index = ips.value.findIndex(ip => ip.ip === ipAddress) + if (index !== -1) { + ips.value[index] = response.data + } + + if (selectedIP.value?.ip === ipAddress) { + selectedIP.value = response.data + } + + return response.data + } catch (err) { + error.value = err.message + throw err + } + } + + async function getIPHistory(ipAddress, hours = 24) { + try { + const response = await axios.get(`/api/ips/${ipAddress}/history?hours=${hours}`) + return response.data + } catch (err) { + console.error('Erreur chargement historique:', err) + throw err + } + } + + async function startScan() { + try { + await axios.post('/api/scan/start') + } catch (err) { + error.value = err.message + throw err + } + } + + function selectIP(ip) { + selectedIP.value = ip + } + + function clearSelection() { + selectedIP.value = null + } + + // WebSocket + function connectWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${protocol}//${window.location.host}/ws` + + ws.value = new WebSocket(wsUrl) + + ws.value.onopen = () => { + console.log('WebSocket connecté') + wsConnected.value = true + + // Heartbeat toutes les 30s + setInterval(() => { + if (ws.value?.readyState === WebSocket.OPEN) { + ws.value.send('ping') + } + }, 30000) + } + + ws.value.onmessage = (event) => { + try { + const message = JSON.parse(event.data) + handleWebSocketMessage(message) + } catch (err) { + console.error('Erreur parsing WebSocket:', err) + } + } + + ws.value.onerror = (error) => { + console.error('Erreur WebSocket:', error) + wsConnected.value = false + } + + ws.value.onclose = () => { + console.log('WebSocket déconnecté') + wsConnected.value = false + + // Reconnexion après 5s + setTimeout(connectWebSocket, 5000) + } + } + + function handleWebSocketMessage(message) { + console.log('Message WebSocket:', message) + + switch (message.type) { + case 'scan_start': + // Notification début de scan + break + + case 'scan_complete': + // Rafraîchir les données après scan + fetchIPs() + stats.value = message.stats + break + + case 'ip_update': + // Mise à jour d'une IP + const updatedIP = ips.value.find(ip => ip.ip === message.data.ip) + if (updatedIP) { + Object.assign(updatedIP, message.data) + } + break + + case 'new_ip': + // Nouvelle IP détectée + fetchIPs() // Recharger pour être sûr + break + } + } + + function disconnectWebSocket() { + if (ws.value) { + ws.value.close() + ws.value = null + wsConnected.value = false + } + } + + return { + // État + ips, + selectedIP, + loading, + error, + stats, + filters, + wsConnected, + + // Computed + filteredIPs, + + // Actions + fetchIPs, + fetchStats, + updateIP, + getIPHistory, + startScan, + selectIP, + clearSelection, + connectWebSocket, + disconnectWebSocket + } +}) diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..4b6ee94 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,26 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: { + colors: { + // Palette Monokai (guidelines-css.md) + monokai: { + bg: '#272822', + text: '#F8F8F2', + comment: '#75715E', + green: '#A6E22E', + pink: '#F92672', + cyan: '#66D9EF', + purple: '#AE81FF', + yellow: '#E6DB74', + orange: '#FD971F', + }, + }, + }, + }, + plugins: [], +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..eca67bb --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true + }, + '/ws': { + target: 'ws://localhost:8080', + ws: true + } + } + } +}) diff --git a/guidelines-css.md b/guidelines-css.md new file mode 100644 index 0000000..a96f11a --- /dev/null +++ b/guidelines-css.md @@ -0,0 +1,17 @@ +# guidelines-css.md + +## Palette Monokai +- backgrounds : #272822 +- text : #F8F8F2 +- accents : #A6E22E, #F92672, #66D9EF + +## Cases IP +- Couleurs selon état +- Bordure en ligne : solid +- Bordure hors ligne : dashed +- Transparency offline configurable +- Halo ping animé (CSS keyframes) + +## Responsive +- grille fluide +- colonnes collapsibles diff --git a/modele-donnees.md b/modele-donnees.md new file mode 100644 index 0000000..d2d1d26 --- /dev/null +++ b/modele-donnees.md @@ -0,0 +1,28 @@ +# modele-donnees.md + +## Tables SQLite + +### Table ip +- ip (PK) +- name +- known (bool) +- location +- host +- first_seen +- last_seen +- last_status +- mac +- vendor +- hostname +- open_ports (JSON) + +### Table ip_history +- id +- ip (FK) +- timestamp +- status +- open_ports (JSON) + +## Index recommandés +- index sur last_status +- index sur ip_history.timestamp diff --git a/prompt-claude-code.md b/prompt-claude-code.md new file mode 100644 index 0000000..a4b0cf3 --- /dev/null +++ b/prompt-claude-code.md @@ -0,0 +1,24 @@ +# prompt-claude-code.md + +## Rôle +Tu es Claude Code. Tu génères un projet complet backend (FastAPI), frontend (Vue 3), Docker, basé sur les spécifications fournies dans: +- consigne-parametrage.md +- consigne-design_webui.md +- modele-donnees.md +- architecture-technique.md +- workflow-scan.md +- guidelines-css.md +- tests-backend.md + +## Objectif +Créer l’application IPWatch : un scanner réseau WebUI permettant de visualiser les IP libres, connues, inconnues, états réseau, ports ouverts, historique 24h, configuration YAML. + +## Livrables +1. Structure complète du projet +2. Code backend FastAPI +3. Modèles SQLAlchemy +4. Tâches de scan (ping, ARP, ports) +5. WebSockets + API REST +6. Frontend Vue 3 + Tailwind +7. Dockerfile + docker-compose +8. Tests backend diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0d005ec --- /dev/null +++ b/pytest.ini @@ -0,0 +1,20 @@ +[pytest] +# Configuration pytest pour IPWatch +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Options par défaut +addopts = + -v + --tb=short + --strict-markers + +# Markers +markers = + asyncio: marque les tests asynchrones + integration: marque les tests d'intégration + unit: marque les tests unitaires + +asyncio_mode = auto diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..4914fb3 --- /dev/null +++ b/start.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Script de démarrage rapide IPWatch + +set -e + +echo "=========================================" +echo " IPWatch - Scanner Réseau Temps Réel" +echo "=========================================" +echo "" + +# Vérifier si Docker est installé +if ! command -v docker &> /dev/null; then + echo "❌ Docker n'est pas installé" + echo "Installez Docker depuis: https://docs.docker.com/get-docker/" + exit 1 +fi + +# Vérifier si docker-compose est installé +if ! command -v docker compose &> /dev/null; then + echo "❌ docker-compose n'est pas installé" + echo "Installez docker-compose depuis: https://docs.docker.com/compose/install/" + exit 1 +fi + +# Créer les dossiers nécessaires +echo "📁 Création des dossiers..." +mkdir -p data logs + +# Vérifier la config +if [ ! -f config.yaml ]; then + echo "⚠️ config.yaml non trouvé" + echo "Veuillez créer un fichier config.yaml avec votre configuration réseau" + exit 1 +fi + +# Build de l'image +echo "" +echo "🔨 Construction de l'image Docker..." +docker compose build + +# Démarrage +echo "" +echo "🚀 Démarrage d'IPWatch..." +docker compose up -d + +# Attendre que le service soit prêt +echo "" +echo "⏳ Attente du démarrage du service..." +sleep 5 + +# Vérifier l'état +if docker-compose ps | grep -q "Up"; then + echo "" + echo "✅ IPWatch est démarré avec succès!" + echo "" + echo "📊 Accédez à l'interface web:" + echo " 👉 http://localhost:8080" + echo "" + echo "📝 Commandes utiles:" + echo " - Logs: docker-compose logs -f" + echo " - Arrêter: docker-compose down" + echo " - Redémarrer: docker-compose restart" + echo "" +else + echo "" + echo "❌ Erreur lors du démarrage" + echo "Consultez les logs: docker-compose logs" + exit 1 +fi diff --git a/tests-backend.md b/tests-backend.md new file mode 100644 index 0000000..9d8f848 --- /dev/null +++ b/tests-backend.md @@ -0,0 +1,14 @@ +# tests-backend.md + +## Tests unitaires +- test_ping() +- test_port_scan() +- test_classification() +- test_sqlalchemy_models() +- test_api_get_ip() +- test_api_update_ip() +- test_scheduler() + +## Tests d'intégration +- scan complet réseau simulé +- WebSocket notifications diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a9c5a9b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Tests IPWatch diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..4cc82ea --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,123 @@ +""" +Tests pour les endpoints API +""" +import pytest +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from backend.app.main import app +from backend.app.core.database import Base, get_db +from backend.app.models.ip import IP + + +# Setup DB de test +@pytest.fixture +def test_db(): + """Fixture base de données de test""" + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + TestingSessionLocal = sessionmaker(bind=engine) + return TestingSessionLocal + + +@pytest.fixture +def client(test_db): + """Fixture client de test""" + def override_get_db(): + db = test_db() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_get_db + return TestClient(app) + + +class TestAPIEndpoints: + """Tests pour les endpoints API""" + + def test_root_endpoint(self, client): + """Test endpoint racine""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert "name" in data + assert data["name"] == "IPWatch API" + + def test_health_check(self, client): + """Test health check""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert "status" in data + assert data["status"] == "healthy" + + def test_get_all_ips_empty(self, client): + """Test récupération IPs (vide)""" + response = client.get("/api/ips/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == 0 + + def test_get_stats_empty(self, client): + """Test stats avec DB vide""" + response = client.get("/api/ips/stats/summary") + assert response.status_code == 200 + data = response.json() + assert data["total"] == 0 + assert data["online"] == 0 + assert data["offline"] == 0 + + def test_get_ip_not_found(self, client): + """Test récupération IP inexistante""" + response = client.get("/api/ips/192.168.1.100") + assert response.status_code == 404 + + def test_update_ip(self, client, test_db): + """Test mise à jour IP""" + # Créer d'abord une IP + db = test_db() + ip = IP( + ip="192.168.1.100", + name="Test", + known=False, + last_status="online" + ) + db.add(ip) + db.commit() + db.close() + + # Mettre à jour via API + update_data = { + "name": "Updated Name", + "known": True, + "location": "Bureau" + } + + response = client.put("/api/ips/192.168.1.100", json=update_data) + assert response.status_code == 200 + + data = response.json() + assert data["name"] == "Updated Name" + assert data["known"] is True + assert data["location"] == "Bureau" + + def test_delete_ip(self, client, test_db): + """Test suppression IP""" + # Créer une IP + db = test_db() + ip = IP(ip="192.168.1.101", last_status="online") + db.add(ip) + db.commit() + db.close() + + # Supprimer via API + response = client.delete("/api/ips/192.168.1.101") + assert response.status_code == 200 + + # Vérifier suppression + response = client.get("/api/ips/192.168.1.101") + assert response.status_code == 404 diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..2fcde53 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,134 @@ +""" +Tests pour les modèles SQLAlchemy +""" +import pytest +from datetime import datetime +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from backend.app.core.database import Base +from backend.app.models.ip import IP, IPHistory + + +class TestSQLAlchemyModels: + """Tests pour les modèles de données""" + + @pytest.fixture + def db_session(self): + """Fixture session DB en mémoire""" + # Créer une DB SQLite en mémoire + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + + Session = sessionmaker(bind=engine) + session = Session() + + yield session + + session.close() + + def test_create_ip(self, db_session): + """Test création d'une IP""" + ip = IP( + ip="192.168.1.100", + name="Test Server", + known=True, + location="Bureau", + host="Serveur", + last_status="online", + mac="00:11:22:33:44:55", + vendor="Dell", + open_ports=[22, 80, 443] + ) + + db_session.add(ip) + db_session.commit() + + # Vérifier la création + retrieved = db_session.query(IP).filter(IP.ip == "192.168.1.100").first() + assert retrieved is not None + assert retrieved.name == "Test Server" + assert retrieved.known is True + assert retrieved.last_status == "online" + assert len(retrieved.open_ports) == 3 + + def test_create_ip_history(self, db_session): + """Test création d'historique IP""" + # Créer d'abord une IP + ip = IP( + ip="192.168.1.101", + last_status="online" + ) + db_session.add(ip) + db_session.commit() + + # Créer entrée historique + history = IPHistory( + ip="192.168.1.101", + timestamp=datetime.utcnow(), + status="online", + open_ports=[80, 443] + ) + + db_session.add(history) + db_session.commit() + + # Vérifier + retrieved = db_session.query(IPHistory).filter( + IPHistory.ip == "192.168.1.101" + ).first() + + assert retrieved is not None + assert retrieved.status == "online" + assert len(retrieved.open_ports) == 2 + + def test_ip_history_relationship(self, db_session): + """Test relation IP <-> IPHistory""" + # Créer une IP + ip = IP( + ip="192.168.1.102", + last_status="online" + ) + db_session.add(ip) + db_session.commit() + + # Créer plusieurs entrées historiques + for i in range(5): + history = IPHistory( + ip="192.168.1.102", + status="online" if i % 2 == 0 else "offline", + open_ports=[] + ) + db_session.add(history) + + db_session.commit() + + # Vérifier la relation + ip = db_session.query(IP).filter(IP.ip == "192.168.1.102").first() + assert len(ip.history) == 5 + + def test_cascade_delete(self, db_session): + """Test suppression en cascade""" + # Créer IP + historique + ip = IP(ip="192.168.1.103", last_status="online") + db_session.add(ip) + db_session.commit() + + history = IPHistory( + ip="192.168.1.103", + status="online", + open_ports=[] + ) + db_session.add(history) + db_session.commit() + + # Supprimer l'IP + db_session.delete(ip) + db_session.commit() + + # Vérifier que l'historique est supprimé aussi + history_count = db_session.query(IPHistory).filter( + IPHistory.ip == "192.168.1.103" + ).count() + + assert history_count == 0 diff --git a/tests/test_network.py b/tests/test_network.py new file mode 100644 index 0000000..8353cab --- /dev/null +++ b/tests/test_network.py @@ -0,0 +1,98 @@ +""" +Tests unitaires pour les modules réseau +Basé sur tests-backend.md +""" +import pytest +import asyncio +from backend.app.services.network import NetworkScanner + + +class TestNetworkScanner: + """Tests pour le scanner réseau""" + + @pytest.fixture + def scanner(self): + """Fixture scanner avec réseau de test""" + return NetworkScanner(cidr="192.168.1.0/24", timeout=1.0) + + def test_generate_ip_list(self, scanner): + """Test génération liste IP depuis CIDR""" + ip_list = scanner.generate_ip_list() + + # Vérifier le nombre d'IPs (254 pour un /24) + assert len(ip_list) == 254 + + # Vérifier format + assert "192.168.1.1" in ip_list + assert "192.168.1.254" in ip_list + assert "192.168.1.0" not in ip_list # Adresse réseau exclue + assert "192.168.1.255" not in ip_list # Broadcast exclu + + @pytest.mark.asyncio + async def test_ping(self, scanner): + """Test fonction ping""" + # Ping localhost (devrait marcher) + result = await scanner.ping("127.0.0.1") + assert result is True + + # Ping IP improbable (devrait échouer rapidement) + result = await scanner.ping("192.0.2.1") + assert result is False + + @pytest.mark.asyncio + async def test_ping_parallel(self, scanner): + """Test ping parallélisé""" + ip_list = ["127.0.0.1", "192.0.2.1", "192.0.2.2"] + + results = await scanner.ping_parallel(ip_list, max_concurrent=10) + + # Vérifier que tous les résultats sont présents + assert len(results) == 3 + assert "127.0.0.1" in results + assert results["127.0.0.1"] is True + + def test_classification(self, scanner): + """Test classification d'état IP""" + # IP en ligne + connue + status = scanner.classify_ip_status(is_online=True, is_known=True) + assert status == "online" + + # IP hors ligne + connue + status = scanner.classify_ip_status(is_online=False, is_known=True) + assert status == "offline" + + # IP en ligne + inconnue + status = scanner.classify_ip_status(is_online=True, is_known=False) + assert status == "online" + + # IP hors ligne + inconnue + status = scanner.classify_ip_status(is_online=False, is_known=False) + assert status == "offline" + + @pytest.mark.asyncio + async def test_port_scan(self, scanner): + """Test scan de ports""" + # Scanner des ports communs sur localhost + ports = [22, 80, 443, 9999] # 9999 probablement fermé + + open_ports = await scanner.scan_ports("127.0.0.1", ports) + + # Au moins vérifier que la fonction retourne une liste + assert isinstance(open_ports, list) + + # Tous les ports retournés doivent être dans la liste demandée + for port in open_ports: + assert port in ports + + def test_get_mac_vendor(self, scanner): + """Test lookup fabricant MAC""" + # Tester avec des MACs connus + vendor = scanner._get_mac_vendor("00:0C:29:XX:XX:XX") + assert vendor == "VMware" + + vendor = scanner._get_mac_vendor("B8:27:EB:XX:XX:XX") + assert vendor == "Raspberry Pi" + + # MAC inconnu + vendor = scanner._get_mac_vendor("AA:BB:CC:DD:EE:FF") + assert vendor == "Unknown" diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py new file mode 100644 index 0000000..e5519e2 --- /dev/null +++ b/tests/test_scheduler.py @@ -0,0 +1,76 @@ +""" +Tests pour le scheduler APScheduler +""" +import pytest +import asyncio +from backend.app.services.scheduler import ScanScheduler + + +class TestScheduler: + """Tests pour le scheduler""" + + @pytest.fixture + def scheduler(self): + """Fixture scheduler""" + sched = ScanScheduler() + yield sched + if sched.is_running: + sched.stop() + + def test_scheduler_start_stop(self, scheduler): + """Test démarrage/arrêt du scheduler""" + assert scheduler.is_running is False + + scheduler.start() + assert scheduler.is_running is True + + scheduler.stop() + assert scheduler.is_running is False + + def test_add_ping_scan_job(self, scheduler): + """Test ajout tâche ping scan""" + scheduler.start() + + async def dummy_scan(): + pass + + scheduler.add_ping_scan_job(dummy_scan, interval_seconds=60) + + jobs = scheduler.get_jobs() + job_ids = [job.id for job in jobs] + + assert 'ping_scan' in job_ids + + def test_add_port_scan_job(self, scheduler): + """Test ajout tâche port scan""" + scheduler.start() + + async def dummy_scan(): + pass + + scheduler.add_port_scan_job(dummy_scan, interval_seconds=300) + + jobs = scheduler.get_jobs() + job_ids = [job.id for job in jobs] + + assert 'port_scan' in job_ids + + def test_remove_job(self, scheduler): + """Test suppression de tâche""" + scheduler.start() + + async def dummy_scan(): + pass + + scheduler.add_ping_scan_job(dummy_scan, interval_seconds=60) + + # Vérifier présence + jobs = scheduler.get_jobs() + assert len(jobs) == 1 + + # Supprimer + scheduler.remove_job('ping_scan') + + # Vérifier absence + jobs = scheduler.get_jobs() + assert len(jobs) == 0 diff --git a/workflow-scan.md b/workflow-scan.md new file mode 100644 index 0000000..593c759 --- /dev/null +++ b/workflow-scan.md @@ -0,0 +1,14 @@ +# workflow-scan.md + +## Pipeline + +1. Charger configuration YAML +2. Générer liste IP du CIDR +3. Ping (parallélisé) +4. ARP + MAC vendor +5. Port scan selon intervalle +6. Classification état +7. Mise à jour SQLite +8. Détection nouvelles IP +9. Push WebSocket +10. Mise à jour UI