Operazioni asincrone

Come visto precedentemente, la classe Client già supporta l’esecuzione asincrona di gran parte dei suoi metodi, tramite l’uso delle callback success, error e progress. Esiste però anche un approccio più moderno all’esecuzione asincrona che supporta direttamente il modulo asyncio di Python, in modo da poter usare costrutti come async e await; per fare questo è stata introdotta la classe AsyncClient.

Nota

La classe AsyncClient è disponibile solo se si usa Python versione 3.6 o successive.

E” possibile convertire oggetti di classe Client a oggetti di classe AsyncClient e viceversa tramite i metodi kongalib.Client.as_async() e kongalib.AsyncClient.as_sync(); eventuali connessioni a server e database verranno preservate.

Classe AsyncClient

class kongalib.AsyncClient(impl=None)

La classe AsyncClient, analogamente alla classe Client, permette di connettersi ad un server Konga e di eseguire comandi sullo stesso; la differenza è nel fatto che questa classe è costruita esplicitamente per lavorare con asyncio di Python, ed è disponibile solo se si usa Python >= 3.6.

Tutti i metodi che esistono nella classe Client e che possono essere invocati sia in maniera asincrona (specificando le callback di success, error e progress) che sincrona (generalmente omettendo di specificare la callback di success), nella classe AsyncClient accettano eventualmente la sola callback di progress, in quanto vengono sempre eseguiti in maniera asincrona tramite l’event loop di asyncio, che si assume sia in esecuzione. La progress viene eseguita in un thread separato, ed ha la forma progress(type, completeness, state, userdata); i parametri interessanti di questa callback sono completeness (percentuale di completamento, ossia un numero che varia da 0.0 a 100.0; se -1.0 indica una percentuale di completamento indefinita), state (stringa che specifica l’eventuale stato corrente dell’operazione) e userdata, che è un parametro aggiuntivo che viene normalmente passato alla chiamata asincrona dall’utente per tenere traccia di un eventuale stato.

Come per la classe Client, oggetti di classe AsyncClient possono essere usati come contesti per il costrutto with: all’ingresso del blocco verrà iniziata una transazione, mentre in uscita verrà eseguita una commit o una rollback della stessa a seconda che ci sia stata o meno un’eccezione all’interno del blocco di istruzioni. Da notare che dal momento che siamo in ambito asincrono, andrà usato il costrutto async with al posto del semplice with.

as_sync()

Ritorna un oggetto Client equivalente a questo client, preservando le connessioni già presenti.

authenticate(username, password, progress=None, userdata=None, timeout=180000, new_password=None)

Effettua un accesso al database attivo sulla connessione corrente, identificando l’utente tramite i parametri username e password. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà un dict con informazioni dettagliate sull’utente autenticato, oppure viene lanciata l’eccezione Error in caso di errore.

backup_database(password, backup_name, driver, name, auto=True, overwrite=False, position=0, store_index=False, progress=None, userdata=None, timeout=180000)

Esegue un backup del database specificato sul server attualmente connesso. Se auto è False, è necessario specificare un nome per il backup tramite backup_name, altrimenti il backup viene considerato automatico ed un nome univoco è assegnato dal server. Se overwrite è False ed un backup con lo stesso nome esiste già sul server, non sarà possibile eseguire il backup. position permette di specificare dove eseguire il backup, ed è una combinazione delle costanti kongalib.BACKUP_ON_COMPUTER e kongalib.BACKUP_ON_CLOUD, mentre store_index specifica se includere l’indice di ricerca full-text nel backup. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

begin_transaction(pause_indexing=False, deferred=False)

Inizia una transazione sul database attivo nella connessione corrente. Se pause_indexing è True, l’indicizzazione del database è disabilitata sul server. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

close_database(backup=False, progress=None, userdata=None, timeout=180000)

Chiude il database attivo sulla connessione corrente, restituendo un oggetto asyncio.Future per l’esecuzione asincrona; in caso di errore verrà lanciata l’eccezione Error.

Nota

Se backup è True, il server esegue un backup automatico del database prima di chiuderlo.

code_exists(tablename, code, code_azienda, num_esercizio, extra_where=None)

Controlla l’esistenza del codice code nella tabella tablename per l’azienda e l’esercizio specificati in code_azienda e num_esercizio. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà un valore booleano; in caso di errore verrà lanciata un’eccezione di classe Error.

commit_transaction(resume_indexing=False)

Esegue una COMMIT della transazione sul database attivo nella connessione corrente. Se resume_indexing è True, l’indicizzazione del database è abilitata sul server. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

