From 22a1933b1e012602c817817f4583697e25028382 Mon Sep 17 00:00:00 2001 From: Hamed Karbasi Date: Mon, 21 Feb 2022 16:23:24 +0330 Subject: [PATCH] add variable support for query string --- README.md | 42 ++++++++++++++++++++------------------- src/QueryEditor.tsx | 5 +++-- src/datasource.ts | 15 +++++++++----- src/img/query-string.png | Bin 0 -> 23440 bytes 4 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 src/img/query-string.png diff --git a/README.md b/README.md index 36acfa8..1740067 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,17 @@ [![Build](https://github.com/grafana/grafana-starter-datasource/workflows/CI/badge.svg)](https://github.com/grafana/grafana-starter-datasource/actions?query=workflow%3A%22CI%22) -This plugin provides a datasource to connect a REST API to [nodegraph](https://grafana.com/docs/grafana/latest/visualizations/node-graph/) panel of Grafana. +This plugin provides a data source to connect a REST API to [nodegraph](https://grafana.com/docs/grafana/latest/visualizations/node-graph/) panel of Grafana. ![Graph Example](src/img/graph-example.png) -## What is Grafana Data Source Plugin? - -Grafana supports a wide range of data sources, including Prometheus, MySQL, and even Datadog. There’s a good chance you can already visualize metrics from the systems you have set up. In some cases, though, you already have an in-house metrics solution that you’d like to add to your Grafana dashboards. Grafana Data Source Plugins enables integrating such solutions with Grafana. - ## Getting started 1. Use Grafana 7.4 or higher -- Download and place the datasouce in grafana/plugins directory. +- Download and place the data source in `grafana/plugins` directory. -This plugin is not signed yet, Grafana will not allow loading it by default. you should enable it by adding: +This plugin is not signed yet; Grafana will not allow loading it by default. You have to enable it by adding: for example, if you are using Grafana with containers, add: @@ -24,25 +20,25 @@ for example, if you are using Grafana with containers, add: -e "GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=hamedkarbasi93-nodegraphapi-datasource" ``` -2. You can now add the the data source. Just enter the url of your API app and push "Save & Test". You will get an error in case of connection failure. +2. You can now add the data source. Just enter the URL of your API app and push "Save & Test." You will get an error in case of connection failure. - > Note: The browser should have access to the application not the grafana server. + > Note: The browser should have access to the application, not the Grafana server. ![Add Datasource](src/img/add-datasource.png) -3. In grafana dashboard, pick the Nodegraph panel and have the graph visualization. +3. In the Grafana dashboard, pick the Nodegraph panel and visualize the graph. ## API Configuration -You REST API application should return data in the following format: +The REST API application should return data in the following format: - > Note: You API application should handle CORS policy. Otherwise you will face CORS-Policy error in Grafana. + > Note: Your API application should handle CORS policy. Otherwise, you will face a CORS-Policy error in Grafana. ### Fetch Graph Fields This route returns the nodes and edges fields defined in the [parameter tables](https://grafana.com/docs/grafana/latest/visualizations/node-graph/#data-api). -This would help the plugin to create desired parameters for the graph. -For nodes, `id` and for edges, `id`, `source` and `target` fields are required and the other fields are optional. +It would help the plugin to create desired parameters for the graph. +For nodes, `id` and for edges, `id`, `source`, and `target` fields are required. Other fields are optional. endpoint: `/api/graph/fields` @@ -110,7 +106,7 @@ content format example: ### Fetch Graph Data -This route returns the graph data which is intended to visualize. +This route returns the graph data, which is intended to visualize. endpoint: `/api/graph/data` @@ -151,12 +147,12 @@ Data Format example: } ``` -For more detail of the variables please visit [here](https://grafana.com/docs/grafana/latest/visualizations/node-graph/#data-api). +For more detail of the variables, please visit [here](https://grafana.com/docs/grafana/latest/visualizations/node-graph/#data-api). ### Health -This route is for testing the health of the API which is used by the *Save & Test* action while adding the plugin.[(Part 2 of the Getting Started Section)](#getting-started). -Currently, it only needs to return `200` status code in case of a success connection. +This route is for testing the health of the API, which is used by the *Save & Test* action while adding the plugin.[(Part 2 of the Getting Started Section)](#getting-started). +Currently, it only needs to return the `200` status code in case of a successful connection. endpoint: `/api/health` @@ -166,7 +162,7 @@ success status code: `200` ## API Example -In `example` folder you can find a simple API application in Python Flask. +In the `example` folder, you can find a simple API application in Python Flask. ### Requirements: @@ -180,6 +176,12 @@ python run.py ``` The application will be started on `http://localhost:5000` +## Query Configuration +You can pass a query string to apply for the data endpoint of the graph via *Query String*. Like any other query, you can utilize variables too: + + ![Add Datasource](src/img/query-string.png) + With variable `$service` defined as `processors`, above query will produce this endpoint: + `/api/graph/data?query=text1&service=processors` ## Compiling the data source by yourself 1. Install dependencies @@ -215,7 +217,7 @@ The application will be started on `http://localhost:5000` ## Contributing -Thank you for considering contributing! If you find an issue, or have a better way to do something, feel free to open an issue, or a PR. +Thank you for considering contributing! If you find an issue or have a better way to do something, feel free to open an issue or a PR. ## License diff --git a/src/QueryEditor.tsx b/src/QueryEditor.tsx index ea9f7ae..fbdb1de 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditor.tsx @@ -23,10 +23,11 @@ export class QueryEditor extends PureComponent {
); diff --git a/src/datasource.ts b/src/datasource.ts index fc57620..98178ae 100644 --- a/src/datasource.ts +++ b/src/datasource.ts @@ -8,11 +8,12 @@ import { MutableDataFrame, FieldType, FieldColorModeId, - //QueryResultMeta, } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; +import { getTemplateSrv } from '@grafana/runtime'; + import { MyQuery, MyDataSourceOptions, defaultQuery } from './types'; export class DataSource extends DataSourceApi { @@ -26,10 +27,11 @@ export class DataSource extends DataSourceApi { async query(options: DataQueryRequest): Promise { const promises = options.targets.map(async target => { const query = defaults(target, defaultQuery); + const dataQuery = getTemplateSrv().replace(query.queryText, options.scopedVars); // fetch graph fields from api - const responseGraphFields = await this.doRequest('/api/graph/fields', `query=${query.queryText}`); + const responseGraphFields = await this.doRequest('/api/graph/fields', `${dataQuery}`); // fetch graph data from api - const responseGraphData = await this.doRequest('/api/graph/data', `query=${query.queryText}`); + const responseGraphData = await this.doRequest('/api/graph/data', `${dataQuery}`); // extract fields of the nodes and edges in the graph fields object const nodeFieldsResponse = responseGraphFields.data.nodes_fields; const edgeFieldsResponse = responseGraphFields.data.edges_fields; @@ -96,11 +98,14 @@ export class DataSource extends DataSourceApi { return Promise.all(promises).then(data => ({ data: data[0] })); } async doRequest(endpoint: string, params?: string) { - const result = await getBackendSrv().datasourceRequest({ + // const result = await getBackendSrv().datasourceRequest({ + // method: 'GET', + // url: `${this.baseUrl}${endpoint}${`?${params}`}`, + // }); + const result = getBackendSrv().datasourceRequest({ method: 'GET', url: `${this.baseUrl}${endpoint}${params?.length ? `?${params}` : ''}`, }); - return result; } diff --git a/src/img/query-string.png b/src/img/query-string.png new file mode 100644 index 0000000000000000000000000000000000000000..85d40de93ad5e2d824c46eaec4c5cb976e316fd0 GIT binary patch literal 23440 zcmeEuQ+Q=vw`OcqDt0BQSe2w=yJA}v+qP}nwr$(aj_qW}PUrjnbNaHM(|3K*`(o{} z=Cd&7JNFuV*AA7H7Da@^fdc^nK@|HVBo6`tHuoh@!$5s)S2|gSzP=#r1;i9#zCIo> z1|eVj*bc&~4hq&r4$iuEh9JgP)|Q5}_WE{)hF10_)(%(T-8>*5KS0EU_!V6;&NrMr z(3M`EfYa+~>vvf(0^ikrzHtO-3pV?NY00V4Y@eQ5J1=TEFK;xSx3#<|(NOrI6~e&G zxnD8!`TUHL{yyh!WjB5m%?DQ+05a>jS-tGZf0)H?Z*;kyYCOedU$wh?YA6gt6!1*| z##TYmxgAvC-<`MbVn1mA86*;7`tyI3OfH?kKbBY5aDHJS9N)YBPJ7`+iXRP_H7RIm zNr{WwDx@$@@b5G;lwD8tfakYn`22^kpZEdmTAQwl@&AT!p^- zq^^>d8=yQL;H*X2e7o{6C5mBf3$pd@8bR%6B|2w>W0+c zcF$+6FDM|u_5Kp1|2ruzD@*yg>q{Aw{Ndc@TF}2+n_54A4za2hFLC-AXJZFwa9#se_mrtrLRM~6TZvMsIgkZ zv4(P`wsQH=>gHw^Wjx_wA*K!y#g2pghaB+fZ6~bJ`N`Y z&>!z+>RR>7dShH8b=K0kpo*stLR{={;~&o!k)^8E3bxYWP2H5ggR&wBRw2hz8)$gidV<7EF3NFAvQLOb+#I-&Mr0Um{X$(SAT>QPw)e&=U zI@|3W-%Ee|)z5y=7jEv*xW#&xmv9A&7KVfO1FkLFFqv%Ez-%TOL}pwK4MG3sGAmtV z8|xP2GCL+SN4TT3I@&uo3robx!-v=DGq1kY(bk)*7rVS>E5JZO=p!n&Ynkv(5g^(% zkkZRkDD~}lH*v6Nx@(W7>yD5)NgLn8W-evq58GJOJ!gxefw2C3asaR9RX3roPbxy9-v^zFa36teHnt1&#Ta!MmJ00fVmE&nwrv+C=yJ6D(4TuI2Ew{)?wGLlayXTuM=Y_t-LE+P-0>@2dPEv}jd$Ls z*&Qsi<#iEyfW$7XaJEK!cygr6LlcarJGkObsNYVVFlQyj=Y2#Etlx#eu@Hiww&*80 z`l}YVZsvjIkxPzD_nVZ`%V5U=15aZrrlylsQ+MU26R!{BV+qksmf#u|&p6257Wyo8 z20)xVi93h%C$^o-Q40-@V0w%Xp*k(LM-hk8HaEpq6r4l;x2Y?&77r#PY}z#xGl}u~krkdG_Pa-K4>z%!Re)o83<~iwu%Jk3GY#HQ6K# z>4&%Xeuj9|FnZ_xkF%pqdOC_T+n44tH&s_^Pq(%=RZ_Pn--LQ&4`?(y;^G6n5Zv!v zMcfN(%m=wJDmjD8JRwjv@Y5R*aR8X>fEQf_kJNZ_Iy?5mU&W|lwkGo!k0MbJm4^Q@ zy78QIBpX#)(Apk5=wvdj&pEAhVU7 zt#YI$4VX4hr`&y_2!melX<;0bN2+hHn9oF&#GVCjCS>}=eJ5x4%xYab4$c(h58F<4 z=`)u@A~F=>p=3xl52gZ!ta4YG-%%Kla61vhlW9ttB~48!3k%Ur!9qiUF0O9PD>vf` z$aOn^(S$)vS{feR<>3@YX&6D-@p!YvOi~D~%Ipn5RUCujA5tb#4Hl*O&t@zKh(>q_ib;2%!TktaiO*{m%?H>)S&aV8cd0XeD-(n*4cqq9PBquo))kix&A%4*mUmGYx>Z@P zfAd;orKV?PP?wg#IV+(QaTXwfi%xE+g@#JEDHCL}zfFAJTEH_Ty)$f?jJ~^3rb#du zvw5CF6yC49$g)3eSJ4wQl%DpAO1LtHmj0R(Q@# zkdFV9NnXhUAa{?z1`6X0>UP&h%>JiSHqYt*O$O@}g=Joo*V6E2C)mN>9 z(<;$1;Y&c^P0$b#x!`HOrV#@5Mr>aYne1fC1&Q|}V^m?iNChFCB9xMwQ9|NA0yQ>1 z2EzUDfX=-fHkGSBCW<{OrowmO-%J;(Kptg(I$%q;k3AYCQ`ptpC3!&3=I|TUa|LY( zfj&!}s&3b9@7=Gp05(fUP*M7Q7mfEs^Xeh|Xr)Lav;q%vSrF^L; ztVw>1-5~`K5*;9#Z~o$#uThK=alRPj*TRIi))h7g?-aAQm|WA-*3Fodhfo(G?eeS~ z$&)|#g0cGSVrsu}l>R-RH?~(23MIBscQA=4h&zf}5 zHjhLOwX-58mnd3V=xO2IKXG)(K zF&<@u9)KeHk|KbB=Sdtww}|7g&4#3WF;X^1guYmTyfbY2wXgduj7k#V{z}bt)nJb= zEbh+KIgn(Ga;x|=(B!<6n0k25g#okOo!c_b>5Y(o|094%XIinT*KwmoD5?CfCU zT+;M6cDuYS@B*9X+-cFhzOT>i+qZ8akzJP^M|XF>mnuI)<@JTzL_H7G{TAbfeD5{> zFy8HSi4qulIOAhRXA_l6`gg&`I$X9YRjhg^vIu4y3`HEZRyX^m(3Z2=HGWh95;`SR zJ=-#-P_qsOjR}jQJ@L|$D|hrpGsiPcEOaO&xF`k5MvsZKW{dE3r)OV+yUfix zzasO2PvXJB#j~cYgJMb76il5bTE0&9Ie1LZ@iL`xD^vEcsdR?=kACVszmctbmia9} z@1(H~Yh>gjiO17D&BAyQaBla-t?KOX;9w(<wozw0Kp$Y;py8RUqRr*O|JW6SaGLx zHjkLP8x=ItdF#pM@e9Su{CyXtONQpq)Id@CAItxm^IfZd z%z1U7_+k<{PwI64CagvJtMNxWbD`S(${Nn~^>xd^gMop;NcChW$LN1TRtLH{$orX9)X= z9L?5(_e(3%Qb_yeNeVtR`KbiBQzZX=aRO87B{SdB`yf8GR%Lb|bFze0HHgg`wr`T9|bs zE_hH~?R`+D)YR_uY;LYn-eKo?QD!+~hFG|BBd&Fjh|3dgc2M=1u4Q%DX>aOy0rl?^ z=Yfg`OXtqvQ~ryYSb$el9P2A6PHu0@|%1A)`%94+=rWF(kR0Z4hAORNSI4D*Vp&2jzT8BOkK8P z)CaD2;Dv>SnMo$=C;a1-a;EsyrDdB%bQEKp@+YMMdL*+qO8fSJWva5YLmU8pQK$1OukgqY1!V#0}Z++=9Q<#!Mq zPx{a0`Row>g*U(i>VH3F|5GW5V>lM0GFseRqMfLll$SZ~?iTmT$v(g&AzKH;tLx+G z6%r&SA>)so`1jG#Q0G>|uVob)k0ChGfyvCoWtr+?YrfbbzAshMN@e#ZJo~u923ug> zg3s&EW$v+7B;F*O$?gx06Wc6tv!;YYc)QG0D8HFkq)Y2D@i;;ftM^ocDuo~jKNPCJ3EITM#J5;_U6$#SLA|%!7lXO_B5ZM=ZeQ*&O0SeDm95D>?-jCuoTG6vIy)v|=-vD>x4YDY`Z^D2gy&P8 z0I3Qhg9X{Q#Rs-XET8wNNVLuCE@mSX?P2iEz|3A>*wdm zWcS|T=~S{k?}5X9D9)85^4Z`Q7{#Kh^qcS7)y}1Xw1UR0sa`T&{_Rp^Y7Dq7W^hC0 zdn9+BdGKq2(T)o(w|4WF)ID`=|7TjT1Xpz+(WKxg%Ud$uvT^&XU#)xhh?*RPuiXZNQ>B*Q#ah*E$d?QF(?Z zk12d0G!g?0aK0JJVT?_7Nx1+-$C*{EOo6_W{bH=3g_5FFvkhMDy7-B0B-=o6N5CGN z#Xof{>H0Hsv-dC`_U$HGs1iVX|3_!@28Ga*Cz%%W0*mpTu!DsmXV&$1m2O1Z@6-<- zmdGS@bf_)PaQ?@Myx**;6%y?fV}ar4Y{=Cih%9%EP_+jyCW6#C@}*^ z+RCwsp0xEr`=hbF>|*w-$m>QR#+DZ{_Y;;?)n&(PuMU;J32!&ZTUDWRMif`CXtsw> zdOCDG*}C^IV7Xq@`p)g0b~!(1*!g6=-(^fP#Q~m}!AC(lVU)F#KvM#(+0Q z&Xan|%PgBe0!E3g$qp%dT1POhpD*aqt~#aqRn-lkvtR5=$mF~TtGFRDw_NO)0q~x* zItj9~dXq!Krx0Pc@%g=B+`(l^#ohD~tYFB&5s@`$O%{xLY(V_p$8zY&lMR2_t^2@< zdd-(R`Kb|h>pTsduA~a$VF)i3?7x;humf&;@MJcpm$Pb`0-z#}p0O29FptBSU%>#$ zo)gWl3ZOPu?2)O_qBFA^qPl&$%3iOO8g$sU$Dhz$9;?BGpHCd+3N#SBZ0V1~#f-0m zt7tOse%%OEJDN%4_zmgLdm8Yy59z%VqN4M?N<`dFoZ&p5VI(S*6zV!6ytf<*&=C3* zGCNOcpEoBjs$ z0`gEYN6*8oHV#uLW2Y3wO=n~N^qmvnU-?fl-OrsKd7kuJaJfKqXu-rLT2!5PY8j-n z5G`E<(Fe`a*r43MGFQP{1~2=d4fXWoJ9kuFFD`VVOlE|V{;zifq^{(zbV?T>%5dy& z^8!mqGy=s?iH)?=o-End6C3+bNcjrA#M)`>gl93S9Zv&TZV)Z{lc7deao*RnFXIVv zv28%7;du+%)7}s(%NdNJn#Wgf;OrW_(pd%kkKIOk*T1S1PKOZEbnlqnIs|G*ETAIlRuxDSQP`kawJGhlcZ^;a6Bcgw^nZ0m2q0P8Bscguvm^0d z?hvcg`SdvhqVkPU))}xW<@rP=oHJJM@LdhJH|W28-YL1+2_>#DZKhr# zF198Mv93?MBlt<)S~`rlil>&(ITdYsD#yHdNDUMHUKDFB* zgyS*F2Rj9g7nacEqr0X@J>6|C+9P`NIl#BMJ(wRGUPEXlyn5UhMBE-m_t)2hLq}`# z8w1H2kyXrN90Y9e-3i%QzpZ=qpsI7navySI(3Q}?7Mntoy;GB8CrjEATL4MVc-jIu89WHA2(B`nCD-;>mPm!L)?e*AOR-vNF zUr`SU6zW+u4*Gk7i)JQE)j{;d^p5hFH9K^Z=i34qJ??(2wXO*05)U_7ZmX(O;$+8i zF^CL42m}7q`AD_9yR=!;Q9Em3G1Vdys3Y=3bNkK@4W7%`lNy*n?BI7WsNVeHsQ0%F zMy2S=oqUmgyq>6OVlFz!(wdyZb&IOc^Ey1+6E@*o@1N&3t8N$Hlj#Pv(&WhGJ2Lk) zRt{@vdB-IVkA#SFiRxuOocXp=4&5|+ri#7VVXri=cqbj4kdZgRil*Zp*s+3NvEc^W zT|1A3VG$kW?lR?s9L_2v?^L<$Y-4QgV5p|acFhE!-_HYLjpaW!6#kgz!+mve65{wp zv;94MDWd?(=HeL!`B}8~ujS7mBob`)8m7%AIN=lHl}1-qAgk4r*+NujtM@4gSJ{25 zDihLe|LJMx2hRCD8C?dKkURt5_oKaf7+ZTGDV(YgOIdP4BC29M>e7s^80p7{kK^d1 z?Xx41)4@c81n|`(wrs|ftPiW0G>H{%rW19iEXJ#;C+vuV?5+OT3BJBp#~wlBpTIj2 z7QBk%&L(86ffZfuYo`Uh4xGVFT8-9}$FmOqweRXbMc2epileiK_rD31bi;Yh=uc#@ zfb983KOYUBQNna2V-`R3;xz|68d;O}YYv1y)8C$CNxltzMq%f#hrC@@zqucfc8E^w zuJSW8g`;s{KTa^+$z2@8dv{F6sTvPSXZOO0LRKz&PPjdLtyfp=j8AtRG&B-UcfKHK zrO)kLoCjLY;b2s4%H$sTPk7k=Zo>55&Y#ANKW2;D`Y{5M8gY@Ml>S6&h7QV$42LwE%)jRvE$b*dq&!O? zZU6cZCNS6GELN*^v;R!5cdgSx(D^t~GwQlWx9y#OKGJy9KEq#{-COm(wd*Ek@pvMx z{|0Yao;>dYxMXQ%GziyGZi`r5 z^u|0p8Y_0+O|{CZNbiaCv1D7)9=$%n2(9RHh85~thTFTQ)kKd+blI{2{hJd^cwE}z z(Yg7!leNmF8#az}atYZRe@1l(KFvC(mJ`u1Zx7aG)M{^_MWRAVGQ2t`?p-ev63dhJ zj*kbx1A37r)NIjI%ZfA-snLuvb4)yJZA?1I{}>|XjweJPuZYR40RsoJ z#CqBH9bY1`wOnnK)i!{~D^Y&qfL1$0WNe_wLY5$~0eo-`Dti0x#E?RPRvQqCi_Os` z_*zWiR{|lC(UrX280Y-SCVK#s6289Di}XtQqpj7gh5Q^I5ciEIHNx`S6-fbU zUu5!dC5yX}0~rnP8ux?U6Y60&qb>~|x>?UQm2Wzjw`aMkEQ8L5akFh+lcz09zz-IO z8QyxM@xJ?t7ObPbrQ@A@imv(-L^eJ2{g%)u_B~Hk! zi8^bOS%yVff@*k){_wRhyi9wX6~$ffTjkfGtZrBjwY&Q^4!pF8@(iYBD~io&R9n9^+be9>hpPg#KocTZB~wHMtgHo zy?OU8zz~`e7Uxfj47OZ)x1PQtT`Q)S(Pq$}2p{KccdX;AzP2kK{hdPyE@;u2$&WQ> zGg()c?%|fI2@v|COlhDRMwM zm|}TeJHMh^Lp!LNYH!%5su{LfbsBzbLb2U*!|w6ECsskU7h;m!J4JYSeLpR?;z0NZ zlN>T}S7r+(HvvzhPesRTa+Ah82>D{7a%=eY-mFz$$&_7qMc24kHmNnydh?T{KACWP zqBfh&W;Com7}M!M*udaG9tq9RFh&D`ogCx(gZE>q*WuzZ1w95X1TfOvovT4U}`D1%$I69BrrJXIIv)MfF14v zH?ig!9<%n|o$+O!c(-Eb+=Z)I4r0vdW@QiVHlAJbKUZzb(0}dD7GXsF7whvFXAa5T|AcKs?;J`J!8 zhEACw2`Bt~W#F6|M)kJhw&zh7lU`9Af-Idiac3}VftI8GnDpB)4q?KyX`?4aezTG#1M4Mm%d8p8+V0@=)nzi%6WjYFf2WqK!4Y%YV*2LuEO&A zj1{8MmYzK6Bg!7y*xur#*dY0Ju4!vv3=nH{W;$j4l+a?oWqVj$J;K1gJ#PqexZbHL z^`{psL3=~gY|hZ?JbunhWWK4GeyD)`T<4xZ?)7sh7V(S$H|X1Xr@dk8&0Qd#`aQwX z<40r)6J?1*FI($sjK~tYxyvIlP7#u^ObeM>;hDO+;o+N;t5H%BayT`RJl_$? zOGu&>;T;JSk@JHtk0p`WGH{^gW77!NYEC`;_{A5jhbrXbPF0&MQIjxKXMRtGRFt5{ zY#f`C5u6#X(EnOFC};5XmNM91uE5WSbK|w`qVb*Xz@p%W-xX%%R%dy&R-jBcOF% zLNsZu1v_t8krR@OMGqUdJ-g+aHxArB$q~(mGI!V@PmKG{*+<}>rNZ&LQ?Trh6K74K ze?XOfI5~LK8l3Wc=~B^}%--OJq(fG?w|@*Kd^AE)#Ws-6G&kChjcFXHF5|`AGdhbT zzB@`?T|$0!7wzGW^E+*pjnij*o40=d<(-OopREN9mbE!DZ4Kk=VR|=Y1{rRm{mJkG zGfUoVU$oNKOkMwcjm**4F1S!=5{K+1pgmMs2%|~^`*t@d9yIM0hfU&KU$*An#BT|t z3f!Fz z0oeu)7sn zYOZ~*z0T}YHOTq z`_`g3LGs?sF#%dgCn?Nh1zQsacuI{WQvwQ#7n=#+EW3b6zm%D}nJBv8Ux+<7N<6jp z>YHBq`o(~@wOR?eg}No<2P5zlw$~=(g|uY+`Al z)Me4TsIg{**hI<{m=X^d9T(SqZEX1JpMPF4KF~SsSAH^~y!1F%q%p#j^v)`hezl?w)`V?Vuhp6Bqsl(~vX2smBr3qVA z6TzF)sJ558XB|$2py*zw8|*R@XeC4VQ@?0BRn|E@^^Xg2UukeeGjwK@aP!oot@j)oUfa&qNEL6v5`gGZ2rgaxke&c`#M{=ttMu}xY>U%VY~ zg`zl}nCUU4@ieXIRV-0kShg0(ogBXo^Y$h^OaI;t(DD;BNJwQH(+)an5&6eXF$X=R z1jEKdIYlr>!~zxVrr?P2!j>j{0a;h)gc}maB1grtGodwsN8-w)FqY+)m-4hG1Zv2i zS8gd8h1i3PgqSsrjneVUC&5|Uz`Ct;WkaPvXsB4wbkiLeRPtR}|V6(EH zgGpm_-*k>FstOk0qSR+7P39Q2+5Q~+20=^7qPw8!4XpOSKuXq(n@A~dJ;e(Y@sw^Rx{vAGv6?gL z*;sO;mRj9S4!tvWXjRI*5~1LD?iq2$wF)#j`MBXyt*s@uYq`Czu9&t30;DefvA(6C z(>h%kXynxoVz(2v7B>{kTd9@@FF(<>-yNFvAEryRowB#_%(&(U_29=`FHEKZtPJVN zc4Wn6kzbSpi**EaR%_5q{^+)b7-9v2K~fwpM4TMFnH<)YzZ0#k#eV3G_)}$y57inI z+Ku?V;ABj01~T$0OZeyE8eAD1&<~_sTz=3-Z~jM6C!yNSek6v<4J=MR7|AR|ci2pk zx)){m5n+)DxxLN9-vl}vO)ML78vCoY=tX#b`BphXtWym}>&kRb0_b^!kgbEmJO{uI zjHpdTk!?2@;wigbLx^_vTyIKsTaNGyhFiZ!l%DMGh<)1T!xBFIKt`}oxoRuOX8YsU zMvlzo&b#wm$!Qm;c#hrX}`QrP1KCD9|M+ zLbR|F8>y_J3Bi>(o5Ywo(ixjMHcq15@HOR*jL9X16Cs-!u#;&lo6pGOwmCFf5W+9rk^^=@l$+k2>7! zLYGm&W!e*qh$g4a-@%pG%eZD}QAZNYR=lRfaE0Z)TOC5SW( zK68ax`$|=OITc*X+~3rxjCU&>0@AzMb>AQ3(qn^Luk|7~2Tm&tV!d`?lmT)OPPW4d zEz#wU9t%ciu5k5{CF|{bc9pNP`T#=m=CJk4U9OmFt*I#&bpY)a>RNt z6g?anvuw^35rsW>r-JODBOjq)TRGC;))!G_?xd-p*m-lW~ z^AcUEh>3UA50lrgMnp#9yP>PnVh;=ozVhrm;quM8f8#K~(#kmPvZ+HG_^z^$WyQZb zpk37>Q&3ym4+V+F>|}Lug0U~O=NV5H!RiG?+SnebghUj9y&PE4S|J8S|&s z+PQDNfx?Zj&GCVlgp`0pR!RVfKNHRdWlEPpan;Noy^hR-TM36 zM|HG5ZOcUN)luDZS)$$2_39DbgvT3?c8U^%*>FenkzEy9!T@5$%38W`mqJoU61#n83h+3;gQZ94B~Z%a`5wcr*3M$KTQ$csYc%@JS8m_#c?FJTaO=b0 zN2Zx_#l2y8X*GJTqbvZkvKP_b74`zm)5gKNza)5T_Z2z2(yUj9Gsd0^n459UZK#E3{yNWaQ5DQVX+I zKF*GiGA2`_s}J|-aeOr~mfgQL;^8McvA^0|`}STa6WY0hC2wa%xpj#(lwR%mXU7lH zf+IO}vym2j$Wc(mp1t!UlrwsUzJ;m@1O z?kxO)Q*}Ul^@`RVb@h$S6gz0!5-k<5jn9aNYyUI0U0DgQ+m23F7LGtmC%+g76L$9P z&BNE+xj`b%>j%5z?_S-jUqTz15f;!~0_1u+Y{`}qT~*G{!pYQgoLmkfjC5Az_7Ftn zc7gjO?yw$ze|Hvn@vRRc;0YFOS55EF)jB3(I^q-(W)^q&Yf19ZX%Y0wWs1G6a;t(! z*z%CUw>YH=|2d(e%jA&P$37^s{F#x(pWVd-+dUlFAPK#8mlo2t50fmD4UKb|SD%Dc zi<~9d{zK;ux>>I5BB~xdfpih!?keEsiD@7iN*C8Y$-^ug4}Ae5!X;EiL=#t(!-%{b zS%~?>FP|o3TR34-DL0@lV zMo0hNaq(~l`C}GLR=diyU$%6djS1~=aHQe7!uzZmT&tZfG623pcDSTY`KAReSz5=Q zCb=q7*)W2)C)a5Y`{g*i1Rgb1yh|W9pMbOF010)Az~AKPJc3e>gJ{|MCKYwH)dc-$ z9=ZN@A3{VrYm`rl(iI53;*qDu&$cocxa^bH zPLwE`ThpVY=eM>gxH>KlVQ+jdQs7)DtK#ClQV?*S-Y$f!RdOunxL!Bb0meA7(|10R z){@C`Ivw;T>m2!d;ny-5quRSr>UchbAKg|VdoTOGYJ0|u20IQ^(fBHG?g0sKM>dnQ z_4qTeg)b&&inWhUU^{`$DkE(T>FC+s17js^-DzQK>DR%=*T+x34bILz>MnsP^8nRa z8_-wO$Pz{L{;hj1`9-I%(`zt4ha^yPTT;HNM27g3mL%7a9?86KHeGs8?NvvsZjIa| zZqUjI-p$8BJ3uh+{nF27KMTI{(Cz7Ro#K5Rg{Qwdi>30tS+(Bky)?)g)8)%ej^^8ExO_%VlMAHYpGCZAsz84lE01Oq zZEoI?8$7jDwT{;hoit5jdHKmJDRXv_lT7k&jJu|54TwX^;9(-sz*Bi7TF>YS^Y%!M zqwZP#JE07V@sVg9nOrBvtV@77k~gaxMlU``z5;qoaz`Y*#uf?bc!>M7FakcVVWkzb!qegSy9~G^Sf8>qp-jBNS)@ zo|NMGURcbee^>&%aF~i(=L-i0c9>>U+@ktEdU!(_?)0Y9E{@4NdU(O;ftN=bt)6OY z-8`&NV|iZ9IDfz;(`>vCWPC-3H9>OpgTLjNah;^ks)L`LewERWfjpif$ij);3GZ87 zqEnV_Z1U8)4%uB;*>8`lb?3oGk*40Bcqdbt#Ox1SCk5-Vw&{W2c9#neL@vJ33T!Qf zM?txZ$L}$KK^pIwd`=|aNpKB^h!V4XJeyXnA@66>i|R#85*PaOYK2E6BwOe&WjBp{ zW6o1jZQdc57S8JFUaQ6-;ONLM*dO4`nzAicrQAL(RXZO|iz=QR0$>QWH>3-<6QBa^ zsekC|Y?$JIF^et{U<)*`JIe{6wHUr$)!^rzBTSxoXhAVtzuZ?cM(mTxMo$cka>FyAWUzsTs!79@OD+DKEG3?n$_$UR2%B#jJi{q>Ia^s^^y;nkQ3T+)@0jwZqzGHuiF-Tw6+C8l2?oy=R&qw2%GZ65 zs`q@AlaFL=)w`Efv|?L1vL`JLJy_&H@HC*}N$xT}$&DC0dFg@Wa+EPfSpHy1G>CU_ zgHDeo`wOAz&eJF873*MV^s<%kJ?QyGn0sy zy!+&~#1qTr3IZ%Fy2faBctWiv2uq81DByKtfP-1`;5aCsA$x+@JbsMR0jap&-!DF0 zG&0MP6H`VfthcsWqVf&r#x>*q~b(|@(5CuRT*t@xMfP{@fVj82Cjy^vH zJM}XhQat;Vn+waDK%h0^5`l|;+gwHVrwm{5$6a;eL1oHuJexWWvGLPt*eCy~=P*Ie zP>ctnvGoywv*h0-t$wrnP_KI?R%d-&=5)*$5|iiuR>S(DK5a=Q`e-q{Ysimi0@&zp z@MS4Tbl+9#NWNk*{>I^f#tmr8XEL;PdNgy=p%Sv3&wR~->yNYOn;l2=XNbx?W3ge_ z3L6?kC(jRjKrLdJnoh0T-+1fTG04P(rAp-Oba(sieU3nf&ka-*);fz_-rn66(l+7{ zf%gf`i@=(Ld)YRa$@?2#UXS=J=L(`)`MjUpvb|HqCqED7@()@eMP{)CCt2KXw|x<6 zE-miL(ziB``>Dm=g3|<@8hR*v!s@{)#UCXRqJKiwWJEkDS$gw<3ymwpmoR`HB$uNB z`_dZX76nDCmX!lk=O?ritK0xrN{W@8T|F7EDJ8T!2;|Oe-zrKAjW_IDR_!6j8JIRU zsuINy1Z-Zc*8r)B>{n)udB+yBo6*twJUbY}^A)-TJhdT}EgF)cE<%6K#q%ts8WOgV z$b}Sm<~c%|RZKRw4>+Tq*+JOGvVfGzt?R`#3J>TF31`Qj84`rncH`*D^Wo=f@fEW5}y(n(GuY=qn-_p612-l16B?EGb8uK{-HtV9W?%l zc360>ceq?ngf5aaJSaN=;reAr^$htel*tSD9uC4+jvMJ*13vWBsYy%SgXI}ONA!Mu zvHfSQwWyeJ-Ez~Ci&C6d6}zQOlgqYdKPKKQD4iwM?H9HH-z|mDn7l_gx?I+5kDPcStQY|=Y z4we4)y}P{s1hn6_l$^dt?^R>}4Z;WQg|yjno<(b%5wF*5Q=zS=X_TjSuGDegs4`49 z4R5wGH8N~9P*2k+EiEZcg{P`2$?^G7$3z1@@%F*T0GZ5L8q%2^4ef2icI450G;y8s z=rMidA%iac))lcJATwvzAt9207zFa>!DZqwjfDfcax=>28F4|qTtg3?(eVakG_>Km zKR>R=CPzhEWyYZ;CMH(dQi7d(etxFW{GILPNtmOv^#NjFWLRf2>v3~iJ-1=zBihLc zbeud=l;^VQiu`c_q87LV@%uDPVu2X0N?l?(3KZFcnmxXix6vTAKAZPaEwlgwJ+9W6 zp8c`0K&*$zV}p?`11!jg&!ZkyY=RvZCbJhNkmVX4AwU7;XEtT5&HZgZ^Vzpj7A7fh z&ufI!-WhWD49kp&5%^OMgesR5qRMYMG5+PehcnfS$Bft75vk8u*LiWxOs2y$q?Yc= zmIIZvZo7t1{^6_yrRJjz=*fBeh8SNPxbq|s5L~!_{ucm*jh$UVF+m1}K=CGMVr4fD z-hURF{!#M#oJ4lE_+)>H4%#3xw0$iA?$5MMp{}6}TWnzOMz*;0EX}I{?5yFs*va`- za58D)0+Tg_e=n|eG#-sw;OAcX&o2b+TvaMXUZ!<%T26`9e0Y^`8iyg7_;;9K;CLUK z`_!GkAGd`(b%a}c_y0;`NWZC3of`Dg@g^JzAqKB0yd*s}&`@^*QLw@}aV->8K&)CU zIn5N;lLwvhl}5wJNN*~r%SKL$K(p|PuZ`*Np8EUTD}JG-&iObFh9}khnAQ9Blhd(7uDFXTtgl{O?)*x zeivK(<#hD>xs3F?v^G8@-YCuKQYbM#{h4_qiii|_xrpcqdvD~%LZqG}OSP#`k34%o z2tM~deHXto-cLNz1TYs5e)jp}Sth@g!xhy5-nQvozqn10u8{L zF|@6m??QMo_-a$NAMEU;SBi^B-3*hjyouEHYKBifeK)&a!z!o_x!aEt=J9UZbl!DL zLk8N#qdGSo8PX1=GR#RCgo?f+9~cr6qR57Bw1hO>=Dce@cC>$qjkon!>>3#Din4x+ z4Tj?!*l9mr8>})WVAaHwI5EY?4~)RyjB)yvZ-8jNPWO;$C6Ld=`2J&v$HMIwA`8eA(*B&`e)FrKE~zHsD0pq7u8k8nE+=K`zDUrWTKmR&on!eY1-7Nx4<3nSLD;;W^LDffmDLrhPZB?XpS{9L@n z*gH5e-*}{*P7YDB(g_S-87DWiVx`*xC2Brozo@!BJ|j`8-JTF&AlqvW@;K#y0LSdJ zXk*_-=fPL3%lIB5nyMrCmiE{F(ZIf1%3FK&th%YgB)1FlIEL_ld5w}1V-CyRhnQ^% z2hZkCLQe2RHS$(B)xJNX6_W~Qq6H(Ra9bk*L*xh3#2Qw>tOu+r(wmTqU zx1FMAVc52|`$WXYIudt!;v%)8|0N2;rMz>{|6=O1h;3e(Ct^_GcIL?K9FoxTt8^nv_YmL6NIgHG zs$Rl00rpQQ>{5zSEn3cBb9$BG=K@H2h>o zij0x+?cD6S;Xx6_U^|7}%HjAPYA>MKb9Aq-S>CXxg331S6i!nwTj+B=U^{FFmgvFW}~qdBe7pa&5@W;lz!wLx&Uo z38v>q^ksL_-~iy-c-J=-4SIc zPT?)H7ICWg#!Pk(Dk$hUJo0p{o+lDk3pK!Ma+3%efZ9xbkS2A4*0Dm_J{J}yv@XEt z?*wlpp}_f@#?TqjEruO2I_w@0^W~b8f_q@mEzn6&x+nOEUn_c^zo4SKEvYp*LdRGDfpw0{C(wbx=Y!o4XXE9 zzI_+l=MB|*+qY=tQ@=r4QRXaNDn=l*wja{b&-e-sbFz>=__`36 zI*j?o%EHs{1D|G6yT^983TE^-sdq2)yFoAb8M5-@3AUhdiYKKo@%bk5vOlVciOw0N zCdVWe=S~f6h`I^jw!LoS1)*1dr^PAZCKc+olU|gUr8Rj_VOo;)vCN~X-*-axua{;2 zX?cEZy{3gJMxC)>$mP5mgb6Q7bQ}{08w+4L&M6L~Lf37v$fn4Ptm=^2-LErm`w~W* zh*QN^rAeqhCxD*BM$v23hGbi!>=x@T>_@}0J1F1|ytU(O5>JBO@hhf@LdAOuK=5D5B-Mk9>o1g5-qZ;TkDIZvC|!_jg-oX~n^`wZNj7*I-J9wI=i^CxMX1 zvDNSl>jXQ=B7i#LFg-<|_AkDovD{KC4NbzMRNr$E%=BVr_Do9c__|SEDA)CV7rk;d z@3r6g8YpX0sECGQ+)!YuM%V*q&rc?(9~0>5Y4BKJZc*l9(stD3g0IiE5v`qnLe-iH ziZmqVM-Ye`-Y*zduX1=xIxbIy8L7nM@&2{$u~_e(dDTuU<44(K_@}MiE52|ZEP9OK zWRy?nEru}mOrX3**t*r<^;1*(98eRVTAKgD-cB?km!69LIyT%Iby51)9a;vAw*gdH z9aAVyPT5ESW|?~G76IhO9^Yn#+c9(B?=ipR4u20!xbbO-Uw$XFpq4>kV)hYEGJ4F% zBJM&C_Z7;r?>XuFB`C8g&MpBZ5KRGaNY4-YuZrr=UWiUr69pI#0cZCk!n&iELm1;u zN(?hLJIbLd<~ps{az+KL}nAz%*V zTdEmNzO^lYc)9l$i6-({o;$H#)ZinV$?pE5?j256`j$Z+HYn5ApSw*_%x*LT_xmXs zP5TppbKs$KM^D$@r}2?|+o@vdHsl59qpw6s(cT8nE(mffUc-dE#(pU7h8U>dILn*x z?-e)_I6n0Fy%I$^7eQ^e`FL|2k%Ha05KYrEun{}ydwZHI9Z7JEzINO%VcFAWsf&|csP#(1T| z2Eq$4*m1hKMvu{9vm=DVKmwn~vYJVDa~A(rt@iI{nTEVnH<3D@Xp|8%S#}f2uiHbx z6_+U*CFh6e2+k-+3+w$3Y0sl}`xWbwrRrG~y5&`sTW5DZ&@o%Z=0>{B*S53XmSYm%aj7_fP$RM^mn1-oO6Z|dUS zrIJ>LbK`+31x_PFjODN>Q;+_E2zxgD0F9Qe$5B3^+kr@;9Oy8aqFge~Yp3n?W|g8; zkQS$SK=uCmcol85nCC_{5Q8aCish2&F9g!O9lbU%VH$q?`TSI4t<$8mB>jL=kz3t7 z(iUJs3j_FlR!P|RsWe|cTfa!r==W%>L!Z$V>Cb!`ewJ4!J_VNU&xN9}gAo)|2X|3z zsg6}8Z+2F;+@jT7lIF#Ze))=jPv=$qbi)JpuNM5c-UXob0{*nrgw5ZWV}YPCu>)x{ z(0qSm+I)F2S!U+!WXfD1HGjrp@=&@LGiH0zFK1Ma)b|E|Z%y{qjd#ewrfj>aG;M3+ z;0<7)eG;} z1=>ErEm!MqD)!&7lC`hP2aSAKNQz$2-I4k%5Zp(1I{7M>xRp#aX|J7nM6gj()V#&v zwN<;`ja6sN(hxZ1N3n$d-)B27nheftNq$U=al=k6YgQ+C^iM=AuCY?h{%pKOgm`Ab zMz+p&)obs|sTHuCQ(*MoHvB@kxP5-PM@xV*sJ`5JlUr$R;+}1hukkJ6q%^E)T9EyTujJ?8>0$rC{;bK6!M%`?qTv_G%CLb*MDhn0sT+wOJ;FFE z+eY0Lfg>I(vuUIHQn8(Veyj)GCU0>N<)4D3=itNkdM7U;!&K{$tbtWxf(*EG9v-wT zl@bqVg%%fyk{e&g_NHCo7Ohz3o?|C{fS3I*xOq{&ujUu@bv(YY$@r5Q`?$eq#mi<@Wa4$t_lp>)swn_UCf}xoHZE1Z#P?> z_NX|yT!?^$voTu-+z+%tYYGh{(YnO=UMt6=yg^Vrd|;YdElziYZ+l`P`U?{2fH0NdjvNp5-gUtCSSR>Gc(3={O0jD{ zgfGu9C$3rW&3RybV+RV(lH<~1HBMZ}`213=Bvv4KZvn#P6mVIH_JnLs9XjIoY2_>KJv{D{}OY8cBpgojSV*~Y5CbYm6l1tuC_PviHC#*0UzKOeb`^MT#Hq{Vg3I}fJ&=lhCTm5JNsvSrOwHFizl*OZ zsra88wk~@fnOaa4?7=GnuxB}P!Cd4^Jf$Jy1SxDF5yT+k*)LsX4yfefURaq*G*Ogt zJ}*Fr>wBe!<(rnfZ$o9qkqjd+V|Ab0tWwsb9=Y=HN{YwnGwS-Ttv+#wZf3?0l4~3G zQ}ujf9Ir`Hefr#|9(*?0_C5 z%a;Pfed@*F%Ce#R9Shf0C14gm6t($5goKs`#k^3Uc`bEc(S^x=2G^OJSkdsk45@4G5epXL7sQ&8QrzgJ3%K5ok3dd znD?A9NpO%A#+d<6jl5(!Te*8japTVt8@y4^t)E&Po!xbSNUC$iKESUL|4=`%LN=~t zbV&$#xtn7+zWYFjgwFs1fy8|7^>^Ev$}*0VF2zJKd8(vTTw#O)A0FpYJnE+6wIV!?1nq+4BLg0Hg;S|1w*!`=A`uvms znPmy-Jo@fAyKd+LJ#D;v1v766Og@!GO~A&MH-KpDd2g}7xmoTk7DPw!GfXQXIoZ}T zx3v=&%BN4%(jyr)1ds>K@(T)kz0;9A`L4f?owoTSlIVMZP?gh0F_Dp}CUj{`Qq>0@ zliQYgm`X~nqpJ*?yo69ON~V@Ot^%3Y4kI5xyPqIIH{L56Y{WZ%tC_l9TQKLn{Oq5Y z%5NuUkm8U;lcEk=*J>7ARwh|U4*BEU6T`VVj!u*E<+s`G9Lel#oJqOambz&OHeCn) z^(kQ(!Iy8JD&f7*b9Q;OI#he}67E%MmpT)ASD;nxWO}imnZ1INXfSt7Z;d#+5Lz7_ zo&Ga>%MZ-Yt7_3*Nbw#OovGXX3{qHb=BLvTlQ^T0+wjDPIx143?^h(4Pn>NDT85Ay zik;=qa=^%-7FX?kPs;qqNLTU^x$}r_9ci=N-7I#xeJ3cru9$_aZ&;Y){Ni$w%Ij~0 zd|NWXcLnDUJrVO2zQK1=qZs+hBR41OZ(m)@vpGKf;a1Q3mE6H5B%#ac^Lyms?obFB z2$x4?ta2fth;IgZ7OLy<2hg!{8uEq*Ox&zuP5!%1)c!m3TBWmvfyWGFZZr_szTZ25 zfXsSvaC9U=fl4cag=7j1so_sG7(U2XWoYZUaL-c-)e(?;ZOMBs_{Zc8()CT-YMN#- zNf&Cud7>N}BhPpd`wr4y{1>y8)IB565R*OY;Uxas>U&{OLfRw`48di*^JI^lT&9UmYU|)% zm(Lf?CA&`Q_pk$x9_KDz#>Zr24Ha-{7|wJq%q2~pEP{hK7>qQH9?1KxcJj|USap`v z+x3kHYTR=EQ^n5}&NnNWZ<4J6S2c}DEjFNo82r*(44w#83EDL7Iz0I)#b|OuG6MW0 zR}s;YUKw?>g)C`Ava+$k*r^Ju6dkDbimMcr^4PR(LRCAVii1c+s%t0h>KYo76&;n7dP9qg8N@Sw+!=vs)%UX-zcK6V>fA&+Y&$QV!tgOxp@ib~hHE@>+NVqWudN62#i t4@Bj^$l5pm8~%UGb^d>=Xt}|atf+mGa~~RiJ5n1*?WLwt=?k-f{{R(V9