<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Mario's Blog]]></title><description><![CDATA[Progetti innocui talvolta inquietanti]]></description><link>https://mariopiccinelli.it/blog/</link><image><url>https://mariopiccinelli.it/blog/favicon.png</url><title>Mario&apos;s Blog</title><link>https://mariopiccinelli.it/blog/</link></image><generator>Ghost 3.37</generator><lastBuildDate>Fri, 04 Jul 2025 14:22:33 GMT</lastBuildDate><atom:link href="https://mariopiccinelli.it/blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Esperimenti di networking con Python socket]]></title><description><![CDATA[Esperimenti di networking con Python socket]]></description><link>https://mariopiccinelli.it/blog/python-networking/</link><guid isPermaLink="false">660c767d1b6a0200014ab55b</guid><category><![CDATA[python]]></category><category><![CDATA[game development]]></category><category><![CDATA[networking]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Fri, 03 May 2024 22:28:31 GMT</pubDate><content:encoded><![CDATA[<p>Python supporta nativamente i socket, ovvero la possibilità di instaurare una connessione diretta tra due applicativi mediante connessione diretta attraverso il protocollo IP. Questo consente di realizzare in maniera (relativamente) semplice applicazioni in grado di comunicare in maniera continua tra loro attraverso la rete.</p><p>Questo si presta particolarmente bene per la realizzazione di giochi multiplayer, in cui la comunicazione dovrebbe essere continua e con la minor latenza possibile (il che esclude, per esempio, la comunicazione mediante http). </p><p>La tipica architettura prevede l'esistenza di un server centrale a cui i client si connettono: i client trasmettono ciascuno il proprio stato al server (ad esempio, la posizione del giocatore), e il server lo reinvia a tutti gli altri, che in questo modo possono restare sincronizzati.</p><p>In questo piccolo esperimento voglio provare a definire un semplice esempio di architettura client/server. Il server resta in ascolto della richiesta di connessione dei client, e una volta instaurata gestisce ciascuna connessione attraverso un diverso thread.</p><h2 id="il-server">Il Server</h2><p>Iniziamo con il server, che sarà tutto racchiuso in una classe. Per prima cosa vogliamo definire il metodo di inizializzazione della classe, che si occuperà di inizializzare il socket. Già che ci siamo definiamo come proprietà della classe un paio di dizionari per contenere i thread che andremo a creare e i relativi dati scambiati da ciascun client. Impostiamo con settimeout un timeout di connessione di mezzo secondo (il perchè è spiegato più avanti).</p><pre><code>class ThreadedServer(object):

    clients_data = {}
    clients_list = {}

    def __init__(self, host, port):

        self.host = host
        self.port = port
        
        print(f"Apertura socket su porta {self.port}...")
        
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.settimeout(0.5)
        self.sock.bind((self.host, self.port))</code></pre><p>Il metodo centrale di questa classe sarà quello che andrà ad ascoltare le richieste di connessione in ingresso e le passerà a un thread individuale. </p><pre><code>    def listen(self):
                
        self.sock.listen(5)

        listening:bool = True
        print("attesa connessione...")

        while listening:

            try:
                
                client, address = self.sock.accept()
                client.settimeout(60)
                
                print("client connesso!")
                
                new_client_id = str(uuid.uuid4())
                new_thread = threading.Thread(
                    target = self.manage_client_connection, 
                    args = (client, address, new_client_id), 
                    daemon=True
                )
                new_thread.start()
                self.clients_list[new_client_id] = new_thread

            except socket.timeout:
                # print("Timeout")
                pass
            except KeyboardInterrupt:
                listening = False

        print("Thread principale terminato!")</code></pre><p>Un paio di osservazioni:</p><ul><li>Il metodo <em>listen </em>inizializza il socket in ascolto. Il parametro è il numero massimo di connessioni contemporanee supportate (in questo caso 5).</li><li>Il ciclo di attesa è un loop infinito. Usiamo una variabile booleana (<em>listening</em>) per sorvegliarne l'esecuzione.</li><li>Il metodo <em>accept </em>è quello che effettivamente blocca il programma in attesa di connessione. Durante l'inizializzazione abbiamo impostato un timeout sulla fase di connessione (0,5 secondi), che ci consente di far avanzare il loop (sollevando una TimeoutException) anche in assenza di connessioni: in questo modo possiamo sorvegliare la nascita di un'eventuale eccezione di tipo KeyboardError. Questa eccezione avviene quando l'utente preme Control+C, e in questo caso mettiamo a False la variabile di ciclo e possiamo così chiudere il programma; in assenza di questo stratagemma il programma, in assenza di connessioni, rimarrebbe in eterno fermo in attesa e non sarebbe possibile terminarlo da tastiera. Naturalmente, le eccezioni dovute a timeout sono previste e quindi semplicemente ignorate: non è il metodo più efficiente o elegante ma ci accontentiamo.</li><li>Quando viene accettata una connessione, viene generato un id casuale (per identificarla in maniera univoca) con il metodo uuid4. Viene poi costruito e avviato un thread attorno al metodo manage_client_connection, a cui viene passata la nuova connessione. A questo punto il programma principale può dimenticarsi di questa connessione, che sarà d'ora in avanti gestita dal thread, e rimettersi in ascolto della successiva.</li></ul><p>A questo punto non ci resta che realizzare il metodo manage_client_connection, che fa il grosso del lavoro per ciascuno dei socket.</p><pre><code>    def manage_client_connection(self, client:socket.socket, address, client_id:str):

        size = 1024

        # Handshake
        data = msgpack.unpackb(client.recv(size))
        print("Received handshake: ", data)
        handshake_reply = {
            "id": client_id
        }
        client.send(msgpack.packb(handshake_reply))

        while True:

            try:
                
                data = client.recv(size)

                if data:
                    
                    self.clients_data[client_id] = msgpack.unpackb(data)

                    client.send(msgpack.packb(self.clients_data))

                else:
                    raise Exception('Client disconnected')
                
            except Exception as e:

                print(e)
                client.close()
                del self.clients_data[client_id]
                return False</code></pre><p>La comunicazione attraverso il socket è svolta usando la libreria <a href="https://msgpack.org/">msgpack</a>, che consente di convertire in un formato binario dei flussi JSON. Questo fa si che la comunicazione sia estremamente più compatta e quindi efficiente (comunicare direttamente mediante stringhe JSON è ESTREMAMENTE poco efficiente).</p><p>Quando la connessione viene instaurata viene effettuato un handshake: si suppone che il client appena connesso ci mandi un pacchetto dati (che al momento è ignorato, ma è utile predisporlo per il futuro, un domani potrebbe contenere per esempio le credenziali dell'utente del client, o la versione, eccetera) a cui noi rispondiamo con un pacchetto dati contenente l'ID che abbiamo assegnato in precedenza a questa specifica connessione.</p><p>A questo punto inizia il loop infinito: il server resta in attesa di dati dal client, e quando li riceve li salva nel dizionario definito nella classe principale, indicizzandolo con l'uuid della connessione. Una volta ricevuto l'aggiornamento poi mandiamo al client i dati di TUTTI i client, in questo modo il client è sincronizzato con gli stati di tutti gli altri client connessi; ad esempio, possiamo immaginare che, nel caso di un gioco multiplayer, ogni client mandi al server la posizione (x,y) del proprio giocatore, e in cambio riceva le posizioni di tutti gli altri per poterli disegnare correttamente.</p><p>La classe del server è quindi terminata, e la possiamo avviare semplicemente così:</p><pre><code>if __name__ == "__main__":
    ThreadedServer('', 12345).listen()</code></pre><h2 id="il-client">Il Client</h2><p>Il client è un pò più complesso perchè deve contenere, oltre alla parte di networking, tutta la logica di gioco. In questo esempio ci focalizziamo solo sulla componente di networking, e il prototipo di gioco attorno lo lasciamo al prossimo post. Dentro il client quindi ci sarà una classe che si occuperà di gestire la comunicazione con il server.</p><p>La classe definisce una manciata di attributi, commentati qui sotto, e un metodo di inizializzazione che semplicemente imposta host e porta del server a cui connettersi.</p><pre><code>class NetworkConnection:
   
  client_socket = None
  thread = None

  # Il client deve tentare di restare connesso
  _stay_connected = False

  # Stato connessione
  _is_connected = False

  # ID assegnato durante handshake
  player_id = None

  # Ultimo payload ricevuto dal server
  received_data = {}


  def __init__(self, host, port):
    self.host = host
    self.port = port
    self._stay_connected = False</code></pre><p>Definiamo un metodo "connect", che verrà chiamato per attivare la connessione vera e propria, e un metodo "disconnect" che si spiega da solo. Il metodo connect si occupa semplicemente di creare un thread attorno al metodo "_worker", che gestisce le attività legate alla connessione in maniera autonoma. Viene inoltre garantito che il thread sia unico, se esiste già il "connect" lo distrugge prima di ricrearlo.</p><pre><code>  def connect(self):
  
    self._is_connected = False
    
    # Si assicura che eventuale thread esistente sia chiuso
    if (self.thread != None and self.thread.is_alive()):
      self._stay_connected = False
      while(self.thread.is_alive()):
        time.sleep(.1)
  
    self._stay_connected = True
  
    self.thread = threading.Thread(target=self._worker, args=(), daemon=True)
    self.thread.start()
  
  
  def disconnect(self):
    self._stay_connected = False
    
  def is_connected(self):
    return self._is_connected</code></pre><p>A questo punto dobbiamo solo definire il cuore della classe, il metodo "_worker". Questo metodo, che come abbiamo visto è eseguito in un thread, è costruito su due loop infiniti entrambi controllati dalla variabile "stay_connected". </p><p>Il loop interno è in ascolto sul canale di comunicazione, e ogni volta che riceve dati li salva nella variabile d'istanza self.received_data (accessibile dal resto del programma).</p><pre><code>    while(self._stay_connected):
      
      ...
      
      while(self._stay_connected):

        data = self.client_socket.recv(1024)

        try:
          self.received_data = msgpack.unpackb(data)
        except Exception:
          _logger.exception("Errore durante socket recv")
          break
</code></pre><p>Il loop esterno, invece, entra in gioco all'avvio del thread e in tutte le eventuali situazioni in cui in loop interno dovesse morire a causa di errori di connessione o simili. Questo blocco si occupa di effettuare la connessione vera e propria al socket ed eseguire l'handshake (come già visto dal punto di vista del server).</p><pre><code>    while(self._stay_connected):
      
      ...
      self._connect_socket()
      ...
      
      ...
      self._handshake()
      ...
      
      self._is_connected = True

      while(self._stay_connected):
        ...</code></pre><p>In ogni caso, entrambi i loop terminano quando la variabile viene messa a un valore False. Mettendo attorno un pò di gestione di errori e l'implementazione dei metodi di connessione e handshake otteniamo questo codice:</p><pre><code>  def _connect_socket(self):
    self.client_socket = socket.socket()
    self.client_socket.connect((self.host, self.port))


  def _handshake(self):

    _logger.debug("Effettuazione handshake...")

    handshake_payload = {
      "text": "hi"
    }
    
    self.client_socket.send(msgpack.packb(handshake_payload))
    handshake_recv = msgpack.unpackb(self.client_socket.recv(1024))
    self.player_id = handshake_recv['id']
    
    _logger.debug(f"Server assigned me this id: {self.player_id}")


  # Thread principale
  def _worker(self):

    while(self._stay_connected):
      
      try:
        self._connect_socket()
      except Exception:
        _logger.exception("Connessione fallita!")
        time.sleep(1)
        self.client_socket = None
        self.thread = None
        return

      try:
        self._handshake()
      except Exception:
        _logger.exception("Handshake fallito!")
        time.sleep(1)
        self.client_socket = None
        self.thread = None
        return
      
      self._is_connected = True

      while(self._stay_connected):

        data = self.client_socket.recv(1024)

        try:
          self.received_data = msgpack.unpackb(data)
        except Exception:
          _logger.exception("Errore durante socket recv")
          break

    try:
      self.client_socket.close()
    except Exception as _:
      pass
    
    self._is_connected = False

    self.client_socket = None
    self.thread = None

    _logger.debug("Client thread terminato")</code></pre><p>Per completare la classe, creiamo anche un metodo che può essere richiamato dal programma principale per inviare dati (finora abbiamo visto solo la ricezione).</p><pre><code>  def send(self, data:dict):

    if (self.thread == None or not self.thread.is_alive()):
      _logger.warning("send while thread not alive, reconnecting...")
      self.connect()

    if (self.client_socket):
      try:
        packed_data = msgpack.packb(data)
        self.client_socket.send(packed_data)
      except Exception:
        # _logger.exception("Impossibile trasmettere")
        pass</code></pre><h2 id="il-test">Il Test</h2><p>A questo punto dovremmo avere tutto pronto per testare. Costruiamo un semplicissimo script che:</p><ul><li>inizializza la classe ed effettua la connessione</li><li>chiede all'utente di inserire un messaggio</li><li>invia un messaggio</li><li>attende ulteriore conferma dall'utente (simula delay tra invio e ricezione)</li><li>stampa client.received_data, in cui mi aspetto di trovare il messaggio che ho inviato in precedenza.</li></ul><pre><code>client = NetworkConnection(socket.gethostname(), 12345)
client.connect()

messaggio = input("Inserire messaggio:")

client.send(messaggio)

input("...")

print(client.received_data)</code></pre><p>Per prima cosa avviamo il server:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/05/image-4.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/05/image-4.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2024/05/image-4.png 1000w, https://mariopiccinelli.it/blog/content/images/2024/05/image-4.png 1102w" sizes="(min-width: 720px) 720px"></figure><p>Poi avviamo il client. Questo si connette immediatamente al server, effettua l'handshake e riceve un id univoco:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/05/image-5.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/05/image-5.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2024/05/image-5.png 1000w, https://mariopiccinelli.it/blog/content/images/2024/05/image-5.png 1111w" sizes="(min-width: 720px) 720px"></figure><p>Inseriamo un messaggio qualunque nel client:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/05/image-6.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/05/image-6.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2024/05/image-6.png 1000w, https://mariopiccinelli.it/blog/content/images/2024/05/image-6.png 1102w" sizes="(min-width: 720px) 720px"></figure><p>Premiamo ancora invio per superare il secondo input e vediamo che il nostro client ha ricevuto dal server il messaggio precedentemente inviato:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/05/image-7.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/05/image-7.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2024/05/image-7.png 1000w, https://mariopiccinelli.it/blog/content/images/2024/05/image-7.png 1112w" sizes="(min-width: 720px) 720px"></figure><p>Il primo client è terminato e la sua connessione è quindi stata chiusa (come mostra l'errore nella finestra del server). </p><h2 id="conclusione">Conclusione</h2><p>Il codice sopra è estremamente grezzo, con scarsa gestione degli errori e dei casi limite, e funzionicchia dignitosamente ma senza pretese. Diciamo che è un buon esempio accademico. Il prossimo passo sarà provare a inserirlo in un programma Pygame per provare a metterlo alla prova con qualcosa di più visivo. </p><p>Grazie per l'attenzione e alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[9-slice scaling in Pygame]]></title><description><![CDATA[9-slice scaling in Pygame. Come scalare immagini 2d senza che venga fuori una schifezza.]]></description><link>https://mariopiccinelli.it/blog/9-slice-scaling-in-pygame/</link><guid isPermaLink="false">660474631b6a0200014ab4b1</guid><category><![CDATA[python]]></category><category><![CDATA[pygame]]></category><category><![CDATA[graphics]]></category><category><![CDATA[9-slice scaling]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Wed, 27 Mar 2024 20:12:56 GMT</pubDate><media:content url="https://mariopiccinelli.it/blog/content/images/2024/03/bc0cbf61-e364-45c7-8563-2121ea7418e5.png" medium="image"/><content:encoded><![CDATA[<img src="https://mariopiccinelli.it/blog/content/images/2024/03/bc0cbf61-e364-45c7-8563-2121ea7418e5.png" alt="9-slice scaling in Pygame"><p>Quando si lavora con immagini 2D è frequente la necessità di modificarne le dimensioni per adattarle all'ambiente in cui sono posizionate. Però spesso non è possibile procedere con una semplice scalatura, poichè si introdurrebbero delle distorsioni o dei problemi legati al cambio di risoluzione, specialmente quando le proporzioni dell'immagine iniziale non sono rispettate.</p><p>Un caso tipico è il ridimensionamento di pannelli della UI di un videogioco. I pannelli di sfondo sono tipicamente commissionati a un artista e realizzati in una dimensione fissa, e poi spetta al programmatore adattarli, magari in maniera dinamica. Ma questo adattamento deve evitare distorsioni. Vediamo per esempio cosa succede se si ridimensiona indiscriminatamente un pannello con una cornice.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/03/image.png" class="kg-image" alt="9-slice scaling in Pygame"></figure><p>A sinistra l'immagine originale, a destra l'immagine con larghezza triplicata. Si nota subito che è una schifezza: la cornice a destra e sinistra è stata scalata anch'essa, con i risultati che si vedono, e gli angoli sono deformati a causa del cambio di proporzione.</p><p>Il meccanismo che si utilizza in queste situazioni è chiamato 9-slice scaling. Si divide l'immagine in 9 settori, isolando il centro dell'immagine, i bordi e gli angoli, e ciascuna parte viene deformata in maniera diversa: gli angoli non vengono alterati e i lati vengono modificati solo lungo il loro lato maggiore. Maggiori dettagli sull'apposito <a href="https://en.wikipedia.org/wiki/9-slice_scaling">articolo su Wikipedia</a>, da cui ho anche gentilmente rubato la seguente immagine.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/03/image-1.png" class="kg-image" alt="9-slice scaling in Pygame" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/03/image-1.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2024/03/image-1.png 1000w, https://mariopiccinelli.it/blog/content/images/2024/03/image-1.png 1280w" sizes="(min-width: 720px) 720px"></figure><p>Probabilmente da qualche parte tra le librerie di Pygame esiste già un modo per farlo, ma ho voluto provare a farlo da zero come esercizio. </p><p>Supponiamo di avere in input alla nostra funzione l'immagine originale, le dimensioni desiderate dell'immagine da produrre e la larghezza dei margini:</p><pre><code>def scale_image(
  image:pygame.Surface, 
  width:float, 
  height:float, 
  margin:int=10
) -&gt; pygame.Surface:</code></pre><p>Prendiamo nota delle dimensioni iniziali (che ci serviranno per fare i calcoli):</p><pre><code>orig_width, orig_height = image.get_size()</code></pre><p>Creiamo un pannello vuoto con le dimensioni desiderate.</p><pre><code>scaled_image = pygame.Surface((width, height), pygame.SRCALPHA)</code></pre><p>Copiamo i quattro angoli; non hanno bisogno di essere deformati, quindi li possiamo semplicemente copiare nella posizione giusta. E' sufficiente usare il comando blit indicando come primo parametro l'immagine di partenza, come secondo parametro la posizione all'interno della nuova immagine, e come ultimo parametro le coordinate che delimitano l'area da copiare.</p><pre><code># angolo top left
scaled_image.blit(
  image, 
  (0, 0), 
  (0, 0, margin, margin)
)

