hap-cli 0.8.19__tar.gz → 0.8.21__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 (264) hide show
  1. {hap_cli-0.8.19 → hap_cli-0.8.21}/PKG-INFO +2 -1
  2. {hap_cli-0.8.19/hap_cli → hap_cli-0.8.21}/README.md +1 -0
  3. {hap_cli-0.8.19 → hap_cli-0.8.21/hap_cli}/README.md +1 -0
  4. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/README_CN.md +1 -0
  5. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/__init__.py +1 -1
  6. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/config_cmd.py +44 -0
  7. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/action_spec_adapter.py +9 -3
  8. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/flow_node.py +41 -18
  9. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/record.py +80 -5
  10. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/session.py +33 -14
  11. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/update.py +39 -0
  12. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/upload.py +31 -13
  13. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/worksheet.py +57 -7
  14. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/i18n.py +49 -0
  15. hap_cli-0.8.21/hap_cli/tests/test_content_language.py +120 -0
  16. hap_cli-0.8.21/hap_cli/tests/test_control_lookup_guard.py +179 -0
  17. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_core.py +22 -6
  18. hap_cli-0.8.21/hap_cli/tests/test_no_workflow_unsupported.py +92 -0
  19. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +20 -10
  20. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_record_v3_semantic_cli.py +3 -1
  21. hap_cli-0.8.21/hap_cli/tests/test_update_mode.py +90 -0
  22. hap_cli-0.8.21/hap_cli/tests/test_upload_host_resolution.py +93 -0
  23. hap_cli-0.8.21/hap_cli/tests/test_workflow_envelope_errors.py +112 -0
  24. hap_cli-0.8.21/hap_cli/tests/test_workflow_read_write_keys.py +71 -0
  25. hap_cli-0.8.21/hap_cli/tests/test_worksheet_create_section.py +91 -0
  26. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +4 -0
  27. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/PKG-INFO +2 -1
  28. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/SOURCES.txt +8 -0
  29. {hap_cli-0.8.19 → hap_cli-0.8.21}/MANIFEST.in +0 -0
  30. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/__init__.py +0 -0
  31. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/__main__.py +0 -0
  32. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/config.py +0 -0
  33. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/harness.py +0 -0
  34. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/smoke.py +0 -0
  35. {hap_cli-0.8.19 → hap_cli-0.8.21}/app_live_tests/state.py +0 -0
  36. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/__main__.py +0 -0
  37. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/__init__.py +0 -0
  38. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/app_cmd.py +0 -0
  39. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/app_creator_cmd.py +0 -0
  40. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/app_editor_cmd.py +0 -0
  41. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/auth_cmd.py +0 -0
  42. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/calendar_cmd.py +0 -0
  43. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/chart_cmd.py +0 -0
  44. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/chat_cmd.py +0 -0
  45. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/chatbot_cmd.py +0 -0
  46. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/contact_cmd.py +0 -0
  47. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/department_cmd.py +0 -0
  48. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/group_cmd.py +0 -0
  49. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/icon_cmd.py +0 -0
  50. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/instance_cmd.py +0 -0
  51. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/node_cmd.py +0 -0
  52. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/optionset_cmd.py +0 -0
  53. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/page_cmd.py +0 -0
  54. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/post_cmd.py +0 -0
  55. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/record_cmd.py +0 -0
  56. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/region_cmd.py +0 -0
  57. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/role_cmd.py +0 -0
  58. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/update_cmd.py +0 -0
  59. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/upload_cmd.py +0 -0
  60. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/v3_registry.py +0 -0
  61. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/workflow_cmd.py +0 -0
  62. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/commands/worksheet_cmd.py +0 -0
  63. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/context.py +0 -0
  64. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/__init__.py +0 -0
  65. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app.py +0 -0
  66. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/__init__.py +0 -0
  67. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  68. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  69. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
  70. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/charts.py +0 -0
  71. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/cleanup.py +0 -0
  72. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/compiler.py +0 -0
  73. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/config.py +0 -0
  74. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  75. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/errors.py +0 -0
  76. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  77. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/executor.py +0 -0
  78. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/fields.py +0 -0
  79. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/hap.py +0 -0
  80. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/merge.py +0 -0
  81. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  82. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/recording/console.py +0 -0
  83. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  84. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  85. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/recording/report.py +0 -0
  86. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/schema.py +0 -0
  87. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  88. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  89. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/cli.py +0 -0
  90. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/executor.py +0 -0
  91. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  92. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  93. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  94. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  95. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/seed/template.py +0 -0
  96. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/selftest.py +0 -0
  97. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/smoke.py +0 -0
  98. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/steps.py +0 -0
  99. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/store.py +0 -0
  100. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/validate.py +0 -0
  101. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  102. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/auth.py +0 -0
  103. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/calendar_mod.py +0 -0
  104. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/chart.py +0 -0
  105. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/chat.py +0 -0
  106. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/chatbot.py +0 -0
  107. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/cli_access.py +0 -0
  108. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/contact.py +0 -0
  109. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/control_type_codes.py +0 -0
  110. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/department.py +0 -0
  111. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/__init__.py +0 -0
  112. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  113. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  114. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/apply.py +0 -0
  115. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/componentlower.py +0 -0
  116. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/editspec.py +0 -0
  117. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/errors.py +0 -0
  118. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/fieldlower.py +0 -0
  119. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  120. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/models.py +0 -0
  121. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/ops.py +0 -0
  122. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/planner.py +0 -0
  123. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/editor/reader.py +0 -0
  124. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/field_normalizer.py +0 -0
  125. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/field_spec_adapter.py +0 -0
  126. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/filter_translator.py +0 -0
  127. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/global_meta.py +0 -0
  128. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/group.py +0 -0
  129. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/icon_index.py +0 -0
  130. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/instance.py +0 -0
  131. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/logger.py +0 -0
  132. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/optionset.py +0 -0
  133. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/page.py +0 -0
  134. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/post.py +0 -0
  135. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/response_crypto.py +0 -0
  136. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/role.py +0 -0
  137. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/role_perm_builder.py +0 -0
  138. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/token_crypto.py +0 -0
  139. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/v3/__init__.py +0 -0
  140. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/v3/dispatcher.py +0 -0
  141. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/v3/render.py +0 -0
  142. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/v3/schema.py +0 -0
  143. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/view_spec_adapter.py +0 -0
  144. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/workflow.py +0 -0
  145. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/workflow_node_dsl.py +0 -0
  146. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/core/worksheet_templates.py +0 -0
  147. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/hap_cli.py +0 -0
  148. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/locale/__init__.py +0 -0
  149. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/locale/messages.json +0 -0
  150. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/locale/messages.schema.json +0 -0
  151. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/__init__.py +0 -0
  152. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/conftest.py +0 -0
  153. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_app_creator.py +0 -0
  154. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_app_editor.py +0 -0
  155. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_app_export_usage.py +0 -0
  156. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_auth_prerelease.py +0 -0
  157. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_bug_036430.py +0 -0
  158. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_bug_036750.py +0 -0
  159. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_chart.py +0 -0
  160. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_cli_access.py +0 -0
  161. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_config_cmd.py +0 -0
  162. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  163. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_full_e2e.py +0 -0
  164. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_global_meta.py +0 -0
  165. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_i18n.py +0 -0
  166. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_instance_history_cli.py +0 -0
  167. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration.py +0 -0
  168. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_approval.py +0 -0
  169. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  170. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_calendar.py +0 -0
  171. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_destructive.py +0 -0
  172. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_misc.py +0 -0
  173. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_post.py +0 -0
  174. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_social.py +0 -0
  175. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_v3.py +0 -0
  176. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_workflow.py +0 -0
  177. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  178. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
  179. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_multi_profile.py +0 -0
  180. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  181. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_org_id_cli.py +0 -0
  182. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_org_id_docs.py +0 -0
  183. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_parameter_conventions.py +0 -0
  184. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  185. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_record_field_alias.py +0 -0
  186. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
  187. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  188. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_update.py +0 -0
  189. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  190. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  191. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  192. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_v3_session.py +0 -0
  193. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  194. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  195. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  196. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  197. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/utils/__init__.py +0 -0
  198. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/utils/formatting.py +0 -0
  199. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/utils/options.py +0 -0
  200. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli/utils/parameter_mapping.py +0 -0
  201. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/dependency_links.txt +0 -0
  202. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/entry_points.txt +0 -0
  203. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/requires.txt +0 -0
  204. {hap_cli-0.8.19 → hap_cli-0.8.21}/hap_cli.egg-info/top_level.txt +0 -0
  205. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/__init__.py +0 -0
  206. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/__main__.py +0 -0
  207. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/_wftest.py +0 -0
  208. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/charts.py +0 -0
  209. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/cleanup.py +0 -0
  210. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/compiler.py +0 -0
  211. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/config.py +0 -0
  212. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/errors.py +0 -0
  213. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/executor.py +0 -0
  214. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/fields.py +0 -0
  215. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/hap.py +0 -0
  216. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/recording/__init__.py +0 -0
  217. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/recording/console.py +0 -0
  218. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/recording/jsonl.py +0 -0
  219. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/recording/mirror.py +0 -0
  220. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/recording/report.py +0 -0
  221. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/schema.py +0 -0
  222. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/seed/__init__.py +0 -0
  223. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/seed/cli.py +0 -0
  224. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/seed/executor.py +0 -0
  225. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/seed/template.py +0 -0
  226. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/selftest.py +0 -0
  227. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/smoke.py +0 -0
  228. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/step.py +0 -0
  229. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/steps.py +0 -0
  230. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/store.py +0 -0
  231. {hap_cli-0.8.19 → hap_cli-0.8.21}/live_tests/workflow_dsl.py +0 -0
  232. {hap_cli-0.8.19 → hap_cli-0.8.21}/setup.cfg +0 -0
  233. {hap_cli-0.8.19 → hap_cli-0.8.21}/setup.py +0 -0
  234. {hap_cli-0.8.19 → hap_cli-0.8.21}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
  235. {hap_cli-0.8.19 → hap_cli-0.8.21}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
  236. {hap_cli-0.8.19 → hap_cli-0.8.21}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
  237. {hap_cli-0.8.19 → hap_cli-0.8.21}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
  238. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/INDEX.json +0 -0
  239. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  240. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  241. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/create_optionset.yaml +0 -0
  242. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/create_role.yaml +0 -0
  243. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  244. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/delete_record.yaml +0 -0
  245. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/delete_role.yaml +0 -0
  246. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  247. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_app_info.yaml +0 -0
  248. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  249. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  250. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  251. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_details.yaml +0 -0
  252. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  253. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_list.yaml +0 -0
  254. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  255. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  256. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  257. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  258. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_regions.yaml +0 -0
  259. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_role_details.yaml +0 -0
  260. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/get_role_list.yaml +0 -0
  261. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  262. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  263. {hap_cli-0.8.19 → hap_cli-0.8.21}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  264. {hap_cli-0.8.19 → hap_cli-0.8.21}/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.19
