sapiopycommons 2024.11.11a364__py3-none-any.whl → 2024.11.18a366__py3-none-any.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.

Potentially problematic release.


This version of sapiopycommons might be problematic. Click here for more details.

Files changed (47) hide show
  1. sapiopycommons/callbacks/callback_util.py +532 -83
  2. sapiopycommons/callbacks/field_builder.py +537 -0
  3. sapiopycommons/chem/IndigoMolecules.py +2 -0
  4. sapiopycommons/chem/Molecules.py +77 -18
  5. sapiopycommons/customreport/__init__.py +0 -0
  6. sapiopycommons/customreport/column_builder.py +60 -0
  7. sapiopycommons/customreport/custom_report_builder.py +130 -0
  8. sapiopycommons/customreport/term_builder.py +299 -0
  9. sapiopycommons/datatype/attachment_util.py +11 -10
  10. sapiopycommons/datatype/data_fields.py +61 -0
  11. sapiopycommons/datatype/pseudo_data_types.py +440 -0
  12. sapiopycommons/eln/experiment_handler.py +272 -70
  13. sapiopycommons/eln/experiment_report_util.py +653 -0
  14. sapiopycommons/files/complex_data_loader.py +5 -4
  15. sapiopycommons/files/file_bridge.py +31 -24
  16. sapiopycommons/files/file_bridge_handler.py +340 -0
  17. sapiopycommons/files/file_data_handler.py +2 -5
  18. sapiopycommons/files/file_util.py +59 -9
  19. sapiopycommons/files/file_validator.py +92 -6
  20. sapiopycommons/files/file_writer.py +44 -15
  21. sapiopycommons/flowcyto/flow_cyto.py +77 -0
  22. sapiopycommons/flowcyto/flowcyto_data.py +75 -0
  23. sapiopycommons/general/accession_service.py +375 -0
  24. sapiopycommons/general/aliases.py +207 -6
  25. sapiopycommons/general/audit_log.py +189 -0
  26. sapiopycommons/general/custom_report_util.py +212 -37
  27. sapiopycommons/general/exceptions.py +21 -8
  28. sapiopycommons/general/popup_util.py +21 -0
  29. sapiopycommons/general/sapio_links.py +50 -0
  30. sapiopycommons/general/time_util.py +8 -2
  31. sapiopycommons/multimodal/multimodal.py +146 -0
  32. sapiopycommons/multimodal/multimodal_data.py +490 -0
  33. sapiopycommons/processtracking/custom_workflow_handler.py +406 -0
  34. sapiopycommons/processtracking/endpoints.py +22 -22
  35. sapiopycommons/recordmodel/record_handler.py +481 -97
  36. sapiopycommons/rules/eln_rule_handler.py +34 -25
  37. sapiopycommons/rules/on_save_rule_handler.py +34 -31
  38. sapiopycommons/sftpconnect/__init__.py +0 -0
  39. sapiopycommons/sftpconnect/sftp_builder.py +69 -0
  40. sapiopycommons/webhook/webhook_context.py +39 -0
  41. sapiopycommons/webhook/webhook_handlers.py +201 -42
  42. sapiopycommons/webhook/webservice_handlers.py +67 -0
  43. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/METADATA +5 -2
  44. sapiopycommons-2024.11.18a366.dist-info/RECORD +59 -0
  45. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/WHEEL +1 -1
  46. sapiopycommons-2024.11.11a364.dist-info/RECORD +0 -38
  47. {sapiopycommons-2024.11.11a364.dist-info → sapiopycommons-2024.11.18a366.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
- from typing import Any
4
+ from weakref import WeakValueDictionary
3
5
 
6
+ from requests import ReadTimeout
4
7
  from sapiopylib.rest.ClientCallbackService import ClientCallback
5
8
  from sapiopylib.rest.DataMgmtService import DataMgmtServer
6
9
  from sapiopylib.rest.User import SapioUser
@@ -9,22 +12,24 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
9
12
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
10
13
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
11
14
  from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
12
- VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition
15
+ VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
13
16
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
14
17
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
15
- DataRecordSelectionRequest, DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, \
16
- MultiFilePromptRequest
18
+ DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
19
+ TempTableSelectionRequest, DisplayPopupRequest, PopupType
17
20
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
18
- from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
19
21
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
20
22
  from sapiopylib.rest.utils.DataTypeCacheManager import DataTypeCacheManager
21
23
  from sapiopylib.rest.utils.FormBuilder import FormBuilder
22
24
  from sapiopylib.rest.utils.recorddatasinks import InMemoryRecordDataSink
23
25
  from sapiopylib.rest.utils.recordmodel.RecordModelWrapper import WrappedType
24
26
 
25
- from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier
27
+ from sapiopycommons.files.file_util import FileUtil
28
+ from sapiopycommons.general.aliases import FieldMap, SapioRecord, AliasUtil, RecordIdentifier, FieldValue, \
29
+ UserIdentifier
26
30
  from sapiopycommons.general.custom_report_util import CustomReportUtil
27
- from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException
31
+ from sapiopycommons.general.exceptions import SapioUserCancelledException, SapioException, SapioUserErrorException, \
32
+ SapioDialogTimeoutException
28
33
  from sapiopycommons.recordmodel.record_handler import RecordHandler
29
34
 
30
35
 
@@ -32,29 +37,104 @@ class CallbackUtil:
32
37
  user: SapioUser
33
38
  callback: ClientCallback
34
39
  dt_cache: DataTypeCacheManager
40
+ _original_timeout: int
41
+ timeout_seconds: int
35
42
  width_pixels: int | None
36
43
  width_percent: float | None
37
44
 
38
- def __init__(self, context: SapioWebhookContext | SapioUser):
45
+ __instances: WeakValueDictionary[SapioUser, CallbackUtil] = WeakValueDictionary()
46
+ __initialized: bool
47
+
48
+ def __new__(cls, context: UserIdentifier):
49
+ """
50
+ :param context: The current webhook context or a user object to send requests from.
51
+ """
52
+ user = AliasUtil.to_sapio_user(context)
53
+ obj = cls.__instances.get(user)
54
+ if not obj:
55
+ obj = object.__new__(cls)
56
+ obj.__initialized = False
57
+ cls.__instances[user] = obj
58
+ return obj
59
+
60
+ def __init__(self, context: UserIdentifier):
39
61
  """
40
62
  :param context: The current webhook context or a user object to send requests from.
41
63
  """
42
- self.user = context if isinstance(context, SapioUser) else context.user
64
+ if self.__initialized:
65
+ return
66
+ self.__initialized = True
67
+
68
+ self.user = AliasUtil.to_sapio_user(context)
43
69
  self.callback = DataMgmtServer.get_client_callback(self.user)
44
70
  self.dt_cache = DataTypeCacheManager(self.user)
71
+ self._original_timeout = self.user.timeout_seconds
72
+ self.timeout_seconds = self.user.timeout_seconds
45
73
  self.width_pixels = None
46
74
  self.width_percent = None
47
75
 
48
- def set_dialog_width(self, width_pixels: int | None, width_percent: float | None):
76
+ def set_dialog_width(self, width_pixels: int | None = None, width_percent: float | None = None):
49
77
  """
50
78
  Set the width that dialogs will appear as for those dialogs that support specifying their width.
51
79
 
52
80
  :param width_pixels: The number of pixels wide that dialogs will appear as.
53
- :param width_percent: The percentage of the client's screen width that dialogs will appear as.
81
+ :param width_percent: The percentage (as a value between 0 and 1) of the client's screen width that dialogs
82
+ will appear as.
54
83
  """
84
+ if width_pixels is not None and width_percent is not None:
85
+ raise SapioException("Cannot set both width_pixels and width_percent at once.")
55
86
  self.width_pixels = width_pixels
56
87
  self.width_percent = width_percent
57
-
88
+
89
+ def set_dialog_timeout(self, timeout: int):
90
+ """
91
+ Alter the timeout time used for callback requests that create dialogs for the user to interact with. By default,
92
+ a CallbackUtil will use the timeout time of the SapioUser provided to it. By altering this, a different timeout
93
+ time is used.
94
+
95
+ :param timeout: The number of seconds that must elapse before a SapioDialogTimeoutException is thrown by
96
+ any callback that creates a dialog for the user to interact with.
97
+ """
98
+ self.timeout_seconds = timeout
99
+
100
+ def toaster_popup(self, message: str, title: str = "", popup_type: PopupType = PopupType.Info) -> None:
101
+ """
102
+ Display a toaster popup in the bottom right corner of the user's screen.
103
+
104
+ :param message: The message to display in the toaster.
105
+ :param title: The title to display at the top of the toaster.
106
+ :param popup_type: The popup type to use for the toaster. This controls the color that the toaster appears with.
107
+ Info is blue, Success is green, Warning is yellow, and Error is red
108
+ """
109
+ self.callback.display_popup(DisplayPopupRequest(title, message, popup_type))
110
+
111
+ def display_info(self, message: str) -> None:
112
+ """
113
+ Display an info message to the user in a dialog. Repeated calls to this function will append the new messages
114
+ to the same dialog if it is still opened by the user.
115
+
116
+ :param message: The message to display to the user.
117
+ """
118
+ self.callback.display_info(message)
119
+
120
+ def display_warning(self, message: str) -> None:
121
+ """
122
+ Display a warning message to the user in a dialog. Repeated calls to this function will append the new messages
123
+ to the same dialog if it is still opened by the user.
124
+
125
+ :param message: The message to display to the user.
126
+ """
127
+ self.callback.display_warning(message)
128
+
129
+ def display_error(self, message: str) -> None:
130
+ """
131
+ Display an error message to the user in a dialog. Repeated calls to this function will append the new messages
132
+ to the same dialog if it is still opened by the user.
133
+
134
+ :param message: The message to display to the user.
135
+ """
136
+ self.callback.display_error(message)
137
+
58
138
  def option_dialog(self, title: str, msg: str, options: list[str], default_option: int = 0,
59
139
  user_can_cancel: bool = False) -> str:
60
140
  """
@@ -69,8 +149,15 @@ class CallbackUtil:
69
149
  SapioUserCancelledException is thrown.
70
150
  :return: The name of the button that the user selected.
71
151
  """
72
- request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel)
73
- response: int | None = self.callback.show_option_dialog(request)
152
+ request = OptionDialogRequest(title, msg, options, default_option, user_can_cancel,
153
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
154
+ try:
155
+ self.user.timeout_seconds = self.timeout_seconds
156
+ response: int | None = self.callback.show_option_dialog(request)
157
+ except ReadTimeout:
158
+ raise SapioDialogTimeoutException()
159
+ finally:
160
+ self.user.timeout_seconds = self._original_timeout
74
161
  if response is None:
75
162
  raise SapioUserCancelledException()
76
163
  return options[response]
@@ -109,17 +196,27 @@ class CallbackUtil:
109
196
  """
110
197
  return self.option_dialog(title, msg, ["Yes", "No"], 0 if default_yes else 1, False) == "Yes"
111
198
 
112
- def list_dialog(self, title: str, options: list[str], multi_select: bool = False) -> list[str]:
199
+ def list_dialog(self, title: str, options: list[str], multi_select: bool = False,
200
+ preselected_values: list[str] | None = None) -> list[str]:
113
201
  """
114
202
  Create a list dialog with the given options for the user to choose from.
115
203
 
116
204
  :param title: The title of the dialog.
117
205
  :param options: The list options that the user has to choose from.
118
206
  :param multi_select: Whether the user is able to select multiple options from the list.
207
+ :param preselected_values: A list of values that will already be selected when the list dialog is created. The
208
+ user can unselect these values if they want to.
119
209
  :return: The list of options that the user selected.
120
210
  """
121
- request = ListDialogRequest(title, multi_select, options)
122
- response: list[str] | None = self.callback.show_list_dialog(request)
211
+ request = ListDialogRequest(title, multi_select, options, preselected_values,
212
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
213
+ try:
214
+ self.user.timeout_seconds = self.timeout_seconds
215
+ response: list[str] | None = self.callback.show_list_dialog(request)
216
+ except ReadTimeout:
217
+ raise SapioDialogTimeoutException()
218
+ finally:
219
+ self.user.timeout_seconds = self._original_timeout
123
220
  if response is None:
124
221
  raise SapioUserCancelledException()
125
222
  return response
@@ -161,8 +258,6 @@ class CallbackUtil:
161
258
  builder = FormBuilder(data_type, display_name, plural_display_name)
162
259
  for field_def in fields:
163
260
  field_name = field_def.data_field_name
164
- if values and hasattr(field_def, "default_value"):
165
- field_def.default_value = values.get(field_name)
166
261
  column: int = 0
167
262
  span: int = 4
168
263
  if column_positions and field_name in column_positions:
@@ -171,8 +266,15 @@ class CallbackUtil:
171
266
  span = position[1]
172
267
  builder.add_field(field_def, column, span)
173
268
 
174
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
175
- response: FieldMap | None = self.callback.show_form_entry_dialog(request)
269
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
270
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
271
+ try:
272
+ self.user.timeout_seconds = self.timeout_seconds
273
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
274
+ except ReadTimeout:
275
+ raise SapioDialogTimeoutException()
276
+ finally:
277
+ self.user.timeout_seconds = self._original_timeout
176
278
  if response is None:
177
279
  raise SapioUserCancelledException()
178
280
  return response
@@ -209,32 +311,39 @@ class CallbackUtil:
209
311
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
210
312
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
211
313
 
314
+ # Make everything visible, because presumably the caller gave a field name because they want it to be seen.
315
+ modifier = FieldModifier(visible=True, editable=editable)
316
+
212
317
  # Build the form using only those fields that are desired.
318
+ values: dict[str, FieldValue] = {}
213
319
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
214
320
  for field_name in fields:
215
321
  field_def = field_defs.get(field_name)
216
322
  if field_def is None:
217
323
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
218
- if editable is not None:
219
- field_def.editable = editable
220
- field_def.visible = True
221
- if hasattr(field_def, "default_value"):
222
- field_def.default_value = record.get_field_value(field_name)
324
+ values[field_name] = record.get_field_value(field_name)
223
325
  column: int = 0
224
326
  span: int = 4
225
327
  if column_positions and field_name in column_positions:
226
328
  position = column_positions.get(field_name)
227
329
  column = position[0]
228
330
  span = position[1]
229
- builder.add_field(field_def, column, span)
230
-
231
- request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
232
- response: FieldMap | None = self.callback.show_form_entry_dialog(request)
331
+ builder.add_field(modifier.modify_field(field_def), column, span)
332
+
333
+ request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
334
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
335
+ try:
336
+ self.user.timeout_seconds = self.timeout_seconds
337
+ response: FieldMap | None = self.callback.show_form_entry_dialog(request)
338
+ except ReadTimeout:
339
+ raise SapioDialogTimeoutException()
340
+ finally:
341
+ self.user.timeout_seconds = self._original_timeout
233
342
  if response is None:
234
343
  raise SapioUserCancelledException()
235
344
  return response
236
345
 
237
- def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> Any:
346
+ def input_dialog(self, title: str, msg: str, field: AbstractVeloxFieldDefinition) -> FieldValue:
238
347
  """
239
348
  Create an input dialog where the user must input data for a singular field.
240
349
 
@@ -243,8 +352,15 @@ class CallbackUtil:
243
352
  :param field: The definition for a field that the user must provide input to.
244
353
  :return: The response value from the user for the given field.
245
354
  """
246
- request = InputDialogCriteria(title, msg, field, self.width_pixels, self.width_percent)
247
- response: Any | None = self.callback.show_input_dialog(request)
355
+ request = InputDialogCriteria(title, msg, field,
356
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
357
+ try:
358
+ self.user.timeout_seconds = self.timeout_seconds
359
+ response: FieldValue | None = self.callback.show_input_dialog(request)
360
+ except ReadTimeout:
361
+ raise SapioDialogTimeoutException()
362
+ finally:
363
+ self.user.timeout_seconds = self._original_timeout
248
364
  if response is None:
249
365
  raise SapioUserCancelledException()
250
366
  return response
@@ -259,7 +375,7 @@ class CallbackUtil:
259
375
  :param field_name: The name and display name of the string field.
260
376
  :param default_value: The default value to place into the string field, if any.
261
377
  :param max_length: The max length of the string value. If not provided, uses the length of the default value.
262
- If neither this or a default value are not provided, defaults to 100 characters.
378
+ If neither this nor a default value are provided, defaults to 100 characters.
263
379
  :param editable: Whether the field is editable by the user.
264
380
  :param kwargs: Any additional keyword arguments to pass to the field definition.
265
381
  :return: The string that the user input into the dialog.
@@ -320,6 +436,8 @@ class CallbackUtil:
320
436
  msg: str,
321
437
  fields: list[AbstractVeloxFieldDefinition],
322
438
  values: list[FieldMap],
439
+ group_by: str | None = None,
440
+ image_data: list[bytes] | None = None,
323
441
  *,
324
442
  data_type: str = "Default",
325
443
  display_name: str | None = None,
@@ -333,6 +451,10 @@ class CallbackUtil:
333
451
  :param fields: The definitions of the fields to display as table columns. Fields will be displayed in the order
334
452
  they are provided in this list.
335
453
  :param values: The values to set for each row of the table.
454
+ :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
455
+ The user may remove this grouping if they want to.
456
+ :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
457
+ the image data list corresponds to the element at the same index in the values list.
336
458
  :param data_type: The data type name for the temporary data type that will be created for this table.
337
459
  :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
338
460
  name.
@@ -346,12 +468,24 @@ class CallbackUtil:
346
468
  if plural_display_name is None:
347
469
  plural_display_name = display_name + "s"
348
470
 
349
- builder = FormBuilder(data_type, display_name, plural_display_name)
350
- for column in fields:
351
- builder.add_field(column)
471
+ # Key fields display their columns in order before all non-key fields.
472
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
473
+ modifier = FieldModifier(key_field=False)
352
474
 
353
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
354
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
475
+ builder = FormBuilder(data_type, display_name, plural_display_name)
476
+ for field in fields:
477
+ builder.add_field(modifier.modify_field(field))
478
+
479
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
480
+ record_image_data_list=image_data, group_by_field=group_by,
481
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
482
+ try:
483
+ self.user.timeout_seconds = self.timeout_seconds
484
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
485
+ except ReadTimeout:
486
+ raise SapioDialogTimeoutException()
487
+ finally:
488
+ self.user.timeout_seconds = self._original_timeout
355
489
  if response is None:
356
490
  raise SapioUserCancelledException()
357
491
  return response
@@ -361,11 +495,14 @@ class CallbackUtil:
361
495
  msg: str,
362
496
  fields: list[str],
363
497
  records: list[SapioRecord],
364
- editable: bool | None = True) -> list[FieldMap]:
498
+ editable: bool | None = True,
499
+ group_by: str | None = None,
500
+ image_data: list[bytes] | None = None) -> list[FieldMap]:
365
501
  """
366
502
  Create a table dialog where the user may input data into the fields of the table. The table is constructed from
367
- a given list of records. Provided field names must match fields on the definition of the data type of the given
368
- records. The fields that are displayed will have their default value be that of the fields on the given records.
503
+ a given list of records of a singular type. Provided field names must match fields on the definition of the data
504
+ type of the given records. The fields that are displayed will have their default value be that of the fields on
505
+ the given records.
369
506
 
370
507
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
371
508
  previously cached.
@@ -377,9 +514,15 @@ class CallbackUtil:
377
514
  they are provided in this list.
378
515
  :param editable: If true, all fields are displayed as editable. If false, all fields are displayed as
379
516
  uneditable. If none, only those fields that are defined as editable by the data designer will be editable.
517
+ :param group_by: If provided, the created table dialog will be grouped by the field with this name by default.
518
+ The user may remove this grouping if they want to.
519
+ :param image_data: The bytes to the images that should be displayed in the rows of the table. Each element in
520
+ the image data list corresponds to the element at the same index in the records list.
380
521
  :return: A list of dictionaries mapping the data field names of the given field definitions to the response
381
522
  value from the user for that field for each row.
382
523
  """
524
+ if not records:
525
+ raise SapioException("No records provided.")
383
526
  data_types: set[str] = {x.data_type_name for x in records}
384
527
  if len(data_types) > 1:
385
528
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -390,22 +533,193 @@ class CallbackUtil:
390
533
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
391
534
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
392
535
 
536
+ # Key fields display their columns in order before all non-key fields.
537
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
538
+ # Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
539
+ modifier = FieldModifier(visible=True, key_field=False, editable=editable)
540
+
393
541
  # Build the form using only those fields that are desired.
394
542
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
395
543
  for field_name in fields:
396
544
  field_def = field_defs.get(field_name)
397
545
  if field_def is None:
398
546
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
399
- if editable is not None:
400
- field_def.editable = editable
401
- field_def.visible = True
402
- # Key fields display their columns in order before all non-key fields.
403
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
404
- field_def.key_field = False
405
- builder.add_field(field_def)
406
-
407
- request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
408
- response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
547
+ builder.add_field(modifier.modify_field(field_def))
548
+
549
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list,
550
+ record_image_data_list=image_data, group_by_field=group_by,
551
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
552
+ try:
553
+ self.user.timeout_seconds = self.timeout_seconds
554
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
555
+ except ReadTimeout:
556
+ raise SapioDialogTimeoutException()
557
+ finally:
558
+ self.user.timeout_seconds = self._original_timeout
559
+ if response is None:
560
+ raise SapioUserCancelledException()
561
+ return response
562
+
563
+ def multi_type_table_dialog(self,
564
+ title: str,
565
+ msg: str,
566
+ fields: list[(str, str) | AbstractVeloxFieldDefinition],
567
+ row_contents: list[list[SapioRecord | FieldMap]],
568
+ *,
569
+ default_modifier: FieldModifier | None = None,
570
+ field_modifiers: dict[str, FieldModifier] | None = None,
571
+ data_type: str = "Default",
572
+ display_name: str | None = None,
573
+ plural_display_name: str | None = None) -> list[FieldMap]:
574
+ """
575
+ Create a table dialog where the user may input data into the fields of the table. The table is constructed from
576
+ a given list of records of multiple data types or field maps. Provided field names must match with field names
577
+ from the data type definition of the given records. The fields that are displayed will have their default value
578
+ be that of the fields on the given records or field maps.
579
+
580
+ Makes webservice calls to get the data type field definitions of the given records if they weren't
581
+ previously cached.
582
+
583
+ :param title: The title of the dialog.
584
+ :param msg: The message to display in the dialog.
585
+ :param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
586
+ where the first element is a data type name and the second is a field name, or it could be a field
587
+ definition. If it is the former, a query will be made to find the field definition matching tht data type.
588
+ The data type names of the fields must match the data type names of the records in the row contents.
589
+ See the description of row_contents for what to do if you want to construct a field that pulls from a field
590
+ map.
591
+ If two fields share the same field name, an exception will be thrown. This is even true in the case where
592
+ the data type name of the fields is different. If you wish to display two fields from two data types with
593
+ the same name, then you must provide a FieldModifier for at least one of the fields where prepend_data_type
594
+ is True in order to make that field's name unique again. Note that if you do this for a field, the mapping
595
+ of record to field name will use the unedited field name, but the return results of this function will
596
+ use the edited field name in the results dictionary for a row.
597
+ :param row_contents: A list where each element is another list representing the records or a field map that will
598
+ be used to populate the columns of the table. If the data type of a given record doesn't match any of the
599
+ data type names of the given fields, then it will not be used.
600
+ This list can contain up to one field map, which are fields not tied to a record. This is so that you can
601
+ create abstract field definition not tied to a specific record in the system. If you want to define an
602
+ abstract field that pulls from the field map in the row contents, then you must set the data type name to
603
+ Default.
604
+ If a record of a given data type appears more than once in one of the inner-lists of the row contents, or
605
+ there is more than one field map, then an exception will be thrown, as there is no way of distinguishing
606
+ which record should be used for a field, and not all fields could have their values combined across multiple
607
+ records.
608
+ The row contents may have an inner-list which is missing a record of a data type that matches one of the
609
+ fields. In this case, the field value for that row under that column will be null.
610
+ The inner-list does not need to be sorted in any way, as this function will map the inner-list contents to
611
+ fields as necessary.
612
+ The inner-list may contain null elements; these will simply be discarded by this function.
613
+ :param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
614
+ make field definitions from the system behave differently than their system values. If this value is None,
615
+ then a default field modifier is created that causes all specified fields to be both visible and not key
616
+ fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
617
+ disabled by default in order to have the columns in the table respect the order of the fields as they are
618
+ provided to this function.)
619
+ :param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
620
+ the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
621
+ None, then the default modifier will be used.
622
+ :param data_type: The data type name for the temporary data type that will be created for this table.
623
+ :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
624
+ name.
625
+ :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
626
+ the display name + "s".
627
+ :return: A list of dictionaries mapping the data field names of the given field definitions to the response
628
+ value from the user for that field for each row.
629
+ """
630
+ # Set the default modifier to make all fields visible and not key if no default was provided.
631
+ if default_modifier is None:
632
+ default_modifier = FieldModifier(visible=True, key_field=False)
633
+ # To make things simpler, treat null field modifiers as an empty dict.
634
+ if field_modifiers is None:
635
+ field_modifiers = {}
636
+
637
+ # Construct the final fields list from the possible field objects.
638
+ final_fields: list[AbstractVeloxFieldDefinition] = []
639
+ # Keep track of whether any given field name appears more than once, as two fields could have the same
640
+ # field name but different data types. In this case, the user should provide a field modifier or field
641
+ # definition that changes one of the field names.
642
+ field_names: list[str] = []
643
+ for field in fields:
644
+ # Find the field definition for this field object.
645
+ if isinstance(field, tuple):
646
+ field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
647
+ elif isinstance(field, AbstractVeloxFieldDefinition):
648
+ field_def: AbstractVeloxFieldDefinition = field
649
+ else:
650
+ raise SapioException("Unrecognized field object.")
651
+
652
+ # Locate the modifier for this field and store the modified field.
653
+ name: str = field_def.data_field_name
654
+ modifier: FieldModifier = field_modifiers.get(name, default_modifier)
655
+ field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
656
+ final_fields.append(field_def)
657
+
658
+ # Verify that this field name isn't a duplicate.
659
+ # The field name may have changed due to the modifier.
660
+ name: str = field_def.data_field_name
661
+ if name in field_names:
662
+ raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
663
+ f"If you have provided two fields with the same name but different data types, "
664
+ f"consider providing a FieldModifier where prepend_data_type is true for one of "
665
+ f"the fields so that the field names will be different.")
666
+ field_names.append(name)
667
+
668
+ # Get the values for each row.
669
+ values: list[dict[str, FieldValue]] = []
670
+ for row in row_contents:
671
+ # The final values for this row:
672
+ row_values: dict[str, FieldValue] = {}
673
+
674
+ # Map the records for this row by their data type. If a field map is provided, its data type is Default.
675
+ row_records: dict[str, SapioRecord | FieldMap] = {}
676
+ for rec in row:
677
+ # Toss out null elements.
678
+ if rec is None:
679
+ continue
680
+ # Map records to their data type name. Map field maps to Default.
681
+ dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
682
+ # Warn if the same data type name appears more than once.
683
+ if dt in row_records:
684
+ raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
685
+ row_records[dt] = rec
686
+
687
+ # Get the field values from the above records.
688
+ for field in final_fields:
689
+ # Find the object that corresponds to this field given its data type name.
690
+ record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
691
+ # This could be either a record, a field map, or null. Convert any records to field maps.
692
+ if not isinstance(record, dict) and record is not None:
693
+ record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
694
+
695
+ # Find out if this field had its data type prepended to it. If this is the case, then we need to find
696
+ # the true data field name before retrieving the value from the field map.
697
+ name: str = field.data_field_name
698
+ if field_modifiers.get(name, default_modifier).prepend_data_type is True:
699
+ name = name.split(".")[1]
700
+
701
+ # Set the value for this particular field.
702
+ row_values[field.data_field_name] = record.get(name) if record else None
703
+ values.append(row_values)
704
+
705
+ if display_name is None:
706
+ display_name = data_type
707
+ if plural_display_name is None:
708
+ plural_display_name = display_name + "s"
709
+
710
+ builder = FormBuilder(data_type, display_name, plural_display_name)
711
+ for field in final_fields:
712
+ builder.add_field(field)
713
+
714
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values,
715
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
716
+ try:
717
+ self.user.timeout_seconds = self.timeout_seconds
718
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
719
+ except ReadTimeout:
720
+ raise SapioDialogTimeoutException()
721
+ finally:
722
+ self.user.timeout_seconds = self._original_timeout
409
723
  if response is None:
410
724
  raise SapioUserCancelledException()
411
725
  return response
@@ -453,8 +767,15 @@ class CallbackUtil:
453
767
  raise SapioException(f"The data type \"{data_type}\" does not have a layout by the name "
454
768
  f"\"{layout_name}\" in the system.")
455
769
 
456
- request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list)
457
- response: bool = self.callback.data_record_form_view_dialog(request)
770
+ request = DataRecordDialogRequest(title, record, layout, minimized, access_level, plugin_path_list,
771
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
772
+ try:
773
+ self.user.timeout_seconds = self.timeout_seconds
774
+ response: bool = self.callback.data_record_form_view_dialog(request)
775
+ except ReadTimeout:
776
+ raise SapioDialogTimeoutException()
777
+ finally:
778
+ self.user.timeout_seconds = self._original_timeout
458
779
  if not response:
459
780
  raise SapioUserCancelledException()
460
781
 
@@ -464,7 +785,8 @@ class CallbackUtil:
464
785
  values: list[FieldMap],
465
786
  multi_select: bool = True,
466
787
  *,
467
- display_name: str = "Default",
788
+ data_type: str = "Default",
789
+ display_name: str | None = None,
468
790
  plural_display_name: str | None = None) -> list[FieldMap]:
469
791
  """