# ...altri angoli</code></pre><p>Copiamo i quattro lati. Per ciascuno, estraiamo l'immagine parziale da quella originale (con il comando <em>subsurface</em>), la deformiamo nella direzione desiderata (<em>transform.smoothscale</em>) e la copiamo, come già visto per gli angoli, nell'immagine finale:</p><pre><code># top
image_top = image.subsurface(
  pygame.Rect(margin, 0, orig_width-margin*2, margin)
)
scaled_image.blit(
  pygame.transform.smoothscale(
    image_top, 
    (width-margin*2, margin)
  ), 
  (margin, 0)
)

# ...altri lati</code></pre><p>E infine copiamo il centro. Il procedimento è analogo a quanto visto per i lati ma la deformazione è in entrambe le direzioni:</p><pre><code># centro
image_center = image.subsurface(
  pygame.Rect(margin, margin, orig_width-margin*2, orig_height-margin*2)
)

scaled_image.blit(
  pygame.transform.smoothscale(
    image_center, 
    (width-margin*2, height-margin*2)
  ), 
  (margin, margin)
)</code></pre><p>Voilà! Il codice completo è qualcosa del genere:</p><pre><code>import pygame

def scale_image(image:pygame.Surface, width:float, height:float, margin:int=10) -&gt; pygame.Surface:

  orig_width, orig_height = image.get_size()

  scaled_image = pygame.Surface((width, height), pygame.SRCALPHA)
  
  # angolo top left
  scaled_image.blit(image, (0, 0), (0, 0, margin, margin))

  # angolo top right
  scaled_image.blit(image, (width-margin, 0), (orig_width-margin, 0, orig_width-margin, margin))

  # angolo bottom left
  scaled_image.blit(image, (0, height-margin), (0, orig_height-margin, margin, orig_height))

  # angolo bottom right
  scaled_image.blit(image, (width-margin, height-margin), (orig_width-margin, orig_height-margin, orig_width-margin, orig_height))

  # centro (scalato)
  image_center = image.subsurface(pygame.Rect(margin, margin, orig_width-margin*2, orig_height-margin*2))
  scaled_image.blit(pygame.transform.smoothscale(image_center, (width-margin*2, height-margin*2)), (margin, margin))

  # top (scalato)
  image_top = image.subsurface(pygame.Rect(margin, 0, orig_width-margin*2, margin))
  scaled_image.blit(pygame.transform.smoothscale(image_top, (width-margin*2, margin)), (margin, 0))

  # bottom (scalato)
  image_bottom = image.subsurface(pygame.Rect(margin, orig_height - margin, orig_width - margin*2, margin))
  scaled_image.blit(pygame.transform.smoothscale(image_bottom, (width-margin*2, margin)), (margin, height - margin))

  # left (scalato)
  image_left = image.subsurface(pygame.Rect(0, margin, margin, orig_height-margin*2))
  scaled_image.blit(pygame.transform.smoothscale(image_left, (margin, height-2*margin)), (0, margin))

  # right (scalato)
  image_right = image.subsurface(pygame.Rect(orig_width-margin, margin, margin, orig_height-margin*2))
  scaled_image.blit(pygame.transform.smoothscale(image_right, (margin, height-2*margin)), (width-margin, margin))

  return scaled_image</code></pre><p>Il risultato è decisamente meglio, specialmente se lo confrontiamo con quello che abbiamo ottenuto prima:</p><h3 id="9-slice-scaling">9-slice scaling</h3><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/03/image-2.png" class="kg-image" alt="9-slice scaling in Pygame"></figure><h3 id="scaling-grezzo">scaling grezzo</h3><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/03/image.png" class="kg-image" alt="9-slice scaling in Pygame"></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Esperimenti con Pygame]]></title><description><![CDATA[Primi esperimenti con Pygame]]></description><link>https://mariopiccinelli.it/blog/esperimenti-con-pygame/</link><guid isPermaLink="false">65de035f1b6a0200014ab39c</guid><category><![CDATA[python]]></category><category><![CDATA[pygame]]></category><category><![CDATA[game development]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Tue, 27 Feb 2024 22:13:30 GMT</pubDate><media:content url="https://mariopiccinelli.it/blog/content/images/2024/02/mario_screenshot.png" medium="image"/><content:encoded><![CDATA[<img src="https://mariopiccinelli.it/blog/content/images/2024/02/mario_screenshot.png" alt="Esperimenti con Pygame"><p>Recentemente ho iniziato a fare qualche esperimento con Pygame e ho scoperto che è divertente. Quindi proverò a raccontare che roba è e come iniziare, nel caso qualcuno voglia provare a darci un'occhiata.</p><p><a href="https://github.com/pygame-community/pygame-ce">Pygame</a> è una libreria open source per python per la realizzazione di semplici videogiochi e simili applicazioni multimediali in 2D. Il che vuol dire che fornisce gli strumenti necessari per disegnare immagini/animazioni a schermo, gestire gli input tastiera/mouse, gestire le collisioni tra sagome 2D, audio eccetera. In aggiunta a qualunque altra cosa si possa fare normalmente con Python e le sue millemila librerie, ovviamente. </p><p>Può andare a sostituire, per progetti semplici, un game engine più sofisticato quale Godot o Unity, a patto di accettare il fatto che molte delle funzionalità che si danno per scontate non sono disponibili e vanno costruite (visto che non c'è una IDE, è tutto realizzato a codice). </p><p>Ma è proprio lì che sta il divertimento, no? La risposta è no, naturalmente, a meno che l'obiettivo non sia effettivamente imparare come si costruiscono queste funzionalità. In tal caso va benissimo.</p><p><strong>Nota</strong>: esistono due versioni di Pygame, quella originale e Pygame Community Edition (CE). Quest'ultima è un fork che si è sviluppato qualche tempo fa dal codice originale dopo che lo sviluppo di quest'ultimo è andato in stallo, ed è quella consigliabile per il semplice motivo che è quella meglio mantenuta. La sintassi è identica, il modulo python si chiama sempre "pygame", l'unica differenza sta nel comando pip per installare la libreria:</p><pre><code>pip install pygame-ce</code></pre><p>Un semplice programma basato su Pygame è diviso in due parti: l'inizializzazione (in cui vengono predisposti gli oggetti) e il Game Loop.</p><p>Il <strong>Game Loop</strong> è la parte più caratteristica di un programma del genere e rappresenta sostanzialmente l'azione di aggiornare la situazione del gioco e ridisegnare il contenuto della finestra, con una frequenza prestabilita (30/60 frame per secondo). Le parti principali del game loop sono:</p><ul><li><strong>Gestione eventi</strong>: vengono gestiti gli eventi asincroni che si sono generati nel frame precedente (per esempio, un movimento del mouse, la pressione di un tasto, lo scadere di un timer...).</li><li><strong>Pulizia</strong>: il contenuto della finestra viene azzerato (tutto sbiancato, o colorato con un colore o immagine fissa di sfondo).</li><li><strong>Update</strong>: gli oggetti del gioco vengono aggiornati secondo le proprie logiche (gli oggetti che stanno cadendo si muovono verso il basso, i proiettili si spostano lungo la loro traiettoria, un conto alla rovescia si riduce, eccetera).</li><li><strong>Disegno</strong>: tutti gli oggetti che devono apparire a schermo vengono disegnati sulla finestra stessa.</li></ul><p>E così via, il ciclo si ripete all'infinito fino a quando il gioco non termina, a una frequenza sufficiente a dare l'illusione che queste singole schermate statiche siano in realtà una animazione. Facile, no?</p><p>Proviamo a disegnare qualcosa</p><p>Per prima cosa apriamo un terminale, ci spostiamo in una cartella vuota creata allo scopo e predisponiamo un virtual environment Python. E poi ci installiamo pygame.</p><pre><code>python -m venv .venv
.\.venv\Scripts\activate
pip install pygame-ce</code></pre><p>Prima di partire procuriamoci una immagine qualunque, magari un bel png con sfondo trasparente cercato a caso su internet. Magari qualcosa del genere:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/02/mario.png" class="kg-image" alt="Esperimenti con Pygame"></figure><p>A questo punto iniziamo a scrivere il nostro programma in un nuovo file, chiamato per esempio "main.py". Iniziamo a caricare la libreria principale, e importiamo direttamente alcune costanti da pygame.locals (ci faranno comodo quando andremo a catturare gli eventi da tastiera):</p><pre><code>import pygame
from pygame.locals import *</code></pre><p>Inizializziamo pygame, e creiamo la finestra di dimensioni 800x600. E già che ci siamo scriviamo "Hello World" nella barra del titolo, come vuole la tradizione.</p><pre><code>pygame.init()
screen = pygame.display.set_mode((800, 600), 0 , 32)
pygame.display.set_caption('Hello World')</code></pre><p>Predisponiamo l'immagine per l'uso successivo nel game loop. L'immagine viene caricata in una variabile mediante il metodo image.load, cui poi segue il metodo convert_alpha che la converte in una immagine utilizzabile da pygame mantenendo intatta la trasparenza. Estraiamo poi il rettangolo di questa immagine, che altro non è che una rappresentazione delle dimensioni e della posizione dell'immagine. Il rettangolo è uno dei concetti fondamentali in pygame perchè è usato per posizionare l'oggetto quando andremo a disegnarlo (come vedremo poi).</p><pre><code>mario = pygame.image.load("mario.png").convert_alpha()
mario_rect = mario.get_rect()</code></pre><p>A questo punto iniziamo il game loop:</p><pre><code>running = True
while running:</code></pre><p>Dentro il game loop per prima cosa ci occupiamo della gestione degli eventi. Gli eventi scattati durante il frame corrente sono restituiti dal metodo event.get(), e noi li analizziamo uno a uno alla ricerca di eventi interessanti. In questo caso andiamo a cercare l'evento QUIT (la chiusura della finestra) o l'evento KEYUP, key ESCAPE, che rappresenta la pressione del tasto ESC. In entrambi questi casi vogliamo semplicemente uscire dal programma, e per questo è sufficiente mettere False nella variabile del game loop "running".</p><pre><code>	for event in pygame.event.get():
		if event.type == KEYUP and event.key == K_ESCAPE:
			running = False
		if event.type == QUIT:
			running = False</code></pre><p>A questo punto c'è la pulizia. In questo caso vogliamo semplicemente riempire lo schermo con un colore uniforme, per esempio un verde (lo definiamo mediante colori RGB 0-255).</p><pre><code>	GREEN = (76, 181, 71)
	screen.fill(GREEN)</code></pre><p>Ora è il momento di disegnare la nostra immagine. Per disegnare una immagine si usa il metodo blit, e si passa l'immagine stessa e il rettangolo di riferimento di cui abbiamo parlato poco fa.</p><pre><code>	screen.blit(mario, mario_rect)</code></pre><p>A questo punto è sufficiente forzare il refresh dello schermo. Pygame di default usa il double buffering, quindi quello che abbiamo disegnato in questo frame non è visibile nella finestra ma è solo in memoria. Per concludere il disegno della schermata non ci resta che chiamare l'update.</p><pre><code>	pygame.display.update()</code></pre><p>Il programma è terminato, e non ci resta che lanciarlo:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/02/image.png" class="kg-image" alt="Esperimenti con Pygame" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2024/02/image.png 600w, https://mariopiccinelli.it/blog/content/images/2024/02/image.png 809w" sizes="(min-width: 720px) 720px"></figure><p>Il programma completo fino a questo punto:</p><pre><code>import pygame
from pygame.locals import *

GREEN = (76, 181, 71)

pygame.init()

screen = pygame.display.set_mode((800, 600), 0 , 32)
pygame.display.set_caption('Hello World')

mario = pygame.image.load("mario.png").convert_alpha()
mario_rect = mario.get_rect()

running = True
while running:

	for event in pygame.event.get():
		if event.type == KEYUP and event.key == K_ESCAPE:
			running = False
		if event.type == QUIT:
			running = False
			
	screen.fill(GREEN)

	screen.blit(mario, mario_rect)

	pygame.display.update()</code></pre><h2 id="aggiungiamo-un-p-di-movimento">Aggiungiamo un pò di movimento</h2><p>Molto, molto, MOLTO bello ma non particolarmente interessante. Sarebbe divertente quantomeno poter muovere l'immagine usando le frecce sulla tastiera, no? Per fare questo dobbiamo prima fare una digressione sul concetto di fps. </p><p>Come abbiamo visto, il game loop, che si occupa di disegnare i frame, è un loop infinito che gira alla massima velocità permessa dal processore. Questo però non è ideale, perchè significa che il gioco gira più velocemente su pc veloci e più lentamente su pc lenti, mentre noi vorremmo che la velocità fosse approssimativamente identica per tutti i giocatori, quindi con lo stesso numero di fps (frame per second). </p><p>Per fare questo Pygame ci mette a disposizione un metodo da chiamare all'interno del game loop che rallenta ogni ciclo in modo tale da tentare di rispettare il numero massimo di fps che gli abbiamo indicato. Il codice è questo:</p><pre><code># fuori dal game loop
clock:pygame.time.Clock = pygame.time.Clock()

    # dentro il game loop
    dt = clock.tick(60)</code></pre><p>Il metodo tick rallenta il loop per garantire un fps massimo di, in questo caso, 60. Il metodo tick ci restituisce un valore numerico, che qui ho chiamato dt, che rappresenta il numero di millisecondi passati dal frame precedente. </p><p>Questo numero è fondamentale perchè ci permette di fare i nostri calcoli indipendentemente dalla frequenza di aggiornamento. Non dimentichiamo che noi abbiamo fissato una frequenza massima, ma la vera frequenza potrebbe essere inferiore. Per esempio, nei calcoli per il movimento di un personaggio, vogliamo che la velocità percepita dal giocatore sia la medesima anche se la frequenza rallenta, quindi possiamo usare questo valore per riproporzionare i movimenti.</p><p>E ora passiamo ai movimenti. La gestione dei movimenti in realtà è semplicissima, basta aggiungere questo spezzone di codice prima della chiamata a blit.</p><pre><code>	keys=pygame.key.get_pressed()
	speed = 400
	if keys[K_LEFT]:
		mario_rect.move_ip(-1 * speed * dt / 1000, 0)
	if keys[K_RIGHT]:
		mario_rect.move_ip(1 * speed * dt / 1000, 0)
	if keys[K_UP]:
		mario_rect.move_ip(0, -1 * speed * dt / 1000)
	if keys[K_DOWN]:
		mario_rect.move_ip(0, 1 * speed * dt / 1000)	</code></pre><p>Utilizziamo il metodo key_get_pressed per ottenere la lista dei pulsanti premuti in quel momento, e poi cerchiamo i pulsanti che ci interessano (le frecce). In caso di match, modifichiamo la posizione del rect di riferimento usanto il metodo move_ip e passando la distanza su assi x e y in pixel. La distanza è calcolata come prodotto di speed (400) per dt/1000, quindi è di 400 pixel al secondo indipendentemente dalla frequenza di refresh.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2024/02/2024-02-27-22-41-57--2--1.gif" class="kg-image" alt="Esperimenti con Pygame"></figure><p>Il programma completo è questo:</p><pre><code>import pygame
from pygame.locals import *

GREEN = (76, 181, 71)
speed = 400

pygame.init()

screen = pygame.display.set_mode((800, 600), 0 , 32)
pygame.display.set_caption('Hello World')

mario = pygame.image.load("mario.png").convert_alpha()
mario_rect = mario.get_rect()

clock:pygame.time.Clock = pygame.time.Clock()

running = True
while running:

	dt = clock.tick(60)

	for event in pygame.event.get():
		if event.type == KEYUP and event.key == K_ESCAPE:
			running = False
		if event.type == QUIT:
			running = False
			
	screen.fill(GREEN)

	keys=pygame.key.get_pressed()
	if keys[K_LEFT]:
		mario_rect.move_ip(-1 * speed * dt / 1000, 0)
	if keys[K_RIGHT]:
		mario_rect.move_ip(1 * speed * dt / 1000, 0)
	if keys[K_UP]:
		mario_rect.move_ip(0, -1 * speed * dt / 1000)
	if keys[K_DOWN]:
		mario_rect.move_ip(0, 1 * speed * dt / 1000)

	screen.blit(mario, mario_rect)

	pygame.display.update()</code></pre><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Una dashboard coloratissima per SAP]]></title><description><![CDATA["Come faccio a sapere se la mia installazione SAP sta esplodendo o se le interfacce hanno smesso di funzionare, possibilmente senza spendere un milione di euro per un sistema di monitoraggio fatto apposta?"]]></description><link>https://mariopiccinelli.it/blog/una-dashboard-per-sap/</link><guid isPermaLink="false">6571e35b1b6a0200014ab29b</guid><category><![CDATA[sap]]></category><category><![CDATA[abap]]></category><category><![CDATA[prometheus]]></category><category><![CDATA[grafana]]></category><category><![CDATA[dashboard]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Thu, 07 Dec 2023 22:49:00 GMT</pubDate><content:encoded><![CDATA[<p>In questi giorni ho fatto un paio di esperimenti per tentare di risolvere nel modo meno faticoso l'annoso problema "Come faccio a sapere se la mia installazione SAP sta esplodendo o se le interfacce hanno smesso di funzionare, possibilmente senza spendere un milione di euro per un sistema di monitoraggio fatto apposta?". </p><p>Fortunatamente ci sono in giro un sacco di soluzioni di monitoraggio liberamente accessibili e configurabili con poco sforzo, anche se non pensate esattamente per SAP, per cui ho voluto fare un esperimento. In questo post ho realizzato una bellissima dashboard per monitorare in un rapido colpo d'occhio alcuni parametri indicativi del funzionamento del sistema.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-16.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-16.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/12/image-16.png 1000w, https://mariopiccinelli.it/blog/content/images/size/w1600/2023/12/image-16.png 1600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-16.png 1893w" sizes="(min-width: 720px) 720px"></figure><p>L'esperimento coinvolge <a href="https://prometheus.io/">Prometheus</a>, uno scraper in grado autonomamente di prelevare metriche da sistemi esterni mediante chiamate http e registrarle in un archivio time-based; prevede inoltre la possibilità di eseguire query complesse su questi dati archiviati. L'esperimento coinvolge anche <a href="https://grafana.com/">Grafana</a>, un tool per la creazione di dashboard bellissime che si interfaccia direttamente con Prometheus. Più un paio di magheggi direttamente in SAP.</p><h3 id="esposizione-delle-metriche">Esposizione delle metriche</h3><p>Il primo passo da fare è esporre le metriche che ci interessano in un servizio http su SAP, dal quale poi Prometheus potrà andare a leggere (<strong>nota</strong>: c'è bisogno che Prometheus possa comunicare con SAP, quindi preparate una scatola di cioccolatini per il vostro IT di fiducia). Il modo più semplice per realizzare questo risultato è creare un nuovo servizio nella transazione SICF, esponendo un endpoint collegato a una classe ABAP che si occupa di creare l'output al momento dell'interrogazione.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image.png 657w"></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-1.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-1.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-1.png 752w" sizes="(min-width: 720px) 720px"></figure><p>Le metriche da esporre per Prometheus sono costituite da un semplice payload testuale, in cui ciascuna riga contiene il nome della metrica, un elenco facoltativo di label da associare alla misurazione, e il valore stesso. Supponendo di volere esporre una metrica con il conteggio dell'elenco completo degli IDOC presenti nel sistema divisi per tipo, stato e direzione, il mio output avrà una struttura del genere:</p><pre><code># TYPE total_idocs gauge