connect(server=None, host=None, port=0, options=None, timeout=30000, progress=None, userdata=None)

Tenta una connessione ad un server Konga. Il server a cui ci si vuole connettere può essere specificato in due modi: tramite i parametri host e port, oppure tramite un dict server che deve contenere almeno le chiavi host e port. Alternativamente, se server è una stringa e host non è specificato, viene assunta come host. Se host include una specifica di porta e port è 0, port viene ottenuta dalla specifica contenuta nella stringa di host. Il parametro options può essere un dict contenente opzioni aggiuntive per la connessione; al momento le opzioni supportate sono:

  • tenant_key (str): chiave del tenant per stabilire la connessione con un server multitenant.

La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà un dict contenente le informazioni sulla connessione stabilita.

create_database(password, driver, name, desc='', progress=None, userdata=None, timeout=180000)

Crea un nuovo database sul server attualmente connesso; il database avrà nome name e descrizione desc. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà l’UUID del nuovo database; se si verifica un errore viene lanciata l’eccezione Error.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

delete_backup(password, backup_name, position, progress=None, userdata=None, timeout=180000)

Cancella il backup identificato da backup_name dal server connesso. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

delete_database(password, driver, name, delete_cloud_data=None, progress=None, userdata=None, timeout=180000)

Cancella il database specificato. Se delete_cloud_data è None (valore predefinito) la cancellazione sarà negata nel caso ci siano dati binari legati al database al momento presenti nel cloud; in caso contrario i dati binari saranno o meno cancellati dal cloud in base al valore del parametro. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

delete_record(tablename, code=None, id=None, code_azienda=None, num_esercizio=None, log=None, progress=None)

Cancella un record dalla tabella tablename. Il record può essere identificato in due modi: o tramite il solo id, oppure tramite la specifica dei parametri code, code_azienda e num_esercizio. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona; in caso di errore verrà lanciata un’eccezione di classe Error o ErrorList. Al termine dell’operazione, se log è un oggetto di classe OperationLog, esso riceverà ogni eventuale messaggio di log prodotto dal server durante la cancellazione.

disconnect()

Disconnette il server attualmente connesso, oppure non fa nulla se non si è al momento connessi.

fetch_binary(field_or_tablename, id, type, filename=None, check_only=False, progress=None, label=None)

Carica un contenuto binario dal server. field_or_tablename può essere un nome tabella o un campo da cui risolvere il nome tabella; questa tabella unita a id identificano la scheda del database da cui caricare la risorsa; type è uno dei valori della Choice Resources, mentre filename e label hanno senso solo per identificare rispettivamente le risorse di tipo documento ed immagine aggiuntiva. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una tupla di quattro elementi: ( dati, filename, original_filename, checksum ). dati sono i dati binari che sono stati caricati dal server; filename è il nome file interno con cui è identificata la risorsa, original_filename è il nome del file originale che è stato specificato all’atto del salvataggio della risorsa sul server, mentre checksum è un checksum dei dati. Se check_only è True, i dati binari della risorsa non verranno effettivamente caricati dal dispositivo di archiviazione in cui sono depositati, e dati sarà None; questa modalità è utile per verificare l’esistenza di una risorsa e il suo checksum senza effettivamente caricarla da remoto (nel caso di archiviazione su cloud il caricamento potrebbe essere lento).

fetch_image(fieldname, id, type, progress=None, label=None)

Piccolo wrapper alla funzione fetch_binary(), dedicato alle immagini, con l’unica differenza che l’oggetto asyncio.Future restituito una volta completato avrà come valore di ritorno direttamente il contenuto binario dell’immagine.

Esegue una ricerca full-text sul database attivo sulla connessione corrente, limitando la ricerca di text a limit risultati. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una lista di risultati, dove ogni risultato è dict con almeno le chiavi score, tablename, id e display; in caso di errore viene lanciata l’eccezione Error.

get_connection_info()

Restituisce un dict con informazioni sulla connessione corrente, o None se non si è connessi.

get_data_dictionary(progress=None, userdata=None, timeout=180000)

Restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà il dizionario dei dati disponibile sul server attualmente connesso, sotto forma di oggetto di classe kongalib.DataDictionary.

get_id()

Restituisce un ID numerico univoco assegnato dal server alla connessione con questo client, o 0 se non si è connessi.

get_record(tablename, code=None, id=None, field_names=None, row_extra_field_names=None, code_azienda=None, num_esercizio=None, mode=None, mask_binary=None, flags=287, progress=None)

