hap-cli 0.8.17__tar.gz → 0.8.19__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.
Files changed (256) hide show
  1. {hap_cli-0.8.17 → hap_cli-0.8.19}/PKG-INFO +1 -1
  2. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +55 -0
  4. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/record.py +130 -22
  5. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/worksheet_templates.py +55 -0
  6. hap_cli-0.8.19/hap_cli/tests/test_member_relation_array_wrap.py +103 -0
  7. hap_cli-0.8.19/hap_cli/tests/test_select_rejects_relation_value.py +178 -0
  8. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/PKG-INFO +1 -1
  9. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/SOURCES.txt +2 -0
  10. {hap_cli-0.8.17 → hap_cli-0.8.19}/MANIFEST.in +0 -0
  11. {hap_cli-0.8.17 → hap_cli-0.8.19}/README.md +0 -0
  12. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/__init__.py +0 -0
  13. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/__main__.py +0 -0
  14. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/config.py +0 -0
  15. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/harness.py +0 -0
  16. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/smoke.py +0 -0
  17. {hap_cli-0.8.17 → hap_cli-0.8.19}/app_live_tests/state.py +0 -0
  18. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/README.md +0 -0
  19. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/README_CN.md +0 -0
  20. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/__main__.py +0 -0
  21. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/__init__.py +0 -0
  22. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_cmd.py +0 -0
  23. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_creator_cmd.py +0 -0
  24. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/app_editor_cmd.py +0 -0
  25. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/auth_cmd.py +0 -0
  26. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/calendar_cmd.py +0 -0
  27. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chart_cmd.py +0 -0
  28. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chat_cmd.py +0 -0
  29. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/chatbot_cmd.py +0 -0
  30. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/config_cmd.py +0 -0
  31. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/contact_cmd.py +0 -0
  32. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/department_cmd.py +0 -0
  33. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/group_cmd.py +0 -0
  34. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/icon_cmd.py +0 -0
  35. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/instance_cmd.py +0 -0
  36. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/node_cmd.py +0 -0
  37. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/optionset_cmd.py +0 -0
  38. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/page_cmd.py +0 -0
  39. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/post_cmd.py +0 -0
  40. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/record_cmd.py +0 -0
  41. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/region_cmd.py +0 -0
  42. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/role_cmd.py +0 -0
  43. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/update_cmd.py +0 -0
  44. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/upload_cmd.py +0 -0
  45. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/v3_registry.py +0 -0
  46. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/workflow_cmd.py +0 -0
  47. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/commands/worksheet_cmd.py +0 -0
  48. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/context.py +0 -0
  49. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/__init__.py +0 -0
  50. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/action_spec_adapter.py +0 -0
  51. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app.py +0 -0
  52. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/__init__.py +0 -0
  53. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  54. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  55. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/charts.py +0 -0
  56. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/cleanup.py +0 -0
  57. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/compiler.py +0 -0
  58. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/config.py +0 -0
  59. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  60. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/errors.py +0 -0
  61. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  62. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/executor.py +0 -0
  63. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/fields.py +0 -0
  64. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/hap.py +0 -0
  65. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/merge.py +0 -0
  66. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  67. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/console.py +0 -0
  68. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  69. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  70. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/recording/report.py +0 -0
  71. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/schema.py +0 -0
  72. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  73. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  74. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/cli.py +0 -0
  75. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/executor.py +0 -0
  76. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  77. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  78. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  79. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  80. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/seed/template.py +0 -0
  81. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/selftest.py +0 -0
  82. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/smoke.py +0 -0
  83. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/steps.py +0 -0
  84. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/store.py +0 -0
  85. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/validate.py +0 -0
  86. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  87. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/auth.py +0 -0
  88. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/calendar_mod.py +0 -0
  89. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chart.py +0 -0
  90. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chat.py +0 -0
  91. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/chatbot.py +0 -0
  92. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/cli_access.py +0 -0
  93. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/contact.py +0 -0
  94. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/control_type_codes.py +0 -0
  95. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/department.py +0 -0
  96. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/__init__.py +0 -0
  97. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  98. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  99. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/apply.py +0 -0
  100. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/componentlower.py +0 -0
  101. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/editspec.py +0 -0
  102. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/errors.py +0 -0
  103. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/fieldlower.py +0 -0
  104. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  105. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/models.py +0 -0
  106. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/ops.py +0 -0
  107. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/planner.py +0 -0
  108. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/editor/reader.py +0 -0
  109. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/field_normalizer.py +0 -0
  110. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/field_spec_adapter.py +0 -0
  111. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/filter_translator.py +0 -0
  112. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/flow_node.py +0 -0
  113. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/global_meta.py +0 -0
  114. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/group.py +0 -0
  115. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/icon_index.py +0 -0
  116. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/instance.py +0 -0
  117. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/logger.py +0 -0
  118. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/optionset.py +0 -0
  119. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/page.py +0 -0
  120. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/post.py +0 -0
  121. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/response_crypto.py +0 -0
  122. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/role.py +0 -0
  123. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/role_perm_builder.py +0 -0
  124. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/session.py +0 -0
  125. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/token_crypto.py +0 -0
  126. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/update.py +0 -0
  127. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/upload.py +0 -0
  128. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/__init__.py +0 -0
  129. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/dispatcher.py +0 -0
  130. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/render.py +0 -0
  131. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/v3/schema.py +0 -0
  132. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/view_spec_adapter.py +0 -0
  133. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/workflow.py +0 -0
  134. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/workflow_node_dsl.py +0 -0
  135. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/core/worksheet.py +0 -0
  136. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/hap_cli.py +0 -0
  137. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/i18n.py +0 -0
  138. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/__init__.py +0 -0
  139. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/messages.json +0 -0
  140. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/locale/messages.schema.json +0 -0
  141. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/__init__.py +0 -0
  142. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/conftest.py +0 -0
  143. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_creator.py +0 -0
  144. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_editor.py +0 -0
  145. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_app_export_usage.py +0 -0
  146. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_auth_prerelease.py +0 -0
  147. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_bug_036430.py +0 -0
  148. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_bug_036750.py +0 -0
  149. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_chart.py +0 -0
  150. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_cli_access.py +0 -0
  151. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_config_cmd.py +0 -0
  152. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_core.py +0 -0
  153. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  154. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_full_e2e.py +0 -0
  155. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_global_meta.py +0 -0
  156. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_i18n.py +0 -0
  157. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_instance_history_cli.py +0 -0
  158. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration.py +0 -0
  159. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_approval.py +0 -0
  160. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  161. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_calendar.py +0 -0
  162. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_destructive.py +0 -0
  163. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_misc.py +0 -0
  164. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_post.py +0 -0
  165. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_social.py +0 -0
  166. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_v3.py +0 -0
  167. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_workflow.py +0 -0
  168. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  169. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_multi_profile.py +0 -0
  170. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  171. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_id_cli.py +0 -0
  172. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_org_id_docs.py +0 -0
  173. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_parameter_conventions.py +0 -0
  174. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  175. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  176. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_field_alias.py +0 -0
  177. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  178. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  179. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_update.py +0 -0
  180. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  181. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  182. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  183. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_session.py +0 -0
  184. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  185. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  186. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  187. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  188. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  189. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/__init__.py +0 -0
  190. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/formatting.py +0 -0
  191. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/options.py +0 -0
  192. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli/utils/parameter_mapping.py +0 -0
  193. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/dependency_links.txt +0 -0
  194. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/entry_points.txt +0 -0
  195. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/requires.txt +0 -0
  196. {hap_cli-0.8.17 → hap_cli-0.8.19}/hap_cli.egg-info/top_level.txt +0 -0
  197. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/__init__.py +0 -0
  198. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/__main__.py +0 -0
  199. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/_wftest.py +0 -0
  200. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/charts.py +0 -0
  201. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/cleanup.py +0 -0
  202. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/compiler.py +0 -0
  203. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/config.py +0 -0
  204. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/errors.py +0 -0
  205. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/executor.py +0 -0
  206. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/fields.py +0 -0
  207. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/hap.py +0 -0
  208. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/__init__.py +0 -0
  209. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/console.py +0 -0
  210. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/jsonl.py +0 -0
  211. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/mirror.py +0 -0
  212. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/recording/report.py +0 -0
  213. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/schema.py +0 -0
  214. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/__init__.py +0 -0
  215. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/cli.py +0 -0
  216. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/executor.py +0 -0
  217. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/seed/template.py +0 -0
  218. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/selftest.py +0 -0
  219. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/smoke.py +0 -0
  220. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/step.py +0 -0
  221. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/steps.py +0 -0
  222. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/store.py +0 -0
  223. {hap_cli-0.8.17 → hap_cli-0.8.19}/live_tests/workflow_dsl.py +0 -0
  224. {hap_cli-0.8.17 → hap_cli-0.8.19}/setup.cfg +0 -0
  225. {hap_cli-0.8.17 → hap_cli-0.8.19}/setup.py +0 -0
  226. {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
  227. {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
  228. {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
  229. {hap_cli-0.8.17 → hap_cli-0.8.19}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
  230. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/INDEX.json +0 -0
  231. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  232. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  233. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/create_optionset.yaml +0 -0
  234. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/create_role.yaml +0 -0
  235. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  236. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_record.yaml +0 -0
  237. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_role.yaml +0 -0
  238. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  239. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_info.yaml +0 -0
  240. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  241. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  242. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  243. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_details.yaml +0 -0
  244. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  245. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_list.yaml +0 -0
  246. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  247. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  248. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  249. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  250. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_regions.yaml +0 -0
  251. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_role_details.yaml +0 -0
  252. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/get_role_list.yaml +0 -0
  253. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  254. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  255. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  256. {hap_cli-0.8.17 → hap_cli-0.8.19}/sources/v3-api-schema/update_optionset.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.17
3
+ Version: 0.8.19
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """CLI harness for Mingdao HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.8.17"
3
+ __version__ = "0.8.19"
@@ -486,6 +486,34 @@ def build_control(
486
486
  return ctrl
487
487
 
488
488
 
489
+ # The fields a HAP select option may legitimately carry. The base shape
490
+ # is {key, value, isDeleted, index, checked, color} (pd-openweb
491
+ # src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
492
+ # the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
493
+ # no ``type`` field — an option carrying one is malformed input, not a HAP
494
+ # shape, so it is rejected rather than silently dropped.
495
+ _ALLOWED_OPTION_FIELDS = frozenset(
496
+ {"key", "value", "isDeleted", "index", "checked", "color", "score"}
497
+ )
498
+
499
+
500
+ def _option_value_is_object_shaped(v: Any) -> bool:
501
+ """True when an option key/value is a relation/member/object shape
502
+ (``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
503
+ only surface server-side as a ``ControlOptions`` deserialization
504
+ failure on the batch save."""
505
+ if isinstance(v, (list, tuple, dict)):
506
+ return True
507
+ if isinstance(v, str):
508
+ s = v.strip()
509
+ if s[:1] in ("[", "{"):
510
+ try:
511
+ return isinstance(json.loads(s), (list, dict))
512
+ except (ValueError, TypeError):
513
+ return False
514
+ return False
515
+
516
+
489
517
  def _normalize_options(
490
518
  opts: list[str] | list[dict[str, Any]],
491
519
  ) -> list[dict[str, Any]]:
@@ -493,6 +521,10 @@ def _normalize_options(
493
521
 
494
522
  First item is marked as the default selection (checked=True) unless
495
523
  any caller-supplied dict already sets it.
524
+
525
+ Option ``value``/``key`` must be plain text; a relation/object shape
526
+ or an unsupported field (e.g. ``type``) is rejected up front rather
527
+ than silently mangled or dropped.
496
528
  """
497
529
  colors = [
498
530
  "#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
@@ -502,6 +534,27 @@ def _normalize_options(
502
534
  any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
503
535
  for i, o in enumerate(opts):
504
536
  if isinstance(o, dict):
537
+ unknown = set(o) - _ALLOWED_OPTION_FIELDS
538
+ if unknown:
539
+ raise ValueError(
540
+ f"Select option {o!r} has unsupported field(s) "
541
+ f"{sorted(unknown)}; an option only accepts "
542
+ f"{sorted(_ALLOWED_OPTION_FIELDS)}."
543
+ )
544
+ if o.get("value") in (None, ""):
545
+ raise ValueError(
546
+ f"Select option {o!r} is missing a non-empty 'value'."
547
+ )
548
+ if _option_value_is_object_shaped(o.get("value")):
549
+ raise ValueError(
550
+ f"Select option 'value' must be plain text, got a "
551
+ f"relation/object-shaped value: {o.get('value')!r}."
552
+ )
553
+ if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
554
+ raise ValueError(
555
+ f"Select option 'key' must be a string, got a "
556
+ f"relation/object-shaped value: {o.get('key')!r}."
557
+ )
505
558
  entry = {
506
559
  "key": o.get("key") or str(uuid.uuid4()),
507
560
  "value": o["value"],
@@ -510,6 +563,8 @@ def _normalize_options(
510
563
  "checked": o.get("checked", False),
511
564
  "color": o.get("color", colors[i % len(colors)]),
512
565
  }
566
+ if "score" in o:
567
+ entry["score"] = o["score"]
513
568
  else:
514
569
  entry = {
515
570
  "key": str(uuid.uuid4()),
@@ -22,13 +22,56 @@ DISCUSSION_SOURCE_WORKSHEET_ROW = 8
22
22
  _LEGACY_SKIP_TYPES = {30, 31, 32, 33, 37, 38, 53}
23
23
 
24
24
 
25
+ def _option_field_label(ctrl: dict[str, Any]) -> str:
26
+ return str(ctrl.get("controlName") or ctrl.get("controlId") or "option field")
27
+
28
+
29
+ def _reject_relation_shaped_option(ctrl: dict[str, Any], item: Any) -> None:
30
+ """Fail loudly when a select cell is handed a relation/member/sub-table
31
+ *object* shape instead of an option value.
32
+
33
+ A single/multi-select cell legitimately carries only an option key, a
34
+ known label, or an ``add_<text>`` new-option directive — never a
35
+ ``[{"sid": rowId}]`` / ``{...}`` structure. When such a value slips
36
+ through (e.g. row data maps a relation value to the wrong child field),
37
+ the old code passed it straight to the server, which then failed
38
+ deserializing ``ControlOptions`` on the batch save. Catching it here
39
+ points the blame at the offending field instead of a generic 500.
40
+ """
41
+ looks_object = isinstance(item, (list, tuple, dict))
42
+ if not looks_object and isinstance(item, str):
43
+ stripped = item.strip()
44
+ if stripped[:1] in ("[", "{"):
45
+ try:
46
+ parsed = json.loads(stripped)
47
+ except (ValueError, TypeError):
48
+ parsed = None
49
+ looks_object = isinstance(parsed, (list, dict))
50
+ if looks_object:
51
+ raise ValueError(
52
+ f"Field '{_option_field_label(ctrl)}' is a select field and "
53
+ f"expects an option key, a label, or an 'add_<text>' new-option "
54
+ f"value, but got a relation/object-shaped value: {item!r}. "
55
+ f"This value belongs to a relation or member field, not this "
56
+ f"select field — check the field mapping."
57
+ )
58
+
59
+
25
60
  def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
26
61
  """Map option labels (or already-keys) to the option `key` values
27
- the legacy AddWorksheetRow expects (it takes keys, not labels)."""
62
+ the legacy AddWorksheetRow expects (it takes keys, not labels).
63
+
64
+ Relation/member/sub-table *object* shapes (``[{"sid": …}]`` etc.) are
65
+ rejected up front — they are never valid option values and would only
66
+ surface server-side as a ``ControlOptions`` deserialization failure on
67
+ the batch save.
68
+ """
28
69
  if raw is None or raw == "":
29
70
  return []
30
71
  if isinstance(raw, (list, tuple)):
31
- items = [str(v) for v in raw]
72
+ items: list[Any] = list(raw)
73
+ elif isinstance(raw, dict):
74
+ items = [raw]
32
75
  else:
33
76
  items = [s for s in str(raw).split(",") if s != ""]
34
77
  by_label: dict[str, str] = {}
@@ -40,7 +83,9 @@ def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
40
83
  by_label[str(opt.get("value", ""))] = key
41
84
  out: list[str] = []
42
85
  for it in items:
43
- out.append(it if it in valid_keys else by_label.get(it, it))
86
+ _reject_relation_shaped_option(ctrl, it)
87
+ s = str(it)
88
+ out.append(s if s in valid_keys else by_label.get(s, s))
44
89
  return out
45
90
 
46
91
 
@@ -214,6 +259,67 @@ def _legacy_cell_entry(
214
259
  return {"value": _legacy_cell_value(ctrl, value)}
215
260
 
216
261
 
262
+ def _coerce_entity_ids(
263
+ value: Any, id_keys: tuple[str, ...], *, field_label: str,
264
+ ) -> list[str]:
265
+ """Flatten a member / department / org-role / relation cell value to a
266
+ list of id strings, ready to be wrapped as ``[{idKey: id}, ...]``.
267
+
268
+ Accepts every realistic input shape so callers never have to pre-shape:
269
+ * a scalar id, or comma-separated ids (``"a,b"``);
270
+ * a list/tuple mixing ids and ``{idKey: ...}`` objects;
271
+ * a single ``{idKey: ...}`` object — the ``get_records`` read-back
272
+ shape carries display fields (``fullname`` / ``avatar`` / ``name``)
273
+ too, so only the id is kept;
274
+ * a JSON-array/object *string* of the above (e.g. a value copied
275
+ straight out of a record dump).
276
+
277
+ ``id_keys`` lists the accepted id field names in priority order, e.g.
278
+ ``("accountId", "aid")`` for USER_PICKER. An object that carries none of
279
+ them fails loudly instead of being ``str()``-mangled into a junk id —
280
+ that silent mangling sent garbage members to the server before.
281
+ """
282
+ if value is None or value == "":
283
+ return []
284
+ # A JSON-array/object string is parsed structurally rather than
285
+ # comma-split (a member object's JSON has commas inside it).
286
+ if isinstance(value, str):
287
+ s = value.strip()
288
+ if s[:1] in ("[", "{"):
289
+ try:
290
+ value = json.loads(s)
291
+ except (ValueError, TypeError):
292
+ pass
293
+ if isinstance(value, dict):
294
+ items: list[Any] = [value]
295
+ elif isinstance(value, (list, tuple)):
296
+ items = list(value)
297
+ else:
298
+ items = [p for p in str(value).split(",") if p.strip() != ""]
299
+ out: list[str] = []
300
+ for it in items:
301
+ if isinstance(it, dict):
302
+ rid = next(
303
+ (str(it[k]) for k in id_keys if it.get(k) not in (None, "")),
304
+ None,
305
+ )
306
+ if rid is None:
307
+ raise ValueError(
308
+ f"Field '{field_label}': object value is missing an id "
309
+ f"field ({'/'.join(id_keys)}): {it!r}"
310
+ )
311
+ out.append(rid)
312
+ elif isinstance(it, (list, tuple)):
313
+ raise ValueError(
314
+ f"Field '{field_label}': nested array is not a valid id: {it!r}"
315
+ )
316
+ else:
317
+ s2 = str(it).strip()
318
+ if s2:
319
+ out.append(s2)
320
+ return out
321
+
322
+
217
323
  def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
218
324
  """Serialize one cell to the legacy receiveControls `value`
219
325
  string, per the shapes proven in
@@ -226,14 +332,15 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
226
332
  if t == 10: # MULTI_SELECT
227
333
  return json.dumps(_resolve_option_keys(ctrl, value), ensure_ascii=False)
228
334
 
335
+ label = _option_field_label(ctrl)
336
+
229
337
  if t in (29, 35): # RELATE_SHEET / CASCADING_SELECT -> [{"sid": rowId}, ...]
230
338
  # Cascade (35) is a relation to a row in another worksheet, persisted
231
339
  # the same way as a plain relate (29): an array of {"sid": rowId}. A
232
- # bare rowId makes the server answer "服务异常" (bug #642cc8).
233
- if isinstance(value, (list, tuple)):
234
- ids = [str(v) for v in value]
235
- else:
236
- ids = [s for s in str(value).split(",") if s != ""]
340
+ # bare rowId makes the server answer "服务异常" (bug #642cc8). Object
341
+ # inputs ({"sid": rowId, ...} read-back shape) keep only the sid.
342
+ ids = _coerce_entity_ids(value, ("sid", "rowid", "rowId"),
343
+ field_label=label)
237
344
  return json.dumps([{"sid": rid} for rid in ids], ensure_ascii=False)
238
345
 
239
346
  if t == 48: # ORG_ROLE -> [{"organizeId": id}, ...]
@@ -241,20 +348,21 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
241
348
  # of {"organizeId": id} objects; organizeName is display-only and the
242
349
  # server resolves it from the id. A bare organizeId is rejected with
243
350
  # "服务异常" (bug #642cc8). See pd-openweb CellControls/OrgRole.jsx.
244
- ids = value if isinstance(value, (list, tuple)) else \
245
- [s for s in str(value).split(",") if s != ""]
246
- return json.dumps([{"organizeId": str(i)} for i in ids],
247
- ensure_ascii=False)
248
-
249
- if t == 26: # USER_PICKER
250
- ids = value if isinstance(value, (list, tuple)) else \
251
- [s for s in str(value).split(",") if s != ""]
252
- return json.dumps([{"accountId": str(i)} for i in ids],
253
- ensure_ascii=False)
254
- if t == 27: # DEPARTMENT
255
- ids = value if isinstance(value, (list, tuple)) else \
256
- [s for s in str(value).split(",") if s != ""]
257
- return json.dumps([{"departmentId": str(i)} for i in ids],
351
+ ids = _coerce_entity_ids(value, ("organizeId", "id"),
352
+ field_label=label)
353
+ return json.dumps([{"organizeId": i} for i in ids], ensure_ascii=False)
354
+
355
+ if t == 26: # USER_PICKER -> always an array of {"accountId": id}, even
356
+ # for a single member. Object inputs (the full member object the UI
357
+ # holds / get_records returns: {accountId, fullname, avatar, ...})
358
+ # are reduced to their accountId a bare object would fail the save.
359
+ ids = _coerce_entity_ids(value, ("accountId", "aid"),
360
+ field_label=label)
361
+ return json.dumps([{"accountId": i} for i in ids], ensure_ascii=False)
362
+ if t == 27: # DEPARTMENT -> [{"departmentId": id}, ...]
363
+ ids = _coerce_entity_ids(value, ("departmentId", "id"),
364
+ field_label=label)
365
+ return json.dumps([{"departmentId": i} for i in ids],
258
366
  ensure_ascii=False)
259
367
 
260
368
  if t == 36: # SWITCH / checkbox
@@ -471,6 +471,34 @@ def build_control(
471
471
  return ctrl
472
472
 
473
473
 
474
+ # The fields a HAP select option may legitimately carry. The base shape
475
+ # is {key, value, isDeleted, index, checked, color} (pd-openweb
476
+ # src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
477
+ # the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
478
+ # no ``type`` field — an option carrying one is malformed input, not a HAP
479
+ # shape, so it is rejected rather than silently dropped.
480
+ _ALLOWED_OPTION_FIELDS = frozenset(
481
+ {"key", "value", "isDeleted", "index", "checked", "color", "score"}
482
+ )
483
+
484
+
485
+ def _option_value_is_object_shaped(v: Any) -> bool:
486
+ """True when an option key/value is a relation/member/object shape
487
+ (``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
488
+ only surface server-side as a ``ControlOptions`` deserialization
489
+ failure on the batch save."""
490
+ if isinstance(v, (list, tuple, dict)):
491
+ return True
492
+ if isinstance(v, str):
493
+ s = v.strip()
494
+ if s[:1] in ("[", "{"):
495
+ try:
496
+ return isinstance(json.loads(s), (list, dict))
497
+ except (ValueError, TypeError):
498
+ return False
499
+ return False
500
+
501
+
474
502
  def _normalize_options(
475
503
  opts: list[str] | list[dict[str, Any]],
476
504
  ) -> list[dict[str, Any]]:
@@ -478,6 +506,10 @@ def _normalize_options(
478
506
 
479
507
  First item is marked as the default selection (checked=True) unless
480
508
  any caller-supplied dict already sets it.
509
+
510
+ Option ``value``/``key`` must be plain text; a relation/object shape
511
+ or an unsupported field (e.g. ``type``) is rejected up front rather
512
+ than silently mangled or dropped.
481
513
  """
482
514
  colors = [
483
515
  "#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
@@ -487,6 +519,27 @@ def _normalize_options(
487
519
  any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
488
520
  for i, o in enumerate(opts):
489
521
  if isinstance(o, dict):
522
+ unknown = set(o) - _ALLOWED_OPTION_FIELDS
523
+ if unknown:
524
+ raise ValueError(
525
+ f"Select option {o!r} has unsupported field(s) "
526
+ f"{sorted(unknown)}; an option only accepts "
527
+ f"{sorted(_ALLOWED_OPTION_FIELDS)}."
528
+ )
529
+ if o.get("value") in (None, ""):
530
+ raise ValueError(
531
+ f"Select option {o!r} is missing a non-empty 'value'."
532
+ )
533
+ if _option_value_is_object_shaped(o.get("value")):
534
+ raise ValueError(
535
+ f"Select option 'value' must be plain text, got a "
536
+ f"relation/object-shaped value: {o.get('value')!r}."
537
+ )
538
+ if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
539
+ raise ValueError(
540
+ f"Select option 'key' must be a string, got a "
541
+ f"relation/object-shaped value: {o.get('key')!r}."
542
+ )
490
543
  entry = {
491
544
  "key": o.get("key") or str(uuid.uuid4()),
492
545
  "value": o["value"],
@@ -495,6 +548,8 @@ def _normalize_options(
495
548
  "checked": o.get("checked", False),
496
549
  "color": o.get("color", colors[i % len(colors)]),
497
550
  }
551
+ if "score" in o:
552
+ entry["score"] = o["score"]
498
553
  else:
499
554
  entry = {
500
555
  "key": str(uuid.uuid4()),
@@ -0,0 +1,103 @@
1
+ """Member / department / org-role / relation cells must always serialize
2
+ to a JSON *array* of id objects, and must accept object-shaped inputs.
3
+
4
+ A USER_PICKER (26) cell is always an array, even for a single member
5
+ (evidence: a bare ``{"accountId":...}`` object on AddWorksheetRow fails
6
+ server-side). hap-cli's encoder already wraps scalars in an array, but it
7
+ mangled object-shaped inputs: ``[{"accountId": str(i)} for i in value]``
8
+ ran ``str()`` over a whole member dict when the value was a member object
9
+ or — the natural ``get_records`` read-back shape — a list of member
10
+ objects ``[{"accountId","fullname","avatar",...}]``. The id is now
11
+ extracted from such objects and the cell is always emitted as an array.
12
+
13
+ The same fix covers DEPARTMENT (27, ``departmentId``), ORG_ROLE
14
+ (48, ``organizeId``) and RELATE_SHEET/CASCADING (29/35, ``sid``).
15
+ """
16
+
17
+ import json
18
+
19
+ import pytest
20
+
21
+ from hap_cli.core import record as rec_mod
22
+
23
+
24
+ def _val(type_code, value):
25
+ return rec_mod._legacy_cell_value({"type": type_code, "controlName": "f"}, value)
26
+
27
+
28
+ class TestUserPickerAlwaysArray:
29
+ def test_single_account_id_string(self):
30
+ assert _val(26, "a1") == '[{"accountId": "a1"}]'
31
+
32
+ def test_comma_separated_ids(self):
33
+ assert _val(26, "a1,a2") == '[{"accountId": "a1"}, {"accountId": "a2"}]'
34
+
35
+ def test_list_of_ids(self):
36
+ assert _val(26, ["a1", "a2"]) == \
37
+ '[{"accountId": "a1"}, {"accountId": "a2"}]'
38
+
39
+ def test_single_member_object_keeps_only_account_id(self):
40
+ v = {"accountId": "a1", "fullname": "林梦阳", "name": "林梦阳",
41
+ "avatar": "https://x", "avatarMiddle": "https://y"}
42
+ assert _val(26, v) == '[{"accountId": "a1"}]'
43
+
44
+ def test_list_of_member_objects_readback_shape(self):
45
+ v = [{"accountId": "a1", "fullname": "甲", "avatar": "u1"},
46
+ {"accountId": "a2", "fullname": "乙", "avatar": "u2"}]
47
+ assert _val(26, v) == \
48
+ '[{"accountId": "a1"}, {"accountId": "a2"}]'
49
+
50
+ def test_json_array_of_objects_string(self):
51
+ v = json.dumps([{"accountId": "a1", "fullname": "甲"}])
52
+ assert _val(26, v) == '[{"accountId": "a1"}]'
53
+
54
+ def test_json_object_string(self):
55
+ v = json.dumps({"accountId": "a1", "fullname": "甲"})
56
+ assert _val(26, v) == '[{"accountId": "a1"}]'
57
+
58
+ def test_member_object_without_account_id_rejected(self):
59
+ with pytest.raises(ValueError):
60
+ _val(26, {"fullname": "甲", "avatar": "u"})
61
+
62
+ def test_empty_is_empty_array(self):
63
+ assert _val(26, "") == "[]"
64
+ assert _val(26, None) == "[]"
65
+
66
+
67
+ class TestDepartmentObjectShape:
68
+ def test_object_keeps_department_id(self):
69
+ v = {"departmentId": "d1", "departmentName": "研发"}
70
+ assert _val(27, v) == '[{"departmentId": "d1"}]'
71
+
72
+ def test_list_of_objects(self):
73
+ v = [{"departmentId": "d1"}, {"departmentId": "d2"}]
74
+ assert _val(27, v) == \
75
+ '[{"departmentId": "d1"}, {"departmentId": "d2"}]'
76
+
77
+ def test_plain_id_still_works(self):
78
+ assert _val(27, "d1") == '[{"departmentId": "d1"}]'
79
+
80
+
81
+ class TestOrgRoleObjectShape:
82
+ def test_object_keeps_organize_id(self):
83
+ v = {"organizeId": "o1", "organizeName": "主管"}
84
+ assert _val(48, v) == '[{"organizeId": "o1"}]'
85
+
86
+ def test_plain_id_still_works(self):
87
+ assert _val(48, "o1") == '[{"organizeId": "o1"}]'
88
+
89
+
90
+ class TestRelateObjectShape:
91
+ def test_relate_object_keeps_sid(self):
92
+ v = {"sid": "row1", "name": "记录甲"}
93
+ assert _val(29, v) == '[{"sid": "row1"}]'
94
+
95
+ def test_relate_list_of_objects_readback(self):
96
+ v = [{"sid": "row1", "name": "甲"}, {"sid": "row2", "name": "乙"}]
97
+ assert _val(29, v) == '[{"sid": "row1"}, {"sid": "row2"}]'
98
+
99
+ def test_relate_plain_rowid_still_works(self):
100
+ assert _val(29, "row1") == '[{"sid": "row1"}]'
101
+
102
+ def test_cascading_object_keeps_sid(self):
103
+ assert _val(35, {"sid": "row1"}) == '[{"sid": "row1"}]'
@@ -0,0 +1,178 @@
1
+ """A single/multi-select cell must reject relation/object-shaped values.
2
+
3
+ Traced from a live backend error on ``WorksheetService/AddWSRowsBatch``
4
+ (the batch service the main-site ``Worksheet/AddWorksheetRow`` dispatches
5
+ sub-table child rows to). A single-select child field (Type 11) had been
6
+ handed a RELATE_SHEET-shaped value ``[{"sid": "<rowId>"}]`` instead of an
7
+ option key, and the server failed deserialization:
8
+
9
+ Cannot deserialize the current JSON array (e.g. [1,2,3]) into type
10
+ 'MD.Worksheet.Core.Entities.ControlOptions' ...
11
+
12
+ Before this fix, ``_resolve_option_keys`` passed any value it could not
13
+ map to a known key/label straight through to the server (the same soft
14
+ spot as bug #036430). A select cell legitimately carries only an option
15
+ key, a known label, or an ``add_<text>`` new-option directive (evidence:
16
+ pd-openweb src/components/Form/core/utils.js, formatControlToServer
17
+ case 10/11). An object / array-of-objects value is structurally a
18
+ relation/member/sub-table shape and is always wrong here, so it is now
19
+ rejected loudly at the CLI boundary instead of poisoning the server.
20
+ """
21
+
22
+ import json
23
+
24
+ import pytest
25
+
26
+ from hap_cli.core import record as rec_mod
27
+ from hap_cli.core import worksheet_templates as wt
28
+
29
+
30
+ _SELECT_CTRL = {
31
+ "controlId": "c_status",
32
+ "controlName": "状态",
33
+ "type": 11,
34
+ "options": [
35
+ {"key": "k_pass", "value": "已通过"},
36
+ {"key": "k_reject", "value": "已驳回"},
37
+ ],
38
+ }
39
+
40
+ # A sub-table child whose option metadata is NOT carried on the child
41
+ # control (relationControls are often slim) — the structural rejection
42
+ # must still fire without an option set to validate against.
43
+ _SELECT_CTRL_NO_OPTS = {
44
+ "controlId": "cc_status",
45
+ "controlName": "子状态",
46
+ "type": 11,
47
+ }
48
+
49
+ _RELATION_VALUE = [{"sid": "3587620f-f900-4f9c-9803-43827e69ff21"}]
50
+ _RELATION_JSON = json.dumps(_RELATION_VALUE) # '[{"sid": "..."}]'
51
+
52
+
53
+ class TestResolveOptionKeysRejectsRelationShape:
54
+ def test_rejects_list_of_dicts(self):
55
+ with pytest.raises(ValueError, match="状态"):
56
+ rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_VALUE)
57
+
58
+ def test_rejects_relation_json_string(self):
59
+ with pytest.raises(ValueError):
60
+ rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_JSON)
61
+
62
+ def test_rejects_single_dict(self):
63
+ with pytest.raises(ValueError):
64
+ rec_mod._resolve_option_keys(
65
+ _SELECT_CTRL, {"sid": "3587620f-f900-4f9c-9803-43827e69ff21"})
66
+
67
+ def test_rejects_even_without_option_metadata(self):
68
+ # No options to validate against, but the shape is still wrong.
69
+ with pytest.raises(ValueError):
70
+ rec_mod._resolve_option_keys(_SELECT_CTRL_NO_OPTS, _RELATION_VALUE)
71
+
72
+ def test_rejects_json_object_string(self):
73
+ with pytest.raises(ValueError):
74
+ rec_mod._resolve_option_keys(_SELECT_CTRL, '{"sid": "abc"}')
75
+
76
+
77
+ class TestResolveOptionKeysStillAcceptsLegitValues:
78
+ def test_valid_key(self):
79
+ assert rec_mod._resolve_option_keys(_SELECT_CTRL, "k_pass") == ["k_pass"]
80
+
81
+ def test_label_maps_to_key(self):
82
+ assert rec_mod._resolve_option_keys(_SELECT_CTRL, "已通过") == ["k_pass"]
83
+
84
+ def test_add_new_option_directive_passes_through(self):
85
+ # `add_<text>` is how the form encodes "create this option on save".
86
+ assert rec_mod._resolve_option_keys(
87
+ _SELECT_CTRL, "add_新选项") == ["add_新选项"]
88
+
89
+ def test_unknown_plain_text_passes_through(self):
90
+ # Plain scalar text is not an object shape — left to the server
91
+ # (may be a key we cannot see, or a new option). Not our bug.
92
+ assert rec_mod._resolve_option_keys(
93
+ _SELECT_CTRL_NO_OPTS, "甲") == ["甲"]
94
+
95
+ def test_comma_list_of_keys(self):
96
+ assert rec_mod._resolve_option_keys(
97
+ _SELECT_CTRL, "k_pass,k_reject") == ["k_pass", "k_reject"]
98
+
99
+ def test_empty_is_cleared(self):
100
+ assert rec_mod._resolve_option_keys(_SELECT_CTRL, "") == []
101
+ assert rec_mod._resolve_option_keys(_SELECT_CTRL, None) == []
102
+
103
+
104
+ class TestSubTableChildSelectRejectsRelationValue:
105
+ """The reported scenario: a sub-table (34) child single-select gets a
106
+ relation-shaped value while the parent record is created — the child
107
+ row is what the backend batch-saves."""
108
+
109
+ def test_sublist_child_select_relation_value_is_rejected(self):
110
+ sublist_ctrl = {
111
+ "controlId": "c_sub",
112
+ "controlName": "明细",
113
+ "type": 34,
114
+ "relationControls": [_SELECT_CTRL_NO_OPTS],
115
+ }
116
+ rows = [{"子状态": _RELATION_VALUE}]
117
+ with pytest.raises(ValueError):
118
+ rec_mod._legacy_cell_value(sublist_ctrl, rows)
119
+
120
+
121
+ class TestOptionDefinitionRejectsBadShapes:
122
+ """The other injection point: defining a select field's *options*.
123
+
124
+ A select option is only ``{key, value, isDeleted, index, checked,
125
+ color}`` (evidence: pd-openweb widgetConfig/util/setting.js). Option
126
+ ``value`` / ``key`` must be plain text — never a relation/object shape
127
+ — and an option carries no ``type`` field. Before this fix,
128
+ ``_normalize_options`` passed any value/key straight through and
129
+ silently dropped unknown keys like ``type``.
130
+ """
131
+
132
+ def test_option_value_object_shape_rejected(self):
133
+ with pytest.raises(ValueError):
134
+ wt.build_control("DROP_DOWN", "状态",
135
+ options=[{"value": [{"sid": "abc"}]}])
136
+
137
+ def test_option_value_relation_json_string_rejected(self):
138
+ with pytest.raises(ValueError):
139
+ wt.build_control("DROP_DOWN", "状态",
140
+ options=[{"value": _RELATION_JSON}])
141
+
142
+ def test_option_key_object_shape_rejected(self):
143
+ with pytest.raises(ValueError):
144
+ wt.build_control("DROP_DOWN", "状态",
145
+ options=[{"key": [{"sid": "abc"}], "value": "甲"}])
146
+
147
+ def test_option_unknown_type_field_rejected(self):
148
+ with pytest.raises(ValueError, match="type"):
149
+ wt.build_control("DROP_DOWN", "状态",
150
+ options=[{"value": "甲", "type": 0}])
151
+
152
+ def test_option_missing_value_rejected(self):
153
+ with pytest.raises(ValueError):
154
+ wt.build_control("DROP_DOWN", "状态", options=[{"key": "k1"}])
155
+
156
+ # --- legit option definitions still build ---
157
+
158
+ def test_string_options_ok(self):
159
+ out = wt.build_control("DROP_DOWN", "状态", options=["甲", "乙"])
160
+ assert [o["value"] for o in out["options"]] == ["甲", "乙"]
161
+
162
+ def test_dict_option_with_known_fields_ok(self):
163
+ out = wt.build_control(
164
+ "DROP_DOWN", "状态",
165
+ options=[{"value": "甲", "key": "k1", "checked": True}])
166
+ assert out["options"][0]["key"] == "k1"
167
+ assert out["options"][0]["value"] == "甲"
168
+ assert out["options"][0]["checked"] is True
169
+
170
+ def test_scored_option_is_allowed_and_preserved(self):
171
+ # `score` is a legitimate NPS/scoring field — it must not be
172
+ # rejected (it is not a relation shape) and must survive to the
173
+ # wire instead of being silently dropped.
174
+ out = wt.build_control(
175
+ "DROP_DOWN", "满意度",
176
+ options=[{"value": "满意", "score": 5}])
177
+ assert out["options"][0]["value"] == "满意"
178
+ assert out["options"][0]["score"] == 5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.17
3
+ Version: 0.8.19
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0