total_idocs { mestyp="CREMAS" status="03" direct="1" } 3
total_idocs { mestyp="CREMAS" status="26" direct="1" } 2
total_idocs { mestyp="DEBMAS" status="03" direct="1" } 12
total_idocs { mestyp="DEBMAS" status="09" direct="1" } 4
total_idocs { mestyp="DEBMAS" status="10" direct="1" } 1
total_idocs { mestyp="DEBMAS" status="29" direct="1" } 2
total_idocs { mestyp="DEBMAS" status="37" direct="1" } 2
total_idocs { mestyp="DESADV" status="26" direct="1" } 2
total_idocs { mestyp="DESADV" status="30" direct="1" } 13
total_idocs { mestyp="LOIPRO" status="03" direct="1" } 22
total_idocs { mestyp="LOIWCS" status="03" direct="1" } 8
total_idocs { mestyp="LOIWCS" status="29" direct="1" } 1
total_idocs { mestyp="MATMAS" status="03" direct="1" } 9
total_idocs { mestyp="MATMAS" status="26" direct="1" } 16
...</code></pre><p>La prima riga, che inizia con #, è un commento per Prometheus che definisce il tipo di metrica (in questo caso "gauge"); esistono 4 tipi di metrica utilizzabili, questo è il più adatto a questo utilizzo. Ulteriori informazioni <a href="https://prometheus.io/docs/concepts/metric_types/">qui</a>.</p><p>La classe indicata nel servizio SICF effettua una query sulla tabella che contiene le testate degli IDOC e produce un output conforme. Non prendete come esempio il codice qui sotto, ha bisogno di un pò di pulizia e ottimizzazione prima di andare in produzione :-)</p><pre><code>CLASS zmp_idoc_monitor DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .
  PUBLIC SECTION.
    INTERFACES if_http_extension.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS ZMP_IDOC_MONITOR IMPLEMENTATION.

  METHOD if_http_extension~handle_request.

    server-&gt;response-&gt;set_status(
      code   = 200
      reason = 'ok'
    ).

    DATA: output TYPE string.

    " ----- IDOC per tipo/stato -------------------------------------

    TYPES: BEGIN OF ty_idoc_group,
             mestyp TYPE edidc-mestyp,
             status TYPE edidc-status,
             direct TYPE edidc-direct,
             count  TYPE i,
           END OF ty_idoc_group.
    TYPES: tyt_idoc_group TYPE TABLE OF ty_idoc_group.
    DATA: lt_idoc_group TYPE tyt_idoc_group.

    SELECT mestyp, status, direct, COUNT(*) AS count FROM edidc
    GROUP BY mestyp, status, direct
    INTO TABLE @lt_idoc_group.

    output = output &amp;&amp; '# TYPE total_idocs gauge' &amp;&amp; cl_abap_char_utilities=&gt;newline.
    
    loop at lt_idoc_group into data(lv_idoc_group).
        output = output &amp;&amp; |total_idocs \{ mestyp="{ lv_idoc_group-mestyp }" status="{ lv_idoc_group-status }" direct="{ lv_idoc_group-direct }" \} { lv_idoc_group-count }| &amp;&amp; cl_abap_char_utilities=&gt;newline.
    ENDLOOP.

    " ---------------------------------------------------------------

    server-&gt;response-&gt;set_cdata( output ).

    server-&gt;response-&gt;set_header_field(
      name  = 'Content-Type'
      value = 'text/plain'
    ).

  ENDMETHOD.
ENDCLASS.</code></pre><p>A questo punto possiamo provare a effettuare una richiesta direttamente dal browser, inserendo l'indirizzo completo dell'endpoint, e vedremo il risultato.</p><h3 id="installazione-prometheus-e-grafana">Installazione Prometheus e Grafana</h3><p>L'installazione è sempre semplice quando si usa Docker, per l'esattezza Docker Compose. Come unico accorgimento prima di procedere è necessario creare due cartelle, una per la configurazione di prometheus e una per i dati di grafana (più lo storage per prometheus, ma al momento non mi interessa conservare i dati tra un run e l'altro). Nella cartella dedicata a prometheus dobbiamo creare un file <em>prometheus.yml</em> con la configurazione, in cu inseriamo il puntamento al nostro server SAP con le relative credenziali utente.</p><pre><code>global:
  scrape_interval:     15s

  external_labels:
    monitor: 'codelab-monitor'

scrape_configs:

  - job_name: my_idocs

    metrics_path: '/ztestidoc'

    basic_auth:
      username: 'my_sap_username'
      password: 'my_sap_pwd'

    static_configs:
      - targets: ['my_sap_url:port']</code></pre><p>Si noti che è stata configurata una frequenza di scrape di 15 secondi, per questa applicazione probabilmente è fin troppo frequente (e occhio se il codice di estrazione diventa troppo pesante, meglio fare delle prove). Nel mio caso l'estrazione dal db dura complessivamente una decina di millisecondi quindi direi che problemi per ora non ce ne sono :-) .</p><p>A questo punto si può predisporre il file <em>docker-compose.yml</em> per l'avvio dei servizi. In questo caso ho deciso di esporre le porte di prometheus e grafana rispettivamente sulle porte 9010 e 9011 dell'host.</p><pre><code>version: '3.3'

services:

   prometheus:
     image: prom/prometheus
     ports:
       - 9010:9090
     volumes:
       - my_prometheus_config:/etc/prometheus
     restart: always

   grafana:
     image: grafana/grafana-oss
     ports:
       - 9011:3000
     volumes:
       - my_grafana_storage:/var/lib/grafana
     restart: always
     # id -u
     user: "1001"</code></pre><p><strong>Nota</strong>: per qualche motivo grafana si arrabbia per problemi di permessi nella creazione delle sottocartelle sull'host, quindi è necessario indicare esplicitamente l'utente con cui eseguirlo (lo stesso con cui sono state create le cartelle). Per ottenere l'id dell'utente corrente è sufficiente usare il comando "id -u" da riga di comando (sotto linux).</p><p>Ora possiamo avviare entrambi i servizi con il solito comando:</p><pre><code>docker-compose up -d</code></pre><h3 id="configurazione-prometheus">Configurazione Prometheus</h3><p>..che poi non c'è nulla da configurare, abbiamo già fatto tutto nel file di configurazione.</p><p>Possiamo accedere all'interfaccia web di Prometheus alla porta 9010, dovrebbe essere già tutto configurato correttamente e nella schermata "Status-&gt;Targets" dovremmo vedere il nostro servizio up.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-3.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-3.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-3.png 619w"></figure><p>Nella schermata "Graph" possiamo effettuare una semplice query inserendo il nome del nostro datapoint (total_idocs), per vedere gli ultimi valori letti.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-4.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-4.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/12/image-4.png 1000w, https://mariopiccinelli.it/blog/content/images/2023/12/image-4.png 1056w" sizes="(min-width: 720px) 720px"></figure><h3 id="configurazione-grafana">Configurazione Grafana</h3><p>Per quanto riguarda Grafana invece non abbiamo ancora configurato nulla. Si poteva passare all'avvio un file di configurazione già fatto, ma dall'interfaccia si fa tutto in 10 secondi.</p><p>Si accede all'interfaccia usando la porta esposta da docker (nel nostro caso 9011). Le credenziali di default dell'amministratore sono admin - admin (viene chiesto di cambiare la password al primo accesso).</p><p>Da qui possiamo andare su "Connections -&gt; Add new connection" e cercare il plugin per Prometheus.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-5.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-5.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-5.png 917w" sizes="(min-width: 720px) 720px"></figure><p>Per il nostro setup è sufficiente compilare l'indirizzo della macchina. Ricordando che la macchina si trova già nella rete creata da docker-compose, possiamo utilizzare il nome del container e la porta "vera" (NON quella esposta sull'host).</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-6.png" class="kg-image" alt></figure><p>Non sono necessari altri parametri, basta salvare e la connessione è già disponibile. A questo punto c'è "solo" da creare le dashboard per esporre i dati, e qui non c'è modo migliore di andare a tentativi come sto facendo io da qualche tempo :-)</p><p>Una volta che si è presa la mano, i risultati sono spettacolari.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-10.png" class="kg-image" alt></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-11.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-11.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-11.png 781w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-12.png" class="kg-image" alt></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/12/image-13.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/12/image-13.png 600w, https://mariopiccinelli.it/blog/content/images/2023/12/image-13.png 792w" sizes="(min-width: 720px) 720px"></figure><p>Eccetera...</p><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Accesso Google Sheet con API Python]]></title><description><![CDATA[È possibile leggere/scrivere un Google Sheet da un programma python, usando le librerie ufficiali di Google. Vediamo come.]]></description><link>https://mariopiccinelli.it/blog/accesso-google-sheet-con-api-python/</link><guid isPermaLink="false">64ccd74af367460001e72516</guid><category><![CDATA[google]]></category><category><![CDATA[gsheet]]></category><category><![CDATA[python]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Mon, 13 Nov 2023 21:30:00 GMT</pubDate><content:encoded><![CDATA[<p>È possibile accedere ai dati presenti in un Google Sheet (sia in lettura che in scrittura) utilizzando la libreria ufficiale in Python. Questo apre tutto un mondo in termini di automatizzazione dei processi: ad esempio, posso recuperare delle informazioni da un GSheet e usarle per aggiornare un tool di project management quale Redis; al tempo stesso posso reperire informazioni in qualunque modo e scriverle su un GSheet. </p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/11/image.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/11/image.png 600w, https://mariopiccinelli.it/blog/content/images/2023/11/image.png 829w" sizes="(min-width: 720px) 720px"></figure><p>Vediamo un pò come si può fare.</p><h2 id="accesso">Accesso</h2><p>Il primo passo è configurare il proprio account Google per consentire l'accesso alle API. Sostanzialmente, quello che si deve fare è creare un nuovo progetto Google Cloud (un modo in cui Google possa identificare le nostre richieste), e abilitare su questo progetto le API GSheet.</p><p>Consiglio una lettura di <a href="https://developers.google.com/workspace/guides/get-started?hl=it">questa guida</a>, in cui sono spiegati tutti i passaggi.</p><p>Il risultato finale di questo percorso è l'ottenimento di un file <em>credentials.json</em> che contiene le informazioni necessarie alla libreria per accedere al nostro account (saranno comunque necessarie le credenziali, che verranno inserite in un secondo tempo).</p><h2 id="installazione-librerie">Installazione librerie</h2><p>Per l'accesso avremo bisogno di un pò di librerie python, che possiamo installare con le consuete modalità.</p><pre><code>pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib</code></pre><h2 id="autenticazione">Autenticazione</h2><p>Il primo passo da fare è chiedere l'autenticazione. L'autenticazione è effettuata con il proprio account Google. Durante la richiesta viene mostrata la consueta schermata di Google in cui si chiede di consentire l' uso del proprio account. Le informazioni scambiate sono poi salvate in un file locale <em>token.json</em>, in modo tale da restare disponibili per le successive esecuzioni senza richiedere ulteriori autorizzazioni.</p><pre><code>import os
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow

creds = None
token_path = 'token.json', 
credentials_path = 'credentials.json'
scopes = ['https://www.googleapis.com/auth/spreadsheets'] #spreadsheets.readonly

# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.

if os.path.exists(token_path):
    creds = Credentials.from_authorized_user_file(token_path, scopes)

# If there are no (valid) credentials available, let the user log in.

if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
        creds.refresh(Request())
    else:
        flow = InstalledAppFlow.from_client_secrets_file(
            credentials_path, 
            scopes
        )
        creds = flow.run_local_server(port=0)

    # Save the credentials for the next run
    with open(token_path, 'w') as token:
        token.write(creds.to_json())
        
return creds</code></pre><p>Si noti che in testa al codice abbiamo definito il percorso dei file sopracitati e l'array <em>scopes</em>, che contiene la lista dei permessi che stiamo richiedendo (nell'esempio è il permesso ad accedere in lettura/scrittura ai gsheet, possiamo usare l'alternativa <em>spreadsheets.readonly</em> per accedere in sola lettura e non rischiare modifiche indesiderate).</p><h2 id="inizializzazione-servizio">Inizializzazione servizio</h2><p>A partire dall'oggetto autorizzativo costruito prima possiamo ora costruire l'oggetto che rappresenta il servizio, in questo caso il servizio "spreadsheets".</p><pre><code>from googleapiclient.discovery import build

service = build('sheets', 'v4', credentials=creds)
sheet = service.spreadsheets()</code></pre><h2 id="lettura-dati">Lettura dati</h2><p>A questo punto abbiamo tutto quello che ci serve per leggere il contenuto di un gsheet.</p><pre><code>SPREADSHEET_ID = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
SPREADSHEET_NAME = 'xxxxxxxxxxxxxxxxxxx'
RANGE = f'{SPREADSHEET_NAME}!A1:AQ'

result = sheet.values().get(
    spreadsheetId=SPREADSHEET_ID,
    range=RANGE
).execute()

values = result.get('values', [])</code></pre><p>I parametri da fornire sono lo spreadsheet id (l'id unico dello sheet, visibile nell'url, come da immagine sotto), ...</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/11/image-1.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/11/image-1.png 600w, https://mariopiccinelli.it/blog/content/images/2023/11/image-1.png 712w"></figure><p>...il nome della pagina all'interno della spreadsheet (case sensitive), ...</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/11/image-2.png" class="kg-image" alt></figure><p>...e il range da leggere, nel formato <strong>nome_foglio!prima_cella:ultima_cella</strong>.</p><p>A questo punto nella variabile <em>values </em>è presente un array di array, che presenta i risultati letti dal gsheet organizzati per righe.</p><p>Se usiamo pandas possiamo comodamente convertire questi valori in un dataframe, usando la prima riga per determinare il nome delle colonne.</p><pre><code>gsheet = pd.DataFrame(
    values[1:],
    columns=values[0]
)</code></pre><p><strong>Nota</strong>: la dimensione degli array dipende dai dati effettivamente presenti nel foglio, quindi bisogna prestare attenzione quando si leggono fogli parzialmente vuoti perché pandas potrebbe arrabbiarsi (tipo: ci sono 20 colonne ma gli array letti successivamente hanno solo 18 valori perché le ultime due colonne sono vuote). In questi casi bisogna bonificare gli array letti, ad esempio aggiungendo elementi vuoti fino a quando ciascuna riga ha lo stesso numero di elementi della riga di testata.</p><h2 id="lettura-altre-informazioni">Lettura altre informazioni</h2><p>Oltre al puro contenuto delle celle è possibile leggere informazioni aggiuntive, come ad esempio colori, formattazioni o un eventuale link associato alla cella stessa. Per fare questo si effettua un recupero dei dati aggiungendo il parametro "includeGridData".</p><pre><code>response = sheet.get(
  spreadsheetId=SPREADSHEET_ID, 
  ranges=[f'{SPREADSHEET_NAME}!E4:I500'], 
  includeGridData=True
).execute()</code></pre><p>A seguito di questo, le informazioni aggiuntive si possono trovare "scavando" nel nuovo oggetto restituito, di struttura non particolarmente user friendly :-). Nel mio caso, per recuperare i link dalla prima colonna del range, ho usato questo codice:</p><pre><code>for rowdata in response['sheets'][0]['data'][0]['rowData']:
    first_cell = rowdata['values'][0]
    link = first_cell['hyperlink'] if 'hyperlink' in first_cell else ''</code></pre><h2 id="update">Update</h2><p>Per modificare/scrivere valori nelle celle si effettua un update batch, quindi un'unica operazione di update composta da un certo numero di singoli aggiornamenti. Questi aggiornamenti specificano un range di celle, e un array di valori, uno per ciascuna cella da aggiornare. Alla fine l'update è eseguito chiamando la funzione "batchUpdate" sull'oggetto "values()" dello sheet.</p><pre><code>updates = []

updates.append({
  'range': f'{SHEET_NAME}!A1:C1',
  'values': [[
    "prima cella",
    "seconda cella",
    "terza cella"
  ]]
})

# Batch update per valori
sheet.values().batchUpdate(
  spreadsheetId=SPREADSHEET_ID,
  body = {
    'value_input_option': 'USER_ENTERED',
    'data': updates
  }
).execute()</code></pre><p>Esiste anche la possibilità di modificare altre informazioni relative alla pagina a parte i semplici valori, quali ad esempio i colori delle celle (utili per evidenziare valori particolari). Si procede in maniera simile, accodando una serie di update ad un'unica chiamata batch, ma con sintassi leggermente diverse. </p><p>Per esempio, per modificare il colore di un range di celle, si usa questo codice. Si noti che bisogna passare un range sia per le righe che per le colonne; occhio che il range parte da zero (la riga numero zero corrisponde alla prima riga nel gsheet, quindi quella con scritto 1) e l'estremo superiore è <strong>escluso</strong>. Si noti anche che nella sezione "values" dobbiamo passare esattamente un elemento per ciascuna delle celle da aggiornare.</p><pre><code>requests = []

# Esempi di colori celle
WHITE = {"red": 1, "green": 1, "blue": 1}
RED = {"red": 1}
GREY = {"red": 0.8, "green": 0.8, "blue": 0.8}
LIGHTYELLOW = {"red": 1, "green": 0.95, "blue": 0.8}

requests.append({
  "updateCells": {
    #range 0-indexed, con estremo superiore escluso
    "range": {
      "sheetId": SHEET_ID,
      "startRowIndex": index-1,
      "endRowIndex": index,
      "startColumnIndex": 0,
      "endColumnIndex": 3
    },
    "rows": [
      {
      "values": [
          {"userEnteredFormat": {"backgroundColor": RED}},
          {"userEnteredFormat": {"backgroundColor": GREY}},
        ]
      }
    ],
    "fields": "userEnteredFormat.backgroundColor"
  }
})

