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.
- {hap_cli-0.9.0 → hap_cli-0.9.1}/PKG-INFO +2 -1
- {hap_cli-0.9.0/hap_cli → hap_cli-0.9.1}/README.md +1 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1/hap_cli}/README.md +1 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/README_CN.md +1 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/__init__.py +1 -1
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/attachment_cmd.py +38 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chat_cmd.py +1 -0
- hap_cli-0.9.1/hap_cli/core/attachment.py +388 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chat.py +103 -13
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet.py +37 -5
- hap_cli-0.9.1/hap_cli/tests/test_attachment_share.py +434 -0
- hap_cli-0.9.1/hap_cli/tests/test_chat_messages_cli.py +28 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_core.py +107 -1
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fields_mdpublic_parity.py +48 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_full_e2e.py +6 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/PKG-INFO +2 -1
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/SOURCES.txt +1 -0
- hap_cli-0.9.0/hap_cli/core/attachment.py +0 -176
- hap_cli-0.9.0/hap_cli/tests/test_attachment_share.py +0 -148
- {hap_cli-0.9.0 → hap_cli-0.9.1}/MANIFEST.in +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/config.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/fixtures/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/fixtures/feedback_scenario.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/harness.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/app_live_tests/state.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/__main__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/_entry.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/guide_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/context.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/condition_dsl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/department.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/component.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/envelope.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec/field.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/filter_v3.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/group.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/guide.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/layout_check.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/page.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/post.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_defaults.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_form_checks.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_lookups.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/record_rules.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/relation_scope.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/role.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/rule_spec_adapter.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/session.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/update.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_form.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_reference.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/approval.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/chart.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/messages.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/overview.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/post.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/record.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/setup.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/skills.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/workflow.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/en/worksheet.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/index.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/approval.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/chart.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/messages.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/overview.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/post.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/record.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/setup.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/skills.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/workflow.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/guide/zh-Hans/worksheet.md +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/i18n.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_action_mdpublic_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_advanced_setting_guard.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_light_color.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_app_section_name.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_approval_submit_fill_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_approval_wire.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_atomic_write.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_batch_add_atomic.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_calendar_date_range.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_calendar_discussion_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chart_filter_merge.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_attachment_send.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_recall_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_chat_send_socket.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_coded_responses.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl_entries.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_condition_dsl_readback.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_content_language.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_control_lookup_guard.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_dsl_existing_node_refs.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_dsl_multi_record_guard.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_engine_core_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_arg_consistency.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_bidirectional.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_discussion_mentions.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_entry_module.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_field_record_ref.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_http_error_body.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_json_anywhere.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_json_file_arg.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_layout_check.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_node_get_detail.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_node_summary.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_post_cursor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_publish_reasons.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_replicate_workflow.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_save_fields_shape.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_subgroups.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_subtable_read.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_references.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_visibility.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_fb_worksheet_workflows.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_chart_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_record_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_upload_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g1_worksheet_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g2_coverage_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g3_workflow_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g4_approval_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g5_low_frequency_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g6_advanced_fields.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g6_fields_roundtrip.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g7_misc_gaps.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g7b_permanent_delete.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8_filter_operator.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8b_pivot_operators.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g8c_empty_filter_and_help_truth.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_g9_appid_resolve.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_guide.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_guide_sections.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_inner_flow_batch_add.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_instance_envelope.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_jsonschema_mini_copy.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_no_workflow_unsupported.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_noninteractive_confirm.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_filters_group_read.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_filters_group_update.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_page_save_page_id.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_post_scope_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_business_rules.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_defaults.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_doc_id_hidden.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_form_checks.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_run_action.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_record_write_fixes.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_relation_scope.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_rmw_mdpublic_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_role_app_scope.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_rule_filters_and_field_warning.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_save_action_silent_drop.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_save_rule_spec.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_select_add_option.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_subtable_mdpublic_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_update_mode.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_allow_down.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_host_resolution.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_upload_mdpublic_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_bool_flags.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_response_format_json.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_view_mdpublic_parity.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_config_keys.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_inner_scope.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_json_file_arg.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_node_delete_missing.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_save_action_dsl_fields.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_structure_listing.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_trigger_outcome.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wf_trigger_preserve.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_wo4_result_handling.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_code_node.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_condition_field_types.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_condition_slots.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_delete_cascade.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_envelope_errors.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_lifecycle_wire.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_node_wire.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_publish_inner.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_read_write_keys.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_create_section.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_filter_read.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_switch.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/atomic_write.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/confirm.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/__main__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/_wftest.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/charts.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/cleanup.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/compiler.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/config.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/errors.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/executor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/fields.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/hap.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/console.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/recording/report.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/schema.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/seed/template.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/selftest.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/smoke.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/step.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/steps.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/store.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/setup.cfg +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/setup.py +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.9.0 → hap_cli-0.9.1}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {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.
|
|
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 传文件的链接即可——**已经失效的链接照样能传,原样粘贴**——也可以只传路径部分。文件本身不会过期,过期的只是链接,所以失效链接随时可以重新生成。
|
|
@@ -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")
|
|
@@ -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
|
-
|
|
1848
|
-
|
|
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
|
-
|
|
1853
|
-
|
|
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
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
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(
|