hap-cli 0.8.28__tar.gz → 0.8.30__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 (295) hide show
  1. {hap_cli-0.8.28 → hap_cli-0.8.30}/PKG-INFO +1 -1
  2. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/worksheet_cmd.py +122 -3
  4. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/executor.py +6 -12
  5. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/upload.py +25 -1
  6. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/worksheet.py +229 -6
  7. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/messages.json +24 -4
  8. hap_cli-0.8.30/hap_cli/tests/test_upload_allow_down.py +118 -0
  9. hap_cli-0.8.30/hap_cli/tests/test_worksheet_switch.py +174 -0
  10. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/PKG-INFO +1 -1
  11. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/SOURCES.txt +2 -0
  12. {hap_cli-0.8.28 → hap_cli-0.8.30}/MANIFEST.in +0 -0
  13. {hap_cli-0.8.28 → hap_cli-0.8.30}/README.md +0 -0
  14. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/__init__.py +0 -0
  15. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/__main__.py +0 -0
  16. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/config.py +0 -0
  17. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/harness.py +0 -0
  18. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/smoke.py +0 -0
  19. {hap_cli-0.8.28 → hap_cli-0.8.30}/app_live_tests/state.py +0 -0
  20. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/README.md +0 -0
  21. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/README_CN.md +0 -0
  22. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/__main__.py +0 -0
  23. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/__init__.py +0 -0
  24. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_cmd.py +0 -0
  25. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_creator_cmd.py +0 -0
  26. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/app_editor_cmd.py +0 -0
  27. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/attachment_cmd.py +0 -0
  28. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/auth_cmd.py +0 -0
  29. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/calendar_cmd.py +0 -0
  30. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chart_cmd.py +0 -0
  31. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chat_cmd.py +0 -0
  32. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/chatbot_cmd.py +0 -0
  33. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/config_cmd.py +0 -0
  34. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/contact_cmd.py +0 -0
  35. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/department_cmd.py +0 -0
  36. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/group_cmd.py +0 -0
  37. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/guide_cmd.py +0 -0
  38. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/icon_cmd.py +0 -0
  39. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/instance_cmd.py +0 -0
  40. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/node_cmd.py +0 -0
  41. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/optionset_cmd.py +0 -0
  42. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/page_cmd.py +0 -0
  43. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/post_cmd.py +0 -0
  44. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/record_cmd.py +0 -0
  45. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/region_cmd.py +0 -0
  46. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/role_cmd.py +0 -0
  47. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/update_cmd.py +0 -0
  48. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/upload_cmd.py +0 -0
  49. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/v3_registry.py +0 -0
  50. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/commands/workflow_cmd.py +0 -0
  51. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/context.py +0 -0
  52. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/__init__.py +0 -0
  53. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/action_spec_adapter.py +0 -0
  54. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app.py +0 -0
  55. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/__init__.py +0 -0
  56. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  57. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  58. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
  59. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/charts.py +0 -0
  60. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/cleanup.py +0 -0
  61. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/compiler.py +0 -0
  62. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/config.py +0 -0
  63. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  64. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/errors.py +0 -0
  65. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  66. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/executor.py +0 -0
  67. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/fields.py +0 -0
  68. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/hap.py +0 -0
  69. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/merge.py +0 -0
  70. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  71. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/console.py +0 -0
  72. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  73. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  74. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/recording/report.py +0 -0
  75. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/schema.py +0 -0
  76. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  77. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  78. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/cli.py +0 -0
  79. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  80. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  81. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  82. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  83. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/seed/template.py +0 -0
  84. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/selftest.py +0 -0
  85. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/smoke.py +0 -0
  86. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/steps.py +0 -0
  87. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/store.py +0 -0
  88. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/validate.py +0 -0
  89. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  90. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/attachment.py +0 -0
  91. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/auth.py +0 -0
  92. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/calendar_mod.py +0 -0
  93. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chart.py +0 -0
  94. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chat.py +0 -0
  95. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/chatbot.py +0 -0
  96. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/cli_access.py +0 -0
  97. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/contact.py +0 -0
  98. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/control_type_codes.py +0 -0
  99. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/department.py +0 -0
  100. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/__init__.py +0 -0
  101. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  102. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  103. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/apply.py +0 -0
  104. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/componentlower.py +0 -0
  105. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/component.schema.json +0 -0
  106. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
  107. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
  108. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec/field.schema.json +0 -0
  109. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/editspec.py +0 -0
  110. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/errors.py +0 -0
  111. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/fieldlower.py +0 -0
  112. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  113. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/models.py +0 -0
  114. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/ops.py +0 -0
  115. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/planner.py +0 -0
  116. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/editor/reader.py +0 -0
  117. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/field_normalizer.py +0 -0
  118. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/field_spec_adapter.py +0 -0
  119. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/filter_translator.py +0 -0
  120. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/flow_node.py +0 -0
  121. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/global_meta.py +0 -0
  122. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/group.py +0 -0
  123. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/guide.py +0 -0
  124. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/icon_index.py +0 -0
  125. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/instance.py +0 -0
  126. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/logger.py +0 -0
  127. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/optionset.py +0 -0
  128. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/page.py +0 -0
  129. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/post.py +0 -0
  130. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/record.py +0 -0
  131. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/response_crypto.py +0 -0
  132. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/role.py +0 -0
  133. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/role_perm_builder.py +0 -0
  134. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/session.py +0 -0
  135. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/token_crypto.py +0 -0
  136. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/update.py +0 -0
  137. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/__init__.py +0 -0
  138. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/dispatcher.py +0 -0
  139. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/render.py +0 -0
  140. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/v3/schema.py +0 -0
  141. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/view_spec_adapter.py +0 -0
  142. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/workflow.py +0 -0
  143. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/workflow_node_dsl.py +0 -0
  144. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/core/worksheet_templates.py +0 -0
  145. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/approval.md +0 -0
  146. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/chart.md +0 -0
  147. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/messages.md +0 -0
  148. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/overview.md +0 -0
  149. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/post.md +0 -0
  150. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/record.md +0 -0
  151. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/setup.md +0 -0
  152. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/en/skills.md +0 -0
  153. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/index.json +0 -0
  154. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/approval.md +0 -0
  155. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/chart.md +0 -0
  156. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/messages.md +0 -0
  157. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/overview.md +0 -0
  158. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/post.md +0 -0
  159. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/record.md +0 -0
  160. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/setup.md +0 -0
  161. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/guide/zh-Hans/skills.md +0 -0
  162. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/hap_cli.py +0 -0
  163. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/i18n.py +0 -0
  164. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/__init__.py +0 -0
  165. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/locale/messages.schema.json +0 -0
  166. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/__init__.py +0 -0
  167. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/conftest.py +0 -0
  168. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_creator.py +0 -0
  169. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_editor.py +0 -0
  170. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_app_export_usage.py +0 -0
  171. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_approval_submit_fill_cli.py +0 -0
  172. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_attachment_share.py +0 -0
  173. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_auth_prerelease.py +0 -0
  174. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_bug_036430.py +0 -0
  175. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_bug_036750.py +0 -0
  176. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chart.py +0 -0
  177. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chat_attachment_send.py +0 -0
  178. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_chat_recall_cli.py +0 -0
  179. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_cli_access.py +0 -0
  180. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_config_cmd.py +0 -0
  181. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_content_language.py +0 -0
  182. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_control_lookup_guard.py +0 -0
  183. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_core.py +0 -0
  184. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  185. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_full_e2e.py +0 -0
  186. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_global_meta.py +0 -0
  187. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_guide.py +0 -0
  188. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_i18n.py +0 -0
  189. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_instance_history_cli.py +0 -0
  190. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration.py +0 -0
  191. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_approval.py +0 -0
  192. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  193. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_calendar.py +0 -0
  194. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_destructive.py +0 -0
  195. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_misc.py +0 -0
  196. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_post.py +0 -0
  197. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_social.py +0 -0
  198. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_v3.py +0 -0
  199. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_workflow.py +0 -0
  200. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  201. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
  202. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_multi_profile.py +0 -0
  203. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
  204. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  205. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_id_cli.py +0 -0
  206. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_org_id_docs.py +0 -0
  207. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_parameter_conventions.py +0 -0
  208. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  209. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_post_scope_cli.py +0 -0
  210. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  211. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_field_alias.py +0 -0
  212. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  213. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_record_write_fixes.py +0 -0
  214. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_select_add_option.py +0 -0
  215. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
  216. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  217. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_update.py +0 -0
  218. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_update_mode.py +0 -0
  219. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_upload_host_resolution.py +0 -0
  220. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  221. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  222. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  223. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_session.py +0 -0
  224. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  225. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
  226. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  227. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
  228. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  229. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_create_section.py +0 -0
  230. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  231. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  232. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/__init__.py +0 -0
  233. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/formatting.py +0 -0
  234. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/options.py +0 -0
  235. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli/utils/parameter_mapping.py +0 -0
  236. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/dependency_links.txt +0 -0
  237. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/entry_points.txt +0 -0
  238. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/requires.txt +0 -0
  239. {hap_cli-0.8.28 → hap_cli-0.8.30}/hap_cli.egg-info/top_level.txt +0 -0
  240. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/__init__.py +0 -0
  241. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/__main__.py +0 -0
  242. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/_wftest.py +0 -0
  243. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/charts.py +0 -0
  244. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/cleanup.py +0 -0
  245. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/compiler.py +0 -0
  246. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/config.py +0 -0
  247. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/errors.py +0 -0
  248. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/executor.py +0 -0
  249. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/fields.py +0 -0
  250. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/hap.py +0 -0
  251. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/__init__.py +0 -0
  252. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/console.py +0 -0
  253. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/jsonl.py +0 -0
  254. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/mirror.py +0 -0
  255. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/recording/report.py +0 -0
  256. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/schema.py +0 -0
  257. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/__init__.py +0 -0
  258. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/cli.py +0 -0
  259. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/executor.py +0 -0
  260. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/seed/template.py +0 -0
  261. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/selftest.py +0 -0
  262. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/smoke.py +0 -0
  263. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/step.py +0 -0
  264. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/steps.py +0 -0
  265. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/store.py +0 -0
  266. {hap_cli-0.8.28 → hap_cli-0.8.30}/live_tests/workflow_dsl.py +0 -0
  267. {hap_cli-0.8.28 → hap_cli-0.8.30}/setup.cfg +0 -0
  268. {hap_cli-0.8.28 → hap_cli-0.8.30}/setup.py +0 -0
  269. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/INDEX.json +0 -0
  270. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  271. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  272. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/create_optionset.yaml +0 -0
  273. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/create_role.yaml +0 -0
  274. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  275. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_record.yaml +0 -0
  276. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_role.yaml +0 -0
  277. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  278. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_info.yaml +0 -0
  279. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  280. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  281. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  282. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_details.yaml +0 -0
  283. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  284. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_list.yaml +0 -0
  285. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  286. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  287. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  288. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  289. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_regions.yaml +0 -0
  290. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_role_details.yaml +0 -0
  291. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/get_role_list.yaml +0 -0
  292. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  293. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  294. {hap_cli-0.8.28 → hap_cli-0.8.30}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  295. {hap_cli-0.8.28 → hap_cli-0.8.30}/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.28