470
792
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
@@ -475,24 +797,37 @@ class CallbackUtil:
475
797
  they are provided in this list.
476
798
  :param values: The values to set for each row of the table.
477
799
  :param multi_select: Whether the user is able to select multiple rows from the list.
478
- :param display_name: The display name for the temporary data type that will be created.
800
+ :param data_type: The data type name for the temporary data type that will be created for this table.
801
+ :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
802
+ name.
479
803
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
480
804
  the display name + "s".
481
805
  :return: A list of field maps corresponding to the chosen input field maps.
482
806
  """
807
+ if display_name is None:
808
+ display_name = data_type
483
809
  if plural_display_name is None:
484
810
  plural_display_name = display_name + "s"
485
811
 
486
- # Build the form using only those fields that are desired.
487
- request = DataRecordSelectionRequest(display_name, plural_display_name,
488
- fields, values, msg, multi_select)
489
- response: list[FieldMap] | None = self.callback.show_data_record_selection_dialog(request)
812
+ builder = FormBuilder(data_type, display_name, plural_display_name)
813
+ for field in fields:
814
+ builder.add_field(field)
815
+
816
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
817
+ multi_select=multi_select)
818
+ try:
819
+ self.user.timeout_seconds = self.timeout_seconds
820
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
821
+ except ReadTimeout:
822
+ raise SapioDialogTimeoutException()
823
+ finally:
824
+ self.user.timeout_seconds = self._original_timeout
490
825
  if response is None:
491
826
  raise SapioUserCancelledException()
492
827
  return response
493
828
 
494
829
  def record_selection_dialog(self, msg: str, fields: list[str], records: list[SapioRecord],
495
- multi_select: bool = True) -> list[FieldMap]:
830
+ multi_select: bool = True) -> list[SapioRecord]:
496
831
  """