Ottiene il record completo della tabella tablename, sotto forma di dict. Il record può essere identificato in due modi: o tramite il solo id, oppure tramite la specifica dei parametri code, code_azienda e num_esercizio. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà un dict con il record ottenuto; verrà lanciata un’eccezione Error in caso di errore.

index_database(password, driver, name, reset=False, run=True, progress=None, userdata=None, timeout=180000)

Esegue una indicizzazione del database specificato sul server attualmente connesso. Se reset è False, l’indicizzazione è incrementale, ovvero l’indice viene modificato per tenere conto solo dei record inseriti, modificati o cancellati dall’ultima indicizzazione; se invece reset è True l’indice viene prima cancellato e poi, se run è anch’esso True, viene ricreato completamente. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

insert_record(tablename, data, code_azienda=None, num_esercizio=None, log=None, progress=None)

Inserisce un nuovo record nella tabella tablename. Il nuovo record, i cui dati sono passati nel dict data, sarà un record condiviso con tutte le aziende del database se code_azienda e num_esercizio sono None, altrimenti apparterrà solo all’azienda e all’esercizio specificati. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una tupla nella forma (id, code), dove id è l’ID univoco assegnato al record dal server, mentre code è il codice del record (che può essere diverso da quello passato in data se sono attivi i codici automatici per tablename); in caso di errore verrà lanciata un’eccezione di classe Error o ErrorList. Al termine dell’operazione, se log è un oggetto di classe OperationLog, esso riceverà ogni eventuale messaggio di log prodotto dal server durante l’inserimento.

interrupt()

Interrompe tutte le operazioni al momento in esecuzione da parte di questo client.

list_backups(position=0, progress=None, userdata=None, timeout=180000)

Ottiene la lista dei backup disponibili sul server connesso. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una lista di backup; ogni backup è un dict che contiene almeno le chiavi backup_name, uuid, date e size; se si verifica un errore viene lanciata l’eccezione Error.

list_binaries(field_or_tablename, id, type=None, progress=None, full=False)

Ottiene la lista dei dati binari associati ad una scheda del database, identificata da field_or_tablename (che può essere un nome tabella o un campo da cui risolvere il nome tabella) e id. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una lista di tuple, in cui la n-esima tupla ha la forma ( Tipo, NomeAllegato, NomeOriginale ); Tipo è un intero ed è uno dei valori della Choice Resources, NomeAllegato è il nome assegnato internamente a Konga per identificare univocamente il contenuto binario, mentre NomeOriginale è il nome del file originale da cui è stato caricato il contenuto. Se type è specificato, la funzione filtrerà i risultati in baso ad esso, ritornando solo le tuple con il Tipo corretto. Se full è True la n-esima tupla ritornata avrà un valore in più corrispondente all’etichetta dell’immagine aggiuntiva se specificata.

list_databases(driver=None, quick=False, progress=None, userdata=None, timeout=180000)

Restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà la lista dei database disponibili sul server corrente, appartenenti a tutti o ad uno specifico driver. La lista viene tornata sotto forma di dict, le cui chiavi sono i nomi dei driver e i valori le liste dei database appartenenti allo specifico driver. Ogni database nelle liste è un dict che contiene almeno le chiavi name, desc, uuid, created_ts e modified_ts. L’eccezione Error viene lanciata se si verifica un errore. Se quick è True, la funzione ritorna il più velocemente possibile ma la scansione dei database disponibili potrebbe risultare ancora incompleta.

list_drivers(configured=True, progress=None, userdata=None, timeout=180000)

Restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà la lista dei driver di database presenti sul server attualmente connesso, oppure lancia un’eccezione Error su errore. Ogni elemento della lista restituita è un dict che comprende la chiavi name, version e description. Se configured è False, tutti i driver installati sul server sono restituiti, altrimenti verranno restituite solo le informazioni sui driver configurati correttamente ed in esecuzione sul server.

list_servers(timeout=5000, port=0, progress=None, userdata=None)

Esegue una scansione della rete locale alla ricerca dei server Konga disponibili, attendendo al massimo timeout millisecondi per una risposta. port specifica la porta da cui far partire la scansione (default = 51967); sono controllate le successive 10 porte UDP con intervallo di 20 porte (quindi di default vengono scansionate le porte 51967, 51987, 52007, … 52147). La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una lista di dict, le cui chiavi principali sono host, port, name e description.

lock_resource(command, row_id=None)