3
+ Version: 0.8.30
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.28"
3
+ __version__ = "0.8.30"
@@ -919,24 +919,143 @@ def worksheet_delete_button(ctx, worksheet_id, btn_id, app_id, view_id, yes):
919
919
 
920
920
  @worksheet.command("switches")
921
921
  @click.argument("worksheet_id")
922
+ @click.option("--raw", is_flag=True,
923
+ help="Return the server's switch list unannotated.")
922
924
  @pass_context
923
- def worksheet_switches(ctx, worksheet_id):
924
- """Get feature switches for a worksheet."""
925
+ def worksheet_switches(ctx, worksheet_id, raw):
926
+ """Get feature switches for a worksheet.
927
+
928
+ Each switch is listed with the name and group it has on the feature
929
+ switch page, because several numbers share a name and only the group
930
+ tells them apart.
931
+ """
925
932
  try:
926
933
  session = ctx.get_session()
927
934
  data = ws_mod.get_switches(session, worksheet_id)
935
+ if not raw:
936
+ data = ws_mod.annotate_switches(data)
928
937
  ctx.output(data, lambda d: output_json(d))
929
938
  except Exception as e:
930
939
  ctx.handle_error(e)
931
940
 
932
941
 
942
+ @worksheet.command("switch-types")
943
+ @pass_context
944
+ def worksheet_switch_types(ctx):
945
+ """Show the feature switch numbers with their names and groups."""
946
+ rows = [ws_mod.describe_switch(t) for t in sorted(ws_mod.SWITCH_TYPES)]
947
+ ctx.output(
948
+ rows,
949
+ lambda d: output_table(
950
+ d, ["type", "group", "name", "path"],
951
+ ["Type", "Group", "Name", "Path"],
952
+ ),
953
+ )
954
+
955
+
956
+ def _parse_switch_changes(pairs):
957
+ changes = {}
958
+ for raw in pairs:
959
+ if "=" not in raw:
960
+ raise click.UsageError(
961
+ f"Invalid --set value {raw!r}. Use --set TYPE=true|false.")
962
+ key, value = raw.split("=", 1)
963
+ try:
964
+ stype = int(key.strip())
965
+ except ValueError:
966
+ raise click.UsageError(
967
+ f"Invalid switch number {key!r}. Run `hap worksheet "
968
+ "switch-types` for the list.")
969
+ val = value.strip().lower()
970
+ if val not in ("true", "false", "1", "0", "on", "off"):
971
+ raise click.UsageError(
972
+ f"Invalid state {value!r} for switch {stype}. Use true or false.")
973
+ changes[stype] = val in ("true", "1", "on")
974
+ return changes
975
+
976
+
977
+ @worksheet.command("batch-edit-switch")
978
+ @click.argument("worksheet_ids", nargs=-1, required=True)
979
+ @click.option("--set", "pairs", multiple=True, required=True,
980
+ help="Switch to change as TYPE=true|false (repeatable). "
981
+ "Numbers come from `hap worksheet switch-types`.")
982
+ @click.option("--dry-run", is_flag=True,
983
+ help="Show what would change on each worksheet, without saving.")
984
+ @pass_context
985
+ def worksheet_batch_edit_switch(ctx, worksheet_ids, pairs, dry_run):
986
+ """Change several feature switches on one or more worksheets.
987
+
988
+ The switches you do not name keep their current values. Turning the
989
+ 批量操作 or 统计 parent on or off carries its children with it. Each
990
+ worksheet is read back after saving, and any switch that did not take
991
+ is reported rather than counted as success.
992
+ """
993
+ try:
994
+ changes = _parse_switch_changes(pairs)
995
+ session = ctx.get_session()
996
+ if dry_run:
997
+ preview = [
998
+ {**ws_mod.describe_switch(t), "state": v}
999
+ for t, v in changes.items()
1000
+ ]
1001
+ ctx.output(
1002
+ {"worksheetIds": list(worksheet_ids), "changes": preview,
1003
+ "saved": False},
1004
+ lambda d: click.echo(
1005
+ f"Would set {len(preview)} switch(es) on "
1006
+ f"{len(worksheet_ids)} worksheet(s): "
1007
+ + ", ".join(f"{c['path'] or c['type']}={c['state']}"
1008
+ for c in preview)),
1009
+ )
1010
+ return
1011
+ results, failed_sheets = [], []
1012
+ for wid in worksheet_ids:
1013
+ try:
1014
+ res = ws_mod.batch_edit_switch(session, wid, changes)
1015
+ except Exception as exc: # keep going across worksheets
1016
+ results.append({"worksheetId": wid, "error": str(exc)})
1017
+ failed_sheets.append(wid)
1018
+ continue
1019
+ results.append(res)
1020
+ if res.get("failed"):
1021
+ failed_sheets.append(wid)
1022
+ payload = {"results": results,
1023
+ "worksheetsChanged": len(worksheet_ids) - len(failed_sheets),
1024
+ "worksheetsFailed": failed_sheets}
1025
+
1026
+ def _render(d):
1027
+ click.echo(f"Changed {d['worksheetsChanged']} of "
1028
+ f"{len(worksheet_ids)} worksheet(s).")
1029
+ for r in d["results"]:
1030
+ if r.get("error"):
1031
+ click.echo(f" {r['worksheetId']}: {r['error']}")
1032
+ elif r.get("failed"):
1033
+ for t, info in r["failed"].items():
1034
+ click.echo(f" {r['worksheetId']}: switch {t} is "
1035
+ f"{info['actual']!r}, wanted "
1036
+ f"{info['expected']!r}")
1037
+ ctx.output(payload, _render)
1038
+ if failed_sheets:
1039
+ raise click.ClickException(
1040
+ f"{len(failed_sheets)} worksheet(s) did not end up in the "
1041
+ "requested state: " + ", ".join(failed_sheets))
1042
+ except Exception as e:
1043
+ ctx.handle_error(e)
1044
+
1045
+
933
1046
  @worksheet.command("edit-switch")
