From 2bd0ae7a57d867eb7b0727b257cb0bf51f42e6c6 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 7 Jun 2024 13:38:40 -0400 Subject: [PATCH] Add a module for Data Quality Monitoring Job for a SageMaker Endpoint. --- CHANGELOG.md | 1 + README.md | 1 + .../sagemaker-model-monitoring/README.md | 95 +++++++ .../sagemaker-model-monitoring/app.py | 30 +++ .../deployspec.yaml | 25 ++ ...r-model-monitoring-module-architecture.png | Bin 0 -> 56756 bytes ...r-model-monitoring-module-architecture.xml | 1 + .../sagemaker-model-monitoring/pyproject.toml | 41 +++ .../requirements-dev.in | 12 + .../requirements-dev.txt | 233 ++++++++++++++++++ .../requirements.txt | 5 + .../sagemaker_model_monitoring/__init__.py | 0 .../data_quality_construct.py | 102 ++++++++ .../sagemaker_model_monitoring/settings.py | 87 +++++++ .../sagemaker_model_monitoring/stack.py | 149 +++++++++++ .../tests/__init__.py | 0 .../tests/test_app.py | 30 +++ .../tests/test_stack.py | 75 ++++++ 18 files changed, 887 insertions(+) create mode 100644 modules/sagemaker/sagemaker-model-monitoring/README.md create mode 100644 modules/sagemaker/sagemaker-model-monitoring/app.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/deployspec.yaml create mode 100644 modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.png create mode 100644 modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.xml create mode 100644 modules/sagemaker/sagemaker-model-monitoring/pyproject.toml create mode 100644 modules/sagemaker/sagemaker-model-monitoring/requirements-dev.in create mode 100644 modules/sagemaker/sagemaker-model-monitoring/requirements-dev.txt create mode 100644 modules/sagemaker/sagemaker-model-monitoring/requirements.txt create mode 100644 modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/__init__.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/data_quality_construct.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/settings.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/stack.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/tests/__init__.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/tests/test_app.py create mode 100644 modules/sagemaker/sagemaker-model-monitoring/tests/test_stack.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 706db2ca..21af9d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### **Added** +- Added a `sagemaker-model-monitoring-module` module with an example of data quality monitoring of a SageMaker Endpoint. - Added an option to enable data capture in the `sagemaker-endpoint-module`. ### **Changed** diff --git a/README.md b/README.md index 839eb779..5bb3444f 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ See deployment steps in the [Deployment Guide](DEPLOYMENT.md). | [SageMaker Custom Kernel Module](modules/sagemaker/sagemaker-custom-kernel/README.md) | Builds custom kernel for SageMaker Studio from a Dockerfile | | [SageMaker Model Package Group Module](modules/sagemaker/sagemaker-model-package-group/README.md) | Creates a SageMaker Model Package Group to register and version SageMaker Machine Learning (ML) models and setups an Amazon EventBridge Rule to send model package group state change events to an Amazon EventBridge Bus | | [SageMaker Model Package Promote Pipeline Module](modules/sagemaker/sagemaker-model-package-promote-pipeline/README.md) | Deploy a Pipeline to promote SageMaker Model Packages in a multi-account setup. The pipeline can be triggered through an EventBridge rule in reaction of a SageMaker Model Package Group state event change (Approved/Rejected). Once the pipeline is triggered, it will promote the latest approved model package, if one is found. | +| [SageMaker Model Monitoring Module](modules/sagemaker/sagemaker-model-monitoring-module/README.md) | Deploy a data quality monitoring job which runs against a SageMaker Endpoint. | ### Mlflow Modules diff --git a/modules/sagemaker/sagemaker-model-monitoring/README.md b/modules/sagemaker/sagemaker-model-monitoring/README.md new file mode 100644 index 00000000..69d34d41 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/README.md @@ -0,0 +1,95 @@ +# SageMaker Model Monitoring + +## Description + +This module creates a SageMaker Model Monitoring job for data quality. +It requires a deployed model endpoint and the proper check steps +for each monitoring job: + +* Data Quality: [QualityCheck step](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#step-type-quality-check) + +### Architecture + +![SageMaker Model Monitoring Module Architecture](docs/_static/sagemaker-model-monitoring-module-architecture.png "SageMaker Model Monitoring Module Architecture") + +## Inputs/Outputs + +### Input Parameters + +#### Required + +- `endpoint-name`: The name of the endpoint used to run the monitoring job. +- `security-group-id`: The VPC security group IDs, should provide access to the given `subnet-ids`. +- `subnet-ids`: The ID of the subnets in the VPC to which you want to connect your training job or model. +- `model-package-arn`: Model package ARN +- `model-bucket-arn`: S3 bucket ARN for model artifacts +- `kms-key-id`: The KMS key used to encrypted storage and output. +- `data-quality-checkstep-output-prefix`: The S3 prefix in `model-artifacts-bucket-arn` which contains the output from the corresponding [Check step in the SageMaker Pipeline](https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-steps.html#build-and-manage-steps-types). +- `data-quality-output-prefix`: The S3 prefix in `model-artifacts-bucket-arn` to contain the output of the monitoring job. + + +#### Optional + +- `sagemaker-project-id`: SageMaker project id +- `sagemaker-project-name`: SageMaker project name +- `data-quality-instance-count`: The number of ML compute instances to use in the model monitoring job. +- `data-quality-instance-type`: The ML compute instance type for the processing job. +- `data-quality-instance-volume-size-in-gb`: The size of the ML storage volume, in gigabytes, that you want to provision. +- `data-quality-max-runtime-in-seconds`: The maximum length of time, in seconds, the monitoring job can run before it is stopped. +- `data-quality-schedule-expression`: A cron expression that describes details about the monitoring schedule. + +### Sample manifest declaration + +```yaml +name: monitoring +path: modules/sagemaker/sagemaker-model-monitoring +parameters: + - name: sagemaker_project_id + value: dummy123 + - name: sagemaker_project_name + value: dummy123 + - name: model_package_arn + value: arn:aws:sagemaker:::model-package//1 + - name: model_bucket_arn + value: arn:aws:s3::: + - name: data-quality-checkstep-output-prefix + value: model-training-run-1234/dataqualitycheckstep + - name: data-quality-output-prefix + value: model-training-run-1234/monitor/dataqualityoutput + - name: endpoint_name + valueFrom: + moduleMetadata: + group: endpoints + name: endpoint + key: EndpointName + - name: security_group_id + valueFrom: + moduleMetadata: + group: endpoints + name: endpoint + key: SecurityGroupId + - name: kms_key_id + valueFrom: + moduleMetadata: + group: endpoints + name: endpoint + key: KmsKeyId + - name: subnet_ids + valueFrom: + moduleMetadata: + group: networking + name: networking + key: PrivateSubnetIds +``` + +### Module Metadata Outputs + +- `ModelExecutionRoleArn`: SageMaker Model Execution IAM role ARN +- `ModelName`: SageMaker Model name +- `ModelPackageArn`: SageMaker Model package ARN +- `EndpointName`: SageMaker Endpoint name +- `EndpointUrl`: SageMaker Endpoint Url + +#### Output Example + +N/A diff --git a/modules/sagemaker/sagemaker-model-monitoring/app.py b/modules/sagemaker/sagemaker-model-monitoring/app.py new file mode 100644 index 00000000..613559c9 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/app.py @@ -0,0 +1,30 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +import aws_cdk +import cdk_nag + +from sagemaker_model_monitoring.settings import ApplicationSettings +from sagemaker_model_monitoring.stack import SageMakerModelMonitoringStack + +# Load application settings from env vars. +app_settings = ApplicationSettings() + +environment = aws_cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], +) + +app = aws_cdk.App() +stack = SageMakerModelMonitoringStack( + scope=app, + id=app_settings.settings.app_prefix, + env=environment, + **app_settings.parameters.model_dump(), +) + +aws_cdk.Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(log_ignores=False)) + +app.synth() diff --git a/modules/sagemaker/sagemaker-model-monitoring/deployspec.yaml b/modules/sagemaker/sagemaker-model-monitoring/deployspec.yaml new file mode 100644 index 00000000..add0f851 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/deployspec.yaml @@ -0,0 +1,25 @@ +publishGenericEnvVariables: true +deploy: + phases: + install: + commands: + - env + # Install whatever additional build libraries + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + build: + commands: + - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json + # Export metadata + - seedfarmer metadata convert -f cdk-exports.json || true +destroy: + phases: + install: + commands: + # Install whatever additional build libraries + - npm install -g aws-cdk@2.126.0 + - pip install -r requirements.txt + build: + commands: + # execute the CDK + - cdk destroy --force --app "python app.py" diff --git a/modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.png b/modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..b436677802131a7c7d23e072db865e8a3f3b1e92 GIT binary patch literal 56756 zcmc$`Wl)^mwk?d)c;ilh;O_1kT!RMp;1b;32@*WGCP+iD1Pz)X!QC6T;BNQf-Fw$L z_tdHH$NlqFHAPo9&yu;OjxiT8>Zs3fQ`FfbSjZ)7xKVBq+H9}W-_@QE+0A^{8x zC5(cMq_(&5;b-JbJXzexA(P*NiixPGZ&J`xXi!qpDsb>Ps1!meLGr3a(ZL`F4Mgn} zY7I9X*yO4=1hhL<4@Lp}Lre8$)?U`0);k9~-=j8~q%RjS&W+xF7=`zerNRTl!2bIc zjRS`zu_W_b(VT>`AWY(b*vex{?k#UfB`Y612oxMVf+Ddr=O1E90m_stxQw5$_BeE#`>72P_9|Rf%#R9MdvW#sBmm~$)R*I9z4~wy($%lW0(k4)G)3V_;&H|aD3Eb zZoZ79`Wq}-Wz9?bEMaD>pq7s?y$NW9aX)kA}0qk(bV=$0KwlU(a#OvdF; zvSIqGA6$Z>rT)j`_OIbbdw%xGWOrl{adG!8XQ6;Cc0-mWLU!q;Iddvg1P}rxjBD5t zEHKImbca^?GcQ_tuHCuD-KKtpL>c|>{OECe1hic447G`M_xru!PpKZ8hk*~E z;cuVgbOSNJijsuEfHKu!p#A>+M6d`B@@t4;eHd|BID$-*8&bdUem#_Ior}6%!5rP* z+faD~7K+k0k2e}-sC-*jiYA?$E3yD{!~&wxpc%&k2}Dbrk9HGM(z@x;JuxwM%yDnj zx_IwhY;}fb(DDs8$b-P85Rf2C8~X~abSz7nq<27Cic)lW35vP2-tS($BxG!LKh-BG z@mKn?_Ce2iH6$z^9q-*o1Cdz;l@Vcev4dM&r`;^-DZxqw`q9wHh(7L!Y^_rHgMQps zsJaAPEGz;Jz2^NHE}ZW%^kBAd3TpvzC^;vKCylGg>r6tIO;B3nQRqiPTR7jN0PD?^ z1NP0+uuFt+s8ld-8IVURJH^BFWYDHy)cPSpPXyv(s?kG{{RY3OPJ)2Sjq6iE%6FzX z|NP-oWwZN^c|J~uWbUu5BK*k)7V&6s27vO}(nk1Ff@Kl1ReyMyGtM$$H98w8k|LAJ5)MRb2WXt$1`N$qN5n|<4)5DpY-ixtzy#dT|W?v>2%sSuqTy`9TL6GP&0Q7t2tlyT>6b&vx9D~iNzY`KI zV3!f0(s{fvvCe&+|GvJ@(t~8&7?eABdoZZ8p?h8Y_Wm+rrXvsoDo5|8jSM>kY(y`S z3_et16NLwr+6mQ~QS$LXH6=0;O%g^XFfxSohEBEM!NZ#u!(>4=|2oWSG;SeaDsA0&ZLnTU35ODb&^6#krz}Zy&`u?41<#uy(qNMj=O52~& z>hMd)j~-3HMXbO=mRW&^mg*-m=|L4Bk!ywI0uiK{=Xe2;Qal2_)n~rcj@8?U4H?x> z50@2ozxf%8e^F^>bf7Ix)ZpG3hYMaOn1t0!BZ6TPE7BmOG!1wJ{-ZVd**%)G0mq2@ zQH|;{&D3z8^%?Ac-y>k;-(_D;(w}ijSs5D$cspZ7lM+6K{Y)d52duzV2`NmH(jN|^ zA0Kl;VVRm;BL`K~vR&6^sz@v2DW#}z-{P`QSG4nJ9@lMu0%d`g?XK1LJOxio*V!05 z=r5X(H3XGd%R0Fz0#WfVdeZOX+qk1M-5bz-_h)Xqtrf)j!2+cGxI5a-@vA@INRdg; zI!1SzS1VgFNP=NNdP=|w3|5FR5F8{6p61q|+bC2-NW2fq3yU_!q<1Qxa=ObaZ`?6& zA?hc8tWfwAM19zZwJd1~cn#3}Yy*X#Qo*|7qe+*?099t816(|(3pz5KC#AacA+F@euS_mx6{qhqGxxg#*SiDl=Z{Thb*b_o zq^p9Hd=@zY5lhdQOPoj9Ak_Z@*+y+sAazIo0iT9lZ7bD3n|N_1 zTvJ3KghT0qm4Tmq7>2D7>0Iy}PB!>sk$iZPC6Pij1>4|g!-Q$wnOg3(Y;4w09WIIJI2NfnB2x#~cdHXD~mNne=NgBn=p)w1txhG#4gOjn&}eG`FzlsQXLlHvKqH ztvJ2IeiuQ{#eY&v$K@S172{K@rYFTBQycR}%rj3B-XG&wIi5VG*8S5D2OsZ^&OEEG zkr4q;*#UU0!&W{BI)d_R_4BX+-fPkN-yT_5-?TO>J@O|t-G!_1O=_3oNC>W1+2eA) zcMst>QK%+_VpQ6XP)$^&r+$qfyE3@DGOpH=76m*P2sn)xus1pyTr!Qidzz=~Wc7^t z7h_1sMU{wnQyQc7EW?yKs6x}@ms0Z*uvTW543Mu${F7nNrS^9;3telEp*< ztJR{6!N!cEi{Vwjf>I+YCyyR^crYXr5JF!UCmCoiL?YpOgZ>1mzEbvkb986Rt{vfD zkyLs`1&n+Lg;CLYro3NfX5SoUMY6chM`e#PPWpI==n-)ehI2PMJD!-dyeQ3^! z;^80(fdSb9R$feu&lDpD~t@3(GebSE5tNg zcgo%*YF-3R&ZWvOTQ`@OB$1@9kq+KL#>|t82^g2K{9Lx5xKzA7w(l|YS&#oNZg3fe z|FCw5#&-JajK*H5{1~K1`O^N-z}UBpf23pw+s8s-E((p9UAuTJc+1mV>Pn8B7Lv5;8LNe zq@ib987BmoQ!*7`!Nh-w@DZpL*X%w+w+94Vd%8`R2Ip|)JsDUG26w*7RS$Z#uv=a> zpKQC##h$yT(5osGXZQK$|M>G>K{(N=P(EB@U&D`RYlP^a&wQOvCCo)jZ_exq0t+Y{ z|BJDszZmoTzdEn(3{tbA)ya)p#Qs+3yK_6PZAD8%HA=i+5MLtR*e$ z-9B$1lh-rcd8NoqD zqPB%G`x{R}pjlb24DrU_9>*03d)nIwYDESoTW$CT)I~6ztr|FFAsh!%76K(b^CSl zqa5z$33+MDJXt#9f$uBy|E0F5l)UR38DQ<8`P07cpM%M53|Y>7AihxSQ@wEOOFY|9 z-#%JC{vihDPt^A(+JDJAYm9uAmqrkn!YQ5fXxeb-air{CF|M>9B$U{oiqo5XSGTtu zhm7c9x#IFkd%(b1;|8~I=5X90!;<1Kh;t-4ea%h}j1ZdHeZ4r1N4-9np!DNDS|+TH zwUhc!>-xK#<}!Y)#c+({M`F)cyDz!!J7CW3GudNGQzf`9r_o~c+$6-O4%D@R*cI{Fi9KI=73SRm# z;>)82n|-xUJzukvJ8OH?VfD`n5(2P(n~58qC*P2eH=!s7nR$g{0s&w7CE$}7c%Z{w z(W*Cf2D<$6gHzS>5s?j5z@^$CHm#e1zq3tPkQ+^2*Y>N|&`3elvC z?84{>u%RtK>0n?9aTu1_c}7(-Z0 zB7L|#@hXp0TA~OS%#jQ@{}`8XUeMepiON7hX|8IcPi#|ce{gZ$zoNn*GcQJAAEg71 zDAlM|jkABUq47BxTrJ5BxXrIGZc~Ru)GT%lyacl+_%->|}L4*t!3e$_x1Pj#k62GtA!g&tp-7`v2 zYKPMuO`h8-kNQT`;FA;$>8tNP($ze0lnDCqYu@pcaC^YPQJSIxK2Hl$qy>iXhu~1f zlIBg1x`vt+g@pb@H@M{@EMZ@L$I@zDJL0w2!DmTi^%Ut5srquRfGf5@)$u@uyP`X> z%pVis7x~H?;2B36q&22XDqema6kT^DqXc4~EO#_-SFR11Nekp-2)`OC&~pZ(0?0==)18%!?5|U`ES%pAHycHa)s4c7M|dYq zp-D}(U0p0r$cZ5tyPCrGq??oWCYZ5|V}mkZ{1;38E+K-0a=~2de0RN*XOvYM`?=^( z$VrA^!~0`RgLWG`y~oH&0j2Ci&8aEBdX%~@jc}Dkg#_E@-^=SB{$dj3H@%NHBn1azZgEdgUNoAS(RZJ$tLLrN;%*;N#otW*>vftm3XHPKPb`R{E5yek`cuP8 z99eTYmU8o)czLAUEk$Zzjub#=lX=DoK?30t$$9!1jcKy-Z`UO&H6a9_hP71l%IfP6 z2}2`A<(RKoP#7}fA$zlC^zpk$0YEUxd`UxPT6Reg4iyU^KcW48c}XavsD65B;`0Y7 zv?@0%)T)>(*RyPUhzs;*wsYs-jyKtU_%COssvFGM)AuG2C|AJ4VFO+qSL1<*41rp@ z;6zxTnu{YYx5~~UsSp`ggzS&zV2nH2O#MJi)#sk)SdcI}Dja3OXJFNyIu$5z8q#Yq z6v^29<1i=9*kJx~NMfe62jDMb{$BPSN_5eONQrWVDMurCCI}FlEAhYs+3pLn^R|Q& z6EqhsQTP|kU&uC4lMMW-mlpoNzmx>BZCwlgUX@>mW8nV_N>eW|r27X-yJ~xG{)Qge zNVz5jC?eZC4`pHmiepqDGP2DHqeAJlJ#CTUbY3ceSe|}ZSW;~7wu2tN`hNYgL-Na{ z|M?_fQ^2bz3kcCXwQSo+aF{eFUGP|A%dKAIwt~t?z~a#WQ*Et6I0Vqck3j5%U;__y zzX*^`vH=+u%<+E-#i|UWIpv0Gj~@aHro= z7TJ`h)WCqYi}A9@7Fyx^8b12W~Cm*HOn!#lj#ht3eJg+x~}@EO^62M!X1q6}b> z7d5QbNdL9H5C(deqq`5jCqY=6MKUO&-0guIkq=@D(B5nt=d_mZzDGjjqMo4>g1j>AgbaS0$fw(r+9GGN7UcRa{m>DOozt97E3Sns4_fzn>FUNX@74T&7;b zFK`~@M0ngC3htj6*#B_fG4ZXEc{`ZP_`2HgjQ8<+HAuVI3{A*?Q(*e5QGyuJS>CoF zBXoP_Xptvy0(~%9lvz}_xorFDo$GLTCDcB)Tt#(UKI6)9ciup4C|~7y-!PlUkzo1p zm~Fa7KVqTDL5`TaB#yuTv{O}cYheWXcqsYvyUWi3+Y3lX8n4ps@y@=u1J6h%Z_>6h zsajtg-V{%>i?=Na%qI!TSRe?$^5;SDwiZgwZ!qs>LDSQP22 zNu<0V--{V(%*yC3xoF&-PwU0C+f%$K8EO^+v@=jaK5gV$Y!+dCk$r?-xFn1>CaY z`yXl;)LDn+4&ZjDa>|R8*o)4_o^Ae-Joa={V>R(JM14g7b~NNGonhxITY;Aw%f>J) z&8YN{Q4_cbp63xvDNX3?qYp05qweebim&h76! zw-J1cBm#Zko$S;ig@m+h&zF{uk|E}|S90fV+9m5_^ z{*;#A6gY05OwPl}<1rCAY+W=X{;v{(5L>+$LmXjVsT=D+{(L*#7-`Gq!Nrfo#Mt-8 zU$hi-@ui#f(NeW*Oeq?kqWWo)i9PO}68ZHR!1B2JaCr{HPI-Lp92l8WXjEwqTr=(4 zcKOxyVnx|yhi2*i6Z#W8#8!g`Uw<7&z0Zu48#+4<)06h*bSrDvsf(9A+}vC-e^ zL_z(b(L)2T?t*fHbCC%*d$<3BpP*Efq8?AbbcTy?d?PgS_)MvgOeB=n(SCc7D^O(vi?m)N^j- zMs$zllGBqeu3g`k%c(l~c=;tfHbW)Uv}5ZaIR)7_m2zPOXOg!E{e9XcOWc9(PIFpQ z&}lZ!F@H{<1~AjKRcrpNKfS16Y>QdQ$gfd_SZcS4lvvuRGv*Tn95Q=pRqq88OEtk~p824HM z#y!EcDE_z{LHAC=^&D0nyJ3zm)ZL1)E^G5{EnyLItvVySpV^$>jAEc+UpYa7 zecEIkx~shO>rT+V=p4&-Lbs2+kSCudb8~`JKNkumn50v2vys)j ze@$^GDq}mSyAEDvUkobVl7gxKXsT0;bWL?7f5B`ZPHTvnR?(&z=RSuKTFwM zSl>!DtN8IpW5$|Jzah8%d1pP5QH(ftWBIEk7UPcC^eVmA?OJb{)8lN4n(39X`QQI6 zb?RDaPKNiDToO3Q7pY311Y9<#dtmG@bpZi?A1Wc9S)|&Qbp8DKhX?_MHEjOF_8Z?P zYDQzd-UpwXii<7?vej@jQPyZTmav%qhy4qECf!HDq#e5F)VHP0p|ASMRKK$zpguqT zE{doy-6Ij}!$7N+f^s)zNb3{RuFCijF|QN0L*)b`c+J;Zx#S~dQR8y1k*~}8Sy$8Y zPzexnC(pD$WnXL!S8D4-vow~6O6!-8=HlNnKS#eyua*+1y4+-|qgPHA#ZLof{-dl$H6Vev?g@BwZ&%OM&mKfT;k5+#rm_W9mS6DCE zRBS)>d}Yw#_BnT`@U^_%#m%VbcO|8ucS?rggf%`Jchu}Vr1OcgN51P@(6eH2fs)MU zWOI|L8jT3!pe@;~UjNH|-oXnOq`^!v^4^5jr=c!~9)R}5MH1mbgcU`Yf|eP5-R6=^ zLwm)%O-9z|-Xq9ZZG6mxsHC*cyB;O5V-ErCKRXes{ffr54!YaY%}OoaTynXI?a}jx z#-95KtbaN;=6e=MI>WDpYAr!*!rUFsHV3I3*Sn?4*W>Caus`6W4~N!<%9-*_T$k|9Te>=lAbirKjrfUnKPX z>poBQSALA9Y2i>S@tWdM>)~{iUxyLqf7|kwg*%vJ{$g<*TZf-4OyM#z;ni1P zGFNViE^4?zQT|-_o^|pq(DjefRgf9C{PsKp1ja4$>5Q=n8aODgZzdM-X& z7MFE_J0aWP@ofX1c*`oONUSt`3HpC}0p{*|BHHaAjt3rFHrJ)`;;_=f^ImeBfk)aCpERsAwr?$QXYPc3>%<~fkN;X4X){pBPVj3lB zLp#rfsZm#B&rjFlS@yl=2@$`R*u_5ih%9>jO@FHo(_l9)v;EDR(3;1cFaL|s{`$qc zc7uy6L&rU6=@3_E^lTWK->5*+8ulA#?zKU#P>`%Vk1Wm_z88AH;`^1PEviMu8-l=cWkr%qAmigUn2Hs=}Emm&V zIXt1qd9p>q!tMYu#&sq8r+Ef=t#?ftiqjU=x8+7Ara|~8N|08Hhb(cO4FA|oHk|7T zo8HB3>lfXtSZjQWotf&zZsBC6UGTf>vn^$3q8m7A0CjkLF97;33PpDt1uBE(yn z&y426dLo=f#R8$ujBNfRM$6Yg;w}q4<=wM8W%Ca`7@j{*Nfw%=+m-RxUUR`mQ!{xy zQGldA=a$L4?M(4vqIG%LO}?p3`Qd#I>N9SD4Y7S;caE6jZ$pB6Ktz{!h*2t}+dz#$ z^irovWbUE@n2%3#U{S7l0`HtwVKc=+bMZh8GD3i`9UKh*!cDu3n{t61Yd{Hv!G{ds7}Y@HKKEYG6dfe+Qgyw~z>o`* zxOm}6R~q|<|64Bw3|uzwr?%bUiU5)g3%oV;1Ip;Rmx{DH9lk#}`1Q;5Zu6!*AQ^UG z`duT#Vf6o2c~Ta{0DnrST!8>b84SU@f&nJi0)`Q=DmA4C&1Jxd@Lw5`>LJ0^ytH5p zS`Cy)cBuo~jD*VA05h%wOG3$@TmY~&U}~)Y=c&QLFIdz6`_4g1W-pTqL;#!7(n0kM z1(2XFcZ=1=5G%?xMPR)(?>v=B5YS)XT?SPTK-OS3KpHM6MWUdWssGvc|1UxZGqAjE za+q(;;d4xRYi@2{VbU3RcK`HvKh(LgvljelS{p=n3^d^tk1 z>FBPD1MET$E3)>kc{2~i;%K?`N^X=)kY5g0HV8ORGwd#SHWvRxr{O*Djn(3~k}c^5 zm6O9aJPrf|iC{pLOkV+F4p)LPtNfSjf(rrHh~5|TN22Q!!bkPPe`(0SIW}vPm1{P(MYalr9EE|2zU5=D$uD4-TXx_>ac6yfBYS1SFFP zu(&9cg>(O4NfHv`K<@Q8TvUqKtvOzwlAMp+tFk!@Ljdfh0#Mu*FJaSvOTvt?IHpJ( z04pG%HCRFdF?s&}1!3P?2P+YrSjR>(4kSRlSOV97VZ%$8{t+Ft-?nHM3vdH0fR>r9 zOIrZpLK4P|(E%QKWB7v8OuKavfn8`|Y4C*qyB#i?a!czm5mEWqd;*rNqgjFcf}4hn zG0u|{c0S1}C$H6p4i@~|wDPsgmhoeRc)g~k}M*3O` z2jmb2N|($1w~C*b@M+cZa+!3h-mC^(lTTM0X3W$WDLu^Jp6_hgFpL8&Omug%b6<$5 z3+Mh}SR}gGLlVh_CrT7jcl#4a)3~e@e|YTS(5mKUE;KnRy$X8Z{PjH?rdyLu-xjS5 z#nl4V5*-#WpG+7(Ti_w-X6Mx_ZRJc3s?VZ+1jeKe6u`EZ5y0sa3Ft4>{4iy~)k>GW z1(MkX3fy0+;BfQnDqd?;xF))PeST2NH6jpRsw{o*BazApP|8e>7q8>zOsAFyU;+)2 zH3c{{(j0kMa?I*QGETt(w9Ek>4;G0t>y1-6Nfl=z-zsWkOjUuJw>& zjJxIeRJ-eiz_L&vwXGq6P?kwh1Q(BeI9NyB;Hokhk`^%2y_XJn;s8W5agabTOkDQH zYw;8_dXorQbia2gq8gNJQ`&rMRW*-IIZkB4sZ&~Hy+&?XB4!7LL$MCt+9 zdjym|Og);Y%xFlOGy$r0XfQ6yl8A47V$#j8^72ST3^XhP-WWGaEI;3!Kt07BLe^q; zahBpBc9X=}S8=(A#~x+MQkw<9q4XvI25Og1RXAtIjN~WTMTMXmbjeHpSM=i({&9?E zM%tF5Rujc%S!x&4W95p;P5(SCbB8}Wrj8xfKq$cp2m*xGUXRGAmY(hVkK%H`AsOtni zowPxAELKsq$GThY7ofjt}fmu8F__I`fj6fFZQ7e|!XQbgsR- z=|;Gx_mb<1H|w1LM4Vo2hYMkE&3W}(kJ=LmIERT#{SCW2=UZi>-k~iKm309amb=(vH0&ySP4B&^IvZ*-;m7S&t21s(-*WY zy6rLYS z;%-Dt76a{-7}Y0jviogaMjBjxG-Ona-~ME-oe4Wg7UD(nYV{|s*k#b-J;jhN6niwH z86s#uM_SD>2&J!P6%Galt`PTx*5Sd&N^i{4VdafG(eM1XXPy?=aH(c&pOED$N9pTs znDtJZJ|n8GPm3eiM~0sn=nK`Hi-yZ47yRIxcZwTt>WLQ9J!s4K*DD% z?$EQo0{=@myiaN?IY}M~UvmX6DmDH*F<=$XaGViHGClx?RR7Y%5>i3cwUYYzll{Hu zUT8USBnABps6T12MdS>a~76>lMa#a;Umr#WB=Q*M>t+Q&Gk;FR=+LoJ~Sd{4!5z| z)<=z0Rh5l0F}fJ&#X{+uIZNCg2dBqwPxmcJbRobk#=SiuPBb@5^;%-Um77|njr+8G znca`3g@`Lawy-c+@ZAOiciuVS(e|$MgYA0KOX3l!0fbElJcSE*I5x_)QTv|4if(&? z#n@o>ke=&xOTY3s6XEG~#tsI^+|^}MwpIga7(~0F7V_ad?B7j5^0+4%GQ+=n`n(*X zZ5ak%>6yUMf0U0ro|@OMY@!$Wq?_Bqbx@K~%nu)r44Z>1p*OgVhHx&?#fG4&dJ~^` zLF}C`=qYZr$?6hHi~cn=Wxc&_=x-P~WkC`QPVD8Wlq1-M71rcC-^1UE2X4CgyqISB zNN4pw;sHf!t?j9CM{Fw&SSC>+Af>(Fh%c}&IYJl^R}pBt=s$YuRJxnAn=j4?ZvWz0 zeeYkeWuGUAfWwNs=v6xTt@G1|&(v$7Y(?-)z!Ad6ZGv&J)7^ea{Zs}=)4(3b{hcEa z{+u{5C}-adY0hN!aVckYk8Sv~$aqQVlv}TP)r^bMcV+Tg4+Muzzs5}S)_c0Bpq4+n zW!)Ib#rwVAX}O3vMjA)kw+ZgN;>|ZKP1dy7>GhG{NaaFWTzi--5-Gt*uqITD!W54) zLb4xzF^*XU>?6DnFKd{P6$L|(0gF4V!-T*gI4E_^F7pW@x7*D76%!4YV&sVW>wOH@ zYHR0zI*V+RHOk_S>LDh0=^tRB0U{ilnFRybg&MZ8T)J*G`ePckUfXlqYbU~vCVMq? zZGYvQ2-SLeRP{l((y+*1H+WOiErD1a(MK_qxft|w-Te7A%4dn=9!;;FfK5VYL^U9q-C8t4&^%jR1)7Q-S%A#Mf}ThgNLq^D z6ukmRYr^GB?!@wQU!~yb2^h{gOqZv5@M^!c*67<>9m)vv{7FqKeJ+Ux zIvfJuHxJ6Ec{g0i=hky*9(Iq`SxN4W*T$XPW`*wObFR07Xf&7>K7_=maR*_}9Dl@L zUxkYQy}yYbKlA$=rKVC^rA~0)PXlCrSqL)`%Th}%F5{e5;4#Inj20Bytjr!wATYBb zD8VvXro=$7;z){7cP3A01ArhWwyUq6{X zT`v}`)f{7Tnp$V3t@>cMK`;-vNFQOa8RPlfk7}D}(&lP`Z8SI`(2eg==;U9&aa=cZ z9+YoZrBGqM-r^DhA27cV-^V{+-Z0Fdpie*INa&&F0E9Fp1uXH5ew*_4ubUsv{T0_N zb-G&%HSJuSH{0VIc_k*75dj_Pa^F|!tRlG5uChoZ-M!jY&&VCV2efmeLFtHiEySW) zI3r*CV$Y&3;UiK4x+4+X*Z*6sd~C0_qcDiw=nO4f4c9905#MK6NF(tu`={%3?xCMX zt^`y|HKc!Rsj?zdn}*?=Xh=9FJ?X1Juizaiv7kryIi}}$g=-YLXi`yT9ABgGQYo9L z^wSsnmk$meLUEbf-t1-cFwW&UdwUZS^!8ob&Osn?2Tzjo^(8$^s82||Tz;YcYdfq( zE6b1;&J?;kIM{O(N){_Ii+C8I{iZ7~4G5eAe$J`p4D?fHwBCoIKA%dHR5__{v6n;8-3K$VwQUGa8h?H(bVgk zL4t^3Xs1sWP0E@EBy)$-z(HQ=qv^rHrDLzXUWxLR7}yN#lR#!Z zwR~x{d0&qiT9_Miv8zplR;o<3S5}+4RMG6Bf1ek0l>pG#&=Mb9yBH-c*>x*)c#D!2PNvvt z3l9g>p@ldSQmz%7zH>tBCWBPT8kW_ z&I_Q%qz*tn5aNv>Kz02^0Hu^oC(`=Sc|MQb5#IORgvg6;Jf{*;Hv;8Jo+_Ij`}qNP8Uevi)}DnEqF98fC3Fn0H6kt zlR{1)gL%PAfcBXG0oLHelK*!)+YypQ#eyFX7MoQB`oowRr(XI6{?#4pCA!aBfjWwe$Sq^~9 z|9d(4FIWf2&Hiab2o9zLN}+6yLL5-A1PqYc_FnEIzrL{`0|r+CLK%-)?4Tk@ zNp`DV&Jws%fc-O35{RM?FEHWpO4mZG*HJ>AATY!KKqulGy)Mq?2fb8EuhM4LnE z<{dL2sp=ASps?@40VJ7j+O$Qf=V!3BhUgyL7YDa#?Q-&O{iN zSjD`l30BMV(|uCSYKsC2E?ofvmT1X$c5M$pZAfjZGd@?#O4WVVMQ#$z>&gr0)BIBWneOd=lsOHs0Tpf$T-u$ghf6)I zu`LSOGzucg0!~nmJC_HW`g?#-F6vuVJx4E%CVl#ewW?g`pR`FO+j}~ijpEAvOKE91 zt2uh*NIaCwYFMH#j-Y6<*?BT@q;stCV7AgTfl2cLk*P)2ir#?sxJTq1KZ#j#EIZg2(0@ zisEM`+D6}XAK!y%oeANty!4k=gHv4kN}a~ohP5dCdeOKXL#Uac!<2XS{iEjZqW69E zJS^MfiXSOMOhyy1J(sbYX6-Jx@bN;o0Q$SZX+urB-rH<{`H^)nl^OY;Hj=BYW}e-4 zCruWI#X6;hrHUtRo)%}XGyYZNSBesE-vj+grTHaF8Elg=olj+AqC)D;D`AxyVGC1D zZ@&d^xPk1)qLXNoHWgAeQ+M)85#!P|0EVJuzdNi1F9=%Zc4Y1GP@6hY?7lVl)}YlJ z)9UvF#GxM=iB@aanfJL;_FLoxck}bL_|_;~3~9}fV+#vm(Q!iaz52E?vE6b??G~Bv zHJJp^%a9uXs0_R#rSG_{BWy9h`F_e*!S92fQKvRC=2I*R#DEC8t|7iZ2 z$*`L0vzWW;W-d*dRA?L&MUx`%fkQP#Ah~M5tmd@HB*={M^`UQ}tEi^Pn+J()_NF5r zD&xrOdw3nB?n>Qwm?R?ZQW6Tg}yD|HTFex$pT5$NKo&;{bW8ruXH!^)q~; z@O5kH)J8kiM4pVf$kj&|vn!Q|F%%jnObQ_>UecyCiY!HZ#mSON6Ahr_r!t_GfWoyW zfKDkrJdfoLA%LeXzac_Yv#lefSLJi|+loojTF~eS`Cf!UgGcGblE1)(XkEwXIKXKz z08I%VTl>7f>ojjCF6LXXPkU9)EEOuJ4~({!0Ah}nojEAWMnB zN_hKHJ&?cjIL1bsCjETrL+Cpz-3&W_1>QKDxZZUc?IKO1Tgao&h!?QvKDSb6L}_F^$&ObC zX~yVzH0d9cb&(tl!@v%;r5n-=QR+kDEZ!{(CY=^aUVnhdxt*YPicHF*L>yUkZY(Vy zOiy#17F0U?Zsw|WTgl~?AofZ;g@ngh{>Xqo;|Nf^W5<4TkH-mT+xtJu7iXl)mY~BZ z=d#$|JyPu|De~`;;?>4)@&lhD=vjEZ)^YA!N04IoruDsd2geVnXQOubwo6fVt3Os`1EN)R2Y5R8qxtTyi1#!ALa6QFBn1sV0 zy?uO}_NAZ24P((OfW4r_>{L@0g!4D35X5{Ylt?tDI`yX;*)TSZcX4vOD>%|lBJ;q?ng9iU2N8ty~Osv!ym4rAQ&} z619VyASocp2}E%Om_5=(7DV(IIk@+(lQnPM-%tE#&)wcN3$RLzq;^VshkDiDfVcjI zmU(8dM2A4~arKB=76FYwwc>Fv!^BE8F$Ex%nFH5OW0(Vf0ew~~+fEKot`u9$#Qza;5l#kZe+jcU7TGA)KpOs!SE+w=;%Fh&g+r zK+Hx)0`=2h0Vwf*)8L6}Y>2HpbY!h5Gzu(g0k`J`3_iSvKG3L6>h83=r|ld*6%s}# zfr02GLnULV*{Bb|w(qVQMz_OkxJfzDucMebW1*1|(2)(U<65%3OM?a&D5BT|i0`;( zt&K$Vgdr*opbp8Js3ij>kJ9O*)F_`%#1**!!+pTbI{Ji|2~_U;j$D92tXgcNcVir0 z;R~_uwbeFVT}u{)wkYa8VQUxVHi~hKD<<1VeQ22MS4@-6tgu7pWZ{7Um-ree9R^s5 z(G#uVa*6zRf4TY!NDC?Evjt{`1WAj3*si*eb8!mwT93LDFIUy7A)7}R7bJj+O8uu7 z0QVQ0=)uoWXpO*^k>3sP{4YtJR{F50R<2nj?Q_mf z{9q};Bo}0A*B7j}I{n@sE6EaiW2yV+aGoK#1y;xa^?zZy!kL}YpIUV2uH7{L;+P*} zVfa$ulHa35Do1c#-KW<>uz5Qp<)BVE8LsdxXxU zMDYA@In0laM5Xm?@B!$^@a2*LaGi`AsF>)KHS|%;Dl6X2L@NWm%G7h53_yJ(4kXZX zq(A^)2QUq$ccHde0Mo?)Sd6r55jWU{78s<}ZJyH(3;`Hw()*f(`E)84D?YR93oO@- zyvBZI$u4XlaFNw1ml4G@@*{p_7M9f;{4|uP5C+h^Kv}ff$hpW>)&39Dh!eZ~Z6=cu zBoGSJ1c`cdTJbV^ga1&C(Ek5C@}D~b0ujN5W|h;(8Iylf7e8}(&7lAblZN}8=Nlzv zz`qok{*%TTE`pn|c+lr7JUVDDF}M(=&tfL20Kb#HEG_94UWHgqg*KgC28NdFwQnmcF}3v6-~Wp#bUZ?-V-M1F@fN zl3iRcCrh+jOs;a6WXf;WO$x`hw3Avum1q|UT zFCE@JVlDWcP<~?8NvjtFlI)P7{oZyp&^&A*H!d&t!|jsk=%B|$#jvzKt{J@y6K)#-uQ)Sm zHQJ?@h-7gdGbOuH!p*M+<9)Ai8FjAm7Phhmc71W}_DAy-LpE3~^Zbg4r`g9c#TJDU zXDAGao&Wyq9htVD-HU3pXY|`v)-HR4J%2SDaKG2J-lJVJ~H@PjzY{W<7AFAAZl;9*eg!7=9t69@u8#%_Vp~s`tIVIzm>k%Tb3Gn zIZgg79@rf_-Oz3`g%t`qO?k*$6r&O0eW^eGb?^Fv!0Rk3C&<-$6=>lLN+v}*^h9&D zbb0T1D5&qX__tes-@UI$dHFuiMz_JK37y~%IRU3tY_8`6S3A7_IUOjS-SLR$xkk|HIT<#zhrx-@~*Z-3Sr_ z(xDPVgQS9VcQ*_S-6bKdba%IcGIV!`ZCw0zvP!2vrMJ&%%@PxqPB0v-(fjmz{lLitH!pU;@ftKe-PXTkc3*|sOJ zW9DL1R!!w3tXE8>pGY>pt5g=VC`FhJ2(2-=|FNSjX85|#rG843HKnwRr_t+TEzAp9 z9(|Mu+c^bN=H5VJ|9fqOmFTLxtxv4kF15Vg6IBZDr5??#s?5G%1ZtnOiVl=9*Y*(k`_MB zU1!|h(r+?r#ok(CGT$48Z(qaxRa?r`@v59U4@0A4p2bE{=3Ej$oZ76J#6bCiE2loJ zpGZ#X8QzF77D=mc9&~VZxfR;S_c$h6XMKmmulJ+xRJB#Ffg9*Ky9auF zGK37PG7@-QZJ6WuIKv2IZut0~7sdF2gfhHp<5G?HAL)xU!8_sHl<3O1dbHon7Z#t~ zo;Wz}*YCj7ouA5~tB9oc&^SIx^{1UODu+m?54!_FsLYC&b|MERN24UGrK_Team>D6 z%ytBCzmQGpjiUpsJs&_6u0-uCb^Qmtlp4GOb~TbPvS`^@D2BUT>+sR;1x12@lv8kC zXGr)ZO->*HcC=5tB_^TJj)g@dCCxsUUlX5w`# zxrKA*uxju?7D`gt&#oVMpi8YVBaM{jhxJmLGTRo=I^`!4?LLPhaig2ljp-6)I&v-x ztlu_&BZwee_6yv*)1`zX!0^gux;<01s2w+7m8)BxY%_-$y_*goH+fM23auN1ULiwk zv+gavyGka%bLeE`zXu9h_(76K+u}(2Pff9$xk%vFTV{}Fx*35gXdfNmV80QP@!FAH z?9D1hyegz8a92GU^San2hTIteCJDoMAb`^@ekIioq(z65O;YnTZP_g*#ru%ws!|wb z&bDir{5MqjN!ZJRoRaIk1dImGB5)Z>X<_W46~Xuy$gG0H*t|f_<@*+};Eh}z{XmMd z*86Dt&jJ%$5y*^?Gq+A)_#}iBaTVHw-ORiCle_8ne{x!_=7{iv1Pp3f`5stJ6h`dv zCL|>Msz;Gt4dlGuP1R6Ooac=El?1aWNUV+!>U+og+jcgFK_TN4#z`8XE++_()g|{` z^uEecdX}PG5g`D#0SyTF&wnSidmUMX4l%{I!uk)1Jt5hS1z8WCQUQs4!jm7e+R-Cu z^IiOP0t~*fc+h~IVT8c#H!B(tXTW0VpL|BQEW}we;XC_BG0AUK>J^S9LD+P2ped@$_w z9mOfshILyr*J4n>L?hEC?&>Fc7Jq6^787(bNTX*c+(hh=4`_)+6CU4wmFn;8M4G#U9;h`2@>EH@^^pgTlX ztI)fm-PUNCmu0*^J{a$~ElqfzzF%y$8`bfpbBhM=yI4(2sPNuC_P?2@qv{siG5Hcf zkTnQX7}WWxi&ho9w4cjSm*hqo99A@R`Iq}(9L)Z=RQ)hWCBq}EjE`;H{EkJ(aV@9F zZ;minDUm^~ppS!PS7Q_K}ieC+0^=9#AZP{#;^PLkSoJ5%+0M)AOutp{V2A z$Zg|=izn*un;vfJt&WtEs{No;Tc=of{KCs266&h5fAjPPRCY5k%9=tDE-Q33ZkL6Y zSin_LHOWv<8f6>V<8C?0wXsi0f=w3V{-m)e+;2Q_F?*to-!s2|%f;(WPdc>wQ`_jA zR9uR`qkyW-V5YfKSGA|QeWga#65maSjDCOmgC-)mesctzPy_H)_&Sp%;s-zH|I)Ks zrle@8JwmvistBB$Pp=zg$Nx_5159LXGqAgq`SV`od7BzPU90@za`2=sRTZeh)z*W?e^}JZ; zwu&!Vd$1Mcq}&W^1hWZ4XEP#>m{-S@w|o=_O_y^M^5iVXN1-(&-nLpaJM8zmp2Jnq z+?(x}cWE=ESp~Sr9&zU}A-AJ%0)+Q6et(!!nT<#{4j<_*D?bWzQY)b6?2bs^Gn~m0 zl$qVNaZuYJ5_agXDuCh}K1QgFl|Ya$zRq%LeVp$jV|&ea-)+ovr1D;Zmsz@V08-lQ z6vJ3IU2io1U5a3_7nkdj#S;J}q`}z_QYL#IOYneFW0~(CeK9=#{GQiLzfBKMFMzCV z>Y((<%O(PgEbG-w7w7x6N_&9YnJhfUctpI5)7^U%UWyL8#A zSj_tm@wHWZATFbOCklGfB`i@RE8mOeYH_rgFh{t7Ot@M%U6{*z3j0^_>7;w)N52PH zk~u{ZX3-O_V-Mx9zx9dA_OB053nzRRY+W$hBs&J0sE0 zkQ{d3a$JQk^OIWMt_oj`7=uH+LeaS*lTo@*fisL`UyBUT?HTELb4dUiFo?pI(|-^T zE~KRgtF%zCg26TaJGlsdWsip-;skZm5~&wYbKme1s+j>N3Ln|Bl>RAYgT0j{%RZl; zf7a}UHI~J-$0E$0!uMy6U4|r_sG(Dz5WF?$n3C0>6Z{)CSI8%V#fo!@PUrajoF9Ra`iV*4O*vVyyW7d!mKHBLO z4pYn8rhl#I@my|3)!IAtlR_?G(I>IsqGnFKX~CEt==%&rlxf>2VOxZ8rk&2;YZl*q zyGNc>whDqnO#MH${#Mn?bANOE*2}`aW!hJ5&B7i~m}`!)2~Ix%lFXNbz_PppWYwMp zGee>?eXV?(n9>GP`%BhzOqlm|nF?6;awB!(|1NV57>j5l2M88ma&<=&cB*XI(}VYa zsZVx?C++PwV9~M1KEX_t~cul0D%`T85mDzPg}OY!}>;j+L6L^|InGuX;`k#f77|Uv?`(ezh-f zAL+;E>lt>fS=7Ms6TL`MB!MB%$9bUYP}o-AYVcsZWs%q+ro8uKF+56Cxm}k-#htZx zH_VD%hOqTUuVT^Ql|ehQco15nn;grb{FZ^x&niDbZ%e0jt6II9Q5slOB1q)^jd6~Z zNY&%Q1#dI+HdJK4pD{Q3no;U!8BEnW-(U0)PnJnyc)a{th6u!DxdHMCiJ^c- zX(6WRT-y9@Z0Zw>#c6{#7cLK!XFw-rq&nA(xv!nhsT4f?Klp(IZn7vyc&Q-y2YJFtS(C;_` z;O$Nmq`6uWWcPkYi*FQ+UmooIF}vkAmJ_%~o%5+;d*lZ-ey(j!)?KeLnD2Q5KdB`` zPo_w(epDG&KpPl>M=t*6Mp9HhFkMt=Sw5q-g1jra_-m|(rvAn6;u(K=coN+E3AU)_ zpG#X+OjWGXYu9MtGO*5a)cDd(04<9m5~PF@+LaZJ;PL_s5cV$9z72$8P;d}LbV-*~ zBMalcJWP!ZYWu}=NrmicY~0n?|NAxStfle&!n4pIGUm@*?ZnR*>9NlL2EWu4GwJ;E z^JM6@TtdYRx)%+EWvcgh_GF2IbZ0k?%Eh#=Wlyw@{D;V9S%sJV1>VU99Jye6YQcdI zkaKnB7T2PX211$4h+5bR#&ZP(|*t^h_437Q<*ILP-Oq060l!F_- z1#6(3r$C;1TKNJFKhJ2Z;Hw5ijBV@Gv z?mYj4abq2M-E@ULR?iBR(KcFtTn!lm8*tABB0O|ZHiV(C6S^tfguLeK0_|BBzHNk;cE`dNK%FSA zwVN%n8J-^Yq9-DqReVm19UO-l*!4Ey#`?EzDXEWph#~SCgHLD{m5zh9_!RC@_$(cq zW*<1|+ahBJt~nkNsb-KYGn?J1k0?LAEiq*M=wp#fnV+%6C*)FcX2#}b&+EY_40pCV z+)RYaePOq`+S|(?|3$XA=e6zIa&>yE14Y{wGx0geOlWv`c*A0~p=c!*(hQerErm&n zRHjF^l8*lGQbSHuxJX57;#_jL%}@M&@isA4H592tb>}rtyXHj{uOW{T^m(vf081{* zw{)e?X;V1fmwr`qwF{SNub$w5U)}wZUQ{Wk`cb@rq~ZHC(X303!2`dTbE zQ*2KjSq5o0oQvD zkHLkHezSa#cFr9T8(Dm-5c{e?@2Fec!z!OH{K(b_~N9w?AA zfj`{ggOY)Zs9fWNbT-R}=~9*+H*I@wSL+f2)h>YS;u?uQh7I2z%1RI2BfTXOUX7se z+bI&T40HCnrXm;l+f&?>fCMpJ33U|HoT~-wK)|X1mJND6!2}!OEqsd1){74`ZHctT zAtA=4Hb-%^;8FQ9%CVw!f3&tQ@6N`xaMj*5C3@u^@(Uk)SE>lp_}=J6MYOsJW8$1W zw~XVgZ@jYaT(-X{xm|5Mz@jHuB-XnRrNf?EJ>3{81={U2w4SUG=tP~ekJ<+bn4|z( z;RbxvumuS|f6+!?%Ji{_@!q7xd6ntG`>|wftQITUw+AOOc!)JLw2`}Uv9M%jhz=Yy zGN!qubD*)i!}~56?b-Wiik2y81LjCAl{}i0Nvd$SUu9l}bgJ;WFnF(Fb(ZPan%Pw$ z_Nx7{nHP4rcA)zvez)2FukX>+%LXoPELj@$GBua8O?iSa)Dp|FEX2J%e9&C&$BCx0 zpjT@QlNdcZj|caEr`n*##__r9XkPE+aoW5Cr6NUFg|bJ1&ovJImzDJQHSy?-X!If1@~z4SCtNZga4+o^|-3 z&(Tk8BH3@#e&}sn?g)94%cmFAv8b5N8@*8Ppy+-Gd*i2dZ2iff_JubupW?nYJStBa zW_45Vyhi!7#^4zOVpd-=Mg;wROTsba>uvrECzco#xQhb+d23rxvK*<>8=fw9b~an9 z0X(`}_Gvg0M$c{PjeYLb$6S|gTFbs0)zj)nj!7^b_m%K7WIfZ=c~cdBrbK}QA-sRWmzhPs_?T-xDIH0{ zZYulpZA?*li;&LZP5IVC&dSp;pHGm9NMJAqp~~6jh&8|r83VsJV=`uw8!Vtelk-^b zoF~K5#vlh#73L*+U6M8VaR#5%_tMq+=3Uz|C6`C(!R4Xhm{8FCfkUgsForQRa;bC^ z6ypMKpr6UNKR-e1h8u%TelLNZqWrDlJBlX4Al$-{kGMsbnQ{fsFXp`T zEs3*51q(q0c0t4oR4O7gaPjvhl`U+hydq6bIH`;An8{dZ;{-rG{lJm4zNt4YfSy=X z3*CPW4_2BTHWXyf`=Z}xC`w9tnK!i8d&h&j*1MfpTOumNbevOTRgU^PRM7_pg-C?n z-G`HVfRU<+gg_nczkra8Q(X9b>oe>%TMX*ab2Vx?UXR-r{-QIr5J>7pR=SZ}EmV2; z`q$HpVbk_GcH*2OPtetO{I6#l+e)EQx(3_CPt@FArYi8lPplP1Hk0+-yg#h1jcqgrx zNP}NlwlKG4T`U$9q$vASO9j~7B{%byT-M6a@u}vJMkl1Zedm}uo*02}$p$bjPc;8K zNn=QztxelsmB0HtCt#GaVwdlH4mTdY=004!FT#XDq{(O^Dpfrim9Eon+2qtuF4NIp zDeBw^>kML)NrPtImmxh{VoQly2r(7CR6dKq13gBxfjsXBiG0T76B7-+4Nh_nlMRu7 z$~L#FvO7lMF-V^quYAa5jqky}tPuQ@@|+X@#k5jN!q9>It0Zim#V>OFP-AIq+5io& zUG&DT8wNJZZqln~F1>c&uoXTB(cLgU%#Z^f-QXqf!z0Ei{8j7KJiX&SB=gnquUV~I z8YME=zeETe*!+X?2Fv*CHm6%K@0+nJ=nZO0TW!Or)dsJ<6oCn{@cm&KK&##O7;4Np zj(?m3xPW!K?ySG9g)kOy3<^G|bX`(B$Lr-rhF|wc2>LgQ9`F%mjyaW%dvx24jeT`7 zm4e((E^z@E=v~*saW2b_F#FeYD_^$#C}xXWBXC`#acuFH&D|776D+A~4`K!&C1>D85;P&)V?|H%s4s!!17 zHj2xRB^RE16hRQvlJ&>J&P@WVwj>^4dvdnAbz8dFFMe$n-mI~&>P@dng%sxUr=;);e&*vlMl*;kK$CW{=PSSPp)4f3-uUZ76a~3a;`Slo{8(R zE%w}+nVlZTU17am$A$#s_cE}dS+-UERWE<6FCzVIS5LC*8iQWbZ^w0k!TY)y++wOu zmDTaHi6m;0`&Oo&asr#HLEW|%|JuzO{7C%YGG@9X$%k?*Gl24f)ZE5Xg1-&j+mN8F&hzD2216|d=)T9R%tKt3~ zHnh2XWVv{vmK}JtwsiWZXS@fUw2U&D(4iww6--(wn1{D+^N6yy=XCbXP{U_oci<5D zT#86!?+He_To2Mk=U=GLc*d}6+Emv6X#tE& zB{Gpbz*#$=t=EKr+;WqjahI<(HsT35X#5#>CM;S4AoM}B@}jF0m!L*Y>;ULzTSH{ekOwRqGEqu>qEGSLj#oO=D zRq6Y2;~&neGxGB?=GCh5%Twqs_Y2AG#veOx-gyz^=sIYxIV-#m8N9(Z)DEDKAQJ=H zBZP0t%OOB}lubExt3S^@)+}+~1H7??UxiFZZ}#`@*H*g?HHd#NEt}e;hG_^sjA+_= z3tu?u$l_MM+xu0V-_eLFqR0L3Uzv$SU7`AQ^s2>n9Nzy` zPrq$>&#z^84a)-F$b$wBx0=EKHmKQ@NZ-;ISRjNsS-MPV6LBOka=D7^!DUImXh_d z&wrM4BA2S+3k6Gc;+k}`-VwOX?*S_{>Q(v@UYFf;csv!+{`{)x(r&P+NM%p8wb2h` zh-rSbc^>mzEkmcPd=LxeDVK$%~#4|6QdYCbBgd>0%7th6LN?9B}aPA2HarmQR>QPnPR;XR6(^ zn!zj{dxr|J@r+7}pa42}!xAN((6gN5(IMsDBYFeN_FP)m2^KJ)al%oF@yC235`{Jc zzJr@4Cn4CK3z4@i4Oo00b=2mo@Lt8muR)#qh;%p{Yro;MCgY4VVTnNvj|ni<@QeGk zRpgwpW#Qu6Sab?A+jAOocE0br&Z6+=6&4Vgadvk5!IMQ{-fVORT5|>}oR7*f)VU%| zLHhAW56g^|Yts5-v)t1Tlvk}vcx%#qoq|x7%Zjh0W0F$<6A8>}fZlSATb4@hxz!Hqnh!mtRlpd+#5aTfgJ+gGd~5 z(6PdVDG;}D$R_{$`aRODG57){yxQ3ChWvMq7lkZ~UvMChDYi!6%aYWton1k|C4j>q zrfffReNwTRJ)JOUL%u_<_|A8t$SY2&TMk5F<{i9A{X}Q5R!!@L6wc(J6(}Usx=b>% zoH^0MIs~l487772uf!~bHb(Vi{S6u7Nqj!+fcmH32wCZk!u@n$q-ieatj}l3#rqC@ z43yM&V)T?p^@m04MrVUhr6E}q8A&hyyzcco-0LRX8mciWNqE!5XW%=r72tCeb=6cl zP2AEW#bnHURA5%aZK9Gcbkr*~4q;Rg*cn8Vn;q@Jo= zvw)XB_Q-pYfYu`Aa=a|6I3m+u>znqBOeg~kd#pS_XMDS%aEr~>1vM>44?0{^IUC*i zj!f4-dHMIhVIpA@RF|ns6bK1n4C5SEZKD|c1nF7_q|J*Fz@WsJ_-kW{AwpR)>aO5d zV6?kSff`G_*A$)vf!UqjK8>sIhX-YH@5g zbUCC&N>y&QbLazIU04MbiT8}n+9lQ4^MjsJ@m>TNwhJ$%P(BD@NmeRrdQ^{mJs;%E z54q7Mzh-(po{b0?0H-#L1YI4lR)^?9N>~^;s2M#tuV&wg!)kI?zX!XcN5Bz88a;@a zE0kc*+?ucLp=~dal~q3j7RJZaVTEhGH7N-xaX`exeu}PfH#>i{=Qg@3x+@wQ=<*qh za+1`=#BH@Z+NhKh=4{4Fz+IO^fq3J5EOlVk=-p`b@rkoIJGv z1iJUa?G?<-2%^f#%SK#cP%`|x&U8$E^zdg^Ivw4?Mg#M^dGRsq?&zcg6M0IvJ7;j0 zID}xJscukTDdM$ZE=wVYsH0ZG7s7$QWhwr|jU@wiFrWJ-&<2Yv&JrHg0JYCcaitkM z)V`=7;k}f?4=^PlRjf*XQ+UIF&=^DaBy)U$Qe#yDRcu#20Y?X#P{>kS(n6!nAC>ZPq8d z{qIcV&)+X~n8Cq2q5KZ8#`$RdNADI$Yh-nLkaocrW_8tNaYLuWdDK8A5hoY=8p-2wf?=v&-NyDMGud4JZs?|l<(Ya%otl|tqwJ; z#8WMKEv~eXg#No3s9zdQ`-7&w4jD)oi<6DNiMX*x^$dmU{_;5cZcoN z+Goi*f38!eOXlvg$fbLylyI!rL1h+aud@h{w705_b6!C;VM4E$Fm2cNU&`O~buX{G zN4v{uhc2W90#X0+ACm*}mH~focUIEqnqP)dkLH}1;+@gMx-;G}1MseAdjF3Gcc^>= zeC^SM;+GI8Mq(!G!bxe#=%U7e5^>n~>(NxP;*#dvk}kB|I(JmFFOy2Za|vCmld=RD zF5NF|-vGba-0HYOK+9J;PWqA^ScMX6uS5a*3AaB%TMW~}h8}4^kTRxMkPB&40eoNq zg%HPEqyI3HHkLzQIP_1Bi}gKj;j8X>KVLNK%F;2Z0sCga<`-1ve^P`tU=n#hmh&y~ z`5ND6F|dJae4$whhA+#f{8?{wDThK};tkVA8A(n%ou&)VCddlc2TN7^6?H>c5M*Q9 zjhE6_TEMPUOKS?{|0DZbk4o*5WyEW5D$J;nt-(jpdse@_io^zFJg!dViGX~54;bds z7^wm}6#$8xwLk&`q|>GcnMQAcYU%&gB?2*9%lki4MnJiVQI10Kv0dtU+pO2*Sq_v5 zvv~_>JR46_K+bDJSwfyB{_DW{AKrLQnh5# zPg6^HXDsfG9Ol{xte|LxlpA#f)me^F*-Yf2%4dt{lYSCZd%RwcEj8_np;8@8=2UF) zy2ul7-AyrBe)d)Qt7(|%tC;dG(7^kE=f*eWC9zdkDE8?ct7etbFS7xk82@MMmMkHD z9xyCrb2!yBB6V1b+Pio0dS0=YY|E7gSYw$5_1}4{4yVserC8ZNjW?oyF?&sDCPrk zQqG1ttO^-?6LeNYA3fH4v1Q}v!;2%u)Z3?`o570M;6M3eK}o9r(&)z_UR8$OOLYX|76yI_0xwmoQ*%`veAy_98Q zX0LQ{r(-Uq`4kr5;w}sN1R@Cf#6(u|?WLQ$&(qVx*{C_rU6%J{Y17e0hPS1UN3b)) zEO1VTzh{QMRG*e7ong|dHinYrE_VN3)TsNHHr=j`!Gv@Wahv_yA_W_Pd7rG+;#SXxyZ<=c>RI~RVN@7>} zW2X)GG9P)zNk2n)Dm{9i`5XP%z-`|AgGo1f!|5RF1?;Ct;I$ag0$f9033)i?y*J)O z5f^>BceaMNw%9HHk^jo3^WC^BbSF8G-+6P}6{Y5s;&we&v;a7~$zSGcVP`v2#T$=z zN3#L##!@6(} zE9`zt^oe!;EO};Z)>BpHbMAfYPN~(69qxR?R>1krgfjWxQic~els`yc#Eg%}uiowz zKw5SuTh&c(4z5HCbRI1KP3+?iD(=3z>itn)V3q5mQtTzQBWzXRbJZ0)S*Y)9tybUI z?6!ivQ*4w!951y|GkDdp!==)y2HAnv#@|2oWvjG&wDsifllX93xO~g6*XV@YaJ1Z1 zXgTXPxt+>!+LLU!_os;&TB^~|j77=+S);X4`>Nh-BIrY%;T8QRHzQ0ag(vcSyF4p4 zL%?UD2_T!dBb#ABMZ zRe5nPlj&_Cyw7%PC6h0($Z`W_LnCQcr;I3sCV*4DE#b`z;^`V`a?Mwwex1^nb zu=nM3`e2oQ6F1E07m@~(qVP1Ln6Wfh8U@vTz)F6sNZD^ZMuiMVdr~MeiF{rw!CP6$ zUx(`=4!{V&e5OL194*!IU%xc2(62)lwUX~p3_0XDz*`&bTN$ateXP>HIN4wLUYDFiA|f^D0M7=0s($dZ?Y2ld#O;4 zw9*r)-{S;gOZpvIQXXkgtLr371qrOu#TL@=;u}qiyGPjVh+f_kj#4eD(elIj{?@5k zPw>bqXW{?}uagEGm5rSbryf5eZs7s%_XZ36%Pr53UT%MW@uxEG;RQUQss(MwADLP7 z@x1oY`z-W#p?SQ3%F005AXbt#Z&gaUG3%n~+l@cE9|rU0*9#MaDse>w=w3OrDrfD@ z=_vRf{}@iGmrOmaPAacz7H_=)EJpQzefvU{sMPjZ0ow2|qsv|QgDFfpl-t6Jnpr>Z z+dq!Dm0wDYlE(9m&eexq=}ohrx3Z9Vw%@mg{w|jtFbOH}i;U^Kb`r`^@4|z(I9rut zkqKyQvKy4{HJpTEowfuCPVOygTH|p$HSfhNZtQOvZXaDb=rh-Da$@jhz304u9FUr# zgS4T>1k)4-9YLrU$ZivPQchjipzScaB1&!oH1IW!Jx)M7awe!P+y7BJz1D+IAxlWx z;W}36^qZg6AL}V;hiL>thY9*R&O{dV$#i^&l<5KFczMKw9}_+ZNZwb6e;p+Zs79M1 zXDg^2bB^~@Deq0sC8knXuCu~+%d1;i+HoIN;A}>9bpN^#Q^U9s#HqN2gi7XVJg;l{ za}GXop@`+MuL|C}o&Nib4YE@BK2fG#e(o`jcHE2Of&i*he4^mC%GsCz#LL(mfJx${ zsp%AVI%7JjSO{mVZj%@-1?PjL?hhd9I?j5h^@rI_#F6Ec8n(4MR3SJcgRH)2#Rt4} z4tGQT)_l@SaREDfCnG^>1x_&}py09%y06fmpcSP>zCi#Fqs3@^ zy9gwyl8z=1|JUYw=O#Q>XH1~bb5HTj>5kp^Yy?CeEE7w`4%8Ks7gIQn$k5u+akHNc zh+=-4RysFJh@d)*jFm`WYQtNpz4rF2Xgl`tzf0YZT>}eT4+U83`ZY#@woUDXZGwaE zUYx&YFGvVTpZ-47I*Ti}KBLc5XJ=Ud&M00&T0t%SNTt$uXX-(_aFve*TgNSK(VmzIEFN;lap_SIjQ}fG>VuBbKY03Zq=FhQSB_|$8$Se`Lv&~ zrGq}(@Y+z+7$T@Z2hs`?3g%#*N~Z|oz9SS&xLQ<_LQL2&vNkj$G-^uT7j>^wKbTMA z%GQwz*)amX1D_tzzyYs#Urqu`_;p^5)>B0~iEYkX3d{!0P)Dy|kS0M9(+V2iyL4<` z>y?*;N;%yW?``~Yg8dM%­yeQpoZ!s@vYL00Z$oDS^Mv7YD$coekbruX<68zdZh zB_QsV&^9h`Z_vJ@i~0Ho$1W(Mu$e_1A$Zu*Zsr{MENlw5vrWHF!0w%xsw}@;*d1~s z*-cbY^aw%f(R{7U7-%8m6_>^J3K~0_4)+(@eCg=P1;LzG&Uj814{OsDR$wg$(TDT+ zkqvgsw99Y~nCG;Ul zqe3e;H#(UBa0b{wfg(nEY6D9GSP$?eq^7IW3>j;{oDL04hjKg09WHtm0`^IJ_UH4`NOv{)li?^+Nb48^t9!24g zBf{UCpkVK8B(mbo-nzieUTScb`UbU^&?@S@D56zB+ULNPqQJ@4bMM^v*Zh7eHauWf zMG|lEq8Lir#NE2@?#p-WntvQSnm<7PiQQB$U}iX;^=QIB4dEiXhF#8TJGKrA)i*1*Yr%Akc5drQ`iC5u3!FIw3Nd6I+BnZzDY-xm6nngMl7-ZI zxSwtp_BitWci0LVHR7_7XaAbFL??Afm;htR_4^5GUMgP1<<=8+B=B5?N3BIyS8~{? zo%A^&7onf@wKCuj$+Kdg`FN5UNb#>bUAVnVdMN!@AVqrwHrB|f1gv(d!Sxp#Xdo+$`SY8~V~RJq7~B?W2)zgO-Hqnh>B+d< zLicXhQKYyt^uC+$c9DgyMTa*1YJFr7A6I_IM~h5B_Zb#9Abt`p`LP0NB6?h8XE>FM zJOZ{L#f1h-xk2Y?J2~~0ZI`!6DSSsKoL5Iersj>r&1q}1jd%eV{nt|Q@C1W)GGABf5FN=iP<1Grpj{mwuhW%9IfS# zbtythSKe4;;=oJ!5zfKx?l$VNhflO^>0$r`l@y>It0|DoyL&lof}vTv z=`^C8foOn^J?jH8ID}0_wSeu3dCrkZ^lwS*VZ{GMY9ms z@$TjzWi<{?ysA{rIWcP_JOj$hJE^X*9@us9E=HSwnk>7-<4U4 zp?iCP%-pTk{j*Q--S$$Zo<`tINgpdLcB>P z&`O;`g9Xb;vXnqN4Pf9_R~W%&5EkJQ!L+hC7Iv~t_LQx5{K|_ug@++eCc;b4>lA>* z=q`du3f5u_+(er*!;TB^UcU>uM0?_%Ax=Xdma#jvdNUzSo}I4b*u@t>yPM1YB`c+W z*<&3WzTkYvp6j`Witw%!U<%M2=#cA_tW2+kPtv8z`;Tz6*6J-*(6&LvnF!8xd_cf; zsfWMZgJ&cmS$)UP>644lZPIf$T4iWvXa21qhfN`@UBv24fN|{c^mS@M6Xgthlh(8m z$&2D|X;~evH+RYkH|$_u!?*(*&*|_C1^R}k)KJ3V(8@WBMOVBRXO9tgi?!0%?H%F4 zy#J@WZZhb{yRRhM_#056f-&b09^ELF7#(j?)_3`?O_n?Bdpxlvp}lvau+N*S-!1(&PD9Kq@7`rf7#0y(d> z&q+3fT;5fbf|U^7{p#$(0}eYY6Tu*{5~^ivVN2ZmgEhzb#Sj37L(-b@x1A7>0x~6f z>G;;~asfELzS$y21ZbHI%5yZ6Xm=xeh3-8EZDbzSNcF`|e&F@2K zHv5QUHbZ}9@WCx;q0-zQz` z9}C`Y=f@)b8NwuTG~J|QuaM~dPLP&99Ymq9i}jjUfrQ;~FCFK#yQKmd2d?OKz@uI8 zLS6B&?6^XllW3a0#TT+7dAXEAEcDz7vD#IKB&Sa7_S217__o*h^onMuC9L*q8_rHg z#g%Ps?8HBxyQM3YpKi_vd{|_w!7%URPxBW#oo9px*bj&_83Py~cbHpwozf=#;5t^c z#Y=yZVHvB+tRrE`z#&x45F}6Mq5T26x>6i!x|Q|TVfUf&N8$6fn+If}*)mZv@&%>< ztK~dmS+}lpW~LlW`)K}%m{?pRvq>7)Ck0B^C!CF?Ucw5))knVy2?#VUf(Fi*?UM(x zUl$})P*&SaDR3Nh$zs7MuJEU-V^2x%3N-2ZKP|wqs3QZnij^J$J{2v4u}#jH7^*|F z-FG{Z6pZ0Q%M#o`t~yo=)o%q3b{c$!rc~eP-_|dET9B>w%ul75A=o6FK~h@NQ~WWA z5C3p)Fy5~e>--h-To*|Pfo`}tMfN=+vJe8aK`l!bCF()TkK6S^w%N6dr!Y zF}_`4;2Bm?xPjQ+OotUG65jj{;_rT<{W8qW*Rz~NNP(5@QDjrx`2P4N3Mh#K-!}x`U`3TpJ!+L}qM!#o+z(Zs4isCyO%Ey` z8oTpO6W~7xe!>>}syJ@BB{F*;ed{g58VXlP@;;g{g)r2gpzLY#nf`Gkj7gaEpmMf+ z*~NY3Yn0U0FcnFyNKTqfnbk*tesMXv;f*VF95C}bHQud4cZRi?$X=EbqW}d~pfILd zP0R?Zv;E6-LH>vRb0bJ@p65Fa;up2+h8jo?xQ~rg5yZXd`9+Wb9TgNsbbww|eYNbZ zXownsF$W_}bfd>#r2FYe=;e33q5ab>KY_)7FZ_nd#X6mipWxFKPR{^-7Q5|Rqqi$9 zD4l^6N4mdxq*(%=t9o8JDFKUgQ9pVhIu18RSH zx?bmW6<~A{E#EFM0{A?XW86-evqszM34K{Y3uex|htM>dm;fiWghdFyz$nD*wKKmw zk@eaqe#XyyCPTjOn17)A2jGgAo8%3t| z-Qj4ww9bh0LeMP^Bsm7>;IrXkf63OiGZxC9qseR|)RMhrSh9*pOi$&Cl z#Ag%wS7^ql*ER^wI@CREFc{riY}3hf{fKNx3v_GHPRZYxV0eOS*F6;YIyW%j3}*<) zg_|AXIW4pPn~7;i!QZk`5HyG)igfI%tly9&jZaf2=zLk=MjJE9Md`g;gEAnWkzun` znq1*w{tM=^j%}>IWqxPOlQR4zV|op}kxQQ9PNm<{EXU$KBCYq~8`=WtG)JaO+69La z{xuWMm~ba;G9^=vto4|vPg7ykekHN}PW3NH3kxM*l7jhq2Qk<=zN~M%*Cb^0uRmU8 zOA1=V>^qskU(2|cGm@*U?Sl?}pwWAFjMTj4h0 zjY|shWJgzD!EAf7E4R&I(5v?)#Ui88J0{29!gqU@DsS`n)Y9Zc>s;-#8biw> zpPWjq=2&O!;_Pr?#k9eoIKosT8ftw_X{+o_FC1o!1^KW zmMUAbC{_c%@riD%g-?r8{Ykk@L|-E&+e0%Hdpmbyok0dB1ktRi*_q z)v{epb1LBz+9ybPa=MrNxg$d?;O}e}twTMIL2@Ig)eR=>d^4Ws*oiG_QUCZeH3fYr zDy$pr^CvW_l~E#=Q@$08g^t%|9DDg99I0Frd6{ry0c3pU zXK>YTP(3%YQvVzV$ile@2ONbO{Ke=qr=QxW08$4wJbf|X71uaElZt>bA_6k6&n7}c zYJ0-tkIl@YYJhK@E>`BA=&+8L2k!0@%s-eR*xNYDbG%zpycR{M@juYHNWH-1=D(Ap z4kR}sbR-vat9Xn?`9OyRf_>}~BACL?;r(d4!|5ooUu2d4{Y=kskC9Z#Nijy(#o1Z8 zAXO7Q+xyynSfwK``^?)pQitjb-Fh&E8NMA1hz`^@!yzHTUY{!0kAI?pP?BEr|C93l zNJ+X$%%Lg)N71V^;eRCzDY`E^d|dcr5r{)~oDqb~k=m8ef$G8_6tq|y5V^bHa#;BB zn|xDSL~yiQp>KI_BRc8qf!Zn&x+NYuxRZTMUr=(Lf#vGdGrmyow=@76KSCtLoLjkz zuP6AUtJ5;ncMJY^|GZV2tY5sEyyB-Ms+CV(?nL!UWJ#ET{5qzrtnQ@2tB=sq#kKSn z8z?WpgT*bTV2pcyJ=J%L&rh#7_FZ)|1;5?owM2@fC0)}_g7IFFHEQs zM5b@ZBF0w5fnb*_uJ4#?TsJT*m`q=U5oUt?jG15jG8BP}7$DfUU2eHlY+5BV2j2Gp ziT)m05N5H93j+UW8^In7q;(6!1c^l~Lf@C4(4g5lif^zZARzE%q!(!0bKXTd?=S3;4mIR68B&%K(n|JA(fj4;6Jq)q zf(-l_)fU!6mi6|>oU|=BH~v_d=C(Cf7nfWwm=aovY`grU9X5=tIrHwl}j!SBTlb|;T^UivOuQA12 z=!d2|3-Ar}sNNLZd3ooF4OoC33TJkkO-TO{)c+S8LI|6+VWmo zw`{>^f>3l}y;bJ7d;SD)Hkd#4E6%>LP`dw_E#3A@sS7drEr-=PP`emnPi7k(ynqgU zOWcje-9;@bNGHeg$dcJ)KkG^~yg50}&h17-{v1@j!syZLGy~#DNy%vHFTZ64Qt$tL&6zu!$zAppe(v|I9qhFVF<=`r>{p2v`S|NN_nC1f&))vh_Wv|zC zBIy%ajfwn}MKEwA0C~WIQ&Y03N9t~5GGc+CBbd2&`E&RXCzahfb3Qe3V=u0)NQh%M zaJ{X>2X6`~omRfB9KXOHHr3RBjU(1z&kZ&i5LqPg_LSN5Ou^nV9CULb^PV6dV9vgB zk9^(R{B27lXB}|RTew67y7(q6OVscQH!^tBnLZW<;4k?%zIbD#$?Toe^!ScskeDgT3QK(j z!8+`+$EvCi9h|1QhpYF^mKsTK*WX3D-=|%Zcbv5isaF5|d7-Up_6Gmp1O|tVFl!yj z#3)-Yg(YrSIko<9erQ>*V+4hK(2!-F(c zCgyF5pzDFJ)4Jir7KN4ZZ!UOhEc#Z zV1b?gYI$7F;=h8}f1_PjVO)y+zsuX2O@R*{Tho2B3o=tS$=am%QE*lP{hXk1Zu}-X#T=yZYAX4w= zaFo(u7p#~BFBW6!?@CE6&QfwxXK*0P846eDT=~Jrjs8X1QKPN{_P3_)F`s-0yhrKH zn*^YLzjpJUL*XR2*pVt>(!1_V+`(!FrDnTPjW(@PNv3;4+5=g`1qe`Ki}+9cidPSL z4oqUC-oKZH)B2TS-*6FJNA)7lk;&O~+E^!IIiXw=W4gcwy( zo>AiS^RsSr1}oYLwdThSCpBn?1P*K2Q!Y9Oz z%w^HWM+YNL1o-ZoN(cpR4@*l|8l@v9P`Ed`rAzWJ^TZbMm!9)A`l?wRuD;^&xYUQU zc^3JWinH!X0&m!5E9KtnqYiq$&5$&FsO4&R?=2d>6n3#!&yo8wH z39UXUV=PLB)~Onc?k+Jq=y8UmJ~8|-f|Y}72<-MD3u6Rr;y1&8|L%>A<+5AM-GnE% zgb^IQ#t_RypvPe9?vRhlnf)sZgbpzY=&|TiWKuq@VVi>op}1Cr(k_u$`b+e1oCKeZ z(Q|wRuUZ}8L6)yAHz~O5gS*4Ek{AnwnA>SB<2$KP=-+C*<8ZD}%ya{r23-c<`fTHd zrJ$q6ga>0r;KKYs!!{!Zn4AXFxtN0Mf+Aj@53g{QVzKe(^UpCZA$JB&+kk&na#aOr z{kqA#?I!7kPH$^z_PY;KlS{=4BE0K?CkJZsJEjTEuxt-gV!O52$(g3vk?_b}cSDUU z2bU4NCl~AjNv!5RP$iV?+_Vr-^ga+(+I3d1_xSoyOZ_SrhXpI|tFPGfMsQ~-FTE4-3gjA=1v zno^;?!%)he{E?Hv60+Q+L{xe6fdsV%RR#w$JM6;uFM9k+E2aSeUHhz#*J6 z^~cKigDDDRwYPTr#00b0D-`jSUJRN8T8-wm&0V+Cj1bAErm@)<17K-q<}Ain=NYhM z@MaYa@hd~tdxr(%gcE>oko$*b>Y*#!=A3A}No4Dtc&-Z!JS#b(<6HVhlpK=3~2{iMRw%<^>m4AnjHyv##S-#_Pb{ihTZpH?(b!u)t(<5 z;mrm*x*jyikYf}DPj{1x-r9y~8f zKC}DUgLUwIO3(IDv&<{C=49MH$U1y5LcJ`Ho39V*pTbn}nEmjY`8^P+Z$glXBW^8w zkDXKNE>=uxKPL7p6fV|Qw4M$BK)@_hG!qWX)!V(mtmFMp#5#?#9_QYi{`znQ;FhzW zAI#4CkbUfBrd84Wkm;@wPaQOmQz0dv&|xIqrTnf%c}r01?@%%RCr$wJe7gvVmgpp!40jfm%1y8!d1Nj6|vebOYn-3N^@cl_+?s=lp&NNbjNrHr2O5s zud>TSfRYW+IaKZFFGtX$r~zCzx0P*GKs6Dw5`sk{Neal0Uj*!ihk;pXT;!ZczAQ@X zV^0PI=%>t!xZq8L^eGF*BeS9l?sG?|3LG;HaDeSe7*-h<-6Y;S|IQ0Y`Cnh8 zh6OnuZy6kKWhKnf;b7Dl-kXv~hLh=m8gCLUUoI`XUzaeK#o0FAO4JZ17%I`(EgdQo z$}ZVA(7T{x4h*vjtxKX)f;9i7pqZcFQ^!i3)f5~NwXDB^-F@3NF4a0 zyHtHZ%1vUy#ggP7xl|pK)$nBrQW0|Ciz1Z$$$6Uw-z^!jMimYsP@5lgWqb9-SFxCv zBuWvc>JaFly#p5oz2~dr9`2(aycbiz>H6`OfC#6e5Dt;vlf+nUQ1I_kw_QzfV;9UP zHEh*m{*Zr>;;-O;Pyb$Ejqbgp2tE0j>wZgPt5>IMp->CH$zqP>^{+*P#cy!hV|3|mi zCVUHRixQn^C~9M2?-gnt)KJt=oGD;Snogx+sWs#!67v*ld+N{Z$pMAzn>Qcnl_7lB zg~J7Z_X_`=^5;0ae&9RfP? zw4gQjnupuHjI--CuMu6Z$2ghXx4J#G5G>vExK&q+4T`q0Mxu&1!PgRczbCW4+FR=N z=hBupe)YQQMq}*Ni?B+dgD$YY9eEMiNR7e0@ylfJo&ezjz1PyLg(MvLCpMk^={&Tr283AX<=!7 z5~Df+aOr1y&H7jX&N8%vd?7x z@{Kw^(gKK_L^Zp?VyHd`crgWyY~!hX*{+tx)-Qq};9Am95P7*!q{X6g3|}F+he}nG zdCCRXUwftlZCx)4X$-4)+1Pq=x#6UbIicg)T0hsKsSDY@&MF?px7hPFu|T8W?4w8dD}Xn#B#08St_> zIsGL0Yr*jRSh0A!ITDhrBCxNEhPtHB_D?#!_)ND z8-vrDD*39CQ?S+H^AdCk8=FLK8L*qrbr!vj4Q?-61%$}pTLpT?qFJ}ns&_`+;zWHJ}5#%4S>y+us* zO)Q>#xJjvon-$`BcR`}cAb$2QGIwmZjlMnj!%%NJWqFSao;sY#5B5GIx=nq!Obs^00BCC@;i;4t`3;jwnL8IO2><`&B+*YS4L8CWN17#;+@9TER*bn~WWe+{{{c3__jWNL(V@qEJx8 zTor*f#3it{{A0Bwq}N2g$m1f4zCTNgA2V5vb8PxKT&Ak~=idEz$d5(#Q9P`+ZLX&1 zSEyBw5Q?5}x4v@&iLe3pLF^^7$0$#7ti`#z%b!+tG%Iv(Y_qR1Gbg>CcuohV(b;iv z6lV5sr4@3m@~$5M<=vzHI6BpAJk|4*U=UYtq;{Ww)X=nBc6s($n}K(~y&ph$#atv( z51mr$nwZ^V6z!R*-JD^5465Ea_TYSnjm}f69GEzJkm@2V)id&49O%+y*C%gqoB+76n!BqemVaHCFgxe4B~T+AVBu?SvxTyIf0^ z$gj5$(mbdK#-B5O`bfAGE=rvQ90s}A)UK1#WooWIt3{=n5AZo6ODTD9^b}G;=AP)({qxWPsU^CJG?lfyOSp;d*f_emipR z8>SfM;)Qx0^HcwY1^6^ymXs^%!vZ*$BIogCWW(|03elPI(KGaIPeg80H*T;duDp6? zaa;2e@g(#9f~pA)=}|O>mzX;gl>PIO<)nC?>pvs(#^N>|N5fLe1dF1AQlp2<9_*#m zke|6Tlo^pz=U7n2WGM4ISUmaM)3wf3X?_S8rvR&CjiLe1OU&2rXC&v|RcJV$t-r~v zxA`?SbqkdO+_KBPbLf^f>VnoNe(>jQ=|)wax%jx|3Fg6mqm`MG2SU9Lm72JpnL!_D4}h0$-?4)i-G6CW9|Y5vf5?B~R=9RHQcP%+fP zTc?N_LD0#LX_-55{>~c!+GVUo6fsN>q#kF&By3%5%x&{%*D55&U z1hj6h-*(=rnJL2MiDmVmfOsTkE>;ZuiiV-Fgkr2%W;{iocWx~Dx}_h+)Ljp%fVJMZ zW$@vh{QT76udh>VCLgt*7Rw}~y}NH%>O3Hd8~|J{Ni3LXp<-9p+)1I|9ynalOIsY0bE235#U-Hf4Vn2rL z;QNlJ*xzo7SBWeHb!)9urTlZQH#ROD)2Y0DMH{l#v_#iy@l$nJHZx&B{Cb zJEb#SDPl`#n`V&fW1P&_Ast9k-rh6fF?R5OiN1%W}0zEUAFcW(s zXiyJ{j75~`{qNhm3VHmrEb@}O_5Kdss2}#aF$iMS)Q-U9g7wwk%nT_i3h8<9n?N%Q zZSGn?=-%eg8280@H^cuKA!exy<&@_& z!4W(~ekwz5Op!>L!D*R(<6|7wMF4K}Qq1s`|~RZuC25*8S4sbVQb=K|;|% z_)%iMn$?Hll5(lGLp{PYWXZOO%tvfJkiiDKbunv9+!c8N63UyJvp$5E9FVdt#`sW; z3+_&c8%w$al7=mxQM7S)>H`f87?8`)-)B*CF?&~T2;LuI8AaP~$m9hmlnWV~aA1g( z@zd%FaQzQIzv-p5Xt-Qy*rH?lU3Yob<)k}AeHmn*d5d=U%o@>3!5R$<%1@6>tHERV ziNJIpE9Xi7BwDh<2Fj1o+7z`1KgkZ~frpP1a^w~gZpck46USBF%W#gpr{dBN6Wb{9 z-IkUSm~vG|1DSaQZn?R?MB*KAL;fkadm9u}uHGr)dmH&yFpWuv-YFdVo#@$_;~RO! zlyIp&sFrgXuC!6PIszCV)@T$^J~2jqm&!&JLZg1-;L`ZW&1f5Q);dLUIcFApN7AS& zB}b5LP&kTrFfLUj5|v-`ev59nZ~4;4yPXr#u@#=)tg}xyCTv^b^DIEI!eMtxw1Xb% z%rt>${5rT9x3eD^^lmO^W4Gw8`KbQSd$M0pDbHdbS-WZD#{f-DAqQM^nRK4xxm~K- zFNp;Y(|_n)4KdgG92raiNI=$72UY3T zzzO=fEkq@DxkB2x#j0ezuLHVWKwv8UIzKm%)c; z7bJeuJ)_IaC%#IJLtx(c5tbj`u>wuaL^w?#qsjVI;!J5y)Z-g$;VfiIah-H#$c0^bN>3l~Z7g;Nm4U!RH=W;4vEcOf*|{ zt=GlRmm1huGTbcJ_h7=8Lk^)`-FQ4EQI~xD_GRJ|qCm&PrOD(wxj;uvex1>T2=4n6t%6!Q6W$MPR8%93WlE3p zR3 zO((OzN3)Q?(Djkpjn|2Id9N@p-_wV8@WS0blQopp%B^-wYb5s7@*urf%F$K~GD4Y= z{+Nm{ZeQ*PZ3!glPd>Y!C ztM->E;-mBP3qP)%^9--p&%^FGW-GO5!Vb!pJY9z~4Z0F-M$^8-yO0!q;!*FiPZ}sU z8FEX?)%7LuzRN*r4VcL0(Ro~Ch*wgjttVdRi}(tbi>ZIELm%U8O+M+jVN^D zXrN!^OqvYr|JF8Y0gfiGNsU#@X;rTXU5O5&k{yq_{h$~c{zOSwajvp=S%-rjDcgUW zSU0d8#uR0f+S7+Ld86X{ir&FBiqI&|H(!T;o0Z`}lz)^^dFSpYwlPdm+;5p>DA|bu z%90`FI_EFWhS+^1$-=pT68>`Z6|~(z%19tkmAMG})th@3M8{YM%4c>9vH6_`6xV7) zEyF`7fubDo;8T_Oqi>J&vj0gzTa{#fu|QO=nQ}7K-o~hWoP)*Qo1OdVx=LHjw(Bl$ zb(mCF$pYcLuB@-?MR9jI8x&HD!k@5G=6O(r;Xoj&NhYUN&X4_N;bLjwk3QLr+{s6; zPa${vxcp^P&vbY@SiS)SCv!q%>_DO~tifSz{x4+3oM2`C!nze7UMo3yD8+{WJ-je# z+2TgTlVJ4|L5h*fWYZ21=#<*gol9z%5^AcWFy1(w|BN42L_h)nB`MG*YYH6+vD%X$5O>3{6;ELF~o9a1JC+?}=x2 z4(QTL|13#`rj!(PNzDGEUK{(T0Aq9jg zl(BaL>dKQ#iM@MZQsx|u8icVh$#hFR>jc=mVhUR|o%>^XL@olMbfw~ql1zJA1s$iY zN9%Suh+SNTZs(hfZ!v7pZCRKGh!K`FPrZ~Lik8>o<=MQhmpDMpl`9&t?j!!$hd^Vb z&ZWKeatj1Y0FJ%Y_n-_HADV@}74ex?kOAr`fh)BfvG@BK9XUFm(!vj=FlA{EhNn!w z0Bf=R_vyh(Aopd8YU)Q67`T2mVDYHKK0q9`KKa$~M>wL=FfdZ+mD zXr0eaXp1dT6*lfZ$lEP_diFKwl9rVGQzwZa!UjWlDgS6I8TxVI-NoFkq{N?d2v)fz zZz7Urp9{(;uy=l`&Vus!cEW30_xUd2;%Ipu#}>dj9sBEVIT0u9%B0GulT0(Gr4FGr zX7LLKd{70dFn>x`k8HoNTqshMz*6S079nW}U)3tDXT%2(+z$pD{fht{ZNt$$M~+X! zgRPmKlzj&N@(~!0@rYadU!J`A>*HwL_3N=&suIDTSNDRt=mG-P<)~$ZQDUN(f=3h~ zKXwy?N290~d~DuoCNR|7H$S`4zCLJ%t(K`^c&%nlu%0g{p2n!K=*&b^k4o+nipM$( zlz2;wZ2E9iZ%7L)=F4ptjS_ARv8kbnzJlXbB<*w0_AO!)w2jPGw$^X>`4I+Q<33Zz zQqxW$e&6Wtmd>^4-|e`0iPE|Vg{ytwsbk_q$Y*xkU{E7rloAo_TT5Xml@?o31=AHH zW9SXKSGaiuTvdGreG5fV*K(Z5n|bv#b>R!(ojFjBF-`s0AAtbzJ(ZY@_u6N)Mav6Np~_eWT-F?3!1^sjdC+lkm^yA*`_ z18`7bGzy@VbMacUM(O{WJddAq+>pw4j>d&BJJ&lMleXbNF03mBJ}`~U!CM=hlUEqB z!|T=TxxJ4V_w8<5hi3b2E&-`%n-#1r#*$VdaNuMHNqjX{XjTKF3MzjQJDZV7dua$_ z$?`U(Zeuk{10#p7DGH|t`1MRH0A`@(d^X&msN|)G`s zhYSBMQ>Mb-XjaYU{J~%EJLJmxceyICuz5yy?Umic~E zXskZf!?(oQBkwdT0*dR38S%D$_@C*}KtOf-aRaC)sMywPnTx#y2MXiNEH^gGSlU4p zonuc{up3CGNo9fvg-O@DI*9D|*lZ8)DoqII!K#I^Y=h_iC-)$J9(rl`&X7O?q5Ff^vZAEh@$vUVVkW8^_GjDfOZFpBr!D#=y5if ze9y=YD2Km$GE|AGhbOP7CD)nR`bMNLEl6lm4Z#|SNqEx?27*}easE%7(9}amSTF@m zCrT_#{4C{Ktepfcrp$0bW}fKK&!vJuC6853Sf*4nnIjcAPFOm13F6_8N69_}Y%kA(q+y_dbr zUE!(d_@aBYhfw4gAR(DEfJWKeGr)!m__zJkGP3V4`#DDC<))*)}Tn5tN`qxUH$^R54Y=(^9RTypCFU{ zKr`7}%)&|z0X}(&`&udJ`wu_ZpffHMuwd-g4~;HoK8F$|isCTuq61UDDe`_Oa>Kc3 zc*M%f%km{OK~?3TUgFYhDnWPvv_!wmv^dp#J&Si1JXISxis%J1ykz%&-OXr2LSbi| zE_lfn{wF3+!tikgqt^P~TGw})&iwOoqV?fgl6^Dkz4C+9Utg9z zu5bOPRXMZ?QC^B|@9Z;`Kk6Bgp_HL~p%1(2#j9FIVFn6339??#-&BcSy}+pDVolCz z+mGLF`aHec8%B`?KZYTIchk9wtU(-lh6jXsQo3oe6_L`xK%=U^{D<00UkJl^uS)aHlwS3+pRm`A0tNt2plG6?Q?d=4@o_^+b0442+(3`ieLP^Q4mTH)9{Ragqz+bUX ztWXmr_#%7eG1&M!3SCd+mIw|^6MZ3ymr(jwbn6i8xt^~B2j#@N98;cTtJ1*IX51|fL~EOePPPYDX)>DT znOY29v7a5Q2B)R0^geiEyfhWM43%Nm-!cxHDrc9C3O`=WBk6O%iKOC3eKsN>v!Qu| z43^3qg?x&AKjxfJ%Amj%+*5@|;sGR`1*JBLBco2S!z2;G4RcFcL@JZ>H@LI=n_u!ASf-8B!L=R)y8+NtNQr*2DPN9!1YEO7NZk{wRtB!Y^LMDx(EoW>5BU zgM)k~hp-{VPr(9m3vmTGp+FDgm*K#M+)JUrOg+dxxH$gd+E6}HaenUxZ4!o~UkayC zA?ML7x+3w!!rcnV8j)^QMrk_^>Bt>2y&e}VfBCCyvM4?0LbY=tRj9?GBS$rosw! z;mH^2hQw z!%Z&^-g`C#a5Yuqt=z$cQxL%cC5hRrW{~fk8Ta3=5QFiaF$%l}N+zv<&)ABxoGw=B zGYK%s;WQOxD9*#w*ihl6WjUu27_A#*Sji^vVBw?w1LI$`+58%GDLJ5C?g&utDrxUO zx&i1055(T9n-@h2r~}0$m^ajNd_6RCF$Y}dx30Ixs(Xf$@yfPxu#$zfldzymt#zdH zxe(OU)S~<5fa=Ygtjt+2XJRl+%64a15#@~9;WZHXgD0bwAXvc&j$Sbk#_>yumKfR| z`u1#U8#8^fM+r$6Kv{Ww>%1T`os5F?Vv!~P0NvDthSLanI35XrP`q6%3e0VI) za=%_)xva5J=$9GivM1?nY7}kjWZWpPQ?4YS=h4@HjZxfnTX?Oe4Ng6nUf?6>IgarX zQIws*S@5N1@NxDfVB;bDNfP$e8V0nLWjl@eDNSwWMjairk*n2%^;L|}jsK$(0u$RG2d7Bx6_eC)kmI$; zBju9eL2!L5eZm>$kN1rzxL2?yreNLvJ>v=(zJH>DYs}MdU+5R;NIFQ^wl5>vlqf##^qGFBC+CF#rw8wz1cZxPaHgQK0 z>5hqF^9pA zfhhpG+gtYyT*Sv-+^AMz!2NN5{?9=q-PcDn$IFn@yz&;3km2*;uFR!|oO=`Up8L$C zr>%e9%1Av*IcYBQ{1#8*!S_3H{6dz~192gA5wC5xe+H`##_B}|K)H~n*3S!C0NLnI zVw_x0lOK3w#IA*Gt=Ix)l}2YJz51V?(-^cs8?Y~$(^!}fxhf&~L-9@#LCUH%zU2M_ zI21*h$$JKhx9_#4N~aMUlj0!uJaRM;YZdBD(jN#j2-6FZ1*1Y-Mj9(BD3F{^^Af=1YL&t5R6~llz!N zr>i^632g=VsIFyqVf0!rbfNC779J0RLdyh@uUT0Lwf(y0P1beEf}7I&s8 zhuK!|BSy11b)k_zreI^lh;gEE7$$>DYF(kVNZv$P8xPr1V&3_h=?sP4K-^vng zKEfX>DWYsaHlN<%@77!=V%x#N6CeZNCLiB^lwYPAm; zRA)@#FZqMRK?Og5Hz2(g!c%=~Bny%?_fKw(Um0*%GQT(iVrorY!J=kISZ0<0zmlE)rfJANy}D-%j4|+wMZw zhS6yTSR*CwTQZr@Lr=^{tx?j_!*jhY17xNYKmHj4)7oK;PTEEnR^QA1T=I$!q^McZN=o4fE z5#WVk_JY+-s%Y17^g2(n=dPCYv`sDllp*jTFY;!}vhKfuAwUxd|Kz}>%cS4$z#fM_ z-C3cTU;UR!@n%h)_3=F99Su2*0zI|!8loUKJT0b+htx7hXw$+;@D*_3-1O}HdePiG!)99R+)M2OKZHp_Vjst z)Y~Qaocpcr;i(V2MOXe0+SswR?%A;xs`)3&6gl-BGy>02Gr6&k;Zjq&Lh>BCw__)S zF*3z%g12oSPMQ+(UF5^cKI4#eB07%~c&15z&+pd4p^7Ab*TyWm@ds{3NxmSF+h`<) ztU?t6ucYsZU$S&5YZ|h<&1m9hSd?HX zB)KQ$pCBsC-Dc+O8U$d!n#F|~$dzB7Lt%nHtfkXB@a?IFOX06zh%%DE5|;4J{<))Q zX9xpB#40ZQMTt8?{N~1Hsaes7io88hinJdB-3}+rQbeeP6!z0ctKaYJo0CL<_KxEzG`q0px9U9YR;z0+8$cRlmXI&P3V15o0F*Rd27w zA$jGsU2a94!PmfW!ux1AOd?k~ZL@zUEQhUAkv(zQSBdxQeLXp2GXj(HMnsKDn(&!y z*vVM=N6N^e;cdid@93uC+XaCnMfInhG>G5HsO<9tQKdUMPLH|NG3FdX+}D!cZfR7M zIh8-b|JK$1<#g-gFZ5B$^1q*ujkL;)@f9ZPrQEDaLKm7TN+rrwjSJ%gBqc$4RUcu$ zwaqg`hf&6GhXyL^v#@w@xf`=cEN54Jf@Pz;*>b_Uqiyaa9%g zpcItgkgFU1NZcQa>M)pVEnVn4JVqW;J(1GXFDnQ&qrvjm3qbl-~P^ zx)(I<`1%{W*|NR^M4d{rC%sSJPEZ)GjKY)0m}&psci`hZW)KR``Q`-HU`600DiEkv zQ&2_hv8XxVf)(T!>WhB7fEK?;k<&vY=kxzT4{uzz5C6gW%-Pt|Vg;muEfb@Ex$yb> z`)(JGl;RcbyM_zk-|5bL)M#uf+-wL7ihRk#;s@!qlmFogH~+}nO#ukLUxiJ3NYdfK zNaK^tAG6H%bB_m5;^2~+KT(qud)|B4zdTGD)Us&iKfXV;%FY15)?_HVvvOaAjqEG3 zm|4uE{yeZZEZ|Vq|GGJzq*qY*AbP^)@J*55N}QEL1n|rpyIohGDqoDDk_CJW9sGy- zTMJK5;710F0+`#qmz(TiCHA+x^chjwi(&#&O#5zD0O;*Y5u zxZo@@?EY(zgXISih7|Hhgmwa+HPrqWEa#gSF}U>A%p%%k=cXB^^Kh=VUb$VLIU7cOj#=V&8zp zUHqH=a{kK=0GReq;1;=YNm40Wm4Gkan++fS4TgJ37Q9@#rzn}Xj_O7{lUU`A z5LwBmPULfQblFRD%GK`)WU!a z`%}frw2!l~)Qa0lCh5Pt7_}NJ50@KcpC5rXMI1o2$gOwZ@pzS@WjDsIP-6`AZtaUq zJfy%Ka1Psk82pi?>NOHI#$$#6!F0LDN{3em>Gf)x+lV(n0z;rkhV$R;DN^H&-J4*i zJXgOteiy>E4)OA*Yvm&PjKXCe+lO3$-%I7mV^(>pR$uydzd(08j=aR)!;OO>jt5|! zIA~r$l~IPApSd(L3*G<<9R7?reD@O}AVt?JMk|0;Y@85d=%;GF^PB)Y2(%jc#g_j} zF55Ne((4Pjb>psb*%jc6I#U|USE!ZaPh#H(D2{JU9)SI1EN1=g)H;pcN}k2`ldc@r zYoCvrui}9%OJehH=ySj^zSeD*@!O+G)2he>04*ue2acFoIaC~e*NejKp%_ts3z%Vl zsjlP!VCkOf@bX$&YO>qg4(B=k+qwfFtZ%*hVKy?%wRQ76H7OAN2-;h&mMrpYcZRE7 zFU=?J5}=-l2ZI1PDI>tqmdOT&ssS?GT2h4vF}B}izb53ItYQA95EYOjmTPpLUytw| z7pz0brhv<+ON&?DA>;Y5Z-Tc}(K1u0@MXkgxD*kfy)Rc{R=+$jZrb)@h-&BK9&VOi zHe4R}IO}{&GCKR+Wh>dAVhGSLvKaOX-)@#zlZ|*>i-isE`vYP=SQEK?W@L7-ilU#j+{K3@)jWdB^&5bH@b*i)wtJR z-+f`Xre)~RTIH@3*YVmaWHVh9zd7_$e)b_ysDSM;x^5S;ExO}<-fGHKZMd>sshcPB zC8!l_(Q(t3plQlP0XWWX)$;FVG98tZS|9;0Oo4NyiH7U>;4lf6zkTK6b2q7oBXxws z3DgV~U?>C7vujAs`A6oP=Csp1#$x_pe|uihksFKud^StK1`TyBYCu|Vjo69R^}JP{ zKtB7xb@Q$1v0l@D5$`6;<&&O7}0*TOJVfQxB!zgjf0{k#5!S}xN& zFdh)#Hw&UVq4?|)7KdlMW3&Jrehk1q{JR;30VaT1sZ2xJ%Vd4`=5*@}YB#ap zWRN}-)DHksYC8lq8QR21DK-Lw^vWq|22Z>mvOeZX@96|fk@MZ5PZJQLjJ>)QvqT~z zo}0z#ef@j!8!^3-%rj5M63iN}2qZInkMm{f+%w!Pp zx=S52{4Tto2z38Fwl(~`W$UsxsnV7tu;%uooJ5RyivYUE)g<{v3ZQkpbhj>D}|4O=78!-*b~^#Sb+gv zxNxKG?3Fd09(J+aOuN=zKH@Q~rPHYz2R)f>=}f`sNBa;Q49Bnrotz`rtTGa%n`M7| z`++QL1y)$B!djQS^V?Ij@_h(Z8|Z728((ba>f$F*wE=^edJUA4Abw)b z!oTjT?Kt@wY)e1gY=;TuybGeaCf~Bq$d4z1`Kmgl66Y^i@upKk4}K0F|ERFtmg}wX zNmKJN*S*8S{EhwH$v7aGDL3a60u^I#VctBKE0@GuLL23ui6&tI-u70&vo;e_gnK^T z4Xrp)!f@`s{{QK)%NsXQbu~?e+n??%R^s;~@Rl}uf-ZNZXw<87c^@yP@k4F5#S-TM z`xbwCxOI4j#UB-HfZB$*U(Tw!Vws$CM1JoVP3JjjRlE*4DI{@*@AQW4gi*v!P5^fN znxg+2j-$;3UOVdAvsP!3fCMk4(uulL6vPcfHEJqi2TWQbbl(X6yF$_oEJ~TidNe3( z2!(tGEG9oI1glsVARms#XE!H{;)A$NOM~_Wa_^5@WMwk=%K%{l&1#VeJiCxkSQgCl z(FamSU^kd_#A8UD;3t$`GH523*+Ov*QZgcI|L-tlDy~+ywADd$XeJ^I9n43L`j?%H z8M6eu*5{Ugu|!@+CY>ixIS=O7awO#`Mm*sg!cef0=GGC^izfgC(R44&AJ$U<%k%6~ z8vjbhrP==}?aafW{Qf>Zi!HK*kRhaOB?&VXF$g7NCrkFy2xDxC?7Ko~QW(p~PAFL> z*`ib!jcheW!1|_qon_y>%gROTOas z9rwamy+?w|XhH|o?5$X79a|Mc1n<5FxqKlEjHjVnJHAy)H{x=$GQ}$QzR@}hLkk&Z zn@oJP^AT-czKPjxpN-ahX20k(FC%aMP9LeaGxE_EcpvLAv&QW79tz<{afxoS zAC9hyT23DS5b}M2w#@W@*+hr5SQ)xLxb@@xgY%*|UDRL7-xC*|9&S{|qAfw!6wWf} zoNh2%N4Vw0RIzgc*gx{|hI)cam*r!2Rw_h~I~(=~vd6M{dt2w#XNYierUVM(`{(C# z$p{=3A^tz9*9sdeZ)N&F=&_$r9Q{fOiWXyt zUHQ8@ha4NOWFh@&^BI!v2G05h+tyXqN7}1aH!KQ(t;a|A#Eo}Ypm_T&7C45dhM3E~ zL+@s0nO;ldBWbx-*JrC&q3IKAKv_Y~QBL9?k`Dv{xI z8mDj^;&+;uaxils?VQJyf^BCG_ivuW;;jSfo{3|JEt#(#Rhw3sORI8;=FnTL8u z>Do+uS#=+Z5v?Y3a6+}HVB5e{$Ic{3X?S0>e1V&U5~BIL*YGLL#tDX}i9;^K0bl~D4skqKKBD&IflJs5vAqG!Vb9ub=5 zW_th-ngt`m<87-gP5YneFL9)*<4Z7JY64>?H2~d_se#;XCqDAcFxpzoaGBt&mO$hc zRrD#m>&x9srxuB~Hm*j!xls14{^b}q%G5$ld zCg$ThEIyzp(!yRfN2Q5)gZfK19A86gQ@ccpIF~zr=3?RH)1@x`#uuh~{awLUEs`}< zV)89RW*Fuq@?}Il>S@{mvWFM*79d#{uyF1f zOTGz&V;x3anCHzOj#37lTls0-b6)IMs8GFee|mY-1#C9eZ~8>~-99*rQG>DM!Xa^! zWf+&Xvl?HlM9R0P%4df|n-;h!<~PdD7W;#bueB*WWS(^L$QAm}Xj;JA+R0~xYd&Q4 z)#=VcgDtwm^9MsuPXTZ4Ik)SoBeG>>J>ze3zVB$E0+dr$FRHbl^DTrJ<~RGI{OhBx z0(!f$;MM-e7`MhZIn>Q9Vi}$Jcc)GP2N3n=Xae$zNOL3=4*aiF5WU~8reLVeVUnZ!@U?^^ z(|5a*`}e4f3H(`@XsK`XpRnH^bdQGOK0RFtfFAq#wOA)l@^@P99zFS;K&nD<7L;pV zhH-$bZ;ma49PHv%O&+2{>~o$`zw4|~4QcA*dr)ZphVp`qr3v5Q{R%2AHx6*mXg*!& ztSi6+#wqskUzl(>YcedfD4ii31(D4S)3nMNNM8_egleY~Bi{*=&KZV+zV-tX8IS4S znvDK2g2EA@U~owaE`vbksT*!%i=XSS8k=|1Ph9_J?~=tnUL4*XaDd53{cJTCgF>6= zW8TkY00#~KM56X$lj+xx$=u~umxGa0(M!(pRV7&;h5c#GCWFztNh4LRR&XOSew7x7 zDy=%Hl|_X>Wi+xa_5v33wyWL~Ez|stS5{&YhXPgn-q124M7SzXtdKkoSqVU3?)NN~ z7A^WWtAf|PJA8XQ9XrD-1aTdImbn?~A!f);mIMW}!rK&j7A)|{{EMEZ{s)P-cLyR`KM1k%rJbSBAf!m)0Z{U2`>^zz` zire9ofMDW1CDvbugbb;zd)IzWy^@TlI!w#@O)&vGd|le`t;YdEW*ovi5NeG+2#G82 z+k?tk+Z@<@M#|Oq`e*F;s_1b#$2V~y&dV@=wDK>H%h;A2#VfDcNTJ>IcVS)pyM{Rj zw=zTHl2c!}Jo8MV-UsP~2BnEVFOF<3Cq{w^L`Xy`4b)=)dNvqc6n)bdK8S*pF%E;Bc zc(=rr(kFQ%&h>t-p%f`a*Gc^>9m%Kubv!cN;G+ZxQ(a0#$SRbNmac z?o}RL8^T^muTR;MnsY_ol1j~TK9z@PuN$4z$w~StCkNdR5%Wf6-VQ#)=U}Lj`23g? z-+-6j2v!a&(&gdBZHAApvo5>6p=aAb(fNhP>wFHu|I^M8Y?CHZGl-``H`0dD_BDa1y@y(7MSCE_P9Y|s4r%b3dx`SKqBY4mIE6=7GUHhG6@8@3ZrEG$A z9Lb_2_l0rvrp~m+Rf=`iF0cRY+TpmI1mXjsUB#k8yK1`g*I%o;T+@xO5W8>twl?iU z^YbT?2!syyv|^e1ri(W7jL5DISiD?L;1NUGd0RorTEO=!woKwDRz9#s#~FPwqcM=j7tq}J1VPF_jx$Z2 zqXjLDa*EAh;-2MT&P?p9`@xrlX~(ZbWe1TnlGeaa^*m-`b1-pOao}uDDXx9po}pZT z@8j%CXyyzS$$XJE4nUb3P@%v&U#Y|e4a+zrjGy0$M=DvpA~%#jzt}}xk$h-DM)FX8 zz53+2`wrvpeNg$$_r~XnV(KtKyI}}u$W5LTYH`9*aM3`WyVpMHS!Oc^rDtC6GxUHI`cg|F`&!RyitaPYlZx}T3?i(JNO|~R zg_W&J&oB63E6H%*(*3gjaVxY>A7s{P(LZjGWzwdq{*k?6j;sZ z;)2KGT1&Vdrd9P(^o!ZnFLEIzwTVPyYG(MOSp4eo=HGWd_H^mRuw7z|*iWUTm({L) zo5PrPgjTu*Zi{?u@m<&$om*cuBZ})Z*LSC#hAoKMMGPKkXDOVmC*b-s){>d|t|c+{ z55ranP;h3y z5blh0dP5;cB0?Mn3V5#Y@t{6m5z`bE!by5+-v+I$I1-6Ug!IoTc#+_`)kN1 zT&14E$C*epK=*@WCrV=R8Z#qr7aYk4uGy;`0d-Iy397{Mofvew_o{v@i1QhgAagnS zL966XnSbMmLZ=n_4ts6L(O>7+)bR+(TH2w>Reipz1|WeDH3)QPKuMN9aB4j;LJZfN zn{5hmQ8Qxk!xSaa1|u{%&{0Sh#A%Mj6ENQThM<*Pa|wte1m2~ce4i7=gHoTXJ__R4 zf`L~5qP1C~$;up!`Jg)*ygVok^Qip7Bx0ao1(cn$;+b4v(EXD!i+*6gM zUm-YBK`WosB_&Yj9C-KG?4-9$H)i0);$L~}z<8?LkZM3MUZ|=fI<_kvD4uR3Q*mwD zJKwSD0jM*{!-JYkH;{7%GC$({|H~XcH_iMoS^sr*cs!89*7U#BC9>D&D#Sngs>)hA zyH`vznHv#XG3@#qlnu}+NSpvi$9HKphCoM{WtuQgo7hbb#9Vr12v*!15JDjrjLgr~ I8Mw#)2h(z+`~Uy| literal 0 HcmV?d00001 diff --git a/modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.xml b/modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.xml new file mode 100644 index 00000000..014f0b47 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/docs/_static/sagemaker-model-monitoring-module-architecture.xml @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/modules/sagemaker/sagemaker-model-monitoring/pyproject.toml b/modules/sagemaker/sagemaker-model-monitoring/pyproject.toml new file mode 100644 index 00000000..ef8db5f9 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/pyproject.toml @@ -0,0 +1,41 @@ +[tool.ruff] +exclude = [ + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".ruff_cache", + ".tox", + ".venv", + "_build", + "buck-out", + "build", + "dist", + "codeseeder", +] +line-length = 120 +target-version = "py38" + +[tool.ruff.lint] +select = ["F", "I", "E", "W"] +fixable = ["ALL"] + +[tool.mypy] +python_version = "3.8" +strict = true +ignore_missing_imports = true +disallow_untyped_decorators = false +exclude = "codeseeder.out/|example/|tests/" +warn_unused_ignores = false + +[tool.pytest.ini_options] +addopts = "-v --cov=. --cov-report term" +pythonpath = [ + "." +] + +[tool.coverage.run] +omit = ["tests/*"] + +[tool.coverage.report] +fail_under = 80 diff --git a/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.in b/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.in new file mode 100644 index 00000000..e338a527 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.in @@ -0,0 +1,12 @@ +awscli +cdk-nag +cfn-lint +check-manifest +mypy +pip-tools +pydot +pyroma +pytest +ruff +types-PyYAML +types-setuptools diff --git a/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.txt b/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.txt new file mode 100644 index 00000000..7a1ffc7a --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/requirements-dev.txt @@ -0,0 +1,233 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --output-file=requirements-dev.txt requirements-dev.in +# +annotated-types==0.6.0 + # via pydantic +attrs==23.1.0 + # via + # cattrs + # jschema-to-python + # jsii + # jsonschema + # referencing + # sarif-om +aws-cdk-asset-awscli-v1==2.2.201 + # via aws-cdk-lib +aws-cdk-asset-kubectl-v20==2.1.2 + # via aws-cdk-lib +aws-cdk-asset-node-proxy-agent-v6==2.0.1 + # via aws-cdk-lib +aws-cdk-lib==2.115.0 + # via cdk-nag +aws-sam-translator==1.82.0 + # via cfn-lint +awscli==1.32.0 + # via -r requirements-dev.in +boto3==1.34.0 + # via aws-sam-translator +botocore==1.34.0 + # via + # awscli + # boto3 + # s3transfer +build==1.0.3 + # via + # check-manifest + # pip-tools + # pyroma +cattrs==23.2.3 + # via jsii +cdk-nag==2.27.216 + # via -r requirements-dev.in +certifi==2023.11.17 + # via requests +cfn-lint==0.83.5 + # via -r requirements-dev.in +charset-normalizer==3.3.2 + # via requests +check-manifest==0.49 + # via -r requirements-dev.in +click==8.1.7 + # via pip-tools +colorama==0.4.4 + # via awscli +constructs==10.3.0 + # via + # aws-cdk-lib + # cdk-nag +docutils==0.16 + # via + # awscli + # pyroma +exceptiongroup==1.2.0 + # via + # cattrs + # pytest +idna==3.7 + # via requests +importlib-metadata==7.0.0 + # via build +importlib-resources==6.1.1 + # via jsii +iniconfig==2.0.0 + # via pytest +jmespath==1.0.1 + # via + # boto3 + # botocore +jschema-to-python==1.2.3 + # via cfn-lint +jsii==1.93.0 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs +jsonpatch==1.33 + # via cfn-lint +jsonpickle==3.0.2 + # via jschema-to-python +jsonpointer==2.4 + # via jsonpatch +jsonschema==4.20.0 + # via + # aws-sam-translator + # cfn-lint +jsonschema-specifications==2023.11.2 + # via jsonschema +junit-xml==1.9 + # via cfn-lint +mpmath==1.3.0 + # via sympy +mypy==1.7.1 + # via -r requirements-dev.in +mypy-extensions==1.0.0 + # via mypy +networkx==3.2.1 + # via cfn-lint +packaging==23.2 + # via + # build + # pyroma + # pytest +pbr==6.0.0 + # via + # jschema-to-python + # sarif-om +pip-tools==7.3.0 + # via -r requirements-dev.in +pluggy==1.3.0 + # via pytest +publication==0.0.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +pyasn1==0.5.1 + # via rsa +pydantic==2.5.2 + # via aws-sam-translator +pydantic-core==2.14.5 + # via pydantic +pydot==1.4.2 + # via -r requirements-dev.in +pygments==2.17.2 + # via pyroma +pyparsing==3.1.1 + # via pydot +pyproject-hooks==1.0.0 + # via build +pyroma==4.2 + # via -r requirements-dev.in +pytest==7.4.3 + # via -r requirements-dev.in +python-dateutil==2.8.2 + # via + # botocore + # jsii +pyyaml==6.0.1 + # via + # awscli + # cfn-lint +referencing==0.32.0 + # via + # jsonschema + # jsonschema-specifications +regex==2023.10.3 + # via cfn-lint +requests==2.31.0 + # via pyroma +rpds-py==0.13.2 + # via + # jsonschema + # referencing +rsa==4.7.2 + # via awscli +ruff==0.2.2 + # via -r requirements-dev.in +s3transfer==0.9.0 + # via + # awscli + # boto3 +sarif-om==1.0.4 + # via cfn-lint +six==1.16.0 + # via + # junit-xml + # python-dateutil +sympy==1.12 + # via cfn-lint +tomli==2.0.1 + # via + # build + # check-manifest + # mypy + # pip-tools + # pyproject-hooks + # pytest +trove-classifiers==2023.11.29 + # via pyroma +typeguard==2.13.3 + # via + # aws-cdk-asset-awscli-v1 + # aws-cdk-asset-kubectl-v20 + # aws-cdk-asset-node-proxy-agent-v6 + # aws-cdk-lib + # cdk-nag + # constructs + # jsii +types-pyyaml==6.0.12.12 + # via -r requirements-dev.in +types-setuptools==69.0.0.0 + # via -r requirements-dev.in +typing-extensions==4.9.0 + # via + # aws-sam-translator + # cattrs + # jsii + # mypy + # pydantic + # pydantic-core +urllib3==1.26.18 + # via + # botocore + # requests +wheel==0.42.0 + # via pip-tools +zipp==3.17.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/modules/sagemaker/sagemaker-model-monitoring/requirements.txt b/modules/sagemaker/sagemaker-model-monitoring/requirements.txt new file mode 100644 index 00000000..500e6fd0 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/requirements.txt @@ -0,0 +1,5 @@ +aws-cdk-lib==2.126.0 +cdk-nag==2.28.27 +sagemaker==2.207.1 +pydantic~=2.5.3 +pydantic-settings~=2.0.3 diff --git a/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/__init__.py b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/data_quality_construct.py b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/data_quality_construct.py new file mode 100644 index 00000000..9cae0694 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/data_quality_construct.py @@ -0,0 +1,102 @@ +from typing import Any, List + +from aws_cdk import aws_sagemaker as sagemaker +from constructs import Construct + + +class DataQualityConstruct(Construct): + """ + CDK construct to define a SageMaker data quality job definition. + + It defines both the data quality job, corresponding schedule, and configuration. + """ + + def __init__( + self, + scope: Construct, + construct_id: str, + monitor_image_uri: str, + endpoint_name: str, + model_bucket_name: str, + data_quality_checkstep_output_prefix: str, + data_quality_output_prefix: str, + kms_key_id: str, + model_monitor_role_arn: str, + security_group_id: str, + subnet_ids: List[str], + instance_count: int, + instance_type: str, + instance_volume_size_in_gb: int, + max_runtime_in_seconds: int, + schedule_expression: str, + **kwargs: Any, + ) -> None: + super().__init__(scope, construct_id, **kwargs) + + data_quality_job_definition = sagemaker.CfnDataQualityJobDefinition( + self, + "DataQualityJobDefinition", + data_quality_app_specification=sagemaker.CfnDataQualityJobDefinition.DataQualityAppSpecificationProperty( + image_uri=monitor_image_uri, + ), + data_quality_job_input=sagemaker.CfnDataQualityJobDefinition.DataQualityJobInputProperty( + endpoint_input=sagemaker.CfnDataQualityJobDefinition.EndpointInputProperty( + endpoint_name=endpoint_name, + local_path="/opt/ml/processing/input/data_quality_input", + ) + ), + data_quality_job_output_config=sagemaker.CfnDataQualityJobDefinition.MonitoringOutputConfigProperty( + monitoring_outputs=[ + sagemaker.CfnDataQualityJobDefinition.MonitoringOutputProperty( + s3_output=sagemaker.CfnDataQualityJobDefinition.S3OutputProperty( + local_path="/opt/ml/processing/output/data_quality_output", + s3_uri=f"s3://{model_bucket_name}/{data_quality_output_prefix}", + s3_upload_mode="EndOfJob", + ) + ) + ], + kms_key_id=kms_key_id, + ), + job_resources=sagemaker.CfnDataQualityJobDefinition.MonitoringResourcesProperty( + cluster_config=sagemaker.CfnDataQualityJobDefinition.ClusterConfigProperty( + instance_count=instance_count, + instance_type=instance_type, + volume_size_in_gb=instance_volume_size_in_gb, + volume_kms_key_id=kms_key_id, + ) + ), + job_definition_name=f"{endpoint_name}-data-quality-def", + role_arn=model_monitor_role_arn, + data_quality_baseline_config=sagemaker.CfnDataQualityJobDefinition.DataQualityBaselineConfigProperty( + constraints_resource=sagemaker.CfnDataQualityJobDefinition.ConstraintsResourceProperty( + s3_uri=f"s3://{model_bucket_name}/{data_quality_checkstep_output_prefix}/constraints.json" + ), + statistics_resource=sagemaker.CfnDataQualityJobDefinition.StatisticsResourceProperty( + s3_uri=f"s3://{model_bucket_name}/{data_quality_checkstep_output_prefix}/statistics.json" + ), + ), + stopping_condition=sagemaker.CfnDataQualityJobDefinition.StoppingConditionProperty( + max_runtime_in_seconds=max_runtime_in_seconds + ), + network_config=sagemaker.CfnDataQualityJobDefinition.NetworkConfigProperty( + enable_inter_container_traffic_encryption=False, + enable_network_isolation=False, + vpc_config=sagemaker.CfnDataQualityJobDefinition.VpcConfigProperty( + security_group_ids=[security_group_id], subnets=subnet_ids + ), + ), + ) + + data_quality_monitor_schedule = sagemaker.CfnMonitoringSchedule( + self, + "DataQualityMonitoringSchedule", + monitoring_schedule_config=sagemaker.CfnMonitoringSchedule.MonitoringScheduleConfigProperty( + monitoring_job_definition_name=data_quality_job_definition.job_definition_name, + monitoring_type="DataQuality", + schedule_config=sagemaker.CfnMonitoringSchedule.ScheduleConfigProperty( + schedule_expression=schedule_expression, + ), + ), + monitoring_schedule_name=f"{endpoint_name}-data-quality", + ) + data_quality_monitor_schedule.add_depends_on(data_quality_job_definition) diff --git a/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/settings.py b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/settings.py new file mode 100644 index 00000000..2f328ba9 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/settings.py @@ -0,0 +1,87 @@ +"""Defines the stack settings.""" + +from abc import ABC +from typing import List, Optional + +from pydantic import Field, computed_field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class CdkBaseSettings(BaseSettings, ABC): + """Defines common configuration for settings.""" + + model_config = SettingsConfigDict( + case_sensitive=False, + env_nested_delimiter="__", + protected_namespaces=(), + extra="ignore", + populate_by_name=True, + ) + + +class SeedFarmerParameters(CdkBaseSettings): + """SeedFarmer Parameters. + + These parameters are required for the module stack. + """ + + model_config = SettingsConfigDict(env_prefix="SEEDFARMER_PARAMETER_") + + endpoint_name: str + security_group_id: str + subnet_ids: List[str] + model_package_arn: str + model_bucket_arn: str + kms_key_id: str + + sagemaker_project_id: Optional[str] = Field(default=None) + sagemaker_project_name: Optional[str] = Field(default=None) + + # Data quality monitoring options. + data_quality_checkstep_output_prefix: str = Field(default="") + data_quality_output_prefix: str = Field(default="") + data_quality_instance_count: int = Field(default=1, ge=1) + data_quality_instance_type: str = Field(default="ml.m5.large") + data_quality_instance_volume_size_in_gb: int = Field(default=20, ge=1) + data_quality_max_runtime_in_seconds: int = Field(default=3600, ge=1) + data_quality_schedule_expression: str = Field(default="cron(0 * ? * * *)") + + +class SeedFarmerSettings(CdkBaseSettings): + """SeedFarmer Settings. + + These parameters comes from seedfarmer by default. + """ + + model_config = SettingsConfigDict(env_prefix="SEEDFARMER_") + + project_name: str = Field(default="") + deployment_name: str = Field(default="") + module_name: str = Field(default="") + + @computed_field # type: ignore + @property + def app_prefix(self) -> str: + """Application prefix.""" + prefix = "-".join([self.project_name, self.deployment_name, self.module_name]) + return prefix + + +class CdkDefaultSettings(CdkBaseSettings): + """CDK default Settings. + + These parameters come from AWS CDK by default. + """ + + model_config = SettingsConfigDict(env_prefix="CDK_DEFAULT_") + + account: str + region: str + + +class ApplicationSettings(CdkBaseSettings): + """Application settings.""" + + settings: SeedFarmerSettings = Field(default_factory=SeedFarmerSettings) + parameters: SeedFarmerParameters = Field(default_factory=SeedFarmerParameters) # type: ignore[arg-type] + default: CdkDefaultSettings = Field(default_factory=CdkDefaultSettings) # type: ignore[arg-type] diff --git a/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/stack.py b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/stack.py new file mode 100644 index 00000000..c5e9cea0 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/sagemaker_model_monitoring/stack.py @@ -0,0 +1,149 @@ +from typing import Any, List, Optional + +import constructs +from aws_cdk import Stack, Tags +from aws_cdk import aws_iam as iam +from aws_cdk import aws_sagemaker as sagemaker +from cdk_nag import NagSuppressions +from sagemaker import image_uris + +from sagemaker_model_monitoring.data_quality_construct import DataQualityConstruct + + +class SageMakerModelMonitoringStack(Stack): + """ + CDK stack which provisions SageMaker Model Monitoring. + + This stack is deployed to all the deployment environments of the project. + + It creates the data quality monitor job construct and the associated IAM roles and policies. + """ + + def __init__( + self, + scope: constructs.Construct, + id: str, + sagemaker_project_id: Optional[str], + sagemaker_project_name: Optional[str], + endpoint_name: str, + security_group_id: str, + subnet_ids: List[str], + model_package_arn: str, + model_bucket_arn: str, + kms_key_id: str, + # Data quality monitoring options. + data_quality_checkstep_output_prefix: str, + data_quality_output_prefix: str, + data_quality_instance_count: int, + data_quality_instance_type: str, + data_quality_instance_volume_size_in_gb: int, + data_quality_max_runtime_in_seconds: int, + data_quality_schedule_expression: str, + **kwargs: Any, + ) -> None: + super().__init__(scope, id, **kwargs) + + if sagemaker_project_id: + Tags.of(self).add("sagemaker:project-id", sagemaker_project_id) + if sagemaker_project_name: + Tags.of(self).add("sagemaker:project-name", sagemaker_project_name) + + # TODO Add back cross-region support as a separate S3 replica module? + # sagemaker requires model package and inference image uri to be in the same region as model and endpoint + sagemaker.CfnModel.ContainerDefinitionProperty(model_package_name=model_package_arn) + + monitor_image_uri = image_uris.retrieve(framework="model-monitor", region=self.region) + + model_monitor_policy = iam.ManagedPolicy( + self, + "Model Monitor Policy", + document=iam.PolicyDocument( + statements=[ + iam.PolicyStatement( + actions=[ + "s3:PutObject", + "s3:GetObject", + "s3:ListBucket", + ], + effect=iam.Effect.ALLOW, + resources=[ + model_bucket_arn, + f"{model_bucket_arn}/*", + ], + ), + iam.PolicyStatement( + actions=[ + "kms:CreateGrant", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Decrypt", + "kms:DescribeKey", + ], + effect=iam.Effect.ALLOW, + resources=[ + f"arn:{self.partition}:kms:{self.region}:{self.account}:key/*", + ], + ), + ] + ), + ) + + NagSuppressions.add_resource_suppressions( + model_monitor_policy, + suppressions=[ + { + "id": "AwsSolutions-IAM5", + # TODO + "reason": ( + "The IAM policy for create-ssm-param-provider role is already restricted " + "to invoke only create-ssm-param-function" + ), + }, + ], + ) + + model_monitor_role = iam.Role( + self, + "Model Monitor Role", + assumed_by=iam.ServicePrincipal("sagemaker.amazonaws.com"), + managed_policies=[ + model_monitor_policy, + iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSageMakerFullAccess"), + ], + ) + + NagSuppressions.add_resource_suppressions( + model_monitor_role, + suppressions=[ + { + "id": "AwsSolutions-IAM4", + # TODO + "reason": ( + "The IAM policy for create-ssm-param-provider role is already restricted " + "to invoke only create-ssm-param-function" + ), + }, + ], + ) + + model_bucket_name = model_bucket_arn.split(":")[-1] + + DataQualityConstruct( + self, + "Data Quality Construct", + monitor_image_uri=monitor_image_uri, + endpoint_name=endpoint_name, + model_bucket_name=model_bucket_name, + data_quality_checkstep_output_prefix=data_quality_checkstep_output_prefix, + data_quality_output_prefix=data_quality_output_prefix, + kms_key_id=kms_key_id, + model_monitor_role_arn=model_monitor_role.role_arn, + security_group_id=security_group_id, + subnet_ids=subnet_ids, + instance_count=data_quality_instance_count, + instance_type=data_quality_instance_type, + instance_volume_size_in_gb=data_quality_instance_volume_size_in_gb, + max_runtime_in_seconds=data_quality_max_runtime_in_seconds, + schedule_expression=data_quality_schedule_expression, + ) diff --git a/modules/sagemaker/sagemaker-model-monitoring/tests/__init__.py b/modules/sagemaker/sagemaker-model-monitoring/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/modules/sagemaker/sagemaker-model-monitoring/tests/test_app.py b/modules/sagemaker/sagemaker-model-monitoring/tests/test_app.py new file mode 100644 index 00000000..0933827a --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/tests/test_app.py @@ -0,0 +1,30 @@ +import os +import sys + +import pytest + + +@pytest.fixture(scope="function") +def stack_defaults(): + os.environ["SEEDFARMER_PROJECT_NAME"] = "test-project" + os.environ["SEEDFARMER_DEPLOYMENT_NAME"] = "test-deployment" + os.environ["SEEDFARMER_MODULE_NAME"] = "test-module" + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + os.environ["SEEDFARMER_PARAMETER_SAGEMAKER_PROJECT_ID"] = "12345" + os.environ["SEEDFARMER_PARAMETER_SAGEMAKER_PROJECT_NAME"] = "sagemaker-project" + os.environ["SEEDFARMER_PARAMETER_SECURITY_GROUP_ID"] = "example-security-group-id" + os.environ["SEEDFARMER_PARAMETER_SUBNET_IDS"] = "[]" + os.environ["SEEDFARMER_PARAMETER_ENDPOINT_NAME"] = "example-endpoint-name" + os.environ["SEEDFARMER_PARAMETER_MODEL_PACKAGE_ARN"] = "example-model-arn" + os.environ["SEEDFARMER_PARAMETER_MODEL_BUCKET_ARN"] = "example-bucket-arn" + os.environ["SEEDFARMER_PARAMETER_KMS_KEY_ID"] = "example-kms-key-id" + + # Unload the app import so that subsequent tests don't reuse + if "app" in sys.modules: + del sys.modules["app"] + + +def test_app(stack_defaults): + import app # noqa: F401 diff --git a/modules/sagemaker/sagemaker-model-monitoring/tests/test_stack.py b/modules/sagemaker/sagemaker-model-monitoring/tests/test_stack.py new file mode 100644 index 00000000..9dc24515 --- /dev/null +++ b/modules/sagemaker/sagemaker-model-monitoring/tests/test_stack.py @@ -0,0 +1,75 @@ +import os +import sys + +import aws_cdk as cdk +import cdk_nag +import pytest +from aws_cdk.assertions import Annotations, Match, Template + + +@pytest.fixture(scope="function") +def stack_defaults() -> None: + os.environ["CDK_DEFAULT_ACCOUNT"] = "111111111111" + os.environ["CDK_DEFAULT_REGION"] = "us-east-1" + + # Unload the app import so that subsequent tests don't reuse + if "sagemaker_model_monitoring" in sys.modules: + del sys.modules["sagemaker_model_monitoring"] + + +@pytest.fixture(scope="function") +def stack_model_package_input() -> cdk.Stack: + from sagemaker_model_monitoring import settings, stack + + app = cdk.App() + + project_name = "test-project" + dep_name = "test-deployment" + mod_name = "test-module" + + sagemaker_project_id = "12345" + sagemaker_project_name = "sagemaker-project" + endpoint_name = "example-endpoint-name" + security_group_id = "example-security-group-id" + model_package_arn = "example-package-arn" + model_bucket_arn = "arn:aws:s3:::test-bucket" + kms_key_id = "example-kms-key-id" + + # Instantiate a settings object to avoid needing to pass default parameters. + app_settings = settings.SeedFarmerParameters( + sagemaker_project_id=sagemaker_project_id, + sagemaker_project_name=sagemaker_project_name, + endpoint_name=endpoint_name, + security_group_id=security_group_id, + subnet_ids=[], + model_package_arn=model_package_arn, + model_bucket_arn=model_bucket_arn, + kms_key_id=kms_key_id, + ) + + return stack.SageMakerModelMonitoringStack( + scope=app, + id=f"{project_name}-{dep_name}-{mod_name}", + **app_settings.model_dump(), + env=cdk.Environment( + account=os.environ["CDK_DEFAULT_ACCOUNT"], + region=os.environ["CDK_DEFAULT_REGION"], + ), + ) + + +@pytest.fixture(params=["stack_model_package_input"], scope="function") +def test_synthesize_stack_data_quality(stack: cdk.Stack) -> None: + template = Template.from_stack(stack) + template.resource_count_is("AWS::SageMaker::DataQualityJobDefinition", 1) + + +@pytest.fixture(params=["stack_model_package_input"], scope="function") +def test_no_cdk_nag_errors(stack: cdk.Stack) -> None: + cdk.Aspects.of(stack).add(cdk_nag.AwsSolutionsChecks()) + + nag_errors = Annotations.from_stack(stack).find_error( + "*", + Match.string_like_regexp(r"AwsSolutions-.*"), + ) + assert not nag_errors, f"Found {len(nag_errors)} CDK nag errors"