Tenta di eseguire il blocco della risorsa identificata da command. Se row_id è diverso da None, è possibile eseguire il blocco di una singola riga di una tabella del database. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una tupla (result, owner_data), dove owner_data è un dict contenente informazioni sull’utente che detiene già il blocco della risorsa in caso fosse già bloccata, oppure lancia un’eccezione Error in caso di errore.

open_database(driver, name, progress=None, userdata=None, timeout=180000)

Apre un database rendendolo il database attivo per la connessione corrente. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà un dict con le informazioni sul database connesso, oppure viene lanciata l’eccezione Error in caso di errore.

optimize_database(password, driver, name, progress=None, userdata=None, timeout=180000)

Esegue una ottimizzazione del database specificato sul server attualmente connesso. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

query(query, native=False, full_column_names=False, collapse_blobs=False, progress=None, userdata=None, timeout=180000)

Esegue una query SQL sul database attivo nella connessione corrente. Se native è True, la query viene passata al driver del database senza essere interpretata, permettendo l’esecuzione di query native per l’RDBMS sottostante. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una tupla nella forma (affected_rows, column_names, result_set); affected_rows è il numero di righe coinvolte nella query di UPDATE/DELETE, column_names è una lista di nomi di colonne per il result set, mentre result_set è una lista di righe risultati della query, dove ogni riga è una lista di valori corrispondenti alle colonne restituite in column_names. In caso di errore viene lanciata l’eccezione Error.

Nota

Se full_column_names è False, column_names includerà i nomi delle colonne senza nome tabella, altrimenti saranno inclusi i nomi completi delle colonne. Se collapse_blobs è True, i dati di tipo BLOB binari verranno restituiti come [...].

repair_database(password, driver, name, output, progress=None, userdata=None, timeout=180000)

Prova a riparare il database danneggiato specificato, salvando il database recuperato in output. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Nota

Non tutti i driver di database supportano questa operazione, e il recupero del database potrebbe fallire in ogni caso.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

restore_database(password, backup_name, driver, name, change_uuid=True, overwrite=False, position=0, restore_index=True, progress=None, userdata=None, timeout=180000)

Ripristina un database a partire da un backup effettuato in precedenza sul server connesso. Se overwrite è False ed esiste un database gestito da driver con lo stesso nome, la funzione riporterà errore. change_uuid specifica se cambiare l’UUID associato al database oppure se ripristinare quello originale; se si hanno database con lo stesso nome gestiti da driver diversi è opportuno che almeno l’UUID per essi sia diverso, altrimenti si può incorrere in problemi di aliasing. position specifica da dove prendere il backup da rispristinare, e deve essere una delle costanti kongalib.BACKUP_ON_COMPUTER o kongalib.BACKUP_ON_CLOUD; restore_index invece permette di specificare se ripristinare o meno l’indice di ricerca qualora fosse contenuto all’interno del backup. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

rollback_transaction(resume_indexing=False)

Esegue un ROLLBACK della transazione sul database attivo nella connessione corrente. Se resume_indexing è True, l’indicizzazione del database è abilitata sul server. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

select_data(tablename, fieldnamelist=None, where_expr=None, order_by=None, order_desc=False, offset=0, count=None, get_total=False, exist=None, progress=None)

Genera ed esegue una SELECT sul server per ottenere una lista di risultati, a partire dalla tabella tablename. fieldnamelist è una lista di nomi dei campi da ottenere; se un campo fk_X di tablename è una foreign key, si può accedere ai campi della tabella collegata Y specificando «fk_X.Campo_di_Y»; la JOIN corrispondente verrà generata e gestita automaticamente dal server. Analogamente, si possono creare catene di JOIN implicite facendo riferimenti multipli di campi foreign key, per esempio «fk_X.fk_Y.fk_Z.Campo_di_Z».

Se where_expr non è None, può essere il corpo di una espressione WHERE SQL, e può contenere riferimenti nella stessa forma di fieldnamelist, per esempio «(Campo_di_X = 1) AND (fk_X.Campo_di_Y > 5)».

order_by può essere un nome di campo per cui ordinare i risultati, dove order_desc specifica se ordinare in modo ascendente o discendente. offset e count permettono di restituire risultati solo a partire dal numero offset, e limitandosi a count risultati.

La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato dipende dal valore del parametro get_total. Se get_total è True, il risultato sarà una tupla nella forma (result_set, total_rows, exist_results); total_rows sarà il numero totale di righe come se offset e limit non fossero stati specificati, mentre exist_results sarà un dict le cui chiavi saranno gli ID specificati nel parametro exist, e i valori saranno True o False a seconda che il corrispettivo ID sia presente nel database per la tabella tablename oppure no. Se get_total è False, il risultato sarà il solo result_set, ossia una lista di righe risultato della query, dove ogni riga è una lista di valori.