934
1047
  @click.argument("worksheet_id")
935
1048
  @click.argument("switch_id", type=int)
936
1049
  @click.argument("value", type=bool)
937
1050
  @pass_context
938
1051
  def worksheet_edit_switch(ctx, worksheet_id, switch_id, value):
939
- """Edit a single feature switch."""
1052
+ """Turn one feature switch on or off.
1053
+
1054
+ SWITCH_ID is a switch number from `hap worksheet switch-types`. The
1055
+ new state is read back, so a change that did not take is reported as
1056
+ an error instead of success. To change several switches, or the same
1057
+ ones across many worksheets, use `worksheet batch-edit-switch`.
1058
+ """
940
1059
  try:
941
1060
  session = ctx.get_session()
942
1061
  data = ws_mod.edit_switch(session, worksheet_id, switch_id, value)
@@ -141,18 +141,12 @@ class _Resolver:
141
141
 
142
142
  def _record_attachment_cell(descriptors: list[dict[str, Any]]) -> dict[str, Any]:
143
143
  """Wrap general uploaded-file descriptors into a worksheet Attachment
144
- (type 14) cell value (mirrors core upload.record_attachment_cell)."""
145
- attachments = []
146
- for i, d in enumerate(descriptors):
147
- attachments.append({
148
- "fileID": d.get("fileID"), "fileSize": d.get("fileSize"),
149
- "serverName": d.get("serverName"), "filePath": d.get("filePath"),
150
- "fileName": d.get("fileName"), "fileExt": d.get("fileExt"),
151
- "originalFileName": d.get("originalFileName"), "key": d.get("key"),
152
- "url": d.get("url"), "oldOriginalFileName": d.get("originalFileName"),
153
- "index": i, "isEdit": False,
154
- })
155
- return {"attachmentData": [], "attachments": attachments, "knowledgeAtts": []}
144
+ (type 14) cell value. Delegates to the core wrapper so the seeded rows
145
+ carry exactly what any other CLI upload does — the local copy this
146
+ replaced silently dropped each file's download permission."""
147
+ from hap_cli.core.upload import record_attachment_cell
148
+
149
+ return record_attachment_cell(descriptors)
156
150
 