sheet.batchUpdate(
  spreadsheetId=SPREADSHEET_ID, 
  body={
    "requests": requests
  }
).execute()</code></pre><p>Voilà! Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Project management con Python: calcoliamo un Gantt]]></title><description><![CDATA[Impariamo a distribuire un carico di lavoro su più mesi con Python e disegniamo un Gantt di progetto.]]></description><link>https://mariopiccinelli.it/blog/project-management-con-python-calcoliamo-un-gantt/</link><guid isPermaLink="false">648769a9f367460001e7240b</guid><category><![CDATA[python]]></category><category><![CDATA[pandas]]></category><category><![CDATA[plotly]]></category><category><![CDATA[project management]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Wed, 14 Jun 2023 22:05:39 GMT</pubDate><content:encoded><![CDATA[<p>Oggi mi sono trovato ad affrontare un task manuale, e ho deciso di complicarmi la vita (come di consueto) inventandomi uno script Python per fare il lavoro al posto mio. E, nonostante tutto, si è rivelato un buon investimento in termini di tempo risparmiato, e mi ha anche offerto una buona idea per un post sul mio blog. Quindi direi che è stato un buon affare :-)</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-14.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image-14.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/06/image-14.png 1000w, https://mariopiccinelli.it/blog/content/images/2023/06/image-14.png 1012w" sizes="(min-width: 720px) 720px"></figure><p>Il problema è il seguente. Supponiamo di avere una serie di attività da svolgere in sequenza, una dopo l'altra. Ciascuna attività "costa" un tot di giornate di lavoro. Adesso supponiamo di avere a disposizione un tot di mesi, e per ciascuno di essi un certo numero di FTE (full time equivalent), ciascuno dei quali rappresenta il lavoro svolto da un programmatore impiegato full time (quindi se ho due programmatori full time sono 2 FTE, se ho 4 programmatori impiegati sul progetto al 50% sono.. ancora 2 FTE). Dobbiamo valutare indicativamente quando ciascuna fase del progetto inizia e finisce.</p><p>Una valutazione del genere si può fare semplicemente con carta&amp;penna (o excel, se proprio ci vogliamo male). Supponiamo per esempio di avere nel primo mese 5 programmatori full time (5 FTE), che per 20 giorni lavorativi al mese fanno 100 giornate di lavoro. Se il primo task cuba 120 giorni di lavoro, il primo mese sarà tutto occupato e il task occuperà anche le prime 20 giornate del mese successivo. Poi inizia il secondo. E così via. </p><p>Ora proviamo a farlo con Python, che è più divertente che farlo con carta&amp;penna. Useremo un notebook <em>Jupyter </em>(così possiamo vedere i risultati passo passo), <em>pandas </em>come libreria per l'elaborazione (non strettamente necessaria, potevamo anche fare senza) e <em>pyplot </em>per disegnare un bel gantt alla fine.</p><p>Per prima cosa importiamo le nostre dipendenze:</p><pre><code>import pandas as pd
import plotly.express as px
import datetime
from calendar import monthrange</code></pre><p>Prepariamo la nostra lista ordinata di task. Questo passo normalmente è il risultato finale di un'altra elaborazione, ad esempio per estrarre/sommare le stime da uno o più file excel, ma il risultato finale è questo: una lista di oggetti, ciascuno dei quali contiene la chiave "activity" con la descrizione del task e la chiave "total" con il numero di giorni necessari a completarlo.</p><pre><code>tasks = [
    {'activity': '1', 'total': 200},
    {'activity': '2', 'total': 150},
    {'activity': '3', 'total': 40},
    {'activity': '4', 'total': 100},
    {'activity': '5', 'total': 80},
    {'activity': '6', 'total': 30},
    {'activity': '8', 'total': 70}
]</code></pre><p>Adesso prepariamo la lista dei mesi su cui andremo a "spalmare" le giornate di lavoro. Nel nostro caso costruiremo un dataframe pandas, con alcuni valori compilati a manina e altri calcolati. In particolar modo, carichiamo a mano i mesi (si potrebbe automatizzare) e gli FTE disponibili per ciascun mese. Consideriamo un fisso di 20 giorni lavorativi al mese e un'efficienza dell' 80% e usiamo questi valori per calcolare il numero di giorni di lavoro effettivamente disponibili per ciascun mese.</p><pre><code>months = pd.DataFrame(
    list(zip(
        ['2023-09', '2023-10', '2023-11', '2023-12', '2024-01', '2024-02', '2024-03', '2024-04', '2024-05'],
        [5, 6, 5, 6, 5, 6, 4, 7, 6]
    )),
    columns = ['month', 'fte']
)
months['days_month'] = 20
months['dev_efficiency'] = 0.8
months['days_avail_tot'] = months['days_month'] * months['fte'] * months['dev_efficiency']
months['days_avail'] = months['days_avail_tot']</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-8.png" class="kg-image" alt></figure><p>Si noti che abbiamo predisposto due colonne con i giorni disponibili: una che rimarrà così com'è, come dato informativo, e l'altra da cui andremo a scalare man mano i giorni attribuiti ai diversi task.</p><p>Prima di proseguire, predisponiamo per ciascun task una colonna vuota (ci servirà per segnare i giorni allocati per quel mese e quel task).</p><pre><code>for task in tasks:
    months[task['activity']] = 0</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-9.png" class="kg-image" alt></figure><p>A questo punto arriva la parte divertente. Per ciascun mese, prendiamo ciascun task con giorni da allocare in sequenza, e per ciascuno cerchiamo di "consumare" la colonna "days_avail". Se finiscono i giorni disponibili vuol dire che per quel mese abbiamo finito e passiamo oltre; se invece finiscono i giorni del task passiamo a consumare il task successivo. </p><p>Siccome per qualche motivo stiamo usando un dataframe pandas, dobbiamo iterare sulle righe con iterrows(). Ad ogni iterazione facciamo una copia della riga, la modifichiamo con l'algoritmo e poi usiamo questa copia per rimpiazzare la riga originale.</p><p>Una cosa del genere:</p><pre><code>for task in tasks:
    months[task['activity']] = 0

for index, row in months.iterrows():
    
    newrow = row.copy()
    
    for task in tasks:
    
        if (task['total'] != 0):

            if (newrow['days_avail'] &lt; task['total']):
                newrow[task['activity']] = newrow['days_avail']
                task['total'] -= newrow['days_avail']
                newrow['days_avail'] = 0
            else:
                newrow[task['activity']] = task['total']
                newrow['days_avail'] -= task['total']
                task['total'] = 0
    
    
    months.iloc[index] = newrow</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-10.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image-10.png 600w, https://mariopiccinelli.it/blog/content/images/2023/06/image-10.png 630w"></figure><p>Funziona! Come si vede, il primo task occupa i primi due mesi per intero più 24 giorni nel terzo (quindi grossomodo un quarto), il secondo task 56 giorni nel terzo e quasi tutto il quarto, e così via. </p><p>Possiamo abbellire la tabella (e verificare le somme :-) ) aggiungendo manualmente una riga di totali. Con <em>pandas </em>posso calcolare i totali usando semplicemente il comando "<em>sum</em>" (numeric_only), ma questo mi calcola anche i totali sulle colonne dei giorni/mese e dell'efficienza, che non mi interessano. Fortunatamente posso grezzamente andare poi a sbiancare i totali che non voglio, così, tanto per complicarmi la vita.</p><pre><code>months.loc['Total'] = months.sum(numeric_only=True, axis=0)

# Colonne di cui voglio mostrare il totale
columns_to_sum = []
for task in tasks:
    columns_to_sum.append(task['activity'])    
    
# Colonne da sbiancare (tutte quelle che non sono nella lista di prima)
columns_to_blank = []
for column in list(months.columns):
    if column not in columns_to_sum:
        columns_to_blank.append(column)        
for column in columns_to_blank:
    months.loc['Total', column] = ''</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-12.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image-12.png 600w, https://mariopiccinelli.it/blog/content/images/2023/06/image-12.png 667w"></figure><h2 id="disegnare-il-gantt">Disegnare il Gantt</h2><p>A questo punto per rendere tutto più chiaro possiamo disegnare il nostro Gantt. Problema: per disegnare il grafico mi occorre stabilire delle date (approssimative) di inizio e fine di ciascun task, mentre ora ho solo il numero di giorni occupati. Devo fare un pò di ragionamenti.</p><p>Per cominciare, posso calcolare la frazione di mese che un task occupa: se in un mese ho 100 giornate disponibili, un task che in quel mese occupa 10 giorni va a riempire un decimo del mese. A questo punto, usando la funzione <em>monthrange </em>della libreria <em>calendar</em>, posso calcolare il numero di giorni contenuti in quel mese e fare la proporzione: il decimo di mese che ho calcolato prima, se un mese è di 28 giorni, vale circa 2,8 giorni. Naturalmente poi questi calcoli saranno tutti approssimativi, ma non importa.</p><p>Il mio obiettivo è costruire una lista di oggetti, uno per task. Ciascuno di questi oggetti deve contenere il nome del task, la data di inizio e la data di fine. Facciamo un bel loop sulla lista dei task, per ciascun task isoliamo dalla lista costruita in precedenza tutti i mesi che contengono attività per quel task, e di questa lista prendiamo il primo elemento (mese di inizio) e l'ultimo (mese di conclusione, che potrebbe anche coincidere col mese di inizio). Recuperiamo entrambi i mesi come oggetto datetime, e per ciascuno calcoliamo anche il numero di giorni contenuti.</p><pre><code>dicts = []

for index, task in enumerate(tasks):

    records = months.loc[months[task['activity']] != 0]
    records = records.loc[~records.index.isin(['Total'])]

    if (records.empty):
        continue
        
    starting_month = datetime.datetime.strptime(records.iloc[0]['month'], '%Y-%m').date()
    starting_month_days = monthrange(starting_month.year, starting_month.month)[1]
    
    ending_month = datetime.datetime.strptime(records.iloc[-1]['month'], '%Y-%m').date()
    ending_month_days = monthrange(ending_month.year, ending_month.month)[1]</code></pre><p>Per calcolare la data di inizio dobbiamo estrapolare i giorni occupati da eventuali task precedenti che hanno occupato giorni in quel mese, e poi riproporzionare sul numero di giorni effettivi del mese. Un bel +1 e abbiamo il giorno (approssimativo) di inizio del task nel mese.</p><pre><code>    # Giorni occupati nel primo mese da attività precedenti
    
    preceding_days_in_month = sum([records.iloc[0][activity] for activity in [preceding_task['activity'] for preceding_task in tasks[0:index]] ])
    
    if (preceding_days_in_month == 0):
        starting_day = 1
    else:
        starting_day = int(starting_month_days * preceding_days_in_month / records.iloc[0]['days_avail_tot']) + 1
        
    starting_month = starting_month.replace(day=starting_day)</code></pre><p>Per calcolare la data di fine procediamo a ritroso. In questo caso dobbiamo considerare i giorni occupati nel mese da task successivi, ed eventuali giorni rimasti inutilizzati (la colonna days_avail), il tutto poi sempre riproporzionato ai giorni da calendario del mese contro i giorni calcolati da FTE.</p><pre><code>    # Giorni occupati nell'ultimo mese da attività successive
    
    following_days_in_month = sum([records.iloc[-1][activity] for activity in [preceding_task['activity'] for preceding_task in tasks[index+1:]] ])
    
    # Eventuali giorni in avanzo se mese non completamente occupato
    following_days_in_month += records.iloc[-1]['days_avail']
    
    if (following_days_in_month == 0):
        days_after = 0
    else:
        days_after = int(ending_month_days * following_days_in_month / records.iloc[-1]['days_avail_tot'])
        
    ending_day = ending_month_days - days_after
      
    ending_month = ending_month.replace(day=ending_day)</code></pre><p>A questo punto aggiungiamo i dati di questo task al nostro dizionario di date, e chiudiamo il loop.</p><pre><code>    dicts.append(dict(Task=task['activity'], Start=starting_month, Finish=ending_month))</code></pre><p>Andiamo a stampare il risultato, e sarà qualcosa del genere:</p><pre><code>[{'Task': '1', 'Start': datetime.date(2023, 9, 1), 'Finish': datetime.date(2023, 11, 9)}, {'Task': '2', 'Start': datetime.date(2023, 11, 10), 'Finish': datetime.date(2023, 12, 31)}, {'Task': '3', 'Start': datetime.date(2023, 12, 31), 'Finish': datetime.date(2024, 1, 15)}, {'Task': '4', 'Start': datetime.date(2024, 1, 15), 'Finish': datetime.date(2024, 2, 18)}, {'Task': '5', 'Start': datetime.date(2024, 2, 18), 'Finish': datetime.date(2024, 3, 21)}, {'Task': '6', 'Start': datetime.date(2024, 3, 21), 'Finish': datetime.date(2024, 4, 3)}, {'Task': '8', 'Start': datetime.date(2024, 4, 3), 'Finish': datetime.date(2024, 4, 21)}]</code></pre><p>Non ci resta altro da fare che dare questo dizionario in pasto a plotly.</p><pre><code>fig = px.timeline(dicts, x_start="Start", x_end="Finish", y="Task")
fig.update_yaxes(autorange="reversed") 
fig.show()</code></pre><p>E voilà!</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-14.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image-14.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/06/image-14.png 1000w, https://mariopiccinelli.it/blog/content/images/2023/06/image-14.png 1012w" sizes="(min-width: 720px) 720px"></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Installazione Redmine su Docker]]></title><description><![CDATA[Come configurare rapidamente una istanza di Redmine sotto Docker.]]></description><link>https://mariopiccinelli.it/blog/installazione-redmine-su-docker/</link><guid isPermaLink="false">647f95e5f367460001e7231a</guid><category><![CDATA[redmine]]></category><category><![CDATA[Docker]]></category><category><![CDATA[linux]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Wed, 07 Jun 2023 21:59:50 GMT</pubDate><content:encoded><![CDATA[<p>Altro mese, altro esperimento. Stavolta con <a href="https://www.redmine.org/">Redmine</a>, un'applicativo di project management open source. </p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-5.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/2023/06/image-5.png 600w"></figure><p>Perchè c'è un numero di task che posso gestire in un progetto prima di iniziare a maledire GSheet (e il corrispettivo su Teams). Quel numero è indicativamente compreso tra 9 e 11. Ed è arrivato. Ed è passato oltre. Ampiamente.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image.png 600w, https://mariopiccinelli.it/blog/content/images/2023/06/image.png 939w" sizes="(min-width: 720px) 720px"></figure><p>Redmine ha un sacco di funzionalità molto interessanti con cui sto sperimentando ultimamente, tra cui diversi tracker personalizzabili, gantt, news, wiki, forum, campi custom di diverse tipologie e per tutte le esigenze, una matrice di permessi ESTREMAMENTE flessibile, ottimo sistema di filtraggio/ricerca con query custom salvabili, notifiche via email e mille altre ancora, compresa una comoda api per caricamento/update delle issues a partire dai suddetti maledetti excel di cui si parlava poco fa.</p><p>Usando Docker (e docker-compose) è possibile tirare su una installazione di Redmine (e PostgreSQL) in un attimo, usando <a href="https://hub.docker.com/_/redmine/">l'immagine ufficiale</a>. </p><p>Un esempio di docker-compose.yml è più o meno così:</p><pre><code>version: '3.3'

services:

   postgres:
     image: postgres:10
     volumes:
       - ./storage/postgresql-data:/var/lib/postgresql/data
     environment:
       POSTGRES_PASSWORD: "my_postgres_password"
       POSTGRES_DB: "redmine"
       PGDATA: "/var/lib/postgresql/data"
     restart: always

   redmine:
     image: redmine:5
     ports:
       - 2000:3000
     volumes:
       - ./storage/docker_redmine-plugins:/usr/src/redmine/plugins
       - ./storage/docker_redmine-themes:/usr/src/redmine/public/themes
       - ./storage/docker_redmine-data:/usr/src/redmine/files
       - type: bind
         source: ./storage/configuration.yml
         target: /usr/src/redmine/config/configuration.yml
     environment:
       REDMINE_DB_POSTGRES: "postgres"
       REDMINE_DB_USERNAME: "postgres"
       REDMINE_DB_PASSWORD: "my_postgres_password"
       REDMINE_DB_DATABASE: "redmine"
       REDMINE_SECRET_KEY_BASE: "secret_key_base"
     restart: always</code></pre><p>La prima parte descrive il database postgres. Al primo avvio viene inizializzato con la password amministrativa e con il nome db specificati. La seconda parte è il server Redmine vero e proprio, corredato delle informazioni necessarie a collegarsi al database di cui sopra.</p><p>Come si vede sono stati esposti nel filesystem host una serie di volumi, tutti contenuti nella directory "storage". A parte quelli relativi ai dati (sia di redmine che di postgres) ho esposto anche le cartelle "plugins" e "themes" di Redmine, in cui posso copiare temi e plugin che saranno visibili dal prossimo riavvio.</p><p>A questo punto basta un:</p><pre><code>docker-compose up -d</code></pre><p>..e sulla porta 2000 abbiamo già la nostra installazione pronta per fare esperimenti.</p><h2 id="https-con-nginx">HTTPS con Nginx</h2><p>Già che ci siamo, possiamo usare il proxy Nginx per esporre il sito in https dal nostro server. Basta avere a disposizione la coppia di chiavi ssl valide per il proprio dominio (facili da ottenere e gratuite con LetsEncrypt), e aggiungere questa configurazione:</p><pre><code>server{
        listen 6666 ssl;
        server_name my_beautiful_site.it 
        ssl_certificate /cert_path/fullchain.pem;
        ssl_certificate_key /cert_path/privkey.pem;
        location / {
                proxy_pass http://localhost:2000/;
        }
}</code></pre><p>In questa configurazione stiamo esponendo la porta 2000 (quella che abbiamo aperto sul nostro container Redmine) sulla porta 6666 del nostro server, attraverso la connessione https con certificato. In questo caso naturalmente vogliamo anche assicurarci che la porta 9090 sia esposta mentre la 2000 no, ad esempio usando UFW (Uncomplicated FireWall). Per esporre la 6666 basta usare questi comandi:</p><pre><code>ufw allow 6666
ufw reload</code></pre><p>Bloccare la 2000 è meno immediato. In teoria UFW di default blocca qualunque porta che non sia esplicitamente esposta, ma Docker all'avvio di un container modifica per conto suo iptables (il firewall a basso livello di Linux), vanificando la protezione. La soluzione è esplicitare a Docker che la porta 2000 è accessibile solo dalla macchina locale, modificando la riga del docker-compose: </p><pre><code>     ports:
       - 2000:3000</code></pre><p>...così:</p><pre><code>     ports:
       - 127.0.0.1:2000:3000</code></pre><p>Adesso la porta 2000 non è più accessibile dall'esterno.</p><h2 id="configurazione-email">Configurazione email</h2><p>Redmine supporta l'invio di email di notifica configurabili attraverso un server smtp. Configurare un server smtp proprio è divertente ma verrà subito bloccato per sospetto spam da qualunque altro server del pianeta, quindi conviene affidarsi a qualche servizio più professionale. </p><p>Nel mio caso ho voluto sperimentare con <a href="https://www.mailersend.com/">MailerSend</a>, che offre un free-tier da 3000 email al mese che sono più che sufficienti per i miei esperimenti. Per accedere a questo servizio è necessario essere titolari di un dominio web, di cui si dovrà dimostrare il possesso modificando alcuni parametri DNS (accessibili dal pannello di controllo del servizio su cui si è registrato il suddetto dominio). La configurazione iniziale dell'account è un tantino macchinosa e non è un argomento particolarmente universale, quindi passerei oltre. Una volta superato l'ostacolo abbiamo a disposizione le credenziali per inviare mail dal nostro dominio attraverso il loro server SMTP.</p><p>Per configurare Redmine, dobbiamo predisporre dentro il container un file di configurazione denominato "config.yml", nel percorso /usr/src/redmine/config/.</p><p>Per assicurarci che il file persista ai riavvii del container, lo creiamo nel nostro host, ad esempio dentro la cartella "storage" che abbiamo predisposto in precedenza.</p><pre><code>production:
  delivery_method: :smtp
  smtp_settings:
    address: [smtp server url]
    port: [smtp server port]
    domain: [miodominio.it]
    authentication: :login
    user_name: [smtp username]
    password: [smtp password]</code></pre><p>..e poi creiamo il collegamento tra il file esterno e il percorso all'interno del container aggiungendo queste righe al nostro docker-compose, sotto la sezione "volumes":</p><pre><code>       - type: bind
         source: ./storage/configuration.yml
         target: /usr/src/redmine/config/configuration.yml</code></pre><p>A questo punto possiamo stoppare il nostro container e riavviarlo (prima di riavviarlo usiamo il comando "rm" per fare pulizia, non sarebbe necessario dopo il "down" ma male non fa, così siamo certi di partire da un nuovo container pulito).</p><pre><code>docker-compose down
