kongalib 2.0.5__cp314-cp314-macosx_10_15_universal2.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _kongalib.cpython-314-darwin.so +0 -0
- kongalib/__init__.py +394 -0
- kongalib/async_client.py +813 -0
- kongalib/client.py +1045 -0
- kongalib/constants.json +1 -0
- kongalib/constants.py +187 -0
- kongalib/data_dictionary.py +203 -0
- kongalib/db.py +267 -0
- kongalib/expression.py +841 -0
- kongalib/json.py +114 -0
- kongalib/lex.py +1058 -0
- kongalib/scripting.py +766 -0
- kongalib/yacc.py +3276 -0
- kongalib-2.0.5.dist-info/METADATA +150 -0
- kongalib-2.0.5.dist-info/RECORD +21 -0
- kongalib-2.0.5.dist-info/WHEEL +6 -0
- kongalib-2.0.5.dist-info/licenses/LICENSE +165 -0
- kongalib-2.0.5.dist-info/top_level.txt +4 -0
- kongalib-2.0.5.dist-info/zip-safe +1 -0
- kongaui.py +507 -0
- kongautil.py +581 -0
kongaui.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# _ _ _ _
|
|
3
|
+
# | | | (_) |
|
|
4
|
+
# | | _____ _ __ __ _ __ _| |_| |__
|
|
5
|
+
# | |/ / _ \| '_ \ / _` |/ _` | | | '_ \
|
|
6
|
+
# | < (_) | | | | (_| | (_| | | | |_) |
|
|
7
|
+
# |_|\_\___/|_| |_|\__, |\__,_|_|_|_.__/
|
|
8
|
+
# __/ |
|
|
9
|
+
# |___/
|
|
10
|
+
#
|
|
11
|
+
# Konga client library, by EasyByte Software
|
|
12
|
+
#
|
|
13
|
+
# https://github.com/easybyte-software/kongalib
|
|
14
|
+
|
|
15
|
+
from __future__ import print_function
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import os, os.path
|
|
19
|
+
import atexit
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
import textwrap
|
|
23
|
+
import string
|
|
24
|
+
import colorama
|
|
25
|
+
|
|
26
|
+
import kongautil
|
|
27
|
+
|
|
28
|
+
from kongalib.scripting import _TimeoutBlocker
|
|
29
|
+
from kongalib.scripting import proxy as _proxy
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
BUTTON_OK = 0x0001 #: Bottone "Ok"
|
|
34
|
+
BUTTON_YES = 0x0002 #: Bottone "Si"
|
|
35
|
+
BUTTON_YES_ALL = 0x0004 #: Bottone "Si tutti"
|
|
36
|
+
BUTTON_NO = 0x0008 #: Bottone "No"
|
|
37
|
+
BUTTON_NO_ALL = 0x0010 #: Bottone "No tutti"
|
|
38
|
+
BUTTON_CANCEL = 0x0020 #: Bottone "Annulla"
|
|
39
|
+
BUTTON_OPEN = 0x0040 #: Bottone "Apri"
|
|
40
|
+
BUTTON_SAVE = 0x0080 #: Bottone "Salva"
|
|
41
|
+
BUTTON_SAVE_ALL = 0x0100 #: Bottone "Salva tutti"
|
|
42
|
+
BUTTON_CLOSE = 0x0200 #: Bottone "Chiudi"
|
|
43
|
+
BUTTON_DISCARD = 0x0400 #: Bottone "Tralascia"
|
|
44
|
+
BUTTON_APPLY = 0x0800 #: Bottone "Applica"
|
|
45
|
+
BUTTON_RESET = 0x1000 #: Bottone "Ripristina"
|
|
46
|
+
BUTTON_ABORT = 0x2000 #: Bottone "Interrompi"
|
|
47
|
+
BUTTON_RETRY = 0x4000 #: Bottone "Riprova"
|
|
48
|
+
BUTTON_IGNORE = 0x8000 #: Bottone "Ignora"
|
|
49
|
+
|
|
50
|
+
ICON_ERROR = 24 #: Icona di errore
|
|
51
|
+
ICON_QUESTION = 25 #: Icona di domanda
|
|
52
|
+
ICON_WARNING = 26 #: Icona di avviso
|
|
53
|
+
ICON_INFORMATION = 27 #: Icona informativa
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _shutdown():
|
|
58
|
+
try:
|
|
59
|
+
_proxy.ui.shutdown()
|
|
60
|
+
except:
|
|
61
|
+
pass
|
|
62
|
+
atexit.register(_shutdown)
|
|
63
|
+
|
|
64
|
+
if not _proxy.is_valid():
|
|
65
|
+
colorama.init()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _get_term_width():
|
|
70
|
+
import shutil
|
|
71
|
+
return shutil.get_terminal_size(fallback=(80, 24))[0]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def message_box(text, title='', buttons=BUTTON_OK, icon=ICON_INFORMATION):
|
|
76
|
+
"""Mostra una finestra di dialogo modale con titolo *title*, messaggio *text* e icona *icon* (una delle costanti ``ICON_*``). La finestra mostrerà i bottoni identificati
|
|
77
|
+
da *buttons*, che può contenere uno o più costanti ``BUTTON_*`` in *or* tra loro, e ritornerà la costante relativa al bottone selezionato dall'utente per chiudere la
|
|
78
|
+
finestra."""
|
|
79
|
+
if _proxy.is_valid():
|
|
80
|
+
with _TimeoutBlocker():
|
|
81
|
+
return _proxy.ui.message_box(text, title, buttons, icon)
|
|
82
|
+
else:
|
|
83
|
+
print()
|
|
84
|
+
if icon == ICON_WARNING:
|
|
85
|
+
title = colorama.Fore.YELLOW + "WARNING" + (': ' if title else '') + colorama.Fore.RESET + (title or '')
|
|
86
|
+
elif icon == ICON_ERROR:
|
|
87
|
+
title = colorama.Fore.RED + "ERROR" + (': ' if title else '') + colorama.Fore.RESET + (title or '')
|
|
88
|
+
if title:
|
|
89
|
+
print(' ' + colorama.Style.BRIGHT + textwrap.fill(title, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
90
|
+
print()
|
|
91
|
+
print(textwrap.fill(text, width=_get_term_width() - 1))
|
|
92
|
+
print()
|
|
93
|
+
buttons_info = [
|
|
94
|
+
( BUTTON_OK, 'ok' ),
|
|
95
|
+
( BUTTON_YES, 'yes' ),
|
|
96
|
+
( BUTTON_YES_ALL, 'yes all' ),
|
|
97
|
+
( BUTTON_NO, 'no' ),
|
|
98
|
+
( BUTTON_NO_ALL, 'no all' ),
|
|
99
|
+
( BUTTON_CANCEL, 'cancel' ),
|
|
100
|
+
( BUTTON_OPEN, 'open' ),
|
|
101
|
+
( BUTTON_SAVE, 'save' ),
|
|
102
|
+
( BUTTON_SAVE_ALL, 'save_all' ),
|
|
103
|
+
( BUTTON_CLOSE, 'close' ),
|
|
104
|
+
( BUTTON_DISCARD, 'discard' ),
|
|
105
|
+
( BUTTON_APPLY, 'apply' ),
|
|
106
|
+
( BUTTON_RESET, 'reset' ),
|
|
107
|
+
( BUTTON_ABORT, 'abort' ),
|
|
108
|
+
( BUTTON_RETRY, 'retry' ),
|
|
109
|
+
( BUTTON_IGNORE, 'ignore' ),
|
|
110
|
+
]
|
|
111
|
+
buttons_map = {}
|
|
112
|
+
labels = []
|
|
113
|
+
for bit, label in buttons_info:
|
|
114
|
+
if buttons & bit:
|
|
115
|
+
for i, c in enumerate(label):
|
|
116
|
+
if c not in buttons_map:
|
|
117
|
+
buttons_map[c] = bit
|
|
118
|
+
labels.append('%s(%s)%s' % (label[:i], c, label[i+1:]))
|
|
119
|
+
break
|
|
120
|
+
else:
|
|
121
|
+
for c in string.ascii_letters:
|
|
122
|
+
if c not in buttons_map:
|
|
123
|
+
buttons_map[c] = bit
|
|
124
|
+
labels.append('%s (%s)' % (label, c))
|
|
125
|
+
break
|
|
126
|
+
answer = None
|
|
127
|
+
while answer not in buttons_map:
|
|
128
|
+
try:
|
|
129
|
+
answer = input(', '.join(labels) + ': ')
|
|
130
|
+
except KeyboardInterrupt:
|
|
131
|
+
print(colorama.Fore.YELLOW + "aborted" + colorama.Fore.RESET)
|
|
132
|
+
if buttons & BUTTON_CANCEL:
|
|
133
|
+
return BUTTON_CANCEL
|
|
134
|
+
elif buttons & BUTTON_NO:
|
|
135
|
+
return BUTTON_NO
|
|
136
|
+
elif buttons & BUTTON_CLOSE:
|
|
137
|
+
return BUTTON_CLOSE
|
|
138
|
+
elif buttons & BUTTON_DISCARD:
|
|
139
|
+
return BUTTON_DISCARD
|
|
140
|
+
elif buttons & BUTTON_ABORT:
|
|
141
|
+
return BUTTON_ABORT
|
|
142
|
+
return buttons & ~(buttons - 1)
|
|
143
|
+
return buttons_map[answer]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def open_file(message=None, specs=None, path='', multi=False):
|
|
148
|
+
"""Mostra una finestra di caricamento file con titolo *message. *specs* può essere una lista di tuple ``(extension, description)`` per permettere di caricare solo
|
|
149
|
+
file di tipi specifici; *path* è il percorso predefinito, e *multi* permette di selezionare più di un file da caricare.
|
|
150
|
+
Se *multi* è ``False``, la funzione restituisce il percorso del file selezionato o ``None`` se l'utente ha annullato il caricamento, altrimenti restituisce la lista di file
|
|
151
|
+
selezionati. Se eseguita al di fuori di Konga, questa funzione ignora i parametri *specs*, *path* e *multi*, e l'utente dovrà inserire il percorso completo del file da caricare;
|
|
152
|
+
se verrà inserito un percorso vuoto, la funzione restituirà ``None``."""
|
|
153
|
+
if _proxy.is_valid():
|
|
154
|
+
with _TimeoutBlocker():
|
|
155
|
+
return _proxy.ui.open_file(message, specs, path, multi)
|
|
156
|
+
else:
|
|
157
|
+
if message:
|
|
158
|
+
print(colorama.Style.BRIGHT + textwrap.fill(message, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
159
|
+
while True:
|
|
160
|
+
try:
|
|
161
|
+
filename = input('Enter an existing filename to open or none to cancel: ')
|
|
162
|
+
except KeyboardInterrupt:
|
|
163
|
+
print(colorama.Fore.YELLOW + "aborted" + colorama.Fore.RESET)
|
|
164
|
+
return None
|
|
165
|
+
if not filename:
|
|
166
|
+
return None
|
|
167
|
+
if os.path.exists(filename) and os.path.isfile(filename):
|
|
168
|
+
break
|
|
169
|
+
return filename
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def save_file(message=None, spec=None, path=''):
|
|
174
|
+
"""Mostra una finestra di salvataggio file con titolo *message. *spec* può essere una tupla nella forma ``(extension, description)`` per permettere di salvare file di un
|
|
175
|
+
tipo specifico; *path* è il percorso predefinito. La funzione restituisce il percorso del file da salvare oppure ``None`` se l'utente ha annullato il salvataggio.
|
|
176
|
+
Se eseguita al di fuori di Konga, questa funzione ignora i parametri *specs* e *path*, e l'utente dovrà inserire il percorso completo del file da salvare;
|
|
177
|
+
se verrà inserito un percorso vuoto, la funzione restituirà ``None``."""
|
|
178
|
+
if _proxy.is_valid():
|
|
179
|
+
with _TimeoutBlocker():
|
|
180
|
+
return _proxy.ui.save_file(message, spec, path)
|
|
181
|
+
else:
|
|
182
|
+
if message:
|
|
183
|
+
print(colorama.Style.BRIGHT + textwrap.fill(message, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
184
|
+
try:
|
|
185
|
+
filename = input('Enter filename to be saved or none to cancel: ')
|
|
186
|
+
except KeyboardInterrupt:
|
|
187
|
+
print(colorama.Fore.YELLOW + "aborted" + colorama.Fore.RESET)
|
|
188
|
+
return None
|
|
189
|
+
return filename or None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def choose_directory(message=None, path=''):
|
|
194
|
+
"""Mostra una finestra di selezione directory con titolo *message* e percorso iniziale *path*. La funzione restituisce il percorso della directory selezionata oppure ``None``
|
|
195
|
+
se l'utente ha annullato l'operazione.
|
|
196
|
+
Se eseguita al di fuori di Konga, questa funzione ignora il parametro *path*, e l'utente dovrà inserire il percorso completo della directory; se verrà inserito un percorso
|
|
197
|
+
vuoto, la funzione restituirà ``None``."""
|
|
198
|
+
if _proxy.is_valid():
|
|
199
|
+
with _TimeoutBlocker():
|
|
200
|
+
return _proxy.ui.choose_directory(message, path)
|
|
201
|
+
else:
|
|
202
|
+
if message:
|
|
203
|
+
print(colorama.Style.BRIGHT + textwrap.fill(message, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
204
|
+
while True:
|
|
205
|
+
try:
|
|
206
|
+
dirname = input('Enter an existing directory to open or none to cancel: ')
|
|
207
|
+
except KeyboardInterrupt:
|
|
208
|
+
print(colorama.Fore.YELLOW + "aborted" + colorama.Fore.RESET)
|
|
209
|
+
return None
|
|
210
|
+
if not dirname:
|
|
211
|
+
return None
|
|
212
|
+
if os.path.exists(dirname) and os.path.isdir(dirname):
|
|
213
|
+
break
|
|
214
|
+
return dirname
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def select_record(tablename, multi=False, size=None, where_expr=None, code_azienda=None, num_esercizio=None):
|
|
219
|
+
"""Mostra una finestra di selezione record di Konga; la finestra mostrerà i record della tabella *tablename* e avrà dimensione *size* (tupla di due elementi nella forma
|
|
220
|
+
``(width, height)``). *where_expr* può essere un'espressione SQL *WHERE* per filtrare i record selezionabili; *code_azienda* e *code_esercizio* filtrano ulteriormente
|
|
221
|
+
i record visualizzati usando l'azienda e l'esercizio specificati.
|
|
222
|
+
Se *multi* è ``True``, la funzione restituisce una lista di record selezionati sotto forma di ``dict``, altrimenti restituisce il ``dict`` del singolo record selezionato.
|
|
223
|
+
In tutti i casi se l'utente annulla l'operazione, la funzione restituirà ``None``.
|
|
224
|
+
|
|
225
|
+
.. warning::
|
|
226
|
+
Questa funzione è disponibile solo all'interno di Konga; eseguendola da fuori verrà lanciata l'eccezione :class:`kongautil.KongaRequiredError`.
|
|
227
|
+
"""
|
|
228
|
+
if _proxy.is_valid():
|
|
229
|
+
with _TimeoutBlocker():
|
|
230
|
+
return _proxy.ui.select_record(tablename, multi, size, where_expr, code_azienda=code_azienda, num_esercizio=num_esercizio)
|
|
231
|
+
else:
|
|
232
|
+
raise kongautil.KongaRequiredError
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def open_progress(title=None, cancellable=True):
|
|
237
|
+
"""Mostra una finestra di progresso con titolo *title*, potenzialmente annullabile dall'utente se *cancellable* è ``True``; la funzione ritorna immediatamente.
|
|
238
|
+
Se eseguita fuori da Konga, questa funzione ignora il parametro *cancellable*."""
|
|
239
|
+
if _proxy.is_valid():
|
|
240
|
+
_proxy.ui.open_progress(title or u'Operazione in corso…', cancellable)
|
|
241
|
+
else:
|
|
242
|
+
if title:
|
|
243
|
+
print(colorama.Style.BRIGHT + textwrap.fill(title, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
244
|
+
set_progress()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def close_progress():
|
|
249
|
+
"""Nasconde la finestra di progresso precedentemente mostrata con :func:`kongaui.open_progress`."""
|
|
250
|
+
if _proxy.is_valid():
|
|
251
|
+
_proxy.ui.close_progress()
|
|
252
|
+
else:
|
|
253
|
+
print('\033[2K\r', end='')
|
|
254
|
+
sys.stdout.flush()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def set_progress(progress=None, message=None, state=None):
|
|
259
|
+
"""Imposta l'avanzamento corrente nella finestra di progresso precedentemente mostrata con :func:`kongaui.open_progress`. *progress* può essere ``None``
|
|
260
|
+
per specificare un avanzamento indefinito, oppure un valore compreso tra 0 e 100 per specificare la percentuale di avanzamento completata. *message* e
|
|
261
|
+
*state* sono messaggi aggiuntivi da mostrare nella finestra di avanzamento."""
|
|
262
|
+
if _proxy.is_valid():
|
|
263
|
+
_proxy.ui.set_progress(progress, message, state)
|
|
264
|
+
else:
|
|
265
|
+
term_width = _get_term_width()
|
|
266
|
+
def elide(s, width):
|
|
267
|
+
if len(s) > width:
|
|
268
|
+
parts = s.split(' ')
|
|
269
|
+
mid = len(parts) // 2
|
|
270
|
+
before = parts[:mid]
|
|
271
|
+
after = parts[mid:]
|
|
272
|
+
while before or after:
|
|
273
|
+
if len(before) > len(after):
|
|
274
|
+
del before[-1]
|
|
275
|
+
elif after:
|
|
276
|
+
del after[-1]
|
|
277
|
+
s = ' '.join(before) + ' [...] ' + ' '.join(after)
|
|
278
|
+
if len(s) <= width:
|
|
279
|
+
break
|
|
280
|
+
if len(s) > width:
|
|
281
|
+
s = s[:width - 6] + ' [...]'
|
|
282
|
+
return s
|
|
283
|
+
text = []
|
|
284
|
+
if message:
|
|
285
|
+
text.append(message)
|
|
286
|
+
if state:
|
|
287
|
+
text.append(state)
|
|
288
|
+
if not text:
|
|
289
|
+
text.append('Operazione in corso...')
|
|
290
|
+
if (progress is None) or (progress < 0):
|
|
291
|
+
tick = ('\\', '|', '/', '-')[int(time.time() * 5) % 4]
|
|
292
|
+
bar = '%s %s' % (elide(', '.join(text), term_width - 3), tick)
|
|
293
|
+
else:
|
|
294
|
+
progress = ('\u2588' * int((progress * 30) // 100))
|
|
295
|
+
bar = '|%-30s| %s' % (progress, elide(', '.join(text), term_width - 34))
|
|
296
|
+
print('\033[2K\r' + bar, end='')
|
|
297
|
+
sys.stdout.flush()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def is_progress_aborted():
|
|
302
|
+
"""Restituisce ``True`` se l'utente ha annullato la finestra di progresso precedentemente mostrata con :func:`kongaui.open_progress`."""
|
|
303
|
+
if _proxy.is_valid():
|
|
304
|
+
return _proxy.ui.is_progress_aborted()
|
|
305
|
+
else:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def open_window(command, key_id=None, key_code=None, code_azienda=None, num_esercizio=None):
|
|
311
|
+
"""Apre una finestra di Konga mostrando il comando *command*, ed eventualmente aprendo il record identificato univocamente o da *key_id* (ID del record), o
|
|
312
|
+
dalla tupla (*key_code*, *code_azienda*, *code_esercizio*) (Codice del record, codice dell'azienda e codice dell'esercizio).
|
|
313
|
+
|
|
314
|
+
.. warning::
|
|
315
|
+
Questa funzione è disponibile solo all'interno di Konga; eseguendola da fuori verrà lanciata l'eccezione :class:`kongautil.KongaRequiredError`.
|
|
316
|
+
"""
|
|
317
|
+
if _proxy.is_valid():
|
|
318
|
+
_proxy.ui.open_window(command, key_id, key_code, code_azienda, num_esercizio)
|
|
319
|
+
else:
|
|
320
|
+
raise kongautil.KongaRequiredError
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def execute_form(form_data, title=None, message=None, condition=None):
|
|
325
|
+
"""Apre un form di immissione dati con titolo *title*; se *message* è specificato, il testo corrispondente sarà visualizzato in alto nella finestra del form.
|
|
326
|
+
*form_data* deve essere una lista di ``dict`` con le specifiche dei campi da mostrare; nel ``dict`` di un singolo campo, l'unica chiave richiesta è ``name``,
|
|
327
|
+
che deve identificare univocamente il nome del campo. E' possibile specificare l'etichetta da mostrare accando al campo stesso tramite la chiave ``label``;
|
|
328
|
+
la tipologia di dato consentità è specificata tramite la chiave ``type``, che può assumere i valori:
|
|
329
|
+
|
|
330
|
+
* ``str``: testo semplice, con possibile lunghezza massima ``length`` se la chiave è specificata;
|
|
331
|
+
* ``password``: parola chiave;
|
|
332
|
+
* ``int``: valore intero;
|
|
333
|
+
* ``decimal``: valore decimale (:class:`kongalib.Decimal`);
|
|
334
|
+
* ``range``: valore intero compreso tra un valore minimo (specificato dalla chiave ``min`` con valore predefinito ``0``) e un valore massimo (specificato dalla chiave ``max`` con valore predefinito ``100``);
|
|
335
|
+
* ``slider``: simile a ``range`` ma viene visualizzato come cursore di selezione valore scorrevole;
|
|
336
|
+
* ``bool``: valore booleano;
|
|
337
|
+
* ``date``: data (``datetime.date``);
|
|
338
|
+
* ``choice``: valore interno che identifica l'indice di una scelta tra quelle specificate nella chiave ``items`` (lista di stringhe);
|
|
339
|
+
* ``listbox``: simile a ``choice`` ma viene visualizzato come lista di elementi da cui fare una scelta;
|
|
340
|
+
* ``load``: nome di file esistente da caricare;
|
|
341
|
+
* ``save``: nome di file da salvare;
|
|
342
|
+
* ``dir``: nome di directory esistente;
|
|
343
|
+
* ``code``: stringa di testo che identifica il codice di un record, la cui tabella è indicata dalla chiave ``table``;
|
|
344
|
+
* ``company_code``: simile a ``code``, specifica l'azienda su cui gli altri campi ``code`` possono essere ricercati;
|
|
345
|
+
* ``accounting_year_code``: simile a ``code``, specifica l'esercizio su cui gli altri campi ``code`` possono essere ricercati;
|
|
346
|
+
|
|
347
|
+
Se presente, la chiave ``default`` permette di specificare il valore predefinito per un dato campo; inoltre se è presente la chiave ``focus`` (con qualsiasi
|
|
348
|
+
valore), il campo corrispondente prenderà il focus all'avvio della finestra. La chiave ``enabled`` permette di specificare anche se abilitare o meno un campo;
|
|
349
|
+
il suo valore può essere un booleano oppure una stringa il cui contenuto viene valutato allo stesso modo del parametro *condition* (vedi sotto).
|
|
350
|
+
Se l'utente annulla il form la funzione restituisce ``None``, altrimenti un ``dict`` le cui chiavi sono i nome dei campi e i valori i dati immessi dall'utente.
|
|
351
|
+
Il parametro *condition*, se presente, permette di specificare una condizione di validazione per il form sotto forma di espressione Python; i nomi dei campi
|
|
352
|
+
specificati in *form_data* saranno disponibili come variabili nell'esecuzione di questa condizione, il cui esito determinerà se consentire o meno l'uscita
|
|
353
|
+
dal form con successo."""
|
|
354
|
+
if _proxy.is_valid():
|
|
355
|
+
with _TimeoutBlocker():
|
|
356
|
+
return _proxy.ui.execute_form(form_data, title, message, condition)
|
|
357
|
+
else:
|
|
358
|
+
import kongalib, decimal, datetime, getpass
|
|
359
|
+
class InvalidInput(RuntimeError):
|
|
360
|
+
pass
|
|
361
|
+
if title:
|
|
362
|
+
print(colorama.Style.BRIGHT + textwrap.fill(title, width=_get_term_width() - 1) + colorama.Style.RESET_ALL)
|
|
363
|
+
if message:
|
|
364
|
+
print(textwrap.fill(message, width=_get_term_width() - 1))
|
|
365
|
+
result = {}
|
|
366
|
+
for entry in form_data:
|
|
367
|
+
if not isinstance(entry, dict):
|
|
368
|
+
raise RuntimeError("Expected dict as form data entry")
|
|
369
|
+
if 'name' not in entry:
|
|
370
|
+
raise RuntimeError("Expected 'name' key in form data entry dict")
|
|
371
|
+
name = str(entry['name'])
|
|
372
|
+
label = str(entry.get('label', name))
|
|
373
|
+
prompt = input
|
|
374
|
+
enabled = entry.get('enabled') or True
|
|
375
|
+
try:
|
|
376
|
+
enabled = eval(str(enabled), result.copy())
|
|
377
|
+
except Exception as e:
|
|
378
|
+
raise RuntimeError("Exception in enable condition: %s" % str(e))
|
|
379
|
+
|
|
380
|
+
wtype = entry.get('type', str)
|
|
381
|
+
if wtype in ('integer', 'int'):
|
|
382
|
+
try:
|
|
383
|
+
default = str(int(entry.get('default', 0)))
|
|
384
|
+
except:
|
|
385
|
+
default = '0'
|
|
386
|
+
def validate(text):
|
|
387
|
+
try:
|
|
388
|
+
return int(text)
|
|
389
|
+
except:
|
|
390
|
+
raise InvalidInput('Expected integer number')
|
|
391
|
+
elif wtype in ('decimal', kongalib.Decimal, decimal.Decimal):
|
|
392
|
+
try:
|
|
393
|
+
default = str(kongalib.Decimal(entry.get('default', 0)))
|
|
394
|
+
except:
|
|
395
|
+
default = str(kongalib.Decimal(0))
|
|
396
|
+
def validate(text):
|
|
397
|
+
try:
|
|
398
|
+
return kongalib.Decimal(text)
|
|
399
|
+
except:
|
|
400
|
+
raise InvalidInput('Expected decimal number')
|
|
401
|
+
elif wtype in ('range', 'slider'):
|
|
402
|
+
try:
|
|
403
|
+
default = str(int(entry.get('default', 0)))
|
|
404
|
+
except:
|
|
405
|
+
default = '0'
|
|
406
|
+
try:
|
|
407
|
+
min_value = int(entry.get('min', 0))
|
|
408
|
+
except:
|
|
409
|
+
min_value = 0
|
|
410
|
+
try:
|
|
411
|
+
max_value = int(entry.get('max', 100))
|
|
412
|
+
except:
|
|
413
|
+
max_value = 100
|
|
414
|
+
label += ' (%d-%d)' % (min_value, max_value)
|
|
415
|
+
def validate(text):
|
|
416
|
+
try:
|
|
417
|
+
value = int(text)
|
|
418
|
+
if (value < min_value) or (value > max_value):
|
|
419
|
+
raise RuntimeError
|
|
420
|
+
return value
|
|
421
|
+
except:
|
|
422
|
+
raise InvalidInput('Expected integer number between %d and %d' % (min_value, max_value))
|
|
423
|
+
elif wtype in ('bool', 'boolean', bool, 'check'):
|
|
424
|
+
try:
|
|
425
|
+
default = 'Y' if bool(entry.get('default', False)) else 'N'
|
|
426
|
+
except:
|
|
427
|
+
default = 'N'
|
|
428
|
+
def validate(text):
|
|
429
|
+
if text.lower() in ('t', 'true', 'y', 'yes', '1'):
|
|
430
|
+
return True
|
|
431
|
+
if text.lower() in ('f', 'false', 'n', 'no', '0'):
|
|
432
|
+
return False
|
|
433
|
+
raise InvalidInput('Expected boolean value')
|
|
434
|
+
elif wtype in ('date', datetime.date):
|
|
435
|
+
try:
|
|
436
|
+
default = datetime.datetime.strptime(entry.get('default', datetime.date.today()), '%Y-%m-%d').date().isoformat()
|
|
437
|
+
except:
|
|
438
|
+
default = datetime.date.today().isoformat()
|
|
439
|
+
def validate(text):
|
|
440
|
+
try:
|
|
441
|
+
return datetime.datetime.strptime(text, '%Y-%m-%d').date()
|
|
442
|
+
except:
|
|
443
|
+
raise InvalidInput('Expected iso date (YYYY-MM-DD)')
|
|
444
|
+
elif wtype in ('choice', 'listbox', 'combobox'):
|
|
445
|
+
items = entry.get('items', [])
|
|
446
|
+
if (not isinstance(items, (tuple, list))) or (not all([ isinstance(item, str) for item in items ])):
|
|
447
|
+
raise RuntimeError("Expected list of strings as 'items' value")
|
|
448
|
+
print(label)
|
|
449
|
+
for index, item in enumerate(items):
|
|
450
|
+
print("%d) %s" % (index + 1, item))
|
|
451
|
+
label = 'Enter selection'
|
|
452
|
+
try:
|
|
453
|
+
default = str(int(entry.get('default', 0)) + 1)
|
|
454
|
+
except:
|
|
455
|
+
default = '1'
|
|
456
|
+
def validate(text):
|
|
457
|
+
try:
|
|
458
|
+
value = int(text)
|
|
459
|
+
if (value < 1) or (value > len(items)):
|
|
460
|
+
raise RuntimeError
|
|
461
|
+
return value - 1
|
|
462
|
+
except:
|
|
463
|
+
raise InvalidInput('Expected integer number between %d and %d' % (1, len(items)))
|
|
464
|
+
else:
|
|
465
|
+
if wtype == 'password':
|
|
466
|
+
prompt = getpass.getpass
|
|
467
|
+
default = None
|
|
468
|
+
else:
|
|
469
|
+
try:
|
|
470
|
+
default = str(entry.get('default', ''))
|
|
471
|
+
except:
|
|
472
|
+
default = ''
|
|
473
|
+
try:
|
|
474
|
+
length = int(entry.get('length', 0))
|
|
475
|
+
except:
|
|
476
|
+
length = 0
|
|
477
|
+
def validate(text):
|
|
478
|
+
if length and (len(text) > length):
|
|
479
|
+
raise InvalidInput('String lengths exceeds maximum size of %d characters' % length)
|
|
480
|
+
return text
|
|
481
|
+
if not enabled:
|
|
482
|
+
value = default
|
|
483
|
+
else:
|
|
484
|
+
if default is not None:
|
|
485
|
+
label += ' [%s]' % default
|
|
486
|
+
while True:
|
|
487
|
+
try:
|
|
488
|
+
value = prompt(label + ': ')
|
|
489
|
+
except KeyboardInterrupt:
|
|
490
|
+
print(colorama.Fore.YELLOW + "aborted" + colorama.Fore.RESET)
|
|
491
|
+
return None
|
|
492
|
+
if (not value) and (default is not None):
|
|
493
|
+
value = default
|
|
494
|
+
try:
|
|
495
|
+
value = validate(value)
|
|
496
|
+
break
|
|
497
|
+
except InvalidInput as e:
|
|
498
|
+
print(colorama.Fore.RED + str(e) + colorama.Fore.RESET)
|
|
499
|
+
result[name] = value
|
|
500
|
+
if condition is not None:
|
|
501
|
+
if not eval(condition, result.copy()):
|
|
502
|
+
print(colorama.Style.BRIGHT + colorama.Fore.RED + "Form input data validation failed; aborted" + colorama.Style.RESET_ALL)
|
|
503
|
+
result = None
|
|
504
|
+
return result
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
|