From dfe0e65c2aa95d8e982af51e8ab43045cf679382 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Tue, 8 Jun 2021 11:51:32 -0500 Subject: [PATCH 1/9] =?UTF-8?q?Modificada=20regla=20para=20identificar=20r?= =?UTF-8?q?equerimientos=20=C3=BAnicos:=20merge=20req=20!35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/requex/extractor.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 36090e2..e138c34 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ res/text/** dist/ build/ test-corpus/ +TESTING.pdf diff --git a/src/requex/extractor.py b/src/requex/extractor.py index a7eb741..9334b3a 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -623,7 +623,7 @@ class MineroRequerimientos: else req.titulo cambios_titulo = [d for d in difflib.ndiff( titulo_menor, titulo_mayor) if d[0] == '-'] - if cambios_titulo: + if len(set(cambios_titulo)) >= 3: continue descripcion_menor = req.descripcion\ if len(req.descripcion) <= len(requerimiento.descripcion) else\ @@ -632,7 +632,7 @@ class MineroRequerimientos: if descripcion_menor is req.descripcion else req.descripcion cambios_desc = [d for d in difflib.ndiff( descripcion_menor, descripcion_mayor) if d[0] == '-'] - if cambios_desc: + if len(set(cambios_desc)) >= 3: continue if titulo_menor is req.titulo: req.titulo = requerimiento.titulo -- GitLab From ffeb84d308b833c1fb340d5ace2e5a685a26edf5 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Tue, 8 Jun 2021 12:59:48 -0500 Subject: [PATCH 2/9] =?UTF-8?q?Cambios=20al=20requerimiento=206.=20Mejora?= =?UTF-8?q?=20de=20la=20informaci=C3=B3n=20extra=C3=ADda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/requex/extractor.py | 15 ++++++++------- src/requex/modelos.py | 29 ++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 9334b3a..8a337cb 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -528,14 +528,15 @@ class MineroRequerimientos: descripcion = descripcion.strip().capitalize() titulo = self.__limpiar_texto(titulo) descripcion = self.__limpiar_texto(descripcion) - if len(titulo) < 3 or len(descripcion) < 5: - return estado = 'Identificado' - id = len(requerimientos) + 1 - requerimiento = Requerimiento(id, titulo, descripcion, estado) - if self.__es_requerimiento_unico(requerimiento, - requerimientos): - requerimientos.add(requerimiento) + try: + id = len(requerimientos) + 1 + requerimiento = Requerimiento(id, titulo, descripcion, estado) + if self.__es_requerimiento_unico(requerimiento, + requerimientos): + requerimientos.add(requerimiento) + except AttributeError: + pass def __obtener_pronombre_anterior(self, token): posicion_anterior = token.i - 1 diff --git a/src/requex/modelos.py b/src/requex/modelos.py index 436eb1c..129a31b 100644 --- a/src/requex/modelos.py +++ b/src/requex/modelos.py @@ -16,6 +16,9 @@ import copy from datetime import datetime import logging +import re +import string + logger = logging.getLogger(__name__) @@ -246,6 +249,9 @@ class EspecificacionRequerimientosSoftware: class Requerimiento: + clase_caracteres_vacios =\ + '[' + string.whitespace + '\\.,;:¿\\?\'"¡!·\\(\\)\\{\\}\\-_\\[\\]]' + def __init__(self, id, titulo, descripcion, estado): if id is None: raise AttributeError('El identificador de un requerimiento no ' @@ -278,9 +284,9 @@ class Requerimiento: raise AttributeError('El titulo de un requerimiento debe ser ' + 'una cadena') titulo = titulo.strip() - if len(titulo) < 3: + if len(re.sub(Requerimiento.clase_caracteres_vacios, '', titulo)) < 3: raise AttributeError('El titulo de un requerimiento debe tener' - + ' cuando menos tres caracteres') + + ' cuando menos tres caracteres no vacios') self.__titulo = titulo @property @@ -315,9 +321,7 @@ class Requerimiento: raise AttributeError('La descripción de un requerimiento debe' + ' ser una cadena') descripcion = descripcion.strip() - if len(descripcion) < 5: - raise AttributeError('La descripción de un requerimiento ' - + 'debe tener cuando menos 5 caracteres') + self.__valida_descripcion(descripcion) self.__descripcion = descripcion @property @@ -459,6 +463,21 @@ class Requerimiento: + ' bien ' + str(estados_essence) + ' o una ' + 'combinación de un valor en cada conjunto') + def __valida_descripcion(self, descripcion): + if len(re.sub(Requerimiento.clase_caracteres_vacios, + '', descripcion)) < 7: + raise AttributeError('La descripción de un requerimiento debe tener' + + ' cuando menos 7 caracteres no vacíos') + no_palabras = 0 + for palabra in descripcion.split(): + if re.sub(Requerimiento.clase_caracteres_vacios, '', palabra): + no_palabras += 1 + if no_palabras >= 3: + break + if no_palabras < 3: + raise AttributeError('La descripción de un requerimiento debe ' + + 'formarse por al menos tres palabras') + def __str__(self): return 'Requerimiento(id=' + str(self.id) + ',titulo=' + self.titulo\ + ',prioridad=' + str(self.prioridad) + ',descripcion='\ -- GitLab From 6953b8f157f0795a417805c684e43097474d491f Mon Sep 17 00:00:00 2001 From: nachintoch Date: Tue, 8 Jun 2021 13:53:56 -0500 Subject: [PATCH 3/9] Resuelto issue #42 para facilitar la lectura de los registros --- doc/diagramas-clase/requerimiento.png | Bin 16635 -> 17046 bytes doc/diagramas-clase/requerimiento.uxf | 3 ++- src/requex/extractor.py | 3 ++- src/requex/modelos.py | 25 +++++++++++++++++++------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/doc/diagramas-clase/requerimiento.png b/doc/diagramas-clase/requerimiento.png index e317a4cacb9ae0ad0d630635fb8b64bca5e34632..bcb89082005ac87f17b91d879cd1c9cc85be36a8 100644 GIT binary patch delta 16268 zcmey}$T+Q)ae{b#4F?kg1B2?(D|HMEPO+XYjv*Dd-p;L0iM>7lUwcPK$El8vT~-$j ztqMzwW(tXh7PfZ0DAAhyv&86Uij|>L=w16&PN7#rLtl06SmhKO8fX}EHD681$>o`{ zmX~YDzmB5N7}3yms|t@E|7U0Zy6o($xa_^>Z~cB#TvT-K^DkvTiEsT|?`6N;d;k2c z>iL`J?eE}FY!PVQpCCS2fl+d@J|h<{T(5F}rht=2WYL5}4w`iLmAL`+RwG(|G5K>hJI7+SUGgy?%e)pC2EaYrm}e z`Li;V_20jj{`Hqm->=_Wy?x7;FVF4&-~9J|_xpX$2EoC>8Xsoo@0)l!@V0gK{^IB7 zK7HDBkB9YD=($UFH9rh&Z1!y0wCLa8A0L&gM5Ax*U90@&mw{3eR(>|sA$qB(F$r=Km5`2Bnc8lv@S#|SfDaRp$M>ySsgTeNQ}iA!)+C`-O>a zi*C#E)`Q`n?%k7{rp%_}ZXoa`e0|)82mZ1Kw6SfQ%+L8Qfu4U<^J<0 zq{i*5>8wA0qCBAQRsZnlaQF%tZ=pf0jEY5%WR+EX8YkW`)VKE&{k zyp&YXe8Us2?=ELv|0B*=Qd;_QZ%fgJRSOq7rq{pVl2YYvmJyY>!EUW|cwg=BYu~rL zeRFg3ultXW_d9ET-Os^kwLHlaU5dOTF{@Ll?-MC-M(9 zu-x6_UtVtRHsx2OfC$sfb>;FYpP!$fpZxRKF*d$k#vc`5Kg?rj4*S~T@>qxKS7Xtc z_l}C)D`l3iIt;^zPI9nPtX1A z?CenYR+kPt!~3TeIMnw%4Lv`@zv$SpV^_N66>}_YRC%0EUf}6C(kZO2rlw}#QgH8` z02g!XLN9sM%n6Ssu6+Ld`9emI`vUx54>5cfvv~3U_x}H{&s0flFgG&;rO*68ZEfw1 zMOzG3Xc=s%O%eFydgI3J{Dm^RMSoRJ^q2VLwzP%u_K|xj9D=tFH5k>0y6)I|>GbjA z$FCX+9h!DH_>jVsHF?hp4mU1tu$I#}HQPMDq7;(*{y#q4&j0L$NV3nL)h&*;++BBt zXGG@b=JL*Twa@+}`t|kt{l`unEC_vS5z2aOt>MiLSLn>`E}?1{q^tX zIQaYXhdu3GwSGOnZRPK8Z`arF`*rGj_XL5&fQ>Wt7j2ZxV|}vW>4zx>9ICS)Tqr3k zd$s+0%OQuv1qOfSNli$Vv#(oYr#v?^F<|$W?b}~3_l%B?-ni!btE;Q0O`B$2{_ccp zZ*T9yIoH?6C;#k~HkXo;D#;34{bhk@(W3ooEgvnoZr;AV`Ty4a`r1>9|NZ&7IsJV7 zs*B&xoZ(q?X>0u6s;^H@PJY$+`^}p*RjxYIzuhgrpK04LVL`#xn#9C|XZ9!_T99;j zuegcnt^bQAR*EZul6<_~Z?Ajz?zJsi-Fxvvj2?UIOqZh83Z>};pN`V)C4Qh$h_R^-tLu3sv&`q=T~U*j(D z{H^f%7Z&6r(Na_(++0{#TWcGZxM96~%F56a8?+3jtepS&w^#MYN33G6)~`+~-x6CJ z`TAfp`wgR&25;WIbBfK%&JGmJGQ9tUJCVEh>Y)ePmC4D>hO4*xMsy#j`}tHnP8L+u zb}};_s4ocWKl^rf*VU_`rr&fnX#5Rvss9|>`1bns>-E20E@zTfn$kRR)2^=@>owb~ z6+fHiKMQ=y`dwZ>eqYHx_G^jDxzC3ir7$1AQp4$}!6U*J6ch&zMqw3;njJMijgDVz9NnlDj0HyIp0Vj|lP8<~&{Cj^~*#7^Y;_B*Ze}DhdvZJls;xDE+Xz;WgsShoGeN8u&Y2nA8TuTZ9 zCV1By|CRpWc-~JcZ%)FysoV8(I`3X&`Fh%j<>%G#cv<_pKacFHih>XJGzxGrujERU zkdym2d;d@Azw6J3E3`H{sq7Lt@<-aoe73{mmKWaqbM}<>_V+K=Tfr2zaK&`}cqbKs zKbvc>yx-1h%;u!Qvt&~1!zU*vXR!sC?mkxk_j>)x(0aT0MiD-)K)dNpGwPjvBtA*0 zANh7FdV1u7d8d@VX?Sk%QD`~jpcNz0CuiF=rE5#apT7N7Rc!OuJ8-eLYRva`JR_^9 z##7lVoK&s4ihFm0d$a`0X%>)OQF5wp-@S8E$-BF2>A$aF?{<9JDRN}lmTpex6FKG2 z;yPb_;;e7Znd!jM+8EMm(D3r*%U{2$zBVJ7zU|P26?*dxSfbPfoDvm8^?2(4eV*Sh zZ@+H-lHWC~K^7V=i@Y7digbCV>&M#_KkKPfSD9f5Qhim4hoN%Yx^;egojyRUcbVRF zrd4r|o-R+o#Ng$ASFc^ms%OuE25wf(J|;~WkwrqYG*UvRCmaL0O5lAiyQ~PuLIZ(M z^%opLaiG{Dz{LzDCOGT!v z;#jGXD6r-dbNx=KjBS3wrA{1{GKUlm=L zxtwRS?ycm$R}=4t%mpRMRCS(bOO7~L! zzJKrDu@(0#k1IrMnQi>(bD)r7%ap|iZ{EB)Q|IP8YyIV2InU%O&Oa;;S#BWUpcAlH@|=T7R240 zyULG4QKYR%obBKl+dt8sc8ME&uW~B3917?=l(6{h9Zp;=w@cUNo{Mi>$I~JZqwPIY$_-4 zy}s<5@BjYo+r;0mudh%3{qyI~>hJINR((AsIqy@o!%R>sOUMCKK%~TM|GUO*c|~<- zhk-^sd#yTS(N393bx+JoZ```&G{-M`dj!vq?WYmE!;fXa+>bf*M)7+Fwh|rRC-2 zB_t+1KJxAI<;%{_&c43Bxw*Od_x60`Y_{~22Zj5l_0|&&zI^|Fe{Xg8|J#=@Cv!J{ zDh&ub&$Z&vhYtk_2loH_m7Sixyl$gUqC!i6gqJSQHsAUtzmMNmtqxy5&!#eHFVBp* zbJwn1S-7vpuKr(5c=+{pe)$iQT#XV}?slHPl2~0`otx`h>-KPG%)4)IZ=d_1!2=4} zi>(K@d0x=G7x(qu-QC~5ecQEb*Rf-6Pcklx+!ATKdpGuuPr~tK%a*mZwY}f}KTh|1 zt#)Q|^5UgSS&y2T*PAEn-%0%Q>9jt~w2-!9)!f`%N5xNhZ8rZtJ@tORUTltoUs!nf z+3CV90#1n*GKU;MA%u+xV3=9`!{`0v6BGh51-m{zxyvm*$F4T&YHHfg zty@ibJ_{aO8N7U5%+5`_ch4>tKltfr ze)+1&<>L2cq&IqndOUvoSVMB_l`B_PSpJ)n<^uBijtQrqe%XKGkc8vwP(zS`^Y&D~ z?A$%EW1;7{=T7tOYMp+C8E0|q{xPRMC()w%+nP926O$$04;HI9d2Ho&j$16pbK|ST zowB2xg^vZlMa9P-uaA3Xt}Q=b=E&UJ1Tk^gzO;?uj*Z*L+?*$s@1oA-uH`~UTNe0EmW zmGVU&B3)S%1W$Uw*PpN9=e034Wz9YC>DaMjCr^4#uHsj!e|)@u2WP=O#wFbSpu+cMYoh?j zKyx>DcXc(jiV3J`Z^D*$B5a(Vnin)1-MB%y!LwEIw&&WpON>iTWPKO8u(?sdDN*97 zq4C4@i#ZiT)p_i;H)s4;Kik~gyfcQc{wn`$(b=(_?`>f_EY+rDAL zf^G5=5*PSulK6PH1vg#hJro|bL%k*UfIz1h-#NFN^LV#CWKnDhnPR|^$L?|2y-&t5 z?TA*&;aRh0iR;HD{JeKBE;#t|*Vos@#l;uOe1CVh`s*vvY4%lLR_xpN@893w5&z!Y zG+yFo`^ZdIDrvvxx1EkZytu!&Y~YosSNh+-w6=GXt#r;|PA86)GKm6r_%?98JTEgl zxx~iAq=i2^I$Fc*S$W&rjqjg4S@Jos=j8>wi|C7hF{r2** z`W>GHm3vpNuxzdP{H!;2%1qC*k>)JivTq^AD7i??>HWmXtS+e767puT%E7k|GqY!{ zo|8~=|K`n~XU*@gc>Da>vq@admwBBubX)7yT^1PJ=mn+SRelmm7vCo2#{W_$Z*T9|*x2IY;@n)_V=qq~ zV*FVz!d$l1_F7+0PYXx$c~HD_`EvwI)ZF*_WU95vMp}cXWdfHtn{C{;ow5l9d-v`8 zciG?m=rm=`_3PHn>i_YzSVAX!@sB-EN}D+}b?2(L2*)qxP+an=<>7({FJ=pX8o@P; zcNIV_6;KTiZOt_Ma?X)+^2mK&@U{Nc)%o@RcCPVTbMmC;SKoM0+E-{*+-m9gP`E1n zQFlV!ZPPjK{cx=5|abeZd)!n*%`)4NQmO}*x4o$c;C-w z{9KhoKeJJwO}_rm#Oo4@B5ggd`OITCoaMZB?b?P_yLbP-vNAY)UChn9cTK&;=E%*d zx8rJxY>NFl*Sh?Q{zlQ@;NYrdp$DHSs`2dGHhayQHCL`*f4!nB@wULFs}rB*EPSW+ z>-X=j-rQ%;o=sRd;YI1mrpwLe-4A}>{N%*b?uPSK zhd!K`@lRhUSI#H#!&X~1tA~GnetuPKm?O=!+iKICdfh1-u3o#A_UW-5$m`~lX6cu5#8*xhBh2?-0z`}_OXuU=jK?M>vDn>vmK zRRzB^QZzu$IkBnu!`8bji&C#e^W1Kmu&!)c=Wm`ZNu{Mz!Di*W^2^#Ka5_xhuErpr zyFj?1eyYIf zzD>=%$0O*Z!E^J<+$$vqP1GIiobp2N{|sCoz16^F5jd37r!DCCsT2e5g-w2S@z0w# zI!cDE2h%?%?5p{<)s`*l%AGrF?8K99Pq@OR6t-aVnUg0^US971SxUL((1g^xp1$?s zNzYh7u^z9Y`By_zXs%q19oM1llisWfw7X=L<#P4Jj9;>DDz|KA9D0yl&9kJar0LY% z)*EIHvVpCCvuro4o_mPl>4m-S919InBFlp=vkLPR8H#=5{mi<&-KR!m+qP|I!tZRH zxHX{q-nQ8_zu#_uc6eRMHeO|AW%K-dU)K662CChWggt0H&1TL;(GmOIjo8;Av-3= zi|HEI)X20hUNmc#lxhFu&7v&=K@SfJygk3N;PEluua%XRJGX87_V@Sqrqren3MalK zZ;(IqM0E0gF>%qU`aFjAY+v8JcyV!Y*4w4t(H9{ z((nP>Z|{=aVN&m$vxsaj9Vw~6gR97)#ce# z=)}3;IU~5ebg1Bg{kx~@yEzp_+KhzQZdy1>iaxmi?p@x~vV?c9i?an{1kB9M1HIGJ z)8pgi-+7mnmCa#%Q5BeZMz0A}j~*3eyJ^9g{PWqfXB#(eJbKjC!`0cjx$e8d4$~iX z`Fpl+Z;xFUzrQc`+4JY`@9zHos{X2WI>RXi#-r!Xo_$$q0jhNaxDq>#+v*!IeVJDAJ?&i106}X#cE|@-j`c%*6H`f#z>SLEJU*6u< zHfz?bTeoiA+f(^in5{U>8r1d|36#i5Ni?|p7t*L%T$9JE_+Z=O&6|VwMl3S`+5A+M zr>U^1$Sxu>vh&;W$g@w2tm3)zcgtx3mYzL(_J*LjnVTSbioQlH-!*lDBk;=ThY%j)nLerDF?kvLkf z{BTw|zeGPjFYnJeq7zQNe)VdeP35A0N8d@@a+cWiHNRtxyXTjDkee@EfBPl5V{g{A zLmM`3Y`ojv-oCzu?eUjw+sx)~Zu*oN@L5)Kfs#O5&R^*{haCR1IdQZqrr!0lZ@aNa zJW689d#1L}{XAEA8!tyDrlzvqp8a`=R^o|k_4^NM^32)QEZ~%QVojc-#DwYDSy={A z;$`M@ML$U1K7aP?%dHac!7c$+@ESbqYCISJ8PEUrt;{y${>___adF=)wO_oA-(S}o zQup{+Z(AGN*H4zt|JzP%DuC!%D$J&G|KrEPJM1Q=rlw|QN*C>Geq4CIX2G_0vxIt4 zvo?bV+Z;rvF06M>iL?OCh={aZy1Mp2L0xU_-{te`jKte4v`b1#e0+Uh*1x>(z@aFr z#Pe`Z)z?=iC#&b*-*>eA^mP5l{~p}kU2en7p}3@~^&zN1s27uQH$5XGqqul-`+NbX zRZ;M%mXLOXDL1A*G??WeqWrA>)BE--f}r8F=kx3JR&Vq1^INxOje)Zum$SyR>0#au zovTl`BuRveoaa&27Va)_e|2%Od;0l#x{ei{r(&X_vhwrO+muAuxSXd>n%e2QOS$yV zkB`sK&!4WaXGyA)MBSf{?(^hY4hf_uCOTT`@$m4xSkCh7>;7%q+9u|dmw#Vb59?>G zo_2p{ae8HC_QkoU&X+#- zLFi^q%aoTo0_CEA|Niv}YB?mZzyA1f*XsZO{vJJgH2wU%m-iRXylx-Fe@Vb>#_ZXL zLtAW?PW%4tTT;1k>IzPSnlP??Z<$wrerR01p;oG7hbx&Q^ z_|o6a=g*uwXJ&5xJlES}`{}ov9(Vq+SKjWc^W<=&)`hN(8x2Exe@IJ7%~57s9@}2q zP+y=n$G$$UwrrmCxeteO&6rM?h;_Wp&dwJ8`(hHP1sKD>Ssj&&19> z*V<&Tz-lkocQJ{o+k#eat>%O}CtpdwzcaDDUOaEJV8pV`4joqCuKESHge*2V@<(FI z<0hjtffh#b^INm8v$3&NeRnJiKHpsnw|ovq8o%Luw)wFx_q>vyD)_m)7HNy#o+oQt zb>+{-?VE2*$m7qS-lzVOxja(hOmCsU*Itjf-F&;VOyd31o?4sFIVA8y&TfNGdSiWt zNK~V>+%wIfy`WZqfFI|w=B8i?0~R;FqrT=vl0w?Y?(Q(3Zrpv@OEEyA=BlOO8M&m% zuYbN#`V=OmeO#Sqhj?aAPEW7x+@ozJ)fKzDRCv<(V&mej9c;N#dZc0A%uws|yvq$* z&i6_LE!*sHCw+nF>eZ{y1h@20+E=k7(W+kKhUBZm?fe=p{Y{2#NB3)GeuE6HyuWn$ zam0IlRy*)c)V0UcSBs>kQY&w#S;z>iHKq zzxH{X?7Z3^lcLI7ew6w6`OUMdo#ojTAn|DXC5d*e`s1y;cI}!tQSkS#`LkwSn#QwZ z!k1*Rp4N%tQ+_k8v+Cgr#qN#;`?ymUwcp;CdPycG1n%Z#DHrtoQn> z9h|Ti|MI_3M+qyqdcD)ip`=hbFBOZWH)iRXm%?_P(!w z<-%7NYmGSV0?&M#_VQMo3i}%-YkOCI@!wap?!9Bbz4z$q)d!fThe}_b`|#P*_)X3e zwx3zLbm_NmWtArth1+Jl&gay)@Y#0#@*2ZSVHIaSO?&uM#8ZOn%^xA|!wZ!UztfG2 zWtH2p=GKgNJm;%_ia!>pZ<)d=`Qzhp`KPMwo)Q<*mT&g5SXR0B{F1o*$6spr)t_nR zhpo+WUwUfiJ)7V6_g1qrmisx03)NJdIj7sY?Rbb}be8h@8>!6)*x1;ttgJ4qc>6YY zZ%oFmXV28QUP8t?Zd9hHAD5h$WxC33za~F3V?4k6@6Ap8e0O;L4}g zR@SYyx0cFl9%GY2VUe}b_|EmRhoAJ1Ny>qyn20Rb#?U(*1N5-7rNks##yFauB&UsQ|`$fW37L3LNLZDqV+Ue z?Dt1Ux#J~XH8QOG@caAwWJ{%s!gr1}b;`~Kw~h_pG}+v%zi?pz>w)kn54a~k?DM_V zUKYzCceg1wK>U-SQ0AGh>k{vEYA%nYk6d^NXD57}y9 z;rlB2_L6pkGo!l0V8ybRiV_D_ zaMeFwcW2AmwYoCpzn>j@IqBNft5dhfI`Joqt`jcK=}Q!;Ixo()?fAaQlP90MXWZOb zDl3(zX3g{H$t9;>0TwSjf14Fa9&7!Q+hsT9yyw$3?LE`JANaY;bw|o1sUueXQEwnl zojL#E!&EEoBf3)GOml1Bsy@}@dGUSM`3EdbYwBOt2S+SAzAkRLXKmP_gsM}skFHz4 zem38%17GH78vJM$W#gH9!KAiZ;O*>XiFc-(UwqJHQ~2n}ks~Z^_P@Bw5wwlG%w z(uJ+r*9#iIysK}19c9}s&KI*QSbL>`MgOeIl9p}U)9at5S5(YA$8=F+%b_##zfCF< zZ!^!ox2BHm@Y31l`LF6zkI$HJXV!J;odSs+m8Xu!ygd7J``OK#4?N%xlL)!&^LggS zkA;_eZ?cyi)ZkIpza#tDDEQs%m9cIknjA~;LW0%gY_Jn_U_rUW|ymm z?h^jXJJYRyn{jQPA-2nW5C7X;R#sLU4*vOLWAf?m@9(!X4Bj_-S7oV+vON#n+~2qU z?V+dh-%85GL8~>7g@-1ss+$rL0&>ypbzd@;z4Tfiw`}T8w8?_3S#=-Q9`ju$8Wm?y z^s@fVn~?mILGflz{Hxi;pJ`e0Z2f!ZPE1guLGd+6zo+!=xl2yJ4CWeG{N5&z=y9uk z<1T}F#-Ndf1#6E!WuE@%$|irshbG@-x8AtOnYp|6XL@y8`}CJO7xqYg(p_laaVVjB zUwd-ZspDO?_5W<%)rD*{Ha5=KYG+-`_vzswkA&lIv@hQa%J_YCb-4dLo0ao2&z(E> z=u^SI8n?^4cI~=yRz@$`#CQLvPpzP7Af9PgPq3Pw*q(pCtW7+7>xFP9{{G(Ht=qT1 zo;+18!l^ah)Gu+yoH;hNzqagYIi$eZ{Pw3@-%Cw-ZL!0u8aBt2cd);C{o44xQd_ai?B8_ z5>nz>A;_2@E^_tu?bWt+wM$#SEG)P%`Pnr=CJZ`t1 z-Y?;<=nAQ}DrYC@FEkLD?qlG;S6ul_7cXACW8O`Eo2;ov5{20ATsRy3XMgRRr~7_iGg{LP z>bc&$;tQ&?Ch%_jd2RZ%Y4v)syIP`8Utb>|{>tU&(TwSqziS?-yzg4MawXIL{IEC6 z1f3FBSS1>yAC8ZYU%z(k-*>y;PgphKnU{TFT_(p&Pm7B+A2;57&a7S;p7Q(NdXLKu z!cK`A?ui<`^2}_rH|=%%%Hh07zV7zs^X)#L{1O#f4h00Es1N=B z>aKb5u6(1~nisELSyg>m;itE;^V8hPFMmIq&Obl)Ze)(ixzqhgpedM5vyvFO&(E=J zR(-aGvDL@${pPZQHj&x2arX;ec-~_@%%ID2&Z71m-ytn7X2q*|JSRAAc>C)z^vPN; zTeed?&F{PKvi^eMwI%f9l8Y>*2`1yK8CP zwfeWWO#f8>{q=R%E~~!__D|4!Huv+<-%edK{WCURoV6o&xbEkKn^$X(ww*uS@3{Bn zzLvccTI)-Vxo0NXebeydJ83dMV8-M<+>@XEek8JLa<9AwPfN%SgO(qL3RC56t3qn` zmAn+%%rg^y^nu{Z|f;d6uy=7|*Kn97_4M@8?tJ zdn#X7qD53rs5h_7iMM>#HX> ze`@ly1k_8^$tOQr_VCP1<6n-&D(B|RbCOUxpYrR_pHGeV4}Cd%QQks-zYb5Uk753K z@%P-CX?6<@)GNi+&WgiBUxR0V@tYflZ}#onxpF#(@$%)uZ1=%FVQxJbp7UVOpG^iL zMo0J9#_!QzGUI2v% zp9P=cRy>+EpMTC1+csmrh3rNqx(Y2J6AeO)mzuD3oVAm;IB5+UIoo^a!Iol;ZN<#X zXQ;OXNSqR1K9_kec&Ox+C>v-v3DmO(Ei$^HzBQf~G?9DCNkZu6#V1Pz_Rp#pYH{aK z6jkMMHG1Wl;y!u0)1NFa0XfB%kSPWy_Sf(w78iehbhLZ!+_|;2waUuM*4Ea3*0Tzl z=SsBxxO!uwtKRlGS2lts#1zjVPYFGL4jE*-s=Zv+cfr9v->m&o={XU3?bEHyKu$Vx z^VTh=pSKoWs@F05wnptp!g29LtNl{(Ib{_qla*U0a0#{vES~2up|5!79q2+agX=eM zYVuU_md#o6ZS}{a&qUfvHVQgVPcE}pYMD?Z(iX8#@59s0mR;}Pyb00IH?9|HWBQx*<_36>%vrECvAQ~1 znk_+O{^_TA_xH)}e3KERe*a3H>Ao9pptI3wph2LEmo5c`hqvE}jgGc9Fj#OWA;)^w zm6PUM<9qeZtZl%$96 z&dycdcidip)>9lja7RzYmBA2VW8RGk3qdx@@FkwO(69LG(wT(hWag{9_23D+wtKm` zx#wE4&rOKxP;Sb-A@Jy$z|$NGAx3R zm5QI&Pb?`ZNlJRezziBnb)L10|BilR8#w+eIl(K1oOdqXD|V`D#!LgsAKDjmyD+z1Uhc2`x|RL6itf#CQ4IEf zJ{&%9z`@=9_=^`WzJ1$P^+GUXQNnxa&NkkCu_a<`Kv;^>Vi? zq=oYI6fPm!tXk z_?8K@&A85V?3P9tf9B%9mcI+Eg;-~3aQyx?pP(jdX|mFe`=<+D9c_gCt!NP7PK`PpbL)7rSW zM1gOjQR;7$Zg{_#qV}}D9d1s-)k|XH{3jY16ywg{|!tvvoGdzbp z6nVB?XX<+U>Qzv=*@+B|u)Ps{hJhd6zBbMgDC=~o<}_aV>lgdp#;3nBFI~Gf?N@@i z%7r*5zLVDl7B-52*8XXJlr-2={e9i$&BDrII`3b9#iyADy=x#q}m?dy?b}L-`qoHyLa!-0?kbw?H0GTw*LL= z*M`Y%Mhf-1y1I`aKQ1mV_E=zQYFhP9_D}bnT|0JkgnFEnm6g5tPki#MS+hX%R!?ty zy0^DF%PDi=oPwPHZ7;m-nIEPfe#T#0d(h&q5U6Sq&_7tS?EUOtPQKFZM|N%9+B)IQ z#|HzuSB2(!f-fCi0LIwXQh;Z4ixtK2TRv|W|_d24GnGc)tv-wW0~ zKR;ht^Lf!uVRb(rAD;)y9G_<_oKukWkGIRgzIlehkEVs7d{cBN3+ta`W@Z)@74<<FW%KekV*Ie4Rvvqc~%@K8Wb zfq#SJE?cMjckjl=#+u%3+pP5L{)BVq8$ex{sm^EiH`cL%O4fy?)2F679sO+%5^r~q zICaXX@gJygK75cHQaCdimH1k`@p)g|z&L|VsA;Cl!@{ik?tL(0|9tulX4<;kurOZ0ek zT!G_o;vdffMZ@V+4MglY6$Jei|yCL(lmy7G!^XKgB?8Q4}CciQ4M`ux5LXYQobeLMP+JG8#z^0(~E-fuR+52^>!VCwF?YD0^JALo`w^nAI35Uudv0tacS57ugKX(V#fBIBk zdTx7S+mkcC`z%@yYaPk_9X;cjSm1{Jt^ejcayX$Y3p$OKhNF)ID`P z5>pfOKksDU#C*)?+s!n;7^&#gwF9zJ|H+^+efcXV_#*d-gk7d_H;G-&LIwejQU z)QOLeH&^dw-LdsJtIn6H>x0xic-{Nu>O<%2$L?x5@Cn5YEVH6Hc8OLfm^gJm5kBU# zPjPD7oP_r6rJm9rtNbKB{W8?i)lJkdR$Bb=W8o8y-**n(2+NnW77SIC@Ut@tC|BR7 z`Kl#W>EqsVkEM3P9XF0X{yZT-b+-LFw=+8$TK9bleYM-V(P^DPNA*o!>Bh z=IjY}SI(5&h)cKq@a1K2is5q(SKBWK1dc_n43=n#W%AG7$zuD=)K}rm3|4;5tLkYd zU;SNsA$*@#?Sd`vUfip~`uPIrc6-cKWItplU8{SlddFeyvn~>+wv~b={}vu%I5*$^ z{?)5X^Asj{1$0zJFU=9XvVM+1)lu6uc2{#2H*MUw5j1D|$0aB@zE@Oj>$kT;by3#d zA7(lnN%~gl@xtyP@3dPLX=!Y$udu#(tsTGA?h40h)+3AK0-x|sj<8S(^U9KKZ9Fug z5w!Q<+m|m2{tusRS-8-#ezC~y$9;VJx_2+`=`w+xJzNDrNhen7N(P)e+|Hkx z_yKO!4MS^Nm5`GX+ZI3E;e6=UzI}F+FIm{upWX@bM03dZ&(F^vZ~D2t{`dC!Kga9; z^#A|CUwsv{YwZ6+{(270CzqCbYdnen_cZ>{Yu}CD9-M~{AKsBuaP{Qxe;@nnkECfF z+*`M3PJS~) zrY?F|U;qDmef-3=#`Rk~G}ge-Pxf1T)*P&ou9hJ<1%?f7pctS+9IRSa!A1C=WoL!hfY|B|2w#7 z|H}v|=T?o*FM9X92yJ+Y`!nzVP;M^)d?S$gp(K;BgggbAEL^p*=CpJpMyq z(PAd)gTam;DlR+}Oaox4Qv#izr5*uz(Gx(mpr013va=dJzTs*U+<9ftK$>G zCFUGlZlG2FA+cJE1LOdW3jvB^t8!NeuvTkbC~5#LnvjXJZ;OyHO#Hw)|H_I3?Qd5q zRNGj@+o_wvEWbD%9B?g%1hjbCulFm8x%o)c98@>e64+oX%F@ri+(5-NM8Ih)FU(Vo zha5C`G+(b1vS(AT{~BK3T7NJz$iMY)(uxP%S4~1roCMCf zUk)$0?e1dF#>H&8C*4)Ca~ZN|+oYd{8e(E%A)FoYcNfh{U-+O;V6x|xrOS;mCc2h0YvS+Al{!kNYYyEs`Ki(wxIofP zy_U2aE-}CNw&LoO1})JoauK=Wr^wi{{FsuONWc4(hQJ= z4L{Dl|M7a~y_!7seQ%w!d{VwWu$DQOx3luI+O#ug&Lkwq@GSe;xc==S!Hc|(LN(uJ zf;K0zW$nB$|6H=*Yng{WpIcLcVRnP8u0LG<^{5<=Mf8+gJclDQm5h0I)w+C14w!PF zpHWpI(bvz9?cVldcLZxX?>%Q1(BL^Ib@;4Y%i+Xw?sK1Z?Xs%%*{IKEcg{L{^V@yO zkFq(|@jh{}uGs8ze`ANvXU_XGpK+h`R7{fyoFVUa$vM)pF z_kOvnpO5}J^+lF@v(HbT&#g~(2m9o<3AX-BRI#0+YJ9jO=x3rxM%zP?wh!+`F1%G< zB=3CkkHnjm|KDy7C~y}L+IyK_AwZ(Wlx>!6{RuOkjd_ugk)2-~GXX`f`n;rREVHWv6>3|-VBDi|hF|&RLY}vl0GF`R1@Ufe-b90whMs2+YPUE73- zU+-GXw`O8%w&>l!vN~Yy&kdJevOlM*A|~8*DJWpE~oV zWa=#bBTwHtXMLGE^GgB8gL6^!{wDdiFDp*hl<#($HO;f4-mWISVv+ODx7+W}y0_XQ zG%7lpdrpV>MN8pJizb}bja@3ib!SiIW)of!qo9vc5tfH0oLJE=;HzAoo;Mc_!?A8W-%+>iGS3$uxi%-^7`-P^?x4AYj4V&dBdTw z>}t$mK3r{Jnr{{lrrwMJf3y2NXU?3@b$Tyx}GR-wKiVz;!qSdf3keyL8({Sm z&z|-5^|hF_+;8r$r_u~-1JNs&PZ`>#-9~Kto=H^yaRaL;>Sh9cj z?(BD_=H})mCR5Ho-?+ZcEs=|Po4Z=o&reHZt{iUX_n&W9`|i%p<^J>kZRn3PwYFZp z>cD;bzc2l(e*XUcUR+G<-=F99xBu6Dzgy0cR#YU!TKD_y_9Dxy>h!nY-`|gqiMcX+ zp~K!&k@dHZ_si?Y@7uFw%aVV8zq}N#5{-^qtB(yGp%v zzl)YWKR5T(!Ec{(W;F}k^kD&t83S-TNQfw@@0$Hb-&-5 z|Bh#AV48R(V4_#IljVtjH-6mQoF36STV?aNef#XTTs*TXYifYhKUdCIs=O{tLMxVP zmj$NwE_2r@oofFutwuE{&)4bU@1Dg@^gzd!_rMoH;O8}Bw8kI-?ptl_i{V!O^J1CbuCvqygH-M25U zG{mFjh4AOKJX}X2;=cC$R7o!>3EBHY@X)lw!G{#4@SJV%m}60xR8u44%<}v#-$H`` ziL5Vylj_+tJultkH=VN&6$bK)0E1MVGux*_A zH7qP_LgbgtYVm>l|9*>JBf}FO+7}#}aA(#Q-Hltcr!0>2=lq zki7qXC1Yg$vzM2bzcRiwQ;cn^x4V0L-K;HJw~C&wdj0zK+O=!HzrR1dTS`{e)iVFy zo`aumZ_hV3HGO$yP1s9E?I$kvz8sGWMK*2Up8h|(K0Zw2`JqfckCxe;YRBNkfAyti zmMc2laH{zAbo2TrZ{Ox>?7Ahvd;a<70E;+TP~q45P+Lu1eWHNjxBR@kV;|z}y@EQK zPx`Gr`f1`sLF?Cwcb%o*-Px%u()y+?rmcRX%%KLOui;u7vvP8L-m{*)(7*gizUt8y zM-6s!LkV$nGqWU7w%@<%9j8UL@~mcZp83mi-})_AuZHSei4Q;eE@StvP1pGC|1{`M z<~scDo!*tD`}WC+E)@IhyH8--g$P}qeUr@1%mg-9-EP^+@#cDb{nM+Ci3|KqQ(+0UyJWXpDas8Mw`EATlxU7+QVL!v@zYHjV`o14=;3Vsy4y=B^yIJa|0 zPPw0=y`maeUiHb@+2-QTM3s!O*vGD$`XF-mN!X-QxO}G#djXI6D8YdiU<`?o%vIk3WlgJY1kKJ?)qI zgXVd2P3~A6yt6tX_J+{z#hkCEr*eFnoxjhr?$3{bGy4ygZ00oa>)7}0fM&$?8#g3gHEAV?c2)iTW$MCoaQ_|m zzje_D$qFrp99Bh2fYMkRL+Ix0KiBL3PLFyDc87pyk!HtF^CK+_7Zf_>tZyov$@*%g zPE!9QmeVXw8azu*wLW}taj{4JN{{Xn?|-abp8nw{*OG#O4z8UdNA3tF9SXQ$p|~x4 z(XI{LWm{SuH~4_;%6b}j>D@a$mq+jJ?tT?Nf4u`2d+Uk`-i|q5yJr}*oGD9xH+6;H ze1j9yI9mcFJbXErXUv+Fm6H=MVkd;y+?`Wi5Q7PuP+^9 z5Vb6Q6;fNfx9ot}83~Ss1`}o-d#*jbg-$RF*iccBb zUS#`hh1^fytDK50hXM)@CHQ&i%-C7)@9)1@BehL$%7c=S<3-C?S}C>!NN7b#q!@pH z`!=`d*W7ZRCfD7T%J%ZAf=(W)T!+t{JNHHW%95W}Yfq=WYy2)_=U&Ritk@E=!eGt1 zb#Z^1J}LJ^F8zJx&f(UBna!?=3M~`3MA;NUibdF*I96IFcDNXq--%P${Qm9RrD95K zw?pO%IBD=q)#GWJS^Z=Cq?$tsvsVc_akMTJYCY&Sd#CWkhZ6Nm)>_Zq&lD@kp}54W z<={5Y3;(z}Wkk;CuTA5KmElknX)EGx{?PoyWq+vNM1v3CFSQ78IY+i0%$&U7kLaA$ z@9yv4zhlRW^5fHPoYCa5|M%nZg9i&{g(O#3N9&6C$eNj(2Rem^hu7EJ=S7y4mCbpm zxu5+?^}9Pe^Y{Oq_FPP{rT)-_!n=O6S*AXkyZXcWdDE?bRZX)EmXNclm~i^(goDqY zKcDzCIyyRZ(f7YKRr&e+x9{&N<-UFX+&R0N9|g6wcO~b2s&<$OYEB6`fMPo(X8YeY zZp$mGLpuyK;@NA}89)8BnN;`0y!6JcTTXNQqPIuz{Mf#G_w3~&iYI?tcnVY>8H1D-@d>0_ce8%XDYRzBz9uoJC{QS1_mD< z9Ay5RpP#>8m*>=V$4Fc41zgkh;JDkndG{hBo^&TH7rKYxFB z`FTmX)lLdp75r8K>)? z&YCrA-~WHrTkm{dyQH_b_u92Ek=vIpHSH~E|M+_SeyhWa4M2$~koDZuEnBy;wrcE< z{Pg4X_4RZ2M-?zxM#jaN#rp|3akM@x-~i<-P=W{pZ_l4bKdh zdHQg%dBLuaB6ql@=h)RoT}@5i{HO*_wMKd@q<5)uL)^wsjsfC z4($7IUS_7GtnA$R^W9(NWMy6If0kPxK1aY@u5$hF@9&e}Zcab{ z?Afz(^X>0nyS8kz%_mbPhs+y=2?-2U!3E;?Wu!NHg?c0>Cu>Mj*HO3Qz9(p*3> zyko}s=U?}qI3(ftI@Azk-n^#jmz}#Ob}aNf_uOf|eZAYSFykza-9P5k=OkKGe_Io0 zYGbp;`@v!rCy)BI+|G`R<#=v59+dTiCjEu~i z+w6xvyt=AgDc(54V1{C%<+UIQP;hB{lK48qv$(J@@b;S+2&adAqtl8TTX~#IgK$l4ywqRkK{qD_a*(sb{uWMs{<(6lbE}B_&$|^| z?@GVDi7aLR@aJdoo28rn?#h))N^^T2={rmT?ME#-HH@`f4_Uy{H=H}*Kjs9mA9xU@J*yZ@ci~D=a23`rJ zi-!*0)VRI*pxGP+NFDN)A@kRXiOMH>B3g4I!o#=k+ST>6y1II2%)4uAqd$NC9MIR! zFTZa6`sAOJ)%~TUrR8m_TAu31?{j%|MLF;CLd9>hQ|3#ztlv`L&vCm!vHsKlh+Eg; z7Q7N zKX@c+#Q4s+S<0*4Y!hw~DC%fce0E%6hipzxPQulvr>C#4XI%X3+qbf*6DKzL&#^GH zv+Mh~)oW8$_475N8)xoKu0Qmkq*=hr`ypqdU!O|GlDiu74W|5jYYxiGPKg~q4LS1I zvqagh`}54RtzKsK`_E6~JT(KM^G}Ncy0_=wzvVh}=9M|6&KoXV%oA|R)U;7_YB_jQ zW4=Lwkf4*tRXZ=CJA6}iF57vE)9rg~Z0!BI-)nExrKPE@G_bg<&~hoLqTY#xtNE@5 z7c(fb6ou~cxw*SrTUmWMP??_2uKX+Gzv16gr@Eqkyu7@;#;*STy}cR#SFgJfXaE1^ zty^4zY~YX-nJ2K!;D?S(O>dXZc{Y$yr&=8gtR1vuq@^#*ar*lD_RHJ%eT!G$vTd80 ziOH5NTdrKco;+urj?4uAdc%b9+sSWFobc!{_;E*}X6e!oEe5Rzzpqt^U#xU`bK@Zg zj+I4;7aph1zs<;YJv%$w{{Nri9Lq_cK7HEsvm@TV{a}%4m%fLidB=oHmnxksKALt3 zIA!@s@NxC+;{>&54~JjiZ4z)wT!7SSxiRJ7OU0HeWqjs&^>=d34aGXm~^ zQEds3;0uxnnk_71zpz|wAIG!pv4<-^J?XUk8T9gycgTZ7hn(Eq+4T*aG=JMDsS&)ydHCbw`@ZJop^=VWz6Z>S?V9Gd~N|=01Ory*pOl#I07Z zHP^W!{C4UiG2Y)=g-R^}5|^&Xeopqt+Le&5t+H&kc|IQ>-?z8Si2>&yu`Xvn=gemK^ii%l79%CM1Mg*PnJ-e1YrE#}7uzEr%v-%3Ch)yyIol+_hHL)*BDL zd83o^>~K3jFE6it{Jx%jGgLYJ`2FNtdBDzzEaTg^ZMMUfGgaP+w*@X;J!e!nzcEZl zU;i`MEZ@7HzT!zYUp+qF|16g=kLkmDA-0<@4?bT!!9#+LjZMy`V!{8nZ|mQhf(@(9 zeJ*gRY-#k4yAyY-o;tDZ;PtEfjLyzhbmC}LoV{!EEWH`eSWdW~So_M_{#EH}u{MvA zst-Rkew+~!F^q4KHqV=5QyFACYyP}>ZYs+Re&ooVnSOrXw&M=nQBhG>o+sr3-IzN~jTGbf>_DCm8+%2Ih%*F~3`&$~PR2%8jZddfY@ zy?2F_WbppO9WJxkgC%Nm`x`_0`~r6FmblHfLV4AO__ge1TZ1JQ$n5$KDs^>U`(^D) zXmZp$c4%MXz6W0>boL**c5T|vE^D(*Vp;k($|l?Y``G{N@Vb(1yvoYTb{U*Gckb94rg=7%i=NLch*CRpY1ifcgD+JLEG<_~wi6TMyLT^c%Hn$cXF05j zEg?H6CyD9C=j8CTE?%^2mz8P%~|g&X(n`**mVzbUop zgTjd~$s6PkiHNWrJajO;dr$X+cLKs}m(Smvv#Ys(5{KedeI7%5wy$qqytueH>+RLm z;S;$QJ57EkCM&#_!}(0VBkzI2=^Hj|Xn0tboV<9loVcvuS9i~g|I91h+}!5b)!w>x zZP{cWaVJL8$^GIwjB6+F5jU(i)#ce#=p?z|IU}g-jXYHF!2aFS_1&C`B5hnkY&R_& zB}E_HpEpnLX<5R%*TvZaF#@1gxHqH~{>~fJ3TJ##6_|NOuL)FJE)`|FX~CHMbK10N z7cXA?`Loi)H8^;3-FJl@ra$WPK?8KLd3SfU#-^pG%iGoL*jK;r9gl;g%7Pm+XG+@g zvxC~irxbZ6THKZmd-}O@@1K8ve@onQkBX&XZ&|xmmnHf4f}4y9p5^7{=cPft z51B&=zYR|uN?3QuBmOSei9B9A1oK{r!tI*2?%LYCON4@5He(GjYRTiIhYG z;V+LKEi%fSa94_DL;dM1SFU{cP@p$GJ3qhv_qVnF5@#aca)83ZDRINZ)`LFZ_2c$r zv) zJS^>@RqFYU;<2g>v?wjtio%`eT7SxT~>m(V03P%>}K@iYwiGC|1k_7- zMQqQlTkuyxWb@|D&d$z1Yvk&`o}ZnY&bs&SVes(v+CS2DH{5m4AK9;1ROxYHf1`j? z;)zXpxBoc&|0JofQ{J+OWoJcR9^cRH>(=F!mR^0iX2G`o#n1V?s{j4540H6jb?cS_ zb70v&-ifE3oBr{(2yi(cJ0xIif8kI=piRBdy2|ejAAfxKu;F|pTJBP}g`6|+~d~eIaSVh_@_GCtF{9#*KF9brRD9}%AlzRDX;7A zKM-N_yX(lY&>$sjy8_RN)1{@Q5^K44&pyZXqvvgUTH2SWp6`l0H-DK2NPvQwS-36Z z|4e^(_jb8e@87)9(bM}iNA%0L{Cj&Gwf5KkE(;ExT=jF#!g}YFNDGLLPn^t?-ru+( z@s7D@XH0RivC!Yg$NOu~ab;C!Fo)GACUY?7afbv-{xWx&*2)ZyY>oK4Ts+UOU%OWK z?`L{(V#j8u^z`RXo-Fw{9Ta>)s)q#5{rdH5bNcyvd#l^|udj2YFmIctqNyRJDb>OWa@m`1p8z++Ho`3eVFm27kU>_P4R+XjMFS=gyp> z7zr7fEB>5kUe~9lvMS%4H*a3>@_N76{dKkF<>9*f>;M0I^k|V@dQnl3|H4;Wf&{kl zuk<^R`R&1%%nvtRIi{{ETDELiRdw~o_5VuenXL=xHFeZw?Jg~=KEvajFyPFH(p=AL~e;G3n>&CAQo)VwimLf-V-smFzX*1P4+ zjyTcI9J)Ys@#4iRq&}LPnp(Q^`0eKV&G4|^XGg)qreAMt%;!98y`94{?WK;u?d#XC ztN*&Fsu-%rvqP@nufLB^i@jlh@#}}HgpIn-dnpD;)Li{J>w$4~$4`(uxWc-M&OCpd zbt)qM%n9og|NNvmV{<&VpMG0(O!((n-8tt&QxY9Yt3==F8hqKNTym?_Vs+cG73cRA zALi4qzmulC!6&^@p!Ch^xyv|Kx+Zq~G}JiGntDcogIRALtVPrOX4yRP-*C$<1<%MO z+40)#?r55N?3Cl2j%%7cU=AAiJo@ddj_`$X&Vdgoei}e~?n96O=1TQHA^&l3^5%OVYU2c%T(Ie9}JMW2+ zN?6bC!hF4SvE@^p78v|kRjf3__K4c`&o^B~)|iC#`AZb&U%GQgM!MRnoA>3n4`ow5 zB+kg}+OcCrGv~(F9gMcdtIOs|`z3PBmo{ADlis-Fyo2_%>CPR8|xs<{~%C*tzvNxO16d(1q? z1Zq`j@a%7nxp48~Liy~R+}xw<>ZjKGU;Q3*McQVrN&cN3g`b{ye$6Sbs*;+$^30>n zK_7DEBj)nYzu7z&WOLC{)>g*1xtW=U^S8|odZM!}VSD@b(;{s1ce6_3Qm-CyH!&yvmCmsK^FZa6Opti5~pMvBk0|EY*H1T`T_3{>Zea=e$|P;B`SCT4y0Z^R+3(E@brF>_IiaSJ|_OLJoo>1L`LCZkT?RE0Js^$5W9O``t>D(hXaJd(Vssyeqk&tFRxyoSzTRygY|A}?1ip&@Q91`;o!Ay8#A`PZ_>GOc*lc& z8OtVbj@1P>7rYJ&53fyJ!Opbf5O_4E!^OWqnp>@`Bq}P(&E0(?_os^B`ryf84Dp$Y zwJ)q+c>catS#$WeS*HB0QpJiA?rn~-C)_>0D6*fsuDfrWwZGy`_6y}#&MbbMy}RJ& z^hZ4WI+r6Awyj^k-hAnmxS-329+<4`&z<}$?);Z2{rk$69=BY6-cQD6PT#LLM=Us= z&-u*J$`b!%vC6`?iiZ}=TGpPxuvlwO{qCa6+qP{hxbt?l^m6a0^78V7Wt_G>p*#G} zY_x0(d|?|e0UZLWKF_0etu$lD%r^!dlfAiLIzL>X{`U5XR0;7*+ckd$*r`w5EN2vT zkooy*v4@j9vjkq-WNm~vHNCE_efmosf$bSLu5bCeb$ZAyg9Z1c_caJ}Y+6?T-#4*S zZgq zj~`3?d!c=&@@C7Pg`!mz$;r&C_2T!{{N>IFudd#$&$D&!<;RL*Jr_!E^#^O{F5z!& zp0_1`uB6I`x8;}Yw!C(d+Ew{kO>e@!+WLan)#)D(HF}q4sfw~a4>a!YyHNJ<)A>I< z{Q8$86{K3jX8gPy7xWUe`r-Cd{e&GQ&(F#JUH?Wxcv3WXm9n@pkD>q18QOa-|8J`- z6O;UNkgGXsR;Hb?nOWK{!`}z=Bw7=nwC+xhNjp_*ymkGi3x~5r*=G3je92uQ+ZH=T zU!vY|o4B}m=yr~!tzQzYzUQRhcS{tz?DW0u`@-1p^mKKz8FS~hy4C&o;3&;1W>etl zzjn!?2R2#XE`Hn^yW`Jh+rx)Xmxy)z=Gnre84?k6xqp(E7LTES<>jezXH8;XUOl~q zOT4YIv5}o$?geN7J2x@F_cH&!t2<6^anj~+(~GT-jeVsX`RcUWi^Q)F4#lXpFWfeN z=1fm5p0smSA0N3UCof)iH!{jYewi>^`h78F9?#zfVdBS%!9xpMw(h8T5Hu(18z|rH zTP$X6rI@>2|9{T;4Zf?G~?DO7^o!8&J({oF_!R_n3^jWx8t?%s{H!^Z_T_^jg%Q64?v2k*~x|rPc z>&tEJYL~WtSy%uncJA&iV-wtEo+*Fq)vP&lu1sF6Zl=X;bv?c5jn{mRo&x8Z<%Mqh z>i$Ok`xea;^X>HM)6blrJi4v=#bDj-ty@heGizwqv-U@M+-^O+U&LL}b!J9&boA`X z*-82f4W>-*+F8i4CV>H%Z(( z9yjk@=)uM-u6(B9>tYUmJyZ7}aJ3rWWP=B7dDkBOf19lT{qFX#gjmoF!ii;h;4WcK z$Jy=sHf*Rb`1&gJPSn22&JPwBhvK6CRqvwCH?kDs5|&)UsF=Dh-3&SyEBEv5h6 zx)oJh`?vglZF@+oN%+6cPp6$v_#End*4=;BPTu0=Zl&Ab>r`j4%@N>oR^@W8{3Bs- zW6E38EO!Z^zaMVTtF=p+RL;e$DAKm*RoTA-xn*T$W`&k*_3M6xzu!0Wa_!`okDbfQ zzh7MJURu=YJLUZxpZn(fYks_WbuLM{Fs@GI_U+r}|2^ts_90e?{9B!zxw>> zketMU zJR|aXqq}F3$FiR-hvqzqondlce0jj!oIehU3QJD4Djw-)T>0n4#l^2agGO~y7daU( z=brrXcZ=f;a1fn%^J-tx;pb2LCG8a$YNG?5hbGsjyTIZj&st~sId-f4~#)J63g zJp20%3ojQuJ0tmbfu&HopT8o{#B`(D#JZmc@)LhOd%-_vdc0U$(y|%bpLc(66)~P4 zFk|u_?#a)%3%TC$9nu0Psr!~?ZzRg@-??+eyJhC(%bd;mMz#0kt1#3BbV!dc<;{++8wvv`JbLMe8Px^^Hg zYn_m^`8>PdC%MCt%P%lX&Jq)F%JPuNnpq^pd?al?|C}dt1)LIB#3mNpvYc{jXJhag zVbCOa_vLeq=fGnbZ$WdEh_xecCf{1$1{w(yRpjY>lff0#6JPHuc}N*Fi_<0~`Bi1p zVoeXjI@45+egUUdUJ_5@?bbLYw~m-XFo(61-yPu80o zO4FnLUflqVNeQIq=j%^DeRo%Bc3oXvS=qOThudGje(h&H%b{6z_LVx*b2m2J;nOe8 z0hIzBo40Ol{j}9}X?9RU(_6Q1Uv;_c zm*`=BG{6vK@0>#c+g|o_C@v9dRh+iIyXI$3-=p2Tc3nAro2R~Yp~rqHYco(l9SU$t zym9Z|wKhhHqt8G8tp5JaRnL0Xm6PUM<9qeZtZl$*Cd@R*$;p9Cn<$iDzkZ#ck59{7 z;&z`}=_g;X!}$8aBXB8a-o1PG>C>i{%JX%P{r;|CgI|3DEvEpj4){L%|8{@1X%Dt)>pkc&E24^L-+E1SJyf411?#IJ+ z`T9SGlcR)Y`g15QRX7|5QW_~_octlMuAB9t_c~=|<(1EGbeO74{OPjo{_edcFNM_A z)t891E_A8mkxZIx_@gLk=H`gby1xna<|=n;nhrT|v?@05GM*`vTvk>FY5lIZ%r$k} zx7>feUHQ8^mzVq3-%!sy>1W&~U;n4DpkTx1&7V(A)xLfE_Qcakik~bTZv1vzw75B6 zd~R!Coxgv7*5bI<*5Ke^*5B!Qd49U?KVL2E-K@CmXN$~Qfk_tx?iYdc%@l?D-!hg( zERwvLd^>l5hv-7@c5m6XZBc=aef__h=xFIrewPmA8n?0Y%RMQwe6s21&z}<~3WjXl zyjl1K!^1n~pZ=Fc-Ef_H|I#HRE32;1360B^sr_}i=d*I(zsgTfI8EMPT$a=?Sl2=@}=IJR&q(6w{6^ z=rVn8V{oUf!iAAf;LtuhclJc_P`iJo@~0A)$a}AJ`oF+gO-;>7PIcxpVW%xl9{bu( z@GdbB2y#9&VWIrtW5?J|?XUUy>8pOimfQF4?c27k?dkjb`>#jy@$xPeXqzF)eC(D+ zSUrCxV+!+j<-i8!mJRFqE2l^FK5JQTwe;67cisB$?_xU)T4wm4Q2Z&SWIo}Lg9k6C zb6Xt`H#fJ2yz@DAwjHd2r_bKkRX)80WMH$_wK+dBa&mYc39|Zp(S4)6{oHnMp9(Q? z@vq0MmbQKGSKw)UCJcAyA$QMAhkCz*w(M@pz7<``)(d}cHhG%7adN=XbLY;@+Afk+ zx3}q#!W-=^em7X3&ffLu(b4W>$BspKiT+|got~EF)S`H&=8*WL^@>HznmjE552jBy-o>ckjZ=%}!)!gzb&sD{S~s z|Mqopi9lK4)5q~&UvK7@w|nxrGIs9FpxVF9_YM^OHOb7*_O9I^oixk;#6>PpFqljL zCstLDM2YLke`cS4I&1dq*|TPW7PG#sU%hGdYHsB)o%iqG%gf36fvi0I?pIhXVGZX)W~k z_5J$o8)%m0PwSOar?}c4+S=MK_nUjjZ1?WnSwEjWd$!bj`mSBOe*ONvaqr~8hTy?uYE$Jx1a=U)6LKDlV;owd>1H^@ZW)%*zX)zF4yRi4(^ z)jvAY`SD}n9g`O_x>JPx&g^#-e3337%ocAT!sg^5$_2_D6Te=%ac1G%xpO75&!0QD zuloDD!|nV6^QP;^uiD+da;4^)Q%{TDzI~hZEO6nRf~0@ET@LomGYoz-f!5Eh3Xn+3 zw5^{!{d^f;>#-SXdDhm}Kf*biEvLPI_3DtUPuL-W&-no?za$~U=a;ycg$iS1?_OT+ ze>FW-lmT3QT+T?=nScKI`@6fRPc^vlkN4vBI<}^njtdNK)Nz99J`SO_jQ`vVZnZPq zjI{N-1#aaZO;7po>({S?2OXbEPUx(MX?u9z>l178Z&d*&k5EqMl?QDZ*QUES~U+qb#F5^Cz| z)m2sJ=2|bWyCL(lmy0tkJ-xlH?bA=2$)RQ@%m-0z6zlzN-#=usy`!n#bDe#=e8-L2wd-9EaW3XuWAcH;ddXVu{Y@OL2X%RN zyqWm1(CU?{#D)iFf^R?GT*+d;S&BKQePrU!u;fCwmt9S3}nqLcV zN?=mFlb6lUc~||{_M#n^6ee&)ztE0f>UVCAW$|ywOswF^>-CoR+axC$oBwPIy>68wYIXpN%c!HF*9qENt_=oar>2Svic$|mcu92*(R{L(JZr~IsA7q@w>d4!KM3YuEEB4x)GTtILj)cr*CO&>7O+3=5xt!M#aLH z+Gow=yZw3%d)%IiMxAFe3s3dd^JBa7n#MR+D3wp6jm%93quJT#%v z;LW>t-@biQ@_+bj%gU9W^@~MrKkgG*4zUAKL9hQeU5u z*A?PoRy=9&S2yeIourzYJr8{46h+#+4!*n`@N*hZyGagc*`>gPFPST9yxW|m%oJNd zE1X*n37lE@Ir&7{x<8j%!{wY?H9D6w>9TVyG++t*S;w={fa~1U`VBAgt)?9hQ1p7V z(&02Cs0LqkzW-r?ud}>S{o49%ZCrsHz?xPDCay5DXS3!hURM+Aq*JkQZ(|$R#1|_S zDr{aniC(g>Nk8Ovfa0%P!3$^ch$=<#32-@kwmzKj%kW0xjGJf0Yo^>;alq|Eh=7e; zNySkKEtk6Y?C9!Wwmkd*Qh%i7pkxi#Vy1d^#d%!I7B&S-1W9PQJX-R{(7E+Rwts7u zQAkIIrGo~~%Gc|X?Aw|SJ>aox4Qv#izr5*uz(GwOO&(FD|4aUCTj(V62I46XiB&iH z7cMZcX!cJGd2r+73bVz3@a*Mc$?<1B`1qhEk0-BaOF%B%Qwt0PxSUVz%0AM6@Z|kJ zXYbc16wJC3{BVPW)(=v z$B!T9Uy=o??xsv*HZfbUoC%f?9z4JB(@^8o`%?iA?F86wPvCkv$Ki0N(p_b(vpmYH zcGhdYzhIQ9D_-AXwA8oc->c(K3nkOSZQ z$UvV(n#iIHbLx1ycJ8zcWqK68eZ{j#jUCEIQiMuhuJoCk$A0LWqG{V~TbDX%w)E{b zH9rDo%m3NXFVk7zvPHPL>l(|}6MtXYh)qnjdzdq;X3`OjOFYV4-wu1!muQ?$Zc~h@ zRof@F)c9~g*sK#g&3W>IeZ@;s%nB9AxeSKeVq2Y{Y?EAKHyStdPcz$>)zHIsOLa7x`o$9Un zUDp)17@rlNZJIp2s zzkP7lsP?$hrZeg_>9h1z)Q`?cbV;m}@Z?XudB$K-NS%c1ti&5r)r<}=xb0r=V&Asu zr{M~rdwZ+Z=d?CiH|bye{fB|6^U9H)!#aEe-UZEU`q`QmKewtM?I4jnR zP8IN6V4yHp@#n0?u?v0fZ|u5oE^YhyhoRdGIM@1#s%TD@zxl%{zbj_jiWvZfW zSJET`XUMxoLoOslfbEOh!ENmv&A+&MJ9m|f z{JLCUsq;qMV4tz>N&}b7vj&S2?0Z}w{*AKlNjhWj#_MgcV5U@R+y2<~rrY^{8BV#~ zxI}^! diff --git a/doc/diagramas-clase/requerimiento.uxf b/doc/diagramas-clase/requerimiento.uxf index 638e0e1..4ddec5a 100644 --- a/doc/diagramas-clase/requerimiento.uxf +++ b/doc/diagramas-clase/requerimiento.uxf @@ -7,7 +7,7 @@ 190 90 480 - 330 + 340 Package::requex.modelos Requerimiento @@ -25,6 +25,7 @@ Requerimiento +__init__(id: int, titulo: str, descripcion: str, estado: tuple (str, str)) +verificar_completo(): bool +__str__(): str ++__repr__(): str +__eq__(otro: object): bool +__hash__(): int _+obtener_estados_validos(): list de str, list de str_ diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 8a337cb..94d3830 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -364,7 +364,8 @@ class MineroRequerimientos: self.__agregar_requerimiento(requerimientos, titulo, descripcion) self.__agregar_terminos_glosario(glosario, terminos_encontrados, vectores_relevancia) - logger_extraccion_req.debug(requerimientos) + for req in requerimientos: + logger_extraccion_req.debug(req) logger_extraccion_terms.debug(glosario) return requerimientos, glosario diff --git a/src/requex/modelos.py b/src/requex/modelos.py index 129a31b..0cebf72 100644 --- a/src/requex/modelos.py +++ b/src/requex/modelos.py @@ -479,12 +479,25 @@ class Requerimiento: + 'formarse por al menos tres palabras') def __str__(self): - return 'Requerimiento(id=' + str(self.id) + ',titulo=' + self.titulo\ - + ',prioridad=' + str(self.prioridad) + ',descripcion='\ - + self.descripcion + ',estado=' + str(self.estado)\ - + ',volatilidad=' + str(self.volatilidad) + ',tipo='\ - + str(self.tipo) + ',riesgo=' + self.riesgo\ - + ',clausula_organizacion=' + str(self.clausula_organizacion) + ')' + return self.__repr__() + + def __repr__(self): + repr = 'Requerimiento(id=' + str(self.id) + ',titulo=' + self.titulo\ + + ',descripcion=' + self.descripcion + if self.prioridad is not None: + repr += ',prioridad=' + str(self.prioridad) + if self.estado is not None: + repr += ',estado=' + str(self.estado) + if self.volatilidad is not None: + repr += ',volatilidad=' + str(self.volatilidad) + if self.tipo is not None: + repr += ',tipo=' + str(self.tipo) + if self.riesgo: + repr += ',riesgo=' + self.riesgo + if self.clausula_organizacion is not None: + repr += ',clausula_organizacion=' + str(self.clausula_organizacion) + repr += ')' + return repr def __eq__(self, otro): if not otro or type(otro) is not Requerimiento: -- GitLab From b38fab6d89c8fcf669090b05f8712200bec95d49 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Tue, 8 Jun 2021 17:37:20 -0500 Subject: [PATCH 4/9] =?UTF-8?q?Invalidaci=C3=B3n=20de=20oraciones=20interr?= =?UTF-8?q?ogativas=20como=20requerimientos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/requex/extractor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 94d3830..b4539a4 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -379,6 +379,10 @@ class MineroRequerimientos: token, patron_numero, patron_genero, es_patron_valido, ultima_categoria_detectada, colocaciones, completar_titulo, verbo_compuesto) + if token.text in ['¿', '?']: + es_patron_valido = False + titulo = '' + descripcion = '' if not es_patron_valido: return titulo, descripcion, es_patron_valido,\ ultima_categoria_detectada, completar_titulo, verbo_compuesto,\ -- GitLab From 8000bf6d3c01b8c1def3b1124300b5059b2c136b Mon Sep 17 00:00:00 2001 From: nachintoch Date: Tue, 8 Jun 2021 22:05:34 -0500 Subject: [PATCH 5/9] =?UTF-8?q?Regla=20que=20ignora=20requerimientos=20enc?= =?UTF-8?q?ontrados=20al=20final=20de=20una=20oraci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/requex/extractor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/requex/extractor.py b/src/requex/extractor.py index b4539a4..29ee953 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -338,8 +338,6 @@ class MineroRequerimientos: patron_genero = re.compile(r'Gender=\w+') patron_numero = re.compile(r'Number=\w+') for oracion in oraciones_etiquetadas: - # DEBUG los requerimientos podrían detectarse descritos en más de - # una sola oración titulo = '' descripcion = '' terminos_encontrados = [] @@ -347,7 +345,11 @@ class MineroRequerimientos: ultima_categoria_detectada = None completar_titulo = True verbo_compuesto = None + indice_primer_token = 0 + contar_tokens = True for token in oracion: + if contar_tokens: + indice_primer_token += 1 if token.is_space: continue titulo, descripcion, es_patron_valido,\ @@ -359,7 +361,9 @@ class MineroRequerimientos: ultima_categoria_detectada, colocaciones, completar_titulo, verbo_compuesto, terminos_encontrados) - if not titulo: + if titulo: + contar_tokens = False + if not titulo or indice_primer_token > len(oracion) * 0.65: continue self.__agregar_requerimiento(requerimientos, titulo, descripcion) self.__agregar_terminos_glosario(glosario, terminos_encontrados, -- GitLab From 6d56faec14b6737323cad440858680cf7dbde162 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Wed, 9 Jun 2021 13:19:32 -0500 Subject: [PATCH 6/9] Agregado filtro para filtrar textos que son URLS principalmente --- src/requex/__init__.py | 4 +- src/requex/extractor.py | 92 ++++++++++++++++++++++------------------- src/requex/modelos.py | 4 +- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/requex/__init__.py b/src/requex/__init__.py index 56c7e6c..4775aa7 100755 --- a/src/requex/__init__.py +++ b/src/requex/__init__.py @@ -100,9 +100,7 @@ class Requex: self.__estado = Requex.ESTADO_EXTRACCION_REQ minero = MineroRequerimientos() corpus = self.__cargar_corpus(dir_corpus) - textos_corpus = [] - for texto in corpus.values(): - textos_corpus.append(texto) + textos_corpus = [texto for texto in corpus.values()] requerimientos, glosario = minero.extraer_requerimientos(textos_corpus) del minero self.__especificacion_requerimientos.alcances = 'El presente documento'\ diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 29ee953..73a6083 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -20,8 +20,6 @@ import numpy as np import os import re -# import spacy -# spacy.load('es_core_news_md') import es_core_news_md from gensim.corpora import Dictionary @@ -62,6 +60,14 @@ class MineroRequerimientos: archivo_modelo_perifrasis_verbal = os.sep.join(['res', 'modelos', 'perifrasis_verbal.mod']) + patron_genero = re.compile(r'Gender=\w+') + patron_numero = re.compile(r'Number=\w+') + patron_url = re.compile( + r"(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)" + + r"(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|" + + r"(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))") + patron_interrogativo = re.compile(r'\w+\? |\?$') + def __init__(self, relevancia_min=75): if relevancia_min < 0 or relevancia_min > 100: raise AttributeError('La relevancia mínima debe ser un número ' @@ -334,9 +340,6 @@ class MineroRequerimientos: vectores_relevancia): requerimientos = set() glosario = set() - # TODO probablemente deban ser atributos de clase - patron_genero = re.compile(r'Gender=\w+') - patron_numero = re.compile(r'Number=\w+') for oracion in oraciones_etiquetadas: titulo = '' descripcion = '' @@ -356,37 +359,33 @@ class MineroRequerimientos: ultima_categoria_detectada, completar_titulo,\ verbo_compuesto, terminos_encontrados =\ self.__extrae_requerimiento( - titulo, descripcion, token, patron_numero, - patron_genero, es_patron_valido, + titulo, descripcion, token, es_patron_valido, ultima_categoria_detectada, colocaciones, completar_titulo, verbo_compuesto, terminos_encontrados) if titulo: contar_tokens = False - if not titulo or indice_primer_token > len(oracion) * 0.65: - continue - self.__agregar_requerimiento(requerimientos, titulo, descripcion) self.__agregar_terminos_glosario(glosario, terminos_encontrados, vectores_relevancia) - for req in requerimientos: - logger_extraccion_req.debug(req) - logger_extraccion_terms.debug(glosario) + titulo = titulo.strip().capitalize() + descripcion = descripcion.strip().capitalize() + titulo = self.__limpiar_texto(titulo) + descripcion = self.__limpiar_texto(descripcion) + if not titulo or indice_primer_token > len(oracion) * 0.65 or\ + not self.__es_descripcion_valida(descripcion): + continue + self.__agregar_requerimiento(requerimientos, titulo, descripcion) + self.__registra_informacion_extraida(requerimientos, glosario) return requerimientos, glosario - def __extrae_requerimiento(self, titulo, descripcion, token, patron_numero, - patron_genero, es_patron_valido, - ultima_categoria_detectada, colocaciones, - completar_titulo, verbo_compuesto, + def __extrae_requerimiento(self, titulo, descripcion, token, + es_patron_valido, ultima_categoria_detectada, + colocaciones, completar_titulo, verbo_compuesto, terminos_encontrados): pronombre_anterior, es_patron_valido, completar_titulo =\ self.__identifica_extrae_texto_patron( - token, patron_numero, patron_genero, es_patron_valido, - ultima_categoria_detectada, colocaciones, completar_titulo, - verbo_compuesto) - if token.text in ['¿', '?']: - es_patron_valido = False - titulo = '' - descripcion = '' + token, es_patron_valido, ultima_categoria_detectada, + colocaciones, completar_titulo, verbo_compuesto) if not es_patron_valido: return titulo, descripcion, es_patron_valido,\ ultima_categoria_detectada, completar_titulo, verbo_compuesto,\ @@ -400,8 +399,7 @@ class MineroRequerimientos: terminos_encontrados # (SUST*+VERB*+ADJ*+ADV*)+ - def __identifica_extrae_texto_patron(self, token, patron_numero, - patron_genero, es_patron_valido, + def __identifica_extrae_texto_patron(self, token, es_patron_valido, ultima_categoria_detectada, colocaciones, completar_titulo, verbo_compuesto): @@ -418,7 +416,6 @@ class MineroRequerimientos: es_patron_valido, ultima_categoria_detectada, verbo_compuesto,\ completar_titulo, pronombre_anterior =\ self.__detecta_patron_verbal(token, es_patron_valido, - patron_numero, patron_genero, ultima_categoria_detectada, colocaciones, verbo_compuesto, completar_titulo, @@ -446,10 +443,10 @@ class MineroRequerimientos: return es_patron_valido, ultima_categoria_detectada, completar_titulo,\ pronombre_anterior, verbo_compuesto - def __detecta_patron_verbal(self, token, es_patron_valido, patron_numero, - patron_genero, ultima_categoria_detectada, - colocaciones, verbo_compuesto, - completar_titulo, pronombre_anterior): + def __detecta_patron_verbal(self, token, es_patron_valido, + ultima_categoria_detectada, colocaciones, + verbo_compuesto, completar_titulo, + pronombre_anterior): if token.lemma_ in self.__verbos_clave: es_patron_valido = True elif not es_patron_valido and token.lemma_ in self.__verbos_perifrasis\ @@ -459,22 +456,22 @@ class MineroRequerimientos: completar_titulo, pronombre_anterior, ultima_categoria_detectada,\ verbo_compuesto = self.__detecta_posicion_patron_verbal( token, completar_titulo, ultima_categoria_detectada, - pronombre_anterior, verbo_compuesto, patron_genero, - patron_numero, colocaciones) + pronombre_anterior, verbo_compuesto, colocaciones) return es_patron_valido, ultima_categoria_detectada, verbo_compuesto,\ completar_titulo, pronombre_anterior def __detecta_posicion_patron_verbal(self, token, completar_titulo, ultima_categoria_detectada, pronombre_anterior, verbo_compuesto, - patron_genero, patron_numero, colocaciones): if ultima_categoria_detectada in ['NOUN', 'VERB']: token_anterior = token.doc[verbo_compuesto] - if patron_genero.match(token_anterior.tag_) ==\ - patron_genero.match(token.tag_) and\ - patron_numero.match(token_anterior.tag_)\ - == patron_numero.match(token.tag_): + if MineroRequerimientos.patron_genero.findall( + token_anterior.tag_) ==\ + MineroRequerimientos.patron_genero.findall(token.tag_) and\ + MineroRequerimientos.patron_numero.findall( + token_anterior.tag_) ==\ + MineroRequerimientos.patron_numero.findall(token.tag_): colocaciones.add(Colocacion( str(token.doc[verbo_compuesto:token.i]), ' ')) elif ultima_categoria_detectada in ['ADJ', 'ADV']: @@ -533,10 +530,6 @@ class MineroRequerimientos: break def __agregar_requerimiento(self, requerimientos, titulo, descripcion): - titulo = titulo.strip().capitalize() - descripcion = descripcion.strip().capitalize() - titulo = self.__limpiar_texto(titulo) - descripcion = self.__limpiar_texto(descripcion) estado = 'Identificado' try: id = len(requerimientos) + 1 @@ -622,6 +615,16 @@ class MineroRequerimientos: texto = texto[:indice_original + 2] + texto[i - 1:] return texto + def __es_descripcion_valida(self, descripcion): + if MineroRequerimientos.patron_interrogativo.findall(descripcion): + return False + lon_urs = 0 + for urls in MineroRequerimientos.patron_url.findall(descripcion): + lon_urs += len(urls) + if lon_urs >= len(descripcion) * 0.5: + return False + return True + def __es_requerimiento_unico(self, requerimiento, requerimientos): if requerimiento in requerimientos: return False @@ -651,6 +654,11 @@ class MineroRequerimientos: return False return True + def __registra_informacion_extraida(self, requerimientos, glosario): + for req in requerimientos: + logger_extraccion_req.debug(req) + logger_extraccion_terms.debug(glosario) + def obtener_sustantivos_clave(): return ['sistema', 'aplicación', 'software', 'usuario', 'participante', 'paquete', 'mensaje', 'función', 'módulo', 'interfaz', diff --git a/src/requex/modelos.py b/src/requex/modelos.py index 0cebf72..f8b13d7 100644 --- a/src/requex/modelos.py +++ b/src/requex/modelos.py @@ -249,8 +249,8 @@ class EspecificacionRequerimientosSoftware: class Requerimiento: - clase_caracteres_vacios =\ - '[' + string.whitespace + '\\.,;:¿\\?\'"¡!·\\(\\)\\{\\}\\-_\\[\\]]' + clase_caracteres_vacios = '[' + string.whitespace\ + + r'\.,;:¿\?\'"¡!·\(\)\{\}\-_\[\]]' def __init__(self, id, titulo, descripcion, estado): if id is None: -- GitLab From 1018f873562c7e9704ad6e7770ad7b56962c51a0 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Wed, 9 Jun 2021 14:01:26 -0500 Subject: [PATCH 7/9] =?UTF-8?q?Agregado=20filtro=20contra=20requerimientos?= =?UTF-8?q?=20mayormente=20c=C3=B3digo=20XML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- src/requex/extractor.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14e30eb..939d3b2 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ Universidad Nacional Autónoma de México, 2021 - flake8 - pytest - pyinstaller - - fpdf2 + - fpdf2 + - bs4 # Distribución Los lanzamientos de Requex pueden identificarse con etiquetas de Git que diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 73a6083..a127b95 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -21,6 +21,7 @@ import os import re import es_core_news_md +from bs4 import BeautifulSoup from gensim.corpora import Dictionary from gensim.models.phrases import Phrases, Phraser @@ -623,6 +624,13 @@ class MineroRequerimientos: lon_urs += len(urls) if lon_urs >= len(descripcion) * 0.5: return False + descripcion_sn_espacios = descripcion.replace(' ', '') + lon_xml = 0 + for xml in BeautifulSoup(descripcion_sn_espacios, + 'html.parser').find_all(): + lon_xml += len(str(xml)) + if lon_xml >= len(descripcion_sn_espacios) * 0.5: + return False return True def __es_requerimiento_unico(self, requerimiento, requerimientos): -- GitLab From 5d64804acb1e27055ccd9d395b2c48df6e25ae88 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Wed, 9 Jun 2021 17:47:51 -0500 Subject: [PATCH 8/9] =?UTF-8?q?Se=20encontr=C3=B3=20una=20soluci=C3=B3n=20?= =?UTF-8?q?al=20issue=20#40=20revisando=20propiedades=20de=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/requex/extractor.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/requex/extractor.py b/src/requex/extractor.py index a127b95..18636da 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -68,6 +68,9 @@ class MineroRequerimientos: + r"(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|" + r"(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'\".,<>?«»“”‘’]))") patron_interrogativo = re.compile(r'\w+\? |\?$') + patron_codigo = re.compile( + r'(?:\w+\d* ?)(?:\.\w+\d* ?)*(?:\(.*\)|' + + r'[+\-/*!=]?= ?(?:["\']?\w*["\']|\d+))') def __init__(self, relevancia_min=75): if relevancia_min < 0 or relevancia_min > 100: @@ -500,10 +503,15 @@ class MineroRequerimientos: if not titulo: titulo = pronombre_anterior descripcion = pronombre_anterior - if not token.is_punct: + if (not token.is_punct and + not token.doc[token.i - 1].is_left_punct) or\ + (token.is_left_punct and + not token.doc[token.i - 1].is_left_punct): titulo += ' ' titulo += token.text - if not token.is_punct: + if (not token.is_punct and not token.doc[token.i - 1].is_left_punct) or\ + (token.is_left_punct and + not token.doc[token.i - 1].is_left_punct): descripcion += ' ' descripcion += token.text return titulo, descripcion @@ -624,14 +632,21 @@ class MineroRequerimientos: lon_urs += len(urls) if lon_urs >= len(descripcion) * 0.5: return False - descripcion_sn_espacios = descripcion.replace(' ', '') + return not self.__es_codigo(descripcion) + + def __es_codigo(self, texto): + texto_sn_espacios = texto.replace(' ', '') lon_xml = 0 - for xml in BeautifulSoup(descripcion_sn_espacios, - 'html.parser').find_all(): + for xml in BeautifulSoup(texto_sn_espacios, 'html.parser').find_all(): lon_xml += len(str(xml)) - if lon_xml >= len(descripcion_sn_espacios) * 0.5: - return False - return True + if lon_xml >= len(texto_sn_espacios) * 0.5: + return True + lon_codigo = 0 + for codigo in MineroRequerimientos.patron_codigo.findall(texto): + lon_codigo += len(codigo) + if lon_codigo >= len(texto) * 0.5: + return True + return False def __es_requerimiento_unico(self, requerimiento, requerimientos): if requerimiento in requerimientos: -- GitLab From 9cad188866e420b903e700560ba735588e3f4116 Mon Sep 17 00:00:00 2001 From: nachintoch Date: Wed, 9 Jun 2021 18:30:08 -0500 Subject: [PATCH 9/9] =?UTF-8?q?Agregado=20filtro=20para=20candidatos=20a?= =?UTF-8?q?=20t=C3=A9rminos=20del=20glosario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/requex/extractor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/requex/extractor.py b/src/requex/extractor.py index 18636da..f7d532b 100644 --- a/src/requex/extractor.py +++ b/src/requex/extractor.py @@ -518,7 +518,10 @@ class MineroRequerimientos: def __prepara_terminos_glosario(self, token, terminos_encontrados, colocaciones): - if token.lemma_ not in terminos_encontrados: + if token.lemma_ not in terminos_encontrados and not token.is_stop and\ + not token.is_punct and not token.like_num and\ + not token.like_url and not token.like_email and\ + len(token.lemma_) >= 2: if token.lemma_ in colocaciones: terminos_encontrados +=\ Colocacion.colocaciones_con_termino(colocaciones, -- GitLab