OUT OF THE BOX

Quando nel 2006 abbiamo deciso di portare le nostre applicazioni in ambito web ci siamo resi conto che non esisteva nulla di idoneo a fornire nel browser la stessa esperienza d’uso e la stessa interattività cui erano abituati i nostri utenti nelle loro applicazioni desktop.

Non trovando nulla di pronto abbiamo dovuto pensare a come ottenere un framework web che fosse facile da usare e in grado di produrre in tempi rapidi applicazioni complesse, manutenibili, personalizzabili e che dessero agli utenti la stessa esperienza d’uso cui erano abituati.

Dovevamo insomma pensare fuori dagli schemi, ed inventarci l’uno dopo l’altro gli strumenti con cui costrire il nostro framework.

Per questa ragione abbiamo deciso di intitolare “Out of the box” questa serie di articoli in cui esamineremo proprio quelle idee che rendono Genropy così diverso dagli altri strumenti normalmente usati per fare applicativi web.

E partiamo dalle Bag, il contenitore di dati che costituisce l’ossatura portante di Genropy.

Bag

Gerarchico è meglio

Genropy usa un paradigma data driven per descrivere sotto forma di strutture di dati le varie parti dell’applicazione lasciando poi al framework il compito di interpretare questi dati per trasformarli in codice funzionante.

Queste strutture, chiamate Bag, possono essere immaginate come dei contenitori a cui si accede tramite un percorso gerarchico usando API disponibili sia in Python che in Javascript. In questo modo lo sviluppatore usa sia lato server che lato client lo stesso paradigma agevolando non solo lo sviluppo dell’applicativo ma anche il trasferimento dei dati.

Senza entrare in dettagli tecnici possiamo dire che le Bag rappresentano l’ossatura di tutto Genropy e quindi per capire come funziona il framework è necessario darne almeno una breve descrizione.

Vediamo quindi un esempio di Bag in Python:

 >>>from gnr.core.gnrbag import Bag
 >>>mybag=Bag()
 >>>mybag.setItem('alfa.beta.name','John')
 >>>mybag.setItem('alfa.beta.age', 34)
 >>>mybag.getItem('alfa.beta.name')
'John'
 >>> mybag.getItem('alfa.beta').keys()
 ['name', 'age']

Il modo migliore per vedere il contenuto di una bag è quello di chiederne la rappresentazione XML, tramite il metodo toXml:

>>> print(mybag.toXml()
    <?xml version="1.0" encoding="utf-8"?>
    <GenRoBag>
          <alfa>
                 <beta>
                    <name>John</name>
                    <age _T="L">34</age>
                 </beta>
          </alfa>
    </GenRoBag>
>>>

Vediamo ora lo stesso esempio in Javascript:

var mybag=new gnr.GnrBag
mybag.setItem('alfa.beta.name','John')
mybag.setItem('alfa.beta.age', 34)
mybag.getItem('alfa.beta.name')
"John"
mybag.getItem('alfa.beta').keys()
["name", "age"]

mybag.toXml()
"<?xml version="1.0" encoding="utf-8"?>
 <GenRoBag>
   <alfa>
     <beta>
        <name>John</name>
        <age _T="L">34</age>
     </beta>
   </alfa>
 </GenRoBag>"

Nella versione Python i comandi setItem e getItem possono essere rimpiazzati da una sintassi con parentesi quadre:

>>>mybag['alfa.beta.age'] = 34
>>>print (mybag['alfa.beta.age'])
34
>>>

Una Bag è composta da “nodi di bag”: BagNode. Un nodo ha un’ etichetta che lo identifica, un valore e può anche avere degli attributi. Gli attributi possono essere inseriti usando la setItem, come parametri nominati aggiuntivi.

Ad esempio aggiungiamo il campo indirizzo di cui diamo anche latitudine e longitudine come attributi:

>>>mybag.setItem('alfa.beta.indirizzo','Via Roma 13',
                       latitudine=45.4456761,
                       longitudine=9.0906981)

Ma possiamo anche accedere ad essi in modo diretto attraverso i metodi setAttr e getAttr

>>>mybag.setAttr('alfa.beta.indirizzo', piano=4, interno=21)
>>>mybag.getAttr('alfa.beta.indirizzo', 'latitudine')
45.4456761
>>>

Possiamo leggere gli attributi aggiungendo al percorso gerarchico del nodo a cui si trovano, il nome attributo dopo il carattere ‘?’. Questo path può essere utilizzato dal metodo getItem oppure, limitatamente a Python nella sintassi con le parentesi quadre:

>>>mybag.getItem('alfa.beta.indirizzo?latitudine')
45.4456761
>>>mybag['alfa.beta.indirizzo?longitudine']
9.0906981

Quando creiamo una bag possiamo inizializzarne il contenuto, ad esempio da un XML:

>>>mybagxml=mybag.toXml()
>>>newbag=Bag(mybagxml)

In questo caso avremo una nuova bag che avrà lo stesso contenuto di mybag.

Possiamo anche inizializzare una Bag da un file del filesystem:

>>>mybag.toXml('mybag.xml')
>>>newbag=Bag(('mybag.xml')

In questo caso il primo comando avrà scritto nel file system il documento ‘mybag.xml’ e il secondo avrà popolato newbag a partire dallo stesso documento.

Quindi le bag possono essere serializzate e de-serializzate facilmente con i comandi toXml e fromXml ma il contenuto, a differenza di un xml standard, risulta tipizzato. Quindi se salveremo un intero (ad esempio age) troveremo nell’xml il tipo e la rilettura ripristinerà il tipo desiderato.

Una bag come XML può facilmente essere salvata nel file system, in un campo di database, oppure ricevuta o spedita tramite una chiamata di rete.

Una bag può essere anche popolata da un URL, ad esempio accedendo ad un servizio di rete (in questo caso si tratta di openweathermap):

   >>>tempo_a_milano=Bag("http://api.openweathermap.org/data/2.5/weatherappid=mysecretappid&q=milano&mode=xml&units=metric")

>>>print (tempo_a_milano)
 0 - (Bag) current:
 0 - (Bag) city: <id='6542283' name='Milan'>
     0 - (str) coord:   <lat='45.47' lon='9.19'>
     1 - (unicode) country: IT
     2 - (str) sun:   <rise='2019-04-25T04:21:24' set='2019-04-25T18:20:49'>
 1 - (str) temperature:   <max='18.89' unit='celsius' value='16.26' min='14.44'>
 2 - (str) humidity:   <unit='%' value='87'>
 3 - (str) pressure:   <unit='hPa' value='1019'>
 4 - (Bag) wind:
     0 - (str) speed:   <name='Gentle Breeze' value='3.6'>
     1 - (str) gusts:
     2 - (str) direction:   <code='ESE' name='East-southeast' value='120'>
 5 - (str) clouds:   <name='broken clouds' value='75'>
 6 - (str) visibility:   <value='7000'>
 7 - (str) precipitation:   <unit='1h' mode='rain' value='1.52'>
 8 - (str) weather:   <number='501' value='moderate rain' icon='10d'>
 9 - (str) lastupdate:   <value='2019-04-25T10:55:07'>

Da questo esempio notiamo che nella Bag oltre ai valori sono presenti degli attributi. Ad esempio:

temperature:   <max='18.89' unit='celsius' value='16.26' min='14.44'>

Quindi nel nodo di Bag temperature il valore è None ma sono definiti 4 attributi ovvero max, min, unit, value.

Prendiamo ad esempio la temperatura a Milano:

>>>print (tempo_a_milano['current.temperature?value'])
16.26

La funzione digest consente di estrarre secondo un’opportuna sintassi elementi dalla bag in modo da facilitare le elaborazioni successive.

Prendiamo, ad esempio, con la funzione digest tutte le grandezze disponibili con relativa unità di misura:

>>>tempo_a_milano['current'].digest('#k,#a.unit, #a.value')
[(u'city', None, None),
 (u'temperature', u'celsius', u'16.26'),
 (u'humidity', u'%', u'87'),
 (u'pressure', u'hPa', u'1019'),
 (u'wind', None, None),
 (u'clouds', None, u'75'),
 (u'visibility', None, u'7000'),
 (u'precipitation', u'1h', u'1.52'),
 (u'weather', None, u'moderate rain'),
 (u'lastupdate', None, u'2019-04-25T10:55:07')]

Possiamo accedere in lettura ad un elemento della Bag anche dalla posizione tramite il simbolo # seguito dall’indice in base 0.

Ad esempio:

>>>print(mybag['alfa.#0.#1'])
34

Una bag di norma può essere navigata solo in senso discendente ma tramite il comando setBackRef possiamo abilitare la navigazione ascendente con il segmento speciale ‘#^’.

Ad esempio:

>>>mybag.setBackRef()
>>>mybag['alfa.gamma.color']='red'

>>>beta=mybag['alfa.beta']

>>>print (beta['name'])
John

>>>print (beta['#^.gamma.color'])
red

Il segmento speciale ‘#^’ invece di seguire un ramo discendente della Bag risale al nodo superiore. Usando percorsi misti possiamo navigare la Bag in qualunque modo.

Senza ulteriormente dilungarci con esempi possiamo dire che le Bag sono quindi un ottimo contenitore gerarchico che consente di annidare informazioni in modo facile ed intuitivo fornendo della API coerenti per l’estrazione di dati e anche per il caricamento da fonti esterne.

Resolver

Dinamico è meglio

Uno dei vantaggi di una bag è quello ammettere come contenuto di un nodo non solo dei dati ma anche degli oggetti (chiamati resolver) in grado di recuperare o calcolare il valore nel momento in cui viene richiesto.

Supponiamo di voler creare una bag che ci possa dare le condizioni metereologiche sempre aggiornate. Per prima cosa creiamo una classe resolver

from gnr.core.gnrbag import BagResolver
class MeteoResolver(BagResolver):
    apikey='mysecretkey'
    url="http://api.openweathermap.org/data/2.5/weather?appid=%(apikey)s&q=%(city)s&mode=xml&units=metric"

    def load(self):
        return Bag(self.url%dict(apikey=self.apikey,city=self.city))['current']

E infine creiamo una bag dove prepariamo dei resolver per le città desiderate:

>>>meteo=Bag()
>>>for city in ('milano','torino','roma','firenze','como'):
...     meteo[city]=MeteoResolver(city=city)

>>>meteo.keys()
['milano', 'torino', 'roma', 'firenze', 'como']

In questo momento non abbiamo ancora fatto alcuna chiamata per leggere i dati e potremmo creare un gran numero di nodi senza avere rallentamenti dovuti alle richieste di rete.

Inoltre nel codice non è necessario fare alcuna chiamata specifica: basta infatti leggere il dato dalla bag per averlo in modo completamente trasparente. Ad esempio per conoscere la temperatura di Torino scriveremo:

>>>print (meteo['milano.temperature?value'])
15.37

Ogni volta che accederemo ad elementi interni scatteranno i resolver necessari e avremo sempre dei dati aggiornati.

Usando digest vediamo tutte le temperature:

>>>meteo.digest('#k,#v.temperature?value')
[('milano', u'15.41'), ('torino', u'13.61'), ('roma', u'20.34'), ('firenze', u'17.93'), ('como', u'14.82')]

Ogni volta che accediamo a dei valori interni ad un resolver genereremo una nuova chiamata di rete che reperirà nuovamente i dati. Nella maggioranza dei casi però non ci serve avere valori così aggiornati e quindi possiamo avvalerci di una funzionalità di cache offerta dai resolver.

Modifichiamo quindi la nostra bag meteo aggiungendo il parametro cacheTime:

>>>meteo=Bag()
>>>for city in ('milano','torino','roma','firenze','como'):
...     meteo[city]=MeteoResolver(city=city,cacheTime=3600)
>>>

In questo caso una volta letto un valore per una città, tutti i dati della “sotto-bag” relativa saranno considerati validi per 3600 secondi e quindi i dati meteo di ogni città saranno come massimo vecchi di un’ora. Avremo però evitato continue chiamate di rete per accedere al servizio esterno.

Trigger

Essere avvisati è meglio

Una bag può essere anche vista come un contenitore in grado di eseguire delle azioni ogni volta che viene cambiato il contenuto.

Questa funzionalità si attiva con il comando subscribe che può essere messo sia alla bag di radice che in qualunque sotto-bag.

Le sottoscrizioni possono essere relative a specifici eventi (insert, update, delete) oppure relative a tutti (any). Inoltre alla stessa bag o sotto-bag possiamo aggiungere varie sottoscrizioni con nomi diversi.

Vediamo subito un esempio. Ci proponiamo di farci mostrare tutti gli eventi che accadono in una bag.

Creiamo innanzitutto una funzione logEvents:

>>>def logEvents(**kwargs):
...     print (kwargs)

>>>mybag=Bag()
>>>mybag.subscribe('mylogger',any=logEvents)

>>>mybag['alfa']='foo'
{'node': BagNode : alfa at 4424525520, 'ind': 0, 'reason': None, 'evt': 'ins', 'pathlist': []}

>>>mybag['alfa']='bar']
{'node': BagNode : alfa at 4424525520, 'reason': None, 'evt': 'upd_value', 'pathlist': ['alfa'], 'oldvalue': 'foo'}

mybag.setAttr('alfa',color='red',size=67)
{'node': BagNode : alfa at 4424525520, 'reason': True, 'evt': 'upd_attrs', 'pathlist': ['alfa'], 'oldvalue': None}

Proviamo ora ad inserire in un path annidato:

>>>mybag['this.is.a.nested.bag']=456
{'node': BagNode : this at 4424493328, 'ind': 1, 'reason': 'autocreate', 'evt': 'ins', 'pathlist': []}
{'node': BagNode : is at 4424491472, 'ind': 0, 'reason': 'autocreate', 'evt': 'ins', 'pathlist': ['this']}
{'node': BagNode : a at 4424491856, 'ind': 0, 'reason': 'autocreate', 'evt': 'ins', 'pathlist': ['this', 'is']}
{'node': BagNode : nested at 4424491152, 'ind': 0, 'reason': 'autocreate', 'evt': 'ins', 'pathlist': ['this', 'is', 'a']}
{'node': BagNode : bag at 4424491408, 'ind': 0, 'reason': None, 'evt': 'ins', 'pathlist': ['this', 'is', 'a', 'nested']}

Vediamo quindi che la nostra funzione di logEvent è chiamata ad ogni inserimento e ci fornisce gli elementi necessari per svolgere i compiti desiderati.

Conclusione

Le Bag si sono dimostrate uno strumento estremamente versatile perchè ci hanno consentito usare un unico modello concettuale sia nella definizione del database che nella creazione della GUI, delle stampe e in moltissime altre aree del framework.

Il loro uso però non è limitato a Genropy e crediamo che anche in altri contesti potrebbero rivelarsi uno strumento molto utile ad affrontare anche problematiche diverse.