docker-compose rm -v
docker-compose up -d</code></pre><p>Per assicurarci che la configurazione sia stata accettata da Redmine possiamo andare sotto il menù Administration-&gt;Settings-&gt;Email notifications, se NON vediamo un messaggio di errore vuol dire che finora tutto è andato per il verso giusto. </p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-1.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/06/image-1.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/06/image-1.png 1000w, https://mariopiccinelli.it/blog/content/images/2023/06/image-1.png 1050w" sizes="(min-width: 720px) 720px"></figure><p>Si configura un indirizzo (qualunque, anche inesistente) di provenienza per le email seguendo le regole del servizio SMTP; nel caso di MailerSend è obbligatorio che l'indirizzo sia nel dominio con cui è stato configurato l'account. E si preme il pulsante "Send a test email" in basso a destra. Salvo sorprese dovremmo essere arrivati a destinazione.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/06/image-4.png" class="kg-image" alt></figure><p>Successo!</p><p><a href="https://www.redmine.org/projects/redmine/wiki/rest_api">Spoiler della prossima puntata</a></p>]]></content:encoded></item><item><title><![CDATA[ABAP Playground: leggere API REST JSON]]></title><description><![CDATA[Giocando con ABAP: leggere dati in JSON da una API http, e magari salvarli in una tabella custom.]]></description><link>https://mariopiccinelli.it/blog/abap-playground-leggere-api-rest-json/</link><guid isPermaLink="false">64459fe5f367460001e72209</guid><category><![CDATA[abap]]></category><category><![CDATA[sap]]></category><category><![CDATA[btp]]></category><category><![CDATA[api]]></category><category><![CDATA[http]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Mon, 24 Apr 2023 21:38:30 GMT</pubDate><content:encoded><![CDATA[<p>In questi giorni sto facendo un po' di esperimenti in ABAP Cloud su un account trial BTP, e ho scoperto che l'interfaccia IF_HTTP_CLIENT che uso di solito non è disponibile in cloud. Quindi ho voluto fare un rapido esperimento con la nuova IF_HTTP_WEB_CLIENT. </p><p>In questo esperimento proverò ad accedere alla famosa <a href="https://thespacedevs.com/llapi">Launch Library</a>, una API che permette di recuperare informazioni su lanci spaziali. La libreria è a pagamento (eccetto un limite free di 15 chiamate al giorno), ma è disponibile un endpoint di test (<a href="https://lldev.thespacedevs.com">https://lldev.thespacedevs.com</a>) con dati non aggiornati ma senza limiti di chiamate. La documentazione è <a href="https://ll.thespacedevs.com/2.2.0/swagger/">qui</a>.</p><p>Cominciamo.</p><p>Per cominciare creiamo una classe nel nostro ambiente ABAP Cloud che erediti l'interfaccia IF_OO_ADT_CLASSRUN. Questa interfaccia richiede l'implementazione di un metodo, e questo metodo può poi essere chiamato semplicemente premendo F9 dalla schermata di Eclipse; è previsto anche un metodo out-&gt;write che consente di stampare messaggi sulla console.</p><pre><code>CLASS zmp_space DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .
  PUBLIC SECTION.
    INTERFACES: if_oo_adt_classrun.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zmp_space IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
  ENDMETHOD.
ENDCLASS.</code></pre><p>La lettura di un endpoint passa attraverso alcuni step:</p><ul><li>Predisposizione variabili (che è più elegante che dichiararle inline, a quanto dicono).</li></ul><pre><code>    DATA: lo_destination   TYPE REF TO if_http_destination,
          lo_client        TYPE REF TO if_web_http_client,
          lo_http_resp     TYPE REF TO if_web_http_response,
          lv_response_json TYPE string,
          lv_response_code TYPE i,
          cx               TYPE REF TO cx_root.</code></pre><ul><li>Creazione destinazione. Nel nostro caso la creiamo a partire da un URL fisso, ma il metodo consente di usare anche destinations o communication agreements creati nella BTP con autenticazione eccetera (ovviamente NON disponibili nella trial :-| ).</li></ul><pre><code>lo_destination = cl_http_destination_provider=&gt;create_by_url( 'https://lldev.thespacedevs.com/2.2.0/launch/?limit=10' ).</code></pre><ul><li>Creazione dell'istanza client a partire dalla destinazione.</li></ul><pre><code>lo_client = cl_web_http_client_manager=&gt;create_by_http_destination( lo_destination ).</code></pre><ul><li>Esecuzione della chiamata (passando il metodo da usare, nel nostro caso GET).</li></ul><pre><code>lo_http_resp = lo_client-&gt;execute( if_web_http_client=&gt;get ).</code></pre><ul><li>Estrazione dell'esito HTTP e del testo contenuto nella risposta.</li></ul><pre><code>lv_response_code = lo_http_resp-&gt;get_status(  )-code.
lv_response_json = lo_http_resp-&gt;get_text(  ).</code></pre><ul><li>Chiusura del client.</li></ul><pre><code>lo_client-&gt;close(  ).</code></pre><ul><li>Eventuale verifica del buon esito della richiesta (codice 200).</li></ul><pre><code>IF ( lv_response_code NE 200 ).
  out-&gt;write( |Wrong response: { lv_response_code }| ).
  EXIT.
