<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Bit4bit Integraciones</title><link>http://bit4bit.github.io/</link><description>Recent content on Bit4bit Integraciones</description><generator>Hugo -- gohugo.io</generator><language>es-co</language><managingEditor>bit4bit@riseup.net (Jovany Leandro G.C)</managingEditor><webMaster>bit4bit@riseup.net (Jovany Leandro G.C)</webMaster><lastBuildDate>Mon, 20 Nov 2023 13:13:16 -0500</lastBuildDate><atom:link href="http://bit4bit.github.io/index.xml" rel="self" type="application/rss+xml"/><item><title>FreeSWITCH talks to MS Teams</title><link>http://bit4bit.github.io/post/freeswitch_msteams/</link><pubDate>Mon, 20 Nov 2023 13:13:16 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/freeswitch_msteams/</guid><description>&lt;p>Voy explicar como logre conectar FreeSWITCH con MS Team.&lt;/p>
&lt;p>Para configurar MS TEAMS ver &lt;a href="http://bit4bit.github.io/post/kamailio_sbc_msteams/">Kamailio SBC MS Teams&lt;/a>&lt;/p>
&lt;p>Escenario confirmado.&lt;/p>
&lt;div class="mermaid">flowchart LR
MSTeams["MS Teams
v.2023.6.21.5
"]
PBX["FreeSWITCH
v1.10.10
"]
MSTeams ---|TLS/PUBLIC IP| PBX
&lt;/div>
&lt;p>Funcionalidades confirmadas:&lt;/p>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Llamadas MSTeams a PBX&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Llamadas PBX a MSTeams&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Hold&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Park&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Unpark&lt;/li>
&lt;/ul>
&lt;p>Crear &lt;strong>sofia profile&lt;/strong> para ser usado en las conexiones a MS Teams,&lt;/p>
&lt;p>en este profile habilitar:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;tls-only&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;full-id-in-dialplan&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;inbound-use-callid-as-uuid&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;outbound-use-uuid-as-callid&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>crear pasarela a MS Teams:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- sustituir mysbc.bit4bit.in por el nombre registrado en MS Teams --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;gateway&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;mysbc.bit4bit.in&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;realm&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;mysbc.bit4bit.in&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;register&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;false&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;caller-id-in-from&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;contact-in-ping&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;contact-host&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;mysbc.bit4bit.in&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;extension-in-contact&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;ping&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;30&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;register-transport&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;tls&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;cid-type&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;pid&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/gateway&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>al refrescar los perfiles y pasarelas en FreeSWITCH,
ya podríamos ver que tenemos conexiones en ambos sentidos.&lt;/p>
&lt;p>Ahora para lanzar llamadas al MS Teams desde FreeSWITCH.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;condition...&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:sip_secure_media=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:rtp_secure_media=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;bridge&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;{absolute_codec_string=G722}sofia/gateway/mysbc.bit4bit.in/+NUMERO&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/condition&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y para gestionar Hold y Transferencias desde MS Teams.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;extension&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;refer-msteams extract referred_by&amp;#34;&lt;/span> &lt;span style="color:#008080">continue=&lt;/span>&lt;span style="color:#b84">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;condition&lt;/span> &lt;span style="color:#008080">field=&lt;/span>&lt;span style="color:#b84">&amp;#34;${sip_refer_to}&amp;#34;&lt;/span> &lt;span style="color:#008080">expression=&lt;/span>&lt;span style="color:#b84">&amp;#34;microsoft\.com&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;condition&lt;/span> &lt;span style="color:#008080">field=&lt;/span>&lt;span style="color:#b84">&amp;#34;${sip_h_Referred-By}&amp;#34;&lt;/span> &lt;span style="color:#008080">expression=&lt;/span>&lt;span style="color:#b84">&amp;#34;^&amp;lt;(.+)&amp;gt;$&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;set&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;sip_referrer_by_extracted=$1&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/condition&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/extension&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;extension&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;refer-msteam&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;condition&lt;/span> &lt;span style="color:#008080">field=&lt;/span>&lt;span style="color:#b84">&amp;#34;${sip_refer_to}&amp;#34;&lt;/span> &lt;span style="color:#008080">expression=&lt;/span>&lt;span style="color:#b84">&amp;#34;microsoft\.com&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;condition&lt;/span> &lt;span style="color:#008080">field=&lt;/span>&lt;span style="color:#b84">&amp;#34;${sip_referrer_by_extracted}&amp;#34;&lt;/span> &lt;span style="color:#008080">expresion=&lt;/span>&lt;span style="color:#b84">&amp;#34;(.+)&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:sip_invite_to_uri=$1&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:sip_invite_req_uri=$1&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:sip_secure_media=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;export&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;nolocal:rtp_secure_media=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;bridge&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;{absolute_codec_string=G722}sofia/gateway/mysbc.bit4bit.in&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/condition&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/extension&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para recibir llamadas desde MS Teams, debemos ajustar los planes de la siguiente manera.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">&amp;lt;!-- las llamadas desde MS Teams llegarian como 31000@mysbc.bit4bit.in --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;condition&lt;/span> &lt;span style="color:#008080">field=&lt;/span>&lt;span style="color:#b84">&amp;#34;destination_number&amp;#34;&lt;/span> &lt;span style="color:#008080">expresion=&lt;/span>&lt;span style="color:#b84">&amp;#34;^3(\d{9})@.+$&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;set&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;sip_secure_media=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;set&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;disable_q850_reason=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Emacs - Básico 1</title><link>http://bit4bit.github.io/post/emacs_basics_1/</link><pubDate>Sun, 09 Jul 2023 21:54:27 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/emacs_basics_1/</guid><description>&lt;p>GNU EMACS es un editor altamente extensible y vamos aprovechar esta posibilidad
que nos ofrecen y construir un paquete que nos permita cubrir
varios elementos (peticiones HTTP, SON, TDD, EMACS),
el ejercicio entorna a implementar un módulo de EMACS que nos permita obtener el titulo de una incidencia, procediendo con:&lt;/p>
&lt;ul>
&lt;li>Autenticar a Gitea&lt;/li>
&lt;li>Solicitar id de incidencia&lt;/li>
&lt;li>Consultar titulo de incidencia indicado&lt;/li>
&lt;li>E insertar titulo en el árbol del documento org.&lt;/li>
&lt;/ul>
&lt;p>importante antes de empezar:&lt;/p>
&lt;ul>
&lt;li>ser usuario de EMACS.&lt;/li>
&lt;li>nociones en desarrollo de software.&lt;/li>
&lt;li>nociones del lenguaje LISP o uno de sus dialectos.&lt;/li>
&lt;/ul>
&lt;p>Vamos a usar el &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Introduction.html">dialecto ELisp&lt;/a> de EMACS el cual iremos conociendo poco a poco en el transcurso de la implementación.&lt;/p>
&lt;p>Empecemos por crear la estructura mínima del paquete.&lt;/p>
&lt;p>&lt;strong>Makefile&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-makefile" data-lang="makefile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#900;font-weight:bold">.PHONY&lt;/span>&lt;span style="font-weight:bold">:&lt;/span> test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#900;font-weight:bold">test&lt;/span>&lt;span style="font-weight:bold">:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> emacs --batch -L . -L tests -l grieta-tests.el -f ert-run-tests-batch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Cannot open load file: No such file or directory, grieta-tests.el
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>make: *** [Makefile:3: test] Error 255
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Empecemos a ilustrarnos a partir del error&lt;/p>
&lt;p>&lt;code>Cannot open load file: No such file or directory, grieta-tests.el&lt;/code>&lt;/p>
&lt;p>no ubica el archivo &lt;code>grieta-tests.el&lt;/code>, debemos indicar el directorio donde se encuentra el archivo,
usamos los siguientes parametros.&lt;/p>
&lt;ul>
&lt;li>en el parametro &lt;code>-L&lt;/code> le indicamos a &lt;code>ENACS&lt;/code> donde ubicar archivos &lt;code>Lisp&lt;/code>.&lt;/li>
&lt;li>en el parametro &lt;code>-l&lt;/code> le indicamos interpretar el archivo &lt;code>Lisp&lt;/code>.&lt;/li>
&lt;li>en el parametro &lt;code>-f&lt;/code> le indicamos una función a ejecutar.&lt;/li>
&lt;/ul>
&lt;p>En otras palabras le estamos indicando a EMACS que ejecuta la función &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/ert/Running-Tests-in-Batch-Mode.html">ert-run-tests-batch&lt;/a> además que interprete el archivo &lt;code>grieta-tests.el&lt;/code> y que
posiblemente lo puede encontrar en el directorio &lt;code>tests&lt;/code>.
.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>mkdir tests
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>touch tests/grieta-tests.el
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code class="language-ssh" data-lang="ssh">make test
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 0 tests (2023-07-10 21:29:29-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 0 tests, 0 results as expected, 0 unexpected (2023-07-10 21:29:29-0500, 0.000060 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>muy bien no mas errores por ahora, ahora vamos a proceder a iniciar con las pruebas
y por medio de estas iremos realizando la implementación. Para automatizar las pruebas
utilizaremos &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/ert/">ERT&lt;/a>.&lt;/p>
&lt;p>Para empezar definimos como agrupar las pruebas:&lt;/p>
&lt;ul>
&lt;li>&lt;code>UNIT&lt;/code> funciones que solo varían su comportamiento según los argumentos.&lt;/li>
&lt;li>&lt;code>INTEGRATION&lt;/code> funciones que varían su comportamiento según los argumentos u otros medios como variables globales o lecturas fuera del proceso (lectura disco, red, etc..).&lt;/li>
&lt;li>&lt;code>ACCEPTANCE&lt;/code> los mismo criterios que &lt;code>INTEGRATION&lt;/code> pero solo sobre las funciones publicadas es decir sobre las funciones que son accesible por el usuario u otros paquetes.&lt;/li>
&lt;li>&lt;code>MANUAL&lt;/code> pruebas que se deben realizar manualmente ya que el contexto no se puede reproduccir en &lt;code>ACCEPTANCE&lt;/code>.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Cannot open load file: No such file or directory, el-mock
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>make: *** [Makefile:3: test] Error 255
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Uumm ya hemos visto anteriormente este error, &lt;code>el-mock&lt;/code> es una dependencia
que utilizaremos para verificar el comportamiento en las pruebas de &lt;code>INTEGRATION&lt;/code> y &lt;code>ACCEPTANCE&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>curl -q https://raw.githubusercontent.com/rejeep/el-mock.el/master/el-mock.el --output tests/el-mock.el
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 0 tests (2023-07-10 21:46:25-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 0 tests, 0 results as expected, 0 unexpected (2023-07-10 21:46:25-0500, 0.000052 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Excelente, ya no tenemos error al importar la dependencia &lt;code>el-mock&lt;/code> con (&lt;a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html#index-require">require&lt;/a> &amp;rsquo;el-mock).&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-issue-check-url-path condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (void-function grieta--gitea-issue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000067 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Iniciar las pruebas desde &lt;code>INTEGRATION&lt;/code> o &lt;code>ACCEPTANCE&lt;/code> nos facilita tener una perspectiva más cercana a su uso y empezar con anterioridad el flujo de la implementación, hemos decido empezar por confirmar que la petición este en la estructura
esperada por la API de gitea.&lt;/p>
&lt;p>&lt;code>(void-function grieta--gitea-issue)&lt;/code> lo vamos a ver cuando no logra llamar la función, en nuestro caso es por que no existe.&lt;/p>
&lt;!-- creacion funcion grieta--gitea-issue -->
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> (mock-error not-called grieta--gitea-call)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000075 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>(mock-error not-called grieta--gitea-call)&lt;/code> nos indica que se esperaba que se invocará esa función pero no sucedió.&lt;/p>
&lt;!-- creacion funcion grieta--gitea-call -->
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 1 tests (2023-07-10 22:03:12-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000062 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 1 tests, 1 results as expected, 0 unexpected (2023-07-10 22:03:12-0500, 0.000143 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Muy bien hemos ajustado según lo ilustrado por la prueba, ahora juguemos un poco en&lt;br>
aclarar el código, las funciones que creamos &lt;code>grieta--gitea-issue&lt;/code> y &lt;code>grieta--gitea-call&lt;/code> están en las pruebas, movamos estas pruebas a su propio archivo.&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-issue-check-url-path condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (void-function grieta--gitea-issue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000073 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>(void-function grieta--gitea-issue)&lt;/code> ya hemos lo hemos vistos anteriormente, no logra ubicar
la función y claro es debido a que movimos la función a otro archivo, debemos
importar el archivo.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre tabindex="0">&lt;code class="language-output" data-lang="output">Running 1 tests (2023-07-10 22:08:57-0500, selector ‘t’)
passed 1/1 grieta-test-grieta--gitea-issue-check-url-path (0.000060 sec)
Ran 1 tests, 1 results as expected, 0 unexpected (2023-07-10 22:08:57-0500, 0.000137 sec)
&lt;/code>&lt;/pre>&lt;p>Muy bien volvimos a estar estables, ya que empezamos por validar la estructura
de la URL podemos continuar sobre el mismo tema de validar elementos
hacia la API externa, procedamos por ejemplo con traducir la respuesta JSON a
una estructura interna.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; respuesta simplificada&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#b84">&amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">&amp;#39;&lt;/span>(&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#b84">&amp;#34;Test&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-json-&amp;gt;issues condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (ert-test-failed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((should
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (equal &amp;#39;...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (grieta--gitea-issue &amp;#34;gitea.test.org&amp;#34; &amp;#34;demo&amp;#34; &amp;#34;grieta&amp;#34; 1)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :form
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (equal
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((1 &amp;#34;Test&amp;#34;))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> :value nil :explanation
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (different-types
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((1 &amp;#34;Test&amp;#34;))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 2/2 grieta-test-grieta--gitea-json-&amp;gt;issues (0.000051 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>miremos solo una parte&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span> :form
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (equal
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((1 &amp;#34;Test&amp;#34;))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>evidentemente los resultados no son iguales estamos retornando directamente
la respuesta de la API, procedamos a implementar el traductor.&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 2 tests (2023-07-10 22:35:19-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 1/2 grieta-test-grieta--gitea-issue-check-url-path (0.000058 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/2 grieta-test-grieta--gitea-json-&amp;gt;issues (0.000062 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 2 tests, 2 results as expected, 0 unexpected (2023-07-10 22:35:19-0500, 0.000208 sec
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Hemos logrado estar otra vez estables, pero en este caso adicionamos un &lt;code>if&lt;/code>
lo cual nos invita que debemos verificar cual sera el comportamiento cuando no se obtenga
una respuesta desde el servidor.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; respuesta simplificada&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#b84">&amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">&amp;#39;&lt;/span>(&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#b84">&amp;#34;Test&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#008080">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span> &lt;span style="color:#998;font-style:italic">; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 3 tests (2023-07-10 22:39:39-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000064 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data (0.000030 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000067 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-10 22:39:39-0500, 0.000269 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ya con las pruebas en curso pasemos a completar la implementación es decir hacer la petición HTTP.&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Verificamos manualmente que funcione la petición al servidor.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>emacs --batch -l grieta.el --eval&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;(print (grieta--gitea-call &amp;#34;gitea.demo.org&amp;#34; &amp;#34;GET&amp;#34; &amp;#34;/api/v1/repos/bit4bit/prueba/issues/1&amp;#34;))&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;#34;{\&amp;#34;errors\&amp;#34;:null,\&amp;#34;message\&amp;#34;:\&amp;#34;The target couldn&amp;#39;t be found.\&amp;#34;,\&amp;#34;url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/api/swagger\&amp;#34;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>uuum, consultemos a un repositorio público&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>emacs --batch -l grieta.el --eval&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;(print (grieta--gitea-call &amp;#34;gitea.onecluster.org&amp;#34; &amp;#34;GET&amp;#34; &amp;#34;/api/v1/repos/OneTeam/trytondo-analytic_operations/issues/1&amp;#34;))&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;#34;{\&amp;#34;id\&amp;#34;:1846,\&amp;#34;url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/api/v1/repos/OneTeam/trytondo-analytic_operations/issues/1\&amp;#34;,\&amp;#34;html_url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/OneTeam/trytondo-analytic_operations/issues/1\&amp;#34;,\&amp;#34;number\&amp;#34;:1,\&amp;#34;user\&amp;#34;:{\&amp;#34;id\&amp;#34;:5,\&amp;#34;login\&amp;#34;:\&amp;#34;Rodia\&amp;#34;,\&amp;#34;login_name\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;full_name\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;email\&amp;#34;:\&amp;#34;rodia@noreply.onecluster.org\&amp;#34;,\&amp;#34;avatar_url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/avatar/2bb02a97b20d83fd00f4e7e044e9c1ef\&amp;#34;,\&amp;#34;language\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;is_admin\&amp;#34;:false,\&amp;#34;last_login\&amp;#34;:\&amp;#34;0001-01-01T00:00:00Z\&amp;#34;,\&amp;#34;created\&amp;#34;:\&amp;#34;2021-09-05T14:03:55-05:00\&amp;#34;,\&amp;#34;restricted\&amp;#34;:false,\&amp;#34;active\&amp;#34;:false,\&amp;#34;prohibit_login\&amp;#34;:false,\&amp;#34;location\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;website\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;description\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;visibility\&amp;#34;:\&amp;#34;public\&amp;#34;,\&amp;#34;followers_count\&amp;#34;:0,\&amp;#34;following_count\&amp;#34;:0,\&amp;#34;starred_repos_count\&amp;#34;:0,\&amp;#34;username\&amp;#34;:\&amp;#34;Rodia\&amp;#34;},\&amp;#34;original_author\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;original_author_id\&amp;#34;:0,\&amp;#34;title\&amp;#34;:\&amp;#34;AÃ±adir Centro de Costos en Encabezado a Facturas de Cliente y de Proveedor\&amp;#34;,\&amp;#34;body\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;ref\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;assets\&amp;#34;:[],\&amp;#34;labels\&amp;#34;:[],\&amp;#34;milestone\&amp;#34;:null,\&amp;#34;assignee\&amp;#34;:null,\&amp;#34;assignees\&amp;#34;:null,\&amp;#34;state\&amp;#34;:\&amp;#34;open\&amp;#34;,\&amp;#34;is_locked\&amp;#34;:false,\&amp;#34;comments\&amp;#34;:0,\&amp;#34;created_at\&amp;#34;:\&amp;#34;2023-07-01T00:01:50-05:00\&amp;#34;,\&amp;#34;updated_at\&amp;#34;:\&amp;#34;2023-07-01T00:01:50-05:00\&amp;#34;,\&amp;#34;closed_at\&amp;#34;:null,\&amp;#34;due_date\&amp;#34;:null,\&amp;#34;pull_request\&amp;#34;:null,\&amp;#34;repository\&amp;#34;:{\&amp;#34;id\&amp;#34;:2134,\&amp;#34;name\&amp;#34;:\&amp;#34;trytondo-analytic_operations\&amp;#34;,\&amp;#34;owner\&amp;#34;:\&amp;#34;OneTeam\&amp;#34;,\&amp;#34;full_name\&amp;#34;:\&amp;#34;OneTeam/trytondo-analytic_operations\&amp;#34;}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ajaa, investigando un poco sobre la API nos indica que para repositorios privados
debemos que una Api Key, y ya ilustrados sobre este escenario procedamos a implementarlo,
pero antes revisemos algunos caminos.&lt;/p>
&lt;ol>
&lt;li>crear otra función ejemplo &lt;code>grieta--gitea-auth-call&lt;/code>.&lt;/li>
&lt;li>usar una variable global o función que nos entregue el api-key.&lt;/li>
&lt;li>adicionar otro parámetro a la función &lt;code>grieta-gitea-call&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>La primera ya que aunque puede ser la más &amp;ldquo;sencilla&amp;rdquo; tendríamos
redundancia en la lógica que a posterior nos afectaría el mantenimiento, entonces la descartamos.&lt;/p>
&lt;p>Ahora valoremos la segunda, especulemos sobre una posible implementación&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defvar&lt;/span> &lt;span style="color:#008080">grieta-gitea-api-key-call&lt;/span> &lt;span style="color:#008080">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">grieta-gitea-api-key-call&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; acoplamiento&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">grieta-gitea-api-key-call&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>al ejecutar las pruebas estas pasan, pero miremos algo, recordando lo siguiente:&lt;/p>
&lt;ul>
&lt;li>&lt;code>UNIT&lt;/code> funciones que solo varían su comportamiento según los argumentos.&lt;/li>
&lt;li>&lt;code>INTEGRATION&lt;/code> funciones que varían su comportamiento según los argumentos u otros medios como variables globales o lecturas fuera del proceso (lectura disco, red, etc..).&lt;/li>
&lt;li>&lt;code>ACCEPTANCE&lt;/code> los mismo criterios que &lt;code>INTEGRATION&lt;/code> pero solo sobre las funciones publicadas es decir sobre las funciones que son accesible por el usuario u otros paquetes.&lt;/li>
&lt;li>&lt;code>MANUAL&lt;/code> pruebas que se deben realizar manualmente.&lt;/li>
&lt;/ul>
&lt;p>hasta ahora hemos realizado las pruebas en el grupo de &lt;code>INTEGRATION&lt;/code> donde
hemos verificado la interacción con &lt;code>grieta--gitea-call&lt;/code> por medio de un doble,
hasta ahora no nos hemos interesado en probar la función ya que esta es una pasarela un servicio
externo donde no tenemos mas lógica que la peticion al servicio pero ahora
la situación cambia ya que creamos una dependencia implícita (ya que solo leyendo la implementación podemos determinarla) a una variable que va ser
modificada por el usuario creando un acoplamiento de un mecanismo privado a un
mecanismo netamente publico, acá es de suma importancia mantener una clara separacion
entre capas, &lt;code>grieta--gitea-call&lt;/code> pertenece a una capa privada
mientras que &lt;code>grieta-gitea-api-key-call&lt;/code> pertenece a una capa publica,
por lo tanto necesitaríamos algún mecanismo que nos permita pasar la configuración
desde la capa publica a la privada.
ahora especulemos sobre adicionar un parámetro&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">api-key&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>al cambiar la firma de la función debemos actualizar todas las funciones dependientes &lt;a href="https://refactoring.com/">Martin Fowler en Refactoring&lt;/a>
propone una metodología estructura de realizar esto, ya que es algo que estamos haciendo constantemente y es de suma importancia aprender a
refactorizar para no tomar caminos mas &amp;ldquo;rápidos&amp;rdquo; que a largo plazo son perjudiciales, ahora si valoramos nuevamente donde
realizaríamos las pruebas estas irían en &lt;code>INTEGRATION&lt;/code> ya que realizan lecturas
fuera de proceso, y mirando un poco mas de cerca la función esta solo realiza actividades fuera de proceso osea
que si realizáramos pruebas tendríamos que usar dobles para simular el comportamiento de funciones como &lt;code>url-retrieve-synchronously&lt;/code>
pero este tipo de pruebas seria falsas ya que no conocemos o participamos del ciclo de desarrollo de las funciones,
por lo cual este tipo de funciones las verificaremos indirectamente en comportamientos de &lt;code>INTEGRATION&lt;/code> o &lt;code>ACCEPTANCE&lt;/code> o bien &lt;code>MANUAL&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>emacs --batch -l grieta.el --eval&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;(print (grieta--gitea-call &amp;#34;gitea.demo.org&amp;#34; &amp;#34;oeuaeiaoi&amp;#34; &amp;#34;GET&amp;#34; &amp;#34;/api/v1/repos/bit4bit/prueba/issues/1&amp;#34;))&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;#34;{\&amp;#34;id\&amp;#34;:1861,\&amp;#34;url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/api/v1/repos/bit4bit/prueba/issues/1\&amp;#34;,\&amp;#34;html_url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/bit4bit/prueba/issues/1\&amp;#34;,\&amp;#34;number\&amp;#34;:1,\&amp;#34;user\&amp;#34;:{\&amp;#34;id\&amp;#34;:10,\&amp;#34;login\&amp;#34;:\&amp;#34;bit4bit\&amp;#34;,\&amp;#34;login_name\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;full_name\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;email\&amp;#34;:\&amp;#34;bit4bit@noreply.onecluster.org\&amp;#34;,\&amp;#34;avatar_url\&amp;#34;:\&amp;#34;https://gitea.onecluster.org/avatar/5984dea48f59d0cbf32c81e80cc81525\&amp;#34;,\&amp;#34;language\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;is_admin\&amp;#34;:false,\&amp;#34;last_login\&amp;#34;:\&amp;#34;0001-01-01T00:00:00Z\&amp;#34;,\&amp;#34;created\&amp;#34;:\&amp;#34;2021-11-24T14:59:21-05:00\&amp;#34;,\&amp;#34;restricted\&amp;#34;:false,\&amp;#34;active\&amp;#34;:false,\&amp;#34;prohibit_login\&amp;#34;:false,\&amp;#34;location\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;website\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;description\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;visibility\&amp;#34;:\&amp;#34;private\&amp;#34;,\&amp;#34;followers_count\&amp;#34;:0,\&amp;#34;following_count\&amp;#34;:0,\&amp;#34;starred_repos_count\&amp;#34;:0,\&amp;#34;username\&amp;#34;:\&amp;#34;bit4bit\&amp;#34;},\&amp;#34;original_author\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;original_author_id\&amp;#34;:0,\&amp;#34;title\&amp;#34;:\&amp;#34;prueba\&amp;#34;,\&amp;#34;body\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;ref\&amp;#34;:\&amp;#34;\&amp;#34;,\&amp;#34;assets\&amp;#34;:[],\&amp;#34;labels\&amp;#34;:[],\&amp;#34;milestone\&amp;#34;:null,\&amp;#34;assignee\&amp;#34;:null,\&amp;#34;assignees\&amp;#34;:null,\&amp;#34;state\&amp;#34;:\&amp;#34;open\&amp;#34;,\&amp;#34;is_locked\&amp;#34;:false,\&amp;#34;comments\&amp;#34;:0,\&amp;#34;created_at\&amp;#34;:\&amp;#34;2023-07-07T13:57:25-05:00\&amp;#34;,\&amp;#34;updated_at\&amp;#34;:\&amp;#34;2023-07-08T10:07:39-05:00\&amp;#34;,\&amp;#34;closed_at\&amp;#34;:null,\&amp;#34;due_date\&amp;#34;:null,\&amp;#34;pull_request\&amp;#34;:null,\&amp;#34;repository\&amp;#34;:{\&amp;#34;id\&amp;#34;:2098,\&amp;#34;name\&amp;#34;:\&amp;#34;prueba\&amp;#34;,\&amp;#34;owner\&amp;#34;:\&amp;#34;bit4bit\&amp;#34;,\&amp;#34;full_name\&amp;#34;:\&amp;#34;bit4bit/prueba\&amp;#34;}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 3 tests (2023-07-14 20:36:23-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000058 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data (0.000029 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000061 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-14 20:36:23-0500, 0.000264 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>aaa pero que paso?? porque pasaron las pruebas si cambio la firma de la función?? esto es algo que
suele ocurre cuando usamos dobles, por algo se le llama dobles no es el elemento de origen, esto puede aclarar un poco de porque se recomienda evitar los dobles sobre elementos que no sean implementados por el proyecto,&lt;/p>
&lt;p>por ejemplo &lt;code>url-request-method&lt;/code> tiene un ciclo de desarrollo diferente.&lt;/p>
&lt;p>ahora miremos un poco como podemos mitigar el efecto de los dobles&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">api-key&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; es una abstraccion sobre la comunicacion con gitea&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; pero aca tenemos un mecanismo de implementacion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; nuevamente una abstraccion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>antes que nada no debemos forzar los conceptos, si algo no cuadra es porque no va, revisando
nuevamente la función &lt;code>grieta--gitea-issue&lt;/code> se puede ver que ahí un brinco entre diferentes niveles de abstracción (&lt;code>grieta-gitea-call&lt;/code> &lt;code>json-parse-string&lt;/code>, &lt;code>gethash&lt;/code>),
vamos a llevar esto a un solo nivel.&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> (&lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">token&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">token&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>la mitigación se da aislando en lo posible la peticion HTTP &lt;code>grieta—url-auth-request&lt;/code>. Hemos aplanado los niveles de abstracción; por ejemplo &lt;code>grieta--gitea-issue&lt;/code> habla en términos de una petición
y la traducción a estructura interna, mientras que &lt;code>grieta--gitea-call&lt;/code> habla en términos de petición HTTP.
Como movimos las responsabilidades debemos actualizar los dobles usados en las pruebas.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; respuesta simplificada&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#b84">&amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">&amp;#39;&lt;/span>(&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#b84">&amp;#34;Test&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#008080">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span> &lt;span style="color:#998;font-style:italic">; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (wrong-number-of-arguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (lambda
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (host api-key method path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (let
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ((response ...))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (json-parse-string response)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 3)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000073 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 0 results as expected, 3 unexpected (2023-07-14 21:01:44-0500, 0.114529 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 unexpected results:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED grieta-test-grieta--gitea-issue-check-url-path
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Excelente ya se nos manifestó el error, procedamos a actualizar.&lt;/p>
&lt;p>&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> (&lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">token&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">token&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; TODO: api-key?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">nil&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-issue-check-url-path condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (mock-error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (grieta--url-auth-request &amp;#34;gitea.test.org&amp;#34; &amp;#34;GET&amp;#34; &amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (grieta--url-auth-request &amp;#34;GET&amp;#34; nil &amp;#34;https://gitea.test.org/api/v1/repos/demo/grieta/issues/99&amp;#34;))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000096 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data (0.000044 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000082 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 2 results as expected, 1 unexpected (2023-07-14 21:11:49-0500, 0.040699 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>excelente nos estamos ilustrando.&lt;/p>
&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; respuesta simplificada&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#b84">&amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">&amp;#39;&lt;/span>(&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#b84">&amp;#34;Test&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#008080">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span> &lt;span style="color:#998;font-style:italic">; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;token&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;https://gitea.test.org/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Test grieta-test-grieta--gitea-issue-check-url-path condition:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (mock-error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (grieta--url-auth-request &amp;#34;GET&amp;#34; &amp;#34;token&amp;#34; &amp;#34;/api/v1/repos/demo/grieta/issues/99&amp;#34;)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (grieta--url-auth-request &amp;#34;GET&amp;#34; nil &amp;#34;https://gitea.test.org/api/v1/repos/demo/grieta/issues/99&amp;#34;))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FAILED 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000098 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data (0.000043 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000080 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 2 results as expected, 1 unexpected (2023-07-14 21:14:39-0500, 0.041251 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>las pruebas nos esta ilustrando, diciéndonos que piensa hacer con el token?
nuevamente tenemos varios caminos&lt;/p>
&lt;ol>
&lt;li>crear otra función ejemplo &lt;code>grieta--gitea-auth-issue&lt;/code>&lt;/li>
&lt;li>usar una variable global o función que nos entregue el api-key.&lt;/li>
&lt;li>adicionar otro parámetro a la función &lt;code>grieta--gitea-issue&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>ya conocemos las implicaciones de las dos primeras, vayámonos directamente
por la tercera.&lt;/p>
&lt;p>&lt;code>invito hacer el ejerció de llegar al estado presentado desde al archivo anteriormente expuesto a este, haciendo cambios que no dejen fallar las pruebas&lt;/code>
&lt;strong>grieta.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> (&lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">token&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">url-request-method&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-data&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-request-extra-headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#458;font-weight:bold">cons&lt;/span> &lt;span style="color:#b84">&amp;#34;Authorization&amp;#34;&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;token &amp;#34;&lt;/span> &lt;span style="color:#008080">token&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-current-buffer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">url-retrieve-synchronously&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">goto-char&lt;/span> (&lt;span style="color:#008080">point-min&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">search-forward-regexp&lt;/span> &lt;span style="color:#b84">&amp;#34;\n\n&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">buffer-substring&lt;/span> (&lt;span style="color:#008080">point&lt;/span>) (&lt;span style="color:#008080">point-max&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-call&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">path&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let&lt;/span> ((&lt;span style="color:#008080">response&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#458;font-weight:bold">method&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> (&lt;span style="color:#008080">concat&lt;/span> &lt;span style="color:#b84">&amp;#34;https://&amp;#34;&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">path&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">json-parse-string&lt;/span> &lt;span style="color:#008080">response&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#999">defun&lt;/span> &lt;span style="color:#008080">grieta--gitea-issue&lt;/span> (&lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; TODO: api-key?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">let*&lt;/span> ((&lt;span style="color:#008080">remote-issue&lt;/span> (&lt;span style="color:#008080">grieta--gitea-call&lt;/span> &lt;span style="color:#008080">host&lt;/span> &lt;span style="color:#008080">api-key&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> (&lt;span style="color:#900;font-weight:bold">format&lt;/span> &lt;span style="color:#b84">&amp;#34;/api/v1/repos/%s/%s/issues/%d&amp;#34;&lt;/span> &lt;span style="color:#008080">owner&lt;/span> &lt;span style="color:#008080">repo&lt;/span> &lt;span style="color:#008080">issue-id&lt;/span>))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="font-weight:bold">if&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#458;font-weight:bold">list&lt;/span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;number&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#900;font-weight:bold">gethash&lt;/span> &lt;span style="color:#b84">&amp;#34;title&amp;#34;&lt;/span> &lt;span style="color:#008080">remote-issue&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; https://www.gnu.org/software/emacs/manual/html_node/elisp/Named-Features.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; `provide` es necesario para luego importar con `require`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>tests/grieta-tests.el&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-lisp" data-lang="lisp">&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;el-mock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">require&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; UNIT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; INTEGRATION&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; respuesta simplificada&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#b84">&amp;#34;{\&amp;#34;id\&amp;#34;: 1000, \&amp;#34;number\&amp;#34;: 1, \&amp;#34;title\&amp;#34;: \&amp;#34;Test\&amp;#34;}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">&amp;#39;&lt;/span>(&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#b84">&amp;#34;Test&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;token&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">stub&lt;/span> &lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#008080">=&amp;gt;&lt;/span> &lt;span style="color:#008080">nil&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">should&lt;/span> (&lt;span style="color:#900;font-weight:bold">equal&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">nil&lt;/span> &lt;span style="color:#998;font-style:italic">; volveremos a revisar luego si esta valor nos lleva a tener que usar condicionales&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;token&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">1&lt;/span>)))))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#008080">ert-deftest&lt;/span> &lt;span style="color:#008080">grieta-test-grieta--gitea-issue-check-url-path&lt;/span> ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">with-mock&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">;; https://try.gitea.io/api/swagger#/issue/issueGetIssue&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">mock&lt;/span> (&lt;span style="color:#008080">grieta--url-auth-request&lt;/span> &lt;span style="color:#b84">&amp;#34;GET&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;token&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;https://gitea.test.org/api/v1/repos/demo/grieta/issues/99&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#008080">grieta--gitea-issue&lt;/span> &lt;span style="color:#b84">&amp;#34;gitea.test.org&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;token&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;demo&amp;#34;&lt;/span> &lt;span style="color:#b84">&amp;#34;grieta&amp;#34;&lt;/span> &lt;span style="color:#099">99&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; ACCEPTANCE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic">;; MANUAL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(&lt;span style="color:#900;font-weight:bold">provide&lt;/span> &lt;span style="color:#b84">&amp;#39;grieta-tests&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>make &lt;span style="color:#999">test&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Running 3 tests (2023-07-14 21:19:48-0500, selector ‘t’)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 1/3 grieta-test-grieta--gitea-issue-check-url-path (0.000064 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 2/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-invalid-data (0.000033 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> passed 3/3 grieta-test-grieta--gitea-json-&amp;gt;issues-on-valid-data (0.000069 sec)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ran 3 tests, 3 results as expected, 0 unexpected (2023-07-14 21:19:48-0500, 0.000305 sec)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>boom, nuevamente estables, el proceso que realizamos de:&lt;/p>
&lt;ol>
&lt;li>especular&lt;/li>
&lt;li>ilustrar&lt;/li>
&lt;li>ajustar (+ importante no olvidar limpiar y aclarar lo escrito)&lt;/li>
&lt;/ol>
&lt;p>se realiza rápidamente en cuestión de segundos.&lt;/p></description></item><item><title>Freeswitch Integración - event2dial</title><link>http://bit4bit.github.io/post/freeswitch_integration_event2dial/</link><pubDate>Sat, 01 Jul 2023 16:15:01 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/freeswitch_integration_event2dial/</guid><description>&lt;p>Una de las funcionalidades más prácticas o almenos más usadas por empresas
en la gestión de sus usuarios es un mecanimo que conecte al usuario
con un agente ante un evento, los eventos más comunes son:&lt;/p>
&lt;ul>
&lt;li>el usuario solicita que lo contacten (ejemplo al diligenciar un formulario)&lt;/li>
&lt;li>un agente solicita contactar con otro usuario sin conocer el número destino (ejemplo gestión en un CRM).&lt;/li>
&lt;li>contactar a usuario ante una campaña de mercadeo.&lt;/li>
&lt;/ul>
&lt;p>Para FreeSWITCH tenemos muy poca documentación de como realizar este tipo de
integraciones así que vamos a realizar una implementación de este tipo de servicio (event2dial)
desde FreeSWITCH.&lt;/p>
&lt;p>Nuestro escenario sería:&lt;/p>
&lt;ul>
&lt;li>Como consumidor debo estar en la capacidad de solicitar que me contacten lo más antes posible.&lt;/li>
&lt;/ul>
&lt;p>Para satisfacer el escenario anterio en un ambiente web, llegamos al acuerdo que el usuario
pueda diligenciar un formulario para ser contactado.&lt;/p>
&lt;p>FreeSWITCH es un softswitch extenso el cual ofrece diferentes medios para realizar
integraciónes, donde por nombrar algunos:&lt;/p>
&lt;ul>
&lt;li>por medio de &lt;strong>mod_event_socket&lt;/strong>&lt;/li>
&lt;li>por medio de lenguage integrados como: &lt;strong>mod_lua&lt;/strong>, &lt;strong>mod_python&lt;/strong>, &lt;strong>mod_perl&lt;/strong>&lt;/li>
&lt;li>por medio de &lt;strong>mod_xml_curl&lt;/strong>&lt;/li>
&lt;li>por medio de &lt;strong>mod_xml_rpc&lt;/strong>&lt;/li>
&lt;li>por medio de &lt;strong>mod_httapi&lt;/strong>&lt;/li>
&lt;li>o bien una mezcla de todo&lt;/li>
&lt;/ul>
&lt;p>para esta implementación usaremos el modulo &lt;strong>mod_xml_rpc&lt;/strong> el cual nos ofrece un &lt;a href="https://en.wikipedia.org/wiki/Remote_procedure_call">&lt;strong>rpc&lt;/strong>&lt;/a>
para comandar el FreeSWITCH siendo la ejecución de forma sincronica es decir al comandar esperaremos por el resultado,
para unas decenas de llamadas por minuto esto es suficiente pero para superar estos limites necesitariamos otra
estrategia diferente.&lt;/p>
&lt;p>importante antes de empezar:&lt;/p>
&lt;ul>
&lt;li>conocer como instalar y operar un FreeSWITCH&lt;/li>
&lt;li>ejecutable &lt;strong>freeswitch 1.10.9&lt;/strong> con los modulos &lt;strong>mod_xml_rpc&lt;/strong> y &lt;strong>mod_sofia&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>freeswitch.xml&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999;font-weight:bold">&amp;lt;?xml version=&amp;#34;1.0&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;document&lt;/span> &lt;span style="color:#008080">type=&lt;/span>&lt;span style="color:#b84">&amp;#34;freeswitch/xml&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;section&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;configuration&amp;#34;&lt;/span> &lt;span style="color:#008080">description=&lt;/span>&lt;span style="color:#b84">&amp;#34;Various Configuration&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;configuration&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;modules.conf&amp;#34;&lt;/span> &lt;span style="color:#008080">description=&lt;/span>&lt;span style="color:#b84">&amp;#34;Modules&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;modules&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;load&lt;/span> &lt;span style="color:#008080">module=&lt;/span>&lt;span style="color:#b84">&amp;#34;mod_console&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;load&lt;/span> &lt;span style="color:#008080">module=&lt;/span>&lt;span style="color:#b84">&amp;#34;mod_commands&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;load&lt;/span> &lt;span style="color:#008080">module=&lt;/span>&lt;span style="color:#b84">&amp;#34;mod_dptools&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- habilitamos el modulo de nuestro interes --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;load&lt;/span> &lt;span style="color:#008080">module=&lt;/span>&lt;span style="color:#b84">&amp;#34;mod_xml_rpc&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/modules&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- resto de configuración --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- configuracion minima para xml_rpc.conf OJO actualmente escucha en todas la interfaz --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;configuration&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;xml_rpc.conf&amp;#34;&lt;/span> &lt;span style="color:#008080">description=&lt;/span>&lt;span style="color:#b84">&amp;#34;XML RPC&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;settings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;param&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;http-port&amp;#34;&lt;/span> &lt;span style="color:#008080">value=&lt;/span>&lt;span style="color:#b84">&amp;#34;8080&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/settings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/section&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;section&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;dialplan&amp;#34;&lt;/span> &lt;span style="color:#008080">description=&lt;/span>&lt;span style="color:#b84">&amp;#34;Regex/XML Dialplan&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- cuando contactemos el usuario con el `originate` el FreeSWITCH
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> ejecutará el siguiente plan de marcado para controlar su flujo
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> áca podemos indicar acciones como:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> - conectar con un usuario/extensión
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> - conectar con una linea de tierra o mobil
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> - reproducir un audio
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> - mover la llamada a una queue
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> - iniciar un IVR
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"> --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;context&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;event2dial&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- acciones a realizar una vez se logro contactar al usuario --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;extension&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;event2dial&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;condition&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic">&amp;lt;!-- todas las llamadas son contestadas por el agente 1000 --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;set&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;hangup_after_bridge=true&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;bridge&amp;#34;&lt;/span> &lt;span style="color:#008080">data=&lt;/span>&lt;span style="color:#b84">&amp;#34;user/1000&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;action&lt;/span> &lt;span style="color:#008080">application=&lt;/span>&lt;span style="color:#b84">&amp;#34;hangup&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/condition&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/extension&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/context&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/section&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/document&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;!--
```sh @mdtoapp @execute
freeswitch -nonat -lp -conf . -log . -db . -run . -temp . &amp;
sleep 3s
```
-->
&lt;p>primero confirmamos actividad del puerto, en caso de no obtener
respuesta revisar configuración de FreeSWITCH.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>curl -sSG -XGET &lt;span style="color:#b84">&amp;#39;http://localhost:8080/xmlapi/uptime&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"># 2&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ahora, asumamos que tenemos configurada la pasarela &lt;code>celulares&lt;/code> que nos
da salida a llamadas a celulares en colombia, entonces
para lanzar la llamada al numero 3001234567 seria:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>curl -sSG -XGET &lt;span style="color:#b84">&amp;#39;http://localhost:8080/api/originate?&amp;#34;%7Boriginate_timeout%3D30%2Corigination_uuid%3DMYUUID%2Corigination_caller_id_name%3D123%2Corigination_caller_id_number%3D123%7Dsofia%2Fcelulares%2F3001234567%20event2dial%20XML%20event2dial%29&amp;#34;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>aclaraciónes:&lt;/p>
&lt;ul>
&lt;li>se debe codificar por partes los argumentos del comando se separan con &lt;code>%20&lt;/code> y cada argumento se codifica con url, ejemplo de codificacion en ruby seria:
&lt;ul>
&lt;li>&lt;code>['{originate_timeout=30}sofia/gateway/celulares/3001234576', 'event2dial', 'XML', 'event2dial'].map { |v| URI.encode_www_form_component(v) }.join('%20')&lt;/code>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;code>originate_timeout&lt;/code> = 30, esperamos almenos 30 segundos que contesten o colgamos (este comportamiento no aplica para destinos con buzón de voz).&lt;/li>
&lt;li>&lt;code>origination_uuid&lt;/code> = UUIDv4, asignamos identificador a la llamada.&lt;/li>
&lt;li>&lt;code>origination_caller_id_name&lt;/code> = 123, dependiendo de la linea destino podemos enviar un nombre a ser visualizado en el telefono.&lt;/li>
&lt;li>&lt;code>origination_caller_id_number&lt;/code> = 123, dependiendo de la linea destino podemos enviar el numero de origen.&lt;/li>
&lt;/ul>
&lt;p>estados de retorno:&lt;/p>
&lt;p>&lt;code>+OK &amp;lt;uuidv4&amp;gt;&lt;/code> indicando que fue exitoso el lanzamiento de la llamada&lt;/p>
&lt;p>&lt;code>-ERR &amp;lt;cause&amp;gt;&lt;/code> indica el motivo por el cual no se puedo lanzar la llamada&lt;/p>
&lt;p>Resumiendo:&lt;/p>
&lt;ul>
&lt;li>requerimos un FreeSWITCH previamente configurado con una o varias pasarelas a que nos den salida a los destinos requeridos&lt;/li>
&lt;li>activar y configurar &lt;strong>mod_xml_rpc&lt;/strong>&lt;/li>
&lt;li>comandar llamada con &lt;strong>originate&lt;/strong> por medio de HTTP.&lt;/li>
&lt;/ul>
&lt;p>Con lo anterior tenemos todas las herramientas para integrar FreeSWITCH en los flujos de negoció.&lt;/p>
&lt;!--
detener freeswitch
```sh @mdtoapp @execute
freeswitch -conf . -stop -run .
```
--></description></item><item><title>Kamailio as SBC for MS Teams</title><link>http://bit4bit.github.io/post/kamailio_sbc_msteams/</link><pubDate>Sun, 25 Jun 2023 09:11:44 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/kamailio_sbc_msteams/</guid><description>&lt;p>Este post complementa con un ejemplo mínimo pero funcional de lo expuesto en &lt;a href="https://skalatan.de/en/blog/kamailio-sbc-teams">Kamailio as SBC for MS Teams&lt;/a>.&lt;/p>
&lt;p>Escenario confirmado.&lt;/p>
&lt;div class="mermaid">flowchart LR
MSTeams["MS Teams
v.2023.6.21.5
"]
Kamailio["Kamailio
v5.4.4
RTPEngine 11.3.1.4
"]
PBX["FreeSWITCH
v1.10.9
"]
MSTeams ---|TLS/PUBLIC IP| Kamailio
Kamailio ---|UDP/PUBLIC IP| PBX
&lt;/div>
&lt;p>Funcionalidades confirmadas:&lt;/p>
&lt;ul>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Llamadas MSTeams a PBX&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> Llamadas PBX a MSTeams&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Hold&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Park&lt;/li>
&lt;li>&lt;input checked="" disabled="" type="checkbox"> MSTeams Unpark&lt;/li>
&lt;/ul>
&lt;p>Para que esta configuracion sea efectiva debes conectar &lt;a href="https://skalatan.de/en/blog/kamailio-sbc-teams">Kamailio a MS Teams&lt;/a> y
una vez confirmes que MS Teams detecta el Kamailio puedes proceder a usar la configuracion entregada.&lt;/p>
&lt;p>&lt;strong>kamailio/kamailio.cfg&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-kamailio" data-lang="kamailio">#!KAMAILIO
#
# Asesorias y Consultorias a contacto@bit4bit.in
# ACTUALIZAR LOS VALORES A CONTINUACION
#!substdef &amp;#34;!SIPS_PORT!7062!g&amp;#34;
#!substdef &amp;#34;!SIP_PORT!5073!g&amp;#34;
#!substdef &amp;#34;!PBX_IP!3.3.3.3!g&amp;#34;
#!substdef &amp;#34;!PBX_SIP_PORT!5060!g&amp;#34;
#!substdef &amp;#34;!EXTERNAL_IP!2.2.2.2!g&amp;#34;
#!substdef &amp;#34;!EXTERNAL_FQDN!demo.bit4bit.in!g&amp;#34;
debug=2
log_stderror=no
log_facility=LOG_LOCAL0
log_prefix=&amp;#34;{$mt $hdr(CSeq) $ci} &amp;#34;
children=8
alias=EXTERNAL_FQDN:SIPS_PORT
alias=EXTERNAL_FQDN:SIP_PORT
listen=tls:EXTERNAL_IP:SIPS_PORT advertise EXTERNAL_FQDN:SIPS_PORT
listen=udp:EXTERNAL_IP:SIP_PORT advertise EXTERNAL_FQDN:SIP_PORT
enable_tls=yes
tls_max_connections=2048
enable_sctp=no
loadmodule &amp;#34;kex.so&amp;#34;
loadmodule &amp;#34;corex.so&amp;#34;
loadmodule &amp;#34;tm.so&amp;#34;
loadmodule &amp;#34;tmx.so&amp;#34;
loadmodule &amp;#34;sl.so&amp;#34;
loadmodule &amp;#34;rr.so&amp;#34;
loadmodule &amp;#34;pv.so&amp;#34;
loadmodule &amp;#34;maxfwd.so&amp;#34;
loadmodule &amp;#34;textops.so&amp;#34;
loadmodule &amp;#34;textopsx.so&amp;#34;
loadmodule &amp;#34;siputils.so&amp;#34;
loadmodule &amp;#34;xlog.so&amp;#34;
loadmodule &amp;#34;ctl.so&amp;#34;
loadmodule &amp;#34;dispatcher.so&amp;#34;
loadmodule &amp;#34;rtpengine.so&amp;#34;
loadmodule &amp;#34;nathelper.so&amp;#34;
loadmodule &amp;#34;tls.so&amp;#34;
modparam(&amp;#34;rtpengine&amp;#34;, &amp;#34;rtpengine_sock&amp;#34;, &amp;#34;udp:127.0.0.1:2223&amp;#34;)
modparam(&amp;#34;tls&amp;#34;, &amp;#34;config&amp;#34;, &amp;#34;/etc/kamailio/tls.cfg&amp;#34;)
modparam(&amp;#34;dispatcher&amp;#34;, &amp;#34;list_file&amp;#34;, &amp;#34;/etc/kamailio/dispatcher.list&amp;#34;)
modparam(&amp;#34;dispatcher&amp;#34;, &amp;#34;ds_probing_mode&amp;#34;, 1)
modparam(&amp;#34;dispatcher&amp;#34;, &amp;#34;ds_ping_interval&amp;#34;, 10)
####### Routing Logic ########
request_route {
if (!mf_process_maxfwd_header(&amp;#34;10&amp;#34;)) {
sl_send_reply(&amp;#34;483&amp;#34;,&amp;#34;Too Many Hops&amp;#34;);
exit;
}
if(is_method(&amp;#34;OPTIONS&amp;#34;) &amp;amp;&amp;amp; uri==myself &amp;amp;&amp;amp; $rU==$null) {
sl_send_reply(&amp;#34;200&amp;#34;,&amp;#34;Keepalive&amp;#34;);
exit;
}
# FROM MSTEAMS
if($hdr(User-Agent) =~ &amp;#34;Microsoft.PSTNHub&amp;#34;) {
$du = &amp;#34;sip:PBX_IP:PBX_SIP_PORT;transport=udp&amp;#34;;
}
# TO MSTEAMS
else {
$du = &amp;#34;sip:sip.pstnhub.microsoft.com;transport=tls&amp;#34;;
}
if ($rU==$null) {
sl_send_reply(&amp;#34;484&amp;#34;,&amp;#34;Address Incomplete&amp;#34;);
exit;
}
record_route();
route(RELAY);
}
route[RELAY] {
route(REFER);
if (is_method(&amp;#34;INVITE|BYE|CANCEL&amp;#34;)) {
if(!t_is_set(&amp;#34;branch_route&amp;#34;)) t_on_branch(&amp;#34;MANAGE_BRANCH&amp;#34;);
}
if (is_method(&amp;#34;INVITE&amp;#34;)) {
if(!t_is_set(&amp;#34;onreply_route&amp;#34;)) t_on_reply(&amp;#34;MANAGE_REPLY&amp;#34;);
}
if (!t_relay()) {
sl_reply_error();
}
exit;
}
route[RTPMANAGE] {
if (!has_body(&amp;#34;application/sdp&amp;#34;)) {
return;
}
if($dd =~ &amp;#34;sip.pstnhub.microsoft.com&amp;#34;) {
rtpengine_offer(&amp;#34;trust-address replace-origin replace-session-connection ICE=force transcode-PCMU transcode-G722 RTP/SAVP&amp;#34;);
} else {
rtpengine_answer(&amp;#34;trust-address replace-origin replace-session-connection ICE=remove RTP/AVP&amp;#34;);
}
return;
}
route[RTPREPLY] {
if (!has_body(&amp;#34;application/sdp&amp;#34;)) {
return;
}
if ($fd == &amp;#34;PBX_IP&amp;#34;) {
rtpengine_offer(&amp;#34;trust-address replace-origin replace-session-connection ICE=remove RTP/AVP&amp;#34;);
} else {
rtpengine_answer(&amp;#34;trust-address replace-origin replace-session-connection ICE=force transcode-PCMU transcode-G722 RTP/SAVP&amp;#34;);
}
return;
}
route[REFER] {
exclude_hf_value(&amp;#34;Allow&amp;#34;, &amp;#34;REFER&amp;#34;);
return;
}
branch_route[MANAGE_BRANCH] {
route(RTPMANAGE);
}
onreply_route[MANAGE_REPLY] {
route(REFER);
if(status=~&amp;#34;[12][0-9][0-9]&amp;#34;) {
route(RTPREPLY);
}
}
event_route[tm:local-request] {
if (is_method(&amp;#34;OPTIONS&amp;#34;) &amp;amp;&amp;amp; $ru =~ &amp;#34;pstnhub.microsoft.com&amp;#34;) {
append_hf(&amp;#34;Contact: &amp;lt;sip:$fd:SIPS_PORT;transport=tls&amp;gt;\r\n&amp;#34;);
}
}
&lt;/code>&lt;/pre>&lt;p>&lt;strong>kamailio/tls.cfg&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-kamailio" data-lang="kamailio">[server:default]
method = TLSv1.2+
verify_certificate = no
require_certificate = no
private_key = HERE YOUR PRIVATE KEY
certificate = HERE YOUR CERTIFICATE
[client:default]
method = TLSv1.2+
verify_certificate = no
require_certificate = no
private_key = HERE YOUR PRIVATE KEY
certificate = HERE YOUR CERTIFICATE
&lt;/code>&lt;/pre>&lt;p>&lt;strong>kamailio/dispatcher.list&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-csv" data-lang="csv"># setid(integer) destination(sip uri) flags (integer, optional), priority(int,opt), attrs (str,optional)
1 sip:sip.pstnhub.microsoft.com;transport=tls 0 3 socket=tls:HERE EXTERNAL IP:HERE SIPS PORT;ping_from=sip:HERE EXTERNAL FQDN
1 sip:sip2.pstnhub.microsoft.com;transport=tls 0 2 socket=tls:HERE EXTERNAL IP:HERE SIPS PORT;ping_from=sip:HERE EXTERNAL FQDN
1 sip:sip3.pstnhub.microsoft.com;transport=tls 0 1 socket=tls:HERE EXTERNAL IP:HERE SIPS PORT;ping_from=sip:HERE EXTERNAL FQDN
&lt;/code>&lt;/pre>&lt;!--
```sh @mdtoapp @execute
/usr/sbin/kamailio -d 0 -D -c -f kamailio/kamailio.cfg
```
-->
&lt;p>Para escenarios más complejos o funcionalidades no presentes puedes contactarme a &lt;a href="mailto:contacto@bit4bit.in">contacto@bit4bit.in&lt;/a>.&lt;/p>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://skalatan.de/en/blog/kamailio-sbc-teams">Kamailio as SBC for MS Teams&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://github.com/dOpensource/dsiprouter/blob/master/kamailio/configs/kamailio.cfg">DSIPRouter kamailio.cfg&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Escribiendo un Módulo de Tryton ERP Básico 2</title><link>http://bit4bit.github.io/post/writing_module_trytond_basics_2/</link><pubDate>Thu, 22 Jun 2023 16:40:04 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/writing_module_trytond_basics_2/</guid><description>&lt;p>Ahora el siguiente paso para personalizar trytond a nivel de código es la creación de modelos,
para una solución efectiva al problema vamos a dar prioridad a la comunicación entre modelos
para expresar la solución, es decir la solución no esta en los modelos en si sino en la conversación entre estos,
lo anterior es importante ternelo presente cuando modelamos ya que trytond
sigue el patron &lt;a href="https://martinfowler.com/eaaCatalog/activeRecord.html">Active Record&lt;/a> el
cual nos invita a adherir la solución (comportamiento) a las carácteristicas (attributos)
y apoyados &lt;a href="https://www.martinfowler.com/bliki/TellDontAsk.html">en indicar que hacer en vez de preguntar que hacer&lt;/a>
podemos hacer más flexible el modelado antes los cambios, ahora
cuando hablo de conversación entre modelos técnicamente seria la llamada
de los métodos de los objetos representativos de los modelos, veamos un ejercicio un poco
más complejo que el presentado en &lt;strong>Básico 1&lt;/strong>.&lt;/p>
&lt;p>Digamos que nuestro cliente vende minutos de telefonía y mensajes de texto SMS,
actualmente se encuentra con el inconveniente que no tiene una plataforma para llevar
control de la facturación y una vez conversado sobre el asunto
se llega a la conclusión de que:&lt;/p>
&lt;ul>
&lt;li>Tryton ERP es el software adecuado&lt;/li>
&lt;li>se debe vincular Tryton ERP a la plataforma de telefonía para llevar el control.&lt;/li>
&lt;/ul>
&lt;p>En la siguiente conversación con el equipo de telefonía, nos comentan
que diariamente a las 00:00-05 suben el archivo a un servidor y nos podrían
dar acceso por medio de HTTP para descargar el archivo de nombre formateado &lt;code>yyyy-mm-dd.csv&lt;/code>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-csv" data-lang="csv">client_code,billsec,sms,day
AB123,1000,1,2023-06-22
CP535,2000,100,2023-06-22
&lt;/code>&lt;/pre>&lt;p>Con la información anterior se plantean los siguientes escenarios con el cliente
para coordinar la entregar del proyecto:&lt;/p>
&lt;ul>
&lt;li>Como administrador debo ser capaz de asociar tercero a un cliente de telefonía.&lt;/li>
&lt;li>Diariamente conciliar los saldos entregados por telefonía.&lt;/li>
&lt;/ul>
&lt;h3 id="creamos-el-modulo">creamos el modulo&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pip3 install &lt;span style="color:#008080">trytond&lt;/span>&lt;span style="font-weight:bold">==&lt;/span>6.8.0 --user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pip3 install proteus --user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cookiecutter git+https://gitlab.com/bit4bit/cookiecutter-tryton-module.git --directory template --no-input &lt;span style="color:#008080">module_name&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>charging_tel &lt;span style="color:#008080">version&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>6.8.0 &lt;span style="color:#008080">test_with_scenario&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>charging_tel.rst
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">cd&lt;/span> charging_tel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>python3 setup.py develop --user
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>confirmamos que tengamos operatividad&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">cd&lt;/span> charging_tel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 23 tests in 4.381s
OK
&lt;/pre>
&lt;h4 id="escenario-como-administrador-debo-ser-capaz-de-asociar-party-a-un-cliente-de-telefonía">Escenario: Como administrador debo ser capaz de asociar Party a un cliente de telefonía.&lt;/h4>
&lt;p>&lt;strong>tests/scenario_administrator_create_party_with_client_code.rst&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rst" data-lang="rst">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">Administrador Crear Party con campo client_code Scenario&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span>Dependencias&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from proteus import Model, Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from trytond.tests.tools import activate_modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Activacion de modulos&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; config = activate_modules(&amp;#39;charging_tel&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Party con client_code::&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; Party = Model.get(&amp;#39;party.party&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; party = Party()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; party.client_code = &amp;#39;AB123&amp;#39; # valor real uno de los entregados&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; party.save()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; party.client_code&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;#39;AB123&amp;#39;&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
Party = Model.get('party.party')
...
return self._pool[self.database_name][type][name]
KeyError: 'party.party'
&lt;/pre>
&lt;p>&lt;code>KeyError: 'party.party'&lt;/code> ya conocemos que debemos hacer&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pip3 install trytond-party&lt;span style="font-weight:bold">==&lt;/span>6.8.0 --user
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>tryton.cfg&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-config" data-lang="config">[tryton]
version=6.8.0
depends:
ir
party
xml:
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
party.client_code = 'AB123' # valor real uno de los entregados
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.9/doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "&lt;doctest scenario_party_with_client_code.rst[5]>", line 1, in &lt;module>
party.client_code = 'AB123' # valor real uno de los entregados
AttributeError: 'party.party' object has no attribute 'client_code'
&lt;/pre>
&lt;p>&lt;code>AttributeError: 'party.party'&lt;/code> ya conocemos que debemos hacer&lt;/p>
&lt;p>&lt;strong>party.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party&lt;/span>(metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># campo para relacionar party con el cliente de telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, required&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">False&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Le indicamos a trytond el modelo que hemos extendido&lt;/p>
&lt;p>&lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> party
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="color:#b84">&amp;#39;register&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">register&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Pool&lt;span style="font-weight:bold">.&lt;/span>register(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>Party,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>, type_&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;model&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 24 tests in 8.997s
OK
&lt;/pre>
&lt;p>por ahora dejare por fuera la gestión de permisos por usuario, ya
que los usuarios no administradores no deberían ser capaces de cambiar el campo
&lt;code>client_code&lt;/code>&lt;/p>
&lt;h2 id="ahora-pasamos-a-verificar-que-el-client_code-sea-en-el-formato-esperado">ahora pasamos a verificar que el client_code sea en el formato esperado&lt;/h2>
&lt;p>&lt;strong>tests/test_party_client_code.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party_client_code_TestCase&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Party client_code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_invalida_hay_error&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TEL&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;123546&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;3515ABC&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;_535&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;A535.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_valida_no_hay_error&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TELO&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;AB123&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Traceback (most recent call last):
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
result = func(*args, **kwargs)
File "/tmp/d20230622-6588-e3ry82/charging_tel/tests/test_party_client_code.py", line 18, in test_para_entrada_invalida_UserError
party.save()
AssertionError: ValidationError not raised
&lt;/pre>
&lt;p>&lt;code>AssertionError: ValidationError not raised&lt;/code> ya conocemos como proceder&lt;/p>
&lt;p>&lt;strong>party.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">re&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.i18n&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> gettext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party&lt;/span>(metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># relacionamos party con cliente de telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, required&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">False&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">validate_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>validate_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>check_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">check_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> field_names &lt;span style="font-weight:bold">and&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> (field_names &lt;span style="font-weight:bold">&amp;amp;&lt;/span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>}):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">for&lt;/span> record &lt;span style="font-weight:bold">in&lt;/span> records:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>is_allowed_client_code(record&lt;span style="font-weight:bold">.&lt;/span>client_code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">raise&lt;/span> ValidationError(gettext(&lt;span style="color:#b84">&amp;#39;charging_tel.client_code_invalid&amp;#39;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">is_allowed_client_code&lt;/span>(cls, code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># opcional&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> code &lt;span style="font-weight:bold">is&lt;/span> &lt;span style="font-weight:bold">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> &lt;span style="font-weight:bold">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># formato entregado por area de telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> &lt;span style="color:#999">bool&lt;/span>(re&lt;span style="font-weight:bold">.&lt;/span>&lt;span style="font-weight:bold">match&lt;/span>(&lt;span style="color:#b84">r&lt;/span>&lt;span style="color:#b84">&amp;#34;^[A-Z]{2,10}[0-9]{2,8}$&amp;#34;&lt;/span>, code))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 47 tests in 11.937s
OK
&lt;/pre>
&lt;p>a este punto nos preguntamos si &lt;code>client_code&lt;/code> debe ser único por cliente,
lo cual es luego confirmado por el equipo técnico de telefonía,
entonces procedemos confirmar esta restricción.&lt;/p>
&lt;p>&lt;strong>tests/test_party_client_code.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party_client_code_TestCase&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Party client_code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_valida_debe_ser_unico&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _, party &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TEL A&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;AB123&amp;#39;&lt;/span>},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TEL B&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(&lt;span style="color:#900;font-weight:bold">Exception&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;AB123&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_invalida_hay_error&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TEL&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;123546&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;3515ABC&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;_535&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;A535.&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_valida_no_hay_error&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;TELO&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;AB123&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>client_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Traceback (most recent call last):
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
result = func(*args, **kwargs)
File "/tmp/d20230622-7701-1wybgva/charging_tel/tests/test_party_client_code.py", line 99, in test_para_entrada_valida_debe_ser_unico
party.save()
AssertionError: Exception not raised
&lt;/pre>
&lt;p>aja ya sabemos que hacer ;)&lt;/p>
&lt;p>&lt;strong>party.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">re&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> (Unique, fields)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.i18n&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> gettext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party&lt;/span>(metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># relacionamos party con cliente de telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, required&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">False&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">__setup__&lt;/span>(cls):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>__setup__()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># ver https://docs.tryton.org/projects/server/en/latest/ref/models.html#trytond.model.ModelSQL._sql_constraints&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>__table__()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># hacemos efectiva la restriccion desde base de datos&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>_sql_constraints &lt;span style="font-weight:bold">+=&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#b84">&amp;#39;client_code_unique&amp;#39;&lt;/span>, Unique(t, t&lt;span style="font-weight:bold">.&lt;/span>client_code),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;charging_tel.msg_client_code_unique&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">validate_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>validate_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>check_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">check_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> field_names &lt;span style="font-weight:bold">and&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> (field_names &lt;span style="font-weight:bold">&amp;amp;&lt;/span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>}):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">for&lt;/span> record &lt;span style="font-weight:bold">in&lt;/span> records:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>is_allowed_client_code(record&lt;span style="font-weight:bold">.&lt;/span>client_code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">raise&lt;/span> ValidationError(gettext(&lt;span style="color:#b84">&amp;#39;charging_tel.client_code_invalid&amp;#39;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">is_allowed_client_code&lt;/span>(cls, code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># opcional&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> code &lt;span style="font-weight:bold">is&lt;/span> &lt;span style="font-weight:bold">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> &lt;span style="font-weight:bold">True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># formato entregado por area de telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> &lt;span style="color:#999">bool&lt;/span>(re&lt;span style="font-weight:bold">.&lt;/span>&lt;span style="font-weight:bold">match&lt;/span>(&lt;span style="color:#b84">r&lt;/span>&lt;span style="color:#b84">&amp;#34;^[A-Z]{2,10}[0-9]{2,8}$&amp;#34;&lt;/span>, code))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 49 tests in 12.318s
OK
&lt;/pre>
&lt;h4 id="escenario-como-servicio-autónomo-concilio-diariamente-los-saldos-entregados-por-telefonía">Escenario: Como servicio autónomo concilio diariamente los saldos entregados por telefonía.&lt;/h4>
&lt;p>&lt;strong>tests/scenario_create_recharge_tel.rst&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rst" data-lang="rst">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">Crear Cargo de Telefonia&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span>Dependencias&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from proteus import Model, Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from trytond.tests.tools import activate_modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Activacion de modulos&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; config = activate_modules(&amp;#39;charging_tel&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Crear cargo de telefonia:&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; example_csv = open(&amp;#39;tests/tel_2023-06-23.csv&amp;#39;).read().encode()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; ChargeTel = Model.get(&amp;#39;charging_tel.charge_tel&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; ChargeTel.import_csv(example_csv)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; len(ChargeTel.find())&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> 2&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
example_csv = open('tests/tel_2023-06-23.csv').read().encode()
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.9/doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "&lt;doctest scenario_create_charge_tel.rst[3]>", line 1, in &lt;module>
example_csv = open('tests/tel_2023-06-23.csv').read().encode()
FileNotFoundError: [Errno 2] No such file or directory: 'tests/tel_2023-06-23.csv'
&lt;/pre>
&lt;p>Efectivamente nos informa que no tenemos el archivo de muestra, vamos a crearlo&lt;/p>
&lt;p>&lt;strong>tests/tel_2023-06-23.csv&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-csv" data-lang="csv">client_code,billsec,sms,day
ABC123,1000,1,2023-06-22
CP535,2000,100,2023-06-22
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
ChargeTel = Model.get('charging_tel.charge_tel')
...
return self._pool[self.database_name][type][name]
KeyError: 'charging_tel.charge_tel'
&lt;/pre>
&lt;p>Aja, ya vamos volviéndonos hábiles en esto, ya sabemos que debemos hacer y es crear el modelo,
para esto debemos indicarle a trytond que nuestro modelo va a gestionar &lt;a href="https://docs.tryton.org/projects/server/en/latest/tutorial/module/model.html">datos extendiendo la clase de &lt;code>ModelSQL&lt;/code>&lt;/a>
y que a su vez &lt;a href="https://docs.tryton.org/projects/server/en/latest/tutorial/module/view.html">va ser visible al usuario &lt;code>ModelView&lt;/code>&lt;/a> adicionalmente le indicamos
cuales son los &lt;a href="https://docs.tryton.org/projects/server/en/latest/ref/fields.html#field">campos&lt;/a> que vamos a gestionar.&lt;/p>
&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"># le indicamos a trytond que nuestra modelo va a gestionar datos `ModelSQL`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"># y va ha ser presentable desde la interfaz `ModelView`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> party
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> charge_tel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="color:#b84">&amp;#39;register&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">register&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Pool&lt;span style="font-weight:bold">.&lt;/span>register(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>Party,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charge_tel&lt;span style="font-weight:bold">.&lt;/span>ChargeTel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>, type_&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;model&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
ChargeTel.import_csv(example_csv)
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.9/doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "&lt;doctest scenario_create_charge_tel.rst[5]>", line 1, in &lt;module>
ChargeTel.import_csv(example_csv)
AttributeError: type object 'charging_tel.charge_tel' has no attribute 'import_csv'
&lt;/pre>
&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">pass&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Failed example:
ChargeTel.import_csv(example_csv)
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.9/doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "&lt;doctest scenario_create_charge_tel.rst[5]>", line 1, in &lt;module>
ChargeTel.import_csv(example_csv)
AttributeError: type object 'charging_tel.charge_tel' has no attribute 'import_csv'
&lt;/pre>
&lt;p>uuu nos indica que el modelo no tiene el atributo &lt;code>import_csv&lt;/code>, que en nuestro caso
esperamos sea el método de importación, podríamos hacer accesible este método a proteus usando el mecanismo &lt;strong>RPC&lt;/strong> de trytond pero en nuestro caso publicaríamos
este método solo con el objetivo de realizar la prueba, estas situaciones
es un indicio que el camino que hemos tomado no es el mas adecuado sea que
la prueba que planteamos es confusa o bien el mecanismo para probar no es el mas adecuado,
vamos a trasladar la prueba como prueba de unidad.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>rm -f tests/scenario_create_charge_tel.rst
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>tests/test_create_charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">CreateChargeTel&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Create Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_crear_cargos_desde_csv&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>import_csv(&lt;span style="color:#b84">&amp;#39;tests/tel_2023-06-23.csv&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charges &lt;span style="font-weight:bold">=&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>search([])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> got &lt;span style="font-weight:bold">=&lt;/span> [{&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>client_code,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>billsec,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>sms,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>day} &lt;span style="font-weight:bold">for&lt;/span> c &lt;span style="font-weight:bold">in&lt;/span> charges]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="font-weight:bold">.&lt;/span>assertListEqual([
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;ABC123&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">1000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">2000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">100&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ], got)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Traceback (most recent call last):
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
result = func(*args, **kwargs)
File "/tmp/d20230623-7356-1tmqcx4/charging_tel/tests/test_create_charge_tel.py", line 16, in test_crear_cargos_desde_csv
self.assertEqual(len(charges), 2)
AssertionError: 0 != 2
----------------------------------------------------------------------
Ran 72 tests in 15.397s
FAILED (failures=1, errors=1)
&lt;/pre>
&lt;p>excelente ya nos reconoció el método &lt;code>import_csv&lt;/code> y ya con la prueba ilustrando nos,
procedemos a realizar los ajustes sobre el método &lt;code>import_csv&lt;/code>.&lt;/p>
&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 72 tests in 15.477s
OK
&lt;/pre>
&lt;p>muy bien, ya tenemos el metodo que nos importaría desde csv los cargos de telefonía&lt;/p>
&lt;p>ahora vamos a proceder a obtener el archivo desde el endpoint entregado por telefonía.&lt;/p>
&lt;p>&lt;strong>tests/test_import_remote_charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">unittest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ImportFromRemoteChargeTel&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Import from remote endpoint Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_importar_cargos_desde_servicio_remote&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># sustituimos el metodo de descarga por una version&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># ajustada para las pruebas, evitamos las acciones sobre&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># el servicio remoto&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>_downloader &lt;span style="font-weight:bold">=&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>_fake_downloader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>import_remote()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charges &lt;span style="font-weight:bold">=&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>search([])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> got &lt;span style="font-weight:bold">=&lt;/span> [{&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>client_code,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>billsec,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>sms,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>day} &lt;span style="font-weight:bold">for&lt;/span> c &lt;span style="font-weight:bold">in&lt;/span> charges]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="font-weight:bold">.&lt;/span>assertListEqual([
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;ABC123&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">1000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">2000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">100&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ], got)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_fake_downloader&lt;/span>(self, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(&lt;span style="color:#b84">&amp;#39;tests/tel_2023-06-23.csv&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> f&lt;span style="font-weight:bold">.&lt;/span>read()&lt;span style="font-weight:bold">.&lt;/span>encode()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Traceback (most recent call last):
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
result = func(*args, **kwargs)
File "/tmp/d20230623-35869-15s1jvk/charging_tel/tests/test_import_remote_charge_tel.py", line 16, in test_importar_cargos_desde_servicio_remote
ChargeTel.import_remote(downloader=self._fake_downloader)
AttributeError: type object 'charging_tel.charge_tel' has no attribute 'import_remote'
&lt;/pre>
&lt;p>Tal cual, ya vamos conociendo el ciclo de: especular, ilustrar y ajustar :).&lt;/p>
&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> downloader &lt;span style="font-weight:bold">is&lt;/span> &lt;span style="font-weight:bold">None&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> downloader &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">pass&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">pass&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
First list contains 2 additional elements.
First extra element 0:
{'client_code': 'ABC123', 'billsec': 1000, 'sms': 1, 'day': datetime.date(2023, 6, 22)}
+ []
- [{'billsec': 1000,
- 'client_code': 'ABC123',
- 'day': datetime.date(2023, 6, 22),
- 'sms': 1},
- {'billsec': 2000,
- 'client_code': 'CP535',
- 'day': datetime.date(2023, 6, 22),
- 'sms': 100}]
&lt;/pre>
&lt;p>Excelente, ya no tenemos errores por parte Python, ahora la prueba nos
indica que no coinciden los valores, pasemos a ajustar la implementación es
decir vamos a sustituir los &lt;code>pass&lt;/code> por nuestra primera versión de código ejecutable.&lt;/p>
&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">tempfile&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">urllib.request&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># si esto es un requerimiento por parte del cliente, debemos realizar&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># una prueba para garantizar este comportamiento.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># TODO: como procedemos si la codificacion es incorrecta a la entregada?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;ir.date&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> today &lt;span style="font-weight:bold">=&lt;/span> Date&lt;span style="font-weight:bold">.&lt;/span>today()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader(year&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>year,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>month,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>day)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> tempfile&lt;span style="font-weight:bold">.&lt;/span>NamedTemporaryFile(prefix&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charge-tel&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>write(content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>flush()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>import_csv(f&lt;span style="font-weight:bold">.&lt;/span>name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> urllib&lt;span style="font-weight:bold">.&lt;/span>request&lt;span style="font-weight:bold">.&lt;/span>urlopen(&lt;span style="color:#b84">f&lt;/span>&lt;span style="color:#b84">&amp;#34;http://example.com/&lt;/span>&lt;span style="color:#b84">{&lt;/span>year&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>month&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>day&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">.csv&amp;#34;&lt;/span>)&lt;span style="font-weight:bold">.&lt;/span>read()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 95 tests in 0.579s
OK
&lt;/pre>
&lt;p>uii yuiyuii, volvimos a estar estable y ya mas cerca de cerrar este escenario,
ahora para hacer la importación diaria podemos usar &lt;a href="https://docs.tryton.org/projects/server/en/latest/topics/cron.html">Cron&lt;/a> de trytond que nos facilita
programar acciones.&lt;/p>
&lt;p>&lt;strong>tests/scenario_cron_importer.rst&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rst" data-lang="rst">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">=====================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">Cron Charge Importer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">=====================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span>Dependencias&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from proteus import Model, Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from trytond.tests.tools import activate_modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Activacion de modulos&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; config = activate_modules(&amp;#39;charging_tel&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Disparar importador desde Cron::&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; Cron = Model.get(&amp;#39;ir.cron&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron = Cron()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.method = &amp;#39;charging_tel.charge_tel|import_remote&amp;#39;&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.interval_number = 1&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.interval_type = &amp;#39;days&amp;#39;&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.save()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.click(&amp;#39;run_once&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
raise SelectionValidationError(gettext(
trytond.model.modelstorage.SelectionValidationError: The value "charging_tel.charge_tel" for field "Method" in "4" of "Cron" is not one of the allowed options. -
&lt;/pre>
&lt;p>como nos indica el mensaje de error no logra ubicar ese valor, el ajuste que podemos
hacer es indicarle a &lt;code>Cron&lt;/code> acerca de nuestro interés.&lt;/p>
&lt;p>&lt;strong>cron.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelCron&lt;/span>(metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;ir.cron&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">__setup__&lt;/span>(cls):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>__setup__()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>method&lt;span style="font-weight:bold">.&lt;/span>selection&lt;span style="font-weight:bold">.&lt;/span>append(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel|import_remote&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#34;Import Charges From Remote Server&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>e informamos a trytond&lt;/p>
&lt;p>&lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> party
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> charge_tel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> cron
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="color:#b84">&amp;#39;register&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">register&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Pool&lt;span style="font-weight:bold">.&lt;/span>register(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>Party,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charge_tel&lt;span style="font-weight:bold">.&lt;/span>ChargeTel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cron&lt;span style="font-weight:bold">.&lt;/span>ChargeTelCron,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>, type_&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;model&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 95 tests in 0.585s
OK
&lt;/pre>
&lt;p>muy bien todo sigue operativo y ademas ya hemos indicado a trytond nuestra
intención de poder registrar un Cron para importar los registros del servidor remoto.&lt;/p>
&lt;p>Ahora aunque tenemos una implementación trabajando no necesariamente es la mas adecuada,
este punto es crucial ya que si continuamos adicionando caracteristicas sin reajustar el código
a nuevos conceptos o eliminando los que ya cambiaron, rápidamente perderíamos el control
sobre la mantenibilidad y legibilidad, las palabras que usamos en clases, métodos,variables,archivos o carpetas
nos crean un vocabulario y si este vocabulario es obsoleto o distorsionado se nos dificultaría
entender que es lo que el software esta realizando o bien este como participa de los objetivos del cliente,
lo anterior es llamado &lt;a href="https://martinfowler.com/tags/refactoring.html">Refactorizacion&lt;/a>, si queremos
construir un software solido debemos constantemente estar refactorizando.&lt;/p>
&lt;p>Como vemos el modelo &lt;strong>ChargeTel&lt;/strong> tienen los métodos &lt;strong>import_csv&lt;/strong> y &lt;strong>import_remote&lt;/strong> que tiene en común importar un archivo de formato csv,
con los cargos de telefonía? nada, cuando tenemos un comportamiento que no aporta al modelo, movemos este comportamiento por fuera del modelo.&lt;/p>
&lt;p>&lt;strong>charge_tel_importer.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Model
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelImporter&lt;/span>(Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel Importer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;ir.date&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> today &lt;span style="font-weight:bold">=&lt;/span> Date&lt;span style="font-weight:bold">.&lt;/span>today()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader(year&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>year,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>month,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>day)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> tempfile&lt;span style="font-weight:bold">.&lt;/span>NamedTemporaryFile(prefix&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charge-tel&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>write(content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>flush()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>import_csv(f&lt;span style="font-weight:bold">.&lt;/span>name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> urllib&lt;span style="font-weight:bold">.&lt;/span>request&lt;span style="font-weight:bold">.&lt;/span>urlopen(&lt;span style="color:#b84">f&lt;/span>&lt;span style="color:#b84">&amp;#34;http://example.com/&lt;/span>&lt;span style="color:#b84">{&lt;/span>year&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>month&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>day&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">.csv&amp;#34;&lt;/span>)&lt;span style="font-weight:bold">.&lt;/span>read()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/pool.py", line 190, in get
return self._pool[self.database_name][type][name]
KeyError: 'charging_tel.charge_tel_importer'
&lt;/pre>
&lt;p>ya hemos visto anteriormente este error.&lt;/p>
&lt;p>&lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> party
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> charge_tel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> cron
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> charge_tel_importer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="color:#b84">&amp;#39;register&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">register&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Pool&lt;span style="font-weight:bold">.&lt;/span>register(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>Party,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charge_tel&lt;span style="font-weight:bold">.&lt;/span>ChargeTel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cron&lt;span style="font-weight:bold">.&lt;/span>ChargeTelCron,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charge_tel_importer&lt;span style="font-weight:bold">.&lt;/span>ChargeTelImporter,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>, type_&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;model&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
reader = list(csv.DictReader(csvfile))
NameError: name 'csv' is not defined
...
return urllib.request.urlopen(f"http://example.com/{year}-{month}-{day}.csv").read()
NameError: name 'urllib' is not defined
&lt;/pre>
&lt;p>uuups faltaron importar las dependencias&lt;/p>
&lt;p>&lt;strong>charge_tel_importer.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">urllib&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Model
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelImporter&lt;/span>(Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel Importer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;ir.date&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> today &lt;span style="font-weight:bold">=&lt;/span> Date&lt;span style="font-weight:bold">.&lt;/span>today()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader(year&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>year,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>month,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>day)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> tempfile&lt;span style="font-weight:bold">.&lt;/span>NamedTemporaryFile(prefix&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charge-tel&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>write(content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>flush()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>import_csv(f&lt;span style="font-weight:bold">.&lt;/span>name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> urllib&lt;span style="font-weight:bold">.&lt;/span>request&lt;span style="font-weight:bold">.&lt;/span>urlopen(&lt;span style="color:#b84">f&lt;/span>&lt;span style="color:#b84">&amp;#34;http://example.com/&lt;/span>&lt;span style="color:#b84">{&lt;/span>year&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>month&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>day&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">.csv&amp;#34;&lt;/span>)&lt;span style="font-weight:bold">.&lt;/span>read()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
return cls.create(reader)
AttributeError: type object 'charging_tel.charge_tel_importer' has no attribute 'create'
&lt;/pre>
&lt;p>epa, eso nos pasa por duplicar el código.&lt;/p>
&lt;p>&lt;strong>charge_tel_importer.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">urllib&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Model
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelImporter&lt;/span>(Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel Importer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;ir.date&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> today &lt;span style="font-weight:bold">=&lt;/span> Date&lt;span style="font-weight:bold">.&lt;/span>today()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader(year&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>year,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>month,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>day)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> tempfile&lt;span style="font-weight:bold">.&lt;/span>NamedTemporaryFile(prefix&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charge-tel&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>write(content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>flush()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>import_csv(f&lt;span style="font-weight:bold">.&lt;/span>name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> urllib&lt;span style="font-weight:bold">.&lt;/span>request&lt;span style="font-weight:bold">.&lt;/span>urlopen(&lt;span style="color:#b84">f&lt;/span>&lt;span style="color:#b84">&amp;#34;http://example.com/&lt;/span>&lt;span style="color:#b84">{&lt;/span>year&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>month&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>day&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">.csv&amp;#34;&lt;/span>)&lt;span style="font-weight:bold">.&lt;/span>read()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
File "/usr/lib/python3.9/urllib/request.py", line 641, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 404: Not Found
&lt;/pre>
&lt;p>&lt;code>urllib.error.HTTPError: HTTP Error 404: Not Found&lt;/code> nos informa que el un hubo respuesta del servidor,esto nos esta indicando
que la prueba esta haciendo la petición al servidor el cual no existe.&lt;/p>
&lt;p>&lt;strong>tests/test_import_remote_charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">unittest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ImportFromRemoteChargeTel&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Import from remote endpoint Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_importar_cargos_desde_servicio_remote&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTelImporter &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># sustituimos el metodo de descarga por una version&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># ajustada para las pruebas, evitamos las acciones sobre&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># el servicio remoto&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTelImporter&lt;span style="font-weight:bold">.&lt;/span>_downloader &lt;span style="font-weight:bold">=&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>_fake_downloader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTelImporter&lt;span style="font-weight:bold">.&lt;/span>import_remote()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charges &lt;span style="font-weight:bold">=&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>search([])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> got &lt;span style="font-weight:bold">=&lt;/span> [{&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>client_code,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>billsec,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>sms,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>day} &lt;span style="font-weight:bold">for&lt;/span> c &lt;span style="font-weight:bold">in&lt;/span> charges]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="font-weight:bold">.&lt;/span>assertListEqual([
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;ABC123&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">1000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">2000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">100&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ], got)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_fake_downloader&lt;/span>(self, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(&lt;span style="color:#b84">&amp;#39;tests/tel_2023-06-23.csv&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> f&lt;span style="font-weight:bold">.&lt;/span>read()&lt;span style="font-weight:bold">.&lt;/span>encode()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
File "/tmp/d20230624-6103-16gzx79/charging_tel/charge_tel_importer.py", line 29, in import_remote
with tempfile.NamedTemporaryFile(prefix='charge-tel') as f:
NameError: name 'tempfile' is not defined
&lt;/pre>
&lt;p>&lt;code>NameError: name 'tempfile' is not defined&lt;/code> de nuevo una dependencia que no trasladamos al crear el importador, acá se ve lo
importante de las pruebas, Python es un código interpretado y hasta que el interprete no ejecute la linea en cuestión no nos enteraríamos
que el comportamiento es inconsistente o bien hasta que el usuario del software lo reporte.&lt;/p>
&lt;p>&lt;strong>charge_tel_importer.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">csv&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">urllib&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">tempfile&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Model
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelImporter&lt;/span>(Model):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel Importer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_csv&lt;/span>(cls, path):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># https://docs.python.org/3/library/csv.html&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> &lt;span style="color:#999">open&lt;/span>(path, newline&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> csvfile:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reader &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#999">list&lt;/span>(csv&lt;span style="font-weight:bold">.&lt;/span>DictReader(csvfile))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>create(reader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">import_remote&lt;/span>(cls, downloader&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">None&lt;/span>):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;ir.date&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> today &lt;span style="font-weight:bold">=&lt;/span> Date&lt;span style="font-weight:bold">.&lt;/span>today()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>_downloader(year&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>year,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> month&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>month,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day&lt;span style="font-weight:bold">=&lt;/span>today&lt;span style="font-weight:bold">.&lt;/span>day)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> tempfile&lt;span style="font-weight:bold">.&lt;/span>NamedTemporaryFile(prefix&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;charge-tel&amp;#39;&lt;/span>) &lt;span style="font-weight:bold">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>write(content)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> f&lt;span style="font-weight:bold">.&lt;/span>flush()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>import_csv(f&lt;span style="font-weight:bold">.&lt;/span>name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">_downloader&lt;/span>(cls, year, month, day):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> urllib&lt;span style="font-weight:bold">.&lt;/span>request&lt;span style="font-weight:bold">.&lt;/span>urlopen(&lt;span style="color:#b84">f&lt;/span>&lt;span style="color:#b84">&amp;#34;http://example.com/&lt;/span>&lt;span style="color:#b84">{&lt;/span>year&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>month&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">-&lt;/span>&lt;span style="color:#b84">{&lt;/span>day&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">.csv&amp;#34;&lt;/span>)&lt;span style="font-weight:bold">.&lt;/span>read()
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>charge_tel.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModelView, ModelSQL, fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTel&lt;/span>(ModelSQL, ModelView):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Charge Tel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># habilitamos `readonly` para indicar que no puede ser editado luego de creado&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;Client Code&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> billsec &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;Billsec&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sms &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Integer(&lt;span style="color:#b84">&amp;#39;SMS&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> day &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Date(&lt;span style="color:#b84">&amp;#39;DAY&amp;#39;&lt;/span>, readonly&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">True&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 95 tests in 0.597s
OK
&lt;/pre>
&lt;p>ahora renombramos la prueba para hacer la aclaración de que el responsable de importar es &lt;code>ChargeTelImporter&lt;/code>.&lt;/p>
&lt;p>&lt;strong>tests/test_charge_tel_importer.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">import&lt;/span> &lt;span style="color:#555">datetime&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelImporter&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Charge Tel Importer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;charging_tel&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_crear_cargos_desde_csv&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTel &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTelImporter &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChargeTelImporter&lt;span style="font-weight:bold">.&lt;/span>import_csv(&lt;span style="color:#b84">&amp;#39;tests/tel_2023-06-23.csv&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> charges &lt;span style="font-weight:bold">=&lt;/span> ChargeTel&lt;span style="font-weight:bold">.&lt;/span>search([])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> got &lt;span style="font-weight:bold">=&lt;/span> [{&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>client_code,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>billsec,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>sms,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: c&lt;span style="font-weight:bold">.&lt;/span>day} &lt;span style="font-weight:bold">for&lt;/span> c &lt;span style="font-weight:bold">in&lt;/span> charges]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="font-weight:bold">.&lt;/span>assertListEqual([
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;ABC123&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">1000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)},
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">&amp;#39;client_code&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CP535&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;billsec&amp;#39;&lt;/span>: &lt;span style="color:#099">2000&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;sms&amp;#39;&lt;/span>: &lt;span style="color:#099">100&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#39;day&amp;#39;&lt;/span>: datetime&lt;span style="font-weight:bold">.&lt;/span>date(&lt;span style="color:#099">2023&lt;/span>, &lt;span style="color:#099">6&lt;/span>, &lt;span style="color:#099">22&lt;/span>)}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ], got)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>eliminamos la prueba obsoleta.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>rm tests/test_create_charge_tel.py
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 95 tests in 0.623s
OK
&lt;/pre>
&lt;p>Excelente ya estamos estables nuevamente, ahora vamos actualizar el cron para usar el importador.&lt;/p>
&lt;p>&lt;strong>cron.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">ChargeTelCron&lt;/span>(metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;ir.cron&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">__setup__&lt;/span>(cls):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>__setup__()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>method&lt;span style="font-weight:bold">.&lt;/span>selection&lt;span style="font-weight:bold">.&lt;/span>append(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#b84">&amp;#39;charging_tel.charge_tel_importer|import_remote&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#34;Import Charges From Remote Server&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
File "/tmp/d20230624-9646-5mhmjy/charging_tel/tests/scenario_cron_importer.rst", line 21, in scenario_cron_importer.rst
Failed example:
cron.save()
...
raise SelectionValidationError(gettext(
trytond.model.modelstorage.SelectionValidationError: The value "charging_tel.charge_tel|import_remote" for field "Method" in "4" of "Cron" is not one of the allowed options. -
&lt;/pre>
&lt;p>efectivamente ya Cron no puede ubicar el método, debemos actualizar la prueba ya que cambio el método de importación&lt;/p>
&lt;p>&lt;strong>tests/scenario_cron_importer.rst&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rst" data-lang="rst">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">======================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">Cron Charge Importer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">======================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span>Dependencias&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from proteus import Model, Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from trytond.tests.tools import activate_modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Activacion de modulos&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; config = activate_modules(&amp;#39;charging_tel&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Disparar importador desde Cron::&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; Cron = Model.get(&amp;#39;ir.cron&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron = Cron()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.method = &amp;#39;charging_tel.charge_tel_importer|import_remote&amp;#39;&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.interval_number = 1&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.interval_type = &amp;#39;days&amp;#39;&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.save()&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span> &amp;gt;&amp;gt;&amp;gt; cron.click(&amp;#39;run_once&amp;#39;)&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 96 tests in 0.695s
OK
&lt;/pre>
&lt;p>Con la ultima implementación daríamos por cerrado los escenarios planteados
con el cliente ademas quedamos con una suite de pruebas para garantizar lo acordado y documentación de como el software participa de los objetivos del cliente.&lt;/p>
&lt;p>Por ultimo dejo como ejercicio al lector la implementación de las vistas.&lt;/p>
&lt;p>En resumen:&lt;/p>
&lt;ul>
&lt;li>creación del módulo usando la plantilla&lt;/li>
&lt;li>escribir el módulo usando el un ciclo de: especular, ilustrar y ajustar&lt;/li>
&lt;li>plantear los escenarios aceptables acordados con el cliente en &lt;strong>doctest&lt;/strong>&lt;/li>
&lt;li>informar a trytond nuestra intención por medio de &lt;strong>tryton.cfg&lt;/strong> y &lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/li>
&lt;li>extender modelo usando &lt;em>PoolMeta&lt;/em>&lt;/li>
&lt;li>crear nuevo modelo usando &lt;em>ModelSQL&lt;/em> y &lt;em>ModelView&lt;/em>&lt;/li>
&lt;li>informar a trytond por medio &lt;code>ir.cron&lt;/code> la posibilidad de programar tareas recurrentes.&lt;/li>
&lt;/ul>
&lt;p>si deseas generar el proyecto creado en este post puedes usar &lt;a href="http://bit4bit.github.io/post/mdtoapp">mdtoapp&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#008080">ARTIFACT&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#099">1&lt;/span> &lt;span style="color:#008080">TO&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>/tmp/basico-2 ruby mdtoapp.rb &lt;span style="color:#b84">&amp;#39;https://chiselapp.com/user/bit4bit/repository/bit4bit_website/raw?ci=tip&amp;amp;filename=content/post%2fwriting_module_trytond_basics_2.md&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Escribiendo un Modulo de Tryton ERP Básico 1</title><link>http://bit4bit.github.io/post/writing_module_trytond_basics_1/</link><pubDate>Tue, 20 Jun 2023 16:47:56 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/writing_module_trytond_basics_1/</guid><description>&lt;p>Tryton ERP además de ERP es un excelente framework y más su versatilidad a la hora
de ser personalizado, a continuación estudiaremos
un poco como se puede extender el comportamiento y la vista de los módulos, el que considero
como primer paso para aprender a personalizar trytond a nivel de código,
asumo que se tiene conocimiento de Python, Tryton ERP y desarrollo de software en general.&lt;/p>
&lt;p>Como ejercicio de práctica vamos a realizar la extensión del módulo &lt;a href="https://docs.tryton.org/projects/modules-party/en/latest/index.html">party&lt;/a>
y adicionarle un campo, en nuestro caso vamos adicionar el campo &lt;a href="https://www.dane.gov.co/files/sen/nomenclatura/ciiu/CIIU_Rev_4_AC2020.pdf">CIIU&lt;/a>,
que en Colombia se utiliza para describir la actividad económica del tercero.&lt;/p>
&lt;h2 id="creación-del-modulo">Creación del modulo&lt;/h2>
&lt;p>Primero instalamos dependencias necesarias para el desarrollo del módulo.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pip3 install &lt;span style="color:#008080">trytond&lt;/span>&lt;span style="font-weight:bold">==&lt;/span>6.8.0 --user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>pip3 install proteus --user
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Ahora creamos el módulo usando plantilla por medio de &lt;a href="https://www.cookiecutter.io/">cookiecutter&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>cookiecutter git+https://gitlab.com/bit4bit/cookiecutter-tryton-module.git --directory template --no-input &lt;span style="color:#008080">module_name&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>party_ciiu &lt;span style="color:#008080">version&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>6.8.0 &lt;span style="color:#008080">test_with_scenario&lt;/span>&lt;span style="font-weight:bold">=&lt;/span>party_ciiu.rst
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lo anterior nos crea un directorio &lt;code>party_ciiu&lt;/code> con la estructura de un módulo de tryton.&lt;/p>
&lt;p>Activamos el módulo en modo desarrollo.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">cd&lt;/span> party_ciiu
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>python3 setup.py develop --user
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Para confirmar que el módulo este operativo ejecutamos las pruebas.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">cd&lt;/span> party_ciiu
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Y si todo es correcto veríamos&lt;/p>
&lt;pre>
Ran 22 tests in 2.268s
OK
&lt;/pre>
&lt;h2 id="adición-campo-ciiu_code">Adición campo &lt;code>ciiu_code&lt;/code>&lt;/h2>
&lt;p>Nos vamos a orientador durante el desarrollo de la siguiente manera:&lt;/p>
&lt;ul>
&lt;li>mantener el módulo todo el tiempo en operación es decir, no deberíamos tener errores por mucho más que unos minutos.&lt;/li>
&lt;li>aplazar lo que mas podamos el trabajo sobre vista del módulo, ya que cuando editamos la vista se hace visible para que el usuario interactué con el modulo y si este esta incompleto abrimos el camino a las inconsistencias en el sistema.&lt;/li>
&lt;/ul>
&lt;p>Para avanzar y conocer el progreso de nuestro desarrollo, procederemos de la siguiente manera:&lt;/p>
&lt;ul>
&lt;li>describimos los escenarios o pruebas que esperamos (etapa de especulación)&lt;/li>
&lt;li>ejecutamos los escenarios o pruebas (etapa de ilustración)&lt;/li>
&lt;li>corregimos el error, aclaramos la intención y volvemos al primer paso (etapa de ajuste)&lt;/li>
&lt;/ul>
&lt;p>Vamos a empezar creando el escenario que refleje nuestro objetivo de adicionar el campo &lt;code>ciiu_code&lt;/code>&lt;/p>
&lt;p>&lt;strong>tests/scenario_create_party_with_ciiu_code.rst&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rst" data-lang="rst">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">Crear Party con campo ciiu_code Scenario&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">===================&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a61717;background-color:#e3d2d2">&lt;/span>Dependencias&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from proteus import Model, Wizard&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; from trytond.tests.tools import activate_modules
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Activacion de modulos&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; config = activate_modules(&amp;#39;party_ciiu&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84">&lt;/span>Party con ciiu_code&lt;span style="color:#b84">::&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; Party = Model.get(&amp;#39;party.party&amp;#39;)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; party = Party()
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; party.ciiu_code = &amp;#39;0111&amp;#39; # valor real esperados
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; party.save()
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;gt;&amp;gt;&amp;gt; party.ciiu_code
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#b84"> &amp;#39;0111&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ejecutamos los escenarios&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>y vemos nuestro primer error&lt;/p>
&lt;pre>
Failed example:
Party = Model.get('party.party')
...
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/pool.py", line 190, in get
return self._pool[self.database_name][type][name]
KeyError: 'party.party'
&lt;/pre>
&lt;p>&lt;code>KeyError: 'party.party'&lt;/code> nos indica que estamos intentado acceder a un modelo que
no esta registrado, para corregir vamos a instalar el modulo y notificar que nuestro modulo requiere del modulo &lt;code>party&lt;/code> para operar.&lt;/p>
&lt;p>instalamos dependencia&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pip3 install trytond-party&lt;span style="font-weight:bold">==&lt;/span>6.8.0 --user
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>indicamos a trytond que requerimos del modulo &lt;code>party&lt;/code>&lt;/p>
&lt;p>&lt;strong>tryton.cfg&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-config" data-lang="config">[tryton]
version=6.8.0
depends:
ir
party
xml:
&lt;/code>&lt;/pre>&lt;p>ejecutamos los escenarios&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>y vemos nuestro siguiente error&lt;/p>
&lt;pre>
Failed example:
party.ciiu_code = '0110'
Exception raised:
Traceback (most recent call last):
File "/usr/lib/python3.9/doctest.py", line 1336, in __run
exec(compile(example.source, filename, "single",
File "&lt;doctest scenario_create_party_ciiu_code.rst[5]>", line 1, in &lt;module>
party.ciiu_code = '0110'
AttributeError: 'party.party' object has no attribute 'ciiu_code'
&lt;/pre>
&lt;p>&lt;code>AttributeError: 'party.party' object has no attribute 'ciiu_code'&lt;/code> nos indica
que estamos intentando asignar un valor a un atributo que no existe en el modelo &lt;code>party.party&lt;/code>, para corregir necesitamos este atributo en &lt;code>party.party&lt;/code>.&lt;/p>
&lt;p>Extendemos el modelo &lt;code>party.party&lt;/code> y adicionamos el nuevo atributo.&lt;/p>
&lt;p>&lt;strong>party.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party&lt;/span>( metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ciiu_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;CIIU CODE&amp;#39;&lt;/span>, required&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">False&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Le indicamos a tryton el modelo que hemos extendido&lt;/p>
&lt;p>&lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">.&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> party
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>__all__ &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="color:#b84">&amp;#39;register&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">register&lt;/span>():
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Pool&lt;span style="font-weight:bold">.&lt;/span>register(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>Party,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;party_ciiu&amp;#39;&lt;/span>, type_&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="color:#b84">&amp;#39;model&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ejecutamos los escenarios&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>y por fin ya no tenemos el error&lt;/p>
&lt;pre>
Ran 24 tests in 9.231s
OK
&lt;/pre>
&lt;p>Lo anterior nos indica que ya el &lt;code>party.party&lt;/code> tiene el nuevo atributo,
pero aun no hemos realizado una implementación aceptable, una implementación
aceptable es una implementación que se ajuste a los escenarios planteados o acordados
con el cliente, en nuestro caso tomaremos como aceptable la implementación
del &lt;code>ciiu_code&lt;/code> cuando este valide que el &lt;a href="https://www.dane.gov.co/files/sen/nomenclatura/ciiu/CIIU_Rev_4_AC2020.pdf">formato&lt;/a> sea el correcto y este
entre los valores esperados según cliente.&lt;/p>
&lt;p>&lt;strong>tests/test_entrada_es_opcional.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> RequiredValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">PartyCiiu_EntradasRequeridas_TestCase&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Party Ciiu module&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party_ciiu&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_ciiud_code_es_opcional&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">try&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">except&lt;/span> RequiredValidationError:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> self&lt;span style="font-weight:bold">.&lt;/span>fail(&lt;span style="color:#b84">&amp;#39;se espera campo `ciiu_code` como opcional&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>siendo la salida de la ejecución&lt;/p>
&lt;pre>
Ran 47 tests in 12.323s
OK
&lt;/pre>
&lt;p>Lo cual nos indica que no hubo errores, en nuestro caso esta prueba
nos garantiza que si por algún motivo cambia la obligatoriedad del campo
&lt;code>ciiu_code&lt;/code> nos alerte y tomemos las acciones pertinentes.&lt;/p>
&lt;p>Ahora procederemos con la validación del campo, es recomendable partir de la entradas erróneas,
al iniciar con la validación de las entradas erróneas se nos reduce el margen
de que el programa entre en un estado inconsistente, ya que en el manejo de estas
entradas es donde usualmente se presenta la mayoría de inconsistencia.&lt;/p>
&lt;p>&lt;strong>tests/test_ciiu_code.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">PartyCiiu_ciiu_code_TestCase&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Party Ciiu campo ciiu_code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party_ciiu&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_invalida_UserError&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CIIU&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;99999&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;9999&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Traceback (most recent call last):
File "/home/bit4bit/.local/lib/python3.9/site-packages/trytond/tests/test_tryton.py", line 223, in wrapper
result = func(*args, **kwargs)
File "/tmp/d20230621-16221-7fnzm8/party_ciiu/tests/test_ciiu_code.py", line 18, in test_para_entrada_invalida_UserError
party.save()
AssertionError: ValidationError not raised
&lt;/pre>
&lt;p>&lt;code>AssertionError: ValidationError not raised&lt;/code> nos indica que no se lanza el error,
ahora procederemos a corregir, como hemos visto corregir seria realizar la implementación del código o bien corregir un defecto.&lt;/p>
&lt;p>&lt;strong>party_ciiu/party.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> PoolMeta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> fields
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.i18n&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> gettext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Party&lt;/span>( metaclass&lt;span style="font-weight:bold">=&lt;/span>PoolMeta):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> __name__ &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ciiu_code &lt;span style="font-weight:bold">=&lt;/span> fields&lt;span style="font-weight:bold">.&lt;/span>Char(&lt;span style="color:#b84">&amp;#39;CIIU CODE&amp;#39;&lt;/span>, required&lt;span style="font-weight:bold">=&lt;/span>&lt;span style="font-weight:bold">False&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">validate_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">super&lt;/span>()&lt;span style="font-weight:bold">.&lt;/span>validate_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cls&lt;span style="font-weight:bold">.&lt;/span>check_fields(records, field_names)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">check_fields&lt;/span>(cls, records, field_names):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> field_names &lt;span style="font-weight:bold">and&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> (field_names &lt;span style="font-weight:bold">&amp;amp;&lt;/span> {&lt;span style="color:#b84">&amp;#39;ciiu_code&amp;#39;&lt;/span>}):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">for&lt;/span> record &lt;span style="font-weight:bold">in&lt;/span> records:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">if&lt;/span> &lt;span style="font-weight:bold">not&lt;/span> cls&lt;span style="font-weight:bold">.&lt;/span>allowed_ciiu_code(record&lt;span style="font-weight:bold">.&lt;/span>ciiu_code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">raise&lt;/span> ValidationError(gettext(&lt;span style="color:#b84">&amp;#39;party_ciiu.ciiu_code_invalid&amp;#39;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @classmethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">allowed_ciiu_code&lt;/span>(cls, code):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># solo validamos los codigos de interes para nuestra implementacion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> codes &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="font-weight:bold">None&lt;/span>, &lt;span style="color:#b84">&amp;#39;0111&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0170&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0210&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0240&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0311&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0510&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0520&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0610&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">return&lt;/span> code &lt;span style="font-weight:bold">in&lt;/span> codes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 70 tests in 15.363s
OK
&lt;/pre>
&lt;p>ya volvimos a estar estables, ahora vamos a confirmar que el &lt;code>ciiu_code&lt;/code>
este operativo según los acordado con el cliente.&lt;/p>
&lt;p>&lt;strong>tests/test_ciiu_code.py&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.tests.test_tryton&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ModuleTestCase, with_transaction
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.pool&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> Pool
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">from&lt;/span> &lt;span style="color:#555">trytond.model.exceptions&lt;/span> &lt;span style="font-weight:bold">import&lt;/span> ValidationError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">PartyCiiu_ciiu_code_TestCase&lt;/span>(ModuleTestCase):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">&amp;#34;Test Party Ciiu campo ciiu_code&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;party_ciiu&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_valida_no_hay_error&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CIIU Valid&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># solo validamos los de interes para nuestro cliente&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> company_codes &lt;span style="font-weight:bold">=&lt;/span> [&lt;span style="font-weight:bold">None&lt;/span>, &lt;span style="color:#b84">&amp;#39;0111&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0170&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0210&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0240&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0311&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0510&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0520&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;0610&amp;#39;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">for&lt;/span> code &lt;span style="font-weight:bold">in&lt;/span> company_codes:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @with_transaction()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">test_para_entrada_invalida_UserError&lt;/span>(self):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pool &lt;span style="font-weight:bold">=&lt;/span> Pool()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Party &lt;span style="font-weight:bold">=&lt;/span> pool&lt;span style="font-weight:bold">.&lt;/span>get(&lt;span style="color:#b84">&amp;#39;party.party&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party, &lt;span style="font-weight:bold">=&lt;/span> Party&lt;span style="font-weight:bold">.&lt;/span>create([{&lt;span style="color:#b84">&amp;#39;name&amp;#39;&lt;/span>: &lt;span style="color:#b84">&amp;#39;CIIU&amp;#39;&lt;/span>}])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;99999&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;0&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">with&lt;/span> self&lt;span style="font-weight:bold">.&lt;/span>assertRaises(ValidationError):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>ciiu_code &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">&amp;#39;9999&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> party&lt;span style="font-weight:bold">.&lt;/span>save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">del&lt;/span> ModuleTestCase
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>python3 -munittest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;pre>
Ran 71 tests in 15.529s
OK
&lt;/pre>
&lt;p>excelente ahora que hemos validado el comportamiento aceptable por el cliente,
procedemos a activarlo en la interfaz del usuario.&lt;/p>
&lt;p>le indicamos al tryton que vamos a extender las vista &lt;code>party.party_view_tree&lt;/code> usando el archivo &lt;code>view/party_list.xml&lt;/code> y &lt;code>party.party_view_form&lt;/code> con el archivo &lt;code>view/party_form.xml&lt;/code>&lt;/p>
&lt;p>&lt;strong>party.xml&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999;font-weight:bold">&amp;lt;?xml version=&amp;#34;1.0&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;tryton&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;record&lt;/span> &lt;span style="color:#008080">model=&lt;/span>&lt;span style="color:#b84">&amp;#34;ir.ui.view&amp;#34;&lt;/span> &lt;span style="color:#008080">id=&lt;/span>&lt;span style="color:#b84">&amp;#34;party_view_tree&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;model&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>party.party&lt;span style="color:#000080">&amp;lt;/field&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;inherit&amp;#34;&lt;/span> &lt;span style="color:#008080">ref=&lt;/span>&lt;span style="color:#b84">&amp;#34;party.party_view_tree&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;name&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>party_list&lt;span style="color:#000080">&amp;lt;/field&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/record&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;record&lt;/span> &lt;span style="color:#008080">model=&lt;/span>&lt;span style="color:#b84">&amp;#34;ir.ui.view&amp;#34;&lt;/span> &lt;span style="color:#008080">id=&lt;/span>&lt;span style="color:#b84">&amp;#34;party_view_form&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;model&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>party.party&lt;span style="color:#000080">&amp;lt;/field&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;inherit&amp;#34;&lt;/span> &lt;span style="color:#008080">ref=&lt;/span>&lt;span style="color:#b84">&amp;#34;party.party_view_form&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;name&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>party_form&lt;span style="color:#000080">&amp;lt;/field&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/record&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/tryton&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>view/party_form.xml&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999;font-weight:bold">&amp;lt;?xml version=&amp;#34;1.0&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;xpath&lt;/span> &lt;span style="color:#008080">expr=&lt;/span>&lt;span style="color:#b84">&amp;#34;//page[@id=&amp;#39;general&amp;#39;]&amp;#34;&lt;/span> &lt;span style="color:#008080">position=&lt;/span>&lt;span style="color:#b84">&amp;#34;after&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;ciiu_code&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/xpath&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>view/party_list.xml&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999;font-weight:bold">&amp;lt;?xml version=&amp;#34;1.0&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;xpath&lt;/span> &lt;span style="color:#008080">expr=&lt;/span>&lt;span style="color:#b84">&amp;#34;//field[@name=&amp;#39;name&amp;#39;]&amp;#34;&lt;/span> &lt;span style="color:#008080">position=&lt;/span>&lt;span style="color:#b84">&amp;#34;after&amp;#34;&lt;/span>&lt;span style="color:#000080">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;field&lt;/span> &lt;span style="color:#008080">name=&lt;/span>&lt;span style="color:#b84">&amp;#34;ciiu_code&amp;#34;&lt;/span>&lt;span style="color:#000080">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#000080">&amp;lt;/xpath&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#000080">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>e informamos a tryton nuestra intención sobre cambios a la vista, adicionando &lt;code>party.xml&lt;/code> a la lista &lt;code>xml&lt;/code> en &lt;code>tryton.cfg&lt;/code>&lt;/p>
&lt;p>&lt;strong>tryton.cfg&lt;/strong>&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-config" data-lang="config">[tryton]
version=6.8.0
depends:
ir
party
xml:
party.xml
&lt;/code>&lt;/pre>&lt;p>Aclaro que el objetivo del ejercicio no era entrar en las particularidades para la creación del módulo,
sino conocer el procedimiento en general para escribir un módulo de trytond siendo este:&lt;/p>
&lt;ul>
&lt;li>creación del módulo usando la plantilla&lt;/li>
&lt;li>escribir el módulo usando el un ciclo de: especular, ilustrar y ajustar&lt;/li>
&lt;li>plantear los escenarios aceptables acordados con el cliente en &lt;strong>doctest&lt;/strong>&lt;/li>
&lt;li>informar a trytond nuestra intención por medio de &lt;strong>tryton.cfg&lt;/strong> y &lt;strong>&lt;strong>init&lt;/strong>.py&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>Y para finalizar me gustaría comentar acerca de como usamos los dos mecanismos que ofrece &lt;strong>trytond&lt;/strong> para las pruebas, estos son:&lt;/p>
&lt;ol>
&lt;li>archivos &lt;strong>.rst&lt;/strong> o &lt;strong>doctests&lt;/strong>&lt;/li>
&lt;li>pruebas por medio de &lt;strong>unittest&lt;/strong>&lt;/li>
&lt;/ol>
&lt;p>Usamos las pruebas &lt;strong>doctests&lt;/strong> para verificar que el módulo opere aceptablemente en los
escenarios planteados acordados con el cliente, y usamos &lt;strong>unittest&lt;/strong> ya para verificar en detalle el comportamiento
del módulo, diríamos que este último es una lupa a los escenarios de &lt;strong>doctests&lt;/strong>.&lt;/p>
&lt;h3 id="agradecimientos">Agradecimientos&lt;/h3>
&lt;ul>
&lt;li>&lt;code>@rakolnivok28&lt;/code> por las sugerencias&lt;/li>
&lt;/ul></description></item><item><title>Markdown To Application</title><link>http://bit4bit.github.io/post/mdtoapp/</link><pubDate>Mon, 12 Jun 2023 20:59:24 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/mdtoapp/</guid><description>&lt;p>&lt;a href="https://chiselapp.com/user/bit4bit/repository/bit4bit_website/file?name=code/mdtoapp.rb">&lt;strong>mdotapp&lt;/strong>&lt;/a> es otra herramienta más para la programación literaria,
pero con un objetivo practico: hacer ejecutable los post de este sitio, la diferencia fundamental con otras herramientas es que fácilmente
podemos crear una aplicación de software es decir múltiples archivos con código ejecutable.&lt;/p>
&lt;h1 id="uso">Uso&lt;/h1>
&lt;p>se permiten los siguientes atributos en el bloque:&lt;/p>
&lt;ul>
&lt;li>&lt;code>@mdtoapp&lt;/code> marcar el bloque para ser procesado por &lt;code>mdtoapp&lt;/code>&lt;/li>
&lt;li>&lt;code>file:&amp;lt;archivo&amp;gt;&lt;/code> escribir contenido en el archivo indicado, se puede indicar una ruta de directorios.&lt;/li>
&lt;li>&lt;code>@execute&lt;/code> el bloque se debe ejecutar&lt;/li>
&lt;li>&lt;code>executable:&amp;lt;command&amp;gt;&lt;/code> indicar un comando diferente a &lt;code>sh&lt;/code> para ejecutar el bloque&lt;/li>
&lt;li>&lt;code>@silence&lt;/code> no imprimir salida de la ejecución del bloque&lt;/li>
&lt;li>&lt;code>@tdd&lt;/code> el bloque se debe ejecutar en modo TDD&lt;/li>
&lt;li>&lt;code>@artifact&lt;/code> el bloque se ejecutar en modo ARTIFACT&lt;/li>
&lt;/ul>
&lt;p>la ejecución de los bloques es en el orden declarados en el documento.&lt;/p>
&lt;h2 id="ejemplos">Ejemplos&lt;/h2>
&lt;p>ejecutar un script&lt;/p>
&lt;pre>
```sh @mdtoapp file:date.sh @execute
date
```
&lt;/pre>
&lt;p>se puede crear una jerarquía de archivos&lt;/p>
&lt;pre>
```sh @mdtoapp file:demo/demo.sh
echo 'hola'
```
&lt;/pre>
&lt;pre>
```sh @mdtoapp file:run.sh @execute
bash demo/demo.sh
```
&lt;/pre>
&lt;p>para agrupar diferentes bloques en un mismo archivo usar el mismo nombre de archivo&lt;/p>
&lt;pre>
```sh @mdtoapp file:demo.sh
NAME=mdtoapp
```
&lt;/pre>
&lt;pre>
```sh @mdtoapp file:demo.sh @execute
echo $NAME
```
&lt;/pre>
&lt;p>crear un archivo de python y ejecutar las pruebas&lt;/p>
&lt;pre>
```python @mdtoapp file:test.py @execute executable:python3
import unittest
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
def test_split(self):
s = 'hello world'
self.assertEqual(s.split(), ['hello', 'world'])
# check that s.split fails when the separator is not a string
with self.assertRaises(TypeError):
s.split(2)
if __name__ == '__main__':
unittest.main()
```
&lt;/pre>
&lt;p>&lt;code>ruby mdtoapp.rb &amp;lt;mi archivo markdown a ejecutar&amp;gt;&lt;/code>&lt;/p>
&lt;p>los post de este sitio pueden ser ejecutados usando &lt;strong>mdtoapp.rb&lt;/strong>, ejemplo&lt;/p>
&lt;p>&lt;code>ruby mdtoapp.rb 'https://chiselapp.com/user/bit4bit/repository/bit4bit_website/raw?ci=tip&amp;amp;filename=content/post%2fmdtoapp.md'&lt;/code>&lt;/p>
&lt;p>si solo el interes es generar los archivos puedes usar&lt;/p>
&lt;p>&lt;code>TO=/tmp/out ARTIFACT=1 ruby mdtoapp.rb 'https://chiselapp.com/user/bit4bit/repository/bit4bit_website/raw?ci=tip&amp;amp;filename=content/post%2fmdtoapp.md'&lt;/code>&lt;/p></description></item><item><title>Infrastructure Behavior Driven-Development</title><link>http://bit4bit.github.io/post/infrastructure_behavior_driven_development/</link><pubDate>Mon, 12 Jun 2023 14:59:38 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/infrastructure_behavior_driven_development/</guid><description>&lt;p>(WIP)&lt;/p>
&lt;p>&lt;code>Ahí más inventores que inventos dice un amigo mio&lt;/code>.&lt;/p>
&lt;p>Como Desarrollador y Operador de plataformas de software orientadas
a telefonía (VOIP) y como practicante de TDD, me he visto envuelto en mayores responsabilidades
y por lo tanto en la necesidad de mejorar el proceso de configuración y mantenimiento de los diferentes
servicios, en este ejercicio he llegado a la conclusión que muchos
otros ya han llegado y es usar la práctica de un entorno de pruebas automatizadas
para configurar y probar los servicios.&lt;/p>
&lt;p>Lo que buscamos de esta práctica es:&lt;/p>
&lt;ul>
&lt;li>que la configuración de servicios entre en un ciclo de integración y despliegue continuo.&lt;/li>
&lt;li>que la configuración de los servicios este orientado a comportamientos esperados.&lt;/li>
&lt;li>tener un mecanismo para obtener rápidamente feedback en la configuración de los servicios.&lt;/li>
&lt;/ul>
&lt;p>Para ilustrar como proceder vamos a configurar el servicio &lt;strong>pure-ftpd&lt;/strong>
en base a una serie de requerimientos, usando una librería de pruebas automatizadas,
en este caso usare &lt;strong>rspec&lt;/strong> y &lt;strong>ruby&lt;/strong> para el ejercicio.&lt;/p>
&lt;p>inicializamos el entorno de pruebas de &lt;strong>rspec&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>rspec --init
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>iniciamos con una prueba fundamental y es verificar
la sintaxis de configuración.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">require&lt;/span> &lt;span style="color:#b84">&amp;#39;spec_helper&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">require&lt;/span> &lt;span style="color:#b84">&amp;#39;tempfile&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">validate_syntax&lt;/span>(config_path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#b84">%x[timeout 1 /usr/sbin/pure-ftpd &lt;/span>&lt;span style="color:#b84">#{&lt;/span>config_path&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> $?&lt;span style="font-weight:bold">.&lt;/span>exitstatus &lt;span style="font-weight:bold">==&lt;/span> &lt;span style="color:#099">0&lt;/span> &lt;span style="font-weight:bold">||&lt;/span> $?&lt;span style="font-weight:bold">.&lt;/span>exitstatus &lt;span style="font-weight:bold">==&lt;/span> &lt;span style="color:#099">124&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>describe &lt;span style="color:#b84">&amp;#39;pure-ftpd&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> let (&lt;span style="color:#b84">:conf&lt;/span>) { &lt;span style="color:#008080">Tempfile&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>new(&lt;span style="color:#b84">&amp;#39;pure-ftpd&amp;#39;&lt;/span>) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> describe &lt;span style="color:#b84">&amp;#39;configuracion&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> it &lt;span style="color:#b84">&amp;#39;al verificar archivo invalido falla&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>write(&lt;span style="color:#b84">&amp;#39;invalidline&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>flush
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect(validate_syntax(conf&lt;span style="font-weight:bold">.&lt;/span>path))&lt;span style="font-weight:bold">.&lt;/span>to be &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> it &lt;span style="color:#b84">&amp;#39;al verificar archivo valido ok&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>write(&lt;span style="color:#b84">&amp;#39;ChrootEveryone yes&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>flush
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect(validate_syntax(conf&lt;span style="font-weight:bold">.&lt;/span>path))&lt;span style="font-weight:bold">.&lt;/span>to be &lt;span style="font-weight:bold">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>una vez tenemos un mecanismo para confirmar que la sintaxis
del archivo es correcta procedemos a confirmar que el servicio
inicializa y finaliza correctamente en presencia del archivo de configuracion indicado.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">require&lt;/span> &lt;span style="color:#b84">&amp;#39;spec_helper&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#999">require&lt;/span> &lt;span style="color:#b84">&amp;#39;tempfile&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">ftpd_start&lt;/span>(config_path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pid &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#008080">Process&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>spawn(&lt;span style="color:#b84">&amp;#34;/usr/sbin/pure-ftpd &lt;/span>&lt;span style="color:#b84">#{&lt;/span>config_path&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Process&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>detach(pid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#999">sleep&lt;/span> &lt;span style="color:#099">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> port &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#b84">%x{lsof -p &lt;/span>&lt;span style="color:#b84">#{&lt;/span>pid&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84"> -itcp -a -P -n 2&amp;gt; /dev/null}&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>chomp&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#808000">/TCP.+:(\d+)/&lt;/span>,&lt;span style="color:#099">1&lt;/span>&lt;span style="font-weight:bold">].&lt;/span>to_i
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {&lt;span style="color:#b84">pid&lt;/span>: pid, &lt;span style="color:#b84">port&lt;/span>: port}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">ftpd_alive?&lt;/span>(server)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#998;font-style:italic"># http://dev.housetrip.com/2014/03/24/ruby-pid-tip/&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Process&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>kill(&lt;span style="color:#099">0&lt;/span>, server&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#b84">:pid&lt;/span>&lt;span style="font-weight:bold">]&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">rescue&lt;/span> &lt;span style="color:#008080">Errno&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">ESRCH&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">ftpd_stop&lt;/span>(server)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Process&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>kill(&lt;span style="color:#099">9&lt;/span>, server&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#b84">:pid&lt;/span>&lt;span style="font-weight:bold">]&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">rescue&lt;/span> &lt;span style="color:#008080">Errno&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">ESRCH&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>describe &lt;span style="color:#b84">&amp;#39;pure-ftpd&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> let (&lt;span style="color:#b84">:conf&lt;/span>) { &lt;span style="color:#008080">Tempfile&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>new(&lt;span style="color:#b84">&amp;#39;pure-ftpd&amp;#39;&lt;/span>) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> describe &lt;span style="color:#b84">&amp;#39;gestión del servicio&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> it &lt;span style="color:#b84">&amp;#39;iniciar cuando el archivo de configuracion es correcto&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>write(&lt;span style="color:#b84">&amp;#39;Bind 127.0.0.1,0&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>flush
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pid &lt;span style="font-weight:bold">=&lt;/span> ftpd_start(conf&lt;span style="font-weight:bold">.&lt;/span>path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect(ftpd_alive?(pid))&lt;span style="font-weight:bold">.&lt;/span>to be &lt;span style="font-weight:bold">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftpd_stop(pid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> it &lt;span style="color:#b84">&amp;#39;not iniciar cuando el archivo de configuracion es invalido&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>write(&lt;span style="color:#b84">&amp;#39;asdfs&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conf&lt;span style="font-weight:bold">.&lt;/span>flush
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> pid &lt;span style="font-weight:bold">=&lt;/span> ftpd_start(conf&lt;span style="font-weight:bold">.&lt;/span>path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect(ftpd_alive?(pid))&lt;span style="font-weight:bold">.&lt;/span>to be &lt;span style="font-weight:bold">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftpd_stop(pid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;!--
```ruby @mdtoapp file:spec/pure-ftpd_spec.rb
require 'spec_helper'
require 'tempfile'
require 'net/ftp'
describe 'comportamiento' do
let (:conf) { 'pure-ftpd.conf' }
```
```sh @mdtoapp @silence
echo '' > pure-ftpd.users
pure-pw mkdb pure-ftpd.pdb -f pure-ftpd.users
```
-->
&lt;p>los ejercicios anteriores nos empiezan a dar una idea de como vamos a controlar
el servicio durante las pruebas, ahora vamos a proceder a configurar el servicio
en base los requerimientos.&lt;/p>
&lt;p>pure-ftpd.conf&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">Bind 127.0.0.1,8021
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#998;font-style:italic"># funcion de utilidad para reescribir archivos de configuracion durante las pruebas&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">substitute&lt;/span>(path, match, replace)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="color:#008080">File&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>read(path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">File&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>write(path, content&lt;span style="font-weight:bold">.&lt;/span>sub(match, replace))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>before { substitute(&lt;span style="color:#b84">&amp;#39;pure-ftpd.conf&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;/etc/pure-ftpd.pdb&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#34;&lt;/span>&lt;span style="color:#b84">#{&lt;/span>&lt;span style="color:#008080">Dir&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>pwd&lt;span style="color:#b84">}&lt;/span>&lt;span style="color:#b84">/pure-ftpd.pdb&amp;#34;&lt;/span>) }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="no-se-permite-ingreso-anonimo">no se permite ingreso anonimo&lt;/h4>
&lt;p>pure-ftpd.conf&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">NoAnonymous yes
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>it &lt;span style="color:#b84">&amp;#39;no se permite logeo anonimo&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server &lt;span style="font-weight:bold">=&lt;/span> ftpd_start(conf)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Net&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">FTP&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>open(&lt;span style="color:#b84">&amp;#34;127.0.0.1&amp;#34;&lt;/span>, &lt;span style="color:#b84">port&lt;/span>: server&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#b84">:port&lt;/span>&lt;span style="font-weight:bold">]&lt;/span>) &lt;span style="font-weight:bold">do&lt;/span> &lt;span style="font-weight:bold">|&lt;/span>ftp&lt;span style="font-weight:bold">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftp&lt;span style="font-weight:bold">.&lt;/span>login
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>to raise_error(&lt;span style="color:#008080">Net&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">FTPPermError&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftpd_stop(server)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="ingreso-solo-a-usuarios-autorizados">ingreso solo a usuarios autorizados&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pure-pw useradd foo -f pure-ftpd.users -u nobody -d /tmp/foo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;!--
```sh @mdtoapp file:pure-ftpd.users
foo:$argon2id$v=19$m=8192,t=1023,p=1$0Zf62x+GpxYoM2afDVEzPg$c5bZ16UnZJM1NAkCDIoUkrvhFwR4wsVSNp4qmyfnaFc:65534:65534::/tmp/./::::::::::::
```
-->
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sh" data-lang="sh">&lt;span style="display:flex;">&lt;span>pure-pw mkdb pure-ftpd.pdb -f pure-ftpd.users
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>pure-ftpd.conf&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-conf" data-lang="conf">PureDB /etc/pure-ftpd.pdb
&lt;/code>&lt;/pre>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>it &lt;span style="color:#b84">&amp;#39;ingreso a usuarios registrados&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server &lt;span style="font-weight:bold">=&lt;/span> ftpd_start(conf)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Net&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">FTP&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>open(&lt;span style="color:#b84">&amp;#34;127.0.0.1&amp;#34;&lt;/span>, &lt;span style="color:#b84">port&lt;/span>: server&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#b84">:port&lt;/span>&lt;span style="font-weight:bold">]&lt;/span>) &lt;span style="font-weight:bold">do&lt;/span> &lt;span style="font-weight:bold">|&lt;/span>ftp&lt;span style="font-weight:bold">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftp&lt;span style="font-weight:bold">.&lt;/span>login(&lt;span style="color:#b84">&amp;#39;foo&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;foo&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>not_to raise_error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftpd_stop(server)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>it &lt;span style="color:#b84">&amp;#39;subir archivos solo usuarios registrados&amp;#39;&lt;/span> &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> server &lt;span style="font-weight:bold">=&lt;/span> ftpd_start(conf)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> expect &lt;span style="font-weight:bold">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#008080">Net&lt;/span>&lt;span style="font-weight:bold">::&lt;/span>&lt;span style="color:#008080">FTP&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>open(&lt;span style="color:#b84">&amp;#34;127.0.0.1&amp;#34;&lt;/span>, &lt;span style="color:#b84">port&lt;/span>: server&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#b84">:port&lt;/span>&lt;span style="font-weight:bold">]&lt;/span>) &lt;span style="font-weight:bold">do&lt;/span> &lt;span style="font-weight:bold">|&lt;/span>ftp&lt;span style="font-weight:bold">|&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftp&lt;span style="font-weight:bold">.&lt;/span>login(&lt;span style="color:#b84">&amp;#39;foo&amp;#39;&lt;/span>, &lt;span style="color:#b84">&amp;#39;foo&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftp&lt;span style="font-weight:bold">.&lt;/span>puttextfile(__FILE__, &lt;span style="color:#b84">&amp;#39;demo.txt&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>not_to raise_error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">ensure&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ftpd_stop(server)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;!--
```ruby @mdtoapp file:spec/pure-ftpd_spec.rb
end
```
-->
&lt;!-- mdtoapp
```sh @mdtoapp @execute
pwd
cat pure-ftpd.conf
rspec
```
-->
&lt;p>Como vemos es posible usar las pruebas automatizadas como mecanismo para confirmar el comportamiento esperado
del servicio, algunos beneficios que tenemos son:&lt;/p>
&lt;ul>
&lt;li>pruebas automatizadas de confirmación.&lt;/li>
&lt;li>verificar que el servicio siempre inicie (cuantas veces nos ha pasado que hemos modificado incorrectamente un archivo de configuración
y luego no inicia?).&lt;/li>
&lt;li>nos incentiva a llevar los archivos de configuraciones en control de versiones.&lt;/li>
&lt;/ul>
&lt;h3 id="más-inventores-que-inventos">Más inventores que inventos&lt;/h3>
&lt;ul>
&lt;li>&lt;a href="https://developer.ibm.com/tutorials/d-bbd-guide-iac/">https://developer.ibm.com/tutorials/d-bbd-guide-iac/&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>¿Público y/o Públicado?</title><link>http://bit4bit.github.io/post/published_vs_public/</link><pubDate>Thu, 16 Feb 2023 21:09:34 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/post/published_vs_public/</guid><description>&lt;p>Cuando ré/factorizamos surgen nuevos elementos de los cuales
es importante determinar si son públicos y adicionalmente estan publicados,
si no hacemos esta aclaración podemos fácilmente crear referencias innecesarias,
veamos un ejemplo.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Billetera&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">initialize&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="font-weight:bold">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">ingreso&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos &lt;span style="font-weight:bold">&amp;lt;&amp;lt;&lt;/span> &lt;span style="font-weight:bold">[&lt;/span>concepto, valor&lt;span style="font-weight:bold">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">gasto&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos &lt;span style="font-weight:bold">&amp;lt;&amp;lt;&lt;/span> &lt;span style="font-weight:bold">[&lt;/span>concepto, &lt;span style="font-weight:bold">-&lt;/span>valor&lt;span style="font-weight:bold">]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">saldo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos&lt;span style="font-weight:bold">.&lt;/span>inject(&lt;span style="color:#099">0&lt;/span>) { &lt;span style="font-weight:bold">|&lt;/span>saldo, movimiento&lt;span style="font-weight:bold">|&lt;/span> saldo &lt;span style="font-weight:bold">+&lt;/span> movimiento&lt;span style="font-weight:bold">[&lt;/span>&lt;span style="color:#099">1&lt;/span>&lt;span style="font-weight:bold">]&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La clase &lt;code>Billetera&lt;/code> es &lt;code>pública&lt;/code> y &lt;code>publicada&lt;/code> (concepto común al asumir que cualquiera puede referenciar la clase y hacer uso de esta).&lt;/p>
&lt;p>Ahora realicemos una refactorización buscando la apertura del concepto movimiento.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ruby" data-lang="ruby">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Movimiento&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#458;font-weight:bold">self&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>&lt;span style="color:#900;font-weight:bold">debito&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">new&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#458;font-weight:bold">self&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>&lt;span style="color:#900;font-weight:bold">credito&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">new&lt;/span>(concepto, &lt;span style="font-weight:bold">-&lt;/span>valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">balancear&lt;/span>(saldo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> saldo &lt;span style="font-weight:bold">+&lt;/span> @valor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">private&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">initialize&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @concepto &lt;span style="font-weight:bold">=&lt;/span> concepto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @valor &lt;span style="font-weight:bold">=&lt;/span> valor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Cuenta&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">initialize&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos &lt;span style="font-weight:bold">=&lt;/span> &lt;span style="font-weight:bold">[]&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">registrar&lt;/span>(movimiento)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos &lt;span style="font-weight:bold">&amp;lt;&amp;lt;&lt;/span> movimiento
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">saldo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @movimientos&lt;span style="font-weight:bold">.&lt;/span>inject(&lt;span style="color:#099">0&lt;/span>) { &lt;span style="font-weight:bold">|&lt;/span>saldo, movimiento&lt;span style="font-weight:bold">|&lt;/span> movimiento&lt;span style="font-weight:bold">.&lt;/span>balancear(saldo) }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">class&lt;/span> &lt;span style="color:#458;font-weight:bold">Billetera&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">initialize&lt;/span>(cuenta)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @cuenta &lt;span style="font-weight:bold">=&lt;/span> cuenta
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">ingreso&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @cuenta&lt;span style="font-weight:bold">.&lt;/span>registrar &lt;span style="color:#008080">Movimiento&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>debito(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">gasto&lt;/span>(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @cuenta&lt;span style="font-weight:bold">.&lt;/span>registrar &lt;span style="color:#008080">Movimiento&lt;/span>&lt;span style="font-weight:bold">.&lt;/span>credito(concepto, valor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">def&lt;/span> &lt;span style="color:#900;font-weight:bold">saldo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @cuenta&lt;span style="font-weight:bold">.&lt;/span>saldo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold">end&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Las clases &lt;code>Billetera&lt;/code>, &lt;code>Cuenta&lt;/code>, &lt;code>Movimiento&lt;/code> son por defecto &lt;code>públicas&lt;/code> (este es el alcance general pero cada lenguaje tiene
un comportamiento diferente por ejemplo en C# el alcance por defecto es &lt;code>private&lt;/code>) ahora,
¿las nuevas clases (&lt;code>Cuenta&lt;/code>, &lt;code>Movimiento&lt;/code>) deben estar &lt;code>publicadas&lt;/code>, es decir podemos referenciarlas desde otras secciones del programa?
no, no están publicadas son elementos que surgieron de la refactorización y tienen alta cohesión
con el origen de la creación (&lt;code>Billetera&lt;/code>), posteriorme a la llegada de nuevos requerimientos se valoraría si deben ser &lt;code>publicadas&lt;/code>.&lt;/p>
&lt;p>Ahora ¿Cómo podemos hacer explicito este concepto? ni idea (si tengo ideas), es algo que queda a la creatividad del equipo ya
que &lt;code>depende de&lt;/code>.&lt;/p></description></item><item><title>About</title><link>http://bit4bit.github.io/about/</link><pubDate>Wed, 15 Feb 2023 21:38:19 -0500</pubDate><author>bit4bit@riseup.net (Jovany Leandro G.C)</author><guid>http://bit4bit.github.io/about/</guid><description>&lt;p>&lt;img src="http://bit4bit.github.io/bit4bit.png" alt="bit4bit">&lt;/p>
&lt;p>Profesional en el desarrollo de software con más de 10 años de experiencia en areas diversas de IT.&lt;/p>
&lt;p>&lt;strong>enseña lo que sabes aprende lo que no&lt;/strong>&lt;/p>
&lt;p>Consultoría en:&lt;/p>
&lt;ul>
&lt;li>Implementaciones de infraestructura altamente escalable de servicios VOIP (Asterisk, Freeswitch y Kamailio).&lt;/li>
&lt;li>Implementaciones de infraestructura on-premise,serveless o cloud en AWS.&lt;/li>
&lt;li>Implementaciones de infraestructura como código.&lt;/li>
&lt;li>Capacitación en desarrollo de software sólido.&lt;/li>
&lt;li>Desarrollo de software en Elixir, Go, Ruby, Python, PHP u otros según cliente.&lt;/li>
&lt;li>Adaptación y capacitación software contable &lt;a href="https://www.tryton.org">Tryton ERP&lt;/a>.&lt;/li>
&lt;li>Soluciones integradoras telefonía sobre IP (VOIP) al negocio.&lt;/li>
&lt;li>Operador de sistemas operativo GNU/Linux u BSD.&lt;/li>
&lt;li>Operador de servicios IT en la nube AWS &lt;a href="https://www.credly.com/badges/2b51e642-af51-4698-9900-7d2dc0dbd3e8/public_url">Certificación&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="mailto:contacto@bit4bit.in">Contactar contacto@bit4bit.in&lt;/a>&lt;/p>
&lt;p>Código Libre.&lt;/p>
&lt;p>&lt;a href="https://github.com/bit4bit/">GITHUB&lt;/a>
&lt;a href="https://chiselapp.com/user/bit4bit/">CHISELAPP&lt;/a>&lt;/p>
&lt;p>Ubicación - Colombia.&lt;/p></description></item></channel></rss>