157
151
 
158
152
  def _upload_row_attachments(
@@ -209,6 +209,23 @@ def _iso(deadline: Optional[int]) -> Optional[str]:
209
209
  return datetime.fromtimestamp(deadline, tz=timezone.utc).astimezone().isoformat()
210
210
 
211
211
 
212
+ def _download_permission(ext: str) -> dict[str, Any]:
213
+ """The per-file download permission the holder of the file is sent.
214
+
215
+ Whoever stores an attachment decides, per file, whether anyone but
216
+ the uploader may download it — an omitted flag used to read as "no",
217
+ so a file attached by the CLI answered 权限不足 for every colleague.
218
+ pd-openweb sets it in ``formatResponseData``
219
+ (src/components/UploadFiles/utils.js:202-206): non-pictures get
220
+ ``allowDown: true`` plus an empty ``docVersionID``, pictures get
221
+ neither because the viewer already treats a picture as downloadable
222
+ (UploadFiles/File.js:724). Mirrored verbatim, empty dict included.
223
+ """
224
+ if ext.lower() in _PICTURE_EXTS:
225
+ return {}
226
+ return {"allowDown": True, "docVersionID": ""}
227
+
228
+
212
229
  def upload_bytes(
213
230
  session: Session,
214
231
  *,
@@ -239,7 +256,7 @@ def upload_bytes(
239
256
  up = _post_to_store(upload_host(session), token, content,
240
257
  original_name=original, ext=ext,
241
258
  server_name=server_name)
242
- return {
259
+ descriptor = {
243
260
  "fileID": generate_file_oid(),
244
261
  "fileSize": len(content),
245
262
  "etag": up.get("etag"),
@@ -256,6 +273,8 @@ def upload_bytes(
256
273
  # (public-bucket) address, which does not expire.
257
274
  "urlExpiresAt": _iso(url_expires_at(token.get("url"))),
258
275
  }
276
+ descriptor.update(_download_permission(ext))
277
+ return descriptor
259
278
 
260
279
 
261
280
  def source_descriptors(sources: "list[str] | tuple[str, ...]") -> list[dict[str, str]]:
@@ -324,4 +343,9 @@ def record_attachment_cell(descriptors: list[dict[str, Any]]) -> dict[str, Any]:
324
343
  "index": i,
325
344
  "isEdit": False,
326
345
  })
346
+ # Carried, not recomputed: the descriptor already decided this at
347
+ # upload time — see _download_permission().
348
+ for key in ("allowDown", "docVersionID"):
349
+ if key in d:
350
+ attachments[-1][key] = d[key]
327
351
  return {"attachmentData": [], "attachments": attachments, "knowledgeAtts": []}
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
  from typing import Any, Optional
6
6
 
7
- from hap_cli.core.session import Session
7
+ from hap_cli.core.session import Session, APIError
8
8
  from hap_cli.core import worksheet_templates as wt
9
9
 
10
10
  logger = logging.getLogger("hap_cli.worksheet")
@@ -1681,14 +1681,237 @@ def get_switches(
1681
1681
  )
1682
1682
 
1683
1683
 
1684
+ # ── Feature switch catalog ───────────────────────────────────────────────
1685
+ #
1686
+ # GetSwitch answers bare numbers, which say nothing about what they
1687
+ # control, and several numbers share a label: 26 and 36 are both "复制",
1688
+ # 28 and 39 are both "删除", 23 and 32 are both "系统默认打印". Only the
1689
+ # group tells them apart, so the group travels with the name.
1690
+ #
1691
+ # Names, grouping and parent/child structure come from pd-openweb
1692
+ # ``src/pages/FormSet/containers/FunctionalSwitch/config.js``
1693
+ # (``listConfigStr``, ``worksheetSwitch`` / ``viewSwitch`` /
1694
+ # ``recordSwitch`` / ``approveSwitch``, ``batch``, ``statistics``).
1695
+
1696
+ SWITCH_PARENT_BATCH = 25 # 视图 > 批量操作
1697
+ SWITCH_PARENT_STATISTICS = 500 # 工作表 > 统计
1698
+
1699
+ _SWITCH_BATCH_CHILDREN = (24, 26, 23, 27, 28, 29)
1700
+ _SWITCH_STATISTICS_CHILDREN = (50, 51)
1701
+
1702
+ SWITCH_TYPES: dict[int, dict[str, Any]] = {
1703
+ # 工作表
1704
+ 10: {"name": "显示创建按钮", "group": "工作表"},
1705
+ 11: {"name": "讨论", "group": "工作表"},
1706
+ 12: {"name": "日志", "group": "工作表"},
1707
+ 13: {"name": "导入", "group": "工作表"},
1708
+ 14: {"name": "回收站", "group": "工作表"},
1709
+ 500: {"name": "统计", "group": "工作表"},
1710
+ 50: {"name": "公共", "group": "工作表", "parent": SWITCH_PARENT_STATISTICS},
1711
+ 51: {"name": "个人", "group": "工作表", "parent": SWITCH_PARENT_STATISTICS},
1712
+ # 视图
1713
+ 20: {"name": "公开分享", "group": "视图"},
1714
+ 38: {"name": "嵌入链接", "group": "视图"},
1715
+ 21: {"name": "导出", "group": "视图"},
1716
+ 22: {"name": "快捷编辑", "group": "视图"},
1717
+ 25: {"name": "批量操作", "group": "视图"},
1718
+ 24: {"name": "编辑", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1719
+ 26: {"name": "复制", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1720
+ 23: {"name": "系统默认打印", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1721
+ 27: {"name": "导出", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1722
+ 28: {"name": "删除", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1723
+ 29: {"name": "执行自定义动作", "group": "视图", "parent": SWITCH_PARENT_BATCH},
1724
+ # 记录
1725
+ 30: {"name": "公开分享", "group": "记录"},
1726
+ 52: {"name": "内部访问链接", "group": "记录"},
1727
+ 32: {"name": "系统默认打印", "group": "记录"},
1728
+ 33: {"name": "附件下载", "group": "记录"},
1729
+ 34: {"name": "日志", "group": "记录"},
1730
+ 35: {"name": "讨论", "group": "记录"},
1731
+ 36: {"name": "复制", "group": "记录"},
1732
+ 39: {"name": "删除", "group": "记录"},
1733
+ 37: {"name": "重新创建", "group": "记录"},
1734
+ # 审批
1735
+ 40: {"name": "系统字段", "group": "审批"},
1736
+ 41: {"name": "审批流转详情", "group": "审批"},
1737
+ }
1738
+
1739
+
1740
+ def describe_switch(switch_type: int) -> dict[str, Any]:
1741
+ """Name, group and parent of a switch number; empty-ish when unknown."""
1742
+ meta = SWITCH_TYPES.get(switch_type)
1743
+ if not meta:
1744
+ return {"type": switch_type, "name": "", "group": "", "path": ""}
1745
+ parent = meta.get("parent")
1746
+ path = " > ".join(
1747
+ [meta["group"]]
1748
+ + ([SWITCH_TYPES[parent]["name"]] if parent in SWITCH_TYPES else [])
1749
+ + [meta["name"]]
1750
+ )
1751
+ out = {"type": switch_type, "name": meta["name"], "group": meta["group"],
1752
+ "path": path}
1753
+ if parent:
1754
+ out["parent"] = parent
1755
+ return out
1756
+
1757
+
1758
+ def annotate_switches(switches: Any) -> list[dict[str, Any]]:
1759
+ """Return the switch list with name/group/path added to each entry."""
1760
+ out = []
1761
+ for entry in switches or []:
1762
+ if not isinstance(entry, dict):
1763
+ continue
1764
+ merged = dict(entry)
1765
+ merged.update(describe_switch(entry.get("type")))
1766
+ out.append(merged)
1767
+ return out
1768
+
1769
+
1770
+ def _switch_wire_fields(entry: dict[str, Any]) -> dict[str, Any]:
1771
+ """The subset of a switch the page sends back on save.
1772
+
1773
+ ``FunctionalSwitch/index.jsx`` picks ``roleType/type/state/viewIds``;
1774
+ ``displayFlowChart`` rides along where the switch carries one.
1775
+ """
1776
+ out = {k: entry[k] for k in ("roleType", "type", "state", "viewIds")
1777
+ if k in entry}
1778
+ if "displayFlowChart" in entry:
1779
+ out["displayFlowChart"] = entry["displayFlowChart"]
1780
+ out.setdefault("type", entry.get("type"))
1781
+ out.setdefault("roleType", 0)
1782
+ return out
1783
+
1784
+
1785
+ def _apply_switch_linkage(
1786
+ current: list[dict[str, Any]], changes: dict[int, bool],
1787
+ ) -> dict[int, bool]:
1788
+ """Expand ``changes`` with the parent/child rules the page applies.
1789
+
1790
+ * Turning the 批量操作 (25) or 统计 (500) parent on or off carries all
1791
+ of its children with it.
1792
+ * When every 统计 child ends up off, the parent goes off too. The
1793
+ 批量操作 children deliberately do **not** close their parent — the
1794
+ page only does this for 统计 (``index.jsx:108-123``; its comment
1795
+ says 批量操作 but the code targets the statistics parent).
1796
+ """
1797
+ resolved = dict(changes)
1798
+ for parent, children in (
1799
+ (SWITCH_PARENT_BATCH, _SWITCH_BATCH_CHILDREN),
1800
+ (SWITCH_PARENT_STATISTICS, _SWITCH_STATISTICS_CHILDREN),
1801
+ ):
1802
+ if parent in resolved:
1803
+ for child in children:
1804
+ resolved.setdefault(child, resolved[parent])
1805
+ states = {e.get("type"): e.get("state") for e in current
1806
+ if isinstance(e, dict)}
1807
+ states.update(resolved)
1808
+ if any(c in resolved for c in _SWITCH_STATISTICS_CHILDREN):
1809
+ if not any(states.get(c) for c in _SWITCH_STATISTICS_CHILDREN):
1810
+ resolved.setdefault(SWITCH_PARENT_STATISTICS, False)
1811
+ return resolved
1812
+
1813
+
1814
+ def batch_edit_switch(
1815
+ session: Session,
1816
+ worksheet_id: str,
1817
+ changes: dict[int, bool],
1818
+ *,
1819
+ verify: bool = True,
1820
+ ) -> dict[str, Any]:
1821
+ """Apply several switch changes to one worksheet in a single call.
1822
+
1823
+ This is what the feature-switch page itself uses
1824
+ (``Worksheet/BatchEditSwitch``, ``src/api/worksheet.js:2346-2356``):
1825
+ it submits the **whole** ``switchList``, so the untouched switches
1826
+ have to be read first and sent back unchanged or they are lost.
1827
+
1828
+ Parent/child linkage is applied for you (see
1829
+ :func:`_apply_switch_linkage`). With ``verify`` the new state is read
1830
+ back and any switch that did not take is reported, because the
1831
+ endpoint answers ``true`` regardless.
1832
+
1833
+ Returns ``{"worksheetId", "applied": {type: bool}, "failed": {type:
1834
+ {"expected", "actual"}}}``.
1835
+ """
1836
+ current = get_switches(session, worksheet_id) or []
1837
+ if not isinstance(current, list):
1838
+ raise APIError(
1839
+ f"could not read the switches of worksheet {worksheet_id}")
1840
+ resolved = _apply_switch_linkage(current, {int(k): bool(v)
1841
+ for k, v in changes.items()})
1842
+ switch_list = []
1843
+ for entry in current:
1844
+ if not isinstance(entry, dict):
1845
+ continue
1846
+ wire = _switch_wire_fields(entry)
1847
+ if entry.get("type") in resolved:
1848
+ wire["state"] = resolved[entry["type"]]
1849
+ switch_list.append(wire)
1850
+ known = {e.get("type") for e in current if isinstance(e, dict)}
1851
+ for stype, state in resolved.items():
1852
+ if stype not in known:
1853
+ switch_list.append({"type": stype, "state": state, "roleType": 0})
1854
+ session.api_call(
1855
+ "Worksheet", "BatchEditSwitch",
1856
+ {"worksheetId": worksheet_id, "switchList": switch_list},
1857
+ )
1858
+ result: dict[str, Any] = {"worksheetId": worksheet_id,
1859
+ "applied": dict(resolved), "failed": {}}
1860
+ if verify:
1861
+ after = {e.get("type"): e.get("state")
1862
+ for e in (get_switches(session, worksheet_id) or [])
1863
+ if isinstance(e, dict)}
1864
+ for stype, want in resolved.items():
1865
+ if bool(after.get(stype)) != bool(want):
1866
+ result["failed"][stype] = {"expected": want,
1867
+ "actual": after.get(stype)}
1868
+ return result
1869
+
1870
+
1684
1871
  def edit_switch(
1685
1872
  session: Session, worksheet_id: str, switch_id: int, value: bool,
1686
1873
  ) -> dict[str, Any]:
1687
- """Edit a single feature switch."""
1688
- return session.api_call(
1689
- "Worksheet", "EditSwitch",
1690
- {"worksheetId": worksheet_id, "state": switch_id, "value": value},
1691
- )
1874
+ """Turn one feature switch on or off, and confirm it stuck.
1875
+
1876
+ ``EditSwitch`` names the boolean ``state`` and the switch number
1877
+ ``type`` (pd-openweb ``src/api/worksheet.js:2330-2343``). This wrapper
1878
+ used to send ``{state: <switch number>, value: <bool>}`` — the two
1879
+ swapped, plus a ``value`` field the API does not define. The server
1880
+ accepts that quietly and answers ``{"data": true}`` without changing
1881
+ anything, so every edit looked like it worked and none of them did.
1882
+
1883
+ The switch's remaining fields are sent back as they are, the way the
1884
+ page does (``_.pick(o, ['roleType', 'type', 'state', 'viewIds'])`` in
1885
+ ``FormSet/containers/FunctionalSwitch/index.jsx``); dropping
1886
+ ``viewIds`` would reset a view-scoped switch.
1887
+
1888
+ Because a stale payload fails silently rather than loudly, the new
1889
+ state is read back and a write that did not land raises instead of
1890
+ reporting success.
1891
+ """
1892
+ current = {}
1893
+ for entry in get_switches(session, worksheet_id) or []:
1894
+ if isinstance(entry, dict) and entry.get("type") == switch_id:
1895
+ current = entry
1896
+ break
1897
+ payload: dict[str, Any] = {"worksheetId": worksheet_id,
1898
+ "type": switch_id, "state": value}
1899
+ for field in ("roleType", "viewIds", "displayFlowChart"):
1900
+ if field in current:
1901
+ payload[field] = current[field]
1902
+ payload.setdefault("roleType", 0)
1903
+ result = session.api_call("Worksheet", "EditSwitch", payload)
1904
+ applied = None
1905
+ for entry in get_switches(session, worksheet_id) or []:
1906
+ if isinstance(entry, dict) and entry.get("type") == switch_id:
1907
+ applied = entry.get("state")
1908
+ break
1909
+ if bool(applied) != bool(value):
1910
+ raise APIError(
1911
+ f"switch {switch_id} is still {applied!r} after asking for "
1912
+ f"{value!r} — the server reported success but did not apply it"
1913
+ )
1914
+ return result
1692
1915
 
1693
1916
 
1694
1917
  # ── Logs ─────────────────────────────────────────────────────────────────
@@ -3969,6 +3969,18 @@
3969
3969
  "en": "Raw JSON array of new controls to append. Each item matches the AddWorksheetControls `controls` schema (same shape as `worksheet fields --raw` output).",
3970
3970
  "zh-Hans": "要追加的新字段原始 JSON 数组。每一项遵循 AddWorksheetControls 的 `controls` 结构(与 `worksheet fields --raw` 输出形态相同)"
3971
3971
  },
3972
+ "worksheet batch-edit-switch": {
3973
+ "en": "Change several feature switches on one or more worksheets.\n\nThe switches you do not name keep their current values. Turning the\n批量操作 or 统计 parent on or off carries its children with it. Each\nworksheet is read back after saving, and any switch that did not take\nis reported rather than counted as success.",
3974
+ "zh-Hans": "批量修改一张或多张工作表的功能开关。\n\n未指定的开关保持原值。开启或关闭「批量操作」「统计」父开关时,其子项会随之联动。每张表保存后都会回读校验,未生效的开关会被报出而不是计为成功。"
3975
+ },
3976
+ "worksheet batch-edit-switch --dry-run": {
3977
+ "en": "Show what would change on each worksheet, without saving.",
3978
+ "zh-Hans": "仅显示每张工作表将要修改的内容,不实际保存。"
3979
+ },
3980
+ "worksheet batch-edit-switch --set": {
3981
+ "en": "Switch to change as TYPE=true|false (repeatable). Numbers come from `hap worksheet switch-types`.",
3982
+ "zh-Hans": "要修改的开关,格式为 TYPE=true|false(可重复指定)。开关号见 `hap worksheet switch-types`。"
3983
+ },
3972
3984
  "worksheet chart": {
3973
3985
  "en": "Worksheet chart (统计图) management.",
3974
3986
  "zh-Hans": "工作表统计图管理"
@@ -4198,8 +4210,8 @@
4198
4210
  "zh-Hans": "跳过确认"
4199
4211
  },
4200
4212
  "worksheet edit-switch": {
4201
- "en": "Edit a single feature switch.",
4202
- "zh-Hans": "编辑单个功能开关"
4213
+ "en": "Turn one feature switch on or off.\n\nSWITCH_ID is a switch number from `hap worksheet switch-types`. The\nnew state is read back, so a change that did not take is reported as\nan error instead of success. To change several switches, or the same\nones across many worksheets, use `worksheet batch-edit-switch`.",
4214
+ "zh-Hans": "开启或关闭单个功能开关。\n\nSWITCH_ID 为开关号,可用 `hap worksheet switch-types` 查看。修改后会回读校验,未生效将报错而不是报成功。要一次改多个开关、或对多张工作表改同一批开关,请用 `worksheet batch-edit-switch`。"
4203
4215
  },
4204
4216
  "worksheet field-types": {
4205
4217
  "en": "Show field type constants as a reference table.\n\nLists the builder-style CODE names (Text/Number/Relation/...) you\npass via 'update-fields --fields' alongside the wire ControlTypeId\nand the legacy SCREAMING_SNAKE alias still accepted for backward\ncompatibility. Region maps to three ControlTypeIds — pass\n'regionLevel' in the field spec's `extra` to pick one.",
@@ -4705,9 +4717,17 @@
4705
4717
  "en": "Rule type: 0=interact 1=validate 2=lock",
4706
4718
  "zh-Hans": "规则类型:0=交互 1=校验 2=锁定"
4707
4719
  },
4720
+ "worksheet switch-types": {
4721
+ "en": "Show the feature switch numbers with their names and groups.",
4722
+ "zh-Hans": "显示功能开关号及其对应的名称和分组。"
4723
+ },
4708
4724
  "worksheet switches": {
4709
- "en": "Get feature switches for a worksheet.",
4710
- "zh-Hans": "获取工作表的功能开关"
4725
+ "en": "Get feature switches for a worksheet.\n\nEach switch is listed with the name and group it has on the feature\nswitch page, because several numbers share a name and only the group\ntells them apart.",
4726
+ "zh-Hans": "获取工作表的功能开关。每个开关会附带它在功能开关页面上的名称和分组——因为多个开关号共用同一名称,只有分组能区分它们。"
4727
+ },
4728
+ "worksheet switches --raw": {
4729
+ "en": "Return the server's switch list unannotated.",
4730
+ "zh-Hans": "返回服务端原始开关列表,不附加名称和分组。"
4711
4731
  },
4712
4732
  "worksheet update": {
4713
4733
  "en": "Update worksheet alias, description, sidebar name, or sidebar icon.\n\n`--name` and `--icon` change how the worksheet appears in the app\nsidebar; both require --app-id so the section can be resolved.",
@@ -0,0 +1,118 @@
1
+ """Uploaded files must carry their download permission.
2
+
3
+ Reported as "日程/讨论里别人上传的文件下载不了": a colleague opening a file
4
+ attached through the CLI got *您权限不足,无法下载,请联系管理员或文件上传者*,
5
+ while the same file attached from the web UI downloaded fine.
6
+
7
+ The file store does not decide this — the *client* does, per file, when
8
+ it hands the upload result to whatever will hold it. pd-openweb does it
9
+ in ``formatResponseData`` (src/components/UploadFiles/utils.js:202-206):
10
+ every non-picture upload gets ``allowDown: true`` and an empty
11
+ ``docVersionID``; pictures are left alone because the viewer treats them
12
+ as downloadable anyway (UploadFiles/File.js:724). Every attachment
13
+ payload the frontend sends — record comments, record attachment cells —
14
+ is that same object, stringified.
15
+
16
+ The CLI's uploaded-file descriptor never had the field, so anything it
17
+ attached was stored with the permission unset and only the uploader
18
+ could get the bytes back.
19
+ """
20
+
21
+ import json
22
+ from unittest.mock import MagicMock, patch
23
+
24
+ import pytest
25
+
26
+ from hap_cli.core import record as rec_mod
27
+ from hap_cli.core import upload as up_mod
28
+ from hap_cli.core.session import Session
29
+
30
+
31
+ @pytest.fixture
32
+ def session():
33
+ return Session(server_url="https://test.mingdao.com", auth_token="tok123")
34
+
35
+
36
+ class _store:
37
+ """Stand in for the credential + store round trip of an upload."""
38
+
39
+ def __init__(self, ext=".pdf"):
40
+ self.ext = ext
41
+
42
+ def __enter__(self):
43
+ token = {"uptoken": "t", "key": f"doc/20260909/k{self.ext}",
44
+ "serverName": "https://doc.mingdao.com/",
45
+ "fileName": f"k{self.ext}",
46
+ "url": f"https://doc.mingdao.com/doc/k{self.ext}"}
47
+ self._patches = [
48
+ patch.object(up_mod, "get_upload_token", return_value=token),
49
+ patch.object(up_mod, "upload_host",
50
+ return_value="https://upload.qiniup.com/"),
51
+ patch.object(up_mod, "_post_to_store", return_value={
52
+ "key": f"doc/20260909/k{self.ext}", "etag": "e",
53
+ "bucket": "mdoc", "filePath": "doc/20260909/", "fileName": "k",
54
+ "fileExt": self.ext, "originalFileName": "spec",
55
+ }),
56
+ ]
57
+ for p in self._patches:
58
+ p.start()
59
+ return self
60
+
61
+ def __exit__(self, *exc):
62
+ for p in self._patches:
63
+ p.stop()
64
+ return False
65
+
66
+
67
+ class TestDescriptorCarriesDownloadPermission:
68
+
69
+ def test_document_upload_is_downloadable(self, session):
70
+ with _store(".pdf"):
71
+ d = up_mod.upload_bytes(session, content=b"x", name="spec.pdf")
72
+ assert d["allowDown"] is True
73
+ assert d["docVersionID"] == ""
74
+
75
+ def test_archive_upload_is_downloadable(self, session):
76
+ # The reported file was a 13 MB .zip attached to a calendar comment.
77
+ with _store(".zip"):
78
+ d = up_mod.upload_bytes(session, content=b"x", name="proto.zip")
79
+ assert d["allowDown"] is True
80
+
81
+ def test_picture_upload_leaves_the_field_alone(self, session):
82
+ # Mirrors formatResponseData: pictures get neither field.
83
+ with _store(".png"):
84
+ d = up_mod.upload_bytes(session, content=b"x", name="cover.png")
85
+ assert "allowDown" not in d
86
+ assert "docVersionID" not in d
87
+
88
+
89
+ class TestConsumersKeepThePermission:
90
+
91
+ _META = {"md.global": {"APPInfo": {"worksheetRowAppID": "row-app-id"}}}
92
+
93
+ def test_record_comment_attachment_keeps_it(self):
94
+ session = MagicMock()
95
+ session.get_global_meta.return_value = self._META
96
+ session.api_call.return_value = {"code": 1, "data": {"discussionId": "d1"}}
97
+ rec_mod.add_discussion(
98
+ session, "ws1", "row1", "here you go", app_id="app1",
99
+ attachments=[{"fileID": "o_1", "fileExt": ".zip", "allowDown": True,
100
+ "docVersionID": ""}],
101
+ )
102
+ _, _, payload = session.api_call.call_args[0][:3]
103
+ assert json.loads(payload["attachments"])[0]["allowDown"] is True
104
+
105
+ def test_attachment_cell_keeps_it(self):
106
+ cell = up_mod.record_attachment_cell([
107
+ {"fileID": "o_1", "fileExt": ".zip", "originalFileName": "proto",
108
+ "allowDown": True, "docVersionID": ""},
109
+ ])
110
+ assert cell["attachments"][0]["allowDown"] is True
111
+ assert cell["attachments"][0]["docVersionID"] == ""
112
+
113
+ def test_attachment_cell_leaves_a_picture_alone(self):
114
+ cell = up_mod.record_attachment_cell([
115
+ {"fileID": "o_2", "fileExt": ".png", "originalFileName": "cover"},
116
+ ])
117
+ assert "allowDown" not in cell["attachments"][0]
118
+ assert "docVersionID" not in cell["attachments"][0]