ENDIF.</code></pre><ul><li>...il tutto in un blocco try..catch che non fa mai male.</li></ul><p>Il codice completo ha più o meno questo aspetto.</p><pre><code>    DATA: lo_destination   TYPE REF TO if_http_destination,
          lo_client        TYPE REF TO if_web_http_client,
          lo_http_resp     TYPE REF TO if_web_http_response,
          lv_response_json TYPE string,
          lv_response_code TYPE i,
          cx               TYPE REF TO cx_root.

    TRY.

        lo_destination = cl_http_destination_provider=&gt;create_by_url( 'https://lldev.thespacedevs.com/2.2.0/launch/?limit=10' ).
        lo_client = cl_web_http_client_manager=&gt;create_by_http_destination( lo_destination ).
        lo_http_resp = lo_client-&gt;execute( if_web_http_client=&gt;get ).
        lv_response_code = lo_http_resp-&gt;get_status(  )-code.
        lv_response_json = lo_http_resp-&gt;get_text(  ).
        lo_client-&gt;close(  ).

      CATCH cx_root INTO cx.
        out-&gt;write( cx-&gt;get_longtext(  ) ).

    ENDTRY.

    IF ( lv_response_code NE 200 ).
      out-&gt;write( |Wrong response: { lv_response_code }| ).
      EXIT.
    ENDIF.</code></pre><p>Se le cose sono andate per il verso giusto adesso nella variabile lv_response_json abbiamo il contenuto della risposta in formato json, che dobbiamo andare ad esaminare. Ha un aspetto del genere:</p><pre><code>{
    "count": 6874,
    "next": "https://ll.thespacedevs.com/2.2.0/launch/?limit=10&amp;offset=10",
    "previous": null,
    "results": [
        {
            "id": "e3df2ecd-c239-472f-95e4-2b89b4f75800",
            "url": "https://ll.thespacedevs.com/2.2.0/launch/e3df2ecd-c239-472f-95e4-2b89b4f75800/",
            "slug": "sputnik-8k74ps-sputnik-1",
            "name": "Sputnik 8K74PS | Sputnik 1",
            "status": {
                "id": 3,
                "name": "Launch Successful",
                "abbrev": "Success",
                "description": "The launch vehicle successfully inserted its payload(s) into the target orbit(s)."
            },
            "last_updated": "2023-01-21T01:14:08Z",
            "net": "1957-10-04T19:28:34Z",
            "window_end": "1957-10-04T19:28:34Z",
            "window_start": "1957-10-04T19:28:34Z",
            "probability": null,
            "holdreason": null,
            "failreason": null,
            "hashtag": null,
            "launch_service_provider": {
                "id": 66,
                "url": "https://ll.thespacedevs.com/2.2.0/agencies/66/",
                "name": "Soviet Space Program",
                "type": "Government"
            },
            "rocket": {
                "id": 3003,
                "configuration": {
                    "id": 468,
                    "url": "https://ll.thespacedevs.com/2.2.0/config/launcher/468/",
                    "name": "Sputnik 8K74PS",
                    "family": "Sputnik",
                    "full_name": "Sputnik 8K74PS",
                    "variant": "8K74PS"
                }
            },
            "mission": {
                "id": 1430,
                "name": "Sputnik 1",
                "description": "First artificial satellite consisting of a 58 cm pressurized aluminium shell containing two 1 W transmitters for a total mass of 83.6 kg.",
                "launch_designator": null,
                
                .....</code></pre><p>Come si vede, la risposta contiene un array "results" che contiene i singoli lanci. Di ogni istanza di lancio ci interessa l'id, il "net" (Not Earlier Than, che in caso di lanci già avvenuti immagino rappresenti il timestamp dell'effettuazione) e il nome "name". Inoltre ogni lancio contiene un oggetto "mission", che a sua volta contiene un "description" con la descrizione della missione. Nel nostro esempio semplificato ci accontentiamo di estrarre queste informazioni.</p><p>Per prima cosa dobbiamo costruire una struttura di tipi adatta a contenere queste informazioni. Quindi un tipo che rappresenta la missione (con una variabile "description"), un tipo che rappresenta il lancio (contenente "id", "net", "name" e la struttura che abbiamo costruito prima sotto il nome "mission"). Infine un tipo che rappresenta la risposta complessiva, con all'interno una tabella di "results" che rappresenta l'array nel json. Infine definiamo una variabile lv_result costruita su questo tipo, per contenere la nostra risposta una volta parsata.</p><p>Una cosa del genere:</p><pre><code>    TYPES:

      BEGIN OF ty_mission,
        description TYPE string,
      END OF ty_mission,

      BEGIN OF ty_launch,
        id      TYPE c LENGTH 40,
        name    TYPE string,
        net     TYPE string,
        mission TYPE ty_mission,
      END OF ty_launch,

      BEGIN OF ty_launch_response,
        count    TYPE string,
        results  TYPE SORTED TABLE OF ty_launch WITH UNIQUE KEY id, 
      END OF ty_launch_response.

    DATA: lv_result TYPE ty_launch_response.</code></pre><p>A questo punto non ci resta che utilizzare il magico metodo "<strong>/ui2/cl_json=&gt;deserialize</strong>" per trasformare il JSON in una struttura abap:</p><pre><code>    /ui2/cl_json=&gt;deserialize(
      EXPORTING
         json             = lv_response_json
      CHANGING
        data             = lv_result
    ).</code></pre><p>Dopo avere attivato il codice (Ctrl+F3), possiamo mettere un punto di debug a valle del parsing (doppio click sulla sinistra del codice in Eclipse, apparirà un punto blu) e vedere cosa succede quando eseguiamo il codice in modalità terminale con F9. Tip: per fare esperimenti al volo si può aggiungere una istruzione inutile a valle del punto che ci interessa e mettere il punto di debug su quella.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/04/image.png" class="kg-image" alt></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/04/image-1.png" class="kg-image" alt></figure><p>Come si vede i dati ci sono tutti. Vogliamo stamparli a console? </p><pre><code>    LOOP AT lv_result-results INTO DATA(lv_item).
      out-&gt;write( |{ lv_item-net WIDTH = 22 ALIGN = LEFT } { lv_item-name WIDTH = 70 ALIGN = LEFT PAD = ' ' } { lv_item-mission-description }| ).
    ENDLOOP.</code></pre><pre><code>2022-05-05T02:38:00Z   Long March 2D | Jilin-1 Wideband-01C &amp; High Resolution 03D-27 to 33    Earth observation satellites for the Jilin-1 commercial Earth observation satellites constellation.      
2022-04-29T19:55:22Z   Angara 1.2 | Kosmos 2555 (MKA-R)                                       Russian military radar satellite.  
2022-05-09T17:56:30Z   Long March 7  | Tianzhou-4                                             Third cargo delivery mission to the Chinese large modular space station.  
2022-05-06T09:42:00Z   Falcon 9 Block 5 | Starlink Group 4-17                                 A batch of 53 satellites for Starlink mega-constellation - SpaceX's project for space-based Internet communication system.  
2022-05-02T22:49:52Z   Electron | There and Back Again                                        Commercial rideshare mission including payloads for Alba Orbital, Astrix Astronautics, Aurora Propulsion Technologies, E-Space, Spaceflight Inc, Swarm Technologies and UNSEENLABS.  
2022-04-29T21:27:10Z   Falcon 9 Block 5 | Starlink Group 4-16                                 A batch of 53 satellites for Starlink mega-constellation - SpaceX's project for space-based Internet communication system.  
2022-05-13T07:09:00Z   Hyperbola-1 | Jilin-1 Mofang-01A(R)                                    New generation experimental Earth imaging satellite for the Jilin-1 constellation.</code></pre><p>Voilà!</p><hr><p><strong>Bonus</strong>: e se volessimo salvare in una tabella sul db queste informazioni? Possiamo creare una tabella custom con i campi che ci interessano (i campi di testo libero sono CLOB, identificati come abap.string(0))...</p><pre><code>@EndUserText.label : 'ZMP_TBL_LAUNCHES'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zmp_tbl_launches {

  key client    : abap.clnt not null;
  key id        : abap.char(40) not null;
  name          : abap.string(0);
  net           : abap.string(0);
  description   : abap.string(0);

}</code></pre><p>..e poi copiare il contenuto facendo un loop nei results. Per ciascuna iterazione copiamo i campi in una work area (una struttura costruita usando la tabella come modello) e usiamo il comando MODIFY, che crea la riga se non esiste e la aggiorna se esiste. Il controllo dell'esistenza è fatto sulla chiave della tabella (nel nostro caso id, oltre al solito client).</p><pre><code>    DATA: wa_launch TYPE zmp_tbl_launches.

    LOOP AT lv_result-results INTO DATA(lv_launch_temp).

      CLEAR wa_launch.
      
      MOVE-CORRESPONDING lv_launch_temp TO wa_launch.

      wa_launch-description = lv_launch_temp-mission-description.

      MODIFY zmp_tbl_launches FROM @wa_launch.

    ENDLOOP.</code></pre><p>Nell'esempio sopra abbiamo risparmiato un pò di fatica facendo un MOVE-CORRESPONDING, che copia i campi con nomi identici. Naturalmente non vale per "description", che si trova in una struttura all'interno di una colonna.</p><p>Dopo una esecuzione del programma la tabella contiene le informazioni.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/04/image-2.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/04/image-2.png 600w, https://mariopiccinelli.it/blog/content/images/2023/04/image-2.png 972w" sizes="(min-width: 720px) 720px"></figure><hr><p><strong>Altro bonus</strong>: ABAP ha un timestamp standard strutturato come YYYYMMDDhhmmss, che è ovviamente diverso da quello restituito dall'API. Se vogliamo registrare il timestamp in un formato standard, per esempio per poter effettuare ricerche in range o ordinamenti, dobbiamo maltrattare la stringa restituita con un pò di string functions. Grezzamente farei una cosa del genere (sicuramente c'è un modo più elegante).</p><p>Aggiungiamo una colonna net_timestamp alla nostra tabella, con il tipo timestamp:</p><pre><code>@EndUserText.label : 'ZMP_TBL_LAUNCHES'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zmp_tbl_launches {

  key client    : abap.clnt not null;
  key id        : abap.char(40) not null;
  name          : abap.string(0);
  net           : abap.string(0);
  net_timestamp : timestamp;
  description   : abap.string(0);

}</code></pre><p>Modifichiamo il loop di cui sopra per aggiungere la creazione del timestamp. Per creare il timestamp preleviamo le due parti relative a data e ora, e da ciascuna rimuoviamo i caratteri separatori. Concateniamo il risultato ed abbiamo il nostro dato.</p><pre><code>Originale: 1958-02-01T03:47:56Z

Data: 1958-02-01 -&gt; 19580201
Ora: 03:47:56Z -&gt; 034756

Concatenando: 19580201034756</code></pre><p>L'estrazione delle sottostringhe si fa con la tradizionale notazione STRINGA+OFFSET(LUNGHEZZA), e la sostituzione con la tradizionale REPLACE ALL OCCURRENCES. Si, esistono moderne funzioni equivalenti che fanno molto meno anni 80, ma vogliamo mettere la nostalgia del cobol che nasce sfogliando questi programmi?</p><pre><code>    DATA: wa_launch TYPE zmp_tbl_launches.
    DATA: lv_tmp_date TYPE c LENGTH 10,
          lv_tmp_time TYPE c LENGTH 8,
          lv_net_ts type timestamp.

    DELETE FROM zmp_tbl_launches.
    LOOP AT lv_result-results INTO DATA(lv_launch_temp).

      CLEAR wa_launch.
      MOVE-CORRESPONDING lv_launch_temp TO wa_launch.

      wa_launch-description = lv_launch_temp-mission-description.

      lv_tmp_date = lv_launch_temp-net(10).
      REPLACE ALL OCCURRENCES OF '-' IN lv_tmp_date WITH ''.

      lv_tmp_time = lv_launch_temp-net+11(8).
      REPLACE ALL OCCURRENCES OF ':' IN lv_tmp_time WITH ''.

      lv_net_ts = |{ lv_tmp_date }{ lv_tmp_time }|.
      wa_launch-net_timestamp = lv_net_ts.

      MODIFY zmp_tbl_launches FROM @wa_launch.

    ENDLOOP.</code></pre><p>Ed ecco il risultato:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/04/image-3.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/04/image-3.png 600w, https://mariopiccinelli.it/blog/content/images/2023/04/image-3.png 920w" sizes="(min-width: 720px) 720px"></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry]]></title><description><![CDATA[Un servizio rest che analizza un pdf e lo divide in sottodocumenti a partire dalle pagine in cui trova un barcode, e restituisce i documenti in un multipart.]]></description><link>https://mariopiccinelli.it/blog/servizio-rest-per-splittare-documenti-pdf-leggendo-i-barcode-nuovo-esperimento-in-sap-btp-cloud-foundry/</link><guid isPermaLink="false">637a7c2f78a94a00011a0a0f</guid><category><![CDATA[sap]]></category><category><![CDATA[cloud foundry]]></category><category><![CDATA[Docker]]></category><category><![CDATA[python]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Sun, 16 Apr 2023 21:16:27 GMT</pubDate><media:content url="https://mariopiccinelli.it/blog/content/images/2023/04/Immagine-2023-04-16-231603.png" medium="image"/><content:encoded><![CDATA[<img src="https://mariopiccinelli.it/blog/content/images/2023/04/Immagine-2023-04-16-231603.png" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry"><p>Un altro esperimento con barcode, pdf, python e SAP BTP, continuando quando sperimentato <a href="https://mariopiccinelli.it/blog/rest-api-python-su-sap-btp-cloud-foundry/">qui </a>e <a href="https://mariopiccinelli.it/blog/http-api-python-su-sap-btp-cloud-foundry-docker-version/">qui</a>.</p><p>Immaginiamoci una situazione concreta: un operatore carica mediante scanner un documento (di una o più pagine) con appiccicato un barcode sulla prima pagina, e questo documento deve essere caricato in SAP e associato a un oggetto di business  (una fattura, una bolla di entrata merce, un ordine, un qualcosa) precedentemente predisposto con lo stesso barcode. La situazione può essere gestita con meccanismi già disponibili in SAP: il documento entra nell'ERP, viene salvato nell'archivio documentale, viene mandato al servizio OCR che restituisce il valore letto dal barcode sulla prima pagina, e viene effettuata l'associazione interna.</p><p>Adesso immaginiamo una simpatica variante: il cliente appiccica il barcode sulla prima pagina di ciascun documento, ma decide di piazzare il plico di fogli tutti assieme nella multifunzione che quindi produce un grosso pdf con tutti i documenti. A questo punto è un problema, perchè nel meccanismo che abbiamo implementato prima in SAP i documenti entrano uno alla volta. La cosa più sana che potremmo fare in questo momento è usare un qualcosa che divide il plico in N documenti, analizzando i barcode (dove c'è il barcode è la prima pagina di un documento), e poi mandi ciascun documento individualmente a SAP. </p><p>Supponendo di avere un middleware in grado di fare da tramite (fare la richiesta, ricevere i file individuali e mandarli a SAP), proviamo a vedere come potremmo mettere a disposizione di questo middleware un servizio in grado di splittare il documento, da installare nella SAP BTP.</p><p>Supponiamo di volerlo fare in Python. Tutto il codice mostrato in questo esempio è disponibile su <a href="https://github.com/PicciMario/barcode-split-cf">questo repo GitHub</a>. </p><p>La prima cosa che dobbiamo fare è impostare un servizio REST (che chiameremo fantasiosamente <strong>barcodesplit</strong>) di tipo POST che riceverà in ingresso un PDF (nel payload della richiesta) e restituirà un body <a href="https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html">multipart</a> contenente i singoli file. Possiamo predisporre il servizio con la libreria Flask.</p><pre><code>app = Flask(__name__)
@app.route('/barcodesplit', methods=['POST'])
def barcodesplit():
  ...</code></pre><p>A questo punto il metodo request.getdata() di Flask ci restituisce il contenuto dell'oggetto passato. Lo diamo in pasto a pdf2image, che lo converte in una serie di immagini (che potremo poi passare al detector di barcode). Impostiamo una risoluzione di 400dpi (che abbiamo verificato essere sufficiente a interpretare i barcode che usiamo) e una conversione in bianco e nero, che è più veloce.</p><pre><code>pages = pdf2image.convert_from_bytes(
  request.get_data(),
  dpi=400,
  grayscale=True
)</code></pre><p>Il passo successivo è analizzare le pagine una a una, alla ricerca dei barcode, e segnarci il numero delle pagine in cui effettuare lo split del documento di partenza. Restituiamo una lista di tuple, ciascuna delle quali contiene un numero di pagina (l'inizio del sottodocumento) e il valore del barcode.</p><p>Come bonus facciamo la verifica del tipo barcode e il match con una regexp. Il motivo della verifica è che nei documenti potrebbero esserci altri barcode oltre a quelli che andiamo ad appiccicare, e quindi è fondamentale ignorarli riconoscendo la struttura dei nostri (in questo esempio, prendiamo solo barcode CODE128 con valore numerico preceduto da prefisso "DOC").</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-6.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry"></figure><pre><code>barcode_pattern = '^DOC[0-9]+$'
barcode_compiled_pattern = re.compile(barcode_pattern)
barcode_type = 'CODE128'

split_positions = []

for index, page in enumerate(pages):
  found_barcodes = pyzbar.decode(page)
  for obj in found_barcodes:
			
      print(f"Barcode {obj.data.decode('utf-8')} (type {obj.type}) found on page {index}")
			
      if (obj.type != barcode_type): continue
      if (not barcode_compiled_pattern.match(obj.data.decode('utf-8'))): continue
			
      split_positions.append((index, obj.data.decode('utf-8')))</code></pre><p>Mi sono creato un documento di esempio con un pò di barcode (validi e non). Il risultato della procedura di cui sopra assomiglia a questo: una lista di tuple con pagina iniziale e barcode per ciascun sottodocumento.</p><pre><code>[(0, 'DOC001'), (3, 'DOC002'), (5, 'DOC003'), (6, 'DOC004'), (8, 'DOC005')]</code></pre><p>Ora possiamo effettuare lo split vero e proprio. Iniziamo inserendo il request.get_data() in uno stream binario, e lo diamo in pasto a PdfFileReader. </p><p>Per ciascuna delle posizioni in cui dobbiamo splittare il documento, identifichiamo anche l'ultima pagina (che corrisponde alla prima pagina dello split successivo, a meno che non siamo all'ultimo split e in tal caso corrisponderà all'ultima pagina del documento di partenza). </p><p>A questo punto, per ciascuno split creiamo un PdfFileWriter e ci scriviamo dentro una a una le pagine necessarie (addPage()), e per finire andiamo a scrivere il risultato in uno stream binario. Man mano che raccogliamo questi documenti li andiamo a inserire in un dizionario di tuple che rappresenteranno le parti del nostro http multipart di risposta, corredati di nome della parte, nome del file di output e tipo (application/pdf). In questo esempio, impostiamo il nome del file come contatore-barcode.pdf (esempio: <em>001-DOC123456.pdf</em>).</p><pre><code>with io.BytesIO(request.get_data()) as data:	
  
  inputpdf = PdfFileReader(data)

  response_data = {}

  for i, element in enumerate(split_positions):
    
    (first_page, barcode) = element
    
    if i &lt; len(split_positions)-1:
      last_page = split_positions[i+1][0]
    else:
      # For the last split position, the last page
      # is the last page of the document.
      last_page = inputpdf.numPages
          
    output = PdfFileWriter()
    
    for page in range(first_page, last_page):
      output.addPage(inputpdf.getPage(page))

    with io.BytesIO() as tmp:
      output.write(tmp)
      response_data[f"part-{i}"] = (f"{i:03}-{barcode}.pdf", tmp.getvalue(), 'application/pdf')</code></pre><p>Per concludere, possiamo costruire il nostro multipart di risposta passando il dizionario creato prima a MultipartEncoder (dalla libreria requests_toolbelt).</p><pre><code>m = MultipartEncoder(response_data)
return Response(m.to_string(), mimetype=m.content_type)	</code></pre><p>E questa è fatta, abbiamo il nostro file "barcode.py" che contiene il servizio REST.</p><p><strong>NOTA</strong>: il programma descritto sopra e riportato nel repo è ovviamente un esempio didattico, non è ottimizzato e non ha alcuna gestione degli errori. Fatemi il piacere di non prenderlo e metterlo in produzione così com'è, e se lo fate non lamentatevi poi con me.</p><hr><h2 id="docker">Docker</h2><p>Il passo successivo è impacchettare il tutto in un container Docker. </p><p>Rispetto a quando descritto nel precedente <a href="https://mariopiccinelli.it/blog/http-api-python-su-sap-btp-cloud-foundry-docker-version/">post</a> ho deciso di far servire il servizio Flask, anzichè dal suo server http predefinito (che dovrebbe essere usato solo per test), da <a href="https://gunicorn.org/">Gunicorn</a>, un server scritto in Python e pensato proprio per questo genere di servizi. Quindi, anzichè chiamare app.run() dentro lo script python, prepariamo un file bash che si occuperà di eseguire gunicorn e dargli in pasto il nostro servizio flask (l'oggetto "app" nel file "barcode"). Una cosa del genere:</p><pre><code>#!/bin/sh
gunicorn --chdir /app barcode:app -w 1 --threads 1 -b 0.0.0.0:3333</code></pre><p>A questo punto ci occorre un dockerfile, ovvero un file che descrive come creare la nostra immagine docker. </p><ul><li>Partiamo da una immagine Python 3.10 in versione slim (quindi con meno librerie preinstallate di default e quindi dimensioni più ridotte). </li><li>Creiamo la cartella di lavoro /app. </li><li>Eseguiamo apt-get per aggiornare il repository e installare alcune dipendenze di sistema. </li><li>Eseguiamo pip per installare le librerie python necessarie. </li><li>Aggiungiamo i nostri file (barcode.py e entry_point.sh).</li><li>Segnaliamo la necessità di esporre la porta 3333.</li><li>Selezioniamo come entry point il nostro script bash, che sarà quindi eseguito all'avvio dell'immagine docker.</li></ul><pre><code>FROM python:3.10-slim
RUN mkdir -p /app
WORKDIR /app
RUN apt-get update &amp;&amp; apt-get -y install zbar-tools poppler-utils
RUN apt-get clean
RUN pip install flask pyzbar pdf2image cfenv gunicorn PyPDF2 requests_toolbelt
ADD barcode.py entry_point.sh /app/
EXPOSE 3333
ENTRYPOINT ["./entry_point.sh"]</code></pre><p>Ora possiamo creare l'immagine. Si noti che il prefisso prima del nome dell'immagine dev'essere il nome del proprio utente su Dockerhub (altrimenti non sarà possibile caricarvi l'immagine). A meno che non vogliamo limitarci a testare in locale senza caricarlo sulla BTP, in tal caso il prefisso non è necessario.</p><pre><code>docker build -t piccimario/barcode-split-cf:0.1 .</code></pre><p>Una volta creata l'immagine possiamo avviare una istanza locale per testare il tutto.</p><pre><code>docker run -t -i --rm -p 3333:3333 piccimario/barcode-split-cf:0.1</code></pre><p>Possiamo testare la chiamata in locale usando per esempio Postman, e <a href="https://github.com/PicciMario/barcode-split-cf/raw/master/test_bc_multipli_doc.pdf">questo file</a>. Effettuiamo una chiamata a http://localhost:3333/barcodesplit passando come body (binary) il pdf, e il risultato sarà qualcosa del genere:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry"></figure><p>Nell'immagine sopra si vede la prima parte del multipart, preceduta dal saparatore, con il pdf all'interno. Successivamente nella risposta sono presenti tutte le altre parti.</p><p>Per terminare l'istanza possiamo semplicemente premere Ctrl+C (l'opzione --rm fa sì che l'istanza terminata sia automaticamente rimossa dal sistema).</p><hr><h2 id="ma-che-me-ne-faccio-di-questo-multipart">Ma che me ne faccio di questo multipart?</h2><p>La risposta con i PDF in un multipart è una soluzione comoda per un sistema automatizzato, in grado di estrarre i singoli documenti per inviarli a un ERP, ma non è comodissima per effettuare test. Come faccio a provare il servizio e verificare i singoli file restituiti, visto che sono tutti mischiati in una unica risposta?</p><p>Possiamo costruirci un semplice script Python di test che, dopo aver chiamato il nostro servizio, si preoccupa di estrarre i PDF dal multipart e salvarli come singoli file su disco, per consentirci di verificarli. </p><p>Il seguente script legge il pdf da inviare (disponibile nel <a href="https://github.com/PicciMario/barcode-split-cf">repo del progetto</a>), lo invia al servizio in localhost e riceve la risposta. Divide il multipart usando MultipartDecoder (nella libreria requests_toolbelt). Fa un pò di magheggi sull'header di ciascuna parte per recuperare il nome file da scrivere e infine scrive nella cartella corrente un file per ciascuna parte, con il contenuto estratto dalla risposta.</p><pre><code>import requests, os, re
from requests_toolbelt.multipart import decoder

# File di test da inviare
with open('./test_bc_multipli_doc.pdf', 'rb') as f:
  data = f.read()

# Riceve risposta dal servizio
res = requests.post(
  url='http://localhost:3333/barcodesplit',
  data=data,
  headers={'Content-Type': 'application/octet-stream'}
)

# Estrae multipart risposta
multipart_data = decoder.MultipartDecoder.from_response(res)

# Analizza singole parti del multipart
r = re.compile('^filename="([A-Za-z0-9-\.]+)"$')
for part in multipart_data.parts:

  headers = {k.decode(): v.decode() for k,v in dict(part.headers).items()}
  content_disp = [x.strip() for x in headers['Content-Disposition'].split(';')]
  filename = [r.search(x).group(1) for x in content_disp if r.match(x)][0]

  with open(os.path.join('./', filename), 'wb') as file:
    file.write(part.content)</code></pre><p><strong>NOTA</strong>: anche in questo caso non è prevista alcuna gestione degli errori, è solo uno script scritto in fretta&amp;furia per fare un test. Non prendetemi come esempio.</p><p>Il risultato, usando il file di test nel repo, è questo:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-1.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry"></figure><p>E questo è il contenuto del primo file (000-DOC001.pdf):</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-2.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry"></figure><p>Funziona! Si noti che sulla seconda pagina c'è un barcode NON valido (non inizia con DOC...), quindi è corretto che lì non venga effettuata la divisione.</p><hr><h2 id="sap-cloud-foundry">SAP Cloud Foundry</h2><p>Seguendo i passaggi descritti in un <a href="https://mariopiccinelli.it/blog/http-api-python-su-sap-btp-cloud-foundry-docker-version/">precedente post</a> possiamo caricare l'immagine sul nostro account DockerHub, e da lì creare il servizio su un account di trial della SAP BTP Cloud Foundry.</p><pre><code>docker push piccimario/barcode-split-cf:0.1</code></pre><pre><code>cf login</code></pre><pre><code>cf push barcode-split --docker-image piccimario/barcode-split-cf:0.1 --docker-username piccimario -k 512M -m 512M</code></pre><p>Verifichiamo nell'interfaccia web della BPT che il servizio esista e sia attivo:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-4.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/11/image-4.png 600w, https://mariopiccinelli.it/blog/content/images/2022/11/image-4.png 942w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-3.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/11/image-3.png 600w, https://mariopiccinelli.it/blog/content/images/2022/11/image-3.png 918w" sizes="(min-width: 720px) 720px"></figure><p>A questo punto possiamo eseguire il nostro script di test dopo averlo modificato per puntare all'endpoint "barcodesplit" del servizio esposto nella BTP, nel mio caso</p><pre><code>https://barcode-split.cfapps.us10-001.hana.ondemand.com/barcodesplit</code></pre><p>(oppure testarlo sl volo con Postman...)</p><p>E voilà, funziona!</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/11/image-5.png" class="kg-image" alt="Servizio REST per splittare documenti PDF leggendo barcode, nuovo esperimento in SAP BTP Cloud Foundry" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/11/image-5.png 600w, https://mariopiccinelli.it/blog/content/images/2022/11/image-5.png 982w" sizes="(min-width: 720px) 720px"></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Costruire un tastierino numerico con Arduino - Seconda Parte]]></title><description><![CDATA[Seconda parte del mio esperimento nella costruzione di un tastierino numerico basato su Arduino. Ora si fa sul serio.]]></description><link>https://mariopiccinelli.it/blog/costruire-un-tastierino-numerico-con-arduino-seconda-parte/</link><guid isPermaLink="false">63f7d9fc78a94a00011a1181</guid><category><![CDATA[arduino]]></category><category><![CDATA[jlpcb]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Thu, 23 Feb 2023 21:52:37 GMT</pubDate><media:content url="https://mariopiccinelli.it/blog/content/images/2023/02/Immagine.png" medium="image"/><content:encoded><![CDATA[<img src="https://mariopiccinelli.it/blog/content/images/2023/02/Immagine.png" alt="Costruire un tastierino numerico con Arduino - Seconda Parte"><p>Come promesso, finalmente arriva il seguito del mio post "<a href="https://mariopiccinelli.it/blog/costruire-un-tastierino-numerico-esperimenti-con-arduino/">Costruire un tastierino numerico con Arduino - Prima Parte</a>". Nella scorsa puntata eravamo arrivati a costruire su protoboard una semplice matrice di pulsanti connessa ad un Arduino, che emulava una tastiera. Abbiamo poi fatto <a href="https://mariopiccinelli.it/blog/esperimenti-arduino-encoder/">una piccola digressione</a> per aggiungere al prototipo un encoder.</p><p>Il passo successivo era naturalmente quello di fare un pò di ordine. Il groviglio di fili ha il suo fascino, ma onestamente non lo rifarei :-) Ho voluto provare a vedere se mi ricordavo qualcosa degli anni di Ingegneria, e provare a disegnare un PCB un pò più stabile.</p><p>Ho scelto di usare EasyEDA, un software gratuito browser-based che offre tutte le funzionalità necessarie a disegnare il circuito e a comporre il pcb.</p><p>Ho iniziato a disegnare il circuito:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-16.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-16.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-16.png 998w" sizes="(min-width: 720px) 720px"></figure><p>Da questo ho prodotto il pcb. Per prima cosa si posizionano i componenti sulla scheda, decidendone quindi la dimensione. In questa fase i collegamenti tra i componenti sono mostrati come semplici segmenti (viola).</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-17.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte"></figure><p>Successivamente si procede a tracciare i collegamenti sul rame. In questo caso lavoriamo con una scheda a due layer, il che significa che i collegamenti possono essere realizzati su entrambi i lati. L'unica accortezza in questo procedimento è non incrociare i fili :-) Se non si trova il modo di tirare i collegamenti senza incroci è possibile interrompere il filo su un layer e riprenderlo sull'altro utilizzando un via (che è semplicemente un collegamento di rame tra i due layer).</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-21.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte"></figure><p>I collegamenti possono essere tracciati automaticamente con la funzione di autorouting oppure a mano uno a uno. Io di solito uso l'autorouting e poi sistemo a mano i collegamenti che hanno bisogno di un pò di affetto in più. Il risultato è questo.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-18.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte"></figure><p>I collegamenti in rosso sono sul layer top, quelli in blu sul bottom. In giallo è visibile la serigrafia (testo o simboli semplicemente disegnati sulla scheda).</p><p>Una volta finito il disegno si può, con un semplice pulsante, creare i file di produzione e mandarli al carrello del sito <a href="https://jlcpcb.com/">jlpcb.com</a>, da cui è possibile ordinare per pochi euro il prodotto finito. </p><p>Si aspetta qualche settimana (se si è scelta la spedizione economica) e a un certo punto arriva a casa un bel pacchetto blu con le nostre schede fresche fresche di fabbrica.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-19.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-19.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-19.png 621w"></figure><p>Ho messo anche un disegnetto nella serigrafia del layer bottom, giusto per vedere come veniva fuori :-)</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-20.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-20.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-20.png 643w"></figure><p>A questo punto si aspettano altre settimane affinchè Aliexpress riesca a consegnarmi una manciata di switch meccanici, e poi si passa a scaldare il saldatore.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-22.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte" srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-22.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-22.png 649w"></figure><p>Con dei copritasto che avevo in giro per casa il risultato è ancora migliore.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-23.png" class="kg-image" alt="Costruire un tastierino numerico con Arduino - Seconda Parte"></figure><p>E con questa mi sono tolto la soddisfazione di vedere funzionare un pcb disegnato da me, che era dai tempi dell'università che non mi capitava.</p><p>La prossima versione avrà un display e un sacco di led colorati, ma penso che ci vorrà un pò di tempo. Nel frattempo mi godo questo primo risultato.</p><p>Grazie per la lettura e alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Esperimenti Arduino: encoder]]></title><description><![CDATA[Utilizzare un encoder rotativo con Arduino.]]></description><link>https://mariopiccinelli.it/blog/esperimenti-arduino-encoder/</link><guid isPermaLink="false">63f34b6078a94a00011a10ce</guid><category><![CDATA[arduino]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Mon, 20 Feb 2023 21:13:11 GMT</pubDate><content:encoded><![CDATA[<p>Come rapida appendice al mio post precedente "<a href="https://mariopiccinelli.it/blog/costruire-un-tastierino-numerico-esperimenti-con-arduino/">Costruire un tastierino numerico con Arduino: prima parte</a>" ho voluto provare l'esperimento di aggiungere un encoder rotativo al mio prototipo. </p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-14.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-14.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-14.png 672w"></figure><p>Un encoder rotativo è sostanzialmente una manopola che trasmette al microcontrollore degli impulsi che descrivono la sua rotazione a passi discreti (un "click" alla volta). Rispetto ad un potenziometro ha il vantaggio di consentire la rotazione infinita in entrambe le direzioni, oltre al discorso dei passi discreti citato prima. E, come valore aggiunto, spesso gli encoder contengono anche un pulsante (pressione verticale sul perno).</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-10.png" class="kg-image" alt></figure><p>L'obiettivo non mi è ancora del tutto chiaro, sto sperimentando. Un possibile utilizzo molto interessante è per lo scrolling: anziché premere le frecce su&amp;giù per scorrere lungo un listato posso girare la manopola, e dietro le quinte arduino trasmette al computer l'equivalente delle frecce direzionali. Altri possibili utilizzi sono le funzioni multimediali (volume, più pausa/play sul pulsante).</p><p>A livello pratico, un encoder presenta due uscite (escludendo il pulsante).</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-12.png" class="kg-image" alt></figure><p>Durante la rotazione, ad ogni click queste due uscite vengono messe a GND per alcuni millisecondi e poi riportate a flottanti (devono essere configurati i <a href="https://en.wikipedia.org/wiki/Pull-up_resistor">pullup </a>su Arduino per fissare a VCC il valore a riposo). I segnali generati dai due pin sono sfasati, e la fase dipende dal verso di rotazione. Analizzando quindi l'evoluzione dei due segnali si può identificare l'effettuazione di un "click" e la sua direzione.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-13.png" class="kg-image" alt></figure><p>Fortunatamente non saremo costretti a gestire tutta la questione via software perché c'è già una comoda libreria per Arduino che gestisce l'interfaccia per noi: <a href="https://www.arduinolibraries.info/libraries/encoder-button">EncoderButton</a>. La possiamo installare dal package manager di Arduino.</p><p>Di seguito un esempio di codice. Nella parte iniziale del programma importiamo la libreria, prepariamo le costanti usate per indicare i pin utilizzati e inizializziamo l'oggetto EncoderButton.</p><pre><code>#include &lt;EncoderButton.h&gt;