3
+ Version: 0.8.21
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -313,6 +313,7 @@ discover every subcommand.
313
313
  | `config show` | Display the current configuration summary |
314
314
  | `config completion [bash\|zsh\|fish] [--install]` | Enable `<Tab>` completion |
315
315
  | `config language [LANG]` | Switch UI language (`en` / `zh-Hans` / `zh-Hant` / `ja`) |
316
+ | `config update [auto\|notify\|off]` | Self-upgrade mode; `notify` only reports, `off` never checks |
316
317
  | `config log on / off / status` | Toggle and inspect API logging |
317
318
  | `config log level LEVEL` | `DEBUG / INFO / WARNING / ERROR / CRITICAL` |
318
319
  | `config log view [--lines N] [--no-follow]` | Tail the log file |
@@ -284,6 +284,7 @@ discover every subcommand.
284
284
  | `config show` | Display the current configuration summary |
285
285
  | `config completion [bash\|zsh\|fish] [--install]` | Enable `<Tab>` completion |
286
286
  | `config language [LANG]` | Switch UI language (`en` / `zh-Hans` / `zh-Hant` / `ja`) |
287
+ | `config update [auto\|notify\|off]` | Self-upgrade mode; `notify` only reports, `off` never checks |
287
288
  | `config log on / off / status` | Toggle and inspect API logging |