497
832
  Create a record selection dialog for a list of records for the user to choose from. Provided field names must
498
833
  match fields on the definition of the data type of the given records.
@@ -505,8 +840,10 @@ class CallbackUtil:
505
840
  they are provided in this list.
506
841
  :param records: The records to display as rows in the table.
507
842
  :param multi_select: Whether the user is able to select multiple records from the list.
508
- :return: A list of field maps corresponding to the chosen input records.
843
+ :return: A list of the selected records.
509
844
  """
845
+ if not records:
846
+ raise SapioException("No records provided.")
510
847
  data_types: set[str] = {x.data_type_name for x in records}
511
848
  if len(data_types) > 1:
512
849
  raise SapioException("Multiple data type names encountered in records list for record table popup.")
@@ -521,21 +858,28 @@ class CallbackUtil:
521
858
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
522
859
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
523
860
 
861
+ # Key fields display their columns in order before all non-key fields.
862
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
863
+ # Also make everything visible, because presumably the caller give a field name because they want it to be seen.
864
+ modifier = FieldModifier(visible=True, key_field=False)
865
+
524
866
  # Build the form using only those fields that are desired.
525
- field_def_list: list = []
867
+ builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
526
868
  for field_name in fields:
527
869
  field_def = field_defs.get(field_name)
528
870
  if field_def is None:
529
871
  raise SapioException(f"No field of name \"{field_name}\" in field definitions of type \"{data_type}\"")
530
- field_def.visible = True
531
- # Key fields display their columns in order before all non-key fields.
532
- # Unmark key fields so that the column order is respected exactly as the caller provides it.
533
- field_def.key_field = False
534
- field_def_list.append(field_def)
535
-
536
- request = DataRecordSelectionRequest(type_def.display_name, type_def.plural_display_name,
537
- field_def_list, field_map_list, msg, multi_select)
538
- response: list[FieldMap] | None = self.callback.show_data_record_selection_dialog(request)
872
+ builder.add_field(modifier.modify_field(field_def))
873
+
874
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
875
+ multi_select=multi_select)
876
+ try:
877
+ self.user.timeout_seconds = self.timeout_seconds
878
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
879
+ except ReadTimeout:
880
+ raise SapioDialogTimeoutException()
881
+ finally:
882
+ self.user.timeout_seconds = self._original_timeout
539
883
  if response is None:
540
884
  raise SapioUserCancelledException()
541
885
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
@@ -602,7 +946,7 @@ class CallbackUtil:
602
946
 
603
947
  # If CustomReportCriteria was provided, it must be wrapped as a CustomReport.
604
948
  if isinstance(custom_search, CustomReportCriteria):
605
- custom_search: CustomReport = CustomReport(False, None, custom_search)
949
+ custom_search: CustomReport = CustomReport(False, [], custom_search)
606
950
  # If a string was provided, locate the report criteria for the predefined search in the system matching this
607
951
  # name.
608
952
  if isinstance(custom_search, str):
@@ -611,7 +955,13 @@ class CallbackUtil:
611
955
  request = InputSelectionRequest(data_type, msg, search_types, only_key_fields, record_blacklist,
612
956
  record_whitelist, preselected_records, custom_search, scan_criteria,
613
957
  multi_select)
614
- response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
958
+ try:
959
+ self.user.timeout_seconds = self.timeout_seconds
960
+ response: list[DataRecord] | None = self.callback.show_input_selection_dialog(request)
961
+ except ReadTimeout:
962
+ raise SapioDialogTimeoutException()
963
+ finally:
964
+ self.user.timeout_seconds = self._original_timeout
615
965
  if response is None:
616
966
  raise SapioUserCancelledException()
617
967
  return RecordHandler(self.user).wrap_models(response, wrapper_type)
@@ -635,14 +985,21 @@ class CallbackUtil:
635
985
  for field in additional_fields:
636
986
  builder.add_field(field)
637
987
  temp_dt = builder.get_temporary_data_type()
638
- request = ESigningRequestPojo(title, msg, show_comment, temp_dt)
639
- response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
988
+ request = ESigningRequestPojo(title, msg, show_comment, temp_dt,
989
+ width_in_pixels=self.width_pixels, width_percentage=self.width_percent)
990
+ try:
991
+ self.user.timeout_seconds = self.timeout_seconds
992
+ response: ESigningResponsePojo | None = self.callback.show_esign_dialog(request)
993
+ except ReadTimeout:
994
+ raise SapioDialogTimeoutException()
995
+ finally:
996
+ self.user.timeout_seconds = self._original_timeout
640
997
  if response is None:
641
998
  raise SapioUserCancelledException()
642
999
  return response
643
1000
 
644
1001
  def request_file(self, title: str, exts: list[str] | None = None,
645
- show_image_editor: bool = False, show_camera_button: bool = False) -> (str, bytes):
1002
+ show_image_editor: bool = False, show_camera_button: bool = False) -> tuple[str, bytes]:
646
1003
  """