// Encoder inputs
const byte ENC_A = 0;
const byte ENC_B = 1;
const byte ENC_BUTTON = 21;
EncoderButton eb1(ENC_A, ENC_B, ENC_BUTTON);
volatile int scrollSpeed;</code></pre><p>Prima di proseguire predisponiamo i metodi di callback dei due eventi generati dal componente (rotazione e pressione pulsante). In questo esempio per ciascuna rotazione voglio inviare un certo numero di pressioni di freccia su o giù (a seconda del verso di rotazione) usando la libreria Keyboard già usata nella scorsa puntata, e alla pressione del pulsante voglio cambiare la velocità di scroll (ovvero il numero di pressioni di una freccia per ciascun "click" dell'encoder).</p><pre><code>void onEb1Encoder(EncoderButton&amp; eb) {
  if (eb.increment() &gt; 0){
    for (int i = 0; i &lt; scrollSpeed; i++){
      Keyboard.press(KEY_UP_ARROW);
      Keyboard.release(KEY_UP_ARROW);
    }
  }
  else {
    for (int i = 0; i &lt; scrollSpeed; i++){
      Keyboard.press(KEY_DOWN_ARROW);
      Keyboard.release(KEY_DOWN_ARROW);
    }
  }
}

void onEncoderButtonClick(EncoderButton&amp; eb){
  if (scrollSpeed == 1){
    scrollSpeed = 5;
  }
  else {
    scrollSpeed = 1;
  }
}</code></pre><p>Nel setup configuriamo i pin come <a href="https://en.wikipedia.org/wiki/Pull-up_resistor">pullup</a> e associamo le routine di callback al componente.</p><pre><code>void setup(){
  ...
  pinMode(ENC_BUTTON, INPUT_PULLUP);
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);  
  eb1.setEncoderHandler(onEb1Encoder);
  eb1.setClickHandler(onEncoderButtonClick);
  scrollSpeed = 1;
  ...
}</code></pre><p>Nel loop principale dobbiamo solo chiamare un metodo per consentire l'aggiornamento continuo dello stato del componente e la chiamata agli eventuali eventi:</p><pre><code>void loop(){
  ...
  eb1.update();
  ...
}</code></pre><p>A questo punto non ci resta che collegare l'encoder all'Arduino aggiungendo 5 fili al groviglio, e tutto dovrebbe funzionare.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-15.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-15.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-15.png 689w"></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Aprire SAP GUI da terminale o Keepass]]></title><description><![CDATA[Come aprire una connessione SAP da terminale o usando Keepass.]]></description><link>https://mariopiccinelli.it/blog/aprire-connessione-sap-da-terminale-o-keepass/</link><guid isPermaLink="false">63dcdaa078a94a00011a1031</guid><category><![CDATA[sap]]></category><category><![CDATA[keepass]]></category><category><![CDATA[command-line]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Fri, 03 Feb 2023 23:14:50 GMT</pubDate><content:encoded><![CDATA[<p>Quando si ha a che fare con tanti clienti con SAP, si passa buona parte del proprio tempo a saltellare da un sistema a un altro (da un cliente a un altro, da sviluppo a test a produzione, ecc..). </p><p>E ogni volta che si accede a un sistema bisogna cercarlo nella lista, aprirlo, poi andare a cercare le credenziali nel proprio password manager, poi copiaincollare le credenziali, verificare di avere impostato il giusto codice client e la giusta lingua, e poi finalmente ci siamo. </p><p>Salvo che non ho sbagliato a copiare la password e devo ricominciare da capo. </p><p>Scomodo.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-7.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-7.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2023/02/image-7.png 1000w, https://mariopiccinelli.it/blog/content/images/2023/02/image-7.png 1380w" sizes="(min-width: 720px) 720px"><figcaption>Immagine di repertorio :-)</figcaption></figure><p>Fortunatamente l'installazione standard della SAP GUI su Windows si porta appresso un misconosciuto programmino, "sapshcut", che consente di aprire una connessione direttamente con un comando da terminale:</p><pre><code>start sapshcut -system="ABC" -desc="DESCRIZIONE" -client=123 -user="XXXX" -pw="XXXXX" -maxgui -language="EN"</code></pre><p>E' sufficiente scrivere il nome della connessione così come è stata salvata nel SAP Logon (vedi sotto), le tre lettere del nome sistema, il client, le credenziali e la lingua desiderata per il login. L'opzione MAXGUI è facoltativa, serve ad aprire la GUI a schermo pieno.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-1.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-1.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-1.png 893w" sizes="(min-width: 720px) 720px"></figure><p>E' già più comodo, ma non risolve la situazione. Dovrei tenermi una lista di comandi da qualche parte, e (peggio del peggio) ciascuno di questi dovrebbe contenere le credenziali in chiaro. </p><p>Non bellissimo.</p><p>Per fortuna viene in nostro aiuto <a href="https://keepass.info/">Keepass</a> 2,  un comodo password manager che lavora esclusivamente in locale (no cloud), open source e gratuito. Possiamo scriverci dentro le nostre credenziali, e proteggerle in un file criptato in locale con un'unica password globale. Le possiamo mettere in ordine in cartelle (magari per progetto) e gli possiamo dare una descrizione comoda. Possiamo perfino assegnare dei colori diversi (magari per distinguere dev da prod) e icone personalizzate.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-5.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2023/02/image-5.png 600w, https://mariopiccinelli.it/blog/content/images/2023/02/image-5.png 689w"></figure><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-4.png" class="kg-image" alt></figure><p>Un'altra funzione molto comoda è la possibilità di salvare, per ciascuna credenziale, un url. E' possibile poi selezionare la voce dalla lista, premere Ctrl+U (o fare doppio click sulla colonna Url) per aprire direttamente l'indirizzo, tipicamente nel browser. </p><p>E' inoltre possibile usare un url del tipo "cmd://", per eseguire un qualunque comando. Possiamo quindi inserire nelle entry relative ai sistemi SAP un url per richiamare sapshcut, in questa maniera:</p><pre><code>cmd://sapshcut -system={s:system} -desc="{s:desc}" -client={s:client} -user="{USERNAME}" -pw="{PASSWORD}" -maxgui -language="EN"</code></pre><p>I campi tra parentesi graffe sono variabili interpretate da Keepass. USERNAME e PASSWORD corrispondono, ovviamente, alle credenziali inserite. Le altre, identificate come S:&lt;nome&gt;, sono impostate per ciascuna entry nel tab "Advanced":</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/02/image-6.png" class="kg-image" alt></figure><p>Voilà. Possiamo selezionare una voce e premere Ctrl+U, e come per magia abbiamo la sessione aperta.</p><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[TCode Helper - sperimentando con Python e TKinter]]></title><description><![CDATA[Creiamo una piccola utility per quelli come me che lavorano con SAP e dimenticano sempre i transaction code, sperimentando con Python e Tkinter.]]></description><link>https://mariopiccinelli.it/blog/tcode-helper-sperimentando-con-python-e-tkinter/</link><guid isPermaLink="false">63c1ca4e78a94a00011a0ee7</guid><category><![CDATA[sap]]></category><category><![CDATA[python]]></category><category><![CDATA[tkinter]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Fri, 13 Jan 2023 22:39:59 GMT</pubDate><content:encoded><![CDATA[<p>La prima cosa che si impara quando si lavora con SAP è che tutte le funzioni sono accessibili, oltre che navigando in un menu con tipo cinquemila voci su dodici livelli, digitandone il codice nella casella in alto a sinistra nel client. Si tratta dei Transaction Code, tcode per gli amici, poichè le singole applicazioni in SAP sono chiamate, come richiede la tradizione informatica degli anni 80, "transazioni".</p><p>Il problema è che questi tcode sono tanti e hanno nomi assolutamente spassosi come me22n, se38, we05, se22, /opt/vim_wp eccetera. So bene che ricordarsi a memoria 800 codici è un punto di orgoglio importante per qualunque analista SAP, ma non sarebbe bello per quelli come me che non si ricordano cosa hanno mangiato a pranzo due ore prima avere un qualche programmino immediato da richiamare con il quale sia possibile cercare "idoc log" per ricordarsi della we05, o "manutenzione servizi" quando proprio "/iwfnd/maint_service" ce l'ho sulla punta della lingua ma non mi viene? </p><p>Del tipo: </p><blockquote>Non mi ricordo come mostrare i log degli IDOC. Premo shift+F12 e mi appare la finestra di ricerca. Digito "idoc log" e nella lista mi appare in bella vista il codice WE05. Esclamo qualcosa del tipo "maccertoecchecca**olousoseicentovoltealgiorno"  facendo impensierire il mio vicino di scrivania (che pure ne ha viste di tutti i colori con me vicino), poi premo ESC e torno a lavorare.</blockquote><p>Approfittando del fatto che avevo proprio voglia di fare qualche esperimento con <a href="https://docs.python.org/3/library/tkinter.html">TKinter</a>, la libreria grafica nativa di Python, ho provato a realizzare qualcosa del genere.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/01/image-2.png" class="kg-image" alt></figure><hr><p>Nel post che segue cerco di spiegare i singoli passaggi per la realizzazione dell'applicazione, cercando di seguire una specie di filo logico. Per ulteriori dettagli conviene sfogliare direttamente il codice disponibile qui: <a href="https://github.com/PicciMario/TCodeHelper">https://github.com/PicciMario/TCodeHelper</a>.</p><p>Negli spezzoni di codice qui sotto, per facilitare la leggibilità, non mostro dettagli come la gestione degli errori o funzionalità meno interessanti. I vari "self" sono perchè nel codice completo tutto è dentro una classe, se riesco cerco di toglierli per rendere il codice più leggibile, ma può essermene scappato qualcuno. Non usate questo codice per cercare di farci qualcosa di utile, piuttosto guardate nel repository la versione completa.</p><hr><p>Per prima cosa si deve creare una finestra Tkinter. Si importano un pò di librerie si chiama il costruttore Tk(). Già che ci siamo impostiamo una icona e il titolo.</p><pre><code>window = tk.Tk()
window.title("TCode Helper - " + version)
window.geometry("400x400")

logo = Image.open(logo_filename)
app_icon = ImageTk.PhotoImage(logo)
window.wm_iconphoto(False, app_icon)</code></pre><p>La parte più divertente in tkinter è come sempre la creazione dei componenti dell'interfaccia (e questa sotto è solo una piccola parte):</p><pre><code># Creazione componenti UI
small_info_image=logo.resize((12, 12))
small_info_image_tk = ImageTk.PhotoImage(small_info_image)
info_img = ttk.Button(window, image=small_info_image_tk, command=popup_about)
info_img.grid(row=0, column=1, sticky="nsew")

search_field = ttk.Entry(window, width=50)
[...]
search_field.bind("&lt;KeyRelease&gt;", _text_callback)
search_field.bind("&lt;Tab&gt;", focus_tree)

columns = ('code', 'descr')
tree = ttk.Treeview(window, columns=columns, show='headings')
[...]

tree.column("code", minwidth=0, width=100, stretch=tk.NO) 
[...]
tree.bind('&lt;&lt;TreeviewSelect&gt;&gt;', _selected_tcode)
tree.bind("&lt;Tab&gt;", focus_search)

descr_text = RichText(window, height=5, borderwidth=5, relief=tk.FLAT, wrap=tk.WORD)
[...]</code></pre><p>E alla fine si mostra la finestra:</p><pre><code>window.mainloop()</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/01/image-3.png" class="kg-image" alt></figure><p>Come si vede ci sono diversi binding legati agli oggetti della UI. Per esempio, il campo di testo e la treeview hanno la pressione del tasto TAB collegati a metodi che effettuano l'uno il focus sull'altro; in questo modo è possibile passare rapidamente dal campo di ricerca alla lista con la pressione di un tasto. Nel caso di spostamento verso la lista si forza anche la selezione del primo elemento mostrato.</p><pre><code>def focus_tree(self, event):
	if (tree.get_children()):
		child_id = tree.get_children()[0]
		tree.focus_set()
		tree.focus(child_id)
		tree.selection_set(child_id)
	return("break")

def focus_search(self, event):
	search_field.focus_set()
	search_field.selection_range(0, tk.END)
	return("break")</code></pre><p>Infine, sulla lista si implementa un binding a una funzione che mostra nella casella in fondo il codice della transazione selezionata e la relativa descrizione (la casella è predisposta allo scopo di mostrare una descrizione estesa, ma non ho ancora avuto voglia di mettermi a scrivere un poema per ciascuna delle 80 transazioni nella lista che ho accumulato finora).</p><pre><code>def _selected_tcode(self, event):

if (len(tree.selection()) &gt; 0):

	selected_id = tree.selection()[0]
	selected_item = tree.item(selected_id)
		selected_tcode = next((x for x in self.tcodes if x['code'] == selected_item['values'][0]), "")

	descr_text.delete('1.0', tk.END)
	descr_text.insert("end", f"{selected_tcode['code']}\n", "bold")
	descr_text.insert("end", f"{selected_tcode['descr']}", "base")
	descr_text.insert("end", f"\n\nKeywords ({selected_tcode['score']}): {selected_tcode['keywords']}", "italic")

return("break")</code></pre><p>Il binding più interessante è sul key release della casella di ricerca, che rigenera la lista dei codici trovati in base a quanto scritto e verrà descritto più avanti.</p><p>Prima di proseguire, abbiamo anche creato una manciata di binding legati direttamente alla finestra o globali.</p><p>Abbiamo creato un binding sulla chiusura della finestra. Quando la finestra viene chiusa (o quando premiamo ESC) non vogliamo che il programma termini, vogliamo solo che venga ridotto a icona nella tray bar di windows, e resti pronto per l'uso successivo. Per questa funzionalità usiamo la libreria "pystray". Nel menu contestuale dell'icona mettiamo due voci, una di default per mostrare la finestra e una per terminare l'applicazione.</p><pre><code>window.protocol('WM_DELETE_WINDOW', withdraw_window)
window.bind('&lt;Escape&gt;', withdraw_window)

self.menu = (
	pystray.MenuItem('Mostra finestra', show_window, default=True),
	pystray.MenuItem('Termina applicazione', quit_window)
)
    