288
289
  | `config log level LEVEL` | `DEBUG / INFO / WARNING / ERROR / CRITICAL` |
289
290
  | `config log view [--lines N] [--no-follow]` | Tail the log file |
@@ -284,6 +284,7 @@ discover every subcommand.
284
284
  | `config show` | Display the current configuration summary |
285
285
  | `config completion [bash\|zsh\|fish] [--install]` | Enable `<Tab>` completion |
286
286
  | `config language [LANG]` | Switch UI language (`en` / `zh-Hans` / `zh-Hant` / `ja`) |
287
+ | `config update [auto\|notify\|off]` | Self-upgrade mode; `notify` only reports, `off` never checks |
287
288
  | `config log on / off / status` | Toggle and inspect API logging |
288
289
  | `config log level LEVEL` | `DEBUG / INFO / WARNING / ERROR / CRITICAL` |
289
290
  | `config log view [--lines N] [--no-follow]` | Tail the log file |
@@ -258,6 +258,7 @@ hap --json worksheet record list WORKSHEET_ID
258
258
  | `config show` | 显示 CLI 配置摘要 |
259
259
  | `config completion [bash\|zsh\|fish] [--install]` | 启用 `<Tab>` 补全 |
260
260
  | `config language [LANG]` | 切换显示语言(`en` / `zh-Hans` / `zh-Hant` / `ja`) |
261
+ | `config update [auto\|notify\|off]` | 自动升级开关;`notify` 只提示不安装,`off` 不检查 |
261
262
  | `config log on / off / status` | 日志开关与状态 |
262
263
  | `config log level LEVEL` | 设置日志级别(`DEBUG/INFO/WARNING/ERROR/CRITICAL`) |
263
264
  | `config log view [--lines N] [--no-follow]` | 跟踪日志文件 |
