hap-cli 0.8.18__tar.gz → 0.8.20__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.8.18 → hap_cli-0.8.20}/PKG-INFO +1 -1
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/record.py +131 -22
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/session.py +24 -13
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/worksheet.py +19 -4
- hap_cli-0.8.20/hap_cli/tests/test_control_lookup_guard.py +179 -0
- hap_cli-0.8.20/hap_cli/tests/test_member_relation_array_wrap.py +103 -0
- hap_cli-0.8.20/hap_cli/tests/test_workflow_envelope_errors.py +112 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/SOURCES.txt +3 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/MANIFEST.in +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/README.md +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/README.md +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/context.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_core.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/charts.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/config.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/errors.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/executor.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/fields.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/hap.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/schema.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/step.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/steps.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/store.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/setup.cfg +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/setup.py +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.18 → hap_cli-0.8.20}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Record CRUD module for Mingdao HAP."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import uuid
|
|
5
6
|
from typing import Any, Optional
|
|
6
7
|
|
|
@@ -8,6 +9,8 @@ from hap_cli.core.session import Session, APIError
|
|
|
8
9
|
from hap_cli.core import worksheet as ws_mod
|
|
9
10
|
from hap_cli.core import global_meta as gm
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger("hap_cli.record")
|
|
13
|
+
|
|
11
14
|
# Discussion source types — pd-openweb src/components/comment/config.js
|
|
12
15
|
# WORKSHEET = 7 whole-worksheet discussion
|
|
13
16
|
# WORKSHEETROW = 8 single record/row discussion
|
|
@@ -259,6 +262,67 @@ def _legacy_cell_entry(
|
|
|
259
262
|
return {"value": _legacy_cell_value(ctrl, value)}
|
|
260
263
|
|
|
261
264
|
|
|
265
|
+
def _coerce_entity_ids(
|
|
266
|
+
value: Any, id_keys: tuple[str, ...], *, field_label: str,
|
|
267
|
+
) -> list[str]:
|
|
268
|
+
"""Flatten a member / department / org-role / relation cell value to a
|
|
269
|
+
list of id strings, ready to be wrapped as ``[{idKey: id}, ...]``.
|
|
270
|
+
|
|
271
|
+
Accepts every realistic input shape so callers never have to pre-shape:
|
|
272
|
+
* a scalar id, or comma-separated ids (``"a,b"``);
|
|
273
|
+
* a list/tuple mixing ids and ``{idKey: ...}`` objects;
|
|
274
|
+
* a single ``{idKey: ...}`` object — the ``get_records`` read-back
|
|
275
|
+
shape carries display fields (``fullname`` / ``avatar`` / ``name``)
|
|
276
|
+
too, so only the id is kept;
|
|
277
|
+
* a JSON-array/object *string* of the above (e.g. a value copied
|
|
278
|
+
straight out of a record dump).
|
|
279
|
+
|
|
280
|
+
``id_keys`` lists the accepted id field names in priority order, e.g.
|
|
281
|
+
``("accountId", "aid")`` for USER_PICKER. An object that carries none of
|
|
282
|
+
them fails loudly instead of being ``str()``-mangled into a junk id —
|
|
283
|
+
that silent mangling sent garbage members to the server before.
|
|
284
|
+
"""
|
|
285
|
+
if value is None or value == "":
|
|
286
|
+
return []
|
|
287
|
+
# A JSON-array/object string is parsed structurally rather than
|
|
288
|
+
# comma-split (a member object's JSON has commas inside it).
|
|
289
|
+
if isinstance(value, str):
|
|
290
|
+
s = value.strip()
|
|
291
|
+
if s[:1] in ("[", "{"):
|
|
292
|
+
try:
|
|
293
|
+
value = json.loads(s)
|
|
294
|
+
except (ValueError, TypeError):
|
|
295
|
+
pass
|
|
296
|
+
if isinstance(value, dict):
|
|
297
|
+
items: list[Any] = [value]
|
|
298
|
+
elif isinstance(value, (list, tuple)):
|
|
299
|
+
items = list(value)
|
|
300
|
+
else:
|
|
301
|
+
items = [p for p in str(value).split(",") if p.strip() != ""]
|
|
302
|
+
out: list[str] = []
|
|
303
|
+
for it in items:
|
|
304
|
+
if isinstance(it, dict):
|
|
305
|
+
rid = next(
|
|
306
|
+
(str(it[k]) for k in id_keys if it.get(k) not in (None, "")),
|
|
307
|
+
None,
|
|
308
|
+
)
|
|
309
|
+
if rid is None:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
f"Field '{field_label}': object value is missing an id "
|
|
312
|
+
f"field ({'/'.join(id_keys)}): {it!r}"
|
|
313
|
+
)
|
|
314
|
+
out.append(rid)
|
|
315
|
+
elif isinstance(it, (list, tuple)):
|
|
316
|
+
raise ValueError(
|
|
317
|
+
f"Field '{field_label}': nested array is not a valid id: {it!r}"
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
s2 = str(it).strip()
|
|
321
|
+
if s2:
|
|
322
|
+
out.append(s2)
|
|
323
|
+
return out
|
|
324
|
+
|
|
325
|
+
|
|
262
326
|
def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
263
327
|
"""Serialize one cell to the legacy receiveControls `value`
|
|
264
328
|
string, per the shapes proven in
|
|
@@ -271,14 +335,15 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
271
335
|
if t == 10: # MULTI_SELECT
|
|
272
336
|
return json.dumps(_resolve_option_keys(ctrl, value), ensure_ascii=False)
|
|
273
337
|
|
|
338
|
+
label = _option_field_label(ctrl)
|
|
339
|
+
|
|
274
340
|
if t in (29, 35): # RELATE_SHEET / CASCADING_SELECT -> [{"sid": rowId}, ...]
|
|
275
341
|
# Cascade (35) is a relation to a row in another worksheet, persisted
|
|
276
342
|
# the same way as a plain relate (29): an array of {"sid": rowId}. A
|
|
277
|
-
# bare rowId makes the server answer "服务异常" (bug #642cc8).
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
ids = [s for s in str(value).split(",") if s != ""]
|
|
343
|
+
# bare rowId makes the server answer "服务异常" (bug #642cc8). Object
|
|
344
|
+
# inputs ({"sid": rowId, ...} read-back shape) keep only the sid.
|
|
345
|
+
ids = _coerce_entity_ids(value, ("sid", "rowid", "rowId"),
|
|
346
|
+
field_label=label)
|
|
282
347
|
return json.dumps([{"sid": rid} for rid in ids], ensure_ascii=False)
|
|
283
348
|
|
|
284
349
|
if t == 48: # ORG_ROLE -> [{"organizeId": id}, ...]
|
|
@@ -286,20 +351,21 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
286
351
|
# of {"organizeId": id} objects; organizeName is display-only and the
|
|
287
352
|
# server resolves it from the id. A bare organizeId is rejected with
|
|
288
353
|
# "服务异常" (bug #642cc8). See pd-openweb CellControls/OrgRole.jsx.
|
|
289
|
-
ids =
|
|
290
|
-
|
|
291
|
-
return json.dumps([{"organizeId":
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
354
|
+
ids = _coerce_entity_ids(value, ("organizeId", "id"),
|
|
355
|
+
field_label=label)
|
|
356
|
+
return json.dumps([{"organizeId": i} for i in ids], ensure_ascii=False)
|
|
357
|
+
|
|
358
|
+
if t == 26: # USER_PICKER -> always an array of {"accountId": id}, even
|
|
359
|
+
# for a single member. Object inputs (the full member object the UI
|
|
360
|
+
# holds / get_records returns: {accountId, fullname, avatar, ...})
|
|
361
|
+
# are reduced to their accountId — a bare object would fail the save.
|
|
362
|
+
ids = _coerce_entity_ids(value, ("accountId", "aid"),
|
|
363
|
+
field_label=label)
|
|
364
|
+
return json.dumps([{"accountId": i} for i in ids], ensure_ascii=False)
|
|
365
|
+
if t == 27: # DEPARTMENT -> [{"departmentId": id}, ...]
|
|
366
|
+
ids = _coerce_entity_ids(value, ("departmentId", "id"),
|
|
367
|
+
field_label=label)
|
|
368
|
+
return json.dumps([{"departmentId": i} for i in ids],
|
|
303
369
|
ensure_ascii=False)
|
|
304
370
|
|
|
305
371
|
if t == 36: # SWITCH / checkbox
|
|
@@ -320,6 +386,41 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
320
386
|
return "" if value is None else str(value)
|
|
321
387
|
|
|
322
388
|
|
|
389
|
+
def _controls_for_write(
|
|
390
|
+
session: Session, worksheet_id: str,
|
|
391
|
+
) -> list[dict[str, Any]]:
|
|
392
|
+
"""Fetch the worksheet's controls for a write, refusing an empty list.
|
|
393
|
+
|
|
394
|
+
Every write path type-encodes its cells against this metadata, so an
|
|
395
|
+
empty list silently disables the encoder: option/member/relation/
|
|
396
|
+
sub-table values would all take the "unknown field -> send the raw
|
|
397
|
+
value" fallback and reach the server in whatever shape the caller
|
|
398
|
+
happened to type. That is how a hand-written 1-D sub-table value
|
|
399
|
+
(``[{childControlId: value}, ...]``) escaped to the server and failed
|
|
400
|
+
deserialization into ``List<Cell>`` — the encoder would otherwise have
|
|
401
|
+
rewritten it to the 2-D form.
|
|
402
|
+
|
|
403
|
+
``get_worksheet_controls`` answers ``[]`` for any response that does
|
|
404
|
+
not carry a ``controls`` list — a soft failure, ``data: null``, or a
|
|
405
|
+
permission-degraded envelope — none of which raise. A real worksheet
|
|
406
|
+
always has controls, so an empty list means "we could not read the
|
|
407
|
+
fields", never "this worksheet has none". Abort before writing.
|
|
408
|
+
"""
|
|
409
|
+
controls = ws_mod.get_worksheet_controls(session, worksheet_id)
|
|
410
|
+
if not controls:
|
|
411
|
+
logger.warning(
|
|
412
|
+
"GetWorksheetControls returned no fields for worksheet %s; "
|
|
413
|
+
"aborting the write instead of sending unencoded cells",
|
|
414
|
+
worksheet_id,
|
|
415
|
+
)
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"Could not read the fields of worksheet {worksheet_id}, so "
|
|
418
|
+
"nothing was written. Check the worksheet id and that your "
|
|
419
|
+
"account can open it, then try again."
|
|
420
|
+
)
|
|
421
|
+
return controls
|
|
422
|
+
|
|
423
|
+
|
|
323
424
|
def encode_controls_by_id(
|
|
324
425
|
session: Session,
|
|
325
426
|
worksheet_id: str,
|
|
@@ -356,7 +457,7 @@ def encode_controls_by_id(
|
|
|
356
457
|
(in-place edits keyed by ``rowid``, fresh adds, and deletes — see
|
|
357
458
|
:func:`_sublist_update_value`).
|
|
358
459
|
"""
|
|
359
|
-
controls =
|
|
460
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
360
461
|
by_id: dict[str, dict[str, Any]] = {
|
|
361
462
|
c.get("controlId"): c for c in controls if c.get("controlId")
|
|
362
463
|
}
|
|
@@ -371,6 +472,14 @@ def encode_controls_by_id(
|
|
|
371
472
|
raw = rc.get("value")
|
|
372
473
|
ctrl = by_id.get(cid) or by_ref.get(cid)
|
|
373
474
|
if ctrl is None:
|
|
475
|
+
# Deliberate escape hatch (see docstring), but a value that
|
|
476
|
+
# skips the type encoder is exactly how a malformed cell
|
|
477
|
+
# reaches the server, so leave a trace: this is the only
|
|
478
|
+
# place a write can go out unencoded.
|
|
479
|
+
logger.warning(
|
|
480
|
+
"Field %r is not on worksheet %s; its value is sent "
|
|
481
|
+
"unencoded (no type conversion applied)", cid, worksheet_id,
|
|
482
|
+
)
|
|
374
483
|
out.append({"controlId": cid, "value": raw})
|
|
375
484
|
continue
|
|
376
485
|
out.append({
|
|
@@ -540,7 +649,7 @@ def batch_create_by_name(
|
|
|
540
649
|
by child controlId; single/multi select -> option keys; etc.).
|
|
541
650
|
Evidence: hap_cli/tests/answer_create_record.md.
|
|
542
651
|
"""
|
|
543
|
-
controls =
|
|
652
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
544
653
|
by_name: dict[str, dict[str, Any]] = {}
|
|
545
654
|
for c in controls:
|
|
546
655
|
for key in (c.get("controlName"), c.get("alias"),
|
|
@@ -599,7 +708,7 @@ def batch_update_by_name(
|
|
|
599
708
|
:func:`_sublist_update_value`). Record updates never use the V3
|
|
600
709
|
rows endpoint — V3 silently drops relation / sub-table cells.
|
|
601
710
|
"""
|
|
602
|
-
controls =
|
|
711
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
603
712
|
by_name: dict[str, dict[str, Any]] = {}
|
|
604
713
|
for c in controls:
|
|
605
714
|
for key in (c.get("controlName"), c.get("alias"),
|
|
@@ -529,28 +529,39 @@ class Session:
|
|
|
529
529
|
# Detect each and unwrap accordingly.
|
|
530
530
|
if not isinstance(result, dict):
|
|
531
531
|
return result
|
|
532
|
+
# A non-empty ``exception`` means the call failed, whatever envelope
|
|
533
|
+
# it arrived in. This is the frontend's own and only test — mdyAPI
|
|
534
|
+
# rejects on ``if (responseData.exception)`` alone and never looks at
|
|
535
|
+
# ``status`` (pd-openweb src/common/global.js:846-849). Checking it
|
|
536
|
+
# before any unwrapping is what makes workflow failures visible: the
|
|
537
|
+
# workflow envelope has no ``state``, so it used to skip the error
|
|
538
|
+
# branch entirely and hand the caller a "successful" empty result.
|
|
539
|
+
exception_msg = str(result.get("exception", "") or "")
|
|
540
|
+
if exception_msg:
|
|
541
|
+
code = result.get("code", result.get("state"))
|
|
542
|
+
# Some HAP deployments tunnel auth failures through the exception
|
|
543
|
+
# text/code instead of HTTP 401. Surface those as
|
|
544
|
+
# AuthenticationError so the CLI shows the login hint.
|
|
545
|
+
if _looks_like_auth_failure(code, exception_msg):
|
|
546
|
+
raise AuthenticationError(exception_msg, status_code=None)
|
|
547
|
+
raise APIError(exception_msg, code=code)
|
|
532
548
|
if "status" in result and "data" in result and "state" not in result:
|
|
533
|
-
# Workflow envelope
|
|
534
|
-
#
|
|
535
|
-
#
|
|
549
|
+
# Workflow envelope: ``status`` carries no success/failure
|
|
550
|
+
# semantics (the frontend ignores it), and a failure would have
|
|
551
|
+
# been caught by the exception check above.
|
|
536
552
|
return result.get("data", result)
|
|
537
553
|
if "state" not in result:
|
|
538
554
|
return result
|
|
539
555
|
if result.get("state") == 0:
|
|
540
|
-
exception_msg = str(result.get("exception", "") or "")
|
|
541
556
|
code = result.get("code")
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
if _looks_like_auth_failure(code, exception_msg):
|
|
557
|
+
# state=0 with no exception text — still a failure, just an
|
|
558
|
+
# unlabelled one.
|
|
559
|
+
if _looks_like_auth_failure(code, ""):
|
|
546
560
|
raise AuthenticationError(
|
|
547
|
-
|
|
561
|
+
"Authentication failed (upstream reported login required).",
|
|
548
562
|
status_code=None,
|
|
549
563
|
)
|
|
550
|
-
raise APIError(
|
|
551
|
-
exception_msg or "Unknown API error",
|
|
552
|
-
code=code,
|
|
553
|
-
)
|
|
564
|
+
raise APIError("Unknown API error", code=code)
|
|
554
565
|
return result.get("data", result)
|
|
555
566
|
|
|
556
567
|
# ── API Call Methods ──────────────────────────────────────────────────
|
|
@@ -68,10 +68,25 @@ def get_worksheet_controls(
|
|
|
68
68
|
if isinstance(result, dict):
|
|
69
69
|
inner = result.get("data")
|
|
70
70
|
if isinstance(inner, dict):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
controls = inner.get("controls", [])
|
|
72
|
+
elif isinstance(inner, list):
|
|
73
|
+
controls = inner
|
|
74
|
+
else:
|
|
75
|
+
controls = result.get("controls", [])
|
|
76
|
+
else:
|
|
77
|
+
controls = []
|
|
78
|
+
if not controls:
|
|
79
|
+
# A real worksheet always has controls, so an empty result means
|
|
80
|
+
# the envelope was not the expected shape (soft failure, data:
|
|
81
|
+
# null, permission-degraded response). Callers that type-encode
|
|
82
|
+
# cells against this metadata silently lose their encoding when
|
|
83
|
+
# it comes back empty, so leave a trace at the source.
|
|
84
|
+
logger.warning(
|
|
85
|
+
"GetWorksheetControls returned no fields for worksheet %s "
|
|
86
|
+
"(response keys: %s)", worksheet_id,
|
|
87
|
+
sorted(result) if isinstance(result, dict) else type(result).__name__,
|
|
88
|
+
)
|
|
89
|
+
return controls
|
|
75
90
|
|
|
76
91
|
|
|
77
92
|
def get_worksheet_views(
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""A record write must abort when the worksheet's fields can't be read.
|
|
2
|
+
|
|
3
|
+
Traced from a live backend error on ``Worksheet.AddWSRowData.DetailTable
|
|
4
|
+
.Details``:
|
|
5
|
+
|
|
6
|
+
Cannot deserialize the current JSON object (e.g. {"name":"value"})
|
|
7
|
+
into type 'System.Collections.Generic.List`1[MD.Entity.Worksheet.Cell]'
|
|
8
|
+
... Path '[0].66961f256a6cbc06cce98326'
|
|
9
|
+
|
|
10
|
+
The server had received a sub-table cell whose value was a *1-D* array of
|
|
11
|
+
``{childControlId: value}`` maps instead of the 2-D
|
|
12
|
+
``[[{controlId, value}, ...], ...]`` array it deserializes into
|
|
13
|
+
``List<Cell>``.
|
|
14
|
+
|
|
15
|
+
``encode_controls_by_id`` normally rewrites exactly that mistake: it looks
|
|
16
|
+
the sub-table control up, runs ``_legacy_cell_value`` (type 34) over the
|
|
17
|
+
value and re-emits the 2-D form with a ``tempRowId`` cell per row. The one
|
|
18
|
+
way the wrong shape can escape is the "control not found -> send the raw
|
|
19
|
+
value" fallback. And because ``get_worksheet_controls`` returns ``[]``
|
|
20
|
+
whenever the response doesn't carry a ``controls`` list (soft failure,
|
|
21
|
+
``data: null``, permission-degraded envelope), *every* cell of that write
|
|
22
|
+
silently took the fallback — the type encoder was effectively switched
|
|
23
|
+
off with no signal to the user.
|
|
24
|
+
|
|
25
|
+
Two guards, both exercised here:
|
|
26
|
+
* an empty control list on a write path fails loudly, and
|
|
27
|
+
* an individual unresolved field reference is logged before its value
|
|
28
|
+
is passed through.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
|
|
34
|
+
import pytest
|
|
35
|
+
|
|
36
|
+
from hap_cli.core import record as rec_mod
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_SUB_CTRL = {
|
|
40
|
+
"controlId": "66962b546c73406b9268582d",
|
|
41
|
+
"controlName": "明细",
|
|
42
|
+
"type": 34,
|
|
43
|
+
"relationControls": [
|
|
44
|
+
{"controlId": "66961f256a6cbc06cce98326", "controlName": "编号",
|
|
45
|
+
"type": 2},
|
|
46
|
+
{"controlId": "66962af95957d709b346d1ad", "controlName": "数量",
|
|
47
|
+
"type": 6},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# The shape an LLM / hand-written payload typically produces: one map per
|
|
52
|
+
# child row, keyed by child controlId, values left in their native JSON
|
|
53
|
+
# types. This is what the server choked on.
|
|
54
|
+
_MAP_SHAPED_ROWS = json.dumps([
|
|
55
|
+
{"66961f256a6cbc06cce98326": "WL001238",
|
|
56
|
+
"66962af95957d709b346d1ad": 2},
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def controls(monkeypatch):
|
|
62
|
+
"""Stub the worksheet control fetch; assign to the list to change it."""
|
|
63
|
+
holder = [_SUB_CTRL]
|
|
64
|
+
|
|
65
|
+
def _fake(session, worksheet_id, app_id=""):
|
|
66
|
+
return holder[0] if isinstance(holder[0], list) else holder
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(rec_mod.ws_mod, "get_worksheet_controls", _fake)
|
|
69
|
+
return holder
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _set(holder, value):
|
|
73
|
+
holder[0] = value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def warnings():
|
|
78
|
+
"""Collect this module's warnings off its own logger.
|
|
79
|
+
|
|
80
|
+
``caplog`` listens on the root logger, and ``hap config log on``
|
|
81
|
+
detaches ``hap_cli`` from root (``propagate = False``), so a test that
|
|
82
|
+
happens to run after one is deaf to these records. Attaching here is
|
|
83
|
+
independent of that.
|
|
84
|
+
"""
|
|
85
|
+
records: list[logging.LogRecord] = []
|
|
86
|
+
|
|
87
|
+
class _Collect(logging.Handler):
|
|
88
|
+
def emit(self, record):
|
|
89
|
+
records.append(record)
|
|
90
|
+
|
|
91
|
+
handler = _Collect(level=logging.WARNING)
|
|
92
|
+
log = logging.getLogger("hap_cli.record")
|
|
93
|
+
log.addHandler(handler)
|
|
94
|
+
try:
|
|
95
|
+
yield records
|
|
96
|
+
finally:
|
|
97
|
+
log.removeHandler(handler)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestEmptyControlListAborts:
|
|
101
|
+
def test_encode_controls_by_id_raises(self, controls):
|
|
102
|
+
_set(controls, [])
|
|
103
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
104
|
+
rec_mod.encode_controls_by_id(
|
|
105
|
+
None, "ws_1",
|
|
106
|
+
[{"controlId": _SUB_CTRL["controlId"],
|
|
107
|
+
"value": _MAP_SHAPED_ROWS}],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def test_update_path_raises_too(self, controls):
|
|
111
|
+
_set(controls, [])
|
|
112
|
+
with pytest.raises(ValueError):
|
|
113
|
+
rec_mod.encode_controls_by_id(
|
|
114
|
+
None, "ws_1",
|
|
115
|
+
[{"controlId": _SUB_CTRL["controlId"], "value": "x"}],
|
|
116
|
+
for_update=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def test_batch_create_by_name_raises(self, controls):
|
|
120
|
+
_set(controls, [])
|
|
121
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
122
|
+
rec_mod.batch_create_by_name(None, "ws_1", [{"明细": []}])
|
|
123
|
+
|
|
124
|
+
def test_batch_update_by_name_raises(self, controls):
|
|
125
|
+
_set(controls, [])
|
|
126
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
127
|
+
rec_mod.batch_update_by_name(
|
|
128
|
+
None, "ws_1", [{"rowid": "r1", "明细": []}])
|
|
129
|
+
|
|
130
|
+
def test_error_names_the_worksheet_not_the_field(self, controls):
|
|
131
|
+
"""The old message blamed the field ("Field '明细' not found").
|
|
132
|
+
|
|
133
|
+
With no controls at all the field is not the problem — say so, or
|
|
134
|
+
the user goes hunting for a field that exists.
|
|
135
|
+
"""
|
|
136
|
+
_set(controls, [])
|
|
137
|
+
with pytest.raises(ValueError) as exc:
|
|
138
|
+
rec_mod.batch_create_by_name(None, "ws_1", [{"明细": []}])
|
|
139
|
+
assert "not found" not in str(exc.value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestUnresolvedFieldIsLogged:
|
|
143
|
+
def test_warning_names_the_field(self, controls, warnings):
|
|
144
|
+
out = rec_mod.encode_controls_by_id(
|
|
145
|
+
None, "ws_1", [{"controlId": "c_nope", "value": "v"}])
|
|
146
|
+
# Still passed through — explicit ids must keep reaching the server.
|
|
147
|
+
assert out == [{"controlId": "c_nope", "value": "v"}]
|
|
148
|
+
assert any("c_nope" in r.getMessage() for r in warnings)
|
|
149
|
+
|
|
150
|
+
def test_no_warning_when_resolved(self, controls, warnings):
|
|
151
|
+
rec_mod.encode_controls_by_id(
|
|
152
|
+
None, "ws_1",
|
|
153
|
+
[{"controlId": _SUB_CTRL["controlId"], "value": "[]"}])
|
|
154
|
+
assert warnings == []
|
|
155
|
+
|
|
156
|
+
def test_empty_control_list_is_logged_before_the_abort(
|
|
157
|
+
self, controls, warnings):
|
|
158
|
+
_set(controls, [])
|
|
159
|
+
with pytest.raises(ValueError):
|
|
160
|
+
rec_mod.encode_controls_by_id(
|
|
161
|
+
None, "ws_1", [{"controlId": "c_any", "value": "v"}])
|
|
162
|
+
assert any("ws_1" in r.getMessage() for r in warnings)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestMapShapedSubTableIsStillNormalized:
|
|
166
|
+
"""The guards must not weaken the repair that already works."""
|
|
167
|
+
|
|
168
|
+
def test_map_rows_become_2d_cell_lists(self, controls):
|
|
169
|
+
out = rec_mod.encode_controls_by_id(
|
|
170
|
+
None, "ws_1",
|
|
171
|
+
[{"controlId": _SUB_CTRL["controlId"],
|
|
172
|
+
"value": _MAP_SHAPED_ROWS}],
|
|
173
|
+
)
|
|
174
|
+
rows = json.loads(out[0]["value"])
|
|
175
|
+
assert isinstance(rows, list) and isinstance(rows[0], list)
|
|
176
|
+
cells = {c["controlId"]: c["value"] for c in rows[0]}
|
|
177
|
+
assert cells["66961f256a6cbc06cce98326"] == "WL001238"
|
|
178
|
+
assert cells["66962af95957d709b346d1ad"] == "2" # stringified
|
|
179
|
+
assert "tempRowId" in cells
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Member / department / org-role / relation cells must always serialize
|
|
2
|
+
to a JSON *array* of id objects, and must accept object-shaped inputs.
|
|
3
|
+
|
|
4
|
+
A USER_PICKER (26) cell is always an array, even for a single member
|
|
5
|
+
(evidence: a bare ``{"accountId":...}`` object on AddWorksheetRow fails
|
|
6
|
+
server-side). hap-cli's encoder already wraps scalars in an array, but it
|
|
7
|
+
mangled object-shaped inputs: ``[{"accountId": str(i)} for i in value]``
|
|
8
|
+
ran ``str()`` over a whole member dict when the value was a member object
|
|
9
|
+
or — the natural ``get_records`` read-back shape — a list of member
|
|
10
|
+
objects ``[{"accountId","fullname","avatar",...}]``. The id is now
|
|
11
|
+
extracted from such objects and the cell is always emitted as an array.
|
|
12
|
+
|
|
13
|
+
The same fix covers DEPARTMENT (27, ``departmentId``), ORG_ROLE
|
|
14
|
+
(48, ``organizeId``) and RELATE_SHEET/CASCADING (29/35, ``sid``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
from hap_cli.core import record as rec_mod
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _val(type_code, value):
|
|
25
|
+
return rec_mod._legacy_cell_value({"type": type_code, "controlName": "f"}, value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestUserPickerAlwaysArray:
|
|
29
|
+
def test_single_account_id_string(self):
|
|
30
|
+
assert _val(26, "a1") == '[{"accountId": "a1"}]'
|
|
31
|
+
|
|
32
|
+
def test_comma_separated_ids(self):
|
|
33
|
+
assert _val(26, "a1,a2") == '[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
34
|
+
|
|
35
|
+
def test_list_of_ids(self):
|
|
36
|
+
assert _val(26, ["a1", "a2"]) == \
|
|
37
|
+
'[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
38
|
+
|
|
39
|
+
def test_single_member_object_keeps_only_account_id(self):
|
|
40
|
+
v = {"accountId": "a1", "fullname": "林梦阳", "name": "林梦阳",
|
|
41
|
+
"avatar": "https://x", "avatarMiddle": "https://y"}
|
|
42
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
43
|
+
|
|
44
|
+
def test_list_of_member_objects_readback_shape(self):
|
|
45
|
+
v = [{"accountId": "a1", "fullname": "甲", "avatar": "u1"},
|
|
46
|
+
{"accountId": "a2", "fullname": "乙", "avatar": "u2"}]
|
|
47
|
+
assert _val(26, v) == \
|
|
48
|
+
'[{"accountId": "a1"}, {"accountId": "a2"}]'
|
|
49
|
+
|
|
50
|
+
def test_json_array_of_objects_string(self):
|
|
51
|
+
v = json.dumps([{"accountId": "a1", "fullname": "甲"}])
|
|
52
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
53
|
+
|
|
54
|
+
def test_json_object_string(self):
|
|
55
|
+
v = json.dumps({"accountId": "a1", "fullname": "甲"})
|
|
56
|
+
assert _val(26, v) == '[{"accountId": "a1"}]'
|
|
57
|
+
|
|
58
|
+
def test_member_object_without_account_id_rejected(self):
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
_val(26, {"fullname": "甲", "avatar": "u"})
|
|
61
|
+
|
|
62
|
+
def test_empty_is_empty_array(self):
|
|
63
|
+
assert _val(26, "") == "[]"
|
|
64
|
+
assert _val(26, None) == "[]"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestDepartmentObjectShape:
|
|
68
|
+
def test_object_keeps_department_id(self):
|
|
69
|
+
v = {"departmentId": "d1", "departmentName": "研发"}
|
|
70
|
+
assert _val(27, v) == '[{"departmentId": "d1"}]'
|
|
71
|
+
|
|
72
|
+
def test_list_of_objects(self):
|
|
73
|
+
v = [{"departmentId": "d1"}, {"departmentId": "d2"}]
|
|
74
|
+
assert _val(27, v) == \
|
|
75
|
+
'[{"departmentId": "d1"}, {"departmentId": "d2"}]'
|
|
76
|
+
|
|
77
|
+
def test_plain_id_still_works(self):
|
|
78
|
+
assert _val(27, "d1") == '[{"departmentId": "d1"}]'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestOrgRoleObjectShape:
|
|
82
|
+
def test_object_keeps_organize_id(self):
|
|
83
|
+
v = {"organizeId": "o1", "organizeName": "主管"}
|
|
84
|
+
assert _val(48, v) == '[{"organizeId": "o1"}]'
|
|
85
|
+
|
|
86
|
+
def test_plain_id_still_works(self):
|
|
87
|
+
assert _val(48, "o1") == '[{"organizeId": "o1"}]'
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestRelateObjectShape:
|
|
91
|
+
def test_relate_object_keeps_sid(self):
|
|
92
|
+
v = {"sid": "row1", "name": "记录甲"}
|
|
93
|
+
assert _val(29, v) == '[{"sid": "row1"}]'
|
|
94
|
+
|
|
95
|
+
def test_relate_list_of_objects_readback(self):
|
|
96
|
+
v = [{"sid": "row1", "name": "甲"}, {"sid": "row2", "name": "乙"}]
|
|
97
|
+
assert _val(29, v) == '[{"sid": "row1"}, {"sid": "row2"}]'
|
|
98
|
+
|
|
99
|
+
def test_relate_plain_rowid_still_works(self):
|
|
100
|
+
assert _val(29, "row1") == '[{"sid": "row1"}]'
|
|
101
|
+
|
|
102
|
+
def test_cascading_object_keeps_sid(self):
|
|
103
|
+
assert _val(35, {"sid": "row1"}) == '[{"sid": "row1"}]'
|