select_data_as_dict(tablename, fieldnamelist=None, where_expr=None, order_by=None, order_desc=False, offset=0, count=None, get_total=False, progress=None)

Esattamente come select_data(), ma l’oggetto asyncio.Future restituito una volta completato ritornerà un result_set sotto forma di lista di dict, anzichè una lista di liste.

store_binary(field_or_tablename, id, type, filename=None, original_filename=None, data=None, desc=None, force_delete=False, code_azienda=None, progress=None, label=None)

Salva un contenuto binario sul server. field_or_tablename può essere un nome tabella o un campo da cui risolvere il nome tabella; questa tabella unita a id identificano la scheda a cui abbinare la risorsa; type è uno dei valori della Choice*``Resources``; *filename permette di specificare un nome file interno con cui identificare la risorsa (se None il server genererà un nome univoco automaticamente); original_filename è il nome file originale i cui dati si stanno salvando sul server; data sono i dati binari effettivi; desc è la descrizione da abbinare alla risorsa; code_azienda infine identifica l’azienda su cui si sta operando. Per le risorse di tipo immagine aggiuntiva è necessario specificare una label da abbinare all’immagine per identificarla univocamente. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà il nome del file interno usato dal server per identificare la risorsa, che come detto sopra è uguale a filename se quest’ultimo è diverso da None, altrimenti sarà il nome file generato dal server. Se data è None, la funzione cancella i dati binari associati alla scheda; force_delete in questo caso può essere True se si desidera cancellare il riferimento ai dati anche se i dati non sono raggiungibili dal server.

unlock_resource(command, row_id=None)

Rilascia il blocco della risorsa identificata da tablename e row_id. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona, e verrà lanciata l’eccezione Error in caso di errore.

update_record(tablename, data, code=None, id=None, code_azienda=None, num_esercizio=None, log=None, progress=None)

Aggiorna un record esistente nella tabella tablename. Il record, i cui dati da aggiornare sono passati nel dict data, può essere identificato in due modi: o tramite il solo id, oppure tramite la specifica dei parametri code, code_azienda e num_esercizio. La funzione restituisce un oggetto asyncio.Future per l’esecuzione asincrona; in caso di errore verrà lanciata un’eccezione di classe Error o ErrorList. Al termine dell’operazione, se log è un oggetto di classe OperationLog, esso riceverà ogni eventuale messaggio di log prodotto dal server durante l’aggiornamento.

upgrade_database(password, driver, name, progress=None, userdata=None, timeout=180000)

Aggiorna il database specificato all’ultima versione disponibile. La funzione restituisce un oggetto asyncio.Future il cui risultato una volta completato sarà una tupla (log, old_version, new_version), dove il log dell’operazione è sotto forma di una lista di stringhe, oppure viene lanciata l’eccezione Error in caso di errore.

Avvertimento

E” necessaria la password del server per poter eseguire questa operazione.

Esempi

Di seguito vengono riportati alcuni esempi di utilizzo della classe AsyncClient; si assume che ci sia un server Konga disponibile su localhost e che esista su di esso un database SQLite «demo» inizializzato con i dati di esempio.

Esempio di connessione e lista dell’archivio clienti presenti sul database:

import asyncio
import kongalib

async def main():
        c = kongalib.AsyncClient()
        await c.connect(host='localhost')
        await c.open_database('sqlite', 'demo')
        await c.authenticate('admin', '')
        results = await c.select_data('EB_ClientiFornitori', ['Codice', 'RagioneSociale'], 'Tipo = 1')
        for row in results:
                print(row)

asyncio.run(main())

Esempio di come ottenere tutto il record di un documento fiscale (testata e righe):

import asyncio
import kongalib

def print_data(data, indent=1):
        # Stampa i dati, tralasciando le chiavi speciali (che cominciano con '@')
        lines = [ '%s: %s' % (key, value) for key, value in data.items() if not key.startswith('@') ]
        print("\n".join((indent * "\t") + line for line in lines))

async def main():
        c = kongalib.Client()
        await c.connect(host='localhost')
        await c.open_database('sqlite', 'demo')
        await c.authenticate('admin', '')
        record = await c.get_record('EB_DocumentiFiscali', 11, code_azienda='00000001')
        print("Testata:")
        print_data(record)
        rows = record.get('@rows', [])
        for i, row in enumerate(rows):
                print("\tRiga %d:" % (i + 1))
                print_data(row, 2)

asyncio.run(main())