@@ -1,3 +1,3 @@
1
1
  """CLI harness for Mingdao HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.8.19"
3
+ __version__ = "0.8.21"
@@ -122,6 +122,8 @@ def config_completion(shell, install):
122
122
  @pass_context
123
123
  def config_show(ctx):
124
124
  """Display current harness configuration."""
125
+ from hap_cli.core.update import get_update_mode
126
+
125
127
  log_cfg = load_logging_config()
126
128
  data = {
127
129
  "logging": {
@@ -129,6 +131,7 @@ def config_show(ctx):
129
131
  "level": log_cfg["level"],
130
132
  "file": str(resolve_log_path(log_cfg)),
131
133
  },
134
+ "update": get_update_mode(),
132
135
  }
133
136
 
134
137
  def _human(d):
@@ -136,6 +139,7 @@ def config_show(ctx):
136
139
  click.echo(f" enabled : {d['logging']['enabled']}")
137
140
  click.echo(f" level : {d['logging']['level']}")
138
141
  click.echo(f" file : {d['logging']['file']}")
142
+ click.echo(f"update : {d['update']}")
139
143
 
140
144
  ctx.output(data, _human)
141
145
 
@@ -205,6 +209,46 @@ def log_status(ctx):
205
209
  _echo_log_status(ctx, cfg)
206
210
 
207
211
 
212
+ # ── `hap config update` ───────────────────────────────────────────────────
213
+
214
+
215
+ @config.command("update")
216
+ @click.argument(
217
+ "mode", required=False, type=click.Choice(["auto", "notify", "off"]),
218
+ )
219
+ @pass_context
220
+ def config_update(ctx, mode):
221
+ """Control whether hap-cli upgrades itself.
222
+
223
+ \b
224
+ auto check hourly and install new versions (default)
225
+ notify check hourly and tell you, but never install
226
+ off never check
227
+
228
+ Use ``notify`` or ``off`` to pin the version in scripted or
229
+ production environments. Without an argument, shows the current
230
+ setting. ``hap update`` still upgrades on demand in every mode.
231
+ """
232
+ from hap_cli.core.session import load_config, save_config
233
+ from hap_cli.core.update import get_update_mode
234
+
235
+ if mode is None:
236
+ current = get_update_mode()
237
+ if ctx.json_mode:
238
+ from hap_cli.utils.formatting import output_json
239
+ output_json({"update": current})
240
+ else:
241
+ click.echo(f"Self-update: {current}")
242
+ return
243
+
244
+ save_config({"update": mode})
245
+ if ctx.json_mode:
246
+ from hap_cli.utils.formatting import output_json
247
+ output_json({"status": "ok", "update": mode})
248
+ else:
249
+ click.echo(f"Self-update set to: {mode}")
250
+
251
+
208
252
  # ── `hap config language` ─────────────────────────────────────────────────
209
253
 
210
254
 
@@ -110,9 +110,15 @@ def build_button_payload(spec: dict[str, Any]) -> dict[str, Any]:
110
110
  # in the UI; for the dedicated 二次确认 button use clickType 2.
111
111
  if out["clickType"] == 1 and confirm:
112
112
  out["clickType"] = 2
113
- out["confirmMsg"] = confirm_msg or "你确认执行此操作吗?"
114
- out["sureName"] = spec.get("sureName", "确认")
115
- out["cancelName"] = spec.get("cancelName", "取消")
113
+ # Dialog text is stored on the button itself, so it must be
114
+ # written in the application's language, not ours. Follows the
115
+ # active CLI language; an explicit value always wins.
116
+ from hap_cli.i18n import content_default
117
+
118
+ out["confirmMsg"] = confirm_msg or content_default("button.confirmMsg")
119
+ out["sureName"] = spec.get("sureName") or content_default("button.sure")
120
+ out["cancelName"] = (
121
+ spec.get("cancelName") or content_default("button.cancel"))
116
122
 
117
123
  # enableWhen → filters (reuse the shared filter translator).
118
124
  enable_when = spec.get("enableWhen")
@@ -394,27 +394,50 @@ def save_node(
394
394
  if select_node_id:
395
395
  data["selectNodeId"] = select_node_id
396
396
  data.update(config)
397
- # Branch items (flowNodeType 2) have an asymmetric read/write key for
398
- # their filter: getNodeDetail returns it as ``conditions`` (see
399
- # pd-openweb Branch/index.jsx, which reads ``result.conditions``), but
400
- # saveNode only persists it under ``operateCondition`` (the same file
401
- # sends ``saveNode({ operateCondition: data })``). The value shape is
402
- # identical. Saving with the read-side ``conditions`` key is silently
403
- # ignored by the server — the save still reports success but the
404
- # condition is dropped. Accept the read key so copying a node's
405
- # read structure straight back round-trips correctly.
406
- if flow_node_type == 2 and "conditions" in data:
407
- if "operateCondition" not in data:
408
- data["operateCondition"] = data["conditions"]
409
- logger.info(
410
- "save_node: branch item %s — mapped read-side key "
411
- "'conditions' to write-side 'operateCondition'", node_id)
412
- # ``conditions`` is not a saveNode key; drop it so it doesn't ride
413
- # along as dead weight.
414
- data.pop("conditions", None)
397
+ _map_read_side_keys(data, node_id)
415
398
  return session.workflow_call("flowNode/saveNode", data)
416
399
 
417
400
 
401
+ # Node config keys whose read form differs from their write form. Reading
402
+ # a node, editing one value and writing it back is the natural way to work
403
+ # with these APIs, but the server ignores the read-side key on save — the
404
+ # call still reports success while the value is dropped.
405
+ #
406
+ # The filter is asymmetric on every filtering node in pd-openweb, not just
407
+ # on branches: the component holds it as ``conditions`` and posts it as
408
+ # ``operateCondition``, with an identical value shape.
409
+ # Branch/index.jsx:184 saveNode({ operateCondition: conditions })
410
+ # GetMoreRecord/index.jsx:184 saveNode({ operateCondition: conditions })
411
+ # Search/index.jsx:190 saveNode({ operateCondition: conditions })
412
+ # FindSystem/index.jsx:110 saveNode({ operateCondition: conditions })
413
+ # ``conditions`` is never itself a saveNode key, so translating it is safe
414
+ # for any node type.
415
+ _READ_TO_WRITE_KEYS = {
416
+ "conditions": "operateCondition",
417
+ }
418
+
419
+
420
+ def _map_read_side_keys(data: dict[str, Any], node_id: str) -> None:
421
+ """Rewrite read-side config keys to the keys saveNode persists.
422
+
423
+ An explicit write-side key always wins; the read-side key is then
424
+ dropped either way so it cannot ride along as dead weight.
425
+ """
426
+ for read_key, write_key in _READ_TO_WRITE_KEYS.items():
427
+ if read_key not in data:
428
+ continue
429
+ if write_key in data:
430
+ logger.info(
431
+ "save_node: node %s carries both '%s' and '%s'; keeping "
432
+ "the write-side value", node_id, read_key, write_key)
433
+ else:
434
+ data[write_key] = data[read_key]
435
+ logger.info(
436
+ "save_node: node %s — mapped read-side key '%s' to "
437
+ "write-side '%s'", node_id, read_key, write_key)
438
+ data.pop(read_key, None)
439
+
440
+
418
441
  # ── Testing ──────────────────────────────────────────────────────────────
419
442
 
420
443
 
@@ -1,6 +1,7 @@
1
1
  """Record CRUD module for Mingdao HAP."""
2
2
 
3
3
  import json
4
+ import logging
4
5
  import uuid
5
6
  from typing import Any, Optional
6
7
 
@@ -8,6 +9,8 @@ from hap_cli.core.session import Session, APIError
8
9
  from hap_cli.core import worksheet as ws_mod
9
10
  from hap_cli.core import global_meta as gm
10
11
 
12
+ logger = logging.getLogger("hap_cli.record")
13
+
11
14
  # Discussion source types — pd-openweb src/components/comment/config.js
12
15
  # WORKSHEET = 7 whole-worksheet discussion
13
16
  # WORKSHEETROW = 8 single record/row discussion
@@ -383,6 +386,41 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
383
386
  return "" if value is None else str(value)
384
387
 
385
388
 
389
+ def _controls_for_write(
390
+ session: Session, worksheet_id: str,
391
+ ) -> list[dict[str, Any]]:
392
+ """Fetch the worksheet's controls for a write, refusing an empty list.
393
+
394
+ Every write path type-encodes its cells against this metadata, so an
395
+ empty list silently disables the encoder: option/member/relation/
396
+ sub-table values would all take the "unknown field -> send the raw
397
+ value" fallback and reach the server in whatever shape the caller
398
+ happened to type. That is how a hand-written 1-D sub-table value
399
+ (``[{childControlId: value}, ...]``) escaped to the server and failed
400
+ deserialization into ``List<Cell>`` — the encoder would otherwise have
401
+ rewritten it to the 2-D form.
402
+
403
+ ``get_worksheet_controls`` answers ``[]`` for any response that does
404
+ not carry a ``controls`` list — a soft failure, ``data: null``, or a
405
+ permission-degraded envelope — none of which raise. A real worksheet
406
+ always has controls, so an empty list means "we could not read the
407
+ fields", never "this worksheet has none". Abort before writing.
408
+ """
409
+ controls = ws_mod.get_worksheet_controls(session, worksheet_id)
410
+ if not controls:
411
+ logger.warning(
412
+ "GetWorksheetControls returned no fields for worksheet %s; "
413
+ "aborting the write instead of sending unencoded cells",
414
+ worksheet_id,
415
+ )
416
+ raise ValueError(
417
+ f"Could not read the fields of worksheet {worksheet_id}, so "
418
+ "nothing was written. Check the worksheet id and that your "
419
+ "account can open it, then try again."
420
+ )
421
+ return controls
422
+
423
+
386
424
  def encode_controls_by_id(
387
425
  session: Session,
388
426
  worksheet_id: str,
@@ -419,7 +457,7 @@ def encode_controls_by_id(
419
457
  (in-place edits keyed by ``rowid``, fresh adds, and deletes — see
420
458
  :func:`_sublist_update_value`).
421
459
  """