def withdraw_window():
	window.withdraw()
	icon = pystray.Icon("TCode Helper", logo, "TCode Helper", menu)</code></pre><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2023/01/image-4.png" class="kg-image" alt></figure><p> Il binding globale (quindi sempre attivo, anche mentre il programma non è visibile sullo schermo) serve a richiamare il programma in qualunque momento premento Shift+F12 (combinazione strategicamente scelta perchè, a quanto pare, non è catturata dai programmi che uso normalmente). Il metodo collegato elimina l'icona nella traybar e mostra la finestra, forzandola in primo piano e con il focus sulla casella di ricerca per poter iniziare a digitare immediatamente.</p><pre><code>keyboard.add_hotkey('shift+F12', show_window)

def show_window(self):

	if (hasattr(self, 'icon')):
		self.icon.stop()

	window.withdraw()
	window.deiconify()
	window.lift()

	search_field.selection_range(0, tk.END)

	# Mette la finestra in primo piano, ma senza
	# lasciarla bloccata lì.
	window.attributes("-topmost", True)
	window.attributes("-topmost", False)

	window.focus_set()
	search_field.focus_set()	</code></pre><hr><p>Entrando nel vivo del programma, vediamo come all'avvio abbiamo caricato da un file JSON la lista dei codici. </p><pre><code>with open(tcodes_filename, "r") as file:
	tcodes = json.load(file)</code></pre><p>Per ogni TCODE è presente il tcode vero e proprio, la descrizione breve mostrata nell'interfaccia e una stringa di parole chiave. La ricerca viene effettuata su questa lista, che risulta più accurata perchè evita i match da parole presenti in ogni descrizione ("il", "la", "un", "modifica", "crea" eccetera) e consente di aggiungere più testo per ciascun codice senza obbligare a mostrarlo nella descrizione (per una transazione legata agli ordini di acquisto posso aggiungere parole chiave come "ordine", "acquisto", "oda", "purchase", "order" eccetera). La lista è fatta così:</p><pre><code>[
	...
    {
        "code": "ME22N",
        "descr": "Modifica ordini di acquisto (OdA).",
        "keywords": "ordini acquisto OdA purchase order"
    },
    ...
]</code></pre><p>(e al momento è un pò grezza, ci devo lavorare su e accetto volentieri pull request al riguardo...)</p><p>La funzione di ricerca, collegata al callback sulla scrittura nella casella di testo, segue questa logica: per ciascuna parola trovata nella ricerca, analizza la stringa delle parole chiave in ogni transazione e assegna un punteggio alla transazione stessa. Se trovo un match perfetto (quindi l'esatta parola cercata) assegno due punti alla transazione. Se trovo un match parziale un solo punto. Il match perfetto è individuato con una regexp, il match parziale con un semplice string find.</p><pre><code># Identifica parole chiave ricerca
keywords = [x for x in search.upper().split(' ') if len(x) &gt; 1]

# Inizializza lista per punteggi ai tcodes
tcodes_points = [[x, 0] for x in self.tcodes]

# Calcola punteggi 
for keyword in keywords:
	for tcode in tcodes_points:

		pattern = keyword
		text = tcode[0]['keywords'].upper()

		if (re.search("(^| )" + pattern + "( |$)", text)):
			tcode[1] += 2
			print(pattern, tcode)
		elif tcode[0]['keywords'].upper().find(keyword) != -1:
			tcode[1] += 1</code></pre><p>Alla fine mostro nell'albero i tcodes con almeno un punto, ordinati per punteggio decrescente.</p><pre><code># Ordina e ricostruisce lista tcodes con punteggio non nullo
tcodes_points.sort(key=lambda x: x[1], reverse=True)
for x in tcodes_points:
	x[0]['score'] = x[1] 
filtered_list = [x[0] for x in tcodes_points if x[1] &gt; 0]

# Pulisce albero
for i in self.tree.get_children():
	self.tree.delete(i)

# Aggiunge record in albero
for item in filtered_list:
	self.tree.insert('', tk.END, values=(item['code'], item['descr']))</code></pre><p>Il metodo è orribilmente poco efficiente, e richiede un mostruoso numero di check per ciascuna digitazione nella casella di ricerca, ma trattandosi di una lista di decine (al più centinaia) di codici al momento non vale la pena studiare ottimizzazioni esotiche, poichè il risultato è comunque immediato.</p><hr><p>Bonus per chi è arrivato fino in fondo. Ho imparato a usare pyinstaller, una simpatica libreria python che consente di creare un programma eseguibile per windows (anche altre piattaforme, ma io ho solo windows) a partire da uno script python. Basta richiamarlo così:</p><pre><code>pyinstaller -F --noconsole tcode_helper.py</code></pre><p>Ho provveduto a rendere disponibile su github una release eseguibile del programma (a destra nella schermata di github, sotto "releases") se qualcuno ci vuole giocare senza doversi installare l'ambiente di sviluppo Python.</p><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[Leggere una tabella SAP da Python mediante RFC]]></title><description><![CDATA[Come chiamare una procedura RFC su un sistema SAP per leggere da Python il contenuto di una tabella.]]></description><link>https://mariopiccinelli.it/blog/leggere-una-tabella-sap-da-python-mediante-rfc/</link><guid isPermaLink="false">63ae005078a94a00011a0e47</guid><category><![CDATA[python]]></category><category><![CDATA[sap]]></category><category><![CDATA[rfc]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Thu, 29 Dec 2022 21:48:11 GMT</pubDate><content:encoded><![CDATA[<p>Python è comodo per elaborare dati, ma questi dati bisogna procurarseli in qualche modo. Se i dati sono contenuti in una tabella su un sistema SAP S/4, il metodo più diretto è chiamare un modulo RFC.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/12/image-15.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/12/image-15.png 600w, https://mariopiccinelli.it/blog/content/images/2022/12/image-15.png 688w"></figure><hr><h2 id="requisiti">Requisiti</h2><p>Per chiamare un modulo RFC da Python è necessario installare nel sistema la Netweawer RFC SDK. Questa libreria è scaricabile da <a href="https://launchpad.support.sap.com/#/softwarecenter/template/products/_APP=00200682500000001943&amp;_EVENT=DISPHIER&amp;HEADER=Y&amp;FUNCTIONBAR=N&amp;EVENT=TREE&amp;NE=NAVIGATE&amp;ENR=01200314690100002214&amp;V=MAINT">qui</a>, ma il download richiede un S-USER valido (no free trial) e che sia abilitato al download di risorse. Se il vostro S-USER non è abilitato (come accade di solito) dovete chiedere per piacere agli IT della vostra organizzazione.</p><p>Si installa e si prende nota della cartella di installazione.</p><p>Bisogna poi aggiungere la cartella "lib" della nostra installazione nel path di sistema. Su Windows, si apre il pannello di controllo, voce "Sistema e Sicurezza", voce "Sistema", "Impostazioni di sistema avanzate". Nel tab "Avanzate" si preme il pulsante "Variabili d'ambiente...". Nella parte bassa della finestra (Variabili di Sistema) si fa doppio click su "path", e nella finestra che si apre si aggiunge una riga che punta alla cartella di cui sopra.</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/12/image-17.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/12/image-17.png 600w, https://mariopiccinelli.it/blog/content/images/size/w1000/2022/12/image-17.png 1000w, https://mariopiccinelli.it/blog/content/images/2022/12/image-17.png 1504w" sizes="(min-width: 720px) 720px"></figure><p>Bisogna inoltre installare la libreria Python PYRFC, ad esempio con pip:</p><pre><code>pip install pyrfc</code></pre><hr><p>A questo punto siamo pronti a cominciare. Usando la libreria pyrfc possiamo chiamare qualunque function module abilitato per la comunicazione RFC. Possiamo crearne uno noi, o possiamo usarne uno standard: nel nostro caso esiste già un modulo che permette di leggere il contenuto di una tabella SAP, e si chiama "RFC_READ_TABLE". Possiamo richiamarlo con questo codice:</p><pre><code>from pyrfc import Connection

conn = Connection(
    ashost='xxx.xxx.xxx.xxx', 
    sysnr='00', 
    client='XXX', 
    user='xxxxx', 
    passwd='yyyyy'
)

table='MARA'
delimiter = '|'

response = conn.call(
    "RFC_READ_TABLE", 
    QUERY_TABLE=table, 
    DELIMITER=delimiter,
    FIELDS=[{'FIELDNAME':'MANDT'},{'FIELDNAME':'MATNR'}]
)

conn.close()</code></pre><p><strong>Nota</strong>: questo FM ha parecchie limitazioni, come ad esempio il fatto che ogni record restituito non può essere più lungo di 512 caratteri. In questo caso, visto che la MARA è una tabella con millemila colonne, abbiamo usato il parametro "FIELDS" per limitare le colonne lette.</p><p>A questo punto, nella variabile "response" troviamo un dizionario che contiene, tra le altre, le chiavi DATA e FIELDS.</p><p>La chiave FIELDS contiene l'elenco dei campi letti dalla tabella, con il tipo e la posizione all'interno della risposta. Ogni record della risposta, come vedremo dopo, è infatti costruito come stringa in cui i campi sono separati dal separatore specificato al momento della chiamata, e in FIELDS è presente per ciascun campo il carattere di inizio e la lunghezza.</p><pre><code>print(response['FIELDS'])

[{'FIELDNAME': 'MANDT',
  'OFFSET': '000000',
  'LENGTH': '000003',
  'TYPE': 'C',
  'FIELDTEXT': 'Mandante'},
 {'FIELDNAME': 'MATNR',
  'OFFSET': '000004',
  'LENGTH': '000040',
  'TYPE': 'C',
  'FIELDTEXT': 'Codice materiale'}]</code></pre><p>Nella chiave "DATA" sono invece presenti le righe lette:</p><pre><code>print(response['DATA'])

[{'WA': '100|TG10'},
 {'WA': '100|TG11'},
 {'WA': '100|TG12'},
 {'WA': '100|TG13'},
 {'WA': '100|TG14'},
 {'WA': '100|FG129'},
 {'WA': '100|SM0001'},
 {'WA': '100|NS0002'},
 ....</code></pre><p>A questo punto abbiamo in mano tutto quello che ci serve per gestire queste informazioni in Python. Ad esempio, potremmo usare le informazioni relative ai campi e alla loro lunghezza per trasformare le righe in dizionari:</p><pre><code>data = []

for orig_record in [record['WA'] for record in response['DATA']]:

  parsed_record = {}
  
  for field in response['FIELDS']:
  
    parsed_record[field['FIELDNAME']] = orig_record[int(field['OFFSET']):int(field['OFFSET'])+int(field['LENGTH'])].strip()
    
    data.append(parsed_record)</code></pre><p>Potremmo invece trasformare l'output direttamente in un dataframe Pandas, per facilitare successive analisi. Si può ottenere questo risultato con questo comodo snippet:</p><pre><code>import pandas as pd

result = pd.DataFrame(
    [[x.strip() for x in response['DATA'][n]['WA'].split(delimiter)] for n in range(len(response['DATA']))],
    columns=[x['FIELDNAME'] for x in response['FIELDS']]
)</code></pre><p>Questo il risultato:</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/12/image-13.png" class="kg-image" alt></figure><p>Alla prossima!</p>]]></content:encoded></item><item><title><![CDATA[OAuth in Python per testare servizi su SAP BTP]]></title><description><![CDATA[OAuth2 in Python per accedere a un servizio REST ospitato sulla SAP BTP.]]></description><link>https://mariopiccinelli.it/blog/oauth-in-python-per-testare-servizi-su-sap-btp/</link><guid isPermaLink="false">63a0b43d78a94a00011a0db6</guid><category><![CDATA[btp]]></category><category><![CDATA[python]]></category><category><![CDATA[sap]]></category><category><![CDATA[oauth2]]></category><dc:creator><![CDATA[Mario Piccinelli]]></dc:creator><pubDate>Mon, 19 Dec 2022 19:30:24 GMT</pubDate><content:encoded><![CDATA[<p>Nelle <a href="https://mariopiccinelli.it/blog/protezione-servizi-rest-su-btp-mediante-xsuaa/">scorse puntate</a> ho realizzato un servizio REST su BTP, e l'ho protetto utilizzando il servizio XSUAA della BTP che implementa la protezione mediante OAuth. </p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/12/image-11.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/12/image-11.png 600w, https://mariopiccinelli.it/blog/content/images/2022/12/image-11.png 875w" sizes="(min-width: 720px) 720px"></figure><p>Se il servizio è accessibile da browser, l'approuter si occupa di gestire tutte le fasi dell'autenticazione, compreso il rimando verso la pagina di login di SAP e l'invio del token al servizio. Ma se voglio richiamare il servizio senza passare dal browser, ad esempio usando uno script Python, devo occuparmi io di chiedere al server il token da utilizzare per effettuare le chiamate.</p><p>Lo standard OAuth 2.0 prevede <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/which-oauth-2-0-flow-should-i-use">diversi flussi di autenticazione</a>. In questi esperimenti ne ho provati due, i più semplici: il <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/resource-owner-password-flow">Resource Owner Password Flow</a> e il <a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow">Client Credentials Flow</a>.</p><hr><p>Il <strong>Resource Owner Password Flow</strong> è utilizzato quando il client (ovvero chi vuole accedere al servizio) è sufficientemente fidato da poter avere accesso diretto alle credenziali dell'utente. Non è usati nei normali flussi autorizzativi app-app (ad esempio, se voglio autorizzare Facebook ad accedere alle mie foto su Google Drive, non voglio certo inserire in Facebook la mia password Google!), ma è accettabile se, per esempio, il client è uno script costruito su misura per un utilizzo specifico e in esecuzione in un ambiente privato.</p><p>In questo caso, per ottenere il token autorizzativo si effettua una richiesta di tipo "password" al server XSUAA. Bisogna fornire le credenziali dell'utente e le credenziali del client, così come registrate nel server XSUAA (client id e client server). Queste informazioni sono reperibili nel servizio XSUAA nella BTP (sezione "Instances and Subscriptions"), premendo il pulsante "Show Credentials".</p><figure class="kg-card kg-image-card"><img src="https://mariopiccinelli.it/blog/content/images/2022/12/image-10.png" class="kg-image" alt srcset="https://mariopiccinelli.it/blog/content/images/size/w600/2022/12/image-10.png 600w, https://mariopiccinelli.it/blog/content/images/2022/12/image-10.png 677w"></figure><p>A questo punto, nel nostro script Python possiamo fare questa richiesta <strong>al server di autenticazione</strong>:</p><pre><code>print("Richiesta token con grant: PASSWORD")

token_req_payload = {
  'grant_type': 'password',
  'username': username,
  'password': password,
}

token_response = requests.post(
  auth_server_url + '/oauth/token',
  data = token_req_payload,  
  allow_redirects = False,
  auth = (client_id, client_secret)
)

tokens = json.loads(token_response.text)
access_token = tokens['access_token']</code></pre><p>Se tutto è andato per il verso giusto, ora possiamo chiamare direttamente il nostro servizio (NON l'application router) usando il token:</p><pre><code>test_api_url = "https://XXXXXXX.cfapps.us10-001.hana.ondemand.com/"
api_call_headers = {'Authorization': 'Bearer ' + access_token}
api_call_response = requests.get(test_api_url, headers=api_call_headers)

print(api_call_response.text)</code></pre><p>La richiesta sarà gestita con le autorizzazioni dell'utente di cui abbiamo usato le credenziali.</p><hr><p>Il <strong>Client Credentials Flow</strong> è diverso dal precedente poichè in questo caso non si utilizzano le credenziali di un utente ma solo le credenziali dell'applicazione. Questo flusso è infatti pensato per autenticare un servizio verso un altro servizio, senza dargli per esempio accesso diretto ai dati di un utente. La chiamata da effettuare è più semplice:</p><pre><code>print("Richiesta token con grant: CLIENT_CREDENTIALS")

token_req_payload = {
  'grant_type': 'client_credentials'
}

token_response = requests.post(
  auth_server_url + '/oauth/token',
  data = token_req_payload, 
  allow_redirects = False,
  auth = (client_id, client_secret)
)

tokens = json.loads(token_response.text)
access_token = tokens['access_token']</code></pre><p>Una volta ottenuto il token possiamo continuare come visto in precedenza.</p><hr><p><strong>Gestire permessi a livello di applicazione Python su BTP</strong></p><p>Come visto in passato, la presenza del token non è necessaria per accedere all'applicazione, che comunque è accessibile sulla rete pubblica. Il tassello fondamentale per la sicurezza è che l'applicazione stessa, alla ricezione della richiesta, verifichi (utilizzando il server XSUAA) il token ricevuto per capire se è valido e di che tipo è, e se è legato o meno a un utente (o è un client credentials).</p><p>Una modalità semplice per effettuare questa verifica è prevedere nella nostra applicazione basata su Flask un metodo decorato con "@app.before_request". Questo metodo è eseguito prima di qualunque richiesta in entrata, ed è un buon posto per analizzare le credenziali e salvare le informazioni in una variabile globale accessibile agli altri metodi. Una cosa del genere:</p><pre><code>import ...
from sap import xssec

# Global variables
SECURITY_CONTEXT = "security_context"
globals = {}

@app.before_request
def check_credentials():

  global globals

  auth_header = request.headers.get('Authorization')

  if not auth_header:
    return "Errore: nessun header autorizzativo!", 403

  try:
    auth_token = auth_header.split(" ")[1]
    globals[SECURITY_CONTEXT] = xssec.create_security_context(auth_token, uaa_service)
  except:
    return "Errore: token errato!", 403</code></pre><p>Questo metodo ha questa caratteristica: se ritorna qualcosa, quel qualcosa viene restituito e termina immediatamente la chiamata in corso. Quindi, se troviamo dei problemi possiamo semplicemente restituire un 403 Unauthorized e tutto si ferma qui.</p><p>A questo punto, i nostri singoli endpoint possono analizzare l'oggetto globale e chiamarne metodi per verificare, per esempio, la presenza di uno scope o il nome utente (o l'assenza, nel caso di client credentials). Un esempio potrebbe essere una cosa del genere:</p><pre><code>@app.route('/', methods=['GET'])
def info():

  global globals 

  isAuthorized = globals[SECURITY_CONTEXT].check_scope(f"{uaa_service['xsappname']}.split")
  grant_type = globals[SECURITY_CONTEXT].get_grant_type()
  logon_name = globals[SECURITY_CONTEXT].get_logon_name()

  text = (
    f"&lt;h1&gt;Barcode services up&amp;running!&lt;/h1&gt;"
    f"&lt;ul&gt;"
    f"&lt;li&gt;Split scope available: {isAuthorized}&lt;/li&gt;"
    f"&lt;li&gt;Grant type: {grant_type}&lt;/li&gt;"
    f"&lt;li&gt;Logon name: {logon_name}&lt;/li&gt;"
    f"&lt;/ul&gt;"
  )

  return(text)</code></pre><p>Sarà poi l'applicazione a decidere cosa rendere disponibile all'esterno, ad esempio differenziando per grant type o per nome utente.</p><p>Alla prossima!</p>]]></content:encoded></item></channel></rss>