647
1004
  Request a single file from the user.
648
1005
 
@@ -667,7 +1024,13 @@ class CallbackUtil:
667
1024
  return sink.consume_data(chunk, io_obj)
668
1025
 
669
1026
  request = FilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
670
- file_path: str | None = self.callback.show_file_dialog(request, do_consume)
1027
+ try:
1028
+ self.user.timeout_seconds = self.timeout_seconds
1029
+ file_path: str | None = self.callback.show_file_dialog(request, do_consume)
1030
+ except ReadTimeout:
1031
+ raise SapioDialogTimeoutException()
1032
+ finally:
1033
+ self.user.timeout_seconds = self._original_timeout
671
1034
  if file_path is None:
672
1035
  raise SapioUserCancelledException()
673
1036
 
@@ -675,7 +1038,7 @@ class CallbackUtil:
675
1038
  return file_path, sink.data
676
1039
 
677
1040
  def request_files(self, title: str, exts: list[str] | None = None,
678
- show_image_editor: bool = False, show_camera_button: bool = False):
1041
+ show_image_editor: bool = False, show_camera_button: bool = False) -> dict[str, bytes]:
679
1042
  """
680
1043
  Request multiple files from the user.
681
1044
 
@@ -692,7 +1055,13 @@ class CallbackUtil:
692
1055
  exts: list[str] = []
693
1056
 
694
1057
  request = MultiFilePromptRequest(title, show_image_editor, ",".join(exts), show_camera_button)
695
- file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
1058
+ try:
1059
+ self.user.timeout_seconds = self.timeout_seconds
1060
+ file_paths: list[str] | None = self.callback.show_multi_file_dialog(request)
1061
+ except ReadTimeout:
1062
+ raise SapioDialogTimeoutException()
1063
+ finally:
1064
+ self.user.timeout_seconds = self._original_timeout
696
1065
  if not file_paths:
697
1066
  raise SapioUserCancelledException()
698
1067
 
@@ -706,7 +1075,7 @@ class CallbackUtil:
706
1075
  return ret_dict
707
1076
 
708
1077
  @staticmethod
709
- def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]):
1078
+ def __verify_file(file_path: str, file_bytes: bytes, allowed_extensions: list[str]) -> None:
710
1079
  """