422
- controls = ws_mod.get_worksheet_controls(session, worksheet_id)
460
+ controls = _controls_for_write(session, worksheet_id)
423
461
  by_id: dict[str, dict[str, Any]] = {
424
462
  c.get("controlId"): c for c in controls if c.get("controlId")
425
463
  }
@@ -434,6 +472,14 @@ def encode_controls_by_id(
434
472
  raw = rc.get("value")
435
473
  ctrl = by_id.get(cid) or by_ref.get(cid)
436
474
  if ctrl is None:
475
+ # Deliberate escape hatch (see docstring), but a value that
476
+ # skips the type encoder is exactly how a malformed cell
477
+ # reaches the server, so leave a trace: this is the only
478
+ # place a write can go out unencoded.
479
+ logger.warning(
480
+ "Field %r is not on worksheet %s; its value is sent "
481
+ "unencoded (no type conversion applied)", cid, worksheet_id,
482
+ )
437
483
  out.append({"controlId": cid, "value": raw})
438
484
  continue
439
485
  out.append({
@@ -603,7 +649,7 @@ def batch_create_by_name(
603
649
  by child controlId; single/multi select -> option keys; etc.).
604
650
  Evidence: hap_cli/tests/answer_create_record.md.
605
651
  """
606
- controls = ws_mod.get_worksheet_controls(session, worksheet_id)
652
+ controls = _controls_for_write(session, worksheet_id)
607
653
  by_name: dict[str, dict[str, Any]] = {}
608
654
  for c in controls:
609
655
  for key in (c.get("controlName"), c.get("alias"),
@@ -662,7 +708,7 @@ def batch_update_by_name(
662
708
  :func:`_sublist_update_value`). Record updates never use the V3
663
709
  rows endpoint — V3 silently drops relation / sub-table cells.
664
710
  """
665
- controls = ws_mod.get_worksheet_controls(session, worksheet_id)
711
+ controls = _controls_for_write(session, worksheet_id)
666
712
  by_name: dict[str, dict[str, Any]] = {}
667
713
  for c in controls:
668
714
  for key in (c.get("controlName"), c.get("alias"),
@@ -785,6 +831,35 @@ def get_record(
785
831
  return session.api_call("Worksheet", "GetRowDetail", data)
786
832
 
787
833
 
834
+ # The main-site record endpoints have no workflow-suppression parameter.
835
+ # pd-openweb documents UpdateWorksheetRow as worksheetId / rowId /
836
+ # newOldControl / viewId / instanceId / workId / btnId / btnRemark /
837
+ # btnWorksheetId / btnRowId / pushUniqueId / rowStatus / updateType
838
+ # (src/api/worksheet.js:535-548), and AddWorksheetRow likewise has none.
839
+ # ``triggerWorkflow`` belongs to the open API v2 (worksheet/addRow,
840
+ # worksheet/editRow) — a different transport. We used to send it anyway;
841
+ # the main site accepted the body and ignored the flag, so the workflows
842
+ # ran while the caller believed they had been suppressed (confirmed live:
843
+ # a record-update flow stamped its field on an update sent with
844
+ # triggerWorkflow: false).
845
+ #
846
+ # Record writes cannot simply move to the open API: it silently drops
847
+ # RELATE_SHEET / SUB_LIST cells. Until there is a real way to suppress,
848
+ # refusing is the only honest answer — anyone reaching for this is trying
849
+ # to edit data *safely*, and quietly doing the unsafe thing is worse than
850
+ # failing.
851
+ def _reject_workflow_suppression(trigger_workflow: bool) -> None:
852
+ if trigger_workflow:
853
+ return
854
+ raise ValueError(
855
+ "Workflows cannot be suppressed for this write — they will run "
856
+ "whatever this command does, so the option is refused rather than "
857
+ "ignored. To edit records without them, turn the workflow off "
858
+ "first (hap workflow publish <id> --disable), make the change, "
859
+ "then turn it back on."
860
+ )
861
+
862
+
788
863
  def create_record(
789
864
  session: Session,
790
865
  worksheet_id: str,
@@ -802,13 +877,13 @@ def create_record(
802
877
  Returns:
803
878
  Created record info
804
879
  """
880
+ _reject_workflow_suppression(trigger_workflow)
805
881
  return session.api_call(
806
882
  "Worksheet",
807
883
  "AddWorksheetRow",
808
884
  {
809
885
  "worksheetId": worksheet_id,
810
886
  "receiveControls": controls,
811
- "triggerWorkflow": trigger_workflow,
812
887
  },
813
888
  )
814
889
 
@@ -832,6 +907,7 @@ def update_record(
832
907
  Returns:
833
908
  Update result
834
909
  """
910
+ _reject_workflow_suppression(trigger_workflow)
835
911
  return session.api_call(
836
912
  "Worksheet",
837
913
  "UpdateWorksheetRow",
@@ -839,7 +915,6 @@ def update_record(
839
915
  "worksheetId": worksheet_id,
840
916
  "rowId": row_id,
841
917
  "newOldControl": controls,
842
- "triggerWorkflow": trigger_workflow,
843
918
  },
844
919
  )
845
920
 
@@ -360,11 +360,19 @@ class Session:
360
360
  from hap_cli.core import cli_access
361
361
 
362
362
  cli_access.ensure_before_request(self, url)
363
+ from hap_cli.i18n import get_active_language
364
+
363
365
  headers = {
364
366
  "Content-Type": "application/json",
365
367
  "Authorization": f"md_pss_id {self.auth_token}",
366
368
  "X-Requested-With": "XMLHttpRequest",
367
369
  "User-Agent": hap_user_agent(),
370
+ # Give the server the language context it otherwise has to
371
+ # guess at. Defaults the server fills in (hints, template
372
+ # text) were coming back in Simplified Chinese for
373
+ # all-English applications because API requests carried no
374
+ # language at all — the browser always sends one.
375
+ "Accept-Language": get_active_language(),
368
376
  }
369
377
  # pd-openweb sends the caller's own accountId on every request
370
378
  # (``src/common/global.js`` ``disposeRequestParams``). Some
@@ -529,28 +537,39 @@ class Session:
529
537
  # Detect each and unwrap accordingly.
530
538
  if not isinstance(result, dict):
531
539
  return result
540
+ # A non-empty ``exception`` means the call failed, whatever envelope
541
+ # it arrived in. This is the frontend's own and only test — mdyAPI
542
+ # rejects on ``if (responseData.exception)`` alone and never looks at
543
+ # ``status`` (pd-openweb src/common/global.js:846-849). Checking it
544
+ # before any unwrapping is what makes workflow failures visible: the
545
+ # workflow envelope has no ``state``, so it used to skip the error
546
+ # branch entirely and hand the caller a "successful" empty result.
547
+ exception_msg = str(result.get("exception", "") or "")
548
+ if exception_msg:
549
+ code = result.get("code", result.get("state"))
550
+ # Some HAP deployments tunnel auth failures through the exception
551
+ # text/code instead of HTTP 401. Surface those as
552
+ # AuthenticationError so the CLI shows the login hint.
553
+ if _looks_like_auth_failure(code, exception_msg):
554
+ raise AuthenticationError(exception_msg, status_code=None)
555
+ raise APIError(exception_msg, code=code)
532
556
  if "status" in result and "data" in result and "state" not in result:
533
- # Workflow envelope. status==0 is "ok" here historically; we
534
- # don't have a clean way to detect failures via status alone,
535
- # so we just unwrap data and trust HTTP status for errors.
557
+ # Workflow envelope: ``status`` carries no success/failure
558
+ # semantics (the frontend ignores it), and a failure would have
559
+ # been caught by the exception check above.
536
560
  return result.get("data", result)
537
561
  if "state" not in result:
538
562
  return result
539
563
  if result.get("state") == 0:
540
- exception_msg = str(result.get("exception", "") or "")
541
564
  code = result.get("code")
542
- # Some HAP deployments tunnel auth failures through state=0 with
543
- # specific codes/messages instead of HTTP 401. Surface those as
544
- # AuthenticationError too so the CLI shows the login hint.
545
- if _looks_like_auth_failure(code, exception_msg):
565
+ # state=0 with no exception text still a failure, just an
566
+ # unlabelled one.
567
+ if _looks_like_auth_failure(code, ""):
546
568
  raise AuthenticationError(
547
- exception_msg or "Authentication failed (upstream reported login required).",
569
+ "Authentication failed (upstream reported login required).",
548
570
  status_code=None,
549
571
  )
550
- raise APIError(
551
- exception_msg or "Unknown API error",
552
- code=code,
553
- )
572
+ raise APIError("Unknown API error", code=code)
554
573
  return result.get("data", result)
555
574
 
556
575
  # ── API Call Methods ──────────────────────────────────────────────────
@@ -994,7 +1013,7 @@ PROFILE_FIELDS = (
994
1013
  # are properties of the human at the terminal / the install, not of any
995
1014
  # one account, so logger and the language commands keep reading/writing
996
1015
  # them via the plain load_config()/save_config() top-level accessors.
997
- GLOBAL_FIELDS = ("logging", "language", "language_source")
1016
+ GLOBAL_FIELDS = ("logging", "language", "language_source", "update")
998
1017
 
999
1018
 
1000
1019
  def _empty_v2_doc() -> dict:
@@ -25,6 +25,8 @@ from typing import Any, Optional
25
25
 
26
26
  import requests
27
27
 
28
+ from hap_cli.core.session import load_config
29
+
28
30
  logger = logging.getLogger("hap_cli.update")
29
31
 
30
32
  PYPI_PACKAGE = "hap-cli"
@@ -172,6 +174,27 @@ def save_state(state: dict) -> None:
172
174
  # ── Background check orchestration ───────────────────────────────────────
173
175
 
174
176
 
177
+ UPDATE_MODES = ("auto", "notify", "off")
178
+ DEFAULT_UPDATE_MODE = "auto"
179
+
180
+
181
+ def get_update_mode() -> str:
182
+ """Resolve the self-update mode from config.
183
+
184
+ ``auto`` checks and upgrades, ``notify`` checks and reports without
185
+ upgrading, ``off`` does not check at all. Anything unrecognised — or
186
+ no setting — is ``auto``, the behaviour that shipped before this was
187
+ configurable.
188
+ """
189
+ try:
190
+ cfg = load_config() or {}
191
+ except Exception as e:
192
+ logger.warning("could not read the update mode from config: %s", e)
193
+ return DEFAULT_UPDATE_MODE
194
+ mode = cfg.get("update")
195
+ return mode if mode in UPDATE_MODES else DEFAULT_UPDATE_MODE
196
+
197
+
175
198
  def maybe_spawn_background_check() -> None:
176
199
  """Spawn a detached background update check if it is due.
177
200
 
@@ -179,6 +202,8 @@ def maybe_spawn_background_check() -> None:
179
202
  *before* spawning so rapid consecutive commands cannot stack
180
203
  duplicate children — at worst a failed child costs one interval.
181
204
  """
205
+ if get_update_mode() == "off":
206
+ return
182
207
  kind = get_install_kind()
183
208
  if kind in ("editable", "unknown"):
184
209
  return
@@ -246,6 +271,15 @@ def run_background_update() -> None:
246
271
  save_state(state)
247
272
  return
248
273
 
274
+ if get_update_mode() == "notify":
275
+ # Report it and stop — the point of this mode is that the
276
+ # installed version never changes underneath a running script.
277
+ state["last_result"] = "update_available"
278
+ state["notified"] = False
279
+ save_state(state)
280
+ logger.info("update to %s available (notify-only mode)", latest)
281
+ return
282
+
249
283
  ok, output = perform_upgrade(get_install_kind())
250
284
  if ok:
251
285
  state["last_result"] = "upgraded"
@@ -280,6 +314,11 @@ def consume_startup_notice() -> Optional[str]:
280
314
  f"Automatic upgrade to {latest} failed — "
281
315
  "run `hap update` to upgrade manually."
282
316
  )
317
+ elif result == "update_available":
318
+ latest = state.get("latest_version") or "a newer version"
319
+ msg = (
320
+ f"hap-cli {latest} is available — run `hap update` to install it."
321
+ )
283
322
  else:
284
323
  return None
285
324
 
@@ -30,12 +30,11 @@ from urllib.parse import urlparse
30
30
 
31
31
  import requests
32
32
 
33
- from hap_cli.core.session import Session
33
+ from hap_cli.core.session import Session, SessionError
34
34
  logger = logging.getLogger("hap_cli.upload")
35
35
 
36
- # Public-SaaS qiniu upload endpoint. Self-hosted overrides this via
37
- # FileStoreConfig.uploadHost in Global/GetGlobalMeta.
38
- _PUBLIC_UPLOAD_HOST = "https://upload.qiniup.com/"
36
+ # The upload endpoint is always read from Global/GetGlobalMeta — there is
37
+ # deliberately no constant to fall back to, see upload_host().
39
38
 
40
39
  # bucket ids (pd-openweb kc/utils): 4=picture(mdpic), 3=document(mdoc).
41
40
  _PICTURE_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg",
@@ -67,17 +66,36 @@ def _bucket_for_ext(ext: str) -> int:
67
66
 
68
67
 
69
68
  def upload_host(session: Session) -> str:
70
- """Resolve the file-store upload endpoint. Self-hosted environments
71
- publish it as ``FileStoreConfig.uploadHost``; the public SaaS leaves
72
- it empty and uses qiniu directly."""
73
- try:
74
- meta = session.get_global_meta()
75
- host = (meta.get("FileStoreConfig") or {}).get("uploadHost")
69
+ """Resolve this environment's file-store upload endpoint.
70
+
71
+ Global/GetGlobalMeta answers ``{config: {...}, "md.global": {...}}``
72
+ and publishes the file store under ``md.global.FileStoreConfig`` —
73
+ the same object every frontend upload posts to
74
+ (``md.global.FileStoreConfig.uploadHost``, pd-openweb
75
+ src/library/plupload/createUploader.js:43 and 12 sibling call sites,
76
+ none of which has a fallback). We used to read ``FileStoreConfig``
77
+ at the top level, so it was never found on any environment and a
78
+ hardcoded public-qiniu constant was used instead. That constant
79
+ equals the public SaaS's own uploadHost, which hid the bug there and
80
+ broke every environment with its own file store: the bytes went to
81
+ public qiniu while the upload token came from the environment's
82
+ store, so the store answered 401 with nothing to point at.
83
+
84
+ A missing host is an error rather than a guess — guessing is what
85
+ turned a routing bug into an unexplainable 401.
86
+ """
87
+ meta = session.get_global_meta()
88
+ for scope in (meta.get("md.global"), meta):
89
+ if not isinstance(scope, dict):
90
+ continue
91
+ host = (scope.get("FileStoreConfig") or {}).get("uploadHost")
76
92
  if host:
77
93
  return host
78
- except Exception as e: # non-fatal: fall back to the public endpoint
79
- logger.warning("uploadHost lookup via GetGlobalMeta failed: %s", e)
80
- return _PUBLIC_UPLOAD_HOST
94
+ raise SessionError(
95
+ "This server did not publish a file upload address, so the file "
96
+ "cannot be uploaded. Check that you are signed in to the right "
97
+ "server, then try again."
98
+ )
81
99
 
82
100
 
83
101
  def get_upload_token(