sapiopycommons 2024.8.28a313__tar.gz → 2024.8.28a314__tar.gz

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 (53) hide show
  1. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/.gitignore +2 -0
  2. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/PKG-INFO +4 -2
  3. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/README.md +1 -0
  4. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/pyproject.toml +2 -2
  5. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/callbacks/callback_util.py +277 -35
  6. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/chem/IndigoMolecules.py +1 -0
  7. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/chem/Molecules.py +1 -0
  8. sapiopycommons-2024.8.28a314/src/sapiopycommons/eln/experiment_report_util.py +214 -0
  9. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/file_bridge.py +16 -10
  10. sapiopycommons-2024.8.28a314/src/sapiopycommons/files/file_bridge_handler.py +318 -0
  11. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/file_util.py +13 -6
  12. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/file_validator.py +71 -0
  13. sapiopycommons-2024.8.28a314/src/sapiopycommons/general/accession_service.py +375 -0
  14. sapiopycommons-2024.8.28a314/src/sapiopycommons/general/custom_report_util.py +262 -0
  15. sapiopycommons-2024.8.28a314/src/sapiopycommons/multimodal/multimodal.py +146 -0
  16. sapiopycommons-2024.8.28a314/src/sapiopycommons/multimodal/multimodal_data.py +487 -0
  17. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/recordmodel/record_handler.py +278 -45
  18. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/webhook/webhook_handlers.py +58 -1
  19. sapiopycommons-2024.8.28a314/tests/_do_not_add_init_py_here +0 -0
  20. sapiopycommons-2024.8.28a314/tests/accession_test.py +41 -0
  21. sapiopycommons-2024.8.28a314/tests/bio_reg_test.py +26 -0
  22. sapiopycommons-2024.8.28a314/tests/chem_test.py +229 -0
  23. sapiopycommons-2024.8.28a314/tests/data_type_models.py +37332 -0
  24. sapiopycommons-2024.8.28a314/tests/kappa.chains.fasta +45 -0
  25. sapiopycommons-2024.8.28a314/tests/mafft_test.py +66 -0
  26. sapiopycommons-2024.8.28a314/tests/test.gb +124 -0
  27. sapiopycommons-2024.8.28a313/src/sapiopycommons/general/custom_report_util.py +0 -90
  28. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/LICENSE +0 -0
  29. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/__init__.py +0 -0
  30. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/callbacks/__init__.py +0 -0
  31. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/chem/__init__.py +0 -0
  32. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/datatype/__init__.py +0 -0
  33. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/datatype/attachment_util.py +0 -0
  34. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/eln/__init__.py +0 -0
  35. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/eln/experiment_handler.py +0 -0
  36. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/eln/plate_designer.py +0 -0
  37. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/__init__.py +0 -0
  38. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/complex_data_loader.py +0 -0
  39. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/file_data_handler.py +0 -0
  40. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/files/file_writer.py +0 -0
  41. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/__init__.py +0 -0
  42. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/aliases.py +0 -0
  43. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/exceptions.py +0 -0
  44. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/popup_util.py +0 -0
  45. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/storage_util.py +0 -0
  46. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/general/time_util.py +0 -0
  47. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/processtracking/__init__.py +0 -0
  48. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/processtracking/endpoints.py +0 -0
  49. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/recordmodel/__init__.py +0 -0
  50. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/rules/__init__.py +0 -0
  51. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/rules/eln_rule_handler.py +0 -0
  52. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/rules/on_save_rule_handler.py +0 -0
  53. {sapiopycommons-2024.8.28a313 → sapiopycommons-2024.8.28a314}/src/sapiopycommons/webhook/__init__.py +0 -0
@@ -6,3 +6,5 @@
6
6
  **/*.versionsBackup
7
7
  **/target
8
8
  /sapiopycommons/sapiopycommons.egg-info/
9
+ /sapiopycommons/dist**
10
+ /.pydevproject
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sapiopycommons
3
- Version: 2024.8.28a313
3
+ Version: 2024.8.28a314
4
4
  Summary: Official Sapio Python API Utilities Package
5
5
  Project-URL: Homepage, https://github.com/sapiosciences
6
6
  Author-email: Jonathan Steck <jsteck@sapiosciences.com>, Yechen Qiao <yqiao@sapiosciences.com>
@@ -17,7 +17,8 @@ Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: sapiopylib>=2023.12.13.174
20
+ Requires-Dist: databind>=4.5
21
+ Requires-Dist: sapiopylib>=2024.5.24.210
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
 