711
1080
  Verify that the provided file was read (i.e. the file path and file bytes aren't None or empty) and that it
712
1081
  has the correct file extension. Raises a user error exception if something about the file is incorrect.
@@ -720,7 +1089,7 @@ class CallbackUtil:
720
1089
  if len(allowed_extensions) != 0:
721
1090
  matches: bool = False
722
1091
  for ext in allowed_extensions:
723
- if file_path.endswith("." + ext):
1092
+ if file_path.endswith("." + ext.lstrip(".")):
724
1093
  matches = True
725
1094
  break
726
1095
  if matches is False:
@@ -734,5 +1103,85 @@ class CallbackUtil:
734
1103
  :param file_name: The name of the file.
735
1104
  :param file_data: The data of the file, provided as either a string or as a bytes array.
736
1105
  """
737
- data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
1106
+ data = io.BytesIO(file_data.encode() if isinstance(file_data, str) else file_data)
738
1107
  self.callback.send_file(file_name, False, data)
1108
+
1109
+ def write_zip_file(self, zip_name: str, files: dict[str, str | bytes]) -> None:
1110
+ """
1111
+ Send a collection of files to the user in a zip file.
1112
+
1113
+ :param zip_name: The name of the zip file.
1114
+ :param files: A dictionary of the files to add to the zip file.
1115
+ """
1116
+ data = io.BytesIO(FileUtil.zip_files(files))
1117
+ self.callback.send_file(zip_name, False, data)
1118
+
1119
+
1120
+ class FieldModifier:
1121
+ """
1122
+ A FieldModifier can be used to update the settings of a field definition from the system.
1123
+ """
1124
+ prepend_data_type: bool
1125
+ display_name: str | None
1126
+ required: bool | None
1127
+ editable: bool | None
1128
+ visible: bool | None
1129
+ key_field: bool | None
1130
+ column_width: int | None
1131
+
1132
+ def __init__(self, *, prepend_data_type: bool = False,
1133
+ display_name: str | None = None, required: bool | None = None, editable: bool | None = None,
1134
+ visible: bool | None = None, key_field: bool | None = None, column_width: int | None = None):
1135
+ """
1136
+ If any values are given as None then that value will not be changed on the given field.
1137
+
1138
+ :param prepend_data_type: If true, prepends the data type name of the field to the data field name. For example,
1139
+ if a field has a data type name X and a data field name Y, then the field name would become "X.Y". This is
1140
+ useful for cases where you have the same field name on two different data types and want to distinguish one
1141
+ or both of them.
1142
+ :param display_name: Change the display name.
1143
+ :param required: Change the required status.
1144
+ :param editable: Change the editable status.
1145
+ :param visible: Change the visible status.
1146
+ :param key_field: Change the key field status.
1147
+ :param column_width: Change the column width.
1148
+ """
1149
+ self.prepend_data_type = prepend_data_type
1150
+ self.display_name = display_name
1151
+ self.required = required
1152
+ self.editable = editable
1153
+ self.visible = visible
1154
+ self.key_field = key_field
1155
+ self.column_width = column_width
1156
+
1157
+ def modify_field(self, field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1158
+ """
1159
+ Apply modifications to a given field.
1160
+
1161
+ :param field: The field to modify.
1162
+ :return: A copy of the input field with the modifications applied.
1163
+ """
1164
+ field = copy_field(field)
1165
+ if self.prepend_data_type is True:
1166
+ field._data_field_name = field.data_field_name + "." + field.data_field_name
1167
+ if self.display_name is not None:
1168
+ field.display_name = self.display_name
1169
+ if self.required is not None:
1170
+ field.required = self.required
1171
+ if self.editable is not None:
1172
+ field.editable = self.editable
1173
+ if self.visible is not None:
1174
+ field.visible = self.visible
1175
+ if self.key_field is not None:
1176
+ field.key_field = self.key_field
1177
+ if self.column_width is not None:
1178
+ field.default_table_column_width = self.column_width
1179
+ return field
1180
+
1181
+
1182
+ def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
1183
+ """
1184
+ Create a copy of a given field definition. This is used to modify field definitions from the server for existing
1185
+ data types without also modifying the field definition in the cache.
1186
+ """
1187
+ return FieldDefinitionParser.to_field_definition(field.to_json())