hap-cli 0.9.0__tar.gz → 0.9.1__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 (413) hide show
  1. {hap_cli-0.9.0 → hap_cli-0.9.1}/PKG-INFO +2 -1
  2. {hap_cli-0.9.0/hap_cli → hap_cli-0.9.1}/README.md +1 -0
  3. {hap_cli-0.9.0 → hap_cli-0.9.1/hap_cli}/README.md +1 -0
  4. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/README_CN.md +1 -0
  5. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/__init__.py +1 -1
  6. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/attachment_cmd.py +38 -0
  7. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chat_cmd.py +1 -0
  8. hap_cli-0.9.1/hap_cli/core/attachment.py +388 -0
  9. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chat.py +103 -13
  10. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet.py +37 -5
  11. hap_cli-0.9.1/hap_cli/tests/test_attachment_share.py +434 -0
  12. hap_cli-0.9.1/hap_cli/tests/test_chat_messages_cli.py +28 -0
  13. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_core.py +107 -1
  14. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fields_mdpublic_parity.py +48 -0
  15. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_full_e2e.py +6 -0
  16. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/PKG-INFO +2 -1
  17. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/SOURCES.txt +1 -0
  18. hap_cli-0.9.0/hap_cli/core/attachment.py +0 -176
  19. hap_cli-0.9.0/hap_cli/tests/test_attachment_share.py +0 -148
  20. {hap_cli-0.9.0 → hap_cli-0.9.1}/MANIFEST.in +0 -0
  21. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/__init__.py +0 -0
  22. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/__main__.py +0 -0
  23. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/config.py +0 -0
  24. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/fixtures/__init__.py +0 -0
  25. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/fixtures/feedback_scenario.py +0 -0
  26. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/harness.py +0 -0
  27. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/smoke.py +0 -0
  28. {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/state.py +0 -0
  29. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/__main__.py +0 -0
  30. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/_entry.py +0 -0
  31. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/__init__.py +0 -0
  32. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_cmd.py +0 -0
  33. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_creator_cmd.py +0 -0
  34. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_editor_cmd.py +0 -0
  35. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/auth_cmd.py +0 -0
  36. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/calendar_cmd.py +0 -0
  37. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chart_cmd.py +0 -0
  38. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chatbot_cmd.py +0 -0
  39. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/config_cmd.py +0 -0
  40. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/contact_cmd.py +0 -0
  41. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/department_cmd.py +0 -0
  42. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/group_cmd.py +0 -0
  43. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/guide_cmd.py +0 -0
  44. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/icon_cmd.py +0 -0
  45. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/instance_cmd.py +0 -0
  46. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/node_cmd.py +0 -0
  47. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/optionset_cmd.py +0 -0
  48. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/page_cmd.py +0 -0
  49. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/post_cmd.py +0 -0
  50. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/record_cmd.py +0 -0
  51. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/region_cmd.py +0 -0
  52. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/role_cmd.py +0 -0
  53. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/update_cmd.py +0 -0
  54. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/upload_cmd.py +0 -0
  55. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/v3_registry.py +0 -0
  56. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/workflow_cmd.py +0 -0
  57. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/worksheet_cmd.py +0 -0
  58. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/context.py +0 -0
  59. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/__init__.py +0 -0
  60. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/action_spec_adapter.py +0 -0
  61. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app.py +0 -0
  62. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/__init__.py +0 -0
  63. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  64. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  65. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
  66. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/charts.py +0 -0
  67. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/cleanup.py +0 -0
  68. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/compiler.py +0 -0
  69. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/config.py +0 -0
  70. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  71. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/errors.py +0 -0
  72. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  73. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/executor.py +0 -0
  74. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/fields.py +0 -0
  75. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/hap.py +0 -0
  76. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/merge.py +0 -0
  77. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  78. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/console.py +0 -0
  79. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  80. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  81. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/report.py +0 -0
  82. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/schema.py +0 -0
  83. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  84. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  85. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/cli.py +0 -0
  86. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/executor.py +0 -0
  87. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  88. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  89. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  90. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  91. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/template.py +0 -0
  92. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/selftest.py +0 -0
  93. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/smoke.py +0 -0
  94. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/steps.py +0 -0
  95. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/store.py +0 -0
  96. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/validate.py +0 -0
  97. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  98. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/auth.py +0 -0
  99. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/calendar_mod.py +0 -0
  100. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chart.py +0 -0
  101. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chatbot.py +0 -0
  102. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/cli_access.py +0 -0
  103. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/condition_dsl.py +0 -0
  104. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/contact.py +0 -0
  105. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/control_type_codes.py +0 -0
  106. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/department.py +0 -0
  107. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/__init__.py +0 -0
  108. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  109. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  110. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/apply.py +0 -0
  111. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/componentlower.py +0 -0
  112. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/component.schema.json +0 -0
  113. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
  114. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
  115. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/field.schema.json +0 -0
  116. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec.py +0 -0
  117. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/errors.py +0 -0
  118. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/fieldlower.py +0 -0
  119. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  120. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/models.py +0 -0
  121. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/ops.py +0 -0
  122. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/planner.py +0 -0
  123. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/reader.py +0 -0
  124. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/field_normalizer.py +0 -0
  125. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/field_spec_adapter.py +0 -0
  126. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/filter_translator.py +0 -0
  127. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/filter_v3.py +0 -0
  128. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/flow_node.py +0 -0
  129. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/global_meta.py +0 -0
  130. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/group.py +0 -0
  131. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/guide.py +0 -0
  132. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/icon_index.py +0 -0
  133. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/instance.py +0 -0
  134. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/layout_check.py +0 -0
  135. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/logger.py +0 -0
  136. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/optionset.py +0 -0
  137. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/page.py +0 -0
  138. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/post.py +0 -0
  139. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record.py +0 -0
  140. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_defaults.py +0 -0
  141. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_form_checks.py +0 -0
  142. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_lookups.py +0 -0
  143. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_rules.py +0 -0
  144. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/relation_scope.py +0 -0
  145. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/response_crypto.py +0 -0
  146. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/role.py +0 -0
  147. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/role_perm_builder.py +0 -0
  148. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/rule_spec_adapter.py +0 -0
  149. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/session.py +0 -0
  150. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/token_crypto.py +0 -0
  151. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/update.py +0 -0
  152. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/upload.py +0 -0
  153. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/__init__.py +0 -0
  154. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/dispatcher.py +0 -0
  155. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/render.py +0 -0
  156. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/schema.py +0 -0
  157. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/view_spec_adapter.py +0 -0
  158. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/workflow.py +0 -0
  159. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/workflow_node_dsl.py +0 -0
  160. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_form.py +0 -0
  161. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_reference.py +0 -0
  162. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_templates.py +0 -0
  163. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/approval.md +0 -0
  164. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/chart.md +0 -0
  165. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/messages.md +0 -0
  166. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/overview.md +0 -0
  167. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/post.md +0 -0
  168. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/record.md +0 -0
  169. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/setup.md +0 -0
  170. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/skills.md +0 -0
  171. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/workflow.md +0 -0
  172. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/worksheet.md +0 -0
  173. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/index.json +0 -0
  174. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/approval.md +0 -0
  175. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/chart.md +0 -0
  176. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/messages.md +0 -0
  177. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/overview.md +0 -0
  178. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/post.md +0 -0
  179. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/record.md +0 -0
  180. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/setup.md +0 -0
  181. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/skills.md +0 -0
  182. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/workflow.md +0 -0
  183. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/worksheet.md +0 -0
  184. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/i18n.py +0 -0
  185. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/__init__.py +0 -0
  186. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/messages.json +0 -0
  187. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/messages.schema.json +0 -0
  188. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/__init__.py +0 -0
  189. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/conftest.py +0 -0
  190. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_action_mdpublic_parity.py +0 -0
  191. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_advanced_setting_guard.py +0 -0
  192. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_creator.py +0 -0
  193. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_editor.py +0 -0
  194. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_export_usage.py +0 -0
  195. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_light_color.py +0 -0
  196. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_section_name.py +0 -0
  197. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_approval_submit_fill_cli.py +0 -0
  198. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_approval_wire.py +0 -0
  199. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_atomic_write.py +0 -0
  200. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_auth_prerelease.py +0 -0
  201. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_batch_add_atomic.py +0 -0
  202. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_bug_036430.py +0 -0
  203. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_bug_036750.py +0 -0
  204. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_calendar_date_range.py +0 -0
  205. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_calendar_discussion_cli.py +0 -0
  206. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chart.py +0 -0
  207. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chart_filter_merge.py +0 -0
  208. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_attachment_send.py +0 -0
  209. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_recall_cli.py +0 -0
  210. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_send_socket.py +0 -0
  211. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_cli_access.py +0 -0
  212. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_coded_responses.py +0 -0
  213. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl.py +0 -0
  214. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl_entries.py +0 -0
  215. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl_readback.py +0 -0
  216. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_config_cmd.py +0 -0
  217. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_content_language.py +0 -0
  218. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_control_lookup_guard.py +0 -0
  219. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  220. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_dsl_existing_node_refs.py +0 -0
  221. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_dsl_multi_record_guard.py +0 -0
  222. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_engine_core_parity.py +0 -0
  223. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_arg_consistency.py +0 -0
  224. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_bidirectional.py +0 -0
  225. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_discussion_mentions.py +0 -0
  226. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_entry_module.py +0 -0
  227. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_field_record_ref.py +0 -0
  228. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_http_error_body.py +0 -0
  229. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_json_anywhere.py +0 -0
  230. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_json_file_arg.py +0 -0
  231. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_layout_check.py +0 -0
  232. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_node_get_detail.py +0 -0
  233. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_node_summary.py +0 -0
  234. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_post_cursor.py +0 -0
  235. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_publish_reasons.py +0 -0
  236. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_replicate_workflow.py +0 -0
  237. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_save_fields_shape.py +0 -0
  238. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_subgroups.py +0 -0
  239. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_subtable_read.py +0 -0
  240. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_references.py +0 -0
  241. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_visibility.py +0 -0
  242. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_workflows.py +0 -0
  243. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_chart_parity.py +0 -0
  244. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_record_parity.py +0 -0
  245. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_upload_parity.py +0 -0
  246. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_worksheet_parity.py +0 -0
  247. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g2_coverage_parity.py +0 -0
  248. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g3_workflow_parity.py +0 -0
  249. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g4_approval_parity.py +0 -0
  250. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g5_low_frequency_parity.py +0 -0
  251. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g6_advanced_fields.py +0 -0
  252. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g6_fields_roundtrip.py +0 -0
  253. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g7_misc_gaps.py +0 -0
  254. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g7b_permanent_delete.py +0 -0
  255. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8_filter_operator.py +0 -0
  256. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8b_pivot_operators.py +0 -0
  257. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8c_empty_filter_and_help_truth.py +0 -0
  258. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g9_appid_resolve.py +0 -0
  259. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_global_meta.py +0 -0
  260. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_guide.py +0 -0
  261. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_guide_sections.py +0 -0
  262. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_i18n.py +0 -0
  263. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_inner_flow_batch_add.py +0 -0
  264. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_instance_envelope.py +0 -0
  265. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_instance_history_cli.py +0 -0
  266. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration.py +0 -0
  267. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_approval.py +0 -0
  268. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  269. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_calendar.py +0 -0
  270. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_destructive.py +0 -0
  271. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_misc.py +0 -0
  272. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_post.py +0 -0
  273. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_social.py +0 -0
  274. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_v3.py +0 -0
  275. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_workflow.py +0 -0
  276. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  277. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_jsonschema_mini_copy.py +0 -0
  278. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
  279. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_multi_profile.py +0 -0
  280. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
  281. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_noninteractive_confirm.py +0 -0
  282. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  283. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_id_cli.py +0 -0
  284. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_id_docs.py +0 -0
  285. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_filters_group_read.py +0 -0
  286. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_filters_group_update.py +0 -0
  287. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_save_page_id.py +0 -0
  288. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_parameter_conventions.py +0 -0
  289. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  290. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_post_scope_cli.py +0 -0
  291. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  292. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_business_rules.py +0 -0
  293. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_defaults.py +0 -0
  294. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_doc_id_hidden.py +0 -0
  295. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_field_alias.py +0 -0
  296. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_form_checks.py +0 -0
  297. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_run_action.py +0 -0
  298. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  299. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_write_fixes.py +0 -0
  300. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_relation_scope.py +0 -0
  301. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_rmw_mdpublic_parity.py +0 -0
  302. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_role_app_scope.py +0 -0
  303. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_rule_filters_and_field_warning.py +0 -0
  304. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_save_action_silent_drop.py +0 -0
  305. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_save_rule_spec.py +0 -0
  306. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_select_add_option.py +0 -0
  307. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
  308. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  309. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_subtable_mdpublic_parity.py +0 -0
  310. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_update.py +0 -0
  311. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_update_mode.py +0 -0
  312. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_allow_down.py +0 -0
  313. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_host_resolution.py +0 -0
  314. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_mdpublic_parity.py +0 -0
  315. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  316. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_bool_flags.py +0 -0
  317. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  318. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  319. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_response_format_json.py +0 -0
  320. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_session.py +0 -0
  321. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  322. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_view_mdpublic_parity.py +0 -0
  323. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_config_keys.py +0 -0
  324. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_inner_scope.py +0 -0
  325. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_json_file_arg.py +0 -0
  326. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_node_delete_missing.py +0 -0
  327. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_save_action_dsl_fields.py +0 -0
  328. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_structure_listing.py +0 -0
  329. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_trigger_outcome.py +0 -0
  330. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_trigger_preserve.py +0 -0
  331. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wo4_result_handling.py +0 -0
  332. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_code_node.py +0 -0
  333. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_condition_field_types.py +0 -0
  334. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_condition_slots.py +0 -0
  335. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_delete_cascade.py +0 -0
  336. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
  337. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_lifecycle_wire.py +0 -0
  338. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  339. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_node_wire.py +0 -0
  340. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_publish_inner.py +0 -0
  341. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
  342. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  343. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_create_section.py +0 -0
  344. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  345. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_filter_read.py +0 -0
  346. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_switch.py +0 -0
  347. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  348. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/__init__.py +0 -0
  349. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/atomic_write.py +0 -0
  350. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/confirm.py +0 -0
  351. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/formatting.py +0 -0
  352. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/options.py +0 -0
  353. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/parameter_mapping.py +0 -0
  354. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/dependency_links.txt +0 -0
  355. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/entry_points.txt +0 -0
  356. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/requires.txt +0 -0
  357. {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/top_level.txt +0 -0
  358. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/__init__.py +0 -0
  359. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/__main__.py +0 -0
  360. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/_wftest.py +0 -0
  361. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/charts.py +0 -0
  362. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/cleanup.py +0 -0
  363. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/compiler.py +0 -0
  364. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/config.py +0 -0
  365. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/errors.py +0 -0
  366. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/executor.py +0 -0
  367. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/fields.py +0 -0
  368. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/hap.py +0 -0
  369. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/__init__.py +0 -0
  370. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/console.py +0 -0
  371. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/jsonl.py +0 -0
  372. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/mirror.py +0 -0
  373. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/report.py +0 -0
  374. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/schema.py +0 -0
  375. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/__init__.py +0 -0
  376. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/cli.py +0 -0
  377. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/executor.py +0 -0
  378. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/template.py +0 -0
  379. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/selftest.py +0 -0
  380. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/smoke.py +0 -0
  381. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/step.py +0 -0
  382. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/steps.py +0 -0
  383. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/store.py +0 -0
  384. {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/workflow_dsl.py +0 -0
  385. {hap_cli-0.9.0 → hap_cli-0.9.1}/setup.cfg +0 -0
  386. {hap_cli-0.9.0 → hap_cli-0.9.1}/setup.py +0 -0
  387. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/INDEX.json +0 -0
  388. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  389. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  390. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/create_optionset.yaml +0 -0
  391. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/create_role.yaml +0 -0
  392. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  393. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_record.yaml +0 -0
  394. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_role.yaml +0 -0
  395. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  396. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_info.yaml +0 -0
  397. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  398. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  399. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  400. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_details.yaml +0 -0
  401. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  402. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_list.yaml +0 -0
  403. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  404. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  405. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  406. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  407. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_regions.yaml +0 -0
  408. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_role_details.yaml +0 -0
  409. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_role_list.yaml +0 -0
  410. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  411. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  412. {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  413. {hap_cli-0.9.0 → hap_cli-0.9.1}/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.9.0
3
+ Version: 0.9.1
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -312,6 +312,7 @@ discover every subcommand.
312
312
 
313
313
  | Command | Description |
314
314
  |---|---|
315
+ | `attachment download FILE_ID --worksheet-id WS --row-id ROW --control-id FIELD [-o PATH]` | Download a worksheet-record attachment with the same record and field permission checks as the web app; existing local files are never overwritten |
315
316
  | `attachment share [ADDRESS]` | Create a link to a stored file that you can pass on (`--hours`, default 48; `--file-id` to share by attachment id, `--short` for a landing page instead of the storage address) |
316
317
 
317
318
  > ADDRESS is the file's link — one that has already stopped working is fine, paste it as-is — or just the path part of it. The file itself does not expire, only the link does, so a dead link can always be revived.
@@ -283,6 +283,7 @@ discover every subcommand.
283
283
 
284
284
  | Command | Description |
285
285
  |---|---|
286
+ | `attachment download FILE_ID --worksheet-id WS --row-id ROW --control-id FIELD [-o PATH]` | Download a worksheet-record attachment with the same record and field permission checks as the web app; existing local files are never overwritten |
286
287
  | `attachment share [ADDRESS]` | Create a link to a stored file that you can pass on (`--hours`, default 48; `--file-id` to share by attachment id, `--short` for a landing page instead of the storage address) |
287
288
 
288
289
  > ADDRESS is the file's link — one that has already stopped working is fine, paste it as-is — or just the path part of it. The file itself does not expire, only the link does, so a dead link can always be revived.
@@ -283,6 +283,7 @@ discover every subcommand.
283
283
 
284
284
  | Command | Description |
285
285
  |---|---|
286
+ | `attachment download FILE_ID --worksheet-id WS --row-id ROW --control-id FIELD [-o PATH]` | Download a worksheet-record attachment with the same record and field permission checks as the web app; existing local files are never overwritten |
286
287
  | `attachment share [ADDRESS]` | Create a link to a stored file that you can pass on (`--hours`, default 48; `--file-id` to share by attachment id, `--short` for a landing page instead of the storage address) |
287
288
 
288
289
  > ADDRESS is the file's link — one that has already stopped working is fine, paste it as-is — or just the path part of it. The file itself does not expire, only the link does, so a dead link can always be revived.
@@ -262,6 +262,7 @@ hap --json worksheet record list WORKSHEET_ID
262
262
 
263
263
  | 命令 | 说明 |
264
264
  |---|---|
265
+ | `attachment download FILE_ID --worksheet-id WS --row-id ROW --control-id FIELD [-o PATH]` | 按网页端相同的记录和字段权限校验下载工作表记录附件;不会覆盖已有本地文件 |
265
266
  | `attachment share [ADDRESS]` | 为已存储的文件生成一条可以转给别人的链接(`--hours`,默认 48 小时;`--file-id` 按附件 ID 分享,`--short` 返回文件介绍页而非存储地址) |
266
267
 
267
268
  > ADDRESS 传文件的链接即可——**已经失效的链接照样能传,原样粘贴**——也可以只传路径部分。文件本身不会过期,过期的只是链接,所以失效链接随时可以重新生成。
@@ -3,7 +3,7 @@
3
3
  import sys as _sys
4
4
  import types as _types
5
5
 
6
- __version__ = "0.9.0"
6
+ __version__ = "0.9.1"
7
7
 
8
8
  # The command tree used to live in hap_cli/hap_cli.py. A module named
9
9
  # after the package it sits in shadows that package: run python with the
@@ -1,5 +1,7 @@
1
1
  """CLI commands for files stored as attachments."""
2
2
 
3
+ from pathlib import Path
4
+
3
5
  import click
4
6
 
5
7
  from hap_cli.context import pass_context
@@ -55,7 +57,43 @@ def attachment_share(ctx, address, hours, file_id, short, display_name, size):
55
57
  ctx.handle_error(e)
56
58
 
57
59
 
60
+ @attachment.command("download")
61
+ @click.argument("file_id")
62
+ @click.option("--worksheet-id", required=True, help="Worksheet containing the record.")
63
+ @click.option("--row-id", required=True, help="Record containing the attachment.")
64
+ @click.option("--control-id", required=True, help="Attachment field containing the file.")
65
+ @click.option(
66
+ "--output", "-o", "output", type=click.Path(path_type=Path, dir_okay=False),
67
+ default=None, help="Save as this path. By default, uses the attachment name.",
68
+ )
69
+ @pass_context
70
+ def attachment_download(ctx, file_id, worksheet_id, row_id, control_id, output):
71
+ """Download a file attached to a worksheet record.
72
+
73
+ FILE_ID is listed in an attachment field or a record operation log.
74
+ Existing local files are never overwritten.
75
+ """
76
+ try:
77
+ session = ctx.get_session()
78
+ data = attachment_mod.download_attachment(
79
+ session,
80
+ file_id,
81
+ worksheet_id=worksheet_id,
82
+ row_id=row_id,
83
+ control_id=control_id,
84
+ output=output,
85
+ )
86
+ ctx.output(data, _download_human)
87
+ except Exception as e:
88
+ ctx.handle_error(e)
89
+
90
+
58
91
  def _human(d):
59
92
  click.echo(d["url"])
60
93
  if d.get("expiresAt"):
61
94
  click.echo(f"Usable until {d['expiresAt']}")
95
+
96
+
97
+ def _download_human(d):
98
+ click.echo(d["path"])
99
+ click.echo(f"{d['bytes']} bytes")
@@ -246,6 +246,7 @@ def chat_messages(
246
246
  page_index=page,
247
247
  page_size=num,
248
248
  category=category,
249
+ keyword=keyword,
249
250
  )
250
251
  if mark_read and category:
251
252
  chat_mod.mark_inbox_read(session, category)
@@ -0,0 +1,388 @@
1
+ """Attachments in the file store: shareable links for stored files.
2
+
3
+ The address handed out at upload time is a private-bucket signature that
4
+ expires within the hour (``upload.url_expires_at``). The file behind it
5
+ does not expire — the object path *is* the URL path — so a dead link is
6
+ re-signable at any time.
7
+
8
+ ``Attachment/GetShareLocalAttachmentUrl`` is what the web UI's
9
+ "分享附件" action calls: pd-openweb
10
+ src/components/shareAttachment/toMobile.js:121 passes
11
+ ``{filePath: file.qiniuPath, hours: 48}``, where ``qiniuPath`` is built
12
+ as ``window.config.FilePath + file.key``
13
+ (src/pages/chat/components/Message/MessageToolbar/index.js:222).
14
+
15
+ That prefix is not decorative. Verified live on 2026-08-31, the server
16
+ accepts the ``config.FilePath`` form and nothing else — a bare key, a
17
+ ``documentHost``-prefixed URL, or the original signed URL with its query
18
+ intact each come back as an empty string with no error state. So this
19
+ module normalizes any address the user has to hand down to a key and
20
+ rebuilds the one prefix that works.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import os
26
+ from pathlib import Path
27
+ import tempfile
28
+ from typing import Any, Optional
29
+ from urllib.parse import urlencode, urlparse, urlunparse
30
+
31
+ import requests
32
+
33
+ from hap_cli.core.session import APIError, Session, SessionError
34
+ from hap_cli.utils.parameter_mapping import parameter_mapping
35
+
36
+ logger = logging.getLogger("hap_cli.attachment")
37
+
38
+ # What the web UI uses when it shares an attachment (toMobile.js:123).
39
+ DEFAULT_SHARE_HOURS = 48
40
+
41
+
42
+ def _same_server_download_headers(session: Session, url: str) -> dict[str, str]:
43
+ """Browser-equivalent auth for HAP-hosted downloads, never for a CDN."""
44
+ def _origin(address: str) -> Optional[tuple[str, str, int]]:
45
+ parsed = urlparse(address)
46
+ scheme = parsed.scheme.casefold()
47
+ if scheme not in {"http", "https"} or not parsed.hostname:
48
+ return None
49
+ port = parsed.port or (443 if scheme == "https" else 80)
50
+ return scheme, parsed.hostname.casefold(), port
51
+
52
+ target_origin = _origin(url)
53
+ matched_address = next(
54
+ (
55
+ candidate
56
+ for candidate in (session.login_url, session.server_url)
57
+ if candidate and _origin(candidate) == target_origin
58
+ ),
59
+ None,
60
+ )
61
+ if target_origin is None or matched_address is None:
62
+ return {}
63
+ origin_parsed = urlparse(matched_address)
64
+ origin = f"{origin_parsed.scheme}://{origin_parsed.netloc}"
65
+ headers = {
66
+ "Authorization": f"md_pss_id {session.auth_token}",
67
+ "Origin": origin,
68
+ "Referer": origin + "/",
69
+ "X-Requested-With": "XMLHttpRequest",
70
+ }
71
+ if session.account_id:
72
+ headers["AccountId"] = session.account_id
73
+ return headers
74
+
75
+
76
+ def _download_name(detail: dict[str, Any], file_id: str) -> str:
77
+ """Choose a safe local filename from attachment metadata."""
78
+ def _without_controls(value: Any) -> str:
79
+ return "".join(
80
+ char for char in str(value)
81
+ if ord(char) >= 32 and not 127 <= ord(char) <= 159
82
+ )
83
+
84
+ fallback = Path(_without_controls(file_id).replace("\\", "/")).name
85
+ if fallback in {"", ".", ".."}:
86
+ fallback = "attachment"
87
+ raw_name = _without_controls(
88
+ detail.get("originalFilename")
89
+ or detail.get("originalFileName")
90
+ or detail.get("fileName")
91
+ or fallback
92
+ )
93
+ name = Path(raw_name.replace("\\", "/")).name
94
+ if name in {"", ".", ".."}:
95
+ name = fallback
96
+ raw_ext = _without_controls(detail.get("fileExt") or detail.get("ext") or "")
97
+ ext = Path(raw_ext.replace("\\", "/")).name
98
+ if ext in {"", ".", ".."}:
99
+ ext = ""
100
+ if ext and not ext.startswith("."):
101
+ ext = "." + ext
102
+ if ext and not name.lower().endswith(ext.lower()):
103
+ name += ext
104
+ safe_name = Path(name.replace("\\", "/")).name
105
+ return safe_name if safe_name not in {"", ".", ".."} else fallback
106
+
107
+
108
+ @parameter_mapping("attachment.download.record-context")
109
+ def download_attachment(
110
+ session: Session,
111
+ file_id: str,
112
+ *,
113
+ worksheet_id: str,
114
+ row_id: str,
115
+ control_id: str,
116
+ output: Optional[Path] = None,
117
+ output_dir: Optional[Path] = None,
118
+ ) -> dict[str, Any]:
119
+ """Download one worksheet-record attachment to a new local file.
120
+
121
+ The web UI first calls ``Attachment/GetAttachmentDetail`` with the full
122
+ record context, then downloads its signed ``downloadUrl`` (MDPublic
123
+ ``src/pages/worksheet/controllers/record.js``
124
+ ``downloadAttachmentById``). Keeping those identifiers in the request
125
+ preserves the same record/field permission check as the UI.
126
+ """
127
+ if not file_id:
128
+ raise ValueError("file_id is required")
129
+ if not worksheet_id or not row_id or not control_id:
130
+ raise ValueError(
131
+ "worksheet_id, row_id, and control_id are required for a record attachment"
132
+ )
133
+
134
+ # Refuse before any network request when the caller named an existing path.
135
+ target = Path(output).expanduser() if output is not None else None
136
+ if target is not None and target.exists():
137
+ raise FileExistsError(f"Refusing to overwrite existing file: {target}")
138
+
139
+ detail = session.api_call(
140
+ "Attachment",
141
+ "GetAttachmentDetail",
142
+ {
143
+ "fileId": file_id,
144
+ "worksheetId": worksheet_id,
145
+ "rowId": row_id,
146
+ "controlId": control_id,
147
+ },
148
+ )
149
+ if not isinstance(detail, dict) or not detail.get("downloadUrl"):
150
+ raise APIError(
151
+ "The attachment has no downloadable address. Check that it still "
152
+ "exists and that you can download files from this record."
153
+ )
154
+
155
+ if target is None:
156
+ directory = Path(output_dir or Path.cwd()).expanduser().resolve()
157
+ target = directory / _download_name(detail, file_id)
158
+ target = target.resolve()
159
+ if target.exists():
160
+ raise FileExistsError(f"Refusing to overwrite existing file: {target}")
161
+ if not target.parent.is_dir():
162
+ raise FileNotFoundError(f"Output directory does not exist: {target.parent}")
163
+
164
+ url = str(detail["downloadUrl"])
165
+ parsed_download = urlparse(url)
166
+ log_query = urlencode({"controlId": control_id, "rowId": row_id})
167
+ url = urlunparse(parsed_download._replace(
168
+ query="&".join(part for part in (parsed_download.query, log_query) if part)
169
+ ))
170
+ candidates = [url]
171
+ private_url = str(detail.get("privateDownloadUrl") or "")
172
+ if private_url and private_url != url:
173
+ candidates.append(private_url)
174
+
175
+ response = None
176
+ for index, candidate in enumerate(candidates):
177
+ parsed = urlparse(candidate)
178
+ safe_url = urlunparse(parsed._replace(query="", fragment=""))
179
+ logger.info("HAP GET %s stream=true", safe_url)
180
+ download_headers = (
181
+ _same_server_download_headers(session, candidate)
182
+ if index == 0 else {}
183
+ )
184
+ request_kwargs: dict[str, Any] = {"stream": True, "timeout": 60}
185
+ if download_headers:
186
+ request_kwargs["headers"] = download_headers
187
+ response = requests.get(candidate, **request_kwargs)
188
+ logger.info(
189
+ "HAP GET %s -> %s content-length=%s",
190
+ safe_url,
191
+ response.status_code,
192
+ response.headers.get("Content-Length", ""),
193
+ )
194
+ try:
195
+ response.raise_for_status()
196
+ except requests.HTTPError:
197
+ response.close()
198
+ if response.status_code == 403 and index + 1 < len(candidates):
199
+ logger.warning(
200
+ "HAP download route refused the signed redirect; "
201
+ "retrying the private attachment URL",
202
+ )
203
+ continue
204
+ raise
205
+ break
206
+
207
+ if response is None: # pragma: no cover - candidates always has url
208
+ raise APIError("The attachment has no downloadable address.")
209
+
210
+ written = 0
211
+ temp_path: Optional[Path] = None
212
+ try:
213
+ fd, temp_name = tempfile.mkstemp(
214
+ dir=target.parent,
215
+ prefix=f".{target.name}.",
216
+ suffix=".part",
217
+ )
218
+ temp_path = Path(temp_name)
219
+ with os.fdopen(fd, "wb") as handle:
220
+ for chunk in response.iter_content(chunk_size=1024 * 1024):
221
+ if chunk:
222
+ handle.write(chunk)
223
+ written += len(chunk)
224
+ handle.flush()
225
+ os.fsync(handle.fileno())
226
+
227
+ # Hard-linking within the same directory publishes the fully-written
228
+ # file atomically and fails instead of replacing a path that appeared
229
+ # during the download.
230
+ os.link(temp_path, target)
231
+ temp_path.unlink()
232
+ temp_path = None
233
+ except BaseException:
234
+ if temp_path is not None and temp_path.exists():
235
+ temp_path.unlink()
236
+ raise
237
+ finally:
238
+ response.close()
239
+
240
+ return {
241
+ "fileId": file_id,
242
+ "name": target.name,
243
+ "path": str(target),
244
+ "bytes": written,
245
+ }
246
+
247
+
248
+ def store_key(address: str) -> str:
249
+ """Reduce any form of a stored file's address to its store key.
250
+
251
+ Accepts a full URL on any of the store's hosts (with or without the
252
+ ``?e=…&token=…`` signature still attached — a dead link pasted out of
253
+ a chat message is the common case), a leading-slash path, or a bare
254
+ key.
255
+ """
256
+ if not address or not address.strip():
257
+ raise ValueError("a file address or key is required")
258
+ address = address.strip()
259
+ if "://" in address:
260
+ address = urlparse(address).path
261
+ else:
262
+ # A bare key may still carry a query if it came from a copied link.
263
+ address = address.split("?", 1)[0]
264
+ return address.lstrip("/")
265
+
266
+
267
+ def _file_path_prefix(session: Session) -> str:
268
+ """The store prefix this endpoint requires in ``filePath``.
269
+
270
+ Bootstrap publishes it as ``config.FilePath`` — the same value chat
271
+ file addresses are built from (``chat.chat_file_host``).
272
+ """
273
+ meta = session.get_global_meta()
274
+ for scope in (meta.get("config"), meta):
275
+ if isinstance(scope, dict) and scope.get("FilePath"):
276
+ return scope["FilePath"]
277
+ raise SessionError(
278
+ "This server did not publish a file address, so a link cannot be "
279
+ "created. Check that you are signed in to the right server, then "
280
+ "try again."
281
+ )
282
+
283
+
284
+ def share_url(
285
+ session: Session,
286
+ address: str = "",
287
+ *,
288
+ hours: int = DEFAULT_SHARE_HOURS,
289
+ file_id: str = "",
290
+ short: bool = False,
291
+ display_name: str = "",
292
+ size: Optional[int] = None,
293
+ ) -> dict[str, Any]:
294
+ """Create a fresh, time-limited link to a file already in the store.
295
+
296
+ ``address`` is anything that identifies the file: its current or
297
+ expired link, or its bare key. ``file_id`` is the other way to name
298
+ one — a knowledge-centre attachment is shared by its id and nothing
299
+ else, which is the shape the share dialog uses for that kind
300
+ (MDPublic src/components/shareAttachment/MobileShareDialog.jsx:64;
301
+ pd-openweb toMobile.js has the same branch).
302
+
303
+ With ``short``, the answer is a short link to a landing page instead
304
+ of the signed store address itself — what the product hands a person
305
+ (MobileShareDialog.jsx:37-59). ``display_name`` and ``size`` are what
306
+ the landing page shows; without them it falls back to the file's own
307
+ key and shows no size.
308
+
309
+ Returns ``{url, expiresAt, hours, key}``, plus ``storeUrl`` when the
310
+ link was shortened.
311
+ """
312
+ from hap_cli.core.upload import _iso, url_expires_at
313
+
314
+ if hours <= 0:
315
+ raise ValueError("hours must be a positive number")
316
+ if not address and not file_id:
317
+ raise ValueError("a file address, key, or file id is required")
318
+ key = store_key(address) if address else ""
319
+ if file_id:
320
+ args: dict[str, Any] = {"fileID": file_id}
321
+ else:
322
+ args = {"filePath": _file_path_prefix(session) + key, "hours": hours}
323
+ url = session.api_call("Attachment", "GetShareLocalAttachmentUrl", args)
324
+ if not url or not isinstance(url, str):
325
+ # An unknown or unreachable file is answered with an empty string
326
+ # and a success state, so there is nothing to detect but this.
327
+ logger.warning("GetShareLocalAttachmentUrl returned %r for key=%s",
328
+ url, key or file_id)
329
+ raise APIError(
330
+ "Could not create a link for that file. Check the address is "
331
+ "complete and that the file still exists."
332
+ )
333
+ out = {
334
+ "url": url,
335
+ "expiresAt": _iso(url_expires_at(url)),
336
+ "hours": hours,
337
+ "key": key,
338
+ }
339
+ if short:
340
+ out["storeUrl"] = url
341
+ out["url"] = _short_landing_link(
342
+ session, url, display_name=display_name or key, size=size)
343
+ return out
344
+
345
+
346
+ # Where the landing page lives, relative to the web host.
347
+ _LANDING_PATH = "/apps/kc/shareLocalAttachment.aspx"
348
+
349
+
350
+ def _short_landing_link(session: Session, signed_url: str, *,
351
+ display_name: str, size: Optional[int]) -> str:
352
+ """Wrap a signed store address in the product's own landing page.
353
+
354
+ The product never hands out the store address directly: it splits the
355
+ signature off, rebuilds it as a landing-page URL carrying the file's
356
+ name, extension, size and the moment the link was made, and shortens
357
+ that (MDPublic src/components/shareAttachment/MobileShareDialog.jsx:
358
+ 37-59; pd-openweb toMobile.js:120-145 identical). So the recipient
359
+ gets a page about the file rather than the bucket's own address.
360
+
361
+ Measured 2026-09-09: ``Attachment/GetShortUrl`` echoes the URL back
362
+ unchanged on Mingdao SaaS, so what this reliably buys is the landing
363
+ page; whether the link is also shortened is up to the environment.
364
+ """
365
+ import os
366
+ import time
367
+ from urllib.parse import parse_qs, quote, urlencode, urlparse
368
+
369
+ parsed = urlparse(signed_url)
370
+ query = parse_qs(parsed.query)
371
+ bare = signed_url.split("?", 1)[0]
372
+ stem, ext = os.path.splitext(display_name or parsed.path.rsplit("/", 1)[-1])
373
+ params = {
374
+ "qiniuPath": bare,
375
+ "qiniutoken": (query.get("token") or [""])[0],
376
+ "e": (query.get("e") or [""])[0],
377
+ "name": stem,
378
+ "ext": ext.lstrip("."),
379
+ "size": "" if size is None else str(size),
380
+ "genTime": str(int(time.time() * 1000)),
381
+ }
382
+ landing = (session.server_url.rstrip("/") + _LANDING_PATH + "?"
383
+ + urlencode(params))
384
+ result = session.api_call("Attachment", "GetShortUrl",
385
+ {"url": quote(landing, safe="@*_+-./")})
386
+ if isinstance(result, dict):
387
+ return result.get("shortUrl") or signed_url
388
+ return result if isinstance(result, str) and result else signed_url
@@ -1,6 +1,7 @@
1
1
  """Chat and messaging management for Mingdao HAP."""
2
2
 
3
3
  import json
4
+ import html
4
5
  import logging
5
6
  import re
6
7
  from typing import Any, Optional
@@ -1821,6 +1822,58 @@ def _simplify_inbox_response(
1821
1822
  _INBOX_SERVER_PAGE1_PAGE_SIZE = 10
1822
1823
 
1823
1824
 
1825
+ @parameter_mapping("chat.messages.inbox-query")
1826
+ def _inbox_query(
1827
+ *,
1828
+ page_index: int,
1829
+ page_size: int,
1830
+ inbox_favorite: int,
1831
+ type_: str,
1832
+ creator_id: Optional[str],
1833
+ keyword: str,
1834
+ ) -> dict[str, Any]:
1835
+ """Translate the CLI inbox query to ``GetInboxMessage`` fields."""
1836
+ return {
1837
+ "pageIndex": page_index,
1838
+ "pageSize": page_size,
1839
+ "inboxFavorite": inbox_favorite,
1840
+ "type": type_,
1841
+ "loadType": 2,
1842
+ "creatorId": creator_id,
1843
+ "keywords": keyword,
1844
+ "clearUnread": False,
1845
+ }
1846
+
1847
+
1848
+ def _inbox_item_matches(item: dict[str, Any], keyword: str) -> bool:
1849
+ """Case-insensitive inbox search over user-facing text, not ids/URLs."""
1850
+ needle = keyword.casefold().strip()
1851
+ if not needle:
1852
+ return True
1853
+
1854
+ def _texts(value: Any, key: str = ""):
1855
+ normalized_key = key.casefold()
1856
+ if (
1857
+ normalized_key.endswith("id")
1858
+ or normalized_key.endswith("ids")
1859
+ or "url" in normalized_key
1860
+ or normalized_key in {"avatar", "createtime", "readtime"}
1861
+ ):
1862
+ return
1863
+ if isinstance(value, dict):
1864
+ for child_key, child_value in value.items():
1865
+ yield from _texts(child_value, str(child_key))
1866
+ elif isinstance(value, list):
1867
+ for child in value:
1868
+ yield from _texts(child, key)
1869
+ elif isinstance(value, str):
1870
+ # Message.content/rawContent may be server-rendered HTML. Search
1871
+ # visible labels, not hidden hrefs or other tag attributes.
1872
+ yield html.unescape(re.sub(r"<[^>]*>", "", value))
1873
+
1874
+ return any(needle in text.casefold() for text in _texts(item))
1875
+
1876
+
1824
1877
  def get_inbox_messages(
1825
1878
  session: Session,
1826
1879
  type_: str,
@@ -1829,6 +1882,7 @@ def get_inbox_messages(
1829
1882
  inbox_favorite: int = 0,
1830
1883
  creator_id: Optional[str] = None,
1831
1884
  category: Optional[str] = None,
1885
+ keyword: str = "",
1832
1886
  ) -> dict[str, Any]:
1833
1887
  """Fetch inbox messages for system / app / workflow / etc categories.
1834
1888
 
@@ -1844,13 +1898,21 @@ def get_inbox_messages(
1844
1898
  ``_INBOX_SERVER_PAGE1_FIXED`` for why this differs from the raw
1845
1899
  server pagination.
1846
1900
 
1847
- Two parameters intentionally **not** exposed because the server
1848
- ignores them:
1901
+ ``keyword`` maps to the API's plural ``keywords`` field. Unlike direct
1902
+ and group chat history, inbox notifications use the main-site API and
1903
+ therefore a different wire name. Current servers return unrelated rows
1904
+ even when that field is present, so the CLI scans raw pages and builds
1905
+ stable result pages by filtering locally over user-facing text.
1906
+
1907
+ One parameter intentionally **not** exposed because the server ignores it:
1849
1908
  - ``loadType`` — pd-openweb always sends 2 (ALL); 1 (UNREAD) is a
1850
1909
  no-op server-side, and the response items carry no per-item
1851
1910
  read/unread flag, so a client-side filter has nothing to key on.
1852
- - ``clearUnread`` — empirically does not affect the badge regardless
1853
- of true/false. Use ``mark_inbox_read()`` (socket emit) instead.
1911
+
1912
+ ``clearUnread`` must still be sent explicitly as false. Although an older
1913
+ server build appeared to ignore it, omitting the field now clears the badge
1914
+ in some environments. Actual mark-read behavior remains the separate
1915
+ socket event in ``mark_inbox_read()``.
1854
1916
  """
1855
1917
  if page_index < 1:
1856
1918
  raise ValueError("page_index must be >= 1")
@@ -1860,14 +1922,14 @@ def get_inbox_messages(
1860
1922
  def _call(server_page: int, server_size: int) -> list[dict[str, Any]]:
1861
1923
  raw = session.api_call(
1862
1924
  "Inbox", "GetInboxMessage",
1863
- {
1864
- "pageIndex": server_page,
1865
- "pageSize": server_size,
1866
- "inboxFavorite": inbox_favorite,
1867
- "type": type_,
1868
- "loadType": 2,
1869
- "creatorId": creator_id,
1870
- },
1925
+ _inbox_query(
1926
+ page_index=server_page,
1927
+ page_size=server_size,
1928
+ inbox_favorite=inbox_favorite,
1929
+ type_=type_,
1930
+ creator_id=creator_id,
1931
+ keyword=keyword,
1932
+ ),
1871
1933
  )
1872
1934
  if not isinstance(raw, dict):
1873
1935
  return []
@@ -1875,6 +1937,33 @@ def get_inbox_messages(
1875
1937
 
1876
1938
  items: list[dict[str, Any]] = []
1877
1939
 
1940
+ if keyword:
1941
+ # The documented `keywords` field is ignored by current servers.
1942
+ # Walk the raw 10-item segmentation from the beginning so CLI page N
1943
+ # means page N of *matching* rows, rather than a sparse filtered view
1944
+ # of raw page N.
1945
+ match_start = (page_index - 1) * page_size
1946
+ match_end = match_start + page_size
1947
+ matches: list[dict[str, Any]] = []
1948
+ server_page = 1
1949
+ while len(matches) < match_end:
1950
+ page_list = _call(server_page, _INBOX_SERVER_PAGE1_PAGE_SIZE)
1951
+ if not page_list:
1952
+ break
1953
+ simplified_page = _simplify_inbox_response({"inboxList": page_list})
1954
+ matches.extend(
1955
+ item for item in simplified_page["list"]
1956
+ if _inbox_item_matches(item, keyword)
1957
+ )
1958
+ if len(page_list) < _INBOX_SERVER_PAGE1_PAGE_SIZE:
1959
+ break
1960
+ server_page += 1
1961
+ result = _simplify_inbox_response(
1962
+ {"inboxList": []}, category=category, base_url=session.login_url
1963
+ )
1964
+ result["list"] = matches[match_start:match_end]
1965
+ return result
1966
+
1878
1967
  if page_index == 1:
1879
1968
  # Page 1 of the user's window: collect at 10-item boundaries (the
1880
1969
  # only segmentation upstream honors when pageIndex starts at 1)
@@ -1895,9 +1984,10 @@ def get_inbox_messages(
1895
1984
  items = _call(page_index, page_size)
1896
1985
 
1897
1986
  synthetic_raw = {"inboxList": items}
1898
- return _simplify_inbox_response(
1987
+ result = _simplify_inbox_response(
1899
1988
  synthetic_raw, category=category, base_url=session.login_url
1900
1989
  )
1990
+ return result
1901
1991
 
1902
1992
 
1903
1993
  def get_card_details(