@@ -50,6 +51,7 @@ This license does not provide any rights to use any other copyrighted artifacts
50
51
  ## Dependencies
51
52
  The following dependencies are required for this package:
52
53
  - [sapiopylib - The official Sapio Informatics Platform Python API package.](https://pypi.org/project/sapiopylib/)
54
+ - [databind - Databind is a library inspired by jackson-databind to de-/serialize Python dataclasses.](https://pypi.org/project/databind/)
53
55
 
54
56
  ## Getting Help
55
57
  If you have a support contract with Sapio Sciences, please use our [technical support channels](https://sapio-sciences.atlassian.net/servicedesk/customer/portals).
@@ -28,6 +28,7 @@ This license does not provide any rights to use any other copyrighted artifacts
28
28
  ## Dependencies
29
29
  The following dependencies are required for this package:
30
30
  - [sapiopylib - The official Sapio Informatics Platform Python API package.](https://pypi.org/project/sapiopylib/)
31
+ - [databind - Databind is a library inspired by jackson-databind to de-/serialize Python dataclasses.](https://pypi.org/project/databind/)
31
32
 
32
33
  ## Getting Help
33
34
  If you have a support contract with Sapio Sciences, please use our [technical support channels](https://sapio-sciences.atlassian.net/servicedesk/customer/portals).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sapiopycommons"
7
- version='2024.08.28a313'
7
+ version='2024.08.28a314'
8
8
  authors = [
9
9
  { name="Jonathan Steck", email="jsteck@sapiosciences.com" },
10
10
  { name="Yechen Qiao", email="yqiao@sapiosciences.com" },
@@ -14,7 +14,7 @@ license = "MPL-2.0"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.10"
16
16
  dependencies = [
17
- 'sapiopylib>=2023.12.13.174'
17
+ 'sapiopylib>=2024.5.24.210', 'databind>=4.5'
18
18
  ]
19
19
  classifiers = [
20
20
  "Intended Audience :: Developers",
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import io
2
4
  from typing import Any
3
5
 
@@ -9,11 +11,11 @@ from sapiopylib.rest.pojo.DataRecord import DataRecord
9
11
  from sapiopylib.rest.pojo.datatype.DataType import DataTypeDefinition
10
12
  from sapiopylib.rest.pojo.datatype.DataTypeLayout import DataTypeLayout
11
13
  from sapiopylib.rest.pojo.datatype.FieldDefinition import AbstractVeloxFieldDefinition, VeloxStringFieldDefinition, \
12
- VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition
14
+ VeloxIntegerFieldDefinition, VeloxDoubleFieldDefinition, FieldDefinitionParser
13
15
  from sapiopylib.rest.pojo.webhook.ClientCallbackRequest import OptionDialogRequest, ListDialogRequest, \
14
16
  FormEntryDialogRequest, InputDialogCriteria, TableEntryDialogRequest, ESigningRequestPojo, \
15
- DataRecordSelectionRequest, DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, \
16
- MultiFilePromptRequest
17
+ DataRecordDialogRequest, InputSelectionRequest, FilePromptRequest, MultiFilePromptRequest, \
18
+ TempTableSelectionRequest
17
19
  from sapiopylib.rest.pojo.webhook.ClientCallbackResult import ESigningResponsePojo
18
20
  from sapiopylib.rest.pojo.webhook.WebhookContext import SapioWebhookContext
19
21
  from sapiopylib.rest.pojo.webhook.WebhookEnums import FormAccessLevel, ScanToSelectCriteria, SearchType
@@ -209,15 +211,15 @@ class CallbackUtil:
209
211
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
210
212
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
211
213
 
214
+ # Make everything visible, because presumably the caller gave a field name because they want it to be seen.
215
+ modifier = FieldModifier(visible=True, editable=editable)
216
+
212
217
  # Build the form using only those fields that are desired.
213
218
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
214
219
  for field_name in fields:
215
220
  field_def = field_defs.get(field_name)
216
221
  if field_def is None:
217
222
  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
223
  if hasattr(field_def, "default_value"):
222
224
  field_def.default_value = record.get_field_value(field_name)
223
225
  column: int = 0
@@ -226,7 +228,7 @@ class CallbackUtil:
226
228
  position = column_positions.get(field_name)
227
229
  column = position[0]
228
230
  span = position[1]
229
- builder.add_field(field_def, column, span)
231
+ builder.add_field(modifier.modify_field(field_def), column, span)
230
232
 
231
233
  request = FormEntryDialogRequest(title, msg, builder.get_temporary_data_type())
232
234
  response: FieldMap | None = self.callback.show_form_entry_dialog(request)
@@ -259,7 +261,7 @@ class CallbackUtil:
259
261
  :param field_name: The name and display name of the string field.
260
262
  :param default_value: The default value to place into the string field, if any.
261
263
  :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.
264
+ If neither this nor a default value are provided, defaults to 100 characters.
263
265
  :param editable: Whether the field is editable by the user.
264
266
  :param kwargs: Any additional keyword arguments to pass to the field definition.
265
267
  :return: The string that the user input into the dialog.
@@ -346,9 +348,13 @@ class CallbackUtil:
346
348
  if plural_display_name is None:
347
349
  plural_display_name = display_name + "s"
348
350
 
351
+ # Key fields display their columns in order before all non-key fields.
352
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
353
+ modifier = FieldModifier(key_field=False)
354
+
349
355
  builder = FormBuilder(data_type, display_name, plural_display_name)
350
- for column in fields:
351
- builder.add_field(column)
356
+ for field in fields:
357
+ builder.add_field(modifier.modify_field(field))
352
358
 
353
359
  request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
354
360
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
@@ -364,8 +370,9 @@ class CallbackUtil:
364
370
  editable: bool | None = True) -> list[FieldMap]:
365
371
  """
366
372
  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.
373
+ a given list of records of a singular type. Provided field names must match fields on the definition of the data
374
+ type of the given records. The fields that are displayed will have their default value be that of the fields on
375
+ the given records.
369
376
 
370
377
  Makes webservice calls to get the data type definition and fields of the given records if they weren't
371
378
  previously cached.
@@ -390,25 +397,181 @@ class CallbackUtil:
390
397
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
391
398
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
392
399
 
400
+ # Key fields display their columns in order before all non-key fields.
401
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
402
+ # Also make everything visible, because presumably the caller gave a field name because they want it to be seen.
403
+ modifier = FieldModifier(visible=True, key_field=False, editable=editable)
404
+
393
405
  # Build the form using only those fields that are desired.
394
406
  builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
395
407
  for field_name in fields:
396
408
  field_def = field_defs.get(field_name)
397
409
  if field_def is None:
398
410
  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)
411
+ builder.add_field(modifier.modify_field(field_def))
406
412
 
407
413
  request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), field_map_list)
408
414
  response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
409
415
  if response is None:
410
416
  raise SapioUserCancelledException()
411
417
  return response
418
+
419
+ def multi_type_table_dialog(self,
420
+ title: str,
421
+ msg: str,
422
+ fields: list[(str, str) | AbstractVeloxFieldDefinition],
423
+ row_contents: list[list[SapioRecord | FieldMap]],
424
+ *,
425
+ default_modifier: FieldModifier | None = None,
426
+ field_modifiers: dict[str, FieldModifier] | None = None,
427
+ data_type: str = "Default",
428
+ display_name: str | None = None,
429
+ plural_display_name: str | None = None) -> list[FieldMap]:
430
+ """
431
+ Create a table dialog where the user may input data into the fields of the table. The table is constructed from
432
+ a given list of records of multiple data types or field maps. Provided field names must match with field names
433
+ from the data type definition of the given records. The fields that are displayed will have their default value
434
+ be that of the fields on the given records or field maps.
435
+
436
+ Makes webservice calls to get the data type field definitions of the given records if they weren't
437
+ previously cached.
438
+
439
+ :param title: The title of the dialog.
440
+ :param msg: The message to display in the dialog.
441
+ :param fields: A list of objects representing the fields in the table. This could either be a two-element tuple
442
+ where the first element is a data type name and the second is a field name, or it could be a field
443
+ definition. If it is the former, a query will be made to find the field definition matching tht data type.
444
+ The data type names of the fields must match the data type names of the records in the row contents.
445
+ See the description of row_contents for what to do if you want to construct a field that pulls from a field
446
+ map.
447
+ If two fields share the same field name, an exception will be thrown. This is even true in the case where
448
+ the data type name of the fields is different. If you wish to display two fields from two data types with
449
+ the same name, then you must provide a FieldModifier for at least one of the fields where prepend_data_type
450
+ is True in order to make that field's name unique again. Note that if you do this for a field, the mapping
451
+ of record to field name will use the unedited field name, but the return results of this function will
452
+ use the edited field name in the results dictionary for a row.
453
+ :param row_contents: A list where each element is another list representing the records or a field map that will
454
+ be used to populate the columns of the table. If the data type of a given record doesn't match any of the
455
+ data type names of the given fields, then it will not be used.
456
+ This list can contain up to one field map, which are fields not tied to a record. This is so that you can
457
+ create abstract field definition not tied to a specific record in the system. If you want to define an
458
+ abstract field that pulls from the field map in the row contents, then you must set the data type name to
459
+ Default.
460
+ If a record of a given data type appears more than once in one of the inner-lists of the row contents, or
461
+ there is more than one field map, then an exception will be thrown, as there is no way of distinguishing
462
+ which record should be used for a field, and not all fields could have their values combined across multiple
463
+ records.
464
+ The row contents may have an inner-list which is missing a record of a data type that matches one of the
465
+ fields. In this case, the field value for that row under that column will be null.
466
+ The inner-list does not need to be sorted in any way, as this function will map the inner-list contents to
467
+ fields as necessary.
468
+ The inner-list may contain null elements; these will simply be discarded by this function.
469
+ :param default_modifier: A default field modifier that will be applied to the given fields. This can be used to
470
+ make field definitions from the system behave differently than their system values. If this value is None,
471
+ then a default field modifier is created that causes all specified fields to be both visible and not key
472
+ fields. (Key fields get displayed first before any non-key fields in tables, so the key field setting is
473
+ disabled by default in order to have the columns in the table respect the order of the fields as they are
474
+ provided to this function.)
475
+ :param field_modifiers: A mapping of data field name to field modifier for changes that should be applied to
476
+ the matching field. If a data field name is not present in the provided dict, or the provided dictionary is
477
+ None, then the default modifier will be used.
478
+ :param data_type: The data type name for the temporary data type that will be created for this table.
479
+ :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
480
+ name.
481
+ :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
482
+ the display name + "s".
483
+ :return: A list of dictionaries mapping the data field names of the given field definitions to the response
484
+ value from the user for that field for each row.
485
+ """
486
+ # Set the default modifier to make all fields visible and not key if no default was provided.
487
+ if default_modifier is None:
488
+ default_modifier = FieldModifier(visible=True, key_field=False)
489
+ # To make things simpler, treat null field modifiers as an empty dict.
490
+ if field_modifiers is None:
491
+ field_modifiers = {}
492
+
493
+ # Construct the final fields list from the possible field objects.
494
+ final_fields: list[AbstractVeloxFieldDefinition] = []
495
+ # Keep track of whether any given field name appears more than once, as two fields could have the same
496
+ # field name but different data types. In this case, the user should provide a field modifier or field
497
+ # definition that changes one of the field names.
498
+ field_names: list[str] = []
499
+ for field in fields:
500
+ # Find the field definition for this field object.
501
+ if isinstance(field, tuple):
502
+ field_def: AbstractVeloxFieldDefinition = self.dt_cache.get_fields_for_type(field[0]).get(field[1])
503
+ elif isinstance(field, AbstractVeloxFieldDefinition):
504
+ field_def: AbstractVeloxFieldDefinition = field
505
+ else:
506
+ raise SapioException("Unrecognized field object.")
507
+
508
+ # Locate the modifier for this field and store the modified field.
509
+ name: str = field_def.data_field_name
510
+ modifier: FieldModifier = field_modifiers.get(name, default_modifier)
511
+ field_def: AbstractVeloxFieldDefinition = modifier.modify_field(field_def)
512
+ final_fields.append(field_def)
513
+
514
+ # Verify that this field name isn't a duplicate.
515
+ # The field name may have changed due to the modifier.
516
+ name: str = field_def.data_field_name
517
+ if name in field_names:
518
+ raise SapioException(f"The field name \"{name}\" appears more than once in the given fields. "
519
+ f"If you have provided two fields with the same name but different data types, "
520
+ f"consider providing a FieldModifier where prepend_data_type is true for one of "
521
+ f"the fields so that the field names will be different.")
522
+ field_names.append(name)
523
+
524
+ # Get the values for each row.
525
+ values: list[dict[str, Any]] = []
526
+ for row in row_contents:
527
+ # The final values for this row:
528
+ row_values: dict[str, Any] = {}
529
+
530
+ # Map the records for this row by their data type. If a field map is provided, its data type is Default.
531
+ row_records: dict[str, SapioRecord | FieldMap] = {}
532
+ for rec in row:
533
+ # Toss out null elements.
534
+ if rec is None:
535
+ continue
536
+ # Map records to their data type name. Map field maps to Default.
537
+ dt: str = "Default" if isinstance(rec, dict) else rec.data_type_name
538
+ # Warn if the same data type name appears more than once.
539
+ if dt in row_records:
540
+ raise SapioException(f"The data type \"{dt}\" appears more than once in the given row contents.")
541
+ row_records[dt] = rec
542
+
543
+ # Get the field values from the above records.
544
+ for field in final_fields:
545
+ # Find the object that corresponds to this field given its data type name.
546
+ record: SapioRecord | FieldMap | None = row_records.get(field.data_type_name)
547
+ # This could be either a record, a field map, or null. Convert any records to field maps.
548
+ if not isinstance(record, dict) and record is not None:
549
+ record: FieldMap | None = AliasUtil.to_field_map_lists([record])[0]
550
+
551
+ # Find out if this field had its data type prepended to it. If this is the case, then we need to find
552
+ # the true data field name before retrieving the value from the field map.
553
+ name: str = field.data_field_name
554
+ if field_modifiers.get(name, default_modifier).prepend_data_type is True:
555
+ name = name.split(".")[1]
556
+
557
+ # Set the value for this particular field.
558
+ row_values[field.data_field_name] = record.get(name) if record else None
559
+ values.append(row_values)
560
+
561
+ if display_name is None:
562
+ display_name = data_type
563
+ if plural_display_name is None:
564
+ plural_display_name = display_name + "s"
565
+
566
+ builder = FormBuilder(data_type, display_name, plural_display_name)
567
+ for field in final_fields:
568
+ builder.add_field(field)
569
+
570
+ request = TableEntryDialogRequest(title, msg, builder.get_temporary_data_type(), values)
571
+ response: list[FieldMap] | None = self.callback.show_table_entry_dialog(request)
572
+ if response is None:
573
+ raise SapioUserCancelledException()
574
+ return response
412
575
 
413
576
  def record_view_dialog(self,
414
577
  title: str,
@@ -464,7 +627,8 @@ class CallbackUtil:
464
627
  values: list[FieldMap],
465
628
  multi_select: bool = True,
466
629
  *,
467
- display_name: str = "Default",
630
+ data_type: str = "Default",
631
+ display_name: str | None = None,
468
632
  plural_display_name: str | None = None) -> list[FieldMap]:
469
633
  """
470
634
  Create a selection dialog for a list of field maps for the user to choose from. Requires that the caller
@@ -475,18 +639,25 @@ class CallbackUtil:
475
639
  they are provided in this list.
476
640
  :param values: The values to set for each row of the table.
477
641
  :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.
642
+ :param data_type: The data type name for the temporary data type that will be created for this table.
643
+ :param display_name: The display name for the temporary data type. If not provided, defaults to the data type
644
+ name.
479
645
  :param plural_display_name: The plural display name for the temporary data type. If not provided, defaults to
480
646
  the display name + "s".
481
647
  :return: A list of field maps corresponding to the chosen input field maps.
482
648
  """
649
+ if display_name is None:
650
+ display_name = data_type
483
651
  if plural_display_name is None:
484
652
  plural_display_name = display_name + "s"
485
653
 
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)
654
+ builder = FormBuilder(data_type, display_name, plural_display_name)
655
+ for field in fields:
656
+ builder.add_field(field)
657
+
658
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, values,
659
+ multi_select=multi_select)
660
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
490
661
  if response is None:
491
662
  raise SapioUserCancelledException()
492
663
  return response
@@ -521,21 +692,22 @@ class CallbackUtil:
521
692
  type_def: DataTypeDefinition = self.dt_cache.get_data_type(data_type)
522
693
  field_defs: dict[str, AbstractVeloxFieldDefinition] = self.dt_cache.get_fields_for_type(data_type)
523
694
 
695
+ # Key fields display their columns in order before all non-key fields.
696
+ # Unmark key fields so that the column order is respected exactly as the caller provides it.
697
+ # Also make everything visible, because presumably the caller give a field name because they want it to be seen.
698
+ modifier = FieldModifier(visible=True, key_field=False)
699
+
524
700
  # Build the form using only those fields that are desired.
525
- field_def_list: list = []
701
+ builder = FormBuilder(data_type, type_def.display_name, type_def.plural_display_name)
526
702
  for field_name in fields:
527
703
  field_def = field_defs.get(field_name)
528
704
  if field_def is None:
529
705
  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)
706
+ builder.add_field(modifier.modify_field(field_def))
707
+
708
+ request = TempTableSelectionRequest(builder.get_temporary_data_type(), msg, field_map_list,
709
+ multi_select=multi_select)
710
+ response: list[FieldMap] | None = self.callback.show_temp_table_selection_dialog(request)
539
711
  if response is None:
540
712
  raise SapioUserCancelledException()
541
713
  # Map the field maps in the response back to the record they come from, returning the chosen record instead of
@@ -736,3 +908,73 @@ class CallbackUtil:
736
908
  """
737
909
  data = io.StringIO(file_data) if isinstance(file_data, str) else io.BytesIO(file_data)
738
910
  self.callback.send_file(file_name, False, data)
911
+
912
+
913
+ class FieldModifier:
914
+ """
915
+ A FieldModifier can be used to update the settings of a field definition from the system.
916
+ """
917
+ prepend_data_type: bool
918
+ display_name: str | None
919
+ required: bool | None
920
+ editable: bool | None
921
+ visible: bool | None
922
+ key_field: bool | None
923
+ column_width: int | None
924
+
925
+ def __init__(self, *, prepend_data_type: bool = False,
926
+ display_name: str | None = None, required: bool | None = None, editable: bool | None = None,
927
+ visible: bool | None = None, key_field: bool | None = None, column_width: int | None = None):
928
+ """
929
+ If any values are given as None then that value will not be changed on the given field.
930
+
931
+ :param prepend_data_type: If true, prepends the data type name of the field to the data field name. For example,
932
+ 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
933
+ useful for cases where you have the same field name on two different data types and want to distinguish one
934
+ or both of them.
935
+ :param display_name: Change the display name.
936
+ :param required: Change the required status.
937
+ :param editable: Change the editable status.
938
+ :param visible: Change the visible status.
939
+ :param key_field: Change the key field status.
940
+ :param column_width: Change the column width.
941
+ """
942
+ self.prepend_data_type = prepend_data_type
943
+ self.display_name = display_name
944
+ self.required = required
945
+ self.editable = editable
946
+ self.visible = visible
947
+ self.key_field = key_field
948
+ self.column_width = column_width
949
+
950
+ def modify_field(self, field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
951
+ """
952
+ Apply modifications to a given field.
953
+
954
+ :param field: The field to modify.
955
+ :return: A copy of the input field with the modifications applied.
956
+ """
957
+ field = copy_field(field)
958
+ if self.prepend_data_type is True:
959
+ field._data_field_name = field.data_field_name + "." + field.data_field_name
960
+ if self.display_name is not None:
961
+ field.display_name = self.display_name
962
+ if self.required is not None:
963
+ field.required = self.required
964
+ if self.editable is not None:
965
+ field.editable = self.editable
966
+ if self.visible is not None:
967
+ field.visible = self.visible
968
+ if self.key_field is not None:
969
+ field.key_field = self.key_field
970
+ if self.column_width is not None:
971
+ field.default_table_column_width = self.column_width
972
+ return field
973
+
974
+
975
+ def copy_field(field: AbstractVeloxFieldDefinition) -> AbstractVeloxFieldDefinition:
976
+ """
977
+ Create a copy of a given field definition. This is used to modify field definitions from the server for existing
978
+ data types without also modifying the field definition in the cache.
979
+ """
980
+ return FieldDefinitionParser.to_field_definition(field.to_json())
@@ -6,6 +6,7 @@ indigo = Indigo()
6
6
  renderer = IndigoRenderer(indigo)
7
7
  indigo.setOption("render-output-format", "svg")
8
8
  indigo.setOption("ignore-stereochemistry-errors", True)
9
+ indigo.setOption("render-stereo-style", "ext")
9
10
  indigo.setOption("aromaticity-model", "generic")
10
11
  indigo.setOption("render-coloring", True)
11
12
  indigo_inchi = IndigoInchi(indigo);
@@ -181,6 +181,7 @@ def mol_to_sapio_substance(mol: Mol, include_stereoisomers: bool = False,
181
181
  # We need to test the INCHI can be loaded back to indigo.
182
182
  indigo_mol = indigo.loadMolecule(molBlock)
183
183
  indigo_mol.aromatize()
184
+ indigo_inchi.resetOptions()
184
185
  indigo_inchi_str = indigo_inchi.getInchi(indigo_mol)
185
186
  molecule["inchi"] = indigo_inchi_str
186
187
  indigo_inchi_key_str = indigo_inchi.getInchiKey(indigo_inchi_str)