From 017eba59fd34307a056095d39d3269ff1d334fe9 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 26 May 2021 08:11:06 +0100 Subject: [PATCH 01/19] docs: Update set-up to take in new instructions This is so users can replicate the example correctly. Ignore the sqlquerygraph.py file from code coverage. This is because it is a collection of other code that has been tested. --- .coveragerc | 1 + README.md | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index d593f5b..14a2a42 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,3 +11,4 @@ exclude_lines = ignore_errors = True omit = tests/* + sqlquerygraph.py diff --git a/README.md b/README.md index eec54af..1fcc245 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To run the code in here, ensure your system meets the following requirements: - Python 3.8 or above; and - [Poetry](https://python-poetry.org/docs/) installed. - [`direnv`](https://direnv.net/) installed, including shell hooks; -- [`.envrc`](https://github.com/avisionh/sqlquerygraph/blob/main/.envrc) allowed/trusted by `direnv` to use the environment variables - see [below](#allowingtrusting-envrc); +- [`.envrc`](https://github.com/avisionh/sqlquerygraph/blob/main/.envrc) allowed/trusted by `direnv` to use the environment variables - see [below](#set-up); @@ -46,15 +46,28 @@ We use [neo4j](https://neo4j.com/) for this project to visualise the dependencie + For Linux, install Docker [here](https://docs.docker.com/engine/install/) and then follow these [instructions](https://docs.docker.com/compose/install/) to install docker-compose. + For Windows, install Docker and Docker Compose together [here](https://docs.docker.com/docker-for-windows/install/). 1. Create a new file, `.secrets`, in the directory where this `README.md` file sits, and store the following in there. This allows you to set the password for your local neo4j instance without exposing it. - ```shell script + ``` export NEO4J_AUTH=neo4j/ + export NEO4J_AUTH=neo4j + export NEO4J_AUTH= + ``` +1. Update your `.env` file to take in the new `.secrets` file you created by entering the below in your shell/terminal: + ```shell script + direnv allow ``` -1. Within this directory that has the `docker-compose.yml` file, run the below in your shell/terminal: +1. Download and the neo4j image. Within this directory that has the `docker-compose.yml` file, run the below in your shell/terminal: ```shell script - docker-compose up -d + docker-compose up ``` -1. If it's the first time you have downloaded the neo4j docker image, wait awhile (maybe an hour, depends on your machine specs). If you have downloaded the neo4j docker image before (such as going through these instructions), then wait a few minutes. Then launch neo4j locally via opening your web browser and entering the following web address: - - http://localhost:7474/browser/ +1. If it's the first time you have downloaded the neo4j docker image, wait awhile (maybe an hour, depends on your machine specs). If you have downloaded the neo4j docker image before (such as going through these instructions), then wait a few minutes. You will know when it's ready when you get the following message in your terminal: + ``` + ... + neo4j | 2021-05-26 06:40:15.270+0000 INFO Bolt enabled on 0.0.0.0:7687. + neo4j | 2021-05-26 06:40:16.412+0000 INFO Remote interface available at http://localhost:7474/ + neo4j | 2021-05-26 06:40:16.414+0000 INFO Started. + ``` + Then launch neo4j locally via opening your web browser and entering the following web address: + - http://localhost:7474/ 1. The username and password will be: ``` username: neo4j @@ -65,7 +78,7 @@ We use [neo4j](https://neo4j.com/) for this project to visualise the dependencie # see name of container running, which most likely is called 'neo4j' docker ps # stop container running - docker stop neo4j + docker stop ``` *** From fd9db681b798d8a1c1040875ddf9462b117ae7c9 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 26 May 2021 08:25:38 +0100 Subject: [PATCH 02/19] feat: Write Cypher to import data This is so we can load data into the neo4j graph database. --- data/example_import.cypher | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 data/example_import.cypher diff --git a/data/example_import.cypher b/data/example_import.cypher new file mode 100644 index 0000000..793441a --- /dev/null +++ b/data/example_import.cypher @@ -0,0 +1,36 @@ +// Create constraints on table_name property to ensure each label has unique table_name +CREATE CONSTRAINT table_name_ConstraintReporting ON (r:Reporting) +ASSERT r.table_name IS UNIQUE; +CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics) +ASSERT a.table_name IS UNIQUE; +CREATE CONSTRAINT table_name_ConstraintRaw ON (g:GithubRepos) +ASSERT g.table_name IS UNIQUE; + +// Create table nodes to join later +LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine +CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); + +LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine +CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); + +LOAD CSV WITH HEADERS FROM "https:file:///github_repos_tables.csv" AS csvLine +CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); + +// Load table dependency data +LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine +MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) +MERGE (a:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) +CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a); +LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine +MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) +MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) +CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); + +LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine +MERGE (a1:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) +MERGE (a2:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) +CREATE (a1)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a2); +LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine +MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) +MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) +CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); From d0b43907efbf2dab66c51b470f22d4dd7d2464fe Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 26 May 2021 08:28:49 +0100 Subject: [PATCH 03/19] perf: Import 500 lines in .csv to neo4j This is so we can get some data into the database in the case where there is a rogue line that aborts the import. This is useful when the .csv files have about hundreds or thousands of rows. This will also reduce the memory overhead of the transaction state. --- data/example_import.cypher | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/data/example_import.cypher b/data/example_import.cypher index 793441a..ee7fe9e 100644 --- a/data/example_import.cypher +++ b/data/example_import.cypher @@ -7,30 +7,30 @@ CREATE CONSTRAINT table_name_ConstraintRaw ON (g:GithubRepos) ASSERT g.table_name IS UNIQUE; // Create table nodes to join later -LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -LOAD CSV WITH HEADERS FROM "https:file:///github_repos_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "https:file:///github_repos_tables.csv" AS csvLine CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); // Load table dependency data -LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a); -LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); -LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine MERGE (a1:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a2:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a1)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a2); -LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); From 01cdc1b0ee57b0da3509f8566f25de81ca53f443 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 26 May 2021 08:50:10 +0100 Subject: [PATCH 04/19] fix: Move .cypher to neo4j folder This is so we can move that into the neo4j container. Also update Cypher queries so they use :auto to avoid issues with manual importing. May not need this :auto call when using Cypher shell though. --- .gitignore | 4 ++++ README.md | 2 +- {data => neo4j}/example_import.cypher | 14 +++++++------- {data => neo4j}/loader.sh | 0 4 files changed, 12 insertions(+), 8 deletions(-) rename {data => neo4j}/example_import.cypher (74%) rename {data => neo4j}/loader.sh (100%) diff --git a/.gitignore b/.gitignore index 304ff16..c79a3f1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,10 @@ __pycache__/ # neo4j data/databases/* data/transactions/* +data/dbms/* +neo4j/databases/* +neo4j/transactions/* +neo4j/dbms/* logs/* # tests / coverage reports diff --git a/README.md b/README.md index 1fcc245..8bf3825 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ We use [neo4j](https://neo4j.com/) for this project to visualise the dependencie ```shell script direnv allow ``` -1. Download and the neo4j image. Within this directory that has the `docker-compose.yml` file, run the below in your shell/terminal: +1. Download the neo4j image. Within this directory that has the `docker-compose.yml` file, run the below in your shell/terminal: ```shell script docker-compose up ``` diff --git a/data/example_import.cypher b/neo4j/example_import.cypher similarity index 74% rename from data/example_import.cypher rename to neo4j/example_import.cypher index ee7fe9e..0f09ea8 100644 --- a/data/example_import.cypher +++ b/neo4j/example_import.cypher @@ -7,30 +7,30 @@ CREATE CONSTRAINT table_name_ConstraintRaw ON (g:GithubRepos) ASSERT g.table_name IS UNIQUE; // Create table nodes to join later -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "https:file:///github_repos_tables.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///github_repos_tables.csv" AS csvLine CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); // Load table dependency data -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a); -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine MERGE (a1:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a2:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a1)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a2); -USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine +:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); diff --git a/data/loader.sh b/neo4j/loader.sh similarity index 100% rename from data/loader.sh rename to neo4j/loader.sh From 72689f1eba46f9e236c7d6fb31bcf3421fc1afe6 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 26 May 2021 08:59:24 +0100 Subject: [PATCH 05/19] docs: Add acknowledgement to Google Cloud public dataset program This is so we can reference where the queries in this repo are based off. Also innclude image of graph database that visualises the table dependencies. --- README.md | 4 ++++ guide/img/table_dependency.png | Bin 0 -> 108151 bytes 2 files changed, 4 insertions(+) create mode 100644 guide/img/table_dependency.png diff --git a/README.md b/README.md index 8bf3825..a3de828 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Parse your SQL queries and represent their structure as a graph. Currently, we implement the ability of representing how each of the tables in a set of SQL query scripts depend on each other. +![](./guide/img/table_dependency.png) + ## Requirements To run the code in here, ensure your system meets the following requirements: - Unix-like operating system (macOS, Linux, ...) - though it might work on Windows; @@ -85,3 +87,5 @@ We use [neo4j](https://neo4j.com/) for this project to visualise the dependencie ## Acknowledgements This builds on the excellent [moz-sql-parser](https://github.com/mozilla/moz-sql-parser) package. + +With thanks also to the [Google Cloud Public Dataset Program](https://cloud.google.com/solutions/datasets) for which the SQL queries in this repo are based off the program's [GitHub repos](https://console.cloud.google.com/marketplace/product/github/github-repos) dataset. diff --git a/guide/img/table_dependency.png b/guide/img/table_dependency.png new file mode 100644 index 0000000000000000000000000000000000000000..5f80f05b85ce9e2ef330ae7a12e5e7d42836c680 GIT binary patch literal 108151 zcmcG#WmsF^vj&Q5gA{LYC{i4XySuir|1Swu1xCATi4mZEQ zoO7Q0?S8nQ_I_rstUYV(%&d85=8e|UPz2z-!a+hp0w^oV=^!DY0g;eUP_Qr%k_6TM zpGZhSNXl~3@4ZbAIwHNO25XP}o|BtqjjmU`##}P$LBjWx!iyBQ4MmASZUtCt`uuqU7}{-bPK2bKRB5_z)Rn)t_jdV@ ziJ9KqL-oRfNUeEy2s$n)yME1oZ+2~2SXX z^Ye@VjL>d#Zt@oxq3cyh`_~y6yWdy-nej-CrOlMHerME4Q{b(DfNC9N^54S{qnG4^ z-2ZuBID|k&Mb+wV6m~d-5%&JQGX-Lrf6W#FiNC{O03y_y5bQ}$(a93nL}dW}&(k(4 z1p$P{Mb&8kN9c$4|5+GzpJ6XWpjiDs@Acw~vqxwqV}^ar0n>Vylhi5)j@*whij5ff zWNZI<&v|*GskG=zE;(>_{%_cCHRR9rT{8bpu-2So1EzL%JsfEkL1& zeYjI;W=1ToQt=|cGjR7ryT!dlj!bCsKEeo<^y}-dbD-r`fR2Gd-qj;HudEa2g(#|J zSZ!TEuR_K5io?k-MU~~61?h*2UQmI^pNqdnTHMHd(-80D3gvlcV#3zudF*NVsVyNd zq^EdMCWFC@(~LFUV_#6oL)}Ewp!Z9M_gaVuk zOXJPyAXyX3*t9>5a$MI#6H`<4;o;$~e`hLoIdGS;{`~n<=X?2iXlQ8i*Wr(%%1Sle zpN6mPW-F7!amhqFEKT0VpkpuC4kl1d&CE2m3k5#4gRHF;xvYtg-qaQGq0mw8d^Tty zCZS0>i}{Xs+7Y7Leq3nW?A>A{%F>s(XtLpTVQ78&ZhW^x$o)NIs91DZHjvk1apR9)MG55^1dry$9E3Q(N3XA2B+hIDr%JfehI3174Q_zdcg zNv{J9>12ud#KZw5SK@oGh69~KA5>MUhQqLltj;qy-qiPA58X%PSniDFm2Z-_Oe~+h zZR6jM007^)S_G>e-$bWUEi|+B{SKyGAnTbsE;WuCF}jymdp^Uu9)B+x$NG-R3Q$fm zH^&!VHf=cATuVsCm21|Vn*|MjZ@BJGe>>Y$M)Y58`8lY54Zb^`2=j2!&E;QB!J_j&Y3qpT6EZ(uNBRm63dV0Dm97-q{sh%Y4wk=n(lq}`< zXKqfBYqeHN)M2IFVAa|>thYawtPwz=xs2@vfCa6ABE$1eh3F1~xgi^*N`7_ca6?$U zXGp=$e6Xt~|N&=z6LXJu~}Sv@{j3)a!D=qv>YhZ$8)TTX3s^UtONo$W(X{ z7sKhu@bHK31*7)P<*=R&d#_n)ShH=t{X)C^FZ<;(QIUtvA(O>Aj~{87dZlMb%o_Qg zq2N4r&)BhepkT{txjUg!>tJF*+S1a)Oaj@d)6%yzDp4=nHhJdmMf6n(zE7h`>1=wo z!Xkc=;>dfXhs!${)hQb=gAUspd@kx$47q~A z2lf*39CJzSa&91%XCtijaTa%RnvuUs9g+(A#J!%1O+|eVy#E@2*7C)Lk5 zs+O~9j(w_Am&v$HY@5FAaM|8@I zTTUVG6KCh~_&q63?!cx@+@#g^%$hi9jDw{XqjS2Wtoofx5pd54<*>?gv-7%+3g-E#yJpC|B4`V}ueKL@T+Ve~#;bVd`EgaQf259@GgF%<3U8&G{ElcudP&VsEyQ~n9o^hDh#t& z`%X>D<#>OVEE1n4PyN0pLIS3T za$L>V(@g|;ICR`4g%RW-iRPp|JmwEbH*dp9p%r`%xK}Jadjyh(8cb{LAss0>W{fb- zpB?>}ODHbY#3aX=*)Om$Nu?DN^O6Kt{oi@{>mRrsw&%2C0A4ZnMgZ)}=iRuN1vT%k zkGVDn>-=wBTM|;1vPp@S zCyHb*sfpgU8Cwp z$b8s*y)yzF>)ipyFH+3^{=&bH>Y%EWwKbyG9DeeiLe$g#K!f7eg~LPwA_no_+qi9? z<`|Z~@Oerch-8p(z4-E@m%YLj%;qckP(tTL=X8bM0s^HZh7L7uHFa-yoPXqmc|~f7myB zo(hQiNHi82z~g&Cp2g=?#d)V&X6g zK(v#c9-PA?xFgbXGA8V0)ts>UK2=W&M&#~V-d+>Bgy&`kK%58Nizj+}zv zgVsxxiA;cM6a5^PC`r1xDd~9dk>(x|?DIZ+i7!7plVz>899&}_v?8S29+sZxa_P4Q zyLr7F`l6CWH1t|8%9>qpd?M=BH;r4}`!AcW;i0hl#`e;BcbMx3JQn<8UM?=PyUYD@ zoa$pPXxAhsM;dTLN&wI05OHR(t6LAmWwBDMlU)|}3^6a{T`T$wIc1F`dMBZjhNmFb za115C7CVl2)v<<;ZvQsoPvAlw$D@&0`s2|uY=75Vs+`)eOxF(^Ql!AX*4s{Vu^hC0 z*cE+kYB9Oum6k@%@7R`_(_#BoyF_7V$>;oiblEC5 z#@_TK?^jee6dZ7l@t4^*WZUTH?K$_F1&$1{6|%azzuNDQ+7_vKGv#8w|@irrGJ%-a* zMG{znR*{9+Bi1WJebK3uOuv9eKXiY39xKrCpPPGfeC*Dz8p^1 zp;D~`E)IncznZJEOn)&9iaQ2p z|KY>d?AV-#UJe5>nrA8xnWDuTF0Rb%>}+VNmw^2$WO*oLdUC`|j4@T2zd+GKaV0Dr zwD1buV?1BHi1|*T*?mvvGc%;BN@H*#4{tD$CiCIJU%VAbc{-EbVTus1!Avt6qendz zfHnJfHkvZnFrcJvIgFgdQ%z1yXitqDbv-9-Um}kjU5+d-Qx)XpY5Ex@YM{s%o*ojb zn7{sZEVqqT)XRGR@evbmBs^B{f|`^d>kSE3pl5@E$kzz$^gO;kwp46fLZ)SNy19X# zaZlW>KR@P#zJ9DcQix>6d16-Yxw`~{B61FHe#5tznLkv;T<_6`QW*v&S{pRzjF)Y9 ziBSKgDUZ6pLbE4QXtYF&K5EN>#e=5dpuaXkhpNXTohaBmY8r>w(c$|v9X~*}p z^EuWgEw~TMSlqrefhEz5GYDmOUZdEIElp2j6OvXz}Kv#;bTmFHB){i9nL{Rw1j}en)8I{+3~c zvuPg#M*;P?;v${W50#PbM_e*4^jWIp>p`c#LwMv6Vy!3`t93G&VAXAWG@oeFCE)OU z6fKRJs4m$1fS^{$>RQm)amBV4h|l7pU5AXGdUgI7=bk_%SrK#fm)~xtl}Sa;)uB5? z{w@v^oJ_TA^C*hCbz+(2k}x$aDkX7KOPQbvtaNp?sC>tcPve-1TPpi>flTa#Vk;Fx zi%MJaP%IiHsI%E0k4caONS%r4NyJXH$r3>Yliv;;(FH${m#_y>nDx<#pUD=PYv*!G z4v&T8?Ic+TxwI~Ez_m}_{P@lf9*{hykm1kGsAe0tkVno6Q;T@NCGi+|(CqTrL5lM3 z$$2jS07R5DjzA#VRy6%R*K|;f1@Q3-m}*!f;6WZ^x`4$x*a+$xFEX8|H8GTUrX4su z#_6P=&v#V8))^gowz)+;SCde3;If%`FHQ$!AtMzU#Xg9-_YIfMrj!Or#J*YIle$0U z)c&5joP^t;$pk{(inJ+xIw*|XLadZD0XRsQdIdtglsCBa&~9-)kc$Zl4Lh?#%3pup zI|9oP5X@a8d#+`1IhHN&*@>x|A4(NSl||l%jc0Zyd9y6U@hh2_eFAeiI!aejx}1ee zy@H|(h$J*RvJl|6D78E1^-e&Jwyo~C4=IlJ<9SY}gWbhoL9YqB6OEwF2vl3P;Zui< zTe3TU=CK3&s+##(^30XaV3l?xgX7)TJYJ&6P4SsTo~5dTpRU2rbf-zOQ1n^Jql^^5 zMPhOB1~E)Z1ZrmHx!qMH8@cPEc}Et6L@omQW*(o$DXBzXKyw`SkS64AnRS_ym`F(- zVeEXaD$;_4+1J<_$FGCumpf#U&z-5U&$!1HoKHebN%Yez(;>249i4$ zM9OkGS`vPYKZ{H6&3nOecp+R3HEVH|AF7Ush)^08B@s3z%V2OwE z4dQFfTpx+D+=lzTW)tPI(f}p|KEQ1oX}hv%nX9!5um>x=)(orK6e%~TC0yG*yuO21 z;669I(KE%|Z{8FBy|Jaud+7NHOHJj9Ohi8g!!!%Tx!USn-IFn3UByMU6_&nv$VCMO zX0+4gnq+Fjun!F8GB~M{pDw4 z(tu~@G?{C4>xzDHax*Y;#y$K_0rXjVeOVSF7Wc&X!fk-YLl;5j-4`!=BSFL!>pQ%m z{KY2V{+j0fk2s(6ogW*Cml(K3$qb5na>wVTCf7B*>-u&|F~ zvLL+`t4uznw1uGSMyYvIxCv?JJ8b<@nI~eQaI0TH=LQa3q_iAopO-_v{N12+5vyTh z3Q3E0&TnEcsXU%!kYKtzFBpYL&2u~w0IZ4)B$fCAK6|pY^E9&Yn%ZnuyAhYgaZZ#7 zE^cBLZ8GUx3nZ;LfEL-;T55{0&vNfw9!I`qC2JcnA5N>_?b}jawB&uozjZWI^6jOlzm6o<$qLj=AFr~xBvRpGk!%wM_|+lz zuv7(z{Bk>gFW@pu!}b#7D^eHybbKFiUoy;kHfdz+Qx}{!{;d-IX_@ybY3?A*HScL} z2xB4aBFO}b9-z2CX7(4nsgRzaN`wEJ@O6P}`A8=-2z5=I)2vn|oqnN-Ck6k|e|`vm zp+BcdaXP9e)23DZ?lBKh|7hj0&lPV*0a`VqT$a5pJ|(nWWqzEFE}DuRq_ z#RTd(;p7q<`1(+Uke&SVi*uw$?Aw?|v{M#4#J92f;v`zdC&_^M#msvw6;MerwWavD z)7s)$G=eW52`J=-Nwt;=PBjYKkyUE<+BT%(lBB=E^gNhPf5W`5G@#%5D>lR(;UpM< zV)XbJo}9LWH6l7MVJJ(3oi zy{1~xX>~G4W;K&tugbLJJ0rO+{W_9Q=m#Y;kq8+B?`kMFyTNW^2Vtwcfc;{vQ6lxX z{t%;PN)cD%^!t+pk(kFL@fMxoF+X(~$Jz)+r)2tCI#QaHn!2AhGo(^k69m#pSgfTFpo9^yFwLR7E-7R= zlHAnJyGXoQ4VS9U8(v5<{hPjbM zdTGfs;wa!UFCK1e5!_viHr$U=U_IaeiLRfM7LEdvE=01h(q^`@n)9$u^)2wYHt^Sh z{Caws#6dtB6#l{r=Nk7(axG0$&BfrR=3cF3-b$wSiu6spB;{}EPf1M2=DiW>bu-u$ zXR%k>t50b;ceP5_4w}YKiLw{B2oEu3Ddsk_Cez0zei>?C_#0Il@nt&E_ zC$38N!?>_fV8L0HBhK;Cs$y-wIzAU^uQz-!{ zcb|RkC`l!aN|S}bkvMyEcA^Kr7T8^XG9V3k74rg<^kn9DD!{18K{bbF{3|qKK&snP zTfLJ?%%$;i>KpR%gF;7lD>bSYs-An^!VMpU2ziNbF4{9n3pjjsIdPQW+w8=8*;4d&4 zD;)7f&`KBkl%gIJwRC-vqr2F%dP4@~L&Rww$Y2T4#h)BizpiEFUL^sb?RBNrABbk?ldWwvM~`w07bDIly^?SVULM#?Kl{BQ6JY>0Yql|veIt7eIo+NU=}HC2 zSK&KXM&3!kuRZPdyEt`>us_KFOhWRpqkxJ6S^?Y43yC~~9-0EQfzw-q$HcISH)L1@ zOOza4FO%-P>yX*Pgj&`XQ#`UVarI0J);hct1a5exen~shlCiWg12ys_QUxUcjuXC| zMTSSp+ekU0fn;gPG=zQ5O+UwzJ80rh_40;Z_xklTq3wMlu6Wrcg(Q%syGUj-kEdhRmrkty|AGKcmXr_ths zJ|P!%qe(h-qkt2_NpP@dv7UsVQt)u!r7>$d-@4NRZQP>Jb72-m!70B#Vxb4Q8@Rt? zT8OrObniW4#XZ>H5^NAl0?c@=28ULlV@L=ptdkXfun312=LzkoP_DGZmWi@nV4Hoa z^0NZG#`+qu1Ix2a9s_R>>VK*EA$^}>-SFGrHz=I-k*B>79!N}2`o!K!5t;@F&vDoO z+&s_|dG(1eE}u1Tp&(r}=I{Bzt3)>A<%9Sxh|n^zb((8e z(BF}J;VP@EDG+gFzvWTioF_i+PmRtcEFbMFZ@&;<$;{qPcvS(8yxv@GOw7cF1);>+ z_q1a4qp^{)H_)~epH9;y*qgVnPb5Gh>IOW{a>T4u1|Qs;B%q0Pe7}SfdfR9n68z~j zeIiz8FsFhWe8ySMOZ&}^LCFtV!{nG3((K+zittH(*hH~}lJ|~3PS*Y?euL=vU0pr& z8k{H8(8wIgB5A#28_5j|`bxT2Of0_|NPdA1nop~E=gyqNK(fxjubmV~3F|T&;1})4 z3Ub-o58g_WtykMo_`Caamp28s9JOVwfO#C@A`A3#jcEZZ;^!vbcwz6& zpDtnqtx70^W%R4YPkr7l(BbB=8eahD>4l~!F&=O#EgjRU0v`k=h9p@FmS{HJ{4!ukLq83X$3d#FBMq4Nz5r}x}1d@b5 z3WJtaK`{iFX{R2O`8R4kV@DDj3&s;6(!xVd8>P{AIcE_=7;t>cQocwCu z>7sh;g{;Ng;rc4Oe;GHX79f18(>sE(>6PLwt}XCv4G@+qgGFDPz^sCwo=?VV;AY7Cp`T^cOsJHzKJ0&RDVHgpczkZ2V2wB_^NQt8$` zD2Cc{)^2-fsZExD`%jJA?E~Ih7df=8<>R{5N-IF9@skUCXgF7_!)Q*4!y?576eGW!qk_#cYCrgOKggleIp9F$?%`&H>=k)I8h{rC zCQs23YO=-w6hcAimNRVkb2^j_NCI5&PlcbD8}pG(VxHqJP|6J?16Wqlkog>{WSORp zewe^$UqJEMFa!PLgc|sdgRi6@9~^?*Oplbtkf+%xNEg*MIShwN9|oiiy7Y~>5A0Ry(UwL z*FlNv;)kQ?1~f3Ac37dt@a?0(QD(VE*#h$EP0K*2#|02->T>Pa3IR=wV$&hP86)331Hu#1Z5M4sL-9xVvQ~wBWu$^7+_u&3qnd=ZIN9H0 zHLwDORupB;u#!{=-ez6!CvZCF(&zDy9pY$PC#B(LiS(Z~8}fOF zfkojY#6#;P!Cyl)3oOYNy+B{nWIe%6$4iH4VSGo=xPLJ}TI(iA==omyRw5n6*`LUK zsjDSu-(afwx#-m*@2995Eu_WdS;8Z2_25QLU9X;oViMPeo_)R9rNP@)t(HdF)J1s? z7G$kwAY9&J{wFfv^$FMQ$Y|2o);;IuufJu{BoGE~5qiR?sYh$HDQlZN`I!+sOHn5Vf=`&xl&=HBjMj@c1?k>Jl_=NSdxvG7vIAD1l#R8^(Vt4jl7c~ zW?HCesH9dFAL(OQSPAq7xm)Rh?RCIR&be9?b8Gv>UsT^mbtFhX<#jNVe$$%_?h)H(7*cJ2zICq?p5A!gPsC(PrSnZoD1nO;$lng>X#?$JFm$29lkZX0+8#a6cBJLRj~d%=%>Brfl%X%|vZ)%w(6_Q%Tn}x6>ruFC>=go5#1E z+L+1Gq7AIs7IndAlX|DCKZTtaYNFO*c7EHazzw0fIcO@y5`^R9Cktg)+R>#h{&CzkfH# zTBEs;H&De;xT$-=We`?o72IVd5j(6nB}H22c8CD>VkB2Ef-mRi9vP=1dOR4lZ`S82 z+`KNpK8Sj!T=zk=v67;IGd?a7C^Zu|phHSHIlQ3)0qfgZZJzCaGCcXTI{`NkF0MgR zfv_}v=pL_}NKs%yAkJWb^qhIF1do0bjxt_kTziEzf-Y%B82Qh_8&Jn11`(6oQ0d3S z>!hImz!@Mo=1Z`jcsd5X?Ry!<{!V5_8e|-hZ@>1+?jxK4gxvSajjP+J0z<+^1d?g9 zI5oJ}R>>ujitDm=Yed)sLd|%Q1enGXff*^rUd0C$4>EJzs($oD$80*fePz)xL1d*D}s37<&DWOQ_7nEu!f${fZ^`dOtz4)X# zUtLcow9yASW3FjZsM@YdbKeW}sl_3ViQ)Z`7qBjrRNl?djkENz-zMTLB_i%d6gOyH z_UFF^@AvwkV5blOVK*lg5l{{-mX>r?U!J?gC-up>)deK0Rj{r5n!#CNdt{GH8sg9q z3avSgJMxd0nRX5JzPs3~@b^v>ZfS97+fwi5!6CI)t7GvFdNx>#Ow?Zc5hUW}@uDvk z@C2Nn6rwI2!2>PH2>qo^=_iZfz{zITPy(`HsL>fP5+ru2rFS*uF$3Ep{i@z^5qpZs zV&Z-al3IH_W4EF`Lge8Wllc+o?Z!LG2nl8Cq7(@{ExCAQ6??Ud{ZH3daoNQeXzhL9 z*xT5mu>oY;db{2#>oo3jt9becF^2+dn3UKZsims;Qd8OS$i$yH7A#6Tdc5Q8T1C-{ zRaC)&cAeh(fQPVYMT-zEY)}8q)ej>7vO#F*T@8}e_527rF~^x~!ioQkG~@Uw9$o)K z)>C031_(7KKz}v@T!KJ``$(&un=9&iSgC;0&3h6&A?-BbfIfZ%l1xNoh{Qa(lapu0 zM{1n|f*)|87+9REcp~66U(ysys z{`@SK=IV-pa4b0{MF$(qgd|$B;;oqNV?wY5QC;`QQNZKML;%?lpEQNY^DFdYjFv8^ zTqNI?F3R}!veg^j!6Ed@I}Fj+#H;jq9pyp|Rp6ivtXBy0%v|0{rtf;3)g{cdC?#%qF;p(?bVIQ7auSFn?K#!RgbS ziiNG^+hHnvf5Rj$jlwR=;xRnFuJ=?{H1r{x$X@h??H2uAVvWB1ImUb4r&TAlF^d%K zF9WaarD_OiDQg^eaY@lw`f+n}*S=`yqQ{1h%8C?sM{=)EEW1qagF-t!;q%JnxuoA@ zpiXEIkyv|vN%*T_i9rq}qIKxUj1rP09Mj#qi*_WdkDI%_muFq7&JA(W3sgfGQ{5sP zeHDC$q(?68oAc4rh7mF3{rq_FAytc_UiuW}ym2j_=p-^C3DLeU=e0Ig*0Eh6I5R7; zoKob;$8h)QWWZ8yPpEP-x`Dj>#8?AID#7)aDBo23lL80Pt%X?Evt@^=Pv()>)EjrL zK7Ct!gRGhC@Q3@TkR96WZw%INv!Y5|kj5?Zg%R2B5A*u|;nQ&`fV6OT1EF(eMKcL@ zdnlV^C-wgvti4IC-4DEL$ghdSj?wm!*ae}f(*14tfdUC{JL)1( z`ec|51y}I~6Y?gg=qSgNsW)TfHQ#dXa>5ENzau}Bc4xBK;CFGRSC2$ zm{gRA8}!ORp@PCzldq`pXSfh4AAc>BmYsX1^&q2f=SSKzF1gWvP&a2p@#P0<$U#e> zzwweUR-d%%f+5kBB~CXbY!G7ySBs7-fTn=??15J$>t@Y*Z-Z$X-);#qtZO4nr~$JM z)B7Ew_0!&2#Sm;cfiK?EHe%9!Z3YJpM(-X|lD&HCT$tjN7VYa@7c7y2sKVvD5@abH zVVtqi0>AmuQ>1l=3SI0YlDkGSY9~i4G>engsFp#z+}7)SmJ@}+6?~g%y$EAQe;!(J zhAB(R3%vgokUu^215vB^c9syHlqsq|xb`B!($Exn=aE*nU0KLu&DSXzU=l1=AzHQ^ z2Zm1BJmalcgua0s)RhRyPe%_Za!xQoi-m4iNo?xX5XkTiju4p3*mG((#-6sYi_h!C zUZ3+uGlaKHf0dSuHoMm~*asbbf#yeaMm1lOCc+>@goxsdxSVOwJvYnL4J9vOYjB*i z#3$$%d|1@t^b8C~Zp!)cRrC{b`UgmjT=V zHh3IR{Pyx|HOWO|5T7jNhluD12&s@rH=)#Uv=g#~r7v@0&eBihvv|OuyId%K14ltW z{0HkY_A))tnSkOou$gov+Ulcp(O^%J^KrnCM{Lv51N~z_Q-K`8aMH^gP?rv}Ye;i? zGSxeLN$E4I7(1&|>tV3il(6fCT$>P!P7AvH-D{jFB!79-{pjY04Jlut^N)ffY}on9 zpXejRcC@f*et=mMu6&iaWKnI;u-oeq@mX=0!mzpY0CrJOavYEQL z)@Fy1^v2Bu-Y*%U%WZyKP|8CIuYD}$a-I=*M;0SNvydB|g-xxw7a$!mm3S5}qe7)! z{78r*wXjtF6Ly&`0d9TMS-LeMFP7$&kaQx3>rJ{`GrN9`BrGlQWzChQXP9kx{qDB5 zmy`AM)9>t`vDRsNe-k!27YI5D{S87y*2M_uqc&+9C@#yy4TK_xBG#te85Y=q`a#0| zajx-RK80Q0J0{)Q8CO;efWKoKtgV&>tWY8=imxamI8#xOO>X_0d(VqG?}9H+GPLpt z7*oXFw5!^>dcojn)cY9k9e_Nzk-(){(_Ff8;dF-_L;jd2aG*iON;7`u*mKzDk*5E^ zFQ?wsQf^&s0-2d(Q_`P3srm!^0i@YqrXS+c3YG9R+0>?iqku7Jfrx@oK@gOdI(9ux zR$WhXw1+Ppm%Y-K?5wym8KacuIOHB8p(4=i=W<*7?HbqH^UogU-io9PEs%VF23wLK zn-m;4{Qf6RFnUvVcGFerKmCyVMot}H&Cb^z{l11+qCyTX&Px;*{V`72&vCaDJj>5= z!m5LZLa_N&DLkd=&S-(&7=>w_rvtz@g5bbS5fvmf@?*SMVi1}YegTY$8ie|oRn6}G znh$3qH8A;|ZmX)m#1+f35IsAWwPQ@+bzkpHjsR{YaAO4R8`ovP>ZPR9^2_*X5+&3+ z;jOqNrM7t!KJ)bPv@ z+xAZJER=CgJaP!5N>=KY3qNeXuOmK3;42(O0_hvBO9#=Y6sNttp0$!UTvMNuwkbLK z30EWEegZ=MsyvA5K@Bc5;brIXAisrD*FT7ymU!QrjGMY}wh3OVM?=pOgH5d2d}_Vc zT$B}f@k&T3u_j^*Ws~}2P?uPj^*6Ajbbw2zS!aN+T1;)2@f!Ffzww~m4&GCHHRz|D z+9H}wV}~*<(^X7jaL5r3P2|xGSl=`%9lU26M?@P=^MT>VscGY>Wluihrt1qi*VtKT zS!%0WB50h7AvG)=W~kdMl(JEL_fW#0fIEN z>q3xJ<)DV%&)-DX%b*xRNb*t)U7~Ffg>t(xIL=G!DnpHUu?)|FS?|cBLfw?6R0M~R z&SpGUaEJiA_M_O(Y{8k%YixefIUSbUIz4a%(@MhallDBSS>+7}EyAbL6SXTO0 zXlHNQ`V@Mrxd1gQS7TwHN$f`wO_Kl&4~ok1fLs#Nk=sVo*cK8XW_e;hDpaQ00-b9o zx0bGlgYgeS^x(h!e3L%G*Zur(W)O|TMj}?k zxjJL$lZr|4`^t zYp}$qZ?JW{@%dva1gD!Q9T%EpzSNM3+s!N{&Z<}KW|O4rojWzR_;3eeA|pSRrXDSE zaSiqkru;1Vi?$uyiOH_Qmy9cf96yXc(4L7Kr#wvJsJf74Ee6}aSJ3C2;G8URvppAG z{t%$IxV3ZKI211XhK$84*ZAakaisP{e1+@QCC_tf`AqDPjB+3FK=Kburza0jP+cJC zo0OWDdUZKdWkM`!;xFZ&29nRj9W?T}1sDYaiu|tUIH6o>eJnrvEC)TEPLWAwmJKLR z)fVm|n6}+*F|BsJtLv?c$9a=6apSH)Di=O9k|v9T!8a??jGfDxdH&OXgo$p#(b}4( zt=06lM2*V|bEWgdifzLt4RV{WHy7!A#ck@l*57?mTK7dHMEon9&TRtLFBVr+2g9A4 z9rvc92NG4es(yyFHh!g;8|<08iOpgeq|jH=mLz?6*qTQH1+5YmbI_t|InQUsa`ri5 zz(;r@&3A<6sm`{)Pd4Y#RRu_ZS9K>nV+U}r(Iq{1F}vLk@3CWe913H!M2%`ExxA?U%7k7R&VO8zSG0dJvaZyDwg1wkeBL;TOQ1Zz({9*F6NT)2tq-}Vq5ve z-{prKV;jRWsdeQeiM6lv3Too8%ZcP${fuP|f*&qLVU%DMMcq#!YWWO!T}{ZM_u8(Z zzC~~;1@4HF;jQ0Du+TrmrRbuA`h@1v&h@rsK(qB<>T_dHraP`hhr$D5!=Q&|Y0k8j zOIPHL*CvlOt`}iO4PtqXPL#!uZ%pzWnK(c~oxS5XF0aP8>g&8XA~#knj04vF#FKF$ z?GCo+KKIW)1OE`H?qm>6)Dv?-T3bXpKIne0MVq8F@;`djlwb!4Nj$wOZqtfDQhT2x z;G{dfKellCX?+!rAnrtJPNjxuN|9iPReh0XFjh9BQCerJ% z-C<$iQeU8=hB;O2U;st&bT4+fdr6L}?|1T*0xOfdc|bepTm5Joc3RG<#6ZWj%_CA- z_oODuD@QIZ!`k7G?cAR8zZ2fnHKAMp&MYvO*fQ4r^szu^mxWbpR%$uM&9mi$#9JW; zS%QJmhAmy%BpY%C9Y$UEroa5dsi~r7@q6qRF7~KA4*+1y|8Ta#{1E9|PiijFZ^}l7 zo+1xwIm{NaeiH#-*4Ibh_*M%#3$kzo(EWy~M0mXa(I>47q^FWB`I|x69W3e}vS* z|33=pGVE;}A*+%;Hvi)2{X@P3+kAG!#0y;jrPupjTQGvAeuD_N!r*T4OTL--#mVNo@b_~S$P2r`V@ zW`edQ2Wj2;Zq(p^xqkl)+?D07`_J%yUu3I${&!dZCF;Oo0Djb3MUWKLf9tHyls;dR zXY1B80!6kxMkaO_N|l$)CO%??dLjeVsTL1OizYknKh2S48|TNztSvl-7e4*|8Mtq` zu$O^MF}bng!*`r7Vcz_@5ZvuRY4jgmBOureK$#P`l$C9x@tbzq!8z)bP}~^M^HPuF zdRM!QaJzkHF3d+~SR+%JUb~u6;q9tvxZbyF(-hDtsj}l2`k}9~jI}mFyq9?GCx2J>3%F2H6>(`>$s6H ze3iraY#9Ggc9QZi!8#{72BFp5CKthoGF zDDcs);^DW`izd{DOSf_5Emba+!vXaadk8-$|369=Ic*|3<7#YzY@jrqqO_wMhneFUHhv-2*!*mLAN zHE1;s=TIVV%_r|S$8};R@84*W81Fe(i+aC#juHEy|3-7$^uW2As`+@cxl^pSo@!(H z#Tln9_{MBRTPvj}wI$Q=*zE5Kr7qsXvbGKfhxjtGR~@a(UFG}7J~qT+lAzw52@`ke3*6LXssdinv= zYPC{K9Uo;A-rDPHPoG^D2r=KFz9cG=p!_(TNq8byi0qv9vq%$Ia*K{47-rYnB(Rdcg;K!wH2Pg>_s^H+@0 zlv*KFpCj9uhx%hn0}c%ph&a z-*;XaZij`ABwA95>G9c^SB9AK*?(E7&f#GQy6^OyVX3(5J?0xPZ(1KV5DaLaxd=3R zuBH&u`pcx2l-;zdyAdq%`qS8Zouc0QAR$i2y6*!C@BVW#EGafSfO@5xq#oxwpZ?$_ zp-*}XhUO;$?*2so_|ttF?H(4?hL`1Rvu;|!@iT?U4u2$AI6Ot%{RgF?7ULFkwerd? zl`HFU?t8O}w0-6(WfO#_TkxI{Z!;NANFVO@3Ag2-q@fqrqk(KS}LRJw)i|`Xgkkc(IJl(BV=O@p7=~S@fn#Qhip!%e7Qqc6*YhJZr4o7 z^=g00^)mM@X=O(1`ZL2NTSV+z#!}ns9syiTnoe2YpSSJfm$O1okl4kBz}UC;&$P6r z#v*{6a%ENd3dZth`D%W48m8Zat_#DYKT)^X`rvJTqBTMyV%o_`&(gt(+R&1&?Dh4X zXZGi-=LZ^}wUN~n8Yki6wrAghPNW~Gm1xB{t(^&5K}}4~fi!nRs%PtoU|njXHa!le z1wuT1(q{L3*XO{eik&UoPJemq#KGBvbWO1bv$K=x=k5HK1qpB^6@6)V8R4*Cv!^`d zV#Dt2x3=3>^s+U4dNn{s;u+3m;$xbIS#TTg?=&D1*?9JK6|%7MogU3!sIv2Y#S>?- zLk?lK5u?jN*n4R#_fObCQ_mZ*Q#Dqqi~2Tz`2K#K%d?U1^MWLBRaKZr#ogwJ&iQQ4 zl>cOWV>s^DjVx=%({9XHk~|LI-%H%vOHtm4-Qw~PG=@2=c>NASzB7JAaFI{z3(zxO z$ zF5lQn?1PRVA8jG7@-N*!?ehUdy;Kgr@N}QAzZIut_q5LP_^&4ghZ$)Ic&Avvp~?Rc z%nh0x^XwNIl>gzvBU*Rv_K*F172%Z1pn9!*X;qHYa^o>)x7CwB zU1tp?ZSia_P4{O>0?FqcfxJCGLTL-@PiwpSkmN7 zWUu`N&Cfc$RDFM7eCR2_CH5uD9;ZoyV^%A>jsd5>G_o}7x~ja~49uY?PpN-qy*$5H z{kUS{nlZtAXN>4lqUT^u5$C$SLmuQYO`WG&=TRypZCXRh3Wjcij9c0K#H>(nQnuWcb#jk zK>huPFelf6bvE_HlhOI&M0D6QI|=VP^CZvXGqbCDa>hAkOrZ-aH@=ftf!`+$0!LBK zCh4;W!pXHIRBnIT)EQO;JyoGxPJ=G~lfza?Oqe91k_pSKsiLXL?`ReO09CU@wCRPb z?w^RzP%pI{x*ZDaET)>EiBlM4jH)cnn>~H5&9l+f+J$;I-;OtN_ut4z?+drS<~(Vl zQq>UQ3H;eQ{p89?!9>BO=gxKm4Hh=n3%Fy_o>ul-EubQ2wwY}4df%Xd(e&7$h0XDk zk5K1_0KN03e^vkQyJ_`LIsT*g`gl5Jd#kIuXtyP6WpB}5p>P5*(=F81BKIu?<%9Fd z23IgXa~V*3NmFkUs?v&~YE;dUuYXf2J?(5ERP{-#R3E3Ho|0)x-7o3n4_7)}e2{8(PItG(CSCVK zt?mQ6i=e`0Q26Xp{;0hm+rFkC;<&QruTfD(4*n_~KIVQD7TJvvw+YWm#SNNYO@{-I zH5|n*17p*_V^hzP)CR+La@foQIY^fX6~?g4?(}Iiy+3*xI+>p^`?Lm8N|o;IkYe=- zpHto*Vm<*9cuX*R*`W^%*mX8kcUk@)pi5ty7>|MAlk7g`t=*=P#|3!ZLbr44zmdF*}+)#OR3 zN7Jbvhkl(8Ozb}Qy-fG-(ZU2+S+K*SK!I`a3jM-2&Bol@CCvg4aKu5d>HF4?Z=CU0 zwYfN6y{hxr`u?g5A$_ixlnKmA(o{c zQ%jqZ{*aoHgZw2iSx7MdT_SbqO1w;ZDE~2R%6M6N+_&lF*XRHG!!G1D^z*C+Z7}^K zBN(stn5(L)`lgH2xBhg81D#`18uWB^nckU=G+A_Hnwf~jk?7Rb9%-iD2(}^}e69S{G-l+F?%j zAY!V{{wyewR;!1(oh^*Uf092aA{s<@hXwyJ@F3@QKxX;Yc1hslzU1SF?_peYw`~<+ zccx-F2u2y3Rd<5s%!1jY+<3((#fTK7THmMPvJ9aCYb(A-vZx%S%h|1z*yT=k*vrF+ zf-Pfp_Oe*5_2cwf_dhGo@eA|?-?$1{W&SG3yldbnCS&^8?HfC-Ql-cAN~>@%Avs^4 zONFS`E5p>h9>~_0ZnZcG@)1|ei))R(bF-BRf9pX!zgGI?I}sMv{=3)i@qcD5w_+HZ zv8ujQH8;XEo`DzDW9RYdQjNZyV=A3)lt^9X!{&4t7oGo9*2`*r+IaF7Z@HU=J^$H1 zxV52w6T|?~pcP;@FMJl0xRVb$Z=!4kGIBcL_3skz^jvT1h5Mz1p%Q|U?QB6&UfYA` zH;J9+(0c7Hv8OX1&+c5vT$n(0ep}^waJWkWUY8V^t2Eie?MVX9T78tjX@;lA((Bvn zxxn+7XWQ&((eOH*wC^3(MFn3)Cv(V@V0_0oOiC30sf9-UOCEIREFT$*vr?J4 zcVk~b%B2#ek5%r|HVaQ#Z(X4BaG9sfZ;QKs}F1K7)p}cc$X<`Jem}0C*QNA1ze(y=Bg51dZ&WH>&jfTCYRH_;?#UTTPm4B}ype zZFpi6ttpnz(Y7otu$hdL>`?8E#(g6C5epQg<>n=>j47Iu7GzaVE96BilEf1@$miaC zyyrr)*O4Ca_*6ooRXA|D3#W#Sg;Lmc_?=}~>N>yLpA1ZJ_DOIA#$w#bu2D-8D_+Cr z1;On_&An7*j?L~n`ApOa%Y|kN2`&+x-DWr2`rse&ZPLqcb&75kS^A#DU6Q>hHx`j~OFf)D8v<-x1~& z3)4oGFe@sruRcM7H-%RT-;Taa>fd>~=RO}-5M&fyDA!N83Pp_{US(cza*&N6-^_m;jY?mJr22~#naZ1 zt4qa>*SNib_@$QZ3(YIu-W}-1fxwTytEOgl<(p`Ui5wkraw}!;HRLP`$lS~w!h~~P zwBT4m&w{TGa{mCkH{>K(n_o?qTL5*A(&1AlO9c?c zDPlC#=?++&7ey~b2dj%@gG3myh)&PeUP2qxCbIX&C@tpdWWGB%`rpsbEAz8c5c8b- zNQiG;w5r5r4)xe|64Yp8|v=`1X+RH-EYY5-jP*GnXleb+l&_d zQdSyveFZdzSgWKjI5?}{Bj;C+EXD{6Z~`|TZH6X{^J{5a8j{@!12$yYc2|0we8vEt zs2Hv#8UB%*DF32ncO>3I-TO2{uQhv#{6Y;AP%Q9hArI5BsgLT_){0W$ajBK8=Vq3g zBIZMSuRr!)Oo@tm(TcnoddwqpH0Nv5Zhgo&3*(kGy8I0b+ops%fVf*gE$Aj@U9=|_b@{&<>~ zI-yUe54MW%yz~ve%`y@jm9sqx2G5{v=s9hr^(9=I* zB2XBea4xoMV?cvRMeq6-e1ZDiDOs+JOIs8YaZxS<=+=Pp6{J;WW2M<$-%A28%(Pw_ zu!*b@h%Z-J+<_-oUKi;9mXv`@wr9&DT_`U__ z!taT1 zZJs{#oR5YQNb@cxx|SbJSQrSHa@iqyhw=J%V$VG}P*NzkNnr>kb3C`Sb!gq zCiR|N)zuO}`4&Dsm;6kb;o44SCmU+2xNIV!eWgo*nLiz4dCb=@yqThk z+N(e(5q~r|esz1Z(^9CPaPt^(b-ri&DBbeTyWnB8UG{ci6=|^bsr89@^+$~)nfD`$ z?^DnpcDK3Qi*MXg7LS11K0mqd$6u;8yG|IS5;N=D(LzbsWkdDUeJ}=xs?ZWO>`+Yf z`+iUP&F+8|3NHy4=DfVwul&v=&9v=9It!Q zG3Qs6mT)@Z4ux_F^$h6@i?R4%5;g_#2$`j7V)i3ahl!Y7kPZ{EmKPBInvS~|>YL6GoD0hSkUXT8u^F1lDxM1%`4;p%*`(1@8g%{#40=kmCAwdGo zwb}mCEs|^(PHqrkulYX>X7YNHi6FXZ9{;WrAIxFX*{Mnj=LjQVAz~c&Lq8UI4#4m@ zQyjwm4Ij(I<}541#WSDRn#4GMsdUOYHC^B0t`o&HZEGVa2|K?Q|Fz|i;!5VK-{%Rj z82+Y<8WzXTJ^!M|qJ2Dcv(nTuFh-|I+U}9Ax_aLX_U*Sqbv+qPb&(A71rW$4LOow0 zw5?*(+*w5?#SUv;N3V1SeKYe)=FKGmf)M!>QOC4QYqb4#)f75Wek3QcgJbSIH0W?G z87YzMt{X%mu2)~IUmx-XtX{jY_4aTw>7Oea2)4U8mk7h@t?W~7s6(ymPd?{D97MxP z!{P6o71W~f)^jJXAogbynLvjNX=f~{v;3uGhXy7PE9A4-Y1#-MYW11wyDh`3OLb z%26zEC$ujZ`Nl4)Ox8|({r)%Ct~xoHZemIL?k|gH2&=L(TAB`xAS#O zurd?ojaJ~OoUjY8%WuyU|8JHSXa0fwTjQz-?K@0prMqU_6*X=9eot4ENp49xWO3v z*A(uMl_k$4{(MK^TAIah^I9~ugP4-osTN9Jwm%L-BChsyRpu+~_h?&WG^|cC)v?x% z@4VA;XA4$E3$YZUzugg*`xXa2yg9)36@Fy9Ihq(_VX0fq3I8q8+`Gf6@1-g%ff6Gk zFwtIN?A%&&Zkd+j&w?@#@>(Hfsd^v$Ky@?Gc~Qy;y|YLGaGn4#!5#^^|47^=pcFGH zr}yNHOhIhHaSLqO39e(64%B`gQ=~!zCUKb6TW!a}? ztdLaTe%}iAKl#T2o}Edx%;waF3FFun%7>f z=J&pYO!*eUbV{IO3w>w#dtQ&r6L@R%X+lAF-zyDb&r$MrzifS7za>_dR>vhW?Sr7| zidR-U{6UazBfJD};bl*zYNlGo%Zmv05omMy#E@<2{gW42$)?2q@2JO?)>rbYPbZU@ zxKxi?b%*0s*;`*BybV6iFDUCx?AMYg?>^|%cR)HU?%3apHr*9+9KB84SJ$(6YO*yB z4$|dM$PW<|JzHYQ`8sYS!{rjy^`8*<&t10p@-K^Ax@&iB>NJv(Bn3S7e*#j4!FsP) z^hgeF&gA1jFuf6GF6pSn#FAYa6gfb=slPgS$_(B_tkL7xWqit-xBl49`n!hU%h7?j z{qu!m-k=CL1YWC9h9%PY-L#I}+49fD9Hl?TK2u_SbL7uRL}HQke~TnU`u$uDQGepp zq+?EB#-T?)*Z$%%a?}q3rU6;?7ETUevo6z*L>=t~*wszj z4G1J6-2^BGe^WBbu3eu1TkBSM!-}EAB;T)+wQ2wuF&Q}pLz2GhlA&TV#pyK4W&Q4zMuJ_Piqm^k+7qQg_z*u}o3$`|WBZ+y zW>a5zYqk{)#>ce9{gDFT=~fs}gv3JGSB|Cq-mjyeNC%y@CT#xN^OX=|!3L2Xj*0OV zH!0GO;_J*Td$iQF0e1+pH_#Wzh{yyB8?BBkeOW% z{`%m>aXBy^s=v*dwvt63l726)$W|+k=eGg;cLO6>xsq)T7mDS*YnmaTs>x~cHEKqv$@wM)yYKk z-^8vcz^zog^QA=vn!J2}O@!>3Yjn9gP8z8!!i7%AE*@n_BJ8chWwSKr8@-F({E-VL z#3yL`GgMgu$e>!P)1l;MUfCsMR+W~pNSIQinTpMvEg_unMG!)RiYv#O(!@47sKq8J z5VuXs#TJ*MN~h@6e3nLfX(_a$@xwY^PF02%xa-g6-aqCiX^e?>v|&`4;xzZTY~U2* zqaaTuk)3%%ZGRg^{oH|YZY-R~SyG0y+GOn_v6(LJ-=6#~wC*3c(8&Ze)Jk>eJs`&k zR|m62pQITHQPI$@h-OfMOM^oZjsRF0Ux}_^EYO--G>F~+@#JOJ-7hjf4x1!pJcQdn z0h({tGmwvc?$1rk8AxQCu_jt9yp5Ag8+)mEMDUn~Iv+z{Bmurx$R$5$3xnz;(v|rUm+EY6F-6lj(CM%40|U=csn|7z?7- z_J5Q4?#PIUer!!_DW}}FFa4k;R=mb%(xL{~P?iH2FE9WkC)(Wch`Rty_y6J8zkYdP zMuT!^O9!)P25r-eGrRjaX5=*s@tGW7x~Vk*Z#j$IuU;7@R_onlerU1Xa z;sttGN01Baowxy=U|deu+6z_<-y)VaUySgxgC9NV85m?WrA=hDF5|5=T1}&lqa_oP z`F$~86~_$Hm}-@pYc|4#%`ALuy#Jek#crjm`RxBjO@YKy<2nnVaVZ$6#s)?u(9>X3 zpaGZYw2nI?3H@=jgGl6_88aBSx3_n~;3zUd23}rXc9T9V&1Uy%6H`-qB_$0FIx4Bi zDOX^yL!zG_9Pih|vnH}YM|ea3&&bRw$gsPzwJk*Cd2-y#eg;k$HT5oHGlBg zVB>SU3%c8?6%l%H$oJk98Lg*ZSpb>ppxV~NPTe1*akZ`Z66&VE^v2DyPMGLyd#gh6 z^=%e}bjnK8PUdRXt*YK%&YM4!?w<@|9i@}r%S5X$@k74g;ir3iF%69%gz)o4E*w{H zje2~m@gj+^{x@Zq;O-63(yHywpgRmt_VsgUxaS)&?ZHe*oNAF8&~q5qbZa2N;c8zs z1<3tn)Ngr4#ry!cL`(o+fCl!j(@TeHGH7Cr`frH!yaNA{OJ10qaR<(J581;XDoUvY-eorr&xlqTX{pzeK zy!T0)cN?q!?cE34I8c1}K|KP}R6&}$Ieyt3BX245;@7R(aJl7>!K-o{h55wN6^2_6 z%>IhF9h24ejY`9VG>j{Zyy~IqkCAe3F|=K_EJ!}ASxgqE8l|*4iS28iza}&7XF^Y# zPte^QsAWpnsad{t|H;v@wf6M!N!a6Lom}-N^yvw&_2k$ryg5mCaAC%IR(G(}Syjcl z@s%Pho=O;Lw<`7?Ll&}fPST~Qs5Hw|0L)%_c?u`yXA2*Fz=QwZx^^wq6o-Wo)GP9oM zi)4Fa8~%;22TOLKT11IGq5FCR?J@Z;@H<8iiSJtswd<}4ldAMrgQaLl9rgZ{a-eXh zv%LTv@QHUHA5RLkIrj?KV{F&*QdC0HkV7M8TS}}Vcx~BwmAI)IMKC^Z?s-KRBj1i6 z7%Y^RKCz|m*m$NbI*;^wU?jVdLQ_yy$nxe+er5~5i_yXqW{DZ6_d2%Ve@To?8Gt2< zOqhZ$ZGC!i5m!~kkpiTJqrNyb@$rd{jm?6C*Au?o#J#;#&FXh9K`Aa7g2!#U$HW6j-KoEiB=|krsI^JsZ4nT z^U9!HeU^u2lECPi_JTj6Vs>W61|9lB5dTY}stSZH!ix~K0(OHhIp!_h{RI1^;C`Ed zg#FYsBogWV0!_{t?BNl7gK%=lh9yQbMi37AHtb$pFweBSygXs*1G|{+Jn24&Z|%>BBP$;39rl^>Fb}|HQ}-h?xSZ+ z7>>%ZpLE^E0UAd)LG%DOk1LCsT@K#?8mBXoB>2-{@V%9yoBdSLeiRh-CTFY^# zR{&RMcI>d1e)jAPPVxReF*T>)rzq`NGKglt?d|^VuHqR+?`hbJ(&RYk@AE(-_`lAS zk}B@gE8^%+u=Rwpva(Y}uvzWB;3qgTY%^JFpZ71aYiBgef>ez^{U+gw&kp*W$z~AuZ5)N_*6y=+ z+1XETp1j@=OjYBmB?5KU?S_VisUK}tB1m#yZDNFut~o0`2^oM>m3qHQaBpcWZPfp9 zsH|aT{$9mh`&8b-;>!UioT)d$`WFmi0#No`DURX56ud_UiaM|1pWC) z?cIi@w5aHs1Tk1Fig|@S^Naf4hMHyLmV`%G=BHh&s{{qCmP!*CpASeVG%_@1Z+4y0 zWBjt%1wXqv8KA=`%pJ`x!y(##att3qBZH~q9!u&qKE^XXMEdcSqOI!3Mn2Lg1~RGI z;;DPNc&xJVVK(#KTe|B=1X0m^3;0>m*-nTKOg7=4Z>p%_gU-x#k#*??Ibzm4@q3)R z>$-kcDbcZ?BaS4OlfJUbsdLdEV{4D)ojk19!?A>ab_pkgxHdtCWgYbo`J)T&*Om>v zxajJ@c2G*idZHgrIBL>>Jbwj+RsD<>~69lHXyZEd*!t(yOY z=8p0PdJvBQsez(q799FEoH6>kMd{YOPhL4IYb}J}b0Un-hU)|-{D@7mHhT4(k9|ip z8e3b&B_6TCP41!0th-%`t<{NQ8K?*}$Pd>;9$`%|1DiSSE@+UmxC7UuhrP0e@|MBs z@?q`hjNxS)MNlwY0HQ196yJWe0x}xU=TK00ZbGGvW3o3Qfx5bSZ5NS0nX|m4bsJ}| z#<~(*B)6JDD2SkUCWDmECgdFwhDX_|uh{(lFdQ44Xk2TGFuF}uxVny5fJMe8UVPs< z=VRp`GjOQfwYw|H(9mQ>*4l#B4jeKU&ngbFmb(BW7DJ`Cwri^qd=nF^kk2UDpI669 z1gnW=QXe{6jCX7!*R?vta|j@9C?a*&*YYD24-)%Kc6zNe z8lUFzE2~m1kn$xm$&Ke2c^BP-00s36QKaLf%5$QlL7Zi~<>pMhv(GwA3>g^;I!}W4 zm~t|jswboBU`1<+XC$UHF=3|wj_`7Qy&SNOt~7iga1QSnb?Ow8Dg%&ST(?`n!oel> zi1#4`ptuSb>o%$-vgntFQtXVTmr>%)@X94J@)nq~28&wzoK@0Zl@rN$48M)|MOx7~ zWq1BfLEjXeLEsAnj~J$g`&MFq^0OQa%r2V3SEyh1JswGrmbFhE%*F~_ZehEvJq0?a)vZO25z3Hpv(lL9 zu(W{UTE@CWWX_5Oi4b@P5XC5r?6+)roUgh2&<)E|clb4eZA>MY*;N;v zQK(CA87u?D53Xv$L3L^~I(9a{3Hf^hny?7ohIu{qee=CD$26qF!YHmO9))pZSo`SX z(6_XRh2dFtG;rnHJnI3SVa1~^_B(Rc?r_%OH&VVdL)62V5e$MNNNiQEN?Dg5V{fL4 zghbMCWiWUBa997-y7KPT|ENq2YZxq4gHw1bO*u4@+Qj;*#Ez~3JPmf-dg8TigA$(g z#-%N3jXRv6NK=ybIlp_bu2(jZc(e16n`XU)R~KSxa94DT5&U&KFsrNSUmu8`$zKUpbbQ1xlUR;~_p;W)PYipnJZ+7s= z#L2j6LlBpv_OmWTE5<%Cmjv^xC+4mPTd^-TOZ;2cEZ~w5Xm9?_^5jl=SyymvS4_=~T9Z_v!h0q4j zdTm`iNTgZ$F?HyUILdirj@?RO%ju~=FN?Y7Jd($FLfY3n{ z=|ryN3z|ik1)BcZda5;YV)}FMbbJQe%A+9D&FsIrAz35!eGw-u&C6OrsnYS9{$J&x zMfkO1-K2v`KG6`Hn9D7xv}@9HHSZ?xHJ1;vu{qawu1GT;5Pn9x{*GcEzg(y%Vg$%O*$9i6J_IbxG z&Oxg;`oo#48!=-Ate@0Eru8tsfg5rgauAF!7J(~gRk9U=KcRDR&AOpspi%~y078&= zrKxS?pWr$L(IIs)uX?j3F0=BZv`26upw8E?97(?dRuAnopzXe<24fisULRLH(Y+gS zDZS;Q*FJz+!w5bAX$dzm4-GYdv&s;K>y*wn1dj5>EMg{?27M>--CFZ3Q$;)KyNUZ| zTI)WD*|tg-Vgwpg^wck7e04O!nXDV}FO@7RN!H2Ch5n|Kk}LncX#|~Fb5XD9(^NMi zNiVK(-6Q#^Igw4p`^}KOvYJ_uAp1}nOg0vQ+}c0aXsCmiq26w}>a68WSO!knAFG0S z=K(p7n)5jbRwas-gCRpj7W5ep&AU5=YnyZy0M*7akbR1Db>zwbr5cBVu5o@;Dd2Kk ztACF;Lo;q{Who}tYM?_^)%m0GwfKXWvHOM892$;{WcTsJ7ah4q{6HEmilvNJ zI0Xb(jtmj&@+OYxR@m<2`892dfcN?3+eRglhAdax-#7GnaMBY2#lQZpLM^|%D`UO2t|LCvXx_7p=T0LwX z0W4yaTn0L>mFVyzR>uz1zHnqOu-+J` zbRhTM>kWC-O=*8wm*H9PdmnW-*AwTt`Y3{>HW(5zTM;BlZ7zsrwS{IC=_~kUq5YGc z@H@nNZ(HIDcfm+PD@@j=aVc9U^2t?uQ;AUXTiPy&1^yb}^+*YAp6|(0(=3PRj+rq2 z?*7XjyWW~B&2=)d^toU=>?+^;VTeEkq2o355ST#eRRkJBGQ_ESH7Ds6G0oR|w^V$V z%`6~?#q7_!aKdKmai%qDiJ3vBhBa???1qfgV(jlay?WKYV+$QBUM6`hk(;pp&7mos zum~Im`paqvh!CLTN)4EUp@fk%vd~N@DoC928yvxqCp{UlA9x zX(^mX9i8k>LLVcwog;-3u3KuGsG4>HEu|40_)JXjF8v^EJtEaT##e?Xjke$wu9Can ziCN<;Thc0zs@6vzh>uqjYgh$!K4-1o<=tHGTw0Caq5#xNb6FGoM&@9O_{#{$qvdqY4Pi4GiR z^JeKFz<4P6qM<}7W;`$&5L0>!!*OO*sWLl**;mw_)jec7``GzC=k&u+VDvg1gu8=w zEoHOVR%Mjze)J#rZga>XVop&>lO{X#JdDA;LQm9Rq5vf=enF3-l1e6QfJV_ z#Dcw+sQ-x$R;#zV5xi~gR-XQ5wCEVvmRd*{=-U79*Ut($5A|Q-08R)x!@rb%c1ZI7 zwQGT=`!d+~yMr*^!T$Zlz_Sq~L&62lGoSvkeV?CMTg#jidG=+VABt}868lHzKfm@5 z4gS};1^$nSi}}A_`-bdaz5yO3{2-9`mdnZMt4xwb&V64fZwjAA@Z`!Pi(xv}Kc9^y zI>MMXiA_wwQK!8?{qC(cMh>r2}AHG}`(djxc)Om;@h zpF<@>sl13Zn* zb_!Achj6Ky&-*LZs@8&ogWn1XX{q&x(l>q`oXA}bUtIoGPkiVcU6$FDCA&Rzt9<^i zh6moBO*9I)PIOi%|B{*)?KHJ?uKe@x=s6U!DmOJ^ zK3_pqh&dO96$%WK9k@QiuB5UwTjf(4#~~n~{%rMM=96`&-L8O@qQsoOu{EIAqF$$y{k z2pPqj5a~Qu{VOlCBqxr>-0YDpgvGg8eO(WaXC|R*184}}LHFly zIXQ7UHQDgZizkS0l5RYUm&x_=ltjO)pthrEa zWqN%$Uum!|s#%@^Fn0aL_5d)qa_AlbIwN*2z3FxHa50+l`!8EO8|mVwghbFjk;Qj@ zg?*cr>9-g3+-dO0S`a6yzE{OgxZ#lK z-D>*?BfgS~v@sTrTpXp^j4*6jtOD9w9>hpBub?OCQaZaVJ5R+KvPvNG3ucLR%CX2S z3qNLh@q2@P>&U>O3+%t?c%FHc%~f9gIjEcDcCj7W3ii1H!UvSBtkOWhfZ4I-JJn*% ztxTK!J}?_xtL1M0E#QFUJR`3e8-q9m8FhIZ;-zg?t5V0mX{TG1GsPkI!8A@>l|!c3 z?PGyWBKq&v2@+fGtH?HEj2&l>Kr4^M0Ce7-WZRD);_q4?Ka%X|(~Sr}C|xk*BeTZ8 z_WzmoI97YHAe>yGL*ZQeB(d-ZHz&^TFBMm@3ao|ym?|3M3*DmDsfOCz44Qwn$M8ijMY+*jPqhq1NJ*A>mlq`ZN{>SU{&4qH3 zdJbwiX7qvh3;1_eR}*WD%i!Ow7&gF%4?=V06}T@3SV$>p*hvqZUDea0WCz4&9D2@DBj;d>S&=8@ z%OD3nA|pr_>CI66j=!Lp3eAz$F^+ml>*PdkoG8_u8STbQ|(ehMR6toCx{pSyo!GUhWD{Z(Y&=KlA# zY<}bIs}odQGiCGgu@|u{(#uS>`D=dkfY6N4rA^`yk8u1=A3`TeSUU*z zm65A(iE`;`TDDQU?OHWVRN$#jJtu?yaU@eL5^mVm_@*+Uv8%uqo$FJ`dV2s)iOXW7 zry%A|HYqYDxAkZO=7)R2tCB*!Z2fx;WL~DokgB8v8K@V;LyY#(X>$Al{cGpbhw!_6g5wzzqFw&FqgNuwrnT#Oc!4yQfNo2pa>tf`Wzm_)3=olP0kb zm)`Vz>exXnC1Tk*p4Mx3xs}+t`O2Pbq_wlk@uO>GP55;g8`RV2u80+y1-`FCCd8^s z(pv1kn&2rAJb?owv6cCq4c7M^~#Xod_6@9a>EqHEYnX5yv333!!3G^@u-|Q!gX=euVY!*r_$PHhq z2bo&6;wL|@Zz-;B8!{uY+m&~jeoo}Ey@}&tZc*6>OmP!5(5tvNGSJq?T;ra(#Y%_2 z1^uhQUlvy?GZEML6y+%Ot0Hdr2(L|Jk&(bm9Kyql^xhBPUC6^4uVNU4o+UF$X|y>3g*!gDL3B?v10V$i7Np_0e8L85nsXX~RaPl#9KGb&^K`O7TZ zbn>B0->^)$&qkYSYvth3ZYI1NY-gR)@LIvsrb`MmDLSDAj{H;0a*@X%+H?-%R^?Q69hwsQ#@*#-@YOi`%T#1nq~I`+8T9zN zT{LFrW?mx@tteo|5nUghh8RZk+{Hg)EW4V&zjGf zPJy3T&;x&|BEvGN4pH0#`>7>QH1mknBSqw3Db}pC0;-!pG~+}BsiWQFdep?%Q`Y9^ zt21=sXA82S39~vfGy8W@6v5aY@5GLdg8Hf#eEVORAcko)zf7uot}GiG#2{ksiY3}f zcx_(pe`cUVK(1Cu&|0Ljz&A`TT&nzDUL=pL??+2AQ+2DberoMweHpg!fJh62C%zG_6ibZQn z9JRF;b2G}N^^cCy82*L^_Y7w6Pt~_i_|f$EK;@X%nVFE&A6H;4s}k*+&p>s{yx4As z>fpI{J8?KOO;Wy;d$9U6GfjGk_>gZnAzlF9H7sb}#VKe#@!m6cc&AAXtjVzbok2xr zi5LdyNIa0|K8QJ(*dLisw_I9e8(niP516MUeDRe0+1b-^u=l46V{?qa1m8Hx3#8En zOizT+xbD9xg%%JLPgJXe9m}+XU^?(~JdjGwyj*0WuT>I%*Ol$qtJ($?BL|EnT47bo zQ(7lWqOSiIG%762cfqNVTw#n6SycL}H$h!)F-;IX7r<(EMpr}zE*NA4I$&g1T;(|` z9=@{j*F6s_eR-R-;??r^9UUEAqoewQMBkT~aImoGY+Q-}vqIKDG5Q4}qKu(ZKYg(J z9IZIOn!Be2-xnJ<`vZ)wEbG|V%c2a0W9x-ptM`7R;alWuRN1TC^FN?lm`zX@v_kZB6SB^(xZTxq*P0c8?V6vVC(bGAC^H=W)ODbMW_0y* z_t$*!#;G?BHrd&fb^1{3E=ZA?1DF(>+j!D|X|}n!*?+$EmYR`KM^seQbn@3QgLtYe zPvGBcM0Dkxs&(5;A4fR<(e|NUkOABktT5N^Y8!PPNq%9@dePo==t#3e$J+5Dx@=vj zp!r7GUhwf|9*FL!hOrrU8E&h%j5bgBBJt}a1U^I21|bjwq{Kyz@p18M%$2y{6_-3# z97POJFvvxv4hS%VZ=&!M6V1ub7e8p!`Sx9d{Ya#NgR^$**19w^-bL$q31q?kh>?!?FMf)+-Wj5_0r%t@au(FkJ~cn`|r&4`kkhpyFnHR=b98Q z?wgRoA&I26rn5H|Ec9_0#T1{@g`X|@5LZMo>+nlib*w;}p=7q)%n=?3?vDI2I?DBcnoQ4%dF8BrD0%g428V0p$IZki5`~LaQ!A zbJsHoi!?6PC35e%Ss(I`6zxE>tI&;ot1h=A@*F14Z!)g|AVKGSwisZ}{~t|P8Bk@@ zL<#9G>5`Q0?i7#`l_b#!!M;W!1TILZVjCl>OD^-|PSzk0(XUYALM>T+9JJ_REl3*(f-{w;>%I^pQK zkBhA*1yFFKtF%#E;+i)_*}+JG)jJmxRx+-20~k$7Ijs7}nYg`d;L$zScMxK%rMEmG zw{DV^OC61E)`sCWo_ceYdnqpA3Jdh^!6s8vbMC56ehcWtj4{LI*!`sCi6OXpado7r z;WR?J1bXhhmJQv*1}kotOP3@YczChTT+Hw@-9AM1F&fvNM|WA%%Wb8^#omvNZI7`B zx4U_dd)BzaMF|K8bp|p|WYIX88?o&Tz+BU6ki=T&O*W9%=#UP`&&M zf01b|3;-^vVIO&mCu-3ZTNV`Mt2$gF6;w7#R#iD(8@xH(a0Y=@Eo>~N!Du97Nw)1; zbk*%y6!H8&uI?1+)nvXXl!aLR7^@;p^}Lk4zHIy<&)vlb{mx|cCpy9s2~8*)QvT#B zozC9MO$Cd+wnrn6?Ng!$R}G^cQ}&K=`MHfb_>Z_n`eQPOeCFXQw?zU+>vIZ6>v>3x z7r2o_F~>)hmIc-)(H#UnX1DNqcDBgi`;6l2%?V;87a2dmcvG>;=33Sue79lFzulKE zv^IB>Nv$|%Jxay8M&x_K3G%7gq>yS52CHlCdHuRBtu#`Ikz-1n3xogLa0wwA*AGK6 z!+Z>Sy%3qu1DDaoF!U(fQAU#ZjSxApJ8fEGspDub#{NLvxBDTx^HtuJAQbo!uNz%j z`fpm7LmMq$AXiXA%Zd1%qlvc+;;mhy$T#Wie@!~h5|EJ-rVVAUnrVPT2p_~GBB1sG z`&Zddn+`J5CiLWE57N_K5j=-{n`BK$RQtZ~^K?#gvAC}{uwQL9%=H4{*xD*n(2o!8 zK3DiMZ3K)%OzPVDz_39b0nGTrfux|@g+ySQmpgZQl|pZJBz`OXA$k-$T%)5X8{veDD{~Zq zGj-O5H1#mbJ(Zklwj_fw4)&f(PR!fqY`-mYo0@X+O|{6go|VO+<`Wa^aXkAT1GmpP z{xopxE3+R&8PB-6`8Qoe=Ka?&Q;?`<9YZ8o+F3dyY9ynTON}Ke1#U0+0ynZsb@BIQ z?3Z+ot{>FY)*I2V@oHFhymKy=V{g^$Mld10jx-hT$YIs{8S>WaSG%?_Wn|Z=Snba- zNiZy;?#1`eo|OZer>(>#)E}PF*j(4VG%D=kt*$TC>><0lJVEWuq&|1?UL2Q3-tzo* zN?*z}855(F+x0u4(t(NVBOLu%*G%p=i)PmgT#i0=ABvLHR0j!dKc>8ZZ{01d9FDwR zpG(Jx=cnXg&<5Ww?tl9HnJQSacM04luT|q6hTf*uJ-fhA-f|Jqa&cHpXQNM5j1Ov| zrz+UJ=sjM!oJa*1>0?Kcr9DuuGCzvekiBAnj9$_{tI zOr5R2$S(hs-&(9!J914;`ViR&HZOD@#N?e?F&^_=j-nSm;!Jx!O=mA?t`J$tNjuWe z$c2LGTfDg(pJH35xB6Euw0_idJ^ub*(EFQKJG?s!^N@DWLqt&v*&Wz^U*H{eJ2{ZH z^J#PuqLaL-O6Ky@!agd4E##3PvacQnB*;G9>!^G8Wn&ZBThNE^DRm4 z8l+a8&9qt=!Wmvzcr@r5b-^a;8ZBxwmgdE+7t8lpuXWGNXh8Zp1W_M32S1SHU1vlx zJxVbpEAK8|C8YBZ9o_EYKA7Sl6je^jG=)L;skScKF65lK-6}>+M$R)d24=UpV7>S@ zz@#PC_a5h3)$RG3dqhCXxt%J`%VsH^bz4W{Vs$Fp(f7}_fO>;@<*B&8-PxR$Z8093 z{S+k*rC3)Nm6vA_&!IM(cOO4yP8#KO;rN98=+hk*Z&Lhw5S}>cu$k68b~(g+&EhJ6 zDYP??`m)J2HN}Sb-$7N9w-)4_TUCBBHjB~Nh{OyLCKZL8|@;BoPcb z%un89HLh3DK`)lZ*V|_oMC+FYl}Mu)Zz5cV9veIV=5UsqW2otx@Ji;X8%>yjklQx? zE=U509G&fKh`#*B1J0{?hyNZNYO>|TH~S$}R7dBC%p4{jiW;dW z^1bD(D>CEip311DYk2dL*_5y?ZYzN;{c=`YMs|@gXXIg~Cyh zmCW#d-{31>_tTbASkkBgta)$*VT`iea+NMlW( z5=lcY>PS`;+SwJHVGF5?YOPi0)*JF9fD-8EHqmAf=p-Q(=6QoVrK4Wdh_~@;1ME=e zq^3%8ZKYoGVcFJWpZMT+S;9|?>3n)_wWT>QL$qz{E)K6t!G}vvkc{i+f(T@w(gAtv zY0;nFqPNArFC9*inT|S`dIEb+HSK5#qB4j6e4p5BErc6}L9DDib(Zr|Gy&0>JE|@B z?~W$;*NC8jGQcMN`FW`PiG7EQ!M(A$d{T%&ly;!hR0v&pjbCZ8e8CU%ZJUlGBJd)` zdNcE9Q#@#l(8`FZP*QKoa{TSf3f51`U!H2F@CMe8xGspjc=zeO`C=cwG^Z%}pTP($ zO2b1VVv@hBfrOIOSE@J_zYvEF7aZr!U{ zLK4DFu>+~AUeC-s@M9W}ayJeO^|AOL(TlRxizUnQBX{^}^|mdVU%$(gF*bzz)OBmj zD;+jA$T?40Pggx}Wh~qalGMJ)nH5U!jeVmzN09MYyTWJRp}!Y%QCw)KwVqOS}je1|H!9HHuqxIo;1SIjMfo40sb31RLCIQXIq_I1p~BXIOqA6Ha<@)91)yff(T>!TSc(a6a#8i=1Df1F0Wia-fP zH9J&!S!j6UE$K}u6Li}NB(1;gqlBxz4lI?IE+Pby>cXN!DEZtOq=yibp><~V4FoCq z(Oi!MP1KRhc9Cv`HP095UuD1IosYPGn+2Ju=ZP4rSJ>r-Nb5_&xv@Ikoc}ielnddV zw_$sb^j?Ldl439T{Ne3YyVd>0+HD%e8%Q^OK#RBe7M}1t?FbqTEs7L|1k-iJKD0!q zURNzWjm02fFEK1^A$Pwk5!%RYFWt;!HiL7|qjW9~MSD6{_+BDHZn(C5>H-mE)o;P? z_N>h9B@k`5JJxgvO&Cl&div#?T79GX7lzU!C{*k*g)yV0Qjk(YGnFZM?2oy8+rnwRd^Zt6U-a|}V(Fd- zxmE6>_IS*M(se+GP#9$N-epL|_W*H+gqjTA8r$Wc26&9?-i0#67*dl}Zj}*l;qyfB zB%0gXoqGC#Ja2dETPltG9=Jmm$NFeq#!R*<0{wJcc)A4@ot_U1%USp#bpFlWR>L1m zd%=q$ry6Is{iX5RS9lW^>7T#%i>FeZ9;XyyML1JMG#gf8O6qk#=P#<*9~T=OugT&c zU4mu8@H>vXSy$_7SzT;DuRj;UOC+{u-ak>(%8?)o(*A;Te83?@7dyYhQY_klIGZ6R zfk21a(5bjPvNiTSgv5D6_xtxX(azU}7L#HC6CrXxx#p&9uPBP$(BHKY`2z(;rzkPV zVpTF5ag!EG=aeJigk=_hsI$48F1-Ds(P~=y)UZW^0FRtPnnJu9d(mA*YBIgY*c5$p zfw9 zX5BlMdq046!(f2|gW`hvPw{;Tc8l-1BnUHEa9HPu&>JYcr_sG?2|{A>&Z%_t^=9OP zI^MNqDbT$++J5Yj1*tQ&LekhbqS|_-YX**izf2*-G{OjadTdvH5N0-%6ie zVw;^5`7fXJEUs@j_iaL!jmTm+TJ=R}rYqWFJ-29Y?_^tQT(D*=kM| zw~h(=B(D8$hin6@cGlw;KdEc3N?ZADW~BIh0Ae-uY^{Qh0xLJOo+InVFM;d{3fs;f zl94mwkc#INuE%jZNvmJ6^T=4YmaYk%9@8u>k+tw9;VtZDW{-H`2aAebWvZ7;VTF<1 z@y^b87Fx|>kHeOJdG`@?6KxV>eawM(^c(Pd7_dK^lG`@EN!9!61MP;`!NR?IFzpqF z_UAIwHpQGx1|9+oDY|82F`tFa%4$!L0oe&9Ltm-+29wLM0HN7nO@9x~;Coq{Xt#vs zGT&oKoIjLz2PQ(v7Crn%7|cx`lR~W?{W|yr1c$>8dz-=eisjk`@n+(s0%OO zyWN1)_^>c%rbUBcJy)?N{9N8LA6l5@IGrw0jgpQF)Vh4s87a7VLh5NX$a8wo_I(Yn zMQ8tVYyo|*dNHf86AZ#FSX!=Qrqupzq@6l5n!#@>!iicWzRG<;#BuFytSaEAb?%+H zLcQen^k>C_xgo*o)Is?dpD#APoAQmLM9sT{_7~;@KKRY$nx_vtmW}t5FGDBHtyJ8? zH!T7!tfn0S(0lf10)!(i*$NCPF5~e+hR{|!Q6)s~k0-w#Ncvd7<_c*n%HlL@zAw<6 z3BbeQ5M83$UI8BLR%=y~!4Nx4veUg9vz&jEB_1Kn6<|3e`de1r?|& z(t(Ih+qTzk(m+a&;MVVN5B1n;Z~~WdE^G|dVQ09vlJbpWOjw&HhQ2Hz#4P9YxStU0 z(;$mY7KeOB{vffZAi=S-0<7EnG**sQ-U3(WNFnDKxGw%|Zz)G5AjNI1F)?A+dqoTl zI{zU$=M@5k*$T9Ltt=6%yB88-{@S-aht)baJ3nta z%$L~i^gU)PfnebgToqo#PSdXZJ$P5{%KLXGCsf_P`bMwtn>DUn^vGQpI}G}X(Xqt( zrrkg3iTr$;ukojOajEQlnl2#2pqpG;%LS1`Z5#-%fDHy7sr7pjlE?T`_Xs;2;ycN6 zn;hO`KT+I52l=01@L$bpfDW&1{J0?!BuE@jMp=(6+HE_9EUo4|_1)1oW}!gjJYaI$HvP7>~z`GL@ z4jPPpA)@g0@u^xdZRoUGf4;wUxP56$0*CTcvceKHTBeh7bevST_HE}#{a_iYezZBQSZ}KXXKD*2&{wtkFXmyWKU{b-cw0nWXyqa996-8Z08Ci!wU>N>xAsciRN$LE zhHFhX20C$)j#p;kxVgk|t$Mm9g! zXzmY~o(R^-#h=fRVJIQXBVJ(P7i9PO5i};mn?|jyGJl&}9*DwaYJ9Pg{b^lIYUg^e z##iPxRqa?I505nzvxxA-p6dY$zR;#+o79^N#1N%d?49dBNDdy*%eRf%7b*LQOJql# z^#41GBan{-36k*lfX+b6cRN3&!4zl=XbF$L$Vr(b&#?kqsXww&=GPnwpDZW&N*yoL zaCfrDQ@(BuWRiFR@xUoupoU2{IJ8~O$$x3^al?VjvDBaA?cde@`cdycKMDuBuF7k)`*5a1pAC2~ z++_9go^eI-Bn{)Ap1bca+??v7I6;+NJow*dL{J2BCg*Q9w5D#0;|U-lP^5|MBC3+nR+SQDjOS7O&pz+F)4Jm~;{w zhjl=O0)?yy*snQ7dcXpt3ee>4DvL$JjPP9?&&I6XCF7K?l0^9{jkY`DFYzl5Ytkg@ zOh7GDn=mCWc2eWCaDwatFAUfRF7+Jwcy>pLw! z5QKy3Rr$r)L|P6lgZ%3RkK`lmC)W)3r=0Zhg&;$VBLN?W4-Tl%AXu|+GtZZL09EAOug4=`3Ir7L0|o<+2r}!; z^De56{||86W5McXQjE>N!VULdBG>*#40+FVX5h7Odk9;B^yG@71RB)z?WhA2?&PXH zLS|yv7#6Gej4`iBD^;q6c~=-}A&XIQ`wJv-@%9CM0Vh|s?kiVx4E|r+&nKK>RoYmw zREr5=Fd;IadUst08m)k5T7Lz==F{Ab2TgJp6Li<9rD?h1w*7a-qAS;;>>`kuF#B=T z!YxPA;@|V6u{M$r=Dia?3C5MX3eUBXRvQ^w~D$BpNe>JY5Ei4zU1+g)>R5j^e; zU=opWL=n@+SeD`RyW^%+Dx?6~u!9XjdEmE@|NAXtk)qC}06rZ$%wisa6z!${IMRcT z;?2tZPuJ!=%sg^!tBEwq{E3vb0t%ndOe6E!WcZ>D!Vi}A702v1$Tv!`=2a94jlg2| z1bJ)McUTnSc779QJy>)cWaN~6v&vd>e~r)#d)zJzh4`CJe+w%0KRA`3LNhS0Rs~$i zu7@SL#){ZV$fE0J^3)ggqBjXMHDj=63QoaJ{VZoS<#Ub}JvpBMj9j(84NI?zK)bON z9aEObZ*9>&%yFAK+O*r9-FmpC=v%m@HaVt%Lgo}4Bs(JYScuxC!yz~%C8i)Y+5K;a zxADJLc1}|zWcz#ohM&^exGsN3LjAWOz8<`FVcfZ4+d#J6s<_GI=FRueDJLxpQIEI& z=~a^>(3c0tT^HGF=!k{IGB+dxNq_(gvyMUr)dc5*xJo3r^3BbW=$I%OkE)tn_b_wh zb^ubZ{UXjM*ZI{c9-Eaj58m(0(`Ky)%k~F2O`1~Oa@+ZNRS{x;c|87RfO@6QCALJj zRSI|o^OdNzb-*`OICk(4Gs^9JVsD@3H(B3=y4TWgdg7v$vxyoYCGim$wao_soi=nbY*7Q@qGw_U4A+Yc(inEEp%mtIVny2 zU^)N-n-Hp8tuiTgG8DLKd&L(6Uf1o%Y-oRuP-VYO4Gfo#FOW->6&IiW9`flQc4#*t z5#G1qGRG~Wec^)u9~locEiBmEJu!S(ZtbGWKHAHC_{*m-gn^4XYP&ORw_4a=A>GSCL_?7;CHR6-N1+VWy>0Bvhbha&mb=r4ETE14B|e8aTBR^zJA$6m z7AmE`A1DCNo9*O8XBpRv-=QJ*`ua5zeAn4sey-I)&c) z>TpZ>@p$eG+EqGLAXD{SI~(lN>um+VPL2tM?Ag@U zP|>v_hP^RmsZSHPstBl)1XBGg*S#e)x3>#6?{RzF?~^}0J|gwl51MQ>fMI{V^y_SS z9J5;7jr$gy@1{P6A$a$|tH3$y#74(AwZDiH&Fhp~&w{~TI2;7JUV~T38#>kj23SB? zm7>kg#Uv!7#l}e;s9*P?=d4u_c#~LBVRsHp7@tu>{D+Pa(HsA$$d?VEz@+R^(o`;f zJRyf(-Sbv9YF@$8`R(*TF-=y4}df(BJ$ZVO0r`@OB3*LWGJIrm)16 z_rE51!&S8)P*Gs*>YKJTWN3QEOv4Kl{yBH}bLN)M$g7cI+Ct%6zeWxCMGC+`V6GiF zv76=GYB}}z^>~?6049Bjd+e(+s0yGykZSGUVBDhh1kDZFvrx6lo1S_T@BKS$?w zu&>bzxL;j6KE@0XSivLmyGjiMvlC`Nho)~j)(yf4j~8KMu!7y7eD594VjKiK zYUq*XE=c+Q;>hn~qX382X*B&*0d=Tn0peAt4=9{5K~B8NS>OY5BP*-UoGo&KJUAW` zr(TQAA;Kx9znP>^HYX^-6HP`$y@Wbx&GPq@d0pjrt5@SMtXyY%h4PLGM_+&iNuCEYt`vsy1-^F*)Q+nr zj@1@=D1`0CZ?(L~{r?l;6)HU;uG-tL1!jFyb6p9}tt~RvI-@)s@=8Jzwf}L{v3Bsa z%6%FVR$+o4OIUV&`w9$o@9p~e^iie`ohaWsIz2GMy78M_}25JFmFpgS@ zA%qy=F;u5ML_8!3%Gu8bY6j^?81_%}J+ph0_O&(6=5EkEudrTH4tMa-L+NHyQh|;e zKC%~1d?#b} z7sbTHq>FsZm@rcV9k{jnPc6Vamk>4B?2@T>G5K&sLTjD0482p~ z{rN#gj7GqikgpvY$locZ=6H2U6IDdQ76ydKrMYB(c%SH-?o!Tul0%wQ@VrWpPM;Zh z*m@t}f$V;%c$05BkP+QU$06dhP+{G?gOh^RnChRbgqagMJ;{GX>+a45+C2Ll2k~`@a;NCTR?fh1oNHj&K;6keSBg2#($}>OBI$!tM1m*g_rQ`RNs2h+D;2ke1HK_B3&jmq z%W48W;sj$01X?fOhiWs&NliNVja?gvF8f#Bku|*W-ovv{dd_x%*0$|BEN{)XtT-9Zw`0z+*$K@(}pPHSofzY<*X2Efrmnc5LB+9Trxp;f@^IGqd3ejDgDDh^1)9?>tGebekIYe5P8398rzVluC16lE$9_@1l7lANDk zt@W)oN_79VW=HDJv;`iGW|vj_qb!Ek3uB-WHcI*FUAg^?jHPE`i=gvwo)OQ>;xEza zDw4p1xcQ9*^1WdfLv-%Iru57%2w0sy-07foLLy2#HcQ{|YwkP}g??O6p+@V$E^>u> z^QPpT{|eGRoC%$qFQ8>sw*B#|eIzUO(2DiM$->iop4)iKt>dVBb;*8Y2n!EW?0p@K zq>PKt0X`N{2Lj;tthoS}5xB^4vpWG5bEO}1rKo5pCK1sOJ%9O!}1t6N*y;Lq%mD8f+2 z1>r)$u36U+E(?5`Tgf#*XHAyJ)LbGHrD8&0FL(3pSp40ujP^=@mgc{y^9=_hJ8NU% zU1|lo$bPXouh_HugGQfA3H~ytfk0K?FK2EXKr|vDHuwc(@4^4qx!=6f;yPz1D0g?0 zfxK4EWF4&PZD?9Ys(p-E@jTev7}gW^&f4WclrDZFMSv1^I@sq z)WJaln$Vrn52G#P-QTlSK$G7ifVyQ1Dnb11TT*X_^_)p>YCmZx3Fs06e{H1Sn#&zN zmdTNc=lXc?4NO!xoEaQ!6oqy@co+TDNet6js9(eeW&aDI@O|ufhR|(K_i%3pfbn0( zYc1|gHNYSu`hvAt?j$I^tb~MwTGYew9AUk-`#nN=0^YdU_phw1xN01{B-*=zlOo5# zhV$*+WHsIRiZgf%Ysc(t67X*8E{)Xq1Hy%J+ z+=Dnj+suKH-%8Sg8z+HDugr6FSj*J01oA!tv;8&SMR>7kJ^>v}Y)m>(%VTy@N~)Z` zT^KMWNjZf_)z})7tm_$*lfWd&eo?&>Ji*N zmZwc8Gl&nTOX+}y>i2)vehm+Cxm|x7Pf~Bu@hg5ySa}7M+*uR^1>2@d8L^02sRN+U zD=BDIZ+#*k3c7{ZN3&73UNEfwk9%#soB|3C(buD1X1Bt8;vwS6fIyfx)R*sUw8RE? z1P=~N{rRq3fGQ6VI{woUQb6PRv6us_yEsKt@89>hUETG=ugs%Y;Q8iMG0_wBT$oSF zR}uhqNJCvqMZrJi8NT$AJNss6A9{Q)z>bH8!w%3a`v-56--+$%Z}Fo zSb;PY!3}bThfvjS!If;dIm%r4T8ga=fhuyin2MZA{*YkB`;>G=MSby20B&6++HK+P zdr{!K&>h%b`K%qdOkSinFnT@5*ALqg#b2_J|C+uz1T0CEkeX37fdg%Cfk{&ml>iYs zSLIGK$TKF)Dt3X~aes9FB%sUCgFOp!V^c|Mo(!BjS;ft$BHTUMkX2+WHrUqR;@ey} zo<`n$b3EI=ud~;l)#3%v`VL36Cf6JPeskNafJuacY7falc8F=Q)iXBWBx|KbPkXAW zwp2W4E^9UTWpqsh}<>L^%ya9}9G zb`8Pr6TH*qnVn$EC@wLY0h{xZlRPGVExS}7L7wei ze>EjNf8l(=w=4}Z9-hNYN-~ZwE+@hzhTV(rc!*fJ@h|*7W(tW5J^i83xu)c>YHk7d z8S9Y4P_gp%66KT?=c@lmRddRPka~5ryw@FsK*v%-O=BM9A5Aq%nyPX@Kt{wAet{Sq z82E%15*No};ZQI8 zS%T3?zp2fGsa=ND@;jMc*rZ9Dp6t>MknXo)8K`a}DhtJ?jcUTt9GuhXZeDlmkP7AN_$CP?ctn2(D|w|zkM^SrEx{s!}&3lGM> z!+;1OXu&w6#N)-Qv%XB+nsShbz;84v^kDI-zw+eVLYKW+Ix3et^bhP0!^&H<6I44B zu`@THE1<-3Ay&e$;PAC!+|zxN(!T|8xHwAQ(Ugm1yYJz5rG=G!S|VGa5-FG7(A zeL0S|Zu839t_*)1+2{3ufzz{7G)$jOn7Ys{&rGRvx$cn>c2YOo#>y|vQ+dr<)?PNm zfiZWUSfY%XJycH!Ry(DYZ)aJM|vy8Tky|JHCJgqMoZg?B-e=|bDY2(e*F)UJ^qv#v6Z<>V79 z&Q30FW_~3kW`b~xZVLw!6=RZb*IqiZ63wGnO)rEASzdZfz+=G@J#tQ6y0$!B8fT4$ zsOZQg)|9b5baa*W2y`s!FxdjaWp^_5C;Mvid?u?8(M>Z)0Wguq?e~o$0|-7gVCny z+$+k1!)V&t6dmJ-A?4saP}m8y%#ikSSo^dnIoL#hY*i!-ERypq*jtpvyWBgBsa>2T zfQ)am1?fbr)|yIV7a)|#T7Utf&2$x;L~b8 zrg3L+KRfZM%g~z?5Br@kgH{TBQfez}ExK`^V7Xb=OBu$CXi7EVVzp++&Q(TG2*WCLw?2T)tLwl96xaBtO>@ z$V*Zcuz`0|3x5B+DPiXsJZF7*RRnDECsMuR)5y?V8XFoX{UH)7^j(*z0VF7BAo+#( zX#_hpX|NcJx=8wstiuo!N~ik8>TQ*CX8WnQ8|)Vw(FKe{)=)qSg3i|TmmU_E2;FBW zW05`rc8G~U{9qG#{KyQ@Z_*bV>9~M|GL&Rm4&d-gc2iBEktFb`QRK{J8*TXb4^#;9 zX>{c!wYcXczQ$sc0ND)=_FD^LM3jAMmM#n_4E)3&1~~Gkb=5bzXIE|< zK$==)S0VWBQ=-T_{HuOoRD&dyNk+ZFR!{gvFK+d2^>}-n48Z*VzM=HIm;vzp2{io! zKGSFWf?;%4Vd&L2&3+#-Tp|B8A=AI$ZocnGyHH3jH(K|{#XZsq`gaXlmDn4EU-3Gt zHw#kx00o>mC kjcsa{mYatd9nc5NpT%3d*)}8G%ZY$P$0y^eadoj|`>^Ba2k%Pu z90xvrEinlb3(ZMFTb{(+x58<50G4e6z?mE16825!@@@iK0n;IL47ayp5sF1RJs-zu zUKb>V`Y#7%V9{4X_l?>0@q1>VYWWSHdGK-26ik;eHRM^IQWwW@#u zJj_i_^gV(X$k&o^E0C%qS04vA3}wJ~3m2XCnkltJy-~I=meke<{FRcPMp4&y8|ArC zj1XcIAK>K9p;N)*(LQNVy#6@J>yO91^naqff@F+m+@A$YcU{`>a|^vF~O|D1MVBn)nL zI+#vkww42;yeE>APJI{L(->(f@*7p6hnthoqBN}NSz6Q4?9nyU9fZvT=d}Gvt{NV? zhc{ADc!PtK`=fdt26#zmd4f+q0c7w26Dbvu3&7tEKM9zK_c?-Qs4mAVy|7Wk1C2I< zCNpvU-9^fIizrX3D+m1-qcvyxulL>U#m6dwnMA6@f zy45U5S^Xm7K8^b$(x0uSG$8ZNQr?v4>Z&WN@$&BWdnZO$Qfy9>YyXd4 zIdlyCV1m5x)J7zvm~oVr5vFXZ*G)M4@OKl$JVTI)Qgg*fkdl1;1>WAA2>Yd1n{JsUf*`wnf5k2~0;af(^PujfVAn^VUuW1moP_6B_hWELrcu zG_`$jBTE?1?(LF{taLi~^!9BD7a`a4Iou5?&%kG0;6EBJtED%QrKfzA{!0VQw0%2+ zDjO%;!vWv53;!eYf6cm-`PJ+)D~yxb>+yL(x2wG^wGQs(L#_JK)i||zj`R%4cu4BmW_dPU}RqxWU^%7@o z8EO{Vpy8RR4NQEPSF37x`D<0Vf+AAz_^v=1na1i!O)Z}h#c4VB9y*`Is6R%0j4VWF?bZ0GymG5aAW0Rt=?8wH{j%7vC$O@>tQqNvM6MBNE! zZ!(e5=sI0{mf`M$C0j_+Sx!$7x?06%vHmbV-JN5h6Sxp1ehRnVAc-+OTrRrZSc3F@CByWP%itm)rn^*JacLe`8m~siB^9#(Nl9 zy+5V?Czs_tfrJyVf9OGvfjT{~qUa=Q1JvZz67mjKPZ_mGJVI+uHoD(O+vcn;;{-hh4;t=fy=isZ0z<+Fhs;(L_-y|NQYA@jkfs?tWh=EJCPL%uv#Viuz+#57dvsc>~~egG7Mp zF-+(?{Y$0Lpl?JYP0$n7>-kATpOXB8KpL>pH=ApJJ-BN-guajeM2yd@#I@JVa%R-# zPlW+Mntpe4yd1i;qCjtg1Bj|m_svLk(Zb@M{cix2uwn-!;Sm8z5M-5 zhJj@8u}D{32_YrNM8>8j8DQd4r(Kx-492^T(4Z*~fk!TZ{-h5)*dA8Vx#VxIlUk6k#Y5=IZigVp}u z$=}ft1>4-?*14;lt88X6dwP3;WQ(lPY`Pd4+83>}4Wt7RU{mkXyb>cyOG$4{uu zpS?&+eE91~-hbWvj7J0-|Lx8vd8SE=eP+8}1iJLp`1IgS5)z(0+zu)DPdC!}1I zTs9BTQix0$9vx%o8B_OB%)Wt^f%O4H!@UtoF1rpIfN66fY|+!NrqG zcfl((f6xBrU-Z@r6(CUP`v%xY^+c_uS{A==d^8su^4`d|&}jrS7L!8`;LV9QnOx7E z2;_zGnK3;(*k%fFtRz8mkm2z0lQeO5)Zz3$XEGR{zGNq4euA2&l1t4k+F=uJS*^#h zK_29h&=ti1KX}py1c>u|4DM1f|E?6ZH8otJZv8<Uq5i$<~Yi|zidv-7ofw$o10 zq;_)OuDn6&3vkE*WGm>+VJHD)HO{m#&vB`^h!GJOMN#mW#D^y+MJiIAak=8Gu4@82 z`?y^t$Osq4JjA{s>zK_eWiYv%qWdPvU;6K1X`=$hX5BAw$(V#7`!rRmuY;FE-#`(3 z(ErHm83V%Kx!=G+Y45zCd8a&v zzb;8T_Md;lX@B#^#HXD!`e4pOh=YX7D7JKeB?TQgr(7IKUo-KW9NJ}k?Kx|srsj%G zP4+txB}YTK+8<2oyDnWN&IC8%nUgGHvhgS<9K{op+wx zQH0ziyzVfEx>$YhOijPksa9aT+CTNb_*^PN?9Nsa_ESnsrR?{8Qnb%P#U@L}$AxAP z#1j;h7RS_^+}bTR^;ow@{f8+0r<89A1YDfP78w^wZ{EOG+cRv6+`=LV;7+mg7*Ia1 zCEW(gT1|oY+=o=hk}EyOb%E-{E^Tj!W1fA&P@W)4ZOyq}6uy5v zbDa)(ghU89yW8~G{$T4COB(A3Vii99GriHqlnYj}X8BRk5EKn}u!CfcBddhxB9M?M zR(F+>6}2QAjk@Z2)p3zbPq_ZOxtwN2h$%j_kF!s+CyOWijy+r;R$?r-JH%)!U*>pL zL`+=5n1eabf3DynA3KJ1qkdhzLcPf;U6Tf}*0eYb6F64$78QE0r4?J|aMRK(av03{AKi&ut4v>VJB2e{gW8>WY=N<3u=Z z;BKgE99qUt^8ao_b&;ZGemTLb^F=var}_5#g1p^#Iq~B1zMycd#+{?KXE@&ez*c`` zQ{Gz;PE$SlVWCr4%3(kEZy(w5>{cLWitPG<8AZwsX&PC2?jspaCD@8uZI+yRh&ve< zpB&v@Z?kRx=~ERu(a-zaf7=`KtlI;N3G3t$$Ogr=z0ny6kAfuq;lrPA7z8038|Wo^ z?Ie{niUBPstL^)y4Ww~o@YbYk6@awn&YH~oC;5#r5iANxlt#6g(NNMa^=231*W~(i zj8YmMn@RK@zettLdT*_atVVKRqIAYw!;Y4LY4<2_?*N4}jmI8yAL+Yj=@1FJM<51g zEj6n-DFmeuFp;CvxK975&X>h{i9^+bBP%VXrCdNNNgnOA9FP%VD9}*aX~+|d zWy^IJ=J-S);j$B8$t1W2J-PzU3|LLT>t`W))4p$wLLub(UeXdk`m;>o<(o+NWL{eo z5Pc?ma#0&r??Wbp$jv3<*R+-4`2?>GCl9BBLxbb%2 zE=jJwmF2fVby4DW6XX;}n8MtgssxTx#!uDWh1&uC-P`@sNAMw@ZWW=>rTc3!cA$xu zfCoC9&jds~e_m(k**Em0v2HIcM+j7WDc6W}w6IWb^y z;x|&ckIBW%JumDC)vwtH3D)tg_$yeS#|DA8m^`6w*=A(UZ);0>_dtSGDT==I{wgK* z>zt(%s)0Xdd3@Z)KB=EfVj`zm6hf}x1>g!(*BWA!AKwa4$QX-~(K?(oT{Cbf`LW_gZeS}5zxFwkM-jXZnAjQt4$ey zbMP^(en9wSL0eq3b&*!n=1JJ~?0?GtwMS{-O*ky}X|4O^Ti^nXDOM~0{W8sTyzJ{I ziO^IYFT&@t!lSBcQE5L-n#S!bHY)0)c1xOlzp=5Y!R@IarW85+3H;%QM2sXo^^pMRRYltSr9Z4j1jD^3w|M^N zSkxCEN6Mk5}t+9AV#n>WxFCc zi)C)Lwcv;mn>#R)@P6f>qvKqe1RzJCS}^+D@5SKS_NzpDXwk7Hs_E>lmZ0la!89X0 z>Wtnt;n`t^<`eK<2rXl=()b>mn_HB~2X!7mAyQ#CX^6*bk+01UWR~?ftMx^RI4%NR z=f0l+ksw5?=gUZPOG>%A|29UvS8U!%+3#pti6{x!i#0wVj~Xswe0cdZ5c25y8mh!B zdwtq_UWzxO`y!qb_3_St)_UoD9*!~8Az2JahagGF8g?O}5i4r+*$I<28{ zM^;-YCgHSX^>ur4cJtV0dttb_S%so*i7yAbqMZRz*L5*U24L1VfEe)CsO}m%SthfB zz<89!fYZpVZ7FY|*m&n-smy2YeN+mu2po6#k`B|bc=7b*7z6C;sWtg=`!=?J|YR1 z``MH=$iP65Ej&5-O~BZW|Cpl^6aiLB$@ZQ?`e=t)1NRe-bCaEY0oA2o=^!aHxBCIQeJE~bDzi`Y<0l`z;ms_ya21M9VQ=-ArEs=+c zkmW!5uVvyn49j{dBo}xOQ8i>Wr`bOH@?bP4jL6dhC0>F)Tz+;2ZrevdL-TVP!evtZ z$0x~esnwHDn#O_-N4PX}5daVboDM9sas?FcBTYWF4Gdk%U?u#WsiJNV52M8dZCJ4b z*GPlF9=?JbgPO3Kxbaa{x>LXWBEmoH?G@V_joslB-WA0;n{r9+cTkMo!cE(xb&q$F#$*+-uB2Q z2N_AvNmxc$7u^(nAzT)e4<7D4s5(IAWmc1%iSgQkr|0DL@?JWPm;QO(j6d_m+bF0= zgAeF9`U-i>vQAppB#7Eb4SNd8byB9s9jRU4!nZ(}UZ zok?GgztikKe-Y#-Olt2u|I#$=cm39?Ldbt?iviXvjaDUN(1UWvGPQ}55a3LRIfB*7 zVbn#(p$wO{yG(QDX=`sZvrDuFr&tV+ZM5U@_JF>l6)hV0Wxo2y2?P(?5H0Yh3dKmN0QbXU7y;rLYy zh$uxS4Nd|QvZVCaldvhOmn0-pg+W3Xz2Pw|=%O#({pZ>@_;@3oo93u+4cgGk+eSdT zx&gCg=SvF&bUakk6267ABsY%*>goxedcs80EeeLCr{(k;yxUI)bZ>EYL{~qWYK)pp zlluZ>{cZi-49l*8D8R5K07C(}L~VdE%db-*(H4&8dpWbmHVnZi$vj|@XGNQkJVpq=ynSDoyIKQ8XLV? zg%8)RK#nvor+)eH==_(rcI)=ajpzhx#OCYF@uRiU6N-t@x9eK5Lmn+-F=}jdB%^aX zPc_JaP%&5F&N6r=l)L9@ z90mZcqexX_bN$BHqA^Bxu;5MKl=Gy%nFW)b4Yks}z_g0cnB`B9&73-<)E_dxs~`-V zVkih(yDoaWJ_s(go{3eDr#5?2IusAb-^XH-2k|l+jffh9kUB0PR`PK7EX3g`Eg(pU z-QN20dTV%&{>DZbEijLf^5!sd^?OzW-}lgK#wa-9(KMczV2`K#f?w%>^ z4sPf>*8R>XJLp^A@Zx5tsyi-c=a{S|kBc*Vs&(;+Z#!P&o;ey;8q#LVJw%P`TTj18 z>`R=>3RL>Sw5h}`>OLR0y-fi;|HX64V@X;ZMTmGDn#1~TC>F`t+yvf zK@=&@!hXB-RWF6|hy0}Qcehdopn?B%3u5Zax6Hfso9)`NuV^daCs*A#ch#vK6Km~-GnV%*F7xTDjU5C!K;#Kw z)4nBzPyP8=xN$&ps!&ZM3}PfkW!(s))%@Aa&|n3I(%`L6r2EVuCggpWW{Zt2J+2Q%;+hh6 zBb_U|dNEmbS%EJv=#?AO2^ODk0WD!7fb+)Prabk^DUitUKFhV`nf*UN>j|&W&kvbg z4u?yhatrgdzuq}m(idtY#l+=R^P=E?Hb1)(x4EKQf=623K+c8ox0o6krxtwIcZR34dn zg62dTPsiCM>FqrUH67{!ItxY>*|cplEcM$(N;CBD5sja91VG+VEf&$>b%cOUV~RB( z4ud)*CdO;fK#U9efFO}QIhw_7gr0#3E3FX-jGQjuEc}8X@;|~0w2<6zcS=lLsw%v- z0o`t(R}t{t%)^3b(cdHPGkQOzsRPBiUE<-G*sTe@plb*MEYEVM zXqTSuLnwM;KbBI+sRwpo z%@Yj5a4*bdd&`~vzrA80N z`X&8q&Fd2Oa=6cL&DaT?tj1I>T>^kyP*{lm9zV2A0fahNxXz|>rjQwyYV`yB>I*Q# zT9yLudu~gJ?-t*Y^!z{vE#kOHpa{^@#$|?Bl`Vxz!Rx*;JcocK$HT*cjS`UjgmwjW z!^U!}zGB@x`l(D$j>|uui@7U;y%5}q<-y0S+g3rC{&j4&rfBG_kP^JISh7@ef;bcW zM0CK8lvj)>+?Nsv9ws3ponj)R8s@B6EjmC34l3D#7YhQeNhEKpVeB`YHk7Kgt z#~>wo>WI(g{Km?iK*{adj;bgx4{rG4Ez`@YB8jjJ%4Ia*1p#j2K{KfPi9+6NY(d*+D^C48{5>(CNRwlX?~g6EyX=);Dc?;}VaK zWd>rtLwjOd-}E29W+$YXCk$}HP;Q(^_9get!(_FmKa9)Ni7rY-A@6uT^O+?Fk0#E0 zg6Ld}CUJ*OrfByBwShj+&d;(97A&K;CV(^$52Q~t)6aZone&@oR4G1MeUJe*c$!22 zAvFr(`6o$RQAOOdm9X6-m#c%&sbspP#T?K5^gzXxPN2>Z6z1zfa`kyre7YXd4wRHH z)cjx~=0}$^?m5EFf&;0v3AJm3FThvFgY+5sSqU!wa=8+rq5!Nq!5~31Ps;0wu0itw zSiUST?$<;brIN%Nq7C;1*k!6ni74;k{1i?<;|}V_i^Bhs;<0|YS&1yxNa0A!*lZ<- z20tjH3s-GU2)<0P;sslWP_~UfBCnlM@+ZVWb^Mzw_r-3lfUPh{^3)j45@AaWG@B1( zTGV<5F6AQr9UOZ3SiXAv!sF8D`;|4tj_}o{t?AX6c52xh3S(Vq)V20&JTZMA0e?A1 zpe^RE&bbG=!1LrPRXYPL2p%m2fEGpmOZlwRH)i=w-?iza?Z6!)O^#O|q=?BnUg~Rw zuEQQ&{!GLj*dvp--6lqH^V*T}v#r>2&6Nc~L?4|B6E@$O7TrUc{R#si58n=3-2BhS zJ8Z8u$RVZ9bUc&yWkNNT!5bpW?-!sfBbJ@=`vcmwWF(>?+QJL?PrnT3>0)FN2GThZ zQ|00`a&*ye>&y8i>}Sj{pe>?epy;*VQvvG*FG?mXo2x@_n`8eS-3tEyx%{WIeuNy- zr!Q2If`yA>T&kB}UV%3bT4@z>dztU|9t9x~eI>Z zghF3?@F`(9(|fsZo_r1X+x;1R%ma`wrI@US>_x9cY9=c>Xu(}-uV66w{Tp0(_*Dk; zdWJN)RcLGn75l4R{vfY4-}DY*XD#>$a_{-?My)E%1)vTuFSsvMyuG2u8%IA03OWp@ zn62J8d^J(j_#pap-afxwdV|YC@%EkR>JfrjJn{hv3oD6$fZ}!S(OT+vZhP2YP;9att7_NIIjH$#csx-D_NR|HEZ> zE`iTd6^afNf;mz)XH5e{l&lBPUd4n0X0hIIOT+==T6zJZ(xhhKAvVEGuXb+73rP67 z2+0u&q?@TWPkT}C$P5%ANyNb0-%r>D^t4h$8aKc`-Xwe9^Z6#cq1`wU?=0EmR&hDu zhSypWX#N$#ZY{UF9(x%1g-J;l3lp0^C1QZa$9AeMi^4Bu63S3IqP9;^e|C1JTHrA) zV#gfi*marLMT@fZtXj+mAH=11%Uj_}v|hJMb4Z{rFU$VC2Cz5fs=^Rn+=Mim=t%iz zz^sGVyBLM-N>_r<9%r4MgDu30y?yx@c>k^|R4k!dC6&t5eqVU|!+$Q2hMw=~2eA9u z@DmPU5{iUDsjv-bbOJg!YkIuo{2-@|z2faiOAsu(SQ>bFuOqn`tX*#TvsTy!foFHP7zp( zopPC_=#@X0I(zw;rTIHF5#O!4ZfNigC3qzW`=(kB>UdTAjhwPQQtuzHzR(Yc@i>WS8 zB$M0kkmwAXTtm#ix?b|GlanzrF%dZ#j`x_k+7U7_GbaTO-`{%tt7u%KagOJ4sU=># zJy_Fmmx}8}jW_VyAFhT@hu;*|z@m;smNoIpf!Li6v%V-XD%nGIo9?BXyDbwltv=n^zo|aueHck`{i8 z4+UU+U$LJbgvbHata6AWncV4*5yrhB6x}Qa#4-df>m4j*wo|Rk9{lI*w`#6lW*j70 zYx~>%(72$+5o~wy`XgK4JqhK@7#UiK_=y70qHg)Y4wjKk{AChXpCDq${Xsuktypi# zUf#g>bjNy9X8;w6jqc^hFDzEKUU~B{NN-x&|~{MYfYP9pC;8~V#RS#*c%?*PB6rbsAzl4 zrr50&GfhBX_HR?Xk99&4JeC_2Jc)J z49E<@gxN4?l_(P{5B38i8$p;f-j>j*o}p`S5GLz%t8S!DJAB1g z;5U<#(Y;)ap6x$w6|R}srS+xJP(eX?wJRNSUZmya&PT@*!$tTk4C$swnCPhS{o+2~ zb7w0>UWofK3GEW428Et(wS`FG(MZ1rv`levaVLVqv+|6TfYvreg6{t=*Yl3@x;g0W z4z&)JvihfazfLsrx6QV22C!9l{>qTqxb<5xfnFI!CgfnDbo9Q@>jv%AAD7q}?iS>T z$59;-JR&vrO1!d3V}0ZTs8=^i&pRa$6q8%ypH73X&5I<0tg0SA&!@&G+EcnLvnFeE z_WwAUSJCear;p23?4sT;@Oi|9yh4W9NqCxR~}N| zNKZDLLy&xBfb7%YU%R_Jy%_7)LM2#K+0ZoKJ?Pu)$q*2!@i#G!g>LH+q4G#f+a^gCMaVK9x92aQS!T^~}cm30b0{fI$~Tbda$;<&!Y zl#XzotEqK%e^LzcLjufjHeaWfR^l>Qgg@uS9~7t2D)gb#*2HVK$gR?L=V84OfQas3ntK+G+OfdL7SI zF%}jRxO9b?e#ykLw~yLG0U4eK0i8remd(h|n2Y>NXz;NE1sYy^>)fh0vi)PgR%kJTDyyf6u;@G!{^D*~iY(wi00LS=u-nWp*}(jF$< zc49U|lI6o2p9?vTeV=iETuqY`^CeP0@%j8Y!)d(4dn@Kdrp1#BL%U8&ksYUz%;M+p z))7`0oLEg~B+kUMy(%tKjFfddSPS!qRAi5B%KWA5Y96Lo_=Tl9G;`8WY5crU8r>L?w3R1LmS7yVq+%o^e;nmHX5DC;k!632kOC-x8-YI^A(i z6=Ub|cZ1;iQd~DAOXFCm$c9pjr$p|Mv~EsHzZCt|>OSed?K|lYmermN>piD+vD$BM z<*za8$F68AKAq__2G)lw zUP=1Ls^rL%S3z9d=qvf$uWX~0kOGfat!yR~QX=*CyNC}6t3Oa0zHK{)4& z@ah)8gjnB5*PmVaanuQ1Fp3sCh@pd~ZWRmxA)6^Es#15v0nL}&%Q~2iRR`5fzSM`x5E&{#p&*&buMQO*C}a#fF|jr@ za-FTK(A3yIMK!m%wsXlk(7m_Qgj=O*)$bXfEnRT4S0{?U&ff3@q6Er)*KCburMJc_ zfXa=PE*A4m&r8lbSUs$~^ROSPc}0JI&xht{>uW+vwU_Gi;oz3L0mpKoahTv`*@0B( z#h!1R5p370cpBjsh6I3LsQ{Bu+-@)#WbNgtNZ_Glub|!`RP+i4{Hf4|%FpF56DY-( zQdtf5N}!{#aeMy4GmW2bM>XCFE(!kYV08I+QZgQmyqe`FU)QILYv;zp$VT>UY@qKU zebUYJB10$Y5x6>~b6Rc;MYdyZ796|~Wy-tF5>23S#@y`;hgmjYnDSb0>Y)ohQ1d$U z?EL*vglFpNVsm-{aYiUu{1{hN)WOr@dfyjt#a|6OhZXyjz%VW*%WPlr)1R~1c^TKl zI_#R{%vSYHS7=a_X+Kd$?5K=1_X=w3t^}nj|FJmQBB;67kArG$QxE7HS429qCRzN{tGPI@w|( zT?10gNR7pu6#BI{2jAHGs-TCv)t%W2q6J6e>u<<3e{q`O5cVK0^doEMcj{cuU7pxI z8LBZBR@=C?*$gM-cRz#;k(@>3YM|+58&u>6ajuxGyXpD5n5@LBBbcm{;zghDhmBcE z9c{)LBLOg?xLqT1zZFR}QBwou#Wdo@cGqpSKQ=C}N}9p#jm*DBjoPy(&5-Nt%WDXPm0N3>+(|DcRwD%kmTDm#>$xq5KN} z>l=mL_~amsftDQw)Pf`5IPEZ!4@t|If!S$@i0&0?>lj_8Q}Hh`71lqcOvi4yV*2)r zAM{+C{_Op(e=U{&4Bx+NXzYjL@b9Ss!4J=)K{>MaXxPLo(nIH5iyYMi*dPA`F*05= zC798qNyI*3bCKd0x9v#51!H>HQZ)t0YDz$77CB+qcOwK%V)qU;@6=VS5VCFlB0eU zQB3fH^}qX~mGhq0cA`hznJAQcN>!yhV4J~Hx3`=0rHY-R4sWPxx|!|oI^y%h#MGv# zOR@DWYd%q!JN4U)ZV76&BRyIn9Xd+7R4!K5R06!=C3pk#`WKa=@QdKQ7xkD06~3zZ z(to-$Y!1%!6KdK9aLmK8yHb`WyuVCs^Xt=MvnCtY&!OJx`UFl(E^9eYhXKM)3hnnQ z(a{!-mVZ#Rzcuf=m?wXjiRJRMQ&cH21P|FDKY9?$Eqj9HWYfk54Rv~Ke_Z?U_Hjti zc+j^kgT*)LqZ+XPN_RYVgUBeEia!ZB{xkzvRRVx}++PShI_CwX`OS0r2x5_)-vrc! z^H+O}7a?d&+gBVEyVNZ|OE&wnTeRwn8WVI|`Vb^tN1d)#L4@yf%#J+(oJia5A5Qda zDns-`(d`M>#WUE!^le)z-j;(|GoI-iO+Be_D(vUyb#-~3J$GA$Et%QqYwPOJX6kc& zeOOweZ3YnZN7WC3sm--Z*CgV8_@FZmk)GnU+9RP3D3X=9{y4XK;x<>0eqN?4^&M?^ zqxRgBVlWjHzQ{2(_OYK1e}-ArMpw7X%8jAt)sfAZVv%t^)QHcTMm!*|gVG{jruP(_ z!#p5*N8&^h_!z#u?+6H2ej2SPPYN5&PT8#w>4zzPLI7H!ZGdnz7%Om1MqjB9q<>Q9 z0emu|q5gAq=mMEJZCWlK?{fv5yaaq!>&5Ic`@`o%R4|&2*s)QsvKAC{yu+iB?oI9# zmXvmEH6V(b36(X;qRxkOpfZ#Q6PMA}HY}Xa5}3VuFdrT{LWQ-DH+wI=^XixXI6cv~ zOW1e@TbBO50Mf@uIn*V#LQ%2J1*D4cCw?*^u&);JX9-~3bH6{SGNl=eoaXsou>+WT z)XsZ5Wf^Ik)6=zfY+&8;-e#O|z`wRZ8H8V1EA+)<*Oq-g3M0bY+cKcF1=O2RUKWa` z`&%X}wZF~~UvL!tpWn{)3#(aobHfJ+FWFye?HC$@vQ}K)RT3U%1u58U0 zceR|zg>iVH22506zuGL6{ zQvk5GXuvG#^Ljn-mCEr}9W8lz=;s72WZlx!izyzDlGF(cXz$5;qZv(3;WD~`{Fyw&d z46lc=P>-xl2Kf`g#VoH;$IZvSN<_BxN4dxar1d5E>~Z^ag*||u{d-;Jeg9q;g*f)b z@HFgZg{^4!%=vUIwO+WydtP{I?8BOwjzEd_)_B4G(SOkEVwGNT4-0#7i1(hEst&4) z>t|oe(K0NeDY8bP5Wls%0fz>e(XxGK+WGhGtS}x20{G=eGG4FORMkEowI$_lJmh?; zPIi{%ipShgOH68EuE&f0GzZ)JlG_OY)AKtju;Z`aqdOP_54R5pcqXKaph>`|@7eq9 z`y?c=QbCXaiaFok(nNUd5Lp2ZR3P%b|HF4B9jt)=1U`6L9rQlK#8PI>767z#dn=Iy z5zn8y$j~-<+$OAiF$j~Od{Q~qhK#;^ay&0}jBr+nXGz36=Wx&L&inl(5-wc|eAFqD zcw3|BR9Eb(C2N`=#4LQ9u?cSB`J*ZB%wk*){hG5;8Zst=h;Zwz0@8?8Y`rl*ebTkr zu0;SA1Uv6&NSu(mS!0Oynp0~*oK*bShZ9cPUwottHMKab4pc_qGq?%0zI}aPtsiBm z2v{m&Zt=CJrk=tkz8oM&*X@O+sy`{l=c)eT#69wMu@WpOAIl1(FGibb=#Y3=%Y5Eo zj}i$q2|ub#YRj#FAM&-nMb;$gGZ)?N`5s(N{mfsPJ1$D4PsB2QiOlwf1yRJB(SwQ` zdP&<2$Sm_V(Y&OY-lRlwzPt1-l+BTGO10w+x!8$+!^wk<0w@*4`|g401n(U`>2Y7r zg>y&$pL2CE*p$x#Ae_$;3B}uliPrOO9JMQ2B?#DWYAGoz+s_tsw09N_yzX0;75@|d z76>p=n``kBKnC?N()`l5@cE!9yV#WtWiDPTRrIr-S#GI_Xm0%b?kaNdS8F?~aX#1` zL3{vzxc6(L;6rmNzaQ)F_1y@gIT-wJk%~&Kdw01Q9XnJQ-l&d@8M$8fs;Y8Of5o2v zg`3YPMqu(1LV7xAoONf~FT4tK8lXZd$_haK%^sui4^7{<^Z1g|j2NfiOP2)H1eZrXkN-6Sd|t73u=2k*{RPHfD_4hQ{6x9wfr(%jA} zmaJHu)O7gKKy$8<&Od#~Y0eg(CLzc@0` zBQq`7rpbSVkC-p>G!$BYC-HiKkR#Wpn>i5mAUyPe##2ClPD$B;^h=W@&FSeN;77Wi zw`_lf35X7D_HWJs7f(#AR}C!miwFnhM{;Y(=7uRPW{x@0^#Yd53I~qC>9A|w-Y=rS zw%@)_gp0`k2q}i)T49{aRTSMeUh4+;y66*;vz?R*0DlJ>c3Z9*FAPqPK`r%jkHM%? zPGXX;@{@13j~B0#)MNtbA|9T@%0-Sx6CuTecV-kh?oJKrTn7gdA5(w@tUrj9iWxF3ctp>NQT-TXOOV^MrjH!K0Yb5zjfzEdo(UUA z5BQ0E0eO==G?iYi+l&9zrB@Hu*$>(-w_mffbM`C(wyBWMu6`@g$SxrgTlUULTkY_W z)uVpEB?K#MvLD9$2>Pz7Dl+mSE^|4Zc~GP0)^%x z03(bZESb?X)CjfsG3xn~k}@Hj=KOr^Yu>@Fj=MgRW`VV7S?4YTAieuBEg&8VSUmFD zeE>hNg}VLjOeD>~?A^6lX(|UZSFY!fli_4~dFkIA4vW~hlu|ns&~9sl+-$?`;Vq1T zFUqLuLl(Dz#1Bt9>?2KVv~X9*#pP{Qe2^pq@i@DV>FdD04*4y<4FirEuOPAXA%5Fl zB|!2(U`&N;T~?5g=Jm&vYO$F-ZXxC{(bR@Q=DjcL->!!?ckDZ$tARY^dYZ`#z{a2S zi3;Yu|E@QAqq1(37yT-d@pRh@E_dt>{U&H)ttBpiXzBfSPQoTo+d~5=na)1g`Ex8X z@{0p!VWJx}>$Uvp4^1XPZfv3k+4jA%^rY4B#-c1JxI4}xdaZ)aV3SygGWN;rqkt_tp0!sY9>-+J~o=AFo; z?}`U=p6TUV5izmj#M#ZMvjgsgbEM>VqmC=x)MVg-pLn=l9`&y z=F>a>T~V)ZQ!}-Qz9+NH!$IC+nCPGFKy@97kz0AOpyC_ZY_rL;%VNu}3C~b5J=8Y< zHYbZSjxfl!9w2G>V1av}_f_p=7`lji80T0F2WJff>4fo`@sI>xSf)zZ)@eq27?7Lx z;@KWhr#tFh87le$^5z!S9WtkH7I^y$hw`VtM)=Qm1J|6~H8Io8xd0@)+Y>JR$kcGy zfu9?)DZ^c74M>Zu{@L2_DR!Qbk|@j`D}p7Z(Zgq}^K9E*Bz$UAu)?jFEDiok8c}a| zap@^SODm`{1PS=@?0_lCHNUj*_i1-)^|Z;j6%JZ);sXE2FM#ZLobn{_U^zDO;dgZ0 z_gB0h)IXsZkIZ%2vu(YE?pwM~)@oMiBB-jPHC!I*g0TT7kRyKluaRtHbM;}EMwE$l zigm<2X}#n4CMP9eu>t|rDBjlgLX8|76bA!qll!3|@~uz|P6OcL@m>&e8dCw;3IJSQ zf_x)gu&R>spv})YZ< z`~BT`?hC|XsU%EZb$0<{R_%^h&yqbozBjXml;1}x)1uRkcUsWW7v2hB2cqE;X6SQs zzO~|A8(;q*9IzPVwtPaKAlW(w3Ld}?jFa^Ivc)Hvsn~fVi4%*yqUXlnCXM5~INyP* z?xr`6G3r_a$V**oE4I{JgV6xoX%Xw6^kdcb5OPjUvy5BQ^}G+-^o}u?NWq{QveS95 z=hP@8%9~f+T*f;QHIRDwm>R9cEI9l_o~;I5!-kTUcw~d`?|?cTRs$`r`NKo5GNxdT z$rp)584#)dN&2VECVSo?>O?<{f@@EiMt}5cQv{<269#(>mH7qo7k7 zXc8`liT7U;X;zt*a;uqp3&uJ4HhFgxF%{iD6Xq zH>4$3vp`{S(Ku_Xt>uxOPb1hc4HJlFZH+K2K&5wp8@@{fyl4*a1v4e_;t1vnM+E1j zI)Z|jO?#mI8hcJEW+qK4zq<(k?Be}=$%Qi%Y#FfCEmxEr`BU8FI8n8yO}3;>>^-xIUEIi#|?aup<>+-`3)G-^gn5ivO49n-10%s$6)k!YQg z69J0s7Vw8iYV+4@_d>@YGf~dbb1)bj5rGkim;Y6=BYNC{lU%cKi_Z)?Rx^Tuh3TM+ zE{Sd#JOB9wb&MD7jx-P(!4-Rl3SJ5M=71b=RC?rHK{uHI>@tZR|tDWS-r^xcKheO1Y_rOW0Y5r!D? zn}6~6(NUX6wrT+j0P(+XAsw2UE_f$S1EM@k^?GBH*FH=t3sPq++=-euuB2gY&huwVQeNi_p5CBB8bS?fHmUK7(5ZsGVyVt?*( zDj_S3;okb_7u$5EKUrCpaIczF(kAR%ZmZ8 z1FEoU6ZZdfZh5mHWug=?Q=CGJd z14W$NTaMn~>X7saCmT`C}b=Gk+G=H2Bjd{Jp9X|Hr-ACGTVK_vx1I)Qv zFO*4dF6p~JDVPO`hsu3<@hug%WujPB(Ftao(Jhi5MMy^1!>sVD;Pg-Cymcw35jC3f}!-BwIg+r)NB$JRGro*+zn!{dMY0a1M{Q z*SLyo6L;6v>KmHr$nc!zCtg9Mb8qiG5nHLvgc}%pL_y||jf~ebVC-YDtRRtpbTmJY z`5P`$QvT6G{L`Ij@X)Zqh?6im4d5pI4hdX|E$tKHPk^pACfh0L8?wegIl!~I_Z8Mbke-QoZ17a0hfScYzdw+X+oortT;@p zL54$QK8Ixe7PO6Q=ms)6UT-F=<}slCIy#e1{`AKH4*?bYD4ag4bhs?%OEhf%@Z5qm z7VXBPzABZ=wWU{n2!3mvU;Nhk6Kix_a@U`ccWRm=-`lS&?yS3T+Nld-tzYubo8tzi zYsRD&uX+1hbpFlBG8wC;Kj@0~@`MM!0~DKUY+6de%B1k~6Ip>89%#xqp^jvao z>T*sRzfSD?yJ3x-&C$Mhy+~7o1HEnjTrN&yZV%D{vRTfBPu$#~qXZ$>xIF-w;QWJ3 z7>gw_n}#_rTwo!8IHoy5kRjF)pi!6!gSg%{3MMYCl)~wm5X{7W_k+-{I%nk;X%_Gv z5vxV)2u3jhwgwF)acHKmxSlge?or;qezq>~`^JgJQ@kpT2IvQ)Y6@M-5R3vWOy#!F zNed*3D0k$$1nB)5mIo{@(ZKl`56m}jAo4C7x6|7W1~|6=WZ!`Me=zEjb6^Fx=^9W@ zy5;`|_YR|aH?Gd7`k#FY8qu?7QP0$sKt}J?GKbJH5*>_W{WAqm)WR6bqc?F~^fZ6rd3$?xIgQ(w%zd>bOa!lmV^~A&reI z%Ew2B^Rv?AxP@1ED#Hn+uWQ%WRF#+A(_n|{*4YW{;a7aCWMW9_O^v zn7z)CK%NP|oeC-QDrtG$!Q3JpFc}^YLiJ0)c_oOr$=CTri|qf`|IaImZMb-1J5}+) zbA1RAqsxPRobn(gb!OExgZ%oHJat$B=(npUbZC%#^S|oH(Nsn7ySe;Fp==7!WZZ~+ z@rF@KFZgTimAnKk5a#^7h+-WVG(eMxA*?r+kpC^4FYkFth%jrp@f-<&d$zvOh?#0(SNNJ~ZQXqG7(^zcIkBtiYxNq@)cX9a-li(=p@BRy{uI=npsWU+u-ySsuOX4{NR6VMD-s`^`^tADE;eq;2rB1pLuAhQWoGB#3V|>5i+pN^B9pR@a8@QkibDq0_ zMMPf4Hzh6or3Fo%9pu!`NDTY_5*(B?&B)450}Q|g0iJ!R)GOzv{PL7*j|M8i+1utx zxG7es-tpU67O92Ad{U^zlQ3fJvvj_%7sP%?r#l_%9C; z?)u)TTcE0Of>Y}!Hr>wRqmQ9ORTS z_?=mtsIDd?cZ91q{K0?W_Yc$uteXNqij8$lFQK~Vlm~k!xbr$r*qkScYBS!bN#3&a~;C-1$8suK6Of&RQze z#NI%qKo_aM!P(_u&-K)Dp>_WA4$+uTHVc#9FV(e%^4Uz8Uw7VX`ci>-e4cLRjjH=I zV;_o3RYi31)M57Ua=;c>m@-+KWcqX?|F4&O&DaC-&(oF)kO!1&coT>X)1uoy*c52E z?T??38*eEzaTB3cx@`C{s4&U5n@64}#&FLqa{Nm9bCK;m93IgAL~OOPDE ztP9KG6)pWCqDK2pS~my3b}YahUOgnuioJ}aisH+5>(Px!yb8K93X=`yh&a*ima@M5 z-fZ4}oTW=6`S2%t>~yAX?kHq%Y`fitIYKf3?eLN+`LgXoc@~-Q%4M3j{n1`-tWSPF z4DOrm!d4LfW_INf2Y68_9tpauOP--c3q9;%tsZ8ZZ_j#mL|g@c6g90v+1S(6MGYL{dBPoO zfzXg+p_CEprdRF6yob*N`H;$5ywYl#*s0%-RIa+q4LPV{Fj;HZk z6S#0mo-W)XQhDB;3S?VtZRPW*=P;(V$8tQ2DutB|O^Tb#KtE}fm6D@XB8sV;oy6U) z#{wxkpvjkDKFjZl`aO>Ck84UrtXnRZCz%E5D0rS?*Vn@gB+}%LB`&VJe_D-6_O$yf zIQDpJCdp>ht{`5Z{uC~>ha@@vB0+cHXHU)6In6xW|IxdB z#hxJqrDS#KFv4*n`hB=;II3)69a5K-FjQ5IYWcQKE+l^lRHk2lG(Ep0{ir$)4udwa zj$@qUFhnD6yVCybrl8Li|Cvn^xeD9VAY*0!+#4hInwT_OfymR)pBCs>1lVuS?S?zUF?l%b+~a(hQEM;B>I zjT20sMDdOm`fUAsQ-XKdo|2M2B5yV!Qc1WR@2&zy2>tfNh7sD(ZCcaU%L2cz!ft%R zL>(|A)FJ9EWLr3Ce0fzAz4m>yCbgb7Sy8W z@D>R47#v$lRVon%qMv98<5ae9jKUTBqM}o4yK6c<7VKg}QVn^_D#|3GH(Ro>%F4iY z60d()xye7Q9PFT%$t8G)E5a>l^mxP7P}7dJWT>k+iJ$}n-2Xp6>LeRH@Hz$+gNTT| zhk3F?DnWI->jtY$S1$Hq=~DcfC0Ia@rgHQ5R?*{$>y*bbj=6W;+P9Yk_%LQ`L)Yl$ zNEB>5z&Gg4$w^yqHc3(I?Sc@b4R}$R(@;75b206=(2^A(9?|D*4ZMiePnem?aAq+ zOB_gb(keDbP>MKc&{{m{skhI*2;;DNKk!N;(Tl7nin~#SGWuWuexEcdz&XkKmgNutql5QHx1r#j)#r*&t0u6{ezZMJHd&TkudIKX)s{g=-&kwlzJtL zK_73ZDYkf`Sec0Y(Ta;FeUehKF|hz=VIIjZor7{0Yn|`;$^o**Y%LMyg!J=buj0^2 zsE^_Rr5SE(-16}#?xhN{YQ)hr!Ex3KWi`b_@N!7|%9_~N>=D9qO}tQH zd`K${?nDuy;^h5fc(2tUSXLX9l=<*R{~)D_xGQ%L5rc)#JWk~o?zZ8}##Oa^?k7hEtP)j}Pn{h+q-d;B zJ4Ls|(WY6K{nxLrHhkQ1tlT&Qqme!0uA*Jnu!E%q#OTpIKKyd~d0#h(U*`kX+Ppvi z?M-jM1JaI*_AClS>b0-v$Ew*w-EibdKU%CxuR^v4zG{osH%XG%R#?Ld2~k>i6sF&; zo{uJ9s`M1)`SR;=3@?|d5wNeVXRs)pB@fA8t^!h|Y~cga+_EOFO?T(}Wp7j+f$qlS z%`IOwv~fAhxMeol)8OCgz<>0IATP&7x=gi1u}S6FgvSLyw=2xm6ys=)9D*62MC#|{ zx1>O`vHgw-odcf-&WWs!*~s?LvDt*tf8(hqZ_B28#vlTLgt{o>279@)9A0?ZfJzk7|XB+g@)3--_tqG3L&hX(S zo+!&CB%n$<%0VHcU5q3~02xQHw+-AZWsWBe7o^|=vWj0{XN3qt@vfOAcuq^z4fg|A)mauL{JP4gs8;EHkP@AQXw;E5=vZJ|hJaHk z@6`*y%#+97svRVzSB=(KV}X_1PGwR<`0r`C|2^$Fh<4+YvUN^ZAvPwNn(7u!rD))l zJ1MO*m7pMBk$JO+S&5{gFgzk-!r?#NB(f$#I#1|BucqtKXKS2%j#=D&`|j7{&V(jU?LX;T&9yq<*WK z?t$HjQ}F*`>Z+rn`ogW!-Q6hN-8l$IgOY+sN=hpY(nu>PNDm+>B_a*d4TFGyB5ZMOqDOsU181F#koYIHS^WGRv$@Z4sOmG%Ttq0U70+{?oWX4 zzY{S3F$Vn!35hnkz>uI@!^y>H+YPy`O)~a)p1c$B@a@7KfQ5mFl}(U*$>?3+=}?<& zl>63FV5jdEY;Lourm5!a@2uL(1V{!E1jW8J@PEd?_KebLa@a-{jgo|}n~~&+-=VchdEsi!=1@3y^$j|{ zIiOuhX+AH@&o>8lA&wY{V)D_1L-V=&YuZm;v;!X#g?$=&X&q0zHm|s@2y@2gPS4q7 z0j!*-xJfW+Jk3e|^iK|Mo zi#YeRbqsODy8O=M0rB}_G;hM!j8R9!6>>7@&+iaAAote&{HSUurXW~gWP&%g3Hg>! zCofw5>PlW+hVEEvi7$$CoJEF5LgobjMB;I<7OUBwdEhdcX#o=HoAjj@Ic>m7$E=-~ zTF2C=ccSwaIpSPUxIRGS%~Wtm1pZO=?t0dXX@9Jmkjn^(hJ^&L(u0bZ;Vp>%Oz zgT_8)%%bR9D?eD8cJ9>bGiw%h_Pp{}50rq{jfx#*G|q=!c0=Hr0!Y;1)|&~bM?R3@ zFr1^QH+uQp*^I~E^rtc=JZCsGMIx{k1 zq{ANg-PBxUn5ExasXo-bx^RYLfEA4x!9LPdKD;RR7C^B$*%Y>gZ5#}W&LgH%wO$uk zK=0d~L+cGeEPX08SbJ`KcZ6md@z;6EvuWt}1hH^^W`MJ-kK`sj)iKLZ6=9m!#}?6x zW*`3JKpkt+&88-ulZAPmll7%S@2xju4Y*q-yy*Y_?!ybcuinf7b<*bSj9FLx}YsgGB|CNW+vhY%MMeZV;*QgHCa(UdzmpWUGqm(dRrRK-pjd-zNQtSVc~ zJEWR$zKeX3E=%Z|Q!G(UrQV)h{U0yZnHPsx^jS@yx(pQ;Rs^Ov{q&9npHXV8v1&@a z&ma{FYJ}O`HR@!8vn=q`=Lf#{em5r*g>PU7C+^1+miu(#L?a_3fc2=fapE@l0)uXo zeogtSu&)Rny?Eb>jY^@5mt-X4EESgQDCGb~+*3J^wy-pg5R4{*%RA+!*T-tfc6#)5 zJ{043zG!ce(QjE$#G;XRP)~L?dZ%;V=s)|@*G;F~etl6Gim+|gd2TR__THTyG9287@3uMU#VM3G`%5k@S{bFZoZKf}j$k1>o-UCP560YAThgc#x>?FA>BAtbQ zT`=$%KYkD+4m4FgI(^q;sogXg6A(}cc1q};Zj?~nXk}l1Hqeo^>d|PE<7{2fgvxCD zj|U#%+MD{v*659|$&WQ2QmGec+C2)1_T>k*3)htQ;rL`j6Tj1nOGgZ~Geh}_60|V~ z3kY=OvuFFwZ?91!<6<3FsCbUm+0q|X)Y8SH5hX!RZUSY+W&e=w!*;q? zwHW?ToGLLyFArC%dOf&m{j<6HZPCbY??4~ws7vPc7_v}B=0_a6G%;k&!6c za#QLrZ4dxt1Z@W1Vt@9h1Fmi3ye+}?w>KjTtKn$n^BuwZkhW4644j{3|CC?eJuK9@ z%9A2Y;tl>a&gYHgoEC;Aj*lw84!u8jhb7g|tWM}YAt}It?O<(rMEm)~dm#3Wp%V<3 zredo>rqs2#KBnuLU6Iqor91TGZf&jfkAT6HXKMy2^=joIa@5@;gb5U|Y|+EFnF&cT zz-r2^EWGG>0lcTYJ5|l%SLnaprmG6;EfKnccl$7NE~EOGM9obcmQ1BgqUU1)c=Xc6 zrXh-h6EwIPs4;U$AFzFLQQ;%KkulQPAyX`-`c1R~r{fN7d8UE5nWm}h`*N@FDhqFi zM+?1|G{*c7P&Wa(0?azRVqhI8&UD{AuS=%V9N;kah?Zq6r05;$witF+6oa zo%Vv4_t>$=rGVJW^IbKI*m?d^9n5hE&XU#8ukU;YsUx~vQ4!m1aM$fAZR*)(i%M5&brm_Sa!5={pDu@_{Hl>~j2 zI+&S?CNg-t;q_LI!*`&3Eb&yx@-^YBA z@JiyBmHyigu!Wc@@?QetR(_=n8LDS;py^WuD9UMux%vWlkbK(BOPc~+`TFi_ViIR)#C{4y!VDQJ=y>h7!Ry?z#NYHXA0e-AxYbXXh z3A}*ij9nd}NvoA3Njsuy}5$z&B z+z!uQc+4edVqoKuQRgfUJr?4?lsLH}K@8GvW!*4PHtWoLhk`adn2xXTAvLdQJ&wpL zQhvi~B{W+0-q#P<846UH38w@Yzq)lMMu8^h{8#UUX_z#pMgijKL3^O5|Mb%?oG@MO z$K)<+7b4$2TK2PR&KAe@w+9SIXztFSVFyI=S`8x=KLB>!dzrm9{Mpjdv^1GGV|!O4 z7hbBfLpK3-+eZ9MzaCIlv&_-2`Fo(Qr?R%jQLUI~^I>*Nox~F>bg?oW)LM#jeE8~i zDwVI9HblxZpwH<`qLUIf{x|ZKh?9k@Rs&si_4=hT@Ho9jMyjwKu>kQUy=KL47}}Xp z@YZwPOPwrC&EnZNFiRli$3eAu;UxvTdyOZyhvbc1oxYkK@sbmO9D!(xfPLwD@|Netii6A zGDYd4K@jS>sJ`}OOvOP+YX%lR_rzBD>Cvx$^UoQL($j)|9humY{j%0?H?zC$U*VR- zu4hRv$5!qMTdKN@NZhPef|7?s;IwWO3Ko_+%K_%W%uIOh&+kZMxy9gK;tS2*82v|3r7v!c;y`0^z#D}XBe z={VoNMr-QR9Z6U|8{@GQiK<*S)s>ynxUC~Y(9*BdC_D1;Sy?%cp0s2(e^9d%r7C@UNoC;yH2H9hzQo3m0;5@ z(WLrKcwg%64U7BDinxNlDbfC&C>%Yh_~YNQa`>~^Jqx_5;Dtp*VuVa6;BO_RL34`3 z>5q%_boDJa4oUrjICd^z)2InL&kW|@H(i~%LnN_2XTJmgbbY?0-Oer$|*uxQP3!g{pydDeSI~T(e z++-TFHSb;lRtRNMZ~v%IpycPM<^Ve{&PV7D&o~oaIhrP1SX{&>`7fUh4*9dBJs-+m?>`0>oZBo| zvoZEAeckv7B8Ix7Uk%3@`}NzWDB1K)L*EzFzv5w+Cjm6wM2a%$1Nx27!FvYYFkinv zF6GWt_#0K0blhjWZ!hAy+%Nv@`Wm?w z(wRS)Hj+JBtgr398&cM;S2FQ?Vz|Yd63ZJ42e0*8zX8BF<}LqjiyH!w7;EsiDDrnr zAz@>?Ofq7p!tiaq^N6nOvR0S1U}SEs`P`8Q{lK@;Q!6;2iyG!FjWoM|1=}B;l^XMw zD%A9)kl0g(QeUKq*eOuR%?H>4kjdJxQesYHE! zNTj>`LOCi=GI!7yZW~(>*c#v~-1fEi*`nGejy~pgYwMcyVX}H|&kTMRq`4^! zkrbV2c%YGO2U)Bu*~gDlded}!dO8j7)i%I=zAr+t8ixixwE<6aw@&QHSPg8FnM+PEem`Vp}tuSeaQbC}aRaB&Y4T&e}WltsPkez<1# zRNRx!pe#S9QbkRwXlT{^*`d)u3V&kqs1#{x?ec18zX4xcJ2BV?q+dSGW$1{-t6-{v zAi+O<%0|ZXBBnDs`L~&XdkV$pm9esb2{JkwsENv2c`L6Nm2e=id+4?lPBX`wN4|-f z$Og2Adia@w=%Fh`ehb#*TP7|JouWcau)NmiF1yt(2=c?8~yybCF!?}2jKEyF&%&o&#|`dMpl?~NK}_{*9gPZ&wC$+#h}-|;BL0`)@6gOh-2Bw*ts_fi2F>|^O> zq#Ao_2!7t7i(!QsV`G!@;g148zp3ZJRBxud15F-AEF}0b>Jigy9+&R*GDQ|&Iroa8 zxBq#ViXI#lFp$VMboU_tMArUM8(Bl4oJD)aqG4bntFJ7@>lWnM!oW!de!5`Sb{9jn zAD#X8&n$lb%v;&2#3X0x&bZQ2(sf<_Pde;q#4kBPT%6#{iex6dp=3T< zqmc>)8eQlBa}`5fu@wu16ZtYTF|nAhmCYhhnRh!=*dZewSv7l;y%Rjbhq^h-pLAb_ zH+}~DMZ{>fyP*mnzkCb>e9r^q?&>iQAZDl6vB~+E?)HV#Mr;>ZNH-n#k|(r-OL4X+ zDSGpa&<}0=ZTPc z^tK(k^@2tW_Zdvz4fQ%tgo|KV5CX;U;2yJ;bG`tkXp_0{hP`G5pV#lo`;6o&7m{}7 zWceanh}SM?gI+(iO>mH@+j6iSJs*5cy@d(U3J;a3KTGHin#vCo6mR1u+4<6$Rr6*( zh~KiyVz}AA*6)PM;3vaJi?)srCU&d)A9KMTT5cYb`j{R;{`1=_`kyOYBEHVQfs78m zDw2MroB&J6NGsR-4vnO^5=h$W^rn|2KF!wg+}-{rbdHuGuUCpk!e=o)-dbxJOvb~X zQ*OU6JDiD5`$#~{62F13lm_SKyg)b}D?Y+ZqLOHty(>q7y898r#53aq;z4WCqHY*7 z(T?9GSQKU?Z}cW4cYjEf>EY#q7%A#Sc!?yu|4oX&U3o)hOuRXW#StSyLjApG!bqpE zCTGBG2@5UBO4~RyfP#A@I}#t1Oq_aZ=4Jq0S5P;h!Nv#sXDa(qS}@U){5~~4`dfNO z`Mf}5NlIAwEiO$sztfD@@+#?}8<4BXLo5V>6(Ib;LJTD$+;u}UV!xQ#+40V|eMfFu zI*=V%26|99Fu1TBw>`LU!WW^g9e_GiwZMhr!Nla~=o0d=k*2Ud>knP16cFa*-BlVpdRyXl%BKwB=)-(S?`z()xcg|EWtRWh`fBM z;T+;e@HKi(;m7_HZ8SRkF)s-*VrNAwOEYzu#^N^b9tp<1Fn2vPuDTa-S~-XNhH_}TXBzGEn%@=?ZP!9_c*OPS zfy?_4oa>i2GZVbDXaABLlwa=3pB$+{jrLX9gS6yn?bV~21Rz`U+5|qFd7bRsI3@^^ zNaSAg9i4H_o2A1Pk2lYApL{IH9dh|J3CaQnMt0Qx!O4GT=Y3G`KJMckjY^?kXy227ml=nN*t4nlq?=q?!Aoxo?hC$b#IIhzhklUBVn zoW{T&*p-7{Pre$%2`JTztqBZuhv%^4(ekP)7T4EB~BtC z%JShQVHv${*(mnk4K{}dn;RzAyh6r1k3&!+8=husUwI2YLg#$Osa;ql6k#Z~2h2+~ zwh!c>I#6?XdV3IrS+y+Lm6?ts%#P0p=#aU}Ud#7`RY-2NsEU$BCGKsi#TGgy#brw? zvFVLSPrAD-Ko!y`^?}rm@~dqlbe7dNL$c0hhHNGaX{n6;1u4c}_clakW0+( zySG@a?1QR1USPikveLT1eS~(`xf7#YKK?3bF7Wl^*Nl+b^Cv<@XeA?#*5r$TVHuR@ zS6vz4{GLeg&$O0+!1_-9oNYZfK01~wgI37sHwiyd`}Dl7GCZ}xx{(WPG6o3|P2-2D1>~57Bm9*VySs5Y zpSA{%sKy%A1K(B+dmCU z%F4FyoG_>dzmkGM6wv~DA5nLsJCRG%Ih;HIGb#uFvS^eWvxv8 zy+D9xdp-PnQQ;GdRD#Pv`GPuLdp2Kmz{ffa-f_56u9rAQ+aCPs;d*U;Z`xK{XaD3+ zJHcXyt^IL>kP8{Z^3mMyjWl4Ff}*1dQovL#=4ie~H&C!4%&q};m8}U%!eqeC?|erX z9Ey!KoDBr6>}xy~HmI}yw9}q*1@nq8hd=pFnVvxLW8CLGrQ^fXU~Xa*`OO2L1rXI< zbw1=!^gh7H-DfhMW3wi&-e;kT>ERl7%Qaprn<0GO0t4y7X-#xA_?-Y5#9}jIX)5v% zAy^k|>ddQ2KK>T|S!AI!k_OM+3noiD9Jbs;V1)QedVZd)aM@LGnxsPs zH+@c=l9sR`kg-{1u#b3mXs_Z;V(^$t3W=QOB_NYYF%{z20IO0K^^{1orl90p z8p6j?N-jV8Ryg&uD+)vRI8myNqwqJS+-Fp{nOd)Ocsw`$nc{L!F5-@U8ipSEVbp(& zZ~yexw2d8H>rl;mJVJurN7D&LnE+tp(&=|*+mo*6U@ZnA2~8h<^H4vws6x2>n^0mUm$ksqSh@)~t7f>p7f zKgP$v8U|Z;?6{AgZM@8V7>-Ks{_JV{c8!UsR{Ne=$~BUQ(^*^LZbNmYPVw{myBdCf zS8?M~tnHI11P;}O?6;_#Zo)Ne!$0wf6p5B!kgK}v6*+9{$muP+`1e3TZOqi*}&y|Y9zP&Tu`qpd;cden5uN)^BpodC36_UHh+~& z6=s^Sp#98;cBw1AyHz2={MWx1^SXC?klhlcu^-DPMkmG5vU8e!eTgLIs zNsM*;zqhV{=qx4hBer;jJL#z86gW11dG!(n_)csF8PsADQ8@{Tn7?^}%4)*|g`Ygt zuyql+Z9wY7KkznTyd_QQ9p{*n%!npx3L{pQpa;1k5bs2*iQJYB`}phbcmB!)#2%Qn zPH5hb>0f)c&GXNi(TYLB(4k8qq|4l=5+?!$u(yofk6JrwkER!I0Iu-e8`a}^IU0I6 zJ8!o6q%_}7nmIZ3asU%}uO3neJQ^krQ*hN$VgLKxsGGlwRBwFHd76!pidigowK3C0 z&`O1-r&RG(i_o!-cWh|5CSKb4W44&3DzS(N1&$fu(rcHeTSO2V*UmWhjOZyxsY z!FQ7nZ@2!s2loKmMY2b1m@9lW@TW4qyI;;ss5OS0F=l(ZvyTE$^2g`j$%3Dj)L_1m z42xaIzDXiCFXc(@BSGM41@|-H3}k7H=O(UT`5B^}Bc!84>=)Eb7YhQ$G)s%$IG=?r z;xnkh-Kq)i@cKG#n(e&UA9Y(xQKd7s1`+AWIBHSV?W>V$9w+8R|;IYk93YDi+!KfA9C4KmiGwAy}R4 z1NWSArYMAew*=~a%AgsO)l~YD`Lz?jHOU$jRa}2dUkwX`IRK3@Z)hXZCU@+)083Wv z*m z*AzEa&i^Gc$LrIOxtjnA@_>Q4hov~@yB)82DPA|?%~mv+{}Wc}Gea~b8Nv6Thtx65 zcp|$!ulG5h{g^G!$m-TsMD^BRu-p$LR&*H(CNhuxpDU5>o~Y>Ip5cJXWpYE0^_EMQ zIRxcXe2$xrjXuIJs-8fVZ9{5@Wo$aOlnei|QcHgAgiqapEi~t9)xqqS*A1B}{}HYs zu21Q|@S{qgXvKg?-lNyF_|^N7PkAbpod2?DriTh|{ayMEZI^Q5``TFo_q2W1l9uix zn2Fijfea4mu|WAW2JW)=fubLnT=)D21MSHa9aPnfJ-?V>hFXkG>3^S=@iV$ZxWrNd zU7g=FdWy2mcU;m#j{->QNABJ;?-lEQyU*S<_ebHy#y-`jSK=xxoDmjbRJE23Kt z(?*o`nHfVD3fTZDu+qrL%f-|b#*llxcC5<7I7Qn;MUxx1^0GjaJmyNln(x;Ye3Z!$;fQg0bptpY*zLFwb_j!)cS52y<4lD=2!1)g3|&zeuk ztkw)m{XA2cex!v)HP1&AYs6lmxEluWgVHN|Y$EE_z~6M#`LT4TmSi9ifSOzEzUE%a zmLQp`^IpHR4g}lbNk>Nb5gTt2O|L3Mz;4&v(6rUbD3NcraoU#A32$hdL=FTBcTs2G`RbKC|t63VGg2T0Ie_nXAc{Y3j;6TCPLr=inJ+=ID z7l5Z>T)~#S08YQ4Y@w-(xKp zgq`kCOZiz?5a*kp62P9M9*rn4R*7OYQ753qK0Cf6wiuF96ZZ`FE-Sl(PI2iQ23x?^ z*oI=ob_Ui~?}{ylru8r~^8pD}d1jFH1j-~tWJ$?Yf=jadXusH2=&}3|G(p_n51%DSYRC6I!5ee?NCYFUy4^S=QA=41s~rzE(fkrUe&_a1g_%~ zwQknRS;^2GX04KLk1Q>!cUJl#i?-?UF9||=^`Q-IUng*tgn|3#B%h8B6y)nyIe$!kH`yI0& z<0w-CeX^tYywzIBIxFn$o%&26dzklh;i@)n|1ITqFDxbVfpLds%EaB6Qs5go%ttmY zrOc5t(w|G5eo6!&uBNT>{nfOz)#WV;A<9_vO*8TJZE11(z}GU>xGhM+6M}HH|k_KM^AP>dg_jc8Ib@_ z=WXNB1t~EqJZo%8q7tGC6wCh?zi`G1$5s{?6%e-b!0Y(*bv9X^CNCF{Vs0dEO=!&i zE9}kM<&_qNAAE(l?M(_#dF-T1X&EcwJB5BHwSV^C_%ROrQc@ve$0!T+i5J<;`VbDi zLtpwd92CxBsFyqc!=Eb^s~mc8Xg7%@Vp~U}i=0U$F<02L=7%Swp>klUF#p53_)D&N zxx_$An<@62Xx!b3?bJx>ODaRW!I8r!CRobPTlGr!`XJ!SR<4B-`9Bb4vr^cJxYS<^ zv0)?D1 zdfS~qjRW7<2rC-RN9+M%`-P1^w1v?k|7wAkQtw<=6_4^uEaMxB-5mu?v@G67IN^SnPnSqbe~(8f7T2QdigsGc60J~zln#^h&ss@@j6NkpcIfZIR0 z&}fLS300bJJ$-xQF$wtvSvx80dt6}>i`X{_!t8%<;0B6C4Ddt4MlVc|wtNMJ)?*Re zs$uGYR#3^;JqtVXX>-oKDc>7gdjI7v_wCq4h{e%9oL8BjjRzlj z*b1(9rb0pRF}&5LyrxM;%fa&U4TO|kCXV}e$nAy~Hy5>@QW}E3r#kb1YoizDT@rrL z(=DFV_QRJ58|Ot&NU{i1e6aL$T^7;6)hDq+{%6atd6Bta1t;qtKI`;xV|Hvco+J*^ z=`L;I`6ESBSWdenyod545c@OCV~G#1KeMPpTMRwysaP9tVF!!G(i<4eR6p@_gg*l{ zLWi&PmP%qcVzQz2g&v1EFu5QI#k-$r{i7$Mj(N?=!BLW?X8Z?6r^u+s<`hD`c_w`I z=k3f&VxKr(_)qKed8AX_$l>nn6dD{EgTD|V+o=c^B^-=~a2wf+)ClBUf$@V@@a%nC z?ys06V2AAAX2`u|aPnQdJzlV^)L$CSYC_CQ{9VHjHT|dzddAr@v3nt3Tl;$XGU8t$ z;#}XW&CTqrQ!EcUfAbe2a>Q8wRJYd&D4iT6{7o?qlSqLh+ThvknFX9*?B$>A zK+w+MGd9LcnvMs5ICQk9`t|02+dG8n=77^Bkf6FR^Td*jBe5hxK5*)ZX^{uystEtK zd3o(_(SD^?v-Hhb>R>T}(98Xw8yULH9Brg0nPbNSHtF?6leOgj!LR3D{sS~pC)dMaksC>mQ{)0;3@FvRT{LFmMOoGDfJ^K+xAFL zZ?-&(!J71vpf(8CRI-?E-Z=k%T|h2kAWa+cC)@y;Id-6DV3mXPQx*IS%IlzK&zv0T zcZx^!%mQ!mG zLGQQH({4&4YMCf@Ircs#fvuy1Xu`sNbnvXIt_CZEF5+%EGvyLl2%sHby>E{PDj|7KJD-MWKRBX?A7;cCjscK0m*kXxt$-!g<*=HG5eav)VSxQdHYS}o@M!J%0H@WSl; zey*UH(o0j{{N0{7eDbqWYe-u->TGCv$uvs{C$cx9^-p1@VCns*r__Z0HID23(2i-D z>WBoog6R83{(xId&@?S?Qmw8(CW0^`+83((_N6)D_kMrAMv9a*hd~|!%;|kDQ&?QI za?u@^wa`Zr*YOcDonWKjWruuunGesB|4NZ!*l~fE9($ICbAkQY%l9u;MYPZ_J04o4M_to|`M);dqbXU#KWBBpz2DM#3pYwwU?n%^g~pZpXf9Pm z^7=jB|AU`9B704>W_RwTMF1ko=$tbRYdTYWI5`?lr?Rn@c}4v|S|Yi7PZvFNxZ|Ai z=e4#Ut~FX5UrbBP@bT8bCc9QehP`lqLC_4LgYL7vF^EX~UE&OGnzZ?gD`bz{Td242 zlkIyg5eoGA{dngzB+VUS_M;Ru#m}=SLHtjf92rUkd zir3DI;_fN0RT;dmI@rb0OQj%5>mGmHtje`$M?xq6fw~t)PwbK&)llKb?ndtboZFfcNT3E_Y_ zV0YuiQ6+FwC8OH(@EJ&zMi+j2@`fdjEaMGf<|zcJKGY)a42)GL&z!*3|mOPb&L zx0*+b#ze~kFN2JUT)tN^1PG}O3Uu_p*BRy}0@m0gmZ+zvF@hx+qxo8EZpmZm$>dJB zPBXvDFW#oBAG|vfsTGLx!#z9CTimKeN_mrt9&zM~9lQV$ntR{rsCd)o{L>}M^)B4* zMtg!gtZphAm2kF@HJkb!zB>AvN)eDdgYn#d5hp~7Iz25u3((fv%`0`rA9_dov%-u6 zeXj0W64db|&-wD-HzB9Tp6|9Xd}a-)aw*s15V6*+Oj-0B0H_JW=HRShI`Yl&gxFn> zc^@{Pvxld2YmgMr-Qhbqj&^t^pm*QoQ9Y>A_N`Z^5{z` z!s3WEDqY%!=a=IwT4E=QMSIqJl!y`bPN44bGt{g(abze*BT}O~9)+;|*czl^S@Z*I z#A?|da3$vm#Xkg8hygDR;eF-WIN&+lewk&Ka*nfR2h$29{rkt{z#fsz>v0gt{Cq1R z((H#J|I93HmCdlchdPdk^HlhUL>4d@C;{*=Ar!6!!6bRC`gXm9Y6BnLYN4uDwoDZs z1(T7bP94noAe!9h=>Hx3ZLtRfBg)xFJ}3ZbFxSTQDbpSq7pM8<)%tT6or2{$UYeI} zto>6egE!#fgYa>Wi~mDc6sC(-RYe%}(a3sZ`JV1B38ajXxSHRLYHK3rRuhI;V7tU? zQJH2x`S^z7s0_zj*pH@!Io!{XxlBEt8WBck@rhzpTq30kG4XcsidH7Y(0yzn@ z+Ygql67oMQWYb^et&Z^B+8OdFM#1i3BXLAD+Mi%7M^$b{3mFMLk#+v9Odl8%^W8CX zwXxB~2JC6mc9mXxbKVe$M!j3}@@t#oxw9?V?x{w5WYRMUnv+%Ekm0m1V)QkD{e?a| zN<9ryH3r@Rt1j}LY-&(B)n)#-o9cw9A*>Px=crgZKAvj|Fc)x-Lu>zDW7hkW42UrR zKLr!$C0UQPsOJs-jW>n{@0`XDX%z?%9i0iM$8-zRN50}p+jcjtyW>ZIWEk^h#&^m zrCkYQV)X5KrHu$g^|8N_&9kwZy}Z>3R`}};C`k_hk_kP!mc$V>T|{!9rMtv-D)o|ZTWf)-=^zy3L$7n>a&%>}aE0&Z)?(_MXsvSJ zL{+S^-)IN-jnDu7^fBiHPRmj$qRzm0c~6+R7ADyd|3a^hjfL>yd=y|SoLbt$$+isD z;K2WhO`>W3EUQpjSK-zvOGt_=B}mKS<#QK?N_g15#>)4*-ANUmT?1cx2K(32**(hNQSZh9E+0JnvaUWKZ=#=b z{Hhw&p!{2lJAr{ z5#tLiLZO!#&M%InW1?H$n|D!z?Nn?A!T4)$|G(pl|@E z93QB4hj4TsZcTEwyW)&|Ag|bqT%E1Bvt?+@O*F3*{w1Lw{N%f&pbv^X!+JeCM;%^9 zYv{{{Gqc;^u)zu9YM{IW8|>e6wgZZcdH2vuF7+9X#>HR2Xf0x$Adsl*t%5N0{S$PH zC-h|&Q2Hy9r2kfHS+O|VTkOi%DNQ&>I(H!vuw97#92YETWl%3 zq3!k}4$~if5n%K}8v=}O%ZiwLGTOc!^|@q0+McBO`cUn=Ni~&`cpVM(c4H$Zx;K;rA}U zhMKyHk)p)e@g8>i8I9*LOJ?0hPmL=(`<7TDxC6C30`P2w zjsfY*?#wD>Nz=BMp4K9K1|N|eoZcgH;y)Fmy^Yay)i$(6o7%kW@Sy`fEn}`e9H>*X#=2*b|*l!<#Wd%U& zU!V<7SKHvf+nee)CyjfXapgc>7)XS%euo@F_qWX>g;%ka#2qC+dyp^V1@34sDc{BS zAa*P20oQKxIwEWbySUX>n4AAvGh*W18unj7(|W)XOVz~ASzfqV8(|Q{$jR6+6m!e} z;%Vpnjo|*j4HF#Iv6w&``RvCG<{{^7`48@J`Z}7xyavF34erIRA)(r z+rc8kgsFeCMSr*VV*^R(cuYQvs;NQPb#RWz1Yzi0%wwV8#-P{3Uqiq=WNiu2=Eb@OLGD}l?8#Q66&R>{8ppv$*jg4zM$2*{r&OScgkr))Y=N$}?j z4ED)*#V-4o`-GF<<%Z4n>6;%38xIH4MCrS=GVS9e$$8P`VNzji>WiU1Z=Q5_z}L&W z55^Z-4QrgFBQ&>X>WCZO?X;#;TAB+Oiv_liyo#UN8?_j?Z+4aMS+8#t!XL>y)uJ@T zHh(^tEOPD&)fXymi8T_QIkTEr*jP|6(>+MAzm_Hc602p2ofZ~!Nq!~aX~c5E=CPi= zLFD^&LqxupCnlsCN7wRE8+r>HlZ~ooMgD7M7i!vRs~0Pgwpu^@H&ep#navxg2C~8Y$v0v1hqQ{0z{@5`D4Ih-pH4uR5z21)=SN zG9NfQI1Ahlo|v)tKGSZF^weBiFbv=(c`Lh~?lM;+88ip`6zvIU0o00PpFRyQ&YsOJ zeexSS#Kz+HUsb;bka{t6oWWEGQr=Doa928MDJgMQWZ@7W-nb9Z2%4;ym(L?^>BNH3 zL#8>s@v#p*h?)u`BNSW~m@#8e5DF?0xozW^nWAGIT|-qd|JI+kwIyh)q6erH1c~vt z#ZU>hUk_xNNZPI(F~7Xz?sgroZ8WijWGKo*7Ewu-$*VNsjkHjASKR&(oSl}x!~spe&Hyc4({;TBX|DEJ*j{}Hic>xyHhWhxiXlySb$}8DJJHM zk9vc46nXoP3(>2z%juCll}vmv5LlSs+`FBJIZ9EVQtRpjAIfOADIdl#K!Y2Z);;f% z>xV>LQ2}T-%_Wdv5OXARO*+={dWlU?5`FHmvRm88j^w*VpaJLxnj8f8HTn2)P>HF{ z?_ac%laC)UvVV=`Uz0(FvdVD_ONWGs0C^ghNi9a*MHqq$ix_f)@cc0u%GC7LNE`z@ z9%xxrdk62Y#7jgRhZ)=2y?=%zJG^Xxn8MC$Cq^#t$z*xLT--}Q3_BjhY9V0UrODuBO*~f9hUvR%q_1yZYk1h&Jl3rhq-hcN zmNXGHBs4UM7fGK+1HVSacG;crM(kw` zYGoW;xkiDqX4^_2`|_#l4<7)o=Hh05926)h1!1F9YUQ^|V%-D@12TDcz-7x3hZ6Q8%DUPw%(@gs7u0Ja1rmIRS zd76kyo^e*NloF*0l(?{!<9+-lK}NngfaTjJ78?ByGr3lS=Cg*XYQL9>uY*JDI=`jZ zkxaEEt0r)O?UV|D|3L`dzq%WWoiSs~6520MDbV4{I-jGeC3COY30J=bV{Rm{lxS^O z$a%Z=07&vBlAB2I>F>kZ3lP>qGjUp`^mnR|GEftTKL1dYV#USDRz(#@bjy~fw2y+_ z-G0mF7$rUZQ%u}Q{W&5K-DWD0IDhd1# z9z;b?O zZK&$$VjoaZ4ow*3&9_czX;cYmi2C44BrH1Rqwa+|<~o?=Yct9JMXhMW6@3mBojN9) zE-$1j53{LaMgFXim*Q8Y$Y zWXiWTse$!Hbim%3f|c&E##((YB!L-G&3a-#(k%|S_SEqBxaA*t%RzXUQ*+b3rLwN-SfXh!aatr`@25)~JDvsJd*A#00q+++7>jew znsf4)W6FC!U}z&sMtK4wTcl$>J^2-_kbiMC?eT@OF^Z}*ni8G3mFhHXG}=5F$8uXp z29;ieD9P6gg`V6Kmv|fVfo_8i3IJcb(8*^^ovt7?1lG%n^|Kqy6R4e*HY-YAs8 z3=OvqE_iYv69^hCD9#YE>W-3t;hQxb%?;Y5R&a#x%Wb=#Gf%2U?8G7okh9}3!rwMj z8lMER+S-BmY)9k{BK}yrjz{EoTI6|km_jw5a};+3024%X8;KbVJitJG(QYC_a=?DJ#4zq;lM~5sT0NM$-;>MR^tC*ty%7QZ6K*}r~k9NQrN2?cA$7DB^ zFUmUt^r+M9u@;dHU`-7gG7;Yh>(Tq9j2Ira0lIR_wLgFQnEp=f z;W-2KD6E%_xIkUV$-0%iiO7pD{Dj1rjjI?97$oQhMuu+*iY=z{R}_U1B4JK%EDThhoj%mYY@8f0#@$u zMrZ!GU(GeTf0~`%>={(S;q>I7Qp|`H^t!5f^8!!c+8V&`0vHhJ)vLN{w}$~P8koyu z>*)rP2P^_24i}JTdL{Rc)|bEPRmB@6d7=3g1(8X4iPxim1L8#}3P*Z9Ufb*O*fE9j zOXE9z|8d0_jt5l9zjZ}M=)JpVKF|RWhZK`JgV*%<+hUAw&dUDn0won+>9jB;;1ac|O zn@m0%zJf6c*@55}MRHbox%x*)f=#fP(}i(*c=$)e<*=>vUYy^9p~KS19qj>R#mc8J ztoQhMy`>m@@csBEF3I^8Kv;3+_eQJUw!hlI7FaXG8m37>nYZonJ__qsjl&)rXJWMC zjqJp&x{lThHqHST*bbb1CrE0w`V^E+l9+ie@M3sSo8eyc*e#??ahD+HxIJxJ2F2RcL1uGx#brIB4GlE1lPh%WxCpB`B$awf{S$8co0%*@pM~ zH@gEPBVqUQ@<&ybMFuW=+)*u@@IZU|-G>ZrKZv)WsZz+HT%UVQC39<1V_-4DwrOc; z8Cu*+Q9*R~&x<);#yYd)QPnVGrpyQ!-_#NI?!rB@b!VrGa{|VvazogHR9o?b6#}B$ zG8g!mIC(5DaFUaC&B#xHjEidy<17YWm}ng>Vbm^7+E{B*hb%vH8P0T9ISxk3QkOttCF?AI3JQ#PgnXA5Tuv^|UijU2 zoYOh2>YL?c?LyxJSd%xhX-wi4nlShg8{5EIj57uE+l&REUUGWWaty?N^Gt@ET~dOx zGQ&d&>B)frS5*G@R&Dyuh4-%`#3atduNnfc`h`c`;^!vB-|X`B$;Lq2o#{1yiNskl zrzXrlX{@&_pYeGmdT!HU@(aJH>1f4$SyKWg%+40K0c^FB(x;_^P7y|6B@fVX5V!w8 zqyC*|dlN2bkRjz$uqkFs803%Q;Z#x*k^v?<^MaJ$bKx0vrt1KI6b-nvCom+ckxs_L zgwt>nUB7m&$Ljry=6J%tYEG~ijhZ%n|Ejn0sk6BnJybitDA}MHP;{IVhKjo@+0;&f zkOx$8*VsD7_3cL+fH|!nSnV*!;!KfJQTDBSN6$O|$t*uaRf#AJpv@rZ_)^NdUNq5? z?-U~Q`F(E0pX%)FR__NMXBtPx^Q?2`2{z6jm)a|iQ|YX&6`KTwCOsc1~jf(&j4PmPBsGkfyW5V%QLs;URu0Io{1m$Gt2W^!jEaUY0Q2*TnXA^|!WG9gy!GQho^LK5xAy~3pj+?!)fFI70DL$NBmHJ8v7^vS<}7ynfg1PO;sWZ0b)9EN~FonKn?y@?M3n5zO$w2=V#$jnMEfQD;*JpiyPUFP-G4N+N( z_7d-N*H`bq^JVi`u$SPHxZmfz3?EJ#_~2KrI{#|2I9QbkUmQ1ilSW69o{mpFT}ji%L(HO7`~{ zOqKpRcyC8eDP{vO4y{{4rA$x1kYWKrM%Jvuk(Rv;K?ggfi*~IuE?{YD01lnt;826v zTtOUmSAsFUM*pQ&NF+D6{xa2a-splYPhOg{z9!*UC*djddey}~=6$vrMO@*@=_YzZ z+=pr39%Nc5*!0~7YSp9JC_9u}p4sIyxYv=fWF8kMA#zsWtD(LzZ2O6PUs#r+d0-G9 zcy-}%7d%FdR-o!PmN!ir0b-WivQHfFNCQXBku)*@eTO->H4?zP026Tl8ex-r37Ckk z6_s(d2wH(xWVV~;VKivClTM-r_}DVwL2rX3b)=xfj-VdZC^&(!Dx>r)eDow`096D_ zMpmdH(QJQ87B(I~Q0;JTdSyFb=f>&@03YS1czsi& zWiH;4Flb!2|0m5E8;#`^%NGmdgUO3dmLfD>N?eQ-K%~G6sqTQj z`jCU+TzF38QfK01&V92dQ;D-R!U3L_4Z9L$)+6r-pm#Pb#&imlOk?E9z+X^vb#-}s zHz@Y-ohSZYzL4EkvhK*@SBSn%|B!^(}Z9ek*E=}+u-TK8TXNJTG?T@7znhR~7@b~~t* zj5;E4vtn12p4VEp-cv{0xqVXE(9F}d`Vy|$(N>06KmQ=BA&xTv2C}H6NVdbL5i*j< zFI(B7BDK(ixS31+|rl}9=u7ye>KOgH_ z9b%kqMN^&)^tf%=O=6dw3jYZQ3-CI96S(Y^!DT(w*}RcIJJ_?^{xvJiG3KGymxGvq z5MXh3&1+>mBFgNsTkJQSN9@<9d;I|3RzwlBn7XcNJ;0>h^hq+_n({c??GR4w7fxqn zZs{EgUXv5NZsPRXK5Yi42n*?sOv=Z>@uSrK&2ebk z{nc<_$Xk;?W_g3NSzNLvE|pa9&}L(~Dsiv`QP0lpE3KNY2TPT(bP{k@kH-4`_t9RM zI0R+a_g%f_mxcgU+l)P~dqui5+2~0I7vhItR55sq!Z{IG+`ajUH{+qv$m6hCa_t%< zac!JBx!FIIWA4mAn}mUHS}}=oStN6Hzf0;~%BGsBA18$MwcFy{;+~}&EOnm{ANxo) zJpav4V6(iZ_2_*}TzY;~1 z=;rl|3F~Xi-w~{{4igagD_97MxVwfbq#7tCESY}0ae8e!SV!+R+!N1$#X#c!Mc&WH zGrB#jnw56xX#S+oDT0mt>lYPkbE{`x!MoZ>JX)smf;ACX0-I`y80C*90)><&cSIZA z0GPVCpjO89XoV5Tc2b&~f!+fsN%5-%xB68f)oVq zO&uz#gWcfP;#n^P zK+IEfR}0&S891t?vqU?Xq~JS>p~q(&dU7*{#FBRH!WHyuL0*}@T%^?dJU_|49E6lv zrj&UnuA4b0E`|u^qlZ)7kE+xW-{vJY>&lLe-(Am|Z3LofW zGSE?{WnxTgC}|}>RX?~TTuXfZ)vv;NTIAqa^3kd??Lzo#slOp&wwgVXBddN&#Q7Lg zqPh~re7yjcWMx0P7(8jwbAZp(#FXVjUs+;p;;Pqvb5DLrh#EEdC{st?0UQp31sW^l87~ z+O~cT63?Gg;q(jM{c*-^8=q$4dN9jGgcw-&XNjmO5jQ^DM)dF<{^(&nG;-Vv`;1xG zjGJvd=vL zH1s~1i-?MKMqp46rrOo-)uywiZEOXh2qi}jS>FH7kc#`u7Kv2Z*fIx`>#9?QzJeGr z_-#2-C_GgpH`^e4VDb>pysy*z_lN{-lF%D>8$^<%}&}#APi+7T3-x-V5u_QaV?C zSR^2@PMCq3{LAB^bXlR9;ZNc7t;Ax8aVqnXQ%Z*qhkBn^Upa|EBR*{(Qm3?uU?9;Id+Zm}aScP4}AI~eGp zr(@&~&Igq8EE>w6saAx1@x%hAL1NwH0AH!u?Ou5ko+qo_ig!e8#PqYk;%@Kcak>~( zYAvuHy_kU4Q(GaIuTU6ufnn3`&mK#MTZIc6Ai%97v6_718e}fqJN?f#hbe})2`U8w zyE#lrg^1h5PH1#UA{_4S4~uMns==-Aw5T=}V28u~d??7{r%r!dn~ab$gHlAsLa?tJ zOXFFt+`mL;^L2<@(z2^eoh_j(HS1of}n%5{@b^`{J$+U0Bwj;H>m z!;0&;4vD^&ohgcolS?l^p!_&4oNBFV7w%xzwPiAkbD1u}CPTpP+0pVR*8O35d>iZ7 z9SdI zz;@|bYGwWvRTLKUtCg?5QZ)ce*kAopl`jnhhef4Da=N~MO{C>yer;}mDF*LA_(j!? z5>6t{+}#Ryu-;PoBRivd`cF*p^wNVHhx^pqSgJXs=r*WC3yYLzV#%l70oNgbHsZo7 zS?@|!1j&aAIUZ*6Y^x0mDny(~#eMQnO{-z}B>F}g`*8U4f*{NR_?8YptoUv4=ON+<8x+Z_rWi^8HBoy<%v3Nk*?5Lhpq zkSOTj_kf3M+uFjCl?jzMWGeZfZyw#~84{kP<)7__@`+U}B)nQ<87wAYkdZ4z8u$>8 z^IVk$;x-#pulmebx8qHFj?Iwl|C2-z7G?!CZ|p%XPxBo2$BeWmGGZv+rp@qZn%k<6 zV!hoe6M$Y+XHJ57i;a7{^Vbb6R=jlh6oa}~m_$VVUCqot^N-RS>f6INXS^H45}7>o z{@TXQpN5munxabdJhK^=qNK5z(2AK&Nl=mnE$={>p1ZO@|lLG$V7l(%Hf_$cv zN}PD4y_?3DmPX6hQAj%XZ2XhavgJ<{uA_6gB(E{*q53Vh%N3UUw}l>9w(`dEmzsAOwsXs(5*>whrH7L7gaX0zbjM3DF8ne<1LV5{MJL9gT41#XTGxYAB3AA$%K=uCNLTOcLzLp>q&MV%HF zX?`8Q`E+gs5a!7xxfEbxrR^b-QWh6y3L7n65#Hq_84XXyzst6(OnR}G!UUN3cWC?i zN$U_5$@0?0nGNgAp$WE}OK=z`L;h}kcG`*Ce%dNqj*>yzRvjxk1Z0rWKH;!4HI0PL z*klK?+v#EwmT6)VA|+DL_2p6$C8!_WWaHqiwB+zIKRZ(`W`FCep;>LnFzm5^(?_4$ znJw&>e=yf1i-(uR;X$E&Yqf{BH4Of<$2VG{kduH#uU?J{ATaR|-o_DB4;zc2+H{+Z zb)gV`4R(lb#VO7cj9#ul+#AN&`>2g#KDBV)T5Zh zqxx%D&gf=(KW_Xx)_Gl^z}tZ+hpk=KM*`Ad7}+Eu0cVL+M!l*P z3cb-B`rcv_PX=ML7h(J?0r!N?%&1vdS|^h>5Vf2Gf= z6+@Wz$N!ghw*j=r6#lt}O=k@th(aPmfiEj?XOtkVKP>EzJsx#7tHpkxtpxPdCRQ zY0HhL)<)7b;SiQzqjgQTdc1Z1x;#HEZ+#1Mc9l%4tQ={!k!gKvT23J3_+2HqR>wR)Aa&LjIa?kbYU7_&+rwCSLg0WTXT zogNouYPgqfjPW#wtdVTX3>3r<;b1s#g^_IR8v=ua6`x~4>Bht&BSF^#9-Te(Jd35E z@PR10y)`Gh%BYtc0u{Fs39_-VQC3rHb?-*4OgFph&Sfnd^I5%a0#msDwo_Xkb z4l=(dnA~Ye^G3$AleTuouGGEzY6$OZ636`T{m2Iod*TV^6Se71v10oCuGCfCcgv68 zL~A-)HhZ#@5J18VM~PYg9K!hUzylykHqR5wo7n7;h5Tm5>0j(9EK?h>SatM`L4JPz zdh_AeH^8oNr{$~M7j0AuBz{WA@*cgZ@r^qrexmD-l_G^Kx74^UJa}@AW6*0C!+`d!b19e0IE%GjXqCJsrOk;0cig~1502@yxZp3f zxw*80%f6+6@H=KE-cGWx#Y`@@bfic|VGny9w=v76GEASdis$NSP{2r1-i1VO@;k#b zvQ$qn)Dc;E4Wd9Dr9_oy8Jq5ehov=)+@8^IX4LLLiaNMDqyUA>zYLEZ&yS%a8Rx}S zUzJwFGalMo;=T?{EO@F0M(u3@n}Vk?ZkBCan%d2r_rK#7MF8Rf@99@h4z|eZ-~;-GXVb&I&s~lR}7os(rh!W zw4Se3?SE^Oo}E~b7v+M_Wv&aWXk^V=z@07ol7m4g+V#M~Bf{sfyj{s6#hi^^)jn|* z(U7!51lZlpJ5d~;yFNeuiY0zvhBiaVOra&=iar^fz*TD0BCl#`i!rZ$V1bz4TvV>R;agKCAFHVQrUzLRdSHd04nPr+S3K4()O(^toAXH2}DIV z2EVBY|I&C4rjOWr7n`AdncW!L|0lDt*=wK{3qZ59ANTanjvpcXvQ_LsAlR3vq|0`8 zx6=Si-=Qpdy>UQbzv!hb_Kd56CWkH_K2_hE%wV5>>}b#bm`7ewrDX8XKZAXZc#Bho zyULC&v%h@^pY^aX&xHEWb<=)ZC{^|_%B=E)n@GM%?s&-#I;Qi6%g}h9vFURF4F+0_fYN<;yYYd=F<`XNqzya5Qso0mswySd_~Tg77FL38h(bxdmyES6RZ35@(@d}r z3+^GkN)3dVn2AYoyPdW5r+iXjVZAoOvhwoj zI*6_y)%eCqv6=uR%lOh!wONKZT-Y~;Ux^9U)-lZ&8(FfJ_EYJE`O+wW^*Tu5CZ|VS zCRiz%t;oj)2P9_VTfOGDo|C4_rtc~Q{Cxnxc090!d>Qtm;YOwW*PjN&J{L88;6mf=IpaO*yi&LBeqzOx5o@gyYN%E+-RId3<6nuC zwE!ovrgdvypUw<)NbD?U8|g?OFg#JuTk=xx-!|^}g-535R31gljmoj(58kVldoP)DnB# zO;I^&!~=31&aCrr`OLO`%kZ}U_{R@hl(gWo!z)ekfL+)GlvPfiujQM-okwMB-2FDE zkBCGw@XH?+am%i>vholq$Nw5i5PzT=E;PW+H|AlCQWB$9@Ne4uPM?ULewENEbsPt$ ze(w2r@QcF8;OkJa#OYya7tB`Q~@29e`Iko6)k10_*wz@&8#1_);Y-%OC@O zx1-jvR%DG>49MV37qm_o9r*Wwa9<{G2 zJwZX(flga-8pW^(_Bx3s9%418NI$GJ!i>C5kxYzth69Yo&{6S>1Qsimb!JP21qkB0 z8`qoSBAceR9lrxk3LZE6;*+&Gx393Mwm)2uW?5NT)!Ramz=0L2h@v@>Q0VaTm2{1= z_X4Icd40-GkJdO#2rML25?hu4_Z2FNdy$N`M-U?AOndxsfPqD8X$=Z138SD2UAfhi z{SY(5%vKx^Y1-q;PufGW zI1-E4`zD`(ec`)rH|%`FGyk{JiW%P`GgD~`4-+%uQQu6l;7h(OFBUGW6xCX{RUuG$ zfRuPYB+%KRa~h+Ad;4X~}mkupOR1Lop}%C%c~{UjCLn1Tf0Frm-= zraZrAPW@I*jI`4j^_X~@ZZp-pg>1Qr{MbiOLnPCpI4tPV$As3iXfaN14>#C-xLMi= zfaLM_F}pWdK58iK&J-Tm6%6Du+PuzP^0C>D>c~WHDfG0OIk{r58o6-s-r8Rsss|PT z*zH2M60XhrD@7I*kkK+QOrb`tyu?of{3(YJ{c{4`A7R8{Hx{Is7pT)NJt*kn*hF6?m7BTfhGy$I(72XT?H3G|_?&zE22 zn=hzLdxf99Q;WV94}W7fGXWYtiJ6&}IQKBW=cbAbaENksbM(@6Mv*r8^t@TM_BkaZ z0i=-yP5*jfzj1G5iecUSR3QY+3Nnz8lpru^X)PL0?@c@Io}f8gpCr=SDJI0|`7uUe zGf!jTScgJ9E(ZiD>zH8^*$cvB44|!RoTM3fRoD;=rRy@{B*p02C5FGn=I`(Iv{p}# zFjc3*bVCHOoE+D4i@SXx9EUb_LqyJ#!v5ft_2}VxikKiy-GPmdjDMW71X)zu3=kN= zy{M#p5w~HGW;_FW1WC;lrEUTVIuQt!WNmo@zcauP3RhXgUc~zRp^;1;C!%;Oe(rGF zeo=>$Xhd`Hqv$-|If#5+DDD&Q>iRF1C3LYXZkq<)MHI$2f&R33Bm~_{u~^hsN6!i~ zm5X1>iL&{8)VhI6k>TpoQQY{?$arsR7JZ1Mek z`Kv;`&0-Z17BE2@|2?`wp%9<-HWt}#m$`4pwcort&?~^9a|r8ihNk1jl*RJO45@%- z9Czo=ss8(rPceBVnL zj}1IuBOIL9Q8tvjBGi1=*LwT5XwY0yS*C0x2jF>=SjlCRe1Hn<>77>H7af4Tt2XSV z)2ucA=3VEKMF8Z@X=fD`-wu(6S?&=D?vHM+^(EUhqw#~VB(=4dFYT7Z;l*@9)Yi`3 zYrg9~9psi7b{n$Q2~}!KbFq3!3gXr8cI!JtRe-xP)$Os$iJl_t`=|YTn+DF1T{; z`1&btgSD;asz16OEii^i_R^`fKHcp*_V7Fe2jrMfIN+hjpQhkVd*5B6k#DWIKe>g{^a;AYGN)hG^H8oGq*)n^fy z#He4anZu9~Y_nB*U?Macb+As-ORVnLl`S* z<1hkVz2h=#pWhgkyv^D8PllGA1hY&zkpWfgyyJi#aiPCwt7Fn<{$7a`m}(!&PmwYV zB>(udWKhGsq>iD;ZsOJ=tT1r>({#2aO4cOecp5mApvu|{$ApI!M#5K~i&#TSb{6Q#C!#MB3r_(K)_7yIiixX_BUbJH9E8HR&-OM^ zmJALe)j7BUvX!iz-~9uVc64H|Z0c@p7EcFIP8Q1Gk|&EfR1wag;%)N4BMS_y_rAK_ zIqSoJPKd4`SgvN;?V>z|YvbZu zsxZUCV|Td7e^i8ZSBhqNdg8136uGYz|0gB%C#uBqWiJ!MlJ&cJhS>hj%2hd21bYo# z(R}Z4O13m`yv?HPK#h@=d-mHmZ>aF3B}6|_JNJUznT<`icW?J+z-bMuxnuzBcCV&- zJF7D1lDg#UzGJRN8O91G&^ez-VY)h4rpD?^djMLETf3(i%@5Q=1--Hy6h`-BI<-Fi zOq;ki+)q#zYNF7(SnCPc--RCFLLuX#g2Ljo)4+z+`VTGZL^_;z4#fo@pNzeq)=xPm zQis}b&3Dd$>bY#;a)*O-s(;sKPx)72CAZLWT@(R|)C~)dY|}??_ zz#Do=5tf@IsZ0TVYriFK@!X5?Pps?2uzuV)77vf%ilIw8DOAs9IJ5(wpH(&m0GV<5 z-gMxk)3zuNQ-Z4`{T{@(s_O$#H&;Q6ZyW_kI>g+2U>uyTfkcE%oi z?^_86z`#1DvlrqIoy1$O4Rmij99Mc($M{=zg_0NUU}@=SQvfJ+2K$aI7GXRF*&+{7 zxfy^p`o<02gW}F4qE180m=;VgeMdZNmN%Z*Q-ld>xN+b6G&oiRlBwxL>=E|nGJC(h zZ-%{&p%&!(1*-F$bkhrQ1(YP_RP=3nh1p6E;U$0goGLoy9dh@AtQ;lT!^=}+*mPAk|d_ngho?x=df>5Y)I)p_D41jON z+n~qWzOJIB!-+*4BRT6Pt_)vR#Z+ykL>-Im4IL$V3sw2wbHA%x zcIhy5j9aLSm|447FMg#QI|=ExoT@pAdbgN?D|8wZ-Z5yD@)A`Sz$B^_0EnOgfVAty zmlsvFiwZsLk`Imz9zLL~@77>shCyCV&u|X`8`vmmcqFbh7a@U)IVCy29%9t+r7$l% z_FQaz#tORmtOz>UiH8(&TIb%hK4j;*0kPNFqFc-{-5*<$7kfzjuYiy7TO~e2SCA;) z0R>W`MerNB@6U#Y#SnxC8&AnKo{{*oenH$1>Fo$C7YQTi(qw2L~px7 z>bN4PB;eiML(QTLDOO0ir(9cP@<-Z0IwVdWA?KFkV^h9+DtXh1OG^K_5IOSUz@67-)>1W!6UIb$%D<<~vI{oLoZWzOSy z(8yGS@KG$}P0)9sipg!)?#a9)hvYvC862~>01Mz4k`G$RA{2+>!*`)iQ6kXQ!TJtY z@Icp(}niSRsc2iPN38@N_Rfq*^ z+Svt%m-h|U8NKBQh8)y|D+erk>Y?Pu=V0Ylv{a)G6rGE#msmj@BNFr&?oth0&E za4HW%{q>Q%$_$H4KbSmXf6^DX`QT6=IiE$8rdL?pI&=37oNJM&w_1=-Hgo!#v`zfa zm4PP>ied^0zd|N0C{s9#8ln*7*9!?>3zyyai@icCK_UUAi#C7{K}ts1P36&dN+-b~ z5^jE6flZlTE8R`)YlE}H95&A}STOBRY)yi><;Q=wrLI)&VNzlAPq+yGE8I}zSp+n~ z71X(zxuvht)J|_@CRhRo5#u<`J|qnjUi97z(>yCL^H z2>_44lvEJqB3v)utV9C7qr?Z_veA}RZwh2N$R)@jjn>avw2j&L`w9>zGd|l~SbhHm zZZ?hlXFmh}_j2|iM%ytY+`6V$&Kg4X1pI;%*!l~e87_)~Is5EfBhX?R*IIpB3VFbv(X*HH&h*a=ttlY1~eLBCd^9 zRTuOj<^Fv;iDK(>NVFi4ds|5lAs~g>^?4R97TG=A_o6R35B~pZkywTK?+-zWg=gFS zKs%qiBX`GI37{5|2Hwal2Y{tIsSN_4g%tJHjqQX}yTL*`g(S1o<7D-vP2*O*#HH(o z9h0r^A!l1V!&6!7%yGi>%)F9AdU?3i%sDaf(ly20+B>cRb0>28sh-xY?-I%7D>3n3 z%+NaI|K5}j>iW_Hl&8JT5?ka#f7{Nj=Ul~z{C1o`3F!ptRElHTWQxpB@6TDIucOnj zuX%S9JoKlaJqB+JTAjJHkNmDEJxr%cTk~j3M*Xe z*6FoSnpw;vhIrjp@3O*KkC$HQfc^9NX3T7lZUiU^>HUqVdB>lAtVr!PSMk-?NQL8X z<(!g^4r640t|a|}6EqPsuNUAl`Pv%U6Ax$}70%KQ&@pq7Nj&~Ntoo_uD(BT;C#{5oQc_Yj z4h|}aK(WR7`JxYxkCa?oSIvilw69+i$zx#=EbZx%^pFQYBZdHIM99Jv{ZGr*)pvx$ z>Rmw)eu|NFK-ILl?E|2nlRfbOZ1l=VT5j`39ct1!LROII@1je>Amlz1Jz0E zdqWcw(NE9LYHi!Aj7?0wg^9m(mH;O~DWyqA?fNGjYO zU$TNPhb;Yaqli7}xQvaBKNuSqwT}-DCakULBY;4v1qB5~m6h_{3<83JjAkpE|1?a) zzZKLe`2+9$DgP{{2Q7n}(p%&Keh08tkj8ODkJZWW2>*7k5(W!CY3)Xdu7tvfcE?Y2gxfGXGeg+RCLHOp*W30cG^ULEQ8oaOPhtX@J=>fK#*O$x$h$=Y3e9 z)a*v50@-&$(3?dg|M)9|_X5b{9xNt%uU_Fp{t^bBeCg2nBU4C)DdVQ3m&^Z8V zi9GPvo;t)3>g6DVs++>Qfz9tchPcHAVV?Ffs>)T6J^>;1F%sy$ahzPW>Jx_}4C0Hg zPIqePfyIPJ26i^jjB+#nyRusG3~WG(^R$~=5c&|b@|TVD_mQ+d=anD)h^xR}Qa&j$ ze7=3Hszpns&r7Ld;AF!#67)a!-Acc)1weougT#qo1)WI8Ih64h#yVb1xnoHmE1IPC<0%(3c(yYn|jz(m0Z*pLHc$ z4gT>6{6=_O-~S-afbV(%%o2&mFwuI%8LX}Dr;;&4Z4)v&x}P5KE%&aZb>L*ab9?`gHA zWQaLCi615uz%>r<{$!X>Ri@(QbWO-_%UMBs`PxmK|9zK77>PIG5b$>4v{vzqgT-vT z98%pxYk%LIzR5+|dz>JZSestD0x;lbvO5$W1MKpBaAv~`^pxnz+SD$ihnTo8CwJ`s zKLY2*({-3;5?J%mwTY5Qo*Irgpqo$rL>IMzXrkua?v0m1ore#8%ZxSD*MrWRs7jmP z>yoH5$uL@p?82h`ip3T5|KFf-%ht$KfG4mgmTJa_CwExm73tjY-{1D4^ub7Q|JA)$ zL54!TydU8j6YQ&3eR*Zvq3=4ya}#zPTn#clD&-un|MBHVcXVbm6)ht;A$6u6Lb6=c zLbG=b3!kohueb+C1BvP7ySzM;ZHa~++-9(dBCjc#W4w(S3HCc#d%fNN`TJ^-=Agvi zq5wk)t-@c}TeUhUr#F8;aifB*XTO9ApfPZs--2lT1`{rQV-{d0x?zx}6mE_166 zDs^zgZ{KO;dS0R+8yva)CO62v19?Km%EJZZ{i_wj87sr(RlN&W0b6YCZckzPiR+`Q zW!gVH8`I6>hzrI$fHX5+jOby4NPIDU)P z9+tsf@ZGEs0iuZ+DU(KRVJ$LLaCaZ z|JDMP{SSu9k+#Qnn;4g2VNx=l?@ioc2LG zD+s$NdtHy;s=l>s->9lh?>1)b+}_+ttGXOAj<={N5$|1IzKtK+%VgTWzPwCpedxmP z3;U1Qn}ZCy8T~t(d>`LyHrTdhJMl47E9RyGj2}Z?AyK{4(>gKi1Ws!|ML7pXHSL}1 zmwvf-dtn^7aje)MG^`%|;#(IrAc{MTy-eTXuSfISB!Gvwzw#+D4B};9`E6+@s~%dQ z*yZEelTX_O9{>aWm_grN~1-)NRaUI1q&G>r=HB)P8Q5#+&VQJ^x&6f;Ef#)#k83 z405lk+%7(}&-8Jz=6E;FPb9wL^N-JQq5;@Hfn`$^D|o{ZpUIM2E?6YPlKQ5$&DMM@`BY!WFDi;=}P# z`gp`mjeZt5Z4~gIC%jEf)_y3kI}j6m`H5UUp*g+) zlReR0SwHS0_kRB=5A5Q3v=ukrJwYDtt7QVhg@70BC55T!9Ayz&Bs6Vj-Sh^U`txn~ zTAW?iM~logH4c6L^-*KK{7dAP0%6Y!=)G9|+uf;UyfCI@c90`@%^zH3|}9 zDzJ%d5ZF>=lH^cX9LoBa7?lRTK_rjqy`2}Pu~|B2`_{l;&-eRZ8|;NNOSt0qHbUv>a@7# z)W6Lo13K)pzF$O~18)4=E4Z(+bSYVS&{hgIAAk7BvA%CWCt&#l&|?hc3VOxd+u!lU zp~FMr;%0cRc5p2$rp);Ztv?(`*L!;1m%05cZX8ZD&2BIm=B}OJrC-3q#3WM(>oRlw z+=L!S$VHjzaNI5%Lw)<*3;_NZ-=1M@>|lQ>z-+Z3f&LN$-w@CM>VtfIk3Z^aw4}Yf zbeU^N+{Z2`r~*b&M9>}G8LBkE7g+p9p{ntJjM+aPnfCb``As9blk{VRdA+TJUre zunU}BeSopRL{l91QN~Ji0`#x2pH-ZqprN4$zgtYY)DwkjsnyGaWPpiF&Qg9se57vF zy1%x4UO4M4cWM>v>p-FeiMKEi_l}hF{WFtpbZV2+PI(%XX@44n&Q~HRbEux6A6BzNKs(}7ql2Sk>U8<+p0&VL zx4V=MX}$q~3#bVF0S4z85G)G9vK)O3{RJ}eYiC`-k&WiQYckqVz{055@W)!x!h{D= zE9w%zH#iD1h|5T7ygwe03%YE+B z{5V(rk&fj$_1>uR*=VyNcVtDJ#~vP0eUY0FT`3Y+tNUWU_Bbvf5s@@WtHhp!j#5Juu}AC- zZ84=}&# zpU)5HoY!-{=REJv=X@RsYoo{+wgTYE74Rnc!sDjWfQ@Xp4OOC56TL97c(J48F_Gre z#kFVEf>JTtJqH>HgS=k9_K*%wMoiMWIP{qHjd8(J?h(hErG~V_@<4h>6u;rwFqsTH z!stcdCsTY9{5~m_Z~Tr&;l6ECkwy3~#5OQd5mXYz@2VJFz?4xJA*W)#Q? zrvtWu6kE4e!-Sa!a@(^pf?!L#n)g7KfB_rn{|gQ`?nJlOA0Y+2K4V;e4U7>)EJ_lcTUHjj;1qPuceyzg6L4O<)*%=ZvJh z%>!+HeV%5>9Abk5&{ubv zzFiq0^5lhh*d2`vG>(#w645u;k*g5R%A_>2>+_3zAZRydbHpXpYo_r74(CRMWuZlj z=%Ab0p2g7EKEOiN>sbXVmRNCS5Lv71A3wTWyYa29ckjs>+2zK>ok$^`^Z@62_VJY7 z+%%ADTeEx2y?nmwF-TN-*S6sIA>hKRie1i!h;&~L#!G6;R8Q}rILunU2D$B21)4y> zjF{0#Qw4WTZ}q&V2_;c^g;M_SzRQxZ)L6;)XoJqwhB}2_)v*Z9WV0nl28JAOxm{0* zVJ-IR+`@jvtNQ{F3n-ApHfXz8F0mAyNd|7NMJ3xYE#TcyY^H}T2J)wugOls@A>A=1 zv*luK%iO~4vB4gZR>4zQ1Mmcd=#II}kiF2Al?UMh2B~}~PbQ5ppSpUUN-b?4%Nb(S z;ZIK99SUAbcWeaE<0xJX_fxZGpo#xd1vas@wJuh;>C3yh&-lG2PMrB| z=ubJB$%g^*7e$US&g9NSq#?x!=QF{xFSF-SNcwtM9%Q=Ozr zb$FAQ14v>&gDi?))ryo~OL23Zd1ccjR{~`)$`Q5)5cn8Eu!X@?3;#r~>{70!E1u5 zJBb9HY%)XyZ;x#Ji zZp^-rpRF>~K5E!nC}^>uGcwIs(lh6pu`MNFKQW$6F$VBjOOW^?(^7}xJBLEPw~F4X z`EX8+dlGFN3M3NgzrYlNRY=66);}r}(m`UmDO|!~ac9(4W~GScs08!3`r)X0pG0bo zCri5eRPFu1=xATj1}B^tg-$u|fdsen76|P(%ZedH{;H5t#C>xMSdM616FB@BMQ^w| z;FpP0utN{T!O;1m>gsMjklicSA=+X{Kci&=sU(RNgiD7OI@X9sy&KkTgPK~+z9~RI zTIm|e3Tug4H3$!((3>OJR3xybQ1G?b=4#(rI>o!kd?t<(gCNV;rw!BluLT@fOq*jo zzZV=6O>wrEgBB_-L|FY(uI}!aD=e-nj>oPJ5bW8G3w&DdK#%6MI1v%YSrnSTY;|Xz zL5O>FGPZf>K(7d2W^54X=P=rSA)$gcR10rD^c2c=*#wkj1DQ<>Z2Oe@z5*r~ML2T)Besq#4J28ocx&F|!V zGB-AMABE#Ve2oUxM|_j=RM`;h17LK#@TRbaJCY)UWLga<%Ew)~%x{vaPtLBgJ_(EQ zpqV2E52>Aeh)a#=O?u?cOa$(mO{T8ey?fzYQZXk!-wKBXYV+llFhJZ_{~6r`9()A8 ze9(M)q6jrWZmAd9>B%paM2dN1V#1SULzI^oR&rgu>;z47B>^9j9&3)fTl6Y0Z}j6%IK{SIgVen**q1*{W{*LE( zo9Af%=i_@&GCAoFzdyAKI@3++pLLs7JQ>$Q+#a3MFUHI8^omu(6CLGs1%=lBL?K(OY>0zuh zHZE0W0jV3Y#CD}k*gf!Toy|Lgq=kJ=8F+sYK{QbS`=}{old(P>?Tnd(+Xa`$1{DqY z7Y(`Wh_JA?vi>vnm2reI_*f{*g+5g4@_hGlMH~!pn164jLVTH6eR-TNCd{+VKfRqd`fO=Yp-<7wY}4OwND+IAjf9nSywvV#sxK>1+t_D*KsVB) zrR0|mVgqBOXl;-P&Qmqo0G42NI#P(-1~`eb)++1?h1()2Q=*3`GFX&jH&4E&#|ao! zEIaFjgcPk&2c3T~Pq;2{)U(-o@w0;0!JP6nGfz|#ynms#l!|i)^(+!Q@DhUnJPBN+5bi6dF R^1TS~IM|%DF1Mtm{2O Date: Thu, 27 May 2021 09:46:07 +0100 Subject: [PATCH 06/19] feat: Write function to write Cypher queries This is so we can create nodes and constraints for each dataset. --- loader.py | 40 +++++++++++++++++++++++++++++++++++++ neo4j/example_import.cypher | 16 ++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 loader.py diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..827c5ad --- /dev/null +++ b/loader.py @@ -0,0 +1,40 @@ +def create_query_constraint(datasets: list, dir_file: str): + """ + Write query to create constraints in Cypher and export as .cypher file. + + :param datasets: List of datasets/schema to create constraints from. + :param dir_file: String of the directory to store Cypher query in. + :return: + """ + aliases = [txt[0].lower() for txt in datasets] + cypher = [] + for name, alias in (datasets, aliases): + query_constraint = ( + f"CREATE CONSTRAINT table_name_Constraint{name} ON ({alias}:{name}) " + f"ASSERT {alias}.table_name IS UNIQUE;" + ) + cypher.append(query_constraint) + + cypher = " ".join(cypher) + with open(file=f"{dir_file}/query_constraints.cypher") as f: + f.write(cypher) + + +def create_query_node_import(datasets: list, dir_file: str): + """ + Write query to create nodes in Cypher and export as .cypher file. + + :param datasets: List of datasets/schema to create nodes from. + :param dir_file: String of the directory to store Cypher query in. + :return: + """ + cypher = [] + for name in datasets: + query_nodes = ( + f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{name}_tables.csv" AS csvLine ' + f"CREATE (:Reporting {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}});" + ) # noqa: E501 + cypher.append(query_nodes) + cypher = " ".join(cypher) + with open(file=f"{dir_file}/query_nodes.cypher") as f: + f.write(cypher) diff --git a/neo4j/example_import.cypher b/neo4j/example_import.cypher index 0f09ea8..58e98ea 100644 --- a/neo4j/example_import.cypher +++ b/neo4j/example_import.cypher @@ -3,7 +3,7 @@ CREATE CONSTRAINT table_name_ConstraintReporting ON (r:Reporting) ASSERT r.table_name IS UNIQUE; CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics) ASSERT a.table_name IS UNIQUE; -CREATE CONSTRAINT table_name_ConstraintRaw ON (g:GithubRepos) +CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos) ASSERT g.table_name IS UNIQUE; // Create table nodes to join later @@ -34,3 +34,17 @@ CREATE (a1)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a2); MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); + +// Delete all nodes with relationships +MATCH (a)-[r]->() +DELETE a, r; + +// Delete all nodes with no relationships +MATCH (a) +DELETE a; + +// Drop constraints and correspondingly, index +call db.constraints +DROP CONSTRAINT table_name_ConstraintReporting; +DROP CONSTRAINT table_name_ConstraintAnalytics; +DROP CONSTRAINT table_name_ConstraintGithubRepos; From dc46389dbb1d8dc2dae4dc5670c9621babad3e10 Mon Sep 17 00:00:00 2001 From: avisionh Date: Sat, 29 May 2021 11:50:00 +0100 Subject: [PATCH 07/19] test: Write to test writer functions This is so we can ensure they work. --- tests/conftest.py | 1 + tests/fixtures/fixture_writer.py | 35 ++++++++++++++++++++++++++++++++ tests/unit/test_unit_writer.py | 11 ++++++++++ loader.py => writer.py | 28 ++++++++++++++----------- 4 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/fixture_writer.py create mode 100644 tests/unit/test_unit_writer.py rename loader.py => writer.py (54%) diff --git a/tests/conftest.py b/tests/conftest.py index 47113dc..b2c4059 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ pytest_plugins = [ "tests.fixtures.fixture_extractor", "tests.fixtures.fixture_exporter", + "tests.fixtures.fixture_writer", ] diff --git a/tests/fixtures/fixture_writer.py b/tests/fixtures/fixture_writer.py new file mode 100644 index 0000000..c8f1297 --- /dev/null +++ b/tests/fixtures/fixture_writer.py @@ -0,0 +1,35 @@ +import pytest + + +@pytest.fixture() +def datasets(): + return ["Reporting", "Analytics", "GithubRepos"] + + +@pytest.fixture() +def dir_file(): + return "neo4j" + + +@pytest.fixture() +def query_constraint(): + return ( + "CREATE CONSTRAINT table_name_ConstraintReporting ON (r:Reporting)\n" + "ASSERT r.table_name IS UNIQUE;\n" + " CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics)\n" + "ASSERT a.table_name IS UNIQUE;\n" + " CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos)\n" + "ASSERT g.table_name IS UNIQUE;\n" + ) + + +@pytest.fixture() +def query_node_import(): + return ( + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine\n' + "CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" + ' USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine\n' + "CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" + ' USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_tables.csv" AS csvLine\n' + "CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" + ) diff --git a/tests/unit/test_unit_writer.py b/tests/unit/test_unit_writer.py new file mode 100644 index 0000000..617fe55 --- /dev/null +++ b/tests/unit/test_unit_writer.py @@ -0,0 +1,11 @@ +import writer + + +def test_create_query_constraint(datasets, dir_file, query_constraint): + query = writer.create_query_constraint(datasets=datasets, dir_file=dir_file) + assert query == query_constraint + + +def test_create_query_node_import(datasets, dir_file, query_node_import): + query = writer.create_query_node_import(datasets=datasets, dir_file=dir_file) + assert query == query_node_import diff --git a/loader.py b/writer.py similarity index 54% rename from loader.py rename to writer.py index 827c5ad..7168659 100644 --- a/loader.py +++ b/writer.py @@ -1,40 +1,44 @@ -def create_query_constraint(datasets: list, dir_file: str): +def create_query_constraint(datasets: list, dir_file: str) -> str: """ Write query to create constraints in Cypher and export as .cypher file. :param datasets: List of datasets/schema to create constraints from. :param dir_file: String of the directory to store Cypher query in. - :return: + :return: String of the queries in the script exported. """ aliases = [txt[0].lower() for txt in datasets] cypher = [] - for name, alias in (datasets, aliases): + for name, alias in zip(datasets, aliases): query_constraint = ( - f"CREATE CONSTRAINT table_name_Constraint{name} ON ({alias}:{name}) " - f"ASSERT {alias}.table_name IS UNIQUE;" + f"CREATE CONSTRAINT table_name_Constraint{name} ON ({alias}:{name})\n" + f"ASSERT {alias}.table_name IS UNIQUE;\n" ) cypher.append(query_constraint) cypher = " ".join(cypher) - with open(file=f"{dir_file}/query_constraints.cypher") as f: + with open(file=f"{dir_file}/query_constraints.cypher", mode="w") as f: f.write(cypher) + return cypher -def create_query_node_import(datasets: list, dir_file: str): + +def create_query_node_import(datasets: list, dir_file: str) -> str: """ Write query to create nodes in Cypher and export as .cypher file. :param datasets: List of datasets/schema to create nodes from. :param dir_file: String of the directory to store Cypher query in. - :return: + :return: String of the queries in the script exported. """ cypher = [] for name in datasets: query_nodes = ( - f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{name}_tables.csv" AS csvLine ' - f"CREATE (:Reporting {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}});" - ) # noqa: E501 + f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{name.lower()}_tables.csv" AS csvLine\n' + f"CREATE (:{name} {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}});\n" # noqa: E501 + ) cypher.append(query_nodes) cypher = " ".join(cypher) - with open(file=f"{dir_file}/query_nodes.cypher") as f: + with open(file=f"{dir_file}/query_nodes.cypher", mode="w") as f: f.write(cypher) + + return cypher From b72405328511d6feb3324577175c6f40d3e76852 Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 22:09:35 +0100 Subject: [PATCH 08/19] feat: Write dynamic Cypher for relationships This is so we can then read these scripts and create in Cypher shell. --- tests/fixtures/fixture_writer.py | 38 ++++++++++++++++++++++++++++---- tests/unit/test_unit_writer.py | 5 +++++ writer.py | 32 +++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/tests/fixtures/fixture_writer.py b/tests/fixtures/fixture_writer.py index c8f1297..aa63bbc 100644 --- a/tests/fixtures/fixture_writer.py +++ b/tests/fixtures/fixture_writer.py @@ -16,9 +16,9 @@ def query_constraint(): return ( "CREATE CONSTRAINT table_name_ConstraintReporting ON (r:Reporting)\n" "ASSERT r.table_name IS UNIQUE;\n" - " CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics)\n" + "CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics)\n" "ASSERT a.table_name IS UNIQUE;\n" - " CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos)\n" + "CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos)\n" "ASSERT g.table_name IS UNIQUE;\n" ) @@ -28,8 +28,38 @@ def query_node_import(): return ( 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine\n' "CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" - ' USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine\n' + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine\n' "CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" - ' USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_tables.csv" AS csvLine\n' + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_tables.csv" AS csvLine\n' "CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" ) + + +@pytest.fixture() +def query_rel(): + return ( + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine\n' + "MERGE (reporting:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (analytics:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (reporting)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(analytics);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_githubrepos_dependency.csv" AS csvLine\n' + "MERGE (reporting:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (reporting)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(githubrepos);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_reporting_dependency.csv" AS csvLine\n' + "MERGE (analytics:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (reporting:Reporting {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (analytics)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(reporting);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_githubrepos_dependency.csv" AS csvLine\n' + "MERGE (analytics:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (analytics)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(githubrepos);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_reporting_dependency.csv" AS csvLine\n' + "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (reporting:Reporting {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (githubrepos)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(reporting);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_analytics_dependency.csv" AS csvLine\n' + "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (analytics:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (githubrepos)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(analytics);\n" + ) diff --git a/tests/unit/test_unit_writer.py b/tests/unit/test_unit_writer.py index 617fe55..fd3392e 100644 --- a/tests/unit/test_unit_writer.py +++ b/tests/unit/test_unit_writer.py @@ -9,3 +9,8 @@ def test_create_query_constraint(datasets, dir_file, query_constraint): def test_create_query_node_import(datasets, dir_file, query_node_import): query = writer.create_query_node_import(datasets=datasets, dir_file=dir_file) assert query == query_node_import + + +def test_create_query_relationship(datasets, dir_file, query_rel): + query = writer.create_query_relationship(datasets=datasets, dir_file=dir_file) + assert query == query_rel diff --git a/writer.py b/writer.py index 7168659..5c2ade5 100644 --- a/writer.py +++ b/writer.py @@ -15,7 +15,7 @@ def create_query_constraint(datasets: list, dir_file: str) -> str: ) cypher.append(query_constraint) - cypher = " ".join(cypher) + cypher = "".join(cypher) with open(file=f"{dir_file}/query_constraints.cypher", mode="w") as f: f.write(cypher) @@ -37,8 +37,36 @@ def create_query_node_import(datasets: list, dir_file: str) -> str: f"CREATE (:{name} {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}});\n" # noqa: E501 ) cypher.append(query_nodes) - cypher = " ".join(cypher) + cypher = "".join(cypher) with open(file=f"{dir_file}/query_nodes.cypher", mode="w") as f: f.write(cypher) return cypher + + +def create_query_relationship(datasets: list, dir_file: str) -> str: + """ + + :param datasets: + :param dir_file: + :return: + """ + aliases = [txt.lower() for txt in datasets] + cypher = [] + for name, alias in zip(datasets, aliases): + for sub_name, sub_alias in zip(datasets, aliases): + if name == sub_name: + continue + else: + query_rel = ( + f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{alias}_{sub_alias}_dependency.csv" AS csvLine\n' # noqa: E501 + f"MERGE ({alias}:{name} {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}})\n" # noqa: E501 + f"MERGE ({sub_alias}:{sub_name} {{table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}})\n" # noqa: E501 + f"CREATE ({alias})-[:HAS_TABLE_DEPENDENCY {{import_datetime: datetime()}}]->({sub_alias});\n" + ) + cypher.append(query_rel) + cypher = "".join(cypher) + with open(file=f"{dir_file}/query_relationships.cypher", mode="w") as f: + f.write(cypher) + + return cypher From 2f79e0b48f313036337b1913f1f7d8573b20d606 Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 22:51:36 +0100 Subject: [PATCH 09/19] feat: Check for existence of file This is so we do not create unecessary import of files that do not exist in the Cypher code. Also simplify the aliasing to deal with cases where have two datasets that start with the same letter. --- tests/fixtures/fixture_writer.py | 46 +++++++++++++------------------- writer.py | 14 ++++++---- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/tests/fixtures/fixture_writer.py b/tests/fixtures/fixture_writer.py index aa63bbc..8837004 100644 --- a/tests/fixtures/fixture_writer.py +++ b/tests/fixtures/fixture_writer.py @@ -3,7 +3,7 @@ @pytest.fixture() def datasets(): - return ["Reporting", "Analytics", "GithubRepos"] + return ["Reporting", "Analytics", "GitHub_Repos"] @pytest.fixture() @@ -18,7 +18,7 @@ def query_constraint(): "ASSERT r.table_name IS UNIQUE;\n" "CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics)\n" "ASSERT a.table_name IS UNIQUE;\n" - "CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos)\n" + "CREATE CONSTRAINT table_name_ConstraintGitHub_Repos ON (g:GitHub_Repos)\n" "ASSERT g.table_name IS UNIQUE;\n" ) @@ -30,8 +30,8 @@ def query_node_import(): "CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine\n' "CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_tables.csv" AS csvLine\n' - "CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///github_repos_tables.csv" AS csvLine\n' + "CREATE (:GitHub_Repos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()});\n" ) @@ -39,27 +39,19 @@ def query_node_import(): def query_rel(): return ( 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine\n' - "MERGE (reporting:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (analytics:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (reporting)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(analytics);\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_githubrepos_dependency.csv" AS csvLine\n' - "MERGE (reporting:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (reporting)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(githubrepos);\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_reporting_dependency.csv" AS csvLine\n' - "MERGE (analytics:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (reporting:Reporting {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (analytics)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(reporting);\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_githubrepos_dependency.csv" AS csvLine\n' - "MERGE (analytics:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (analytics)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(githubrepos);\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_reporting_dependency.csv" AS csvLine\n' - "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (reporting:Reporting {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (githubrepos)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(reporting);\n" - 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///githubrepos_analytics_dependency.csv" AS csvLine\n' - "MERGE (githubrepos:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" - "MERGE (analytics:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" - "CREATE (githubrepos)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(analytics);\n" + "MERGE (a:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (b:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(b);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine\n' + "MERGE (a:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (b:GitHub_Repos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(b);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine\n' + "MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (b:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(b);\n" + 'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine\n' + "MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)})\n" + "MERGE (b:GitHub_Repos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)})\n" + "CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(b);\n" ) diff --git a/writer.py b/writer.py index 5c2ade5..5c7f770 100644 --- a/writer.py +++ b/writer.py @@ -1,3 +1,6 @@ +import os + + def create_query_constraint(datasets: list, dir_file: str) -> str: """ Write query to create constraints in Cypher and export as .cypher file. @@ -55,14 +58,15 @@ def create_query_relationship(datasets: list, dir_file: str) -> str: cypher = [] for name, alias in zip(datasets, aliases): for sub_name, sub_alias in zip(datasets, aliases): - if name == sub_name: + file_name = f"{alias}_{sub_alias}_dependency.csv" + if file_name not in os.listdir(path=dir_file): continue else: query_rel = ( - f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{alias}_{sub_alias}_dependency.csv" AS csvLine\n' # noqa: E501 - f"MERGE ({alias}:{name} {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}})\n" # noqa: E501 - f"MERGE ({sub_alias}:{sub_name} {{table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}})\n" # noqa: E501 - f"CREATE ({alias})-[:HAS_TABLE_DEPENDENCY {{import_datetime: datetime()}}]->({sub_alias});\n" + f'USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///{file_name}" AS csvLine\n' # noqa: E501 + f"MERGE (a:{name} {{table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}})\n" # noqa: E501 + f"MERGE (b:{sub_name} {{table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}})\n" # noqa: E501 + f"CREATE (a)-[:HAS_TABLE_DEPENDENCY {{import_datetime: datetime()}}]->(b);\n" ) cypher.append(query_rel) cypher = "".join(cypher) From 20faddbaa2891702fdb4da5e4fe088468def4af7 Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 23:06:49 +0100 Subject: [PATCH 10/19] feat: Add writing of Cypher to main module This is so the Cypher scripts are also passed into the Docker container. --- sqlquerygraph.py | 13 +++++++++++++ writer.py | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/sqlquerygraph.py b/sqlquerygraph.py index 7755888..6e2e8b1 100644 --- a/sqlquerygraph.py +++ b/sqlquerygraph.py @@ -3,6 +3,7 @@ from extractor import Extractor import exporter +import writer import numpy as np import pandas as pd @@ -92,3 +93,15 @@ print("Exporting table dependencies for relationships\n") print("*******************************************\n") exporter.export_table_dependency(data=df, path_or_buf=args.export_dir) + + print("Creating Cypher queries for neo4j database\n") + print("*******************************************\n") + writer.create_query_constraint( + datasets=args.reference_datasets, dir_file=args.export_dir + ) + writer.create_query_node_import( + datasets=args.reference_datasets, dir_file=args.export_dir + ) + writer.create_query_relationship( + datasets=args.reference_datasets, dir_file=args.export_dir + ) diff --git a/writer.py b/writer.py index 5c7f770..5f8748c 100644 --- a/writer.py +++ b/writer.py @@ -49,9 +49,10 @@ def create_query_node_import(datasets: list, dir_file: str) -> str: def create_query_relationship(datasets: list, dir_file: str) -> str: """ + Write query to create relationship between nodes. - :param datasets: - :param dir_file: + :param datasets: List of datasets/schema to create nodes from. + :param dir_file: String of the directory to store Cypher query in. :return: """ aliases = [txt.lower() for txt in datasets] From b1fe598c5607b20269c1915e19382f92995cd44e Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 23:20:05 +0100 Subject: [PATCH 11/19] style: Title-case datasets This is to adhere to neo4j node naming standards. --- neo4j/example_import.cypher | 2 +- sqlquerygraph.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/neo4j/example_import.cypher b/neo4j/example_import.cypher index 58e98ea..526b593 100644 --- a/neo4j/example_import.cypher +++ b/neo4j/example_import.cypher @@ -47,4 +47,4 @@ DELETE a; call db.constraints DROP CONSTRAINT table_name_ConstraintReporting; DROP CONSTRAINT table_name_ConstraintAnalytics; -DROP CONSTRAINT table_name_ConstraintGithubRepos; +DROP CONSTRAINT table_name_ConstraintGithub_Repos; diff --git a/sqlquerygraph.py b/sqlquerygraph.py index 6e2e8b1..7a2996a 100644 --- a/sqlquerygraph.py +++ b/sqlquerygraph.py @@ -96,12 +96,7 @@ print("Creating Cypher queries for neo4j database\n") print("*******************************************\n") - writer.create_query_constraint( - datasets=args.reference_datasets, dir_file=args.export_dir - ) - writer.create_query_node_import( - datasets=args.reference_datasets, dir_file=args.export_dir - ) - writer.create_query_relationship( - datasets=args.reference_datasets, dir_file=args.export_dir - ) + datasets = [txt.title() for txt in args.reference_datasets] + writer.create_query_constraint(datasets=datasets, dir_file=args.export_dir) + writer.create_query_node_import(datasets=datasets, dir_file=args.export_dir) + writer.create_query_relationship(datasets=datasets, dir_file=args.export_dir) From 55755eba3196c0d38fde729d757e13b8ae826679 Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 23:20:31 +0100 Subject: [PATCH 12/19] docs: Add Cypher to example This is so others can reproduce the graph. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index a3de828..ce3b9ea 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ Parse your SQL queries and represent their structure as a graph. Currently, we implement the ability of representing how each of the tables in a set of SQL query scripts depend on each other. +```cypher +MATCH p=(r:Reporting)-[:HAS_TABLE_DEPENDENCY]->()-[:HAS_TABLE_DEPENDENCY]->() +WHERE r.table_name='user_activity' +RETURN p +``` ![](./guide/img/table_dependency.png) ## Requirements From 7ba270244302b51949a08fba521e433adbc8b3a0 Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 23:29:01 +0100 Subject: [PATCH 13/19] fix: Add empty .csv files This is so the tests pass. The idea is tha the user generates the example .csv file through following the README.md. --- neo4j/analytics_analytics_dependency.csv | 1 + neo4j/analytics_github_repos_dependency.csv | 1 + neo4j/analytics_tables.csv | 1 + neo4j/github_repos_tables.csv | 1 + neo4j/reporting_analytics_dependency.csv | 1 + neo4j/reporting_github_repos_dependency.csv | 1 + neo4j/reporting_tables.csv | 1 + 7 files changed, 7 insertions(+) create mode 100644 neo4j/analytics_analytics_dependency.csv create mode 100644 neo4j/analytics_github_repos_dependency.csv create mode 100644 neo4j/analytics_tables.csv create mode 100644 neo4j/github_repos_tables.csv create mode 100644 neo4j/reporting_analytics_dependency.csv create mode 100644 neo4j/reporting_github_repos_dependency.csv create mode 100644 neo4j/reporting_tables.csv diff --git a/neo4j/analytics_analytics_dependency.csv b/neo4j/analytics_analytics_dependency.csv new file mode 100644 index 0000000..fa0f231 --- /dev/null +++ b/neo4j/analytics_analytics_dependency.csv @@ -0,0 +1 @@ +table_dataset,table_name,dependency_dataset,dependency_name diff --git a/neo4j/analytics_github_repos_dependency.csv b/neo4j/analytics_github_repos_dependency.csv new file mode 100644 index 0000000..fa0f231 --- /dev/null +++ b/neo4j/analytics_github_repos_dependency.csv @@ -0,0 +1 @@ +table_dataset,table_name,dependency_dataset,dependency_name diff --git a/neo4j/analytics_tables.csv b/neo4j/analytics_tables.csv new file mode 100644 index 0000000..0c6c848 --- /dev/null +++ b/neo4j/analytics_tables.csv @@ -0,0 +1 @@ +table_dataset,table_name diff --git a/neo4j/github_repos_tables.csv b/neo4j/github_repos_tables.csv new file mode 100644 index 0000000..0c6c848 --- /dev/null +++ b/neo4j/github_repos_tables.csv @@ -0,0 +1 @@ +table_dataset,table_name diff --git a/neo4j/reporting_analytics_dependency.csv b/neo4j/reporting_analytics_dependency.csv new file mode 100644 index 0000000..fa0f231 --- /dev/null +++ b/neo4j/reporting_analytics_dependency.csv @@ -0,0 +1 @@ +table_dataset,table_name,dependency_dataset,dependency_name diff --git a/neo4j/reporting_github_repos_dependency.csv b/neo4j/reporting_github_repos_dependency.csv new file mode 100644 index 0000000..fa0f231 --- /dev/null +++ b/neo4j/reporting_github_repos_dependency.csv @@ -0,0 +1 @@ +table_dataset,table_name,dependency_dataset,dependency_name diff --git a/neo4j/reporting_tables.csv b/neo4j/reporting_tables.csv new file mode 100644 index 0000000..0c6c848 --- /dev/null +++ b/neo4j/reporting_tables.csv @@ -0,0 +1 @@ +table_dataset,table_name From d80ef11ac355c85e70ba3dd8471f76abfe73135d Mon Sep 17 00:00:00 2001 From: avisionh Date: Mon, 31 May 2021 23:57:25 +0100 Subject: [PATCH 14/19] chore: Run shell script in docker container terminal This is so we can automate import of data. --- docker-compose.yml | 6 ++++++ neo4j/example_import.cypher | 16 ++++++++-------- neo4j/loader.sh | 14 +++----------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 0986daa..32d6e2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,3 +22,9 @@ services: - NEO4J_dbms_memory_pagecache_size=2G - NEO4J_dbms.memory.heap.initial_size=2G - NEO4J_dbms_memory_heap_max__size=2G + import: + build: . + command: > + sh -c "data/loader.sh" + depends_on: + - neo4j diff --git a/neo4j/example_import.cypher b/neo4j/example_import.cypher index 526b593..66da787 100644 --- a/neo4j/example_import.cypher +++ b/neo4j/example_import.cypher @@ -3,34 +3,34 @@ CREATE CONSTRAINT table_name_ConstraintReporting ON (r:Reporting) ASSERT r.table_name IS UNIQUE; CREATE CONSTRAINT table_name_ConstraintAnalytics ON (a:Analytics) ASSERT a.table_name IS UNIQUE; -CREATE CONSTRAINT table_name_ConstraintGithubRepos ON (g:GithubRepos) +CREATE CONSTRAINT table_name_ConstraintGithub_Repos ON (g:Github_Repos) ASSERT g.table_name IS UNIQUE; // Create table nodes to join later -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_tables.csv" AS csvLine CREATE (:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_tables.csv" AS csvLine CREATE (:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///github_repos_tables.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///github_repos_tables.csv" AS csvLine CREATE (:GithubRepos {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset), import_datetime: datetime()}); // Load table dependency data -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_analytics_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a); -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///reporting_github_repos_dependency.csv" AS csvLine MERGE (r:Reporting {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (r)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_analytics_dependency.csv" AS csvLine MERGE (a1:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (a2:Analytics {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a1)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(a2); -:auto USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine +USING PERIODIC COMMIT 500 LOAD CSV WITH HEADERS FROM "file:///analytics_github_repos_dependency.csv" AS csvLine MERGE (a:Analytics {table_name: toString(csvLine.table_name), table_dataset: toString(csvLine.table_dataset)}) MERGE (g:GithubRepos {table_name: toString(csvLine.dependency_name), table_dataset: toString(csvLine.dependency_dataset)}) CREATE (a)-[:HAS_TABLE_DEPENDENCY {import_datetime: datetime()}]->(g); diff --git a/neo4j/loader.sh b/neo4j/loader.sh index 37f39e0..a4d0dfd 100644 --- a/neo4j/loader.sh +++ b/neo4j/loader.sh @@ -1,16 +1,8 @@ # enter docker neo4j instance docker exec -it neo4j bash -# move .csv files into import/ folder +# move .csv files into import/ folder for loading into neo4j mv data/*csv import/ -# access cypher shell -cypher-shell --username $NEO4J_USERNAME --pasword $NEO4J_PASSWORD - -# use default neo4j database -:use neo4j - -# run cypher code - -# exit cypher shell -:exit +# pipe content of .cypher file into cypher-shell +cat data/example_import.cypher | bin/cypher-shell --username $NEO4J_USERNAME --password $NEO4J_PASSWORD --format plain From d876abd08180b90e31295d6179b61065b806bd9d Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 2 Jun 2021 11:01:18 +0100 Subject: [PATCH 15/19] feat: Write Python neo4j loader module This is to run queries in the database. --- loader.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 loader.py diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..ad19ca0 --- /dev/null +++ b/loader.py @@ -0,0 +1,29 @@ +import os +import argparse + +from py2neo import Graph + +NEO4J_AUTH = (os.getenv(key="NEO4J_USERNAME"), os.getenv(key="NEO4J_PASSWORD")) + +g = Graph(auth=NEO4J_AUTH, host="localhost", port=7687, scheme="bolt") + + +if __name__ == """__main__""": + argp = argparse.ArgumentParser() + argp.add_argument("-f", "--file", type=str, help="Path for where Cypher query is.") + args = argp.parse_args() + + print(f"Reading {args.file}\n") + print("*******************************************\n") + with open(file=args.file, mode="r") as f: + queries = f.read() + + print(f"Formatting {args.file} for importing into neo4j\n") + print("*******************************************\n") + queries = queries.split(sep=";") + queries = [txt for txt in queries if txt != "\n"] + + print(f"Executing {args.file} in neo4j\n") + print("*******************************************\n") + for query in queries: + g.evaluate(cypher=query) From 0fc9af3e2b6a8b49e0c76fa5ce3d4f4139afe7a6 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 2 Jun 2021 17:22:04 +0100 Subject: [PATCH 16/19] chore: Create shared volume/directory across containers This is so we can have a separate container handling the importing of data into the database. --- README.md | 2 +- docker-compose.yml | 31 +++- loader.py => neo4j/loader.py | 0 poetry.lock | 289 ++++++++++++++++++++++++++++++++++- pyproject.toml | 2 + 5 files changed, 314 insertions(+), 10 deletions(-) rename loader.py => neo4j/loader.py (100%) diff --git a/README.md b/README.md index ce3b9ea..575fffa 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ python sqlquerygraph.py -sd 'sql' -ed 'neo4j' -rd 'github_repos' 'analytics' 're ### Run neo4j graph database We use [neo4j](https://neo4j.com/) for this project to visualise the dependencies between tables. To install neo4j locally using Docker Compose, follow the below instructions: -1. Install Docker +1. Install and open Docker + For Mac OSX, install Docker and Docker Compose together [here](https://docs.docker.com/docker-for-mac/install/). + For Linux, install Docker [here](https://docs.docker.com/engine/install/) and then follow these [instructions](https://docs.docker.com/compose/install/) to install docker-compose. + For Windows, install Docker and Docker Compose together [here](https://docs.docker.com/docker-for-windows/install/). diff --git a/docker-compose.yml b/docker-compose.yml index 32d6e2a..ca9d507 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ # https://thibaut-deveraux.medium.com/how-to-install-neo4j-with-docker-compose-36e3ba939af0 -version: '3.9' +version: '3.8' services: neo4j: @@ -12,19 +12,34 @@ services: - 7474:7474 - 7687:7687 volumes: - # cannot move files to import/ folder in neo4j because it's read-only - # https://neo4j.com/docs/operations-manual/current/configuration/file-locations/ - # but can move from docker neo4j bash terminal - - ./neo4j:/data + - type: volume + source: neo4j + target: /data environment: - NEO4j_dbms.security.auth_enabled='true' # Raise memory limits - NEO4J_dbms_memory_pagecache_size=2G - NEO4J_dbms.memory.heap.initial_size=2G - NEO4J_dbms_memory_heap_max__size=2G - import: - build: . + + dataloader: + image: python:3.8-buster + container_name: dataloader + env_file: .env + volumes: + - type: volume + source: neo4j + target: /data command: > - sh -c "data/loader.sh" + sh -c "ls data && + ls var/lib + mv data/*csv var/lib/neo4j/import/ && + poetry install && + python -m data/loader.py --file 'data/query_constraints.cypher'" + links: + - neo4j depends_on: - neo4j + +volumes: + neo4j: diff --git a/loader.py b/neo4j/loader.py similarity index 100% rename from loader.py rename to neo4j/loader.py diff --git a/poetry.lock b/poetry.lock index d01fee2..4e1e091 100644 --- a/poetry.lock +++ b/poetry.lock @@ -36,6 +36,17 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "cffi" +version = "1.14.5" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.2.0" @@ -74,6 +85,25 @@ toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] +[[package]] +name = "cryptography" +version = "3.4.7" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "detect-secrets" version = "1.1.0" @@ -109,6 +139,34 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "docker" +version = "5.0.0" +description = "A Python library for the Docker Engine API." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pywin32 = {version = "227", markers = "sys_platform == \"win32\""} +requests = ">=2.14.2,<2.18.0 || >2.18.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.2)"] +tls = ["pyOpenSSL (>=17.5.0)", "cryptography (>=3.4.7)", "idna (>=2.0.0)"] + +[[package]] +name = "english" +version = "2020.7.0" +description = "English language utility library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + [[package]] name = "env-file" version = "2020.12.3" @@ -233,6 +291,14 @@ mo-future = "3.147.20327" mo-imports = "3.149.20327" mo-kwargs = "4.22.21108" +[[package]] +name = "monotonic" +version = "1.6" +description = "An implementation of time.monotonic() for Python 2 & < 3.3" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "moz-sql-parser" version = "4.40.21126" @@ -246,6 +312,18 @@ mo-dots = "4.22.21108" mo-future = "3.147.20327" mo-logs = "4.23.21108" +[[package]] +name = "neotime" +version = "1.7.4" +description = "Nanosecond resolution temporal types" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytz = "*" +six = "*" + [[package]] name = "nodeenv" version = "1.6.0" @@ -289,6 +367,17 @@ pytz = ">=2017.3" [package.extras] test = ["pytest (>=5.0.1)", "pytest-xdist", "hypothesis (>=3.58)"] +[[package]] +name = "pansi" +version = "2020.7.3" +description = "ANSI escape code library for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + [[package]] name = "pluggy" version = "0.13.1" @@ -316,6 +405,17 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +name = "prompt-toolkit" +version = "3.0.18" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "py" version = "1.10.0" @@ -324,6 +424,29 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "py2neo" +version = "2021.1.3" +description = "Python client library and toolkit for Neo4j" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +cryptography = "*" +docker = "*" +english = "*" +monotonic = "*" +neotime = ">=1.7.4,<1.8.0" +packaging = "*" +pansi = ">=2020.7.3" +prompt-toolkit = {version = ">=2.0.7", markers = "python_version >= \"3.6\""} +pygments = ">=2.0.0" +pytz = "*" +six = ">=1.15.0" +urllib3 = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -332,6 +455,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pyflakes" version = "2.3.1" @@ -340,6 +471,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pygments" +version = "2.9.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "pyparsing" version = "2.4.7" @@ -395,6 +534,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "0.17.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pytz" version = "2021.1" @@ -403,6 +553,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32" +version = "227" +description = "Python for Window Extensions" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "5.4.1" @@ -497,10 +655,26 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "websocket-client" +version = "1.0.1" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = false +python-versions = ">=3.6" + [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "ea5d17f878a09644379c9d5f63d65bd25e7d0ef87af420755e3c04d82d5c34c8" +content-hash = "478b152e227629bd4611beba39245317f09b3fd7646433f8ca466cf630f43a17" [metadata.files] appdirs = [ @@ -519,6 +693,45 @@ certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, ] +cffi = [ + {file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"}, + {file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"}, + {file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"}, + {file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"}, + {file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"}, + {file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"}, + {file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"}, + {file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"}, + {file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"}, + {file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"}, + {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"}, + {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"}, + {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"}, + {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"}, + {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"}, + {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"}, + {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"}, + {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"}, + {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"}, + {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"}, + {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"}, + {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"}, + {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"}, + {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"}, + {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, + {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, +] cfgv = [ {file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"}, {file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"}, @@ -585,6 +798,20 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] +cryptography = [ + {file = "cryptography-3.4.7-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1"}, + {file = "cryptography-3.4.7-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6"}, + {file = "cryptography-3.4.7-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959"}, + {file = "cryptography-3.4.7-cp36-abi3-win32.whl", hash = "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d"}, + {file = "cryptography-3.4.7-cp36-abi3-win_amd64.whl", hash = "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873"}, + {file = "cryptography-3.4.7-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177"}, + {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, + {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, +] detect-secrets = [ {file = "detect_secrets-1.1.0-py2.py3-none-any.whl", hash = "sha256:be8cca3dc65f6fd637f5dec9f583f1cf4a680dc1a580b3d2e65a5ac7a277456a"}, {file = "detect_secrets-1.1.0.tar.gz", hash = "sha256:68250b31bc108f665f05f0ecfb34f92423280e48e65adbb887fdf721ed909627"}, @@ -596,6 +823,14 @@ distlib = [ {file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"}, {file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"}, ] +docker = [ + {file = "docker-5.0.0-py2.py3-none-any.whl", hash = "sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd"}, + {file = "docker-5.0.0.tar.gz", hash = "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5"}, +] +english = [ + {file = "english-2020.7.0-py2.py3-none-any.whl", hash = "sha256:aeeaea58698bf703336cf63279d6709482909e2fc1d5da4540abae878ab1e292"}, + {file = "english-2020.7.0.tar.gz", hash = "sha256:7105ed1e9d22b0bd9c1841e7275d3e6e83a34cee475a7e291f70a05721732080"}, +] env-file = [ {file = "env-file-2020.12.3.tar.gz", hash = "sha256:34cbe53b99afaa81209953ee16febcd87121034aa7bf64e229802f51b9c38d66"}, ] @@ -638,9 +873,15 @@ mo-kwargs = [ mo-logs = [ {file = "mo-logs-4.23.21108.tar.gz", hash = "sha256:de4136a7ce215ecbfd7a368588be0a3f1fd8a6521dc2d4aae57cc1c3ba299aab"}, ] +monotonic = [ + {file = "monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c"}, +] moz-sql-parser = [ {file = "moz-sql-parser-4.40.21126.tar.gz", hash = "sha256:b3d37cc8ff118d86009aa12646791549537ec0ae8ac312efd4641289c8eee080"}, ] +neotime = [ + {file = "neotime-1.7.4.tar.gz", hash = "sha256:4e0477ba0f24e004de2fa79a3236de2bd941f20de0b5db8d976c52a86d7363eb"}, +] nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, @@ -693,6 +934,10 @@ pandas = [ {file = "pandas-1.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:2b063d41803b6a19703b845609c0b700913593de067b552a8b24dd8eeb8c9895"}, {file = "pandas-1.2.4.tar.gz", hash = "sha256:649ecab692fade3cbfcf967ff936496b0cfba0af00a55dfaacd82bdda5cb2279"}, ] +pansi = [ + {file = "pansi-2020.7.3-py2.py3-none-any.whl", hash = "sha256:ce2b8acaf06dc59dcc711f61efbe53c836877f127d73f11fdd898b994e5c4234"}, + {file = "pansi-2020.7.3.tar.gz", hash = "sha256:bd182d504528f870601acb0282aded411ad00a0148427b0e53a12162f4e74dcf"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, @@ -701,18 +946,34 @@ pre-commit = [ {file = "pre_commit-2.12.1-py2.py3-none-any.whl", hash = "sha256:70c5ec1f30406250b706eda35e868b87e3e4ba099af8787e3e8b4b01e84f4712"}, {file = "pre_commit-2.12.1.tar.gz", hash = "sha256:900d3c7e1bf4cf0374bb2893c24c23304952181405b4d88c9c40b72bda1bb8a9"}, ] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.18-py3-none-any.whl", hash = "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04"}, + {file = "prompt_toolkit-3.0.18.tar.gz", hash = "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"}, +] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +py2neo = [ + {file = "py2neo-2021.1.3-py2.py3-none-any.whl", hash = "sha256:5766710590457e9489a2dc2a802a9dfd431cc3d08323f7e3e64113db9ec6b1dc"}, + {file = "py2neo-2021.1.3.tar.gz", hash = "sha256:4a2aa4e8df9a5dee46a83e05aa1b8877385cff094486ebd9f49a22701fd4c5d6"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] +pygments = [ + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, +] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -729,10 +990,28 @@ python-dateutil = [ {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, ] +python-dotenv = [ + {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, + {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, +] pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pywin32 = [ + {file = "pywin32-227-cp27-cp27m-win32.whl", hash = "sha256:371fcc39416d736401f0274dd64c2302728c9e034808e37381b5e1b22be4a6b0"}, + {file = "pywin32-227-cp27-cp27m-win_amd64.whl", hash = "sha256:4cdad3e84191194ea6d0dd1b1b9bdda574ff563177d2adf2b4efec2a244fa116"}, + {file = "pywin32-227-cp35-cp35m-win32.whl", hash = "sha256:f4c5be1a293bae0076d93c88f37ee8da68136744588bc5e2be2f299a34ceb7aa"}, + {file = "pywin32-227-cp35-cp35m-win_amd64.whl", hash = "sha256:a929a4af626e530383a579431b70e512e736e9588106715215bf685a3ea508d4"}, + {file = "pywin32-227-cp36-cp36m-win32.whl", hash = "sha256:300a2db938e98c3e7e2093e4491439e62287d0d493fe07cce110db070b54c0be"}, + {file = "pywin32-227-cp36-cp36m-win_amd64.whl", hash = "sha256:9b31e009564fb95db160f154e2aa195ed66bcc4c058ed72850d047141b36f3a2"}, + {file = "pywin32-227-cp37-cp37m-win32.whl", hash = "sha256:47a3c7551376a865dd8d095a98deba954a98f326c6fe3c72d8726ca6e6b15507"}, + {file = "pywin32-227-cp37-cp37m-win_amd64.whl", hash = "sha256:31f88a89139cb2adc40f8f0e65ee56a8c585f629974f9e07622ba80199057511"}, + {file = "pywin32-227-cp38-cp38-win32.whl", hash = "sha256:7f18199fbf29ca99dff10e1f09451582ae9e372a892ff03a28528a24d55875bc"}, + {file = "pywin32-227-cp38-cp38-win_amd64.whl", hash = "sha256:7c1ae32c489dc012930787f06244426f8356e129184a02c25aef163917ce158e"}, + {file = "pywin32-227-cp39-cp39-win32.whl", hash = "sha256:c054c52ba46e7eb6b7d7dfae4dbd987a1bb48ee86debe3f245a2884ece46e295"}, + {file = "pywin32-227-cp39-cp39-win_amd64.whl", hash = "sha256:f27cec5e7f588c3d1051651830ecc00294f90728d19c3bf6916e6dba93ea357c"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -791,3 +1070,11 @@ virtualenv = [ {file = "virtualenv-20.4.6-py2.py3-none-any.whl", hash = "sha256:307a555cf21e1550885c82120eccaf5acedf42978fd362d32ba8410f9593f543"}, {file = "virtualenv-20.4.6.tar.gz", hash = "sha256:72cf267afc04bf9c86ec932329b7e94db6a0331ae9847576daaa7ca3c86b29a4"}, ] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +websocket-client = [ + {file = "websocket-client-1.0.1.tar.gz", hash = "sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81"}, + {file = "websocket_client-1.0.1-py2.py3-none-any.whl", hash = "sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"}, +] diff --git a/pyproject.toml b/pyproject.toml index 4266e61..4fdbbc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,8 @@ pytest = "^6.2.4" pandas = "^1.2.4" numpy = "^1.20.3" pytest-cov = "^2.12.0" +py2neo = "^2021.1.3" +python-dotenv = "^0.17.1" [tool.poetry.dev-dependencies] From 039de63798a6299ba7eb610237f0f11bc4f7c001 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 2 Jun 2021 22:06:57 +0100 Subject: [PATCH 17/19] fix: Move data between containers in shared volume This is so we can apply the Python loader module on the Cypher scripts. --- .coveragerc | 1 + docker-compose.yml | 28 ++++++++++++++++------------ loader.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 loader.py diff --git a/.coveragerc b/.coveragerc index 14a2a42..d83c23f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -12,3 +12,4 @@ ignore_errors = True omit = tests/* sqlquerygraph.py + loader.py diff --git a/docker-compose.yml b/docker-compose.yml index ca9d507..f7c4ad5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,14 +9,15 @@ services: # pass .env file to container env_file: .env ports: - - 7474:7474 - - 7687:7687 + - 7474:7474 # web client + - 7687:7687 # db default port volumes: - - type: volume - source: neo4j - target: /data + - ./neo4j:/data + - ./import:/import environment: - NEO4j_dbms.security.auth_enabled='true' + # listen to incoming connections + - NEO4J_dbms.connector.bolt.listen_address=0.0.0.0:7687 # Raise memory limits - NEO4J_dbms_memory_pagecache_size=2G - NEO4J_dbms.memory.heap.initial_size=2G @@ -26,16 +27,19 @@ services: image: python:3.8-buster container_name: dataloader env_file: .env + ports: + - 5002:5002 volumes: - - type: volume - source: neo4j - target: /data + - ./neo4j:/data + - ./import:/import + - ./poetry.lock:/poetry.lock + - ./pyproject.toml:/pyproject.toml + - ./loader.py:/loader.py command: > - sh -c "ls data && - ls var/lib - mv data/*csv var/lib/neo4j/import/ && + sh -c "mv data/*csv import/ && + pip install poetry && poetry install && - python -m data/loader.py --file 'data/query_constraints.cypher'" + poetry run python -m loader.py --file 'data/query_constraints.cypher'" links: - neo4j depends_on: diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..ad19ca0 --- /dev/null +++ b/loader.py @@ -0,0 +1,29 @@ +import os +import argparse + +from py2neo import Graph + +NEO4J_AUTH = (os.getenv(key="NEO4J_USERNAME"), os.getenv(key="NEO4J_PASSWORD")) + +g = Graph(auth=NEO4J_AUTH, host="localhost", port=7687, scheme="bolt") + + +if __name__ == """__main__""": + argp = argparse.ArgumentParser() + argp.add_argument("-f", "--file", type=str, help="Path for where Cypher query is.") + args = argp.parse_args() + + print(f"Reading {args.file}\n") + print("*******************************************\n") + with open(file=args.file, mode="r") as f: + queries = f.read() + + print(f"Formatting {args.file} for importing into neo4j\n") + print("*******************************************\n") + queries = queries.split(sep=";") + queries = [txt for txt in queries if txt != "\n"] + + print(f"Executing {args.file} in neo4j\n") + print("*******************************************\n") + for query in queries: + g.evaluate(cypher=query) From d2d03eef441a909e7fbbe94ddad5bf6de9da3e95 Mon Sep 17 00:00:00 2001 From: avisionh Date: Wed, 2 Jun 2021 23:13:54 +0100 Subject: [PATCH 18/19] perf: Remove poetry install This is because we only need py2neo package for docker container. --- docker-compose.yml | 16 +++++++--------- neo4j/loader.py | 29 ----------------------------- neo4j/loader.sh | 8 -------- 3 files changed, 7 insertions(+), 46 deletions(-) delete mode 100644 neo4j/loader.py delete mode 100644 neo4j/loader.sh diff --git a/docker-compose.yml b/docker-compose.yml index f7c4ad5..d9871d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,23 +23,21 @@ services: - NEO4J_dbms.memory.heap.initial_size=2G - NEO4J_dbms_memory_heap_max__size=2G - dataloader: + app: image: python:3.8-buster - container_name: dataloader + container_name: app env_file: .env ports: - - 5002:5002 + - 8080:8080 volumes: - ./neo4j:/data - ./import:/import - - ./poetry.lock:/poetry.lock - - ./pyproject.toml:/pyproject.toml - ./loader.py:/loader.py command: > - sh -c "mv data/*csv import/ && - pip install poetry && - poetry install && - poetry run python -m loader.py --file 'data/query_constraints.cypher'" + sh -c "sleep 15 && + mv data/*csv import/ && + pip install neo4j-driver py2neo && + python -m loader.py --file 'data/query_constraints.cypher'" links: - neo4j depends_on: diff --git a/neo4j/loader.py b/neo4j/loader.py deleted file mode 100644 index ad19ca0..0000000 --- a/neo4j/loader.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import argparse - -from py2neo import Graph - -NEO4J_AUTH = (os.getenv(key="NEO4J_USERNAME"), os.getenv(key="NEO4J_PASSWORD")) - -g = Graph(auth=NEO4J_AUTH, host="localhost", port=7687, scheme="bolt") - - -if __name__ == """__main__""": - argp = argparse.ArgumentParser() - argp.add_argument("-f", "--file", type=str, help="Path for where Cypher query is.") - args = argp.parse_args() - - print(f"Reading {args.file}\n") - print("*******************************************\n") - with open(file=args.file, mode="r") as f: - queries = f.read() - - print(f"Formatting {args.file} for importing into neo4j\n") - print("*******************************************\n") - queries = queries.split(sep=";") - queries = [txt for txt in queries if txt != "\n"] - - print(f"Executing {args.file} in neo4j\n") - print("*******************************************\n") - for query in queries: - g.evaluate(cypher=query) diff --git a/neo4j/loader.sh b/neo4j/loader.sh deleted file mode 100644 index a4d0dfd..0000000 --- a/neo4j/loader.sh +++ /dev/null @@ -1,8 +0,0 @@ -# enter docker neo4j instance -docker exec -it neo4j bash - -# move .csv files into import/ folder for loading into neo4j -mv data/*csv import/ - -# pipe content of .cypher file into cypher-shell -cat data/example_import.cypher | bin/cypher-shell --username $NEO4J_USERNAME --password $NEO4J_PASSWORD --format plain From 690280465f7f8a5d0c7b6cc5ac5de77021567c56 Mon Sep 17 00:00:00 2001 From: avisionh Date: Fri, 4 Jun 2021 08:20:59 +0100 Subject: [PATCH 19/19] refactor: Remove call to Python container This is with the understanding that docker-compose is an orchestrator platform in the vein of kubernetes and that the stuff trying to be done with respect to calling the module will be better done in a Dockerfile. Currently Python module cannot connect to neo4j docker container because of hosting. Need to specify the Docker container's IP address in the module. --- README.md | 10 ++++++++++ docker-compose.yml | 25 +------------------------ poetry.lock | 1 + 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 575fffa..6fa94aa 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,16 @@ We use [neo4j](https://neo4j.com/) for this project to visualise the dependencie username: neo4j password: ``` +1. Load the data into the database through entering the following in a separate terminal: + ``` + docker exec -it neo4j bash + # move .csv files into neo4j's import/ directory + mv data/*csv import/ + ``` +1. In your local terminal: + ```shell script + python -m loader.py --file 'neo4j/ - sh -c "sleep 15 && - mv data/*csv import/ && - pip install neo4j-driver py2neo && - python -m loader.py --file 'data/query_constraints.cypher'" - links: - - neo4j - depends_on: - - neo4j - -volumes: - neo4j: diff --git a/poetry.lock b/poetry.lock index 4e1e091..f5354ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,6 +15,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] + name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate"