{oH{~+ zjy5Hh8UcxpBoU%x+^IWd8JVbyUFevknH1U%PSlGe_QZ1_j;CK&wPF>pxdx+lFe#5+ zo;7U*%{8+I1(yQY3?@b?m=k*hv%Or2GPm_4viLQO>YGD@)Dc39q}!dP97P-|<}9Pe zQ~?c<$s2qO4d;>Bys@P}{ye>7UOqLKPc_qF0L!U9A~_-%kQS=@8*vNL)w}lwGD5U6 z26KL=b>JSbYy4*!#=o{{JzX13x)|F(pKqtAPCUyuwB8U+yXl*H347;lrK8^C4}m~C zrCuhkCyL0*U5HpCJIc!5Y};j-$Tq6F?Mh4KC1fGrGPV(=Llg~jKR#X1_>K7`DB0c3 z>9cuDpDeO;n@{VCdjr=5eMd6;wTMB_G$yfzZPTMY}wEs@KVp zrjyBXdyhOMgi@`{8leAJ*4pH5Uz5{zIZBtSo8bj8lq8>wky90}a&Qgdy2E`Gpe?i6 zB;NxFCCZ`2HcXB*nqk4jZCnHWKESKL<@h{&K7jRf&FC#TyNYzw^O2+t+KJz~CYy%Q zjgp9s(%QN#A7H!qK5X2GV;KC{-)qC&_Yqsp=Uw{Z@Qe5g6H!)(T3t4(#OH*#*(g?H zO
1MXqpz2)GwRb)!&4~`YPtbspu;TgTJCZ8 zfct{$;Aajw+9K~Ls%L7Phz3h0XnidyN+ UrZZE8lUw)3^ukC(*uLSe{c*7fdoPh3L#t)3Q5$^v_vPB%VlM}8{%N>bZwdh zaaDCnrSz^*s9Gt_Q+YV)h$8j!a1VW{)IQ$}YbRREmr6)?y =PuQIlOEwkCkMLwGVnc!sy6 znHh$bmKh6gs+jdTHTqx zZ$JL@Z#P%Ixw}^P1iPo6Fp#rMxpq2}y*iOj%nMN2dy1bFGJ=@MXT{g9 2IyLab^=|e(6vCj z6uJ&*w?eysuD|YP>&q@p*)x5M?Vv+l7}A-(zyJQmH#hH|8rK&2 EF`@|-U>Bsei&z~ z@aS_>`6hfi`Fe>Cy{rlib85O$4K!(>fpaInsRkx|It?_52CL9LZj`SZqwaG8T9|Nx z7R_MiM&7ne%(PsBHSxA37zyjsWEtjn8|TKFZhp;DhB2OJj;T?5K__j^Y8z@|swpsl zj+zC5%S1Eas8uEL7N}`9p*5GGPaDtMffLU*O_WnxK|Z9mUk 2vwS!WAxeDRnVl4ja3Zi=rrG@_MO3BS$&`qHz(BCFjc#V{d*@%y6@b tP_E5i5`_ex%FLB*uVz8DmyCnK%xP$5 zEoECyNM96jQ{~W0;qVXR UoYktc>FiujP7vQtC57`KWKPBR z^Em+iiwOMOLSOHd>t!bg3V|oNr3RhNd(~DyPWT33+q+~V5Wi{rz*BPVm0dx}6 6aq?#nwYN$3K|bbdY_(_|O@loE?&@W38{?+W)w-du>wQe^T0ive gf #W&(F!DXC?SW&SF n-B^#7;vE&PwstIAN{0rp?0y`G ze%f}otr%c$+HQHaz_g_75Cz z;57whyCROb~=e^aMN>LyDa+zFX%sj+fm%yG$%YWq`xFD jN fk(S3?)Be(=lyp+;pMhN zQrn@TcVOK)AcnzW2E;?W!H{FC@M4Guqs0`QL=C?{rjmGO9(%{2*90qhMhPvoK6 UM=iX$J0R4MB^%g=3Of9E^D*iSM&yKsIK5y_rjqa z_ktE=s&mmWRv!YJV6wS2tC~Z<3Cct3ZdeJ}zYA8XcGur!|3PPbOy~k@yxH`Ye7^NO z$0)! E0Tz5}GR2FB~8f5l_#Gd%__2Rxs8eVPu7*yRAMV}Mxo{7%8gPe94!KUbZ3UUvC zz_1QBI#An_1~6L3ZUG*?iOm54T1_Ac`IkT{^lGxJ1oq@LaN;fMAzlJW)8wADvW4fq z|L52f>uW=2SbOD`#utK!XI%7Rh9^?JZ0i@5w ^ue zN{t7f+AU3eu$3AbSMndd{Iu5QYuzNa+SbY~^n4D2vkFlgXje)90|#lJ57ap-uB+Yz z+Z`kx08Z8+QY_i#6oeM)8Sx-c21h`|vjb$dM?5 Rku(SkF&nvk3wkDSpT@0=c~ zu?L;gyQ_0FsE7Hq(*rLAI6Zl-ulha5?Ali!4RERe`gb`O5J0QDeVVYAsX3T}ynCbf z7;B?uE8FN7kQy4QW^y84A}m8A${gwuu0)TG96Yp50^6l_;}$n%`ZdVOMQ~p7*t_@R z@jI`6`07VftJ6|L=i2xmrtVEi{^6o`WZgNkGkii+p~&(X5HIjx#4m7%^V*Za3`2NE zwG@Na)Vu%_D@nEZGajZm5A(s{Wu{{7f8BdpI5T^846NM_LMnISPk}1ib7?`i!YaYL zFE7A1Anm;%h=!fqP_e!-_+JX|G`*4IJ(wr=24ny Dr?a*xalOm@VYa!(@_Y@)KR|! zf;tMy?ddR~rqv^p!XoYglWXp&j;oqH;{y&YW;5ehW|9^j{>{J>HD+W0-rGqZ$uU $&}C^mhNJvgDzjQz-(e~Un| z>2TP?t4VARBTw@u{_lWpu4sV@uukz6)B_G65$Or5&8Z&*MH@Eay;E5!Rd@ocVa(|J z*m int: - """ - 获取新的UUID - """ - - if self.snowflake is None: - raise ValueError("please set id generator at first.") - return self.snowflake.next_id() diff --git a/id_generator/idregister.py b/id_generator/idregister.py deleted file mode 100644 index 787faac..0000000 --- a/id_generator/idregister.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -worker id generator -""" - -# !/usr/bin/python -# coding=UTF-8 - - -from threading import Thread -import time -import logging -import redis - - -class Register: - """ - redis封装 - - host 代表redis ip - - port 代表redis端口 - - max_worker_id worker_id的最大值, 默认为100 - - password redis的密码, 默认为空 - """ - - def __init__(self, host, port, max_worker_id=100, password=None): - self.redis_impl = redis.StrictRedis(host=host, port=port, db=0, password=password) - self.loop_count = 0 - self.max_loop_count = 10 - self.worker_id_expire_time = 15 - self.max_worker_id = max_worker_id - self.worker_id = -1 - self.is_stop = False - - def get_lock(self, key): - """ - 获取分布式全局锁,并设置过期时间为30秒 - """ - - if self.redis_impl.setnx(key, 1): - self.redis_impl.expire(key, 30) - return True - if self.redis_impl.ttl(key) < 0: - self.redis_impl.expire(key, 30) - return False - - def stop(self): - """ - 退出注册器的线程 - """ - - self.is_stop = True - - def get_worker_id(self): - """ - 获取全局唯一worker_id, 会创建一个线程给worker id续期 - 失败返回-1 - """ - - self.loop_count = 0 - - def extern_life(my_id): - while 1: - time.sleep(self.worker_id_expire_time / 3) - # 是否关闭了 - if self.is_stop: - return - # 更新生命周期 - if self.worker_id != my_id: - break - try: - self.redis_impl.expire( - f"IdGen:WorkerId:Value:{my_id}", - self.worker_id_expire_time) - except Exception as exe: - logging.error(exe) - continue - - self.worker_id = self.__get_next_worker_id() - if self.worker_id > -1: - Thread(target=extern_life, args=[self.worker_id]).start() - return self.worker_id - - def __get_next_worker_id(self): - """ - 获取全局唯一worker id内部实现 - """ - - cur = self.redis_impl.incrby("IdGen:WorkerId:Index", 1) - - def can_reset(): - try: - reset_value = self.redis_impl.incr("IdGen:WorkerId:Value:Edit") - return reset_value != 1 - except Exception as ept: - logging.error(ept) - return False - - def end_reset(): - try: - self.redis_impl.set("IdGen:WorkerId:Value:Edit", 0) - except Exception as ept: - logging.error(ept) - - def is_available(worker_id: int): - try: - rst = self.redis_impl.get(f"IdGen:WorkerId:Value:{worker_id}") - return rst != "Y" - except Exception as ept: - logging.error(ept) - return False - - if cur > self.max_worker_id: - if can_reset(): - self.redis_impl.set("IdGen:WorkerId:Index", -1) - end_reset() - self.loop_count += 1 - - if self.loop_count > self.max_loop_count: - self.loop_count = 0 - return -1 - - time.sleep(0.2 * self.loop_count) - return self.__get_next_worker_id() - time.sleep(0.2) - return self.__get_next_worker_id() - if is_available(cur): - self.redis_impl.setex( - f"IdGen:WorkerId:Value:{cur}", - self.worker_id_expire_time, - "Y" - ) - self.loop_count = 0 - return cur - - return self.__get_next_worker_id() diff --git a/id_generator/options.py b/id_generator/options.py deleted file mode 100644 index 8558381..0000000 --- a/id_generator/options.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -生成器IdGenerator配置选项 -""" - -# !/usr/bin/python -# coding=UTF-8 - - -class IdGeneratorOptions: - """ - ID生成器配置 - - worker_id 全局唯一id, 区分不同uuid生成器实例 - - worker_id_bit_length 生成的uuid中worker_id占用的位数 - - seq_bit_length 生成的uuid中序列号占用的位数 - """ - - def __init__(self, worker_id=0, worker_id_bit_length=6, seq_bit_length=6): - - # 雪花计算方法,(1-漂移算法|2-传统算法), 默认1。目前只实现了1。 - self.method = 1 - - # 基础时间(ms单位), 不能超过当前系统时间 - self.base_time = 1582136402000 - - # 机器码, 必须由外部设定, 最大值 2^worker_id_bit_length-1 - self.worker_id = worker_id - - # 机器码位长, 默认值6, 取值范围 [1, 15](要求:序列数位长+机器码位长不超过22) - self.worker_id_bit_length = worker_id_bit_length - - # 序列数位长, 默认值6, 取值范围 [3, 21](要求:序列数位长+机器码位长不超过22) - self.seq_bit_length = seq_bit_length - - # 最大序列数(含), 设置范围 [max_seq_number, 2^seq_bit_length-1] - # 默认值0, 表示最大序列数取最大值(2^seq_bit_length-1]) - self.max_seq_number = 0 - - # 最小序列数(含), 默认值5, 取值范围 [5, max_seq_number], 每毫秒的前5个序列数对应编号0-4是保留位 - # 其中1-4是时间回拨相应预留位, 0是手工新值预留位 - self.min_seq_number = 5 - - # 最大漂移次数(含), 默认2000, 推荐范围500-10000(与计算能力有关) - self.top_over_cost_count = 2000 diff --git a/id_generator/snowflake.py b/id_generator/snowflake.py deleted file mode 100644 index ea36cfa..0000000 --- a/id_generator/snowflake.py +++ /dev/null @@ -1,20 +0,0 @@ - -""" -雪花算法生成器接口声明 -""" - -# !/usr/bin/python -# coding=UTF-8 - - -class SnowFlake(): - - def __init__(self, options): - self.options = options - - def next_id(self) -> int: - """ - 获取新的UUID - """ - - return 0 diff --git a/id_generator/snowflake_m1.py b/id_generator/snowflake_m1.py deleted file mode 100644 index 596b76d..0000000 --- a/id_generator/snowflake_m1.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -M1生成器 -""" - -# !/usr/bin/python -# coding=UTF-8 - -import threading -import time -from .snowflake import SnowFlake -from .options import IdGeneratorOptions - - -class SnowFlakeM1(SnowFlake): - """ - M1规则ID生成器配置 - """ - - def __init__(self, options: IdGeneratorOptions): - # 1.base_time - self.base_time = 1582136402000 - if options.base_time != 0: - self.base_time = int(options.base_time) - - # 2.worker_id_bit_length - self.worker_id_bit_length = 6 - if options.worker_id_bit_length != 0: - self.worker_id_bit_length = int(options.worker_id_bit_length) - - # 3.worker_id - self.worker_id = options.worker_id - - # 4.seq_bit_length - self.seq_bit_length = 6 - if options.seq_bit_length != 0: - self.seq_bit_length = int(options.seq_bit_length) - - # 5.max_seq_number - self.max_seq_number = int(options.max_seq_number) - if options.max_seq_number <= 0: - self.max_seq_number = (1 << self.seq_bit_length) - 1 - - # 6.min_seq_number - self.min_seq_number = int(options.min_seq_number) - - # 7.top_over_cost_count - self.top_over_cost_count = int(options.top_over_cost_count) - - # 8.Others - self.__timestamp_shift = self.worker_id_bit_length + self.seq_bit_length - self.__current_seq_number = self.min_seq_number - self.__last_time_tick: int = 0 - self.__turn_back_time_tick: int = 0 - self.__turn_back_index: int = 0 - self.__is_over_cost = False - self.___over_cost_count_in_one_term: int = 0 - self.__id_lock = threading.Lock() - - def __next_over_cost_id(self) -> int: - current_time_tick = self.__get_current_time_tick() - if current_time_tick > self.__last_time_tick: - self.__last_time_tick = current_time_tick - self.__current_seq_number = self.min_seq_number - self.__is_over_cost = False - self.___over_cost_count_in_one_term = 0 - return self.__calc_id(self.__last_time_tick) - - if self.___over_cost_count_in_one_term >= self.top_over_cost_count: - self.__last_time_tick = self.__get_next_time_tick() - self.__current_seq_number = self.min_seq_number - self.__is_over_cost = False - self.___over_cost_count_in_one_term = 0 - return self.__calc_id(self.__last_time_tick) - - if self.__current_seq_number > self.max_seq_number: - self.__last_time_tick += 1 - self.__current_seq_number = self.min_seq_number - self.__is_over_cost = True - self.___over_cost_count_in_one_term += 1 - return self.__calc_id(self.__last_time_tick) - - return self.__calc_id(self.__last_time_tick) - - def __next_normal_id(self) -> int: - current_time_tick = self.__get_current_time_tick() - if current_time_tick < self.__last_time_tick: - if self.__turn_back_time_tick < 1: - self.__turn_back_time_tick = self.__last_time_tick - 1 - self.__turn_back_index += 1 - # 每毫秒序列数的前5位是预留位, 0用于手工新值, 1-4是时间回拨次序 - # 支持4次回拨次序(避免回拨重叠导致ID重复), 可无限次回拨(次序循环使用)。 - if self.__turn_back_index > 4: - self.__turn_back_index = 1 - - return self.__calc_turn_back_id(self.__turn_back_time_tick) - - # 时间追平时, _TurnBackTimeTick清零 - self.__turn_back_time_tick = min(self.__turn_back_time_tick, 0) - - if current_time_tick > self.__last_time_tick: - self.__last_time_tick = current_time_tick - self.__current_seq_number = self.min_seq_number - return self.__calc_id(self.__last_time_tick) - - if self.__current_seq_number > self.max_seq_number: - self.__last_time_tick += 1 - self.__current_seq_number = self.min_seq_number - self.__is_over_cost = True - self.___over_cost_count_in_one_term = 1 - return self.__calc_id(self.__last_time_tick) - - return self.__calc_id(self.__last_time_tick) - - def __calc_id(self, use_time_tick) -> int: - self.__current_seq_number += 1 - return ( - (use_time_tick << self.__timestamp_shift) + - (self.worker_id << self.seq_bit_length) + - self.__current_seq_number - ) % int(1e64) - - def __calc_turn_back_id(self, use_time_tick) -> int: - self.__turn_back_time_tick -= 1 - return ( - (use_time_tick << self.__timestamp_shift) + - (self.worker_id << self.seq_bit_length) + - self.__turn_back_index - ) % int(1e64) - - def __get_current_time_tick(self) -> int: - return int((time.time_ns() / 1e6) - self.base_time) - - def __get_next_time_tick(self) -> int: - temp_time_ticker = self.__get_current_time_tick() - while temp_time_ticker <= self.__last_time_tick: - # 0.001 = 1 mili sec - time.sleep(0.001) - temp_time_ticker = self.__get_current_time_tick() - return temp_time_ticker - - def next_id(self) -> int: - with self.__id_lock: - if self.__is_over_cost: - nextid = self.__next_over_cost_id() - else: - nextid = self.__next_normal_id() - return nextid diff --git a/main.py b/main.py deleted file mode 100644 index e01f3bb..0000000 --- a/main.py +++ /dev/null @@ -1,14 +0,0 @@ -from fastapi import FastAPI - -from api import register_routers -from middleware.blog_visit_middleware import blog_visit_middleware -from middleware.exceptions import global_exception_handler - -app = FastAPI(title="Blog Service") - -# 加载中间件 -app.middleware("http")(global_exception_handler) -app.middleware("http")(blog_visit_middleware) - -# 挂载API -register_routers(app) \ No newline at end of file diff --git a/middleware/__pycache__/blog_visit_middleware.cpython-310.pyc b/middleware/__pycache__/blog_visit_middleware.cpython-310.pyc deleted file mode 100644 index 79f58e41c72d2032ed7715b011fb9ec53716a1d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1218 zcmYjQ&2Jnv6t_L!vmZ&* @y>7fuyI6lf_Dst^?)8m$=3j=h@+o}Iz=bko&L z)e^2f0umKyQ^A2tR3xs51OEVLU(urE#2qBc^CVT)BR{`Szvth3Q}lWvg7w1Rf9&-f zg#I+b>asw11GXFkp@?FRB6AXsa}p73c#B)P9oa2!b0>Erx8)t~<$mPnK@_yI%fq}A zbz0uz-Mkm|fcF_06B^L)OFQaQd>aiqr;rQX8er{xz&@{-RKNyr@?!E)CNm>+MV$Q9}e(c8v~kV}aILxRg+^G`8|;`w$OF z<20`FYqAKaeT2c!Q( Wab+G6kA&fpo z)8}lY*6x0SQ4?Y`?}KICSkxuz*4`sTy*hk^zJr`!U !Wo`H9QbV*zp3+#o1q$B zaERZ9_T5BHr|tmk{;m1er@IE(-L#l0fNl5XA$?_VWgdPA&~9Hd&}wvw0fvF2Nmgo7 zNbQY8F$24{Dv^O?I+M}LAothTPnM6rUq1YHdHlHA1jWhmZ>QfJpBz6~{`%GOho4V= z`SI%WgUgx-rky-d>7KcSYr9O;o^T;RdwVRQOh|1(Hw8>6(pCm#I1^D+r7+_uJVR__ z>L--OZ3TDCq-ZxS@*K|KqRqZ2I3FeHXTmoTPRa|(23-*tDS*M?@su4Xv0*MXkI>=8 zV2Pg5_6=EYO)I$w9cj$8FPJQgsboJPaRq$-x$*YZ;adj>!*?=uvl_iGihY(UIW&>m z=My~4GfFv|C4#m3xUD|E(C(CX_0~nM6-x(F$X9`o` )W6; zdLM6M|33MTJl%BqmW{ ?77uzBotyd~L zH^(j8_U5c-YcM|JY_dZWm5dSyGY!&PE6-ui{yN!qp{=7uYlXaMg8bK@j*7F_&Cp4M QxsTv2q>I t zGK!9oGR2iKBg%%z1{vK-g^hh0gS8)oF=)x4 Jwr<$f_q~0*%vQG4)>GbPkaW!pWBjVvkt+=ycvIrhO*YNY?Dz^3m3Lk()5eFHG zt&tlhd^k2pxT?w)k>Iu{RXA0hKSt$f_^l;65jL2r*2pn9TjgpT--b+h|EUvAp%uo- zm~&b6PK51pvJK*TIX1^x4VykX(dgisR5oWgGjqHZaQrr7vo`ntcWLuqu~V2-WaI>^ zWdi{)RRuXw1XinKO>A>eP1>R@I6I?&Ehb*9`5feAqku>vIvjgV)*@(N<6Cm}b`8__ zT?T+#bb L(|x(hztJj<(; z0&>!OQkKesG3GqTQtkh_QYW$pTtEFalcmDKnJa+{-Gu$!aU*46YIlbQm6yW1uSjoU z`tLhR=#Ri-!k;jUXf2Is!$kZLG_u%_m<-ggM!H8+G9DcD$E}&w`=71exU)KaKhuN_ zYtuhGE=;dYZp`$?E41*1q|oHyCb2G*l6rl&$zENuncHTDC?Z=?}z@s q)gQ$ClK5+ zp7lq12hTzyp~fw9@PuZKW`;IZl4xJ`qcfP)6h&9FGE@g^1!6U8SNbvv{Hq6c;-YT+ zn63Dl*1*$o@W|&c2$RBm>w%e`?+!mWym;{V!olMuUu2$-jA!uck$2neo|3mcPcH>_ z6k?OH#Xx8w5GwANfu%riURrJr7x$K$U!HL1>HLXhf8ZmvaB=eDy|#~2C4WbrU*|yc zuBG}0@@v|@ sJbEajzjAE?{*;e|Y1rg^bt+4I7pr*py6S)_}7 zzj%6 Zn%&P4d`OIRFtbP6B0**@$YMO#!9+q}F658& zGIPBy922gG zb=;~{5KUPVzn#5ja mD2n
diff --git a/middleware/__pycache__/exceptions.cpython-310.pyc b/middleware/__pycache__/exceptions.cpython-310.pyc deleted file mode 100644 index f6e42c59e4986e16a1fd4204790f5fcf35bbd07d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1877 zcmah~&2Jk;6rY)0@7j*-rfJGY1;v5`@*z PW%O2=Sm%=cTSZmc(ZmAgFuWlv-3XYy?Jkb z?`@RNXAz7KO20J@q!IdE5&c64;(eHL0~U%X_ECT>91x2Hnx$dIBfcJ_tW>hsd?PR| zGui8YI>=ZVj7}h$qQ(_O4VGQjtO05=JWFVrX0GT~j%FDFYJlc|%F}!el?qS58!Bni ztg`bh77Ll5n3_6WUT|4ccu^>gSU92;m$1xzSUXjzvbY(AF#~!}?aaw>-)*p9vCMhI zfo1qnz0SC+Z1#_x*j@rILl&mUB5+f}G`NpO^(!e$rx@;I$lQ3dx#PYf&49(RQ)jN$ zXHe}4%wsU)L0H zZrIh1eVa9J`gg(PdZwmA+rjWHcatm+;U+ot9TEC4k zg1ydPPUM Yt?e8=!5{l%jVu$uzu!jzoIV#^@s8 zcj*<-%21=N&1J!-3EzQjpCTvna|I<)T$)}ZG_wL04D>Phx~P4s)q3A~3^w;HwtF^* zl9PipEB3avW&FP PqL)wXv?! }qP1T+x&B~t<%iCr)y>tF$G@y~?mXQ3@virW@|B3! zR+RZ0_d1_neX@M-$+a6>>+1^f%F-_4OZ&J9#-#3rvyn6cCK?fyT8sNK2i4CP?WV(> zAeK7yT*04Lx+Z`=^ul@vX71*x2xqdXUnxIZYa^g zMD!MjCM#3rYGwSST`N~VDp&1twR)<`hm}%hr|L^OS3${)>-fGMvIQa2yw`XnX8))K z`9j@~&N;rlQ958TcV*DE=Z+ultGK(C9Ru#quaZr* QFx2w>8%2wc-E_rsy~B5k-{H;O)aXHk|ZY% e|MlMKce~B;J-|7NRXoEe2d|dHgL=Undi5_byyWfx diff --git a/middleware/__pycache__/exceptions.cpython-312.pyc b/middleware/__pycache__/exceptions.cpython-312.pyc deleted file mode 100644 index 279b97a65fdcdab7326abde88ba7073df0f31306..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3207 zcmbtWZ){W76~FgA|BanEfK!sNC0@x8hQ=f lemG=8gR@2H|Xi%_Yyz%*$($T zH-rU*fL3H80*P*cHWleKwy 0KBsG>kzBD}! z2dh^^^0pKeI}(y0$YKvq#17H$lj7^{)odImB_);PPE`~h#b+$rsk(CL&Z6nHH5W~n zocb`}T!1x1N}&Tc07{Zih#aSta9koKq-9DwIqpQ7m-8G}Q3+WM^0yFKL-($t9lc(9 z2e%6;Wzbzfe`OeR?J&EDV91G5Da}f9pF2t&YBXx!dH65<3VgB;VQ{b=sY?!x7s}@M z2=D>oCIeu3`~WU1a4gF!2K52@&@`II#0X@lu`3$c{3Tjhm3~#d6-O{MfHuaUX?V~J zW6aM(NPQbl`aO1#eb;P8r jad8w?Dv`p}nJT~ mV z#QQVf`Do@B=Vm4*V2OsBr`|I+{=?bx?=*P`#qlzlBUc%?Xi5+@Yc!Qq#iW|1i3qS~ z2Yx#9@l^vN47I@qxf0T2XDVG$C^CC>;OQ@iW`1yI?q@f177qRRFE?%)JEcWxp}u2i zfEsmuWaiy>o}C|g_TKfm@o}2Yss813?sXG-Zp}nhX_kbj#!`aDrirXMp}>(|uA3)( zLeWfu6jccg0nG}zJSio+^xUbLMOw|45eZFVgcfMcO=}~!wV{D)_P27Oz`;X-_As$h zmMuyoRf_U5Sxcuiy5u&H+#3uBLP7uQTzeq&tw4wighKm6ntiFzc};}Ire=q|GM5xj zs+x`DtE`4ag(g=hPaZwRYn5Gc>NqcRONEDv@kyvgg!n*CuV A)tC46@4Mz3 zbB}aQ`I@u7=EvTa%*NJCWnj|z+MlYPAKw1BYV*{Z%@=L+7UXopwydLe_@z%B8|dC( zfAHFo;n>uMrtF5M%-45j*1bCE-1SG-+KaCFQsjQ&pNr+lS@}0)GKX}toc+!#XNLJ7 zNgqkWZ{BuH_&?eE^Su*0GZk% &7fo8(Xp)Tc#Tt@7nL!f6YIV?n&9Y1B)y! z58=NfY!Bhj7r~Hq6!Vyl>FRf7syC0d+{UA?jl7bn+&ZCtW^2uZy^-Dc#&kp5WcYCA z$kE5)NVe`xigirKI+nw#pjbJ#0T%dT&B(#g=SFOq%EpP-&usoesQQM?uJ9x;WW>(L zd{?$EM$zIrS{#2qZ>2#0R!+f}IfvW6iZA#Zo425Q+i-Ke>E6pP1AYGmx_-YNQ+kWN zt(v|6ir>>(!#=3QFnLgA2KIv*C#5&}?SUQa! M{Rj`j-l<}yX zG9Fc!DP7G1tyzeurb#jiRxCAorJ?enh278%7Swoksm2$p>H-$v!6xA`lkvE0hZjrm zR831NEeoDxO7*Xs5r|Xx!=+t~`+K z)!gH6H)#S9`eIUcLO0K1jQ@aM%%T^6k2dMQuPxeGTzh55B7$MQtQN=dNZSIU<9sVN OW6SWaMFfL>uKxf?q3YcL diff --git a/middleware/blog_visit_middleware.py b/middleware/blog_visit_middleware.py deleted file mode 100644 index 64a671e..0000000 --- a/middleware/blog_visit_middleware.py +++ /dev/null @@ -1,41 +0,0 @@ -import re -from fastapi import Request - -from models.blog import BlogVisit -from service import blog_service -from utils.blog_utils import get_user_agent_info, get_client_ip -from config.database import SessionLocal -from middleware.exceptions import AppException - - -async def blog_visit_middleware(request: Request, call_next): - # 检查路径是否匹配 /blog/content/{blog_id} - if match := re.match(r"^/blog/content/(\d+)$", request.url.path): - # 获取客户端信息 - ip = get_client_ip(request) - user_agent = request.headers.get("User-Agent", "") - os, browser = get_user_agent_info(user_agent) - blog_id = int(match.group(1)) - - # 创建访问记录 - blog_visit = BlogVisit( - ip=ip, - os=os, - browser=browser, - uri=request.url.path, - blog_id=blog_id - ) - - db = SessionLocal() - try: - blog_service.add_blog_visit(db, blog_visit) - db.commit() - except Exception as e: - db.rollback() - raise AppException(f"新增博客{blog_id}访问记录失败: {e}") - finally: - db.close() - - # 继续处理请求 - response = await call_next(request) - return response diff --git a/middleware/exceptions.py b/middleware/exceptions.py deleted file mode 100644 index 1fdbf50..0000000 --- a/middleware/exceptions.py +++ /dev/null @@ -1,54 +0,0 @@ -from fastapi import Request, HTTPException, status -from fastapi.responses import JSONResponse -from sqlalchemy.exc import SQLAlchemyError -from config.logging import logger - - -# 自定义异常类 -class AppException(Exception): - def __init__(self, message: str, details=None): - self.message = message - self.details = details - - -# 全局异常处理中间件 -async def global_exception_handler(request: Request, call_next): - try: - # 记录请求信息(可选) - logger.info(f"请求: {request.method} {request.url}") - if request.query_params: - logger.info(f"查询参数: {dict(request.query_params)}") - - response = await call_next(request) - - # 记录响应信息(可选) - if response.status_code >= 400: - logger.warning(f"响应: {response.status_code}") - - return response - - except AppException as e: - # 记录业务异常 - logger.error(f"业务异常: {e.message} - 详情: {e.details}") - return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, - content={"message": e.message, "details": e.details}) - - except SQLAlchemyError as e: - # 记录数据库异常 - #logger.critical(f"数据库异常: {str(e)}", exc_info=True) - return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={"message": "数据库操作失败", "details": str(e)}) - - except Exception as e: - # 记录未知异常(带堆栈信息) - #logger.critical(f"未知异常: {str(e)}", exc_info=True) - return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={"code": 500, "message": "服务器内部错误", "details": str(e)}) - - -def get_credentials_exception() -> HTTPException: - return HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", - headers={"WWW-Authenticate": "Bearer"}, - ) diff --git a/models/__pycache__/base.cpython-310.pyc b/models/__pycache__/base.cpython-310.pyc deleted file mode 100644 index da65d15a1e7583f1f183c07565979b0a88b1be99..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1968 zcmZ`(OK;pZ5Edn^R?=$M>t_ zEnGaqQU8TRSc2tN!8j{yZWj)B3YWV@i? KW$^K+X_#!xEye}8emd`D|B3I5 !l?^(RpWy zj_jN`d`-3{_vD(0L|61q9kC$#@2nHX@1x$LSVFxexrTZVjNXH5y=4&@z4c4^%H{c0 zv0~&8#oCTFxOahmTZ5MNHmkfTN*zAW##^P5V+q>ZQIM5m?Y~Hr+|3H9Tk=4bYQS_? z$TUwtia1dUP|!&$Jawqzx*6fpeV)`3XJ6n^g-4O^rAZ<4SXFUdCa>kd*6nJlvZ}0g zFqWl+L{)HckL+PQcw51132`P;vT**p&e>y>N{cgLaa%C%NLRK5!{0u2xF_4f5w7vo z7cKPF)83XKyLR#E&J?mDfus1fJkqVK6!I`-bFUC@ 4>-QF{qWqtyFTUT~JUrZcnW>+f(XUWVWUA`DQC^Lo)DjM|RPGg( zkU7fGpyBjLcjH(kqg>AJqbSt$e(3zRGP8PkOJREuz%rQ-#xdU1 loKNW&pi6aNr?Mm=A$~;O&wC zm5G6)y&{=T5t1Roti)I%YP6>k7)y1>c)fPZzBtcnMd11bFRnc#A$sC|gwK&paoqZ7 z{~3;Y7s-F2CA~@8fXhdy1;RNYu(_u;4` @B~6Z*;49>{UzN?2>S=q>HMxiTD24|_)`^%DQ*{6VRy$?& z7V8ou9*L<2!6k8UDH$E<&g?$3se{YK=x8pYgk~_9_yRdHqmGaeCi-@tt>6!|b1U)m z3?+XP$A?Eqly!zhdd_}#jvWL5gWj=&$+zz&-$z~TP)@0|-@EWBPVk7xXGnDGAjum! zXP&W)pf$wtfsTi^&qB=aHB4t-{jYuS1P@(-k1hbZ3ce=t4G|N8n=pKfvhPU#6v_5r zLx=U7Jjv5nvN*b_F%xMpsUS${^&6!jVtP31o{7p1q$x#C?Q=gH50gf{0uzRXO9>`C zp&kJNy-JDYQKPcF9;Q`MRHbP|Jjz6T-9v#=1YJ^H+7d6XTI(;fX3Lje%z!qxWvYYs zYf0rccM&xL-I vN0J43Oi6Z=0bEGC++_*>q R Q-|a<6y aWvcl#oAh5 OZwZQEMU;poDk6t6 z-juAU67_k(l&zQ&^ZAGww-QRi=S4GVC6%PlOJ>SyQCfUHYPMQwC2h4SZ5-hVO&5DZ zC$>jjz7tykPw-LFpw(799L$>O%GN~bpdH1&W=X))bZ7&=G(z Mzf)Y()CtbU9Z{7G}-EY$FBAr9&|MA+Edw; z%f3XnbD}4GW}9WJ4rzrEJ3obKCQ=UAmxpcy?s3+1{T5@xom(pp$)EL!O z&2W~Ey(!a#Y@I_qu>9a+mOwXBJXSLAR$yP zklWl?j%P`bda|mP;OMFf=MoC0 Fu9Nll-PN!Dh_x=nmm_zFYq68nq32@e70-+P8v>EqeJ-$pyS!AMUbGoi zjS?K+oj@qeFndOu0w5L8iSWVb`!{nQm_;(j7eh(YSnYNjw^R(l( p(1>Tr0gqVfIiL7izONoc+^cG8EBgGQ`VJTUWa#1h#X<0Yi zFze(}*)$QuUR0+SwX*5H{0F?uaSYQ8@&*S_woBz1EFfdKln++67na#RBp9~ FjA<@S1U=d!7pg=uP4wkw 3Uh zQqUlhhORa`AWNYtX0MkS)kng{IKI^?Gs8i`G>KR*DyJQa)v3vlUr!T3&2dzys(#t= zfgN12ADABeTm$=_JQ%ct)9ecnV5f3^15y__j(bLi9+RP` z|+4 zBrw!F3Ey7bwZ>&uxy-_er89T+8utnG1+Eu51V30uZZj!yA1p|l1crL1dBe#i`(8(l YJN=3mxC6N1OdYv;^4&_lx_|C}0Ua4%!~g&Q diff --git a/models/__pycache__/blog.cpython-310.pyc b/models/__pycache__/blog.cpython-310.pyc deleted file mode 100644 index 36f7b900d3b514c7a1718e202380e8e4747befed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3062 zcmai0-ER{|5WhRycjxm*oDV+x6{(_N3NEw{l~Ac5p`wv!REe|?r%2Ytdo@Szd}hxV zNS>fTp_QUiQA9wX7F0nIYC>C)s-%F_{sn#A*W#}~fR{?Bo!N~YhsI#*#{1jZ*}a+h z?Tr0Y%8=kw?)mG`pGisj8$stM3SbXB!7K k|2i}sLHvglM+W=U$hR>kp4!6B!dRaks) zNSw-$71&H2A^2OslRG~#0DIsG{s3W1rc5MLu_Jbr$R}l@kO+y+M9dh>sDNS|X@GP< zDo1fZ1|W^2M2D@;XOc_$I7cZ!xRb$A8c+{OkmN}P=2CnvWA~CY=^+`?3n%vh>LdMR zfTMmuYXGg~XaLYUK ld5 d42Yl!t_$; zy4HkiGp*ozo?WVh8(>xa{#^6gLiPCXgN=nRtKZ)Q*}VE)^^2KsJ!osQ^VM@VK-PbL zP(6QQa0j5B06Pw-|Fl>=dpqoFpIH6;ban2x+KtZ|H%>fWei1P(41c^>yKtwz_-$kE zS`&%gR6uZ5bM|y&ezA6Op?TpUOQJ`^7VyNUQ!F+CD45>%iBD~(SlWWhTwzI@cSv?{ z_wK-8*mp`S!Pomh#ZrYu@qp>AEB6<=72As` z*qz1E{9`9@D$|>A2y^%nojY)C4tK^qS_ks@PjBvGGc5Acwi?Z@WqLkevOGJVXGT8n z`6P64oXqEsgqABb6srK$L9kfGskk;plCyZlFXtyMH?(O4vF-% (k}vpSsX|pWNr5%VlaTT|M=^s;Z*7D067f_gF){(Dgg?P8iEf9r zT>P_h->&iDW5>q#JC*mti4UoN*e+Cpag5aVz^0QtCy)NkIQJuuBrq*)kEZ^N7qFR^wNzN}z0T?ZzkS)IO&o4kTz5XEw$qnLW00lF47 z?f8mO4(IUg-hwAcgTS!00tKN!UH4GD2HKUOAj*saRZ+$Ih=Jfi&>#pp-3W^*mVsL` z?Zq*si*5xmy_GxNzBf Lo7 zT)lFqdi6Lb53h^0^D~d`U#b7H2vsx0Fx2jT)wp@Kdg+G{uVmxb`RapHkI`0?eTRtf zzYa6go4OaIR?P$^E=4}oootFzW{Mv$ZG!qo;f}IMNF7l{G`zP^;hvKi^m9Kv0g@G| zD8A{=1mT+Q3Hk=KD-*OtNep|0k1;6_dPrQ1V@!a+t$T~D8}2TwP$3m~lZ}ow2|j}j zU4sTKLwiQjV>?elgT`?hnm2FMqz`N}q#u$vKwpAaqi>?v2_j5@sGnVE%%5t^-Gj(r zdPA%RV&>7kd$rlSP$9-vPS)>#UB7)vl#Mr9`?O7*GVjZ6+@`^!hu3N+<{b%YIwy=+ z(b058D{mgZ+<16Ka03h7^yW@(Jl%Ny&yL v0uxUt@)D&@DySDRQ6wt#JknYW>DT!|_EF(Z)+L=9( zI$Cmo61chDZ+B;JXJ@_{{w)yT8Td8-{O_ml`5ES)*s1+I>y1aBLE{o5F_I(AWE=@c z#+h(tTnSgkop5J72~UPiuo*7FWxNTm1Lrx@z6_t>Y1@?+GX8|$!8n;dMslBLB#-Q$ zc3HX;0gAGKayC??s25P54GmJ12UM`3A&U9|4cO3dL5u~fxJk@w^4-cve>N|V$i(Ej zRW&WAvL@T3ru9tL -}pX8KW zGw!-gJUByU0rl8Wj-p;bSsUu3C=V!SLj^?PY23qWo8hmo^Vv|ijrE;*8w$5UwFM bfm@Knk|+hG&`h{~k_bg3Qj-+5p-mLs257SljZ(A)&{i9|t?*2&&2)FC z)sgCdVdp^|yp*Y2{HQYf>GIV3)lW+|oTfLcr_-sSv}|(2Y9=FR^ZGL|Yvt~2_3F(^ z@vrTxH-BCE{dJJlPo^p#T+~}(bh$KF8NUW{<+FR0$+7lVp!Y54+BR7E EMpCW(ql?WWYt=QI)JI(D^F3eKv2paf8v_jry&z2K5#M+D#uWKdER+e)I)w0)`)9 z*FZ<(s2O$CHl+3+-Sw0;J=XoyR}I9PO>Z)pO=aX{(&UrLj4J7AZ2ObRGkPj*jS%D_ z;sarN@=87}6AUiXmsfMiv#GQ$6F2s~ik2K9aw<=dUrfPfLX*?<3>wGehwBG7`Ex3f zlEbQ=%@Yon1Yot4lypMln+Q~EO%1x2Wl#1jT1GRy>;9(QU>?C#=0AIT_n&%`sPD+b zdF@o6l0U5v)w^h^mo=H3RfgqLa85a`okA?0D-aPDQxSrO_74!}nI(T@VsGhdldqSf z^S?0shl&T6Le1mn%F%^=KQKZkiv3H$ZQ~ Q?imrVuAgEw5(HlaMAbAYLyySUQ(vD&$iVY8Sqt8;;AuTu( zM?nw%3bydWsQ5NCv|S+Js)g{xH%gJo-BbDbpBh4MvG;+$b w(PWQA7IB0S~_s~ z*2dclBaOzx1Tf>QQFa0o3oE%KHzXF;W-bm+vmF+bCJ;9q1PRYF&r8r6-A&C?r-mnK zAifb#v9B}^(jh^OmTh)m>j~S?uRzfWkA`*y+lbNex65%u>?rm<5L&2J+AZhvvtzxM zRdCJLR`~)(8m$6WXiSWolJBM&_C(CW=;q{VPs%h^Z_;5;&|cIb`yg%GlQIveAo(O- z5@t3^B!3|g6Uno13=#w}sCz*wxyr}$l~0P)m1%lep1k<*?#C;Cx&z5aN2Xl<{5PxD zFH|mzy`KhIU}oqvpLt?a2t))nsoWb}on^Lj1yKwPZfZs6&z?^JT8Q`Jmvh^Xg) z>P(kT6pM^Gq&i3xPrrP6L==7SXzzl6%l8E**l}%An3Cp?7{2cnyY0w;;p+ipgXwN< zH^hTLbbM%{x#XJM4&n4;gYPQ#pc5s4?07@$*>I!h7o$j58*YTVZ|z3zO*PyW!Q}$X zWed2Pt8kLLUh8>m T4jz)HN0?a*n^9UFBBwi&f*kb~;2R&fSw zUt!gP0ZFuBSn~z3nv=pakvdg`3Qe&nc@B(3_M!L=2px0e%7vS&bLUrQZ^Qkflj 4mAexw)0ZtOezm5jzC_8 l0;-pGkG22AekD0*%tX>2$}*^DydwKsAuJ`hzM$F6hDMV`z;8I4in`zLwpVV z9O{A5a;FjMfMS5AkfzC(7IuBt5ITyzwy;>}IAVxLX#sGVEs-l&1aunVep& u%~sI zl^c jXA{h9 2o;0WI#MZ@i@vjUaT-X*VuLkd!b{Ef!Eq02j^hNp#!hA ZD7ItkZ2{W8H3nX5J4D1EGw`Ab=zmMK9)th@ diff --git a/models/base.py b/models/base.py deleted file mode 100644 index c37d6e1..0000000 --- a/models/base.py +++ /dev/null @@ -1,57 +0,0 @@ -from sqlalchemy import Column, BigInteger, String, DateTime, event -from sqlalchemy.ext.declarative import declared_attr - -from config.auth import context_sub -from config.database import Base -from datetime import datetime - -from utils.common import camel_to_snake -from id_generator import options, generator - -# https://github.com/yitter/IdGenerator/tree/master/Python -options = options.IdGeneratorOptions(worker_id=23) -idgen = generator.DefaultIdGenerator() -idgen.set_id_generator(options) - - -# 第二层基类:包含ID -class IdBase(Base): - __abstract__ = True - - id = Column(BigInteger, primary_key=True, index=True) - - @declared_attr - def __tablename__(cls): - # 自动把数据库实体类名驼峰转为数据库表名下划线 - return camel_to_snake(cls.__name__) - - -# 自动填充id -@event.listens_for(IdBase, 'before_insert', propagate=True) -def before_insert_listener(mapper, connection, target): - if target.id is None: - target.id = idgen.next_id() - - -# 第二层基类:包含ID和审计字段 -class AuditBase(IdBase): - __abstract__ = True - - create_time = Column(DateTime, nullable=True, default=datetime.now) - create_by = Column(String(255), nullable=True) - update_time = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now) - update_by = Column(String(255), nullable=True) - - -@event.listens_for(AuditBase, 'before_insert', propagate=True) -def before_insert(mapper, connection, target): - value = context_sub.get(None) - if value is not None: - target.create_by = value - - -@event.listens_for(AuditBase, 'before_update', propagate=True) -def before_update(mapper, connection, target): - value = context_sub.get(None) - if value is not None: - target.update_by = value diff --git a/models/blog.py b/models/blog.py deleted file mode 100644 index 0d2fcf9..0000000 --- a/models/blog.py +++ /dev/null @@ -1,99 +0,0 @@ -from sqlalchemy import ( - BigInteger, - Boolean, - Column, - Integer, - DECIMAL, - SMALLINT, - String, - LargeBinary -) -from sqlalchemy.orm import relationship - -from models.base import AuditBase, IdBase - - -class Blog(AuditBase): - title = Column(String(255), nullable=True, comment="博客标题") - top_value = Column(Integer, nullable=True, comment="置顶值 越大越靠前") - is_great = Column(Boolean, nullable=True, comment="是否是精品 0否1是") - category_id = Column(BigInteger, nullable=True, comment="博客类别") - summary = Column(String(255), nullable=True, comment="博客内容概要") - content_id = Column(BigInteger, nullable=True, comment="博客内容") - word_count = Column(Integer, nullable=True, comment="字数统计") - read_duration = Column(DECIMAL(10, 2), nullable=True, comment="阅读时长") - - category = relationship( - "BlogCategory", - back_populates="blogs", - primaryjoin="foreign(Blog.category_id) == BlogCategory.id" - ) - - content = relationship( - "BlogContent", - back_populates="blog", - primaryjoin="foreign(Blog.content_id) == BlogContent.id" - ) - - visits = relationship( - "BlogVisit", - back_populates="blog", - primaryjoin="Blog.id == foreign(BlogVisit.blog_id)" - ) - - comments = relationship( - "BlogComment", - back_populates="blog", - primaryjoin="Blog.id == foreign(BlogComment.blog_id)" - ) - - -class BlogCategory(AuditBase): - name = Column(String(45), nullable=True, comment="类别名称") - - blogs = relationship( - "Blog", - back_populates="category", - primaryjoin="BlogCategory.id == foreign(Blog.category_id)" - ) - - -class BlogContent(IdBase): - content = Column(LargeBinary, comment="博客内容") - - blog = relationship( - "Blog", - back_populates="content", - primaryjoin="BlogContent.id == foreign(Blog.content_id)" - ) - - -class BlogVisit(AuditBase): - ip = Column(String(255), nullable=True, comment="ip地址") - os = Column(String(255), nullable=True, comment="操作系统") - browser = Column(String(255), nullable=True, comment="浏览器") - uri = Column(String(255), nullable=True, comment="路径") - blog_id = Column(BigInteger, nullable=True, comment="博客id") - - blog = relationship( - "Blog", - back_populates="visits", - primaryjoin="foreign(BlogVisit.blog_id) == Blog.id" - ) - - -class BlogComment(AuditBase): - blog_id = Column(BigInteger, comment="博客id") - parent_id = Column(BigInteger, comment="父评论id") - name = Column(String(255), comment="评论人昵称") - website = Column(String(255), nullable=True, comment="评论人网站") - ip_address = Column(String(45), nullable=True, comment="评论人ip") - user_agent = Column(String(255), nullable=True, comment="评论人浏览器信息") - content = Column(String(512), comment="评论内容") - is_approved = Column(SMALLINT, comment="是否通过") - - blog = relationship( - "Blog", - back_populates="comments", - primaryjoin="foreign(BlogComment.blog_id) == Blog.id" - ) diff --git a/packages/PyJWT-2.10.1-py3-none-any.whl b/packages/PyJWT-2.10.1-py3-none-any.whl deleted file mode 100644 index 6195c57682405cded6020754dbf8111489923b9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22997 zcmZ6y1B@ 4Mp|7WOQ}R&&LS3%IPUg1B9&lN1&OME zn6svfFm`mAOe@(>UEJz-1C^Sh5~_1GQ_EoX-3ZLQ=8_1I@Z=|cUOBWeyS7IN{hccv zQc8I?l?$WRA5byGOSwsG(gc(maGxBA*9`1J`Q%P#G#QqQv4Gx{lK`ibA^1A4l8O## z42%RnyWB4flOjk3q5XDTF`b-P-P9NcgKmO53C`Snu5{4=P)J67i8gk{Q4ufW5~iV2 zYFh3Hy Kl3^ z=8C=|gU`3c4byHWEwen&d=i^`$&;aMCM~W8k{W}~pidg&(Uig6%-`EpTyeLmhXYj1 zZZ(-tv-5W+S+@~i!)2m#B)2eGD?!0@rc)JLSXmGG$QsI&@Z%GzdvV)EGz3}0jG9!S zCi5Zd8fnkd(kO>rmd1g?%O5@%Yu`N%jhQv;Dm0%Lf4|7oXoO}rma- ezZYTEF5-S<*Aq!-}gBl1P0R8Yw#RZYK*$$ws 62)b=v*i*zJBO}CRJ zEktw8xNQA8kv@-by6U=fH_-~FT}GiwO15Ra`|<6BMFJD>FZ@h6{;NLSge-Y!RKKcw z2?-_N2 nD**@-`Re%L;k3A;_!T5apoq`$_#XHjSnj za*~8H^8EtI+cpzLn~esmH#slSM72D@(N3A=^PYDwaM!mn5LDzw=zCuOWwK=q`M@~i z2i-{Up{{dFoBgsH_DBkelSGge>@jD2#yWFsiFE~)?c>Y6Cr#2O@=4#0aN-CUTnW=m zHEX~JOsgif8cDp$$9+Al6cO%A5^q#Ty4S3`xMJy?7*6Z==$Jk@4pLYSdMzvGs>qY( z#zX f$E?t#Xb0N_3xY8 z#6M@@pUqrAXIE^Jq>_xw_=uAb`dvC*aP40{846ZB%7r@ V-k_M?ZU=y8_%DKKGX{ivF$_#Vr^0^mthEdp;S-`vExeMi>c`f>d3n zc;n(4U%6;s6{!%qAqdr?oO6%5#VQ{>cq|2kZs75@yqFk0-|l0A!d_Q7Q9KP%+-73+ zyF8m;5Y<;6`?uQNpP;tPJv46K4=i?;Y@gcL%wH?`l+P`zc>&zHCPN%rv@bgHwRrLe zl|%F1nE~aX%G|>RcXwzq9vy#?4tb*bl0W5WB8_oo9Rk(Vc>N^yu=j(Xmz2jc2w8(9 z;w(R(m38n&ed)nfOj)K{!7kk+T$g>JG0=X1X;?5w&4iz+wD0f@dDcEyw5Hy|nqHZ7 z$&u-}gSZCc_IOUX>uThthU8g%hWYwXhWvF6x}A0CSIJ~+JWvMS`!%zBX)HaRf2FFH z+Vn8Sd^~t~4v-?jBc $ZI0NA>D*1OK3O4c||dD-;qzo$Bu~A6KP2>e@zX zLh;!R&!J7d1ybzJ);+MYg8DJSHoENeD*q91jL#&u>< 4S}{M6 zOR5HjaS;vkRVHkJpb6cEvPRGeLWi>mt`4>DC6LNQWL?H*lTC#70LUvatwKo5D1>yg z3VA|Orxyz^Ywf9ut6MK$?R7lyzCN-WV)S`7YXk=F%|Bg&zv?c$wru}6lH-jOB{0dq z`+%;*(jcz|_YM3#kWyeXymd^NT=MVh-&gVOx?aqAB>deVg)jt?#ojmD+F*_!Uf5YG z`@tg20Mu{fFwDWkx#$hjGTxUOkQcxQ+!cZ4ZhwbnqYVSBu9wj~Uo&S@-OJy 8%}8Abf#{KE0uZ6h2*Xwp@@T;?vEG!G*@>l%@^^9N#RepPi|Dp zxuBnTffu_dWz!s|v{I!mBFPTcRuPho-Ca$?{-N_z&Lb~q5B*;qI&6d*`66&{izk0r zM-V`P{g7RMvj!o?#ZjiLf9vjefU$1yZ=E4&K}5#@m?)AYTG(-R!8o;`8W7}V)#r?b zi2&kXEF>!qt^mB!SheSM_vi%z(1V0i=$quPXU3<(1QpgP^%F98EoEe@7bf;(Cy) z%%+Jkf^!8J%u~>$yYJb^AMULhQ2F+5i8tmfiK7BH{5677!~xivqJv!%#LxK1wuYo` zw67YK`JWagbrFKHiQfm(4)~0e@y-Z65b3QX-~oPH=Z %z3vwkeES=o}z;y z9Po$7=A0!4yU6Eo`K9WCk2=IOJHVmnLtw84870!WObHAm1O9A4Iwj*UfR0i-me}O{ zAtcPVkS3622i!}+Gp`ViN=M}BO2&|IX4`%c52rr#rU|<;WP2tkPP-pQPl;5+{dvGO z!5l+4&GEb+i)CdTa&q=BjQ}r5S=3^Pv`(=<^ErStV>{Qvm__z?68s-m=QZHXgU8QS z0#N4wxrhA8%3Om-PpN^_vl-xvmk$ftG)?z Cd zY}+FBF-WMgI=?@7(I#9$%Xv{)M$VbM8p+D;Ku~z7tC>ntkfhbOJ3y8|Qe?7I5XX?Y zoo9t`@cBffI!X}?{;1wc>4Q>`gV81x;JpCR*puDB$$1a5NrZQTOEZYAU;RG<=%FLU zL3hEl_ jpJV3}_Pb@)Q8hu9Y)6m*K9 ^^sPZWxXr0 zQQ%N%=@x~#YlqaY)$K%2 #~%Jzqhx?rKobB!om|q$Y2)x>KYLvIYv2ZLrYf zk(jqGAX*#jz&X!iaFtsmHB{|=mh-N*V&30iz`TXZcR-#pt%W3)eBB`v&+T6J7b-y1 zq;V3;zSwY9qDQgho2wp}LKqdo8*ibz4H}@aL^)`JPsh` q8i_gB#eA_p0T=v`b89e*~WVD|jd08`XVAhphtYZ}kkRU#p{Mne(`I zKaR?;zWqcn91O)^SZVde>qi~V0@Ux|1VGCcbXT@yz_Ohvx?bF~c+uC^K~d1$_0#sN zDmQx(S$?FcjT@-42u01|%$a4%4zdCl$^x)uR53uD3}u1bGD?J#vP8u_2aR7WY$i~> zaYyO-L>hWcRB|JO36}+%S1BU4-MqI9-4)+;0)dTX(L%xMRN4i2 GJjVtLC;EX7A 6Ty=~e9sAj`1H7p>=BnG<8 zvdZ`}-$C__^`};Q;F#^zm2euuT{G!qJy*eH$5>h+FpDKB)C@zjrwYB=BjC=d1w3ie z2W3hIMFl|pd$V)cyK!lHhQ4-JfO90@ABbupf5j2Zh*AP${?shr8lN>s(NpQc>eh*~ z#9h^}zF;e+yMd^*x#!bm4A3?kxjkhEpc!XFU05y}j$*X*0!@4>m%&&{ E)b7svM;#QFq2o(i-H`ld_3o$e}-~{y|l~_u1o^L zkKA2cU<~-x3}1*Zsi9xt>5=ui4;K<^9~5gZg2`GK#n#V|HMR!jw*=+aobhc2;=sRc zhBqa$d;NFnARO0mSFh(6o?EH6(x8eu+FpRyqcHX^+qq%*7^-zx@w@N)okEDkqCa=k z**DPwet$}(US_?iX|9d^WEwEhWj{8MT`O@(Dtzyx2<}2p{q)=}-iOYht0hR6iOfzs zF_N)e9{FsYRt5 p4L#XZ)nI38F$)3u+J8mkzNbylz0mNFi$jY?0b5 z<^da(eXA}2*+pyebJzF&i?8&!dw!Rt8K>@M6;@2U2)t5NI&R>M(W-6CG`hr#C6uDh z{mB9ReD3#rQAoac>1Pv!l)wR%9jll^7u^~M`BU19DxwRXVFN*y`>v$8*FF(;Oc;3R zLg)eFi8JV{qnSWtGG*n^m`Qh}1NRAD&Nq?_EInBuB{hW*NA3gI!x85oFz178sJuW# z>#!ABtHQ;{<<=or_Ytz?Z-Q)33|Iz b|{|Vgi6FoHE;MblwVx-Q(Q36svqLJF{eBE8&M__!e0w5SI=|m>h)(f=&>D8%!eK zfchdx4MwkL;q7IveHZ!qm`oApH8NjyToIX{y3(Ma4KlTNc6KO-X36Bo8Aq-OGk>p$ zxS2R*!Nk;LCp5VV^mVPC)@Tru+MQ_ID{D=9uhqDlaC_=>ENo%*JrQH#U+optA)n** zaYF%rFaD~xfi6gxR_ex*<4=X4F;lE+$oEa0<&uV&GH7U6%3H)-7>z#er7(&2Yqs!f zKS9u~i(x8uaN4Xw1+I{)+MO27$sXYpDZ4zFHy@W*vYtmV$=8c@?dVkA`TV(fE57(q z_&UbkfTV?;9V97v2i`O8al$AWdkED1bp~Hw<({kUPmC*R%Z1WuB2<6kN=MkYtK>Q` z*Q*g s^Huds!u5vyWpxP}a=bsl25jsLtO^&v=(jGO2u3 zcqC!I>pSKS!p8z@omXX{_jsvFZLg{TK*04qAq!!lkP3o#FO|~9IGr{T@g81AaOFw5 z)o}f%ZDE`94(Y~O`4AeNWT(=DTEu%q@4i#+x30MrfXt|{f?v?6<=zIa(pP__6es(F zCpK0#7h(NZpZbFuPDJ@+@SS13FEO2-d}|k8RHprO&aK6Eww&Kz4^ine;tEY$G!(=? z1Pe@q7xmKBC3QX<;D!t-egeF0ViRmfyO~5$<0 DJ1@mFZd-N{EPfE{i~?VQII1Bc0mja|=1T zEDHgx*OybAA|cMXI%tgrOf7udM|U@yDSdb9YC%tbp>_B<1Kj8yt#Pwd&D;!QwGq}j zWkq=l?6Y$HQGc|h7iv^`^HMBvzb&a3402oOWYWe-;f{}So@|Nwr4mj;pOE0fpAV@z zdqUcsY+EsP+Tdw-+_qM|@mKGX^9?la(5F0Gw?$qJLg&isCHIxAJCxu1SO9jNt1y1m zXDd7U?umq^GXnYdWOz*DzuLFVP%Se>J>E)|I3edn;3yGx3W2%sQt#)@=g4M_5OdV# zQ+XM7nv4 x}Xr1IA4w;CgQxc!@`)#v^ f5gNW5{UnG z2XpKgfu2P$r|8N(+6*+|2Vv$@)+&5k`ChmwVR@(R=!p=WWj?UW(+!13aJC+*)fzx@ zA7+Iy1>s^2^P Z-V2{6Q^zp7%*m26t|_uRi_^^8z7ZEK?jDp?xqzEOVwf{}iU!iXvXH)o zlAlSRSITwe%KR2-9KOvjxsKPkm0?bz(~l`oMZivb?*w~~th0~ Y9Wza_RIvSnp7ZMee?OP-xm$R zv0H+=IqwqewFcN`X`3Da{6%%m1Vq-v#Z bwteHfzKzdu{`%DWF^O^pBeDtLVP->(avFGut~ z7d~B~Hx$K$*!(#;uF=M&Yi?fUs25Id(9YeFL38IJ-0C`DEnHw<0=73dpJmrLI9K6j z(x@H;vEBVKH;2dszH$)H?16d^7ItT-A6MqnMQUc+S~ItX?VN;YiiDefXAN(}`7n%a zI)> {{Q!KJLqKnl|GJb9wgbpF2jKdAs7ByDHcW$*m z-<#B!&W|i)YWAx*zg;Ru`N*D2v?`_cM n z@F!IH*4u7iO0RdPr^c-6B`r&J)}>NaWTLZYhM-SO^b;S<`EQCQSHG{t^6o)P4Q$(I zi~XVaDMOJ>#8!vedw~ev4@V6_viqCY&odN?^3q`@(`=jtsfW0(Tj;)MoH@VYpjG8R z-OHVARQfH2& ue74MwMw(DkQ z3&_rRoA7wUC|5hW@HbtG;hVVXJK+HC%nEYTo?F?^8=Biehux?ToD*e$b7MNIjHM9> zj*S94bBw;*jvW{3)v^~(W X!0BND(&M80p$cj;%G>zeo$_|S==c4 IkL4>eNQQ6T zFN+++Sne`kxIY%lv!SBTmdh;0Uen7dBaq`*PC}r(qV^;S;`nGI?78Lf$6$w}dDL3C zo8&}AF@bp$Av}JDsBqx8(4+qK5ImKqiZ*^=nyiqnM{Yp7q20Z;er>}C9_B;cUaob= zUV^x3sE#XzAk5I&F@_73GgGr|=lgaG?g c%TC^xhuUR~6YTb9ViWI@DPH4al-YYGrKUUWY<-D)^wrxw zi>65W??ldOmR&CV5T>wz9wN3LD&ui3imqs5^sfN?!vIvSW(dYZ#n`V=T8(Y3ni#L7 ztCVftEVN_2&t;x%>> %Y5Dv#@IVE^a2Q|i%*U}?NepICKzup zbc}o9K<|QTA`t-Gu#+knT6ObF8c{{10xM%&FwVg@$r0tomx6TnZnjvwU4yX|<~oy@ zf~sp~j4%n6vDz1KfLH0>ClvGPJ`{AZllbpnOSWDO*QvuIXdx=w+p;jp5SkroTVR)V zcF$;W3<2or4T+^5XgZ<}YovPC=^6Lz=}QXzE6bkZx6R}$4*flm2+->r(L-kEfhbQ+ zsaXPGikeEfZBs6R==O^+k6uFtyA%mzXoO}Tr@i8IhYZWsHwruR9Z6s6B7F6UDqV>k z4FzAQ#4=z9=*|6kiq|-4yf)oCRU 0Vl{(UvihC?iM!7HG0 z0;@}F`$=?S6ZNy{4?fzuS9I7y*aE+Pr+|9ibXw{>_%o)8>}(^fxwVk p zmVX*y0s3ZLwvffc{Vq2X(T$8lmm;<``1*^Qdqb=02oT6C-XVB)XJF$hJ{X36qV`j_ zp8Ntw)iQooh~cVdG8#%ZkYRDo577VRjU3VM16*N&fbQx3JBs}eX@p~Kzae(t={pqo zWiW3LVZ9Cu13axeTZ~B|fHP Hb}T-<)*=l?6~=l=2>hL}e9gPgRo zMa%BI^t7c@tuK@rq-Vv7_c#uBxr9@Dq5D!PhUB@WNw)F0?Z)bE)1OFmU~#{+;NyxZ zS2XgVTf!cpHFE3E!3P83A;t%tv6nBSb~-z4rSo%(<+)g0Th+CN=3!9$^|ESxIi=Os zR!5D!CV5(lf{%JK=mNU!+QiL!X)r12!Kwx@{6OTOlB;5P-dat!@tf(>nY;68_ZLY^ z-=Osdh%^0kGO%8nsgJ?ipVGD>{EyyPTb`%7#$~OSUXia$D`19vVT135)4WG#Pn% ziQDf97si7N0}hA&Lh23{beJLiiE$Sm9!sX qkbz5kWocP!jx`SO~ zPL@^+w$HZ|mMY2W4W;mdP*rpMavFD&!#X-HH4IyJqMD1)%?45`>c9(xRxq}`;JHmM zi0~K*N?M{=dD{V)htya;Oaa4kc_1=tbd9S~v`{9&q8G%L?wFhh=e{A9 )0JK`)O1 zwtVyO06|op6DBQsx_i#O$GZZ(tx1=}2X_l3uz{UC^0ib2rTU=Z1B`3!Cf-VkWkwDr z6J4z%5vVA6{NpUPJ0%Qym_kcs+yslYvI20%_mI@Tqn3vGL;2)&({bsMf`fFo44O$` z2}i)21?z<%odOPFO0e|>;DiGg2YPoOmM)+#u`tD+F==q5JqWKonQ-`VXeT;>6q3_P z0Xefgh~VzUwj1j5_1C1SpB_XYkrlpa#E@Dm8>M_rU=2(waVd7miD>Mp{APYMoAMw2 z`&eA%L~Wrwi`SZJRj{eJ`z9{>Fa$IZLU-2G4s(X=MP;Fd581t9j5b$O0`uDoY8Xas zp~G;7IzZtg)sa-m`jX0mk(MA|gurvW8@_Rv)Rg-OMWsh#!!kY)hclzOY-kOrV3BFL zGTdVO%MJ39Sv9ei;3DungW^{iK?83yx1>-KW2J4N6kuq8e{5O`BjsnT>l%hz)59_k zBdTqDYtP(Px5@4t05pNIs>kywauZK0x_gkp{r2sANM9c!LA|SnGxk$4{HU2XabX#= zBnJUQLS?f3d3FcDI#wjF*uQ# 085u{4#fPYJ6d zwXOg4cvr2FJ4m;#LV!#i8Ed8;Y96u=OypOrux~kw!IF+pD2YW&5dBZfzzVwvzYYeQ zH#AQRUO-M+aGjLWiysoLBpe@jguuh|mNSY9{|)@xw6)hc`Syu?Z`laQaC6?EQX`$* ziB~s_AsjlsBV(7(rq6in11e+}KkjRdB@EC;j|wv5wU`8MAB<)_h%fH&94X`=WdQ zs&cwl%mpleTn@v!JzNvQeS$*9$Ph?ctiK_G=#z9LH6B)Oq^^0)<>|hYNnIoJIP%s& z_d0~$EABwbjYah>b^!-G6;)j!9dg%70rr8m%Bwg17IZ3RfNzpOwb5}wP)mUrb=;3M z&MDD1E+5B4$2l?vJk80{d7!^ Bb@nS))q^9~JUFyW@sWr0ay=%{ zUwOPo?hpxgJ3+)wG^qjiRV~h!e2<0XF+zc&M6~&a+XS6V&`B9nwM+u%wSfYdkpTF6 zU!qo|C6YjXWpyqJ5B_KBQgw_w01cYHceX`>vO%t&TMra#Md%`&X-`6){at{q0qw1H zxB%iDu1F|@y0Sp1yiVLQD*KL3@E4{}!fAGL6wgFZa( ZZ&vJQ+uE1BUNlXai1j z0t(XrY*4qh- d`Gkpds;6HBSrdP`T)^aF^zkhCNOY+yY(iOd*hkozcQ zi+!#fx>t~}VyYN_l%{W7kh;y9b`?XnEKk|R?unFJ=soRW_eFpkCBc%;bzfcj` Zen zf~gFyG@^pQZGzr(;1ue2Q%d9}L{j3y@X9sI*1hLbded8dL}j**V6AB^*L^er#MdK- zSr&I`rd95=MM(%()j{Hn2e2!5(7uo(Vlr(j3x9_w6f$!WW6f`1G3w|%);f=Abzqr# zd7|w*fApP}$sreiqO-32j)hQ1rc}+ctLkncC$lXg#vCGIV!W6#2n!v9C8;okZRoBX zZT0Wk=>1cns5xu!A^XO-D4{@D9rXf?+)=!Lj bt5{9mco{k zhI-MUYD73}g=k>!*EA4%4TCg+d=_+8bef6aP1nhR{y6PNW 0_H=e#B1OM_E;MhBSkwyUtFGn( z3$ai%%`+s!{X18wJDBTivb#R6>134QM #*cLprGHiQEu)l}Znyjng3 pJOB-C-4m-jGx5fF=?{!p(gR$3HUpXoA~m+6 z;fDcFux}#xAsA*ee_b Et&(I #+q?_ zZTgkwDw=Mc2Afs}nz8*tN>kd#2Bf5jZ&%XsejdX8iqXiJ^eRVRl#?!rgYn7KwOD&s zFo1T%=M;J#vUcoqKA%Bk$quJAErXGM#`w^@ki=y%{gq#Ihdr#!s&Ww2x>Wo1j!? z{X>4+-5Pu!BT?qH)A~O?!?_qE5g<`G+s|wF$N7?RuOGoT5j@NUa#)|dCz2WWn5wF3 z% 1H2P%%{{WO9&c5c?H4UHej;whxpQ+d?c>-X-v(x^uxJgoQZBMF z6gc+1mDMsfuW@MQAG&yMA8k{%0-p8miqctk&Q){CU&n?8VA@$1b_n#pKdpXeka^I8 z{nagdQtF0Ko{E&APb=7c^Bk5c>EM)Z6w!mxOc<_R^;@&9Lu|> <)I 6rm@QuhT zOuO0c@-5&)oJQXbnK0x1-4a4B|67bGpS45$axImjRmRR(0zNwcx-lK986V*{42QOw z|C%rnTr-C`82eR+KDv2eBW!*qh}tSPnXVAaa21H7KHZ=cj2|-wECg^+im0zi;kK~| zGVLj;fZ(Yl2!*AS$YMy294-A0HP#A=0M|Q})c6-uHXG1IL!n8t4C8A bV8Zs^xB-l -JGCsNiJu@ ~^rXnT=z-Kj~ zbGmb#Z~FrKQ3BowH9x1M4wi87LqD}u(!143h$H?y&n;u_Hu&5f@-=_pKR*=zDi^KW zqvT$&DtK4h+(=}hD>nshykp|lWq}8Gsc+Yw8O}g6&6sw_V&&pdA0kteNOcUj#VC{X zsOT%GDzBiG1wMlT9f+)e9Smiv+yz_RRmz^r3 YG_rIoG=VN5q1Oov>cmq?5vmS)GmUv&C9ln*dvr%Gv+ zCgVGCVmuK<=Xf!%mTR!C;PR^hMTG*9?+0~eUgBS35?+4Jr>+I}1KSLcLV>cRimIG~ zBa3sPJ9m%|u?4M=3T-bhgP0{Z&G};?+AI*AP8%G*QBlRN9XUnqI|)>Wgk2^;hjo&R zu1tSmGO=KKkSHY*6t!Uu%>0Bf8JBG!>B7(04(R_bN{#iQqml%{*9x!30EinsT5~lh z%9<=Drc)s* %) zYGQf?&8v@-rUm>a`0bJSB#VQy!8+ui 1TA)0!BAjA+;OY;HN%`w}+6tI;SvRGLeud9`mk{DKlL^&T_dzo_+QR+kN zy3e3C9FDy}a0^oqD#MBtTcMv@Y1`e|*HMTT18xBkM-|Mgj0LS!Xy~CqU9TYlCCW%t z>Kix=a1KfO3<@7+Z}y)zKfFVK)EiYE8FIdw0Q%BIICvI7RND&YFZ&fp#^!U1XTvI& zQ`C{XaBSe`ch5EZkjDl20xXvk )$ UmM*p3W%^dKNc+zCUqO!!o&AmPF+;>j=p5EefT)T3}m6BCfKFm;}AK%7F5V z0;_kwW+t75YL;bTy|k`^2eRVN`@&_Hw0MBJaJk;lYjAxXaLI3-%K{g)L^>;I+0P3s zR|5Mf+@ker?^|+hjxWneUtRi8?ptIPHqMdbe(}Qm&mB*HO-#3D4)E0PjJgmRw_oY_ z+5y CsIZpDoYW1gQhUf^lDcFADLyuP}5!p}u@eJvU xhA` zi!U}Q+ D9+c+mu;g47sjl0+)V|q!PO|{bgAW2kcsKeA`zgO&D2bW+@zZOLyO!M zdOy&wW712SWEpJVyJ~kM5i}i1xp`^@Oupxu0Cn&fhtLc(rvwEy+4L)Ws7|)ao@u5$ zr4dVj$Asz9_(mJfa2OLb9Flf93F|TYMM6e{OLdwx7Dao Dz#io{Z#^vgZKm*AD4nx# z?;bQqE27*|M)}V^)OWcBv-_9W%!?!_Yd+T~acCA8yF>kuUFoTUUflqOr$8AAXcG|3 zx@c <^h@sZ9Ra-6&UJ%+;0(7g=`i9&aPJC75f8v=oR$#0`?Sldfku! z-;1N<{0os#Dw@`qOx(HOL$dlw|kM0Ss^s6knfj`9px_VCYZh15y)F-EDXkpP+<>R&r1i0jebQh9?Ya zKCx#5F_XFW?^Da&MGmhDhSoZy<$qiujfKAT0L{yG?0SQs8rKu`>^CWFJ VA_TZMz_nuJ5P;_XU9lY0?&1By=q@ jwO#95!M{1;Cz9Ca(btYq5L50HC%?oim!gcZN za@skxy^LW-{%7`0C$ux0D)#qggNu7VsdsaE6qfp?pb#(2bdXMtLzJm=ehhBa?^0*D zeeQckKAj=dBaDGimaVZSbBWB6>6j3qEX<97jp^HI+TdCAaFiQB CCp4=zK)Kd&@S(Kd9N7~LDH_q;5@q^Z^B76k)jfc(x8Q&UX43lQ zd17}y7vz;C;_+*L{tdvYr2B#)0Et~$EuYZpQWuIsA6wAqS%{_72@cp_BR`=*af
o>GM1&jR*Wtr`Oa@-jX<#Y&aE%<8&cKb&?E(M~khkbiv z3v&;7Am8kfDo~uk_QzL}vS!wt)$5?f>MO2x*=SK_LXA^} m!Z +y_M-=lsl8+E4ame9?L|_d!>;viOl^xCP2ZlkZd<7Rf<`MahAxm zlTvzBJx{V%DhVdx?9aB;E#R7$rYMcmM6>hySv>)1)s3}XLC=*IHPd~R0LW@3jo#e0 zSIm&@M}N;MPZGsS#z!0!h~?%iCP@xqH7SyE{PoABnr(clNMIK4ZKnuQ!@FQH%(mix zDHYma1zzi20xB-J{U>B44X*tdwJ(U{d0kcCsuNAyW`z%5Y+?DWUZvBj)%~jI?3ZTI zQS4wdcs2MuL`ntr>xs^OX)*F~Bc+OfpnBc!__$IN@(lm~nqI)&*4OS|`x+7?5YRt{ z=zrSREKF@2{uw>ypEPit5vlt_z3m#DY_vd}%=1*aLIuNeV*y5#KEt25UUy`zx_Q6H z?UKg wI2l$KXT3S}(f4N1Eqd9xgBdr|dk(yG?H4p|PZ zY+j#=&=^ZeSK&Ioo(%=bAWkb0S{}w +Xw3TJg`$Z7jtFn0*G6#LGq;o_ z@T%mmLaPiPHnEH$I j#a-kNOy9u^Dy}eWHL+Xp7i;Ud z)wx<@YOct=BrU1}J;)C`y)QGs(Uz@Bn(jtuTOV@(goIk&{F;&kPn@2)?&Fg`^Czhc z-H1ZEaNAkW0T6OC3C9PAoo7_L*f8%96Cr1JBo!-Dc-YbiQlo|y-yS%U-?rwi=w955 z_0wiVXJUnc+{YDpH)eA1tLvi{qMGdPbL%1T(~5Nkj$nE=ZHFcLCB&CT?Hl1@b&dsc z2`A|!fB&5$S)Zd-LI3KX!cq2s=V`UjaXxd^T}ervM!3YFH`>+2#Iwbhn9rx{-$~zJ z-5I6;7O~<{f5xO5Iu@6T3mnO8$aWr~YP7x?Stn;DLPa(H6%Qdu**h#xE`#<-`j;$E zcg2?6Rk99;4FA_HHRq4i#Q#Nv9#9~l|MXh^r>pXx=0e}u6rgWxXl!Bn&p%U?bfpIv z(R^lV+jgacwEI>9z3yZM!6Cikk&V*q)ECIeu$_OlA8FWJY?$R+Iscp-v}oYk^14@n zmsi#F5yDr{8Myrz@mLL!pY>+rn&gGm0{imv)#Ppm=QS_ -clJ& zza()kJM2W3XayMpg(VC+nB9En{53_M$Q=z`5 {H|6bO-iLuvIGgk4h!GQSi9AC{ zQMH9MXkPQfZTJj!XwNO#c9Kun;M69So&bbD#ITyKEKbv3*(`>B6 JNio!oo z7TaTcijqqH{4c6_HKU4+85{_x2nh%X<^MOlv%ay7rK#P2z^D>+ZTk&2G~bt6qNXqj ze>3wfc*asg;SP|CKvxT;%RWXhEv|-)(d1uBwp~O&K4EB735nT&bOM`%liSU1=YrMx z8XU=g6$ECLmwi@)NvA2OUn|jJp+yBLl~(Rp1vNO6jOjut_7uYGTnHuI(ed$n#Xbtb zq@qU!_Kx;>?I&8&3T+ZoyBUbz&Pt+cb}D}39K5VD>+|Rc;!02XQ=zfud}fJT-VNQ` z%)&;`cX($HZahI!lMJVcDuSgN{sjsd67^+j)pbCY@NIkmo49*_trUb#p)%fY(WT`< z`s&77)WFgb5ED#%nm>(KQjj+Fqf*qtUcP^cUJn)GSxmFXSm2*xJ;9PiH*42Jt0Us4 z>WZF ^HQFj_a zk#EJOD*gv$A$p&Q#RY|521@*U!ScLAL(S-}HX&r(L>~}k!J(s|c6!6FaW2Ewa1@zX zJX%NceM2HfhgUMh!xr4Lp=yf`aCHsK46t+1OhZy>JCxH}sK_=ms#z*{#$+#DR{nVM zPZ7b8RMz!+)z9Iko!Umi+DjB3Wv^0m>M~F^>y_#|f8QO4mw2@-(5RGWr@LdqR>{CV zAQKfE!%NF1Bkt2lOj%oeXt7Fd^&hXQfim#tv!!sTnULS!&6fy5(;w-e+(tskvyuqA z#L}v`t5XR(evn#GwO8w_`v&P``T087#2OqlmwNvo_vV_7vpH6B{wkG9azu{n4_f%s z*VWGny0FiPEU>bupqGk1B|LF;SU{jk)-=HMt2GDNvqUx8L_X4vWwz7~1Jj<8CZdXK zH9Rl|z>Lp~QG|+$w`fhviM67&c-P15vfyunYqxbJ2|n