by-framework 0.2.1__tar.gz → 0.2.2.dev0__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.
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/workflows/publish.yml +27 -7
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/PKG-INFO +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-byclaw/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-postgres/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-langfuse/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-phoenix/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/pyproject.toml +1 -1
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/util/discovery_http_client.py +34 -9
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/util/http_client.py +27 -6
- by_framework-0.2.2.dev0/tests/util/test_discovery_http_client_upload.py +181 -0
- by_framework-0.2.2.dev0/tests/util/test_http_client.py +172 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/uv.lock +7 -7
- by_framework-0.2.1/tests/util/test_http_client.py +0 -41
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/RELEASING.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/workflows/ci.yml +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.github/workflows/stale.yml +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.gitignore +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/.pre-commit-config.yaml +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/AGENTS.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/CHANGELOG.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/CLAUDE.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/CODE_OF_CONDUCT.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/CONTRIBUTING.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/LICENSE +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/Makefile +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/README_zh.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/SECURITY.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/assets/img/architecture_en.png +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/assets/img/architecture_zh.png +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/autoformat.sh +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/docs/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/docs/plans/2026-05-15-worker-task-state-stats.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/src/by_framework_adk/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/src/by_framework_adk/_utils.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/src/by_framework_adk/adapter.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/src/by_framework_adk/worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-adk/tests/test_worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-byclaw/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-byclaw/src/by_framework_history_byclaw/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-byclaw/src/by_framework_history_byclaw/byclaw_history.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-byclaw/tests/test_byclaw_history.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-postgres/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-postgres/src/by_framework_history_postgres/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-postgres/src/by_framework_history_postgres/postgres.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-history-postgres/tests/test_postgres_history_storage.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/src/by_framework_langgraph/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/src/by_framework_langgraph/_utils.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/src/by_framework_langgraph/adapter.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/src/by_framework_langgraph/tools.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/src/by_framework_langgraph/worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/tests/test_adapter.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/tests/test_tools.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-langgraph/tests/test_utils.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-langfuse/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-langfuse/src/by_framework_trace_langfuse/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-langfuse/src/by_framework_trace_langfuse/langfuse.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-langfuse/tests/test_langfuse_import.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-phoenix/README.md +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-phoenix/src/by_framework_trace_phoenix/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-phoenix/src/by_framework_trace_phoenix/phoenix.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/libs/by-framework-trace-phoenix/tests/test_phoenix_import.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/pylintrc +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/scripts/python_quality.sh +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/__main__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/client/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/client/byai_client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/client/client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/config.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/constants.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/emitter.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/exceptions.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/logger.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/common/redis_client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/availability.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/delivery_gate.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/discovery.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/extensions/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/extensions/agent_config.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/extensions/plugin.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/extensions/registry.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/extensions/trace_provider.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/action_type.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/agent_state.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/byai_codec.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/byai_command.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/byai_types.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/commands.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/content_codec.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/content_type.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/data_message.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/data_shapes.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/event_type.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/events.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/message.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/message_header.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/responses.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/protocol/results.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/registry.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/agent_config_manager.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/agent_runtime_state.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/file_manager.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/file_paths.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/file_permissions.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/filestore/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/filestore/base.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/filestore/local.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/history/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/history/base.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/history/history_manager.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/history/in_memory.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/runtime/session_manager.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/wakeup_controller.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/core/workspace.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/base.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/common.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/execution.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/http.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/protocol.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/errors/registry.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/util/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/util/generate_message_id.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/_control_handling.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/_execution_tracking.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/_message_processing.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/app.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/byai_context.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/byai_worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/context.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/heartbeat.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/processor.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/runner.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/sandbox/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/sandbox/hook_sandbox.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/worker/worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/client/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/client/test_client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/common/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/common/test_config.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/common/test_exceptions.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/common/test_logger.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/common/test_redis_client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/conftest.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/protocol/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/protocol/test_byai_codec.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/protocol/test_command_wire.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/protocol/test_protocol.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/protocol/test_results.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/history/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/history/test_history_persistence.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_file_access_context.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_file_manager_default_storage.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_file_paths.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_file_permissions.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_filestore_local_collection.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_filestore_local_mutation.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_filestore_local_read.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/runtime/test_filestore_local_search.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/test_availability.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/test_discovery.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/core/test_registry.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/integration/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/integration/test_ask_user_flow.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/integration/test_callback_flow.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/integration/test_logger_integration.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/integration/test_scatter_gather.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/test_langfuse_plugin.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/test_plugin_discovery.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/test_plugin_improvements.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/test_plugin_registry.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/plugin/test_plugin_system.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/util/test_discovery_http_client.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/util/test_discovery_http_client_download.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/__init__.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_app.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_byai_worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_context.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_control_handling.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_emitter.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_gateway_worker.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_heartbeat.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_message_processing.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_processor.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_runner.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_sandbox.py +0 -0
- {by_framework-0.2.1 → by_framework-0.2.2.dev0}/tests/worker/test_workspace.py +0 -0
|
@@ -63,7 +63,7 @@ jobs:
|
|
|
63
63
|
echo "package_name=by-framework-adk" >> "$GITHUB_OUTPUT"
|
|
64
64
|
echo "package_dir=libs/by-framework-adk" >> "$GITHUB_OUTPUT"
|
|
65
65
|
echo "version=${REF_NAME#by-framework-adk-v}" >> "$GITHUB_OUTPUT"
|
|
66
|
-
;;
|
|
66
|
+
;;
|
|
67
67
|
*)
|
|
68
68
|
echo "Unsupported tag: $REF_NAME" >&2
|
|
69
69
|
exit 1
|
|
@@ -152,6 +152,10 @@ jobs:
|
|
|
152
152
|
ref: ${{ github.ref }}
|
|
153
153
|
fetch-depth: 0
|
|
154
154
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
155
|
+
- uses: actions/setup-python@v5
|
|
156
|
+
with:
|
|
157
|
+
python-version: "3.12"
|
|
158
|
+
- uses: astral-sh/setup-uv@v5
|
|
155
159
|
|
|
156
160
|
- name: Create release branch
|
|
157
161
|
env:
|
|
@@ -169,20 +173,36 @@ jobs:
|
|
|
169
173
|
PACKAGE_DIR: ${{ needs.resolve-package.outputs.package_dir }}
|
|
170
174
|
VERSION: ${{ needs.resolve-package.outputs.version }}
|
|
171
175
|
run: |
|
|
172
|
-
# Calculate next
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
# Calculate next version using Python to support pre-releases (e.g., 0.2.3b0 -> 0.2.3b1, 0.2.3 -> 0.2.4.dev0)
|
|
177
|
+
NEXT_VERSION=$(python -c 'if True:
|
|
178
|
+
import re, sys
|
|
179
|
+
version = sys.argv[1]
|
|
180
|
+
match = re.search(r"(a|b|rc|dev)(\d+)$", version)
|
|
181
|
+
if match:
|
|
182
|
+
prefix = version[:match.start()]
|
|
183
|
+
suffix_type = match.group(1)
|
|
184
|
+
suffix_num = int(match.group(2))
|
|
185
|
+
print(f"{prefix}{suffix_type}{suffix_num + 1}")
|
|
186
|
+
else:
|
|
187
|
+
parts = version.split(".")
|
|
188
|
+
try:
|
|
189
|
+
parts[-1] = str(int(parts[-1]) + 1)
|
|
190
|
+
print(".".join(parts) + ".dev0")
|
|
191
|
+
except ValueError:
|
|
192
|
+
print(version + ".dev0")
|
|
193
|
+
' "$VERSION")
|
|
177
194
|
|
|
178
195
|
PYPROJECT="${PACKAGE_DIR}/pyproject.toml"
|
|
179
196
|
sed -i "s/version = \"${VERSION}\"/version = \"${NEXT_VERSION}\"/" "$PYPROJECT"
|
|
180
197
|
|
|
198
|
+
# Regenerate uv.lock to align with the new version bump in workspace
|
|
199
|
+
uv lock
|
|
200
|
+
|
|
181
201
|
BUMP_BRANCH="bump/${PACKAGE_NAME}/v${NEXT_VERSION}"
|
|
182
202
|
git checkout -b "$BUMP_BRANCH"
|
|
183
203
|
git config user.name "github-actions[bot]"
|
|
184
204
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
185
|
-
git add "$PYPROJECT"
|
|
205
|
+
git add "$PYPROJECT" uv.lock
|
|
186
206
|
git commit -m "chore: bump ${PACKAGE_NAME} version to ${NEXT_VERSION}"
|
|
187
207
|
git push origin "$BUMP_BRANCH"
|
|
188
208
|
|
{by_framework-0.2.1 → by_framework-0.2.2.dev0}/src/by_framework/util/discovery_http_client.py
RENAMED
|
@@ -379,14 +379,17 @@ class DiscoveryHttpClient:
|
|
|
379
379
|
Returns:
|
|
380
380
|
HttpResponse from the server
|
|
381
381
|
"""
|
|
382
|
+
p = Path(file_path)
|
|
382
383
|
parts: list[tuple[str, Any]] = []
|
|
383
384
|
if form_fields:
|
|
384
385
|
for key, value in form_fields.items():
|
|
385
386
|
parts.append((key, value))
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
|
|
388
|
+
with open(p, "rb") as f:
|
|
389
|
+
parts.append((file_field, (p.name, f)))
|
|
390
|
+
return await self._upload_with_discovery(
|
|
391
|
+
service_name, path, parts, headers=headers
|
|
392
|
+
)
|
|
390
393
|
|
|
391
394
|
async def upload_multiple(
|
|
392
395
|
self,
|
|
@@ -412,15 +415,21 @@ class DiscoveryHttpClient:
|
|
|
412
415
|
Returns:
|
|
413
416
|
HttpResponse from the server
|
|
414
417
|
"""
|
|
418
|
+
from contextlib import ExitStack
|
|
419
|
+
|
|
415
420
|
parts: list[tuple[str, Any]] = []
|
|
416
421
|
if form_fields:
|
|
417
422
|
for key, value in form_fields.items():
|
|
418
423
|
parts.append((key, value))
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
+
|
|
425
|
+
with ExitStack() as stack:
|
|
426
|
+
for fp in file_paths:
|
|
427
|
+
p = Path(fp)
|
|
428
|
+
f = stack.enter_context(open(p, "rb"))
|
|
429
|
+
parts.append((file_field, (p.name, f)))
|
|
430
|
+
return await self._upload_with_discovery(
|
|
431
|
+
service_name, path, parts, headers=headers
|
|
432
|
+
)
|
|
424
433
|
|
|
425
434
|
async def upload_with_stream(
|
|
426
435
|
self,
|
|
@@ -537,6 +546,22 @@ class DiscoveryHttpClient:
|
|
|
537
546
|
logger.warning(
|
|
538
547
|
"Node-switching retry in %.1fs for service %s", delay, service_name
|
|
539
548
|
)
|
|
549
|
+
|
|
550
|
+
# Reset seek position for any file-like objects in parts before retrying
|
|
551
|
+
for _, val in parts:
|
|
552
|
+
if isinstance(val, tuple):
|
|
553
|
+
for item in val:
|
|
554
|
+
if hasattr(item, "seek") and callable(item.seek):
|
|
555
|
+
try:
|
|
556
|
+
item.seek(0)
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
elif hasattr(val, "seek") and callable(val.seek):
|
|
560
|
+
try:
|
|
561
|
+
val.seek(0)
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
540
565
|
await asyncio.sleep(delay)
|
|
541
566
|
return await self._upload_with_discovery(
|
|
542
567
|
service_name,
|
|
@@ -543,12 +543,15 @@ class ByHttpClient:
|
|
|
543
543
|
Returns:
|
|
544
544
|
HttpResponse from the server
|
|
545
545
|
"""
|
|
546
|
+
path = Path(file_path)
|
|
546
547
|
parts: list[tuple[str, Any]] = []
|
|
547
548
|
if form_fields:
|
|
548
549
|
for key, value in form_fields.items():
|
|
549
550
|
parts.append((key, value))
|
|
550
|
-
|
|
551
|
-
|
|
551
|
+
|
|
552
|
+
with open(path, "rb") as f:
|
|
553
|
+
parts.append((file_field, (path.name, f)))
|
|
554
|
+
return await self._upload(url, parts, headers=headers)
|
|
552
555
|
|
|
553
556
|
async def upload_multiple(
|
|
554
557
|
self,
|
|
@@ -572,13 +575,19 @@ class ByHttpClient:
|
|
|
572
575
|
Returns:
|
|
573
576
|
HttpResponse from the server
|
|
574
577
|
"""
|
|
578
|
+
from contextlib import ExitStack
|
|
579
|
+
|
|
575
580
|
parts: list[tuple[str, Any]] = []
|
|
576
581
|
if form_fields:
|
|
577
582
|
for key, value in form_fields.items():
|
|
578
583
|
parts.append((key, value))
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
584
|
+
|
|
585
|
+
with ExitStack() as stack:
|
|
586
|
+
for fp in file_paths:
|
|
587
|
+
path = Path(fp)
|
|
588
|
+
f = stack.enter_context(open(path, "rb"))
|
|
589
|
+
parts.append((file_field, (path.name, f)))
|
|
590
|
+
return await self._upload(url, parts, headers=headers)
|
|
582
591
|
|
|
583
592
|
async def upload_with_stream(
|
|
584
593
|
self,
|
|
@@ -643,12 +652,24 @@ class ByHttpClient:
|
|
|
643
652
|
|
|
644
653
|
logger.debug("[POST] %s (multipart upload, %d parts)", url, len(parts))
|
|
645
654
|
|
|
655
|
+
# Convert non-file parts (e.g. standard form fields) to (None, value)
|
|
656
|
+
# to ensure httpx renders them without filename attribute,
|
|
657
|
+
# and avoid AsyncClient sync/async conflicts.
|
|
658
|
+
formatted_parts: list[tuple[str, Any]] = []
|
|
659
|
+
for key, val in parts:
|
|
660
|
+
if isinstance(val, tuple):
|
|
661
|
+
formatted_parts.append((key, val))
|
|
662
|
+
elif hasattr(val, "read") and callable(val.read):
|
|
663
|
+
formatted_parts.append((key, val))
|
|
664
|
+
else:
|
|
665
|
+
formatted_parts.append((key, (None, str(val))))
|
|
666
|
+
|
|
646
667
|
try:
|
|
647
668
|
response = await self._client.request(
|
|
648
669
|
method="POST",
|
|
649
670
|
url=url,
|
|
650
671
|
headers=request_headers,
|
|
651
|
-
files=
|
|
672
|
+
files=formatted_parts,
|
|
652
673
|
)
|
|
653
674
|
|
|
654
675
|
logger.debug(
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from by_framework.core.discovery import DiscoveryClient, ServiceInstance
|
|
7
|
+
from by_framework.util.discovery_http_client import DiscoveryHttpClient
|
|
8
|
+
from by_framework.util.http_client import (ByHttpClient, HttpResponse, RetryConfig)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def mock_discovery_client():
|
|
13
|
+
client = MagicMock(spec=DiscoveryClient)
|
|
14
|
+
client.discover = AsyncMock()
|
|
15
|
+
return client
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_http_client():
|
|
20
|
+
client = MagicMock(spec=ByHttpClient)
|
|
21
|
+
client._upload = AsyncMock()
|
|
22
|
+
return client
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def fake_instance():
|
|
27
|
+
return ServiceInstance(id="inst1", host="192.168.1.100", port=8080)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_discovery_upload_single_file(
|
|
32
|
+
mock_discovery_client, mock_http_client, fake_instance, tmp_path
|
|
33
|
+
):
|
|
34
|
+
mock_discovery_client.discover.return_value = fake_instance
|
|
35
|
+
success_response = HttpResponse(
|
|
36
|
+
status_code=200, headers={}, data={"status": "ok"}, is_success=True
|
|
37
|
+
)
|
|
38
|
+
mock_http_client._upload.return_value = success_response
|
|
39
|
+
|
|
40
|
+
client = DiscoveryHttpClient(
|
|
41
|
+
discovery_client=mock_discovery_client,
|
|
42
|
+
http_client=mock_http_client,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
file_path = tmp_path / "test.txt"
|
|
46
|
+
file_path.write_bytes(b"hello")
|
|
47
|
+
|
|
48
|
+
response = await client.upload(
|
|
49
|
+
service_name="my-service",
|
|
50
|
+
path="/upload",
|
|
51
|
+
file_path=file_path,
|
|
52
|
+
form_fields={"field1": "value1"},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
assert response.is_success is True
|
|
56
|
+
mock_http_client._upload.assert_called_once()
|
|
57
|
+
called_args, _ = mock_http_client._upload.call_args
|
|
58
|
+
assert called_args[0] == "http://192.168.1.100:8080/upload"
|
|
59
|
+
parts = called_args[1]
|
|
60
|
+
|
|
61
|
+
assert ("field1", "value1") in parts
|
|
62
|
+
file_part = [p for p in parts if p[0] == "file"][0]
|
|
63
|
+
assert file_part[1][0] == "test.txt"
|
|
64
|
+
assert not isinstance(file_part[1][1], str)
|
|
65
|
+
assert hasattr(file_part[1][1], "read")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.asyncio
|
|
69
|
+
async def test_discovery_upload_multiple_files(
|
|
70
|
+
mock_discovery_client, mock_http_client, fake_instance, tmp_path
|
|
71
|
+
):
|
|
72
|
+
mock_discovery_client.discover.return_value = fake_instance
|
|
73
|
+
success_response = HttpResponse(
|
|
74
|
+
status_code=200, headers={}, data={"status": "ok"}, is_success=True
|
|
75
|
+
)
|
|
76
|
+
mock_http_client._upload.return_value = success_response
|
|
77
|
+
|
|
78
|
+
client = DiscoveryHttpClient(
|
|
79
|
+
discovery_client=mock_discovery_client,
|
|
80
|
+
http_client=mock_http_client,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
f1 = tmp_path / "f1.txt"
|
|
84
|
+
f2 = tmp_path / "f2.txt"
|
|
85
|
+
f1.write_bytes(b"1")
|
|
86
|
+
f2.write_bytes(b"2")
|
|
87
|
+
|
|
88
|
+
response = await client.upload_multiple(
|
|
89
|
+
service_name="my-service",
|
|
90
|
+
path="/upload",
|
|
91
|
+
file_paths=[f1, f2],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert response.is_success is True
|
|
95
|
+
mock_http_client._upload.assert_called_once()
|
|
96
|
+
called_args, _ = mock_http_client._upload.call_args
|
|
97
|
+
parts = called_args[1]
|
|
98
|
+
|
|
99
|
+
file_parts = [p for p in parts if p[0] == "file"]
|
|
100
|
+
assert len(file_parts) == 2
|
|
101
|
+
assert file_parts[0][1][0] == "f1.txt"
|
|
102
|
+
assert file_parts[1][1][0] == "f2.txt"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_discovery_upload_with_stream(
|
|
107
|
+
mock_discovery_client, mock_http_client, fake_instance
|
|
108
|
+
):
|
|
109
|
+
mock_discovery_client.discover.return_value = fake_instance
|
|
110
|
+
success_response = HttpResponse(
|
|
111
|
+
status_code=200, headers={}, data={"status": "ok"}, is_success=True
|
|
112
|
+
)
|
|
113
|
+
mock_http_client._upload.return_value = success_response
|
|
114
|
+
|
|
115
|
+
client = DiscoveryHttpClient(
|
|
116
|
+
discovery_client=mock_discovery_client,
|
|
117
|
+
http_client=mock_http_client,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
response = await client.upload_with_stream(
|
|
121
|
+
service_name="my-service",
|
|
122
|
+
path="/upload",
|
|
123
|
+
file_name="stream.bin",
|
|
124
|
+
content=b"stream content",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert response.is_success is True
|
|
128
|
+
mock_http_client._upload.assert_called_once()
|
|
129
|
+
called_args, _ = mock_http_client._upload.call_args
|
|
130
|
+
parts = called_args[1]
|
|
131
|
+
file_part = [p for p in parts if p[0] == "file"][0]
|
|
132
|
+
assert file_part[1][0] == "stream.bin"
|
|
133
|
+
assert file_part[1][2] == "application/octet-stream"
|
|
134
|
+
assert isinstance(file_part[1][1], io.BytesIO)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_discovery_upload_retry_resets_stream_seeks(
|
|
139
|
+
mock_discovery_client, mock_http_client, fake_instance
|
|
140
|
+
):
|
|
141
|
+
mock_discovery_client.discover.return_value = fake_instance
|
|
142
|
+
|
|
143
|
+
fail_response = HttpResponse(
|
|
144
|
+
status_code=502, headers={}, data="Error", is_success=False
|
|
145
|
+
)
|
|
146
|
+
success_response = HttpResponse(
|
|
147
|
+
status_code=200, headers={}, data="Success", is_success=True
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
async def mock_upload_handler(url, parts, headers=None):
|
|
151
|
+
file_part = [p for p in parts if p[0] == "file"][0]
|
|
152
|
+
file_stream = file_part[1][1]
|
|
153
|
+
|
|
154
|
+
data = file_stream.read()
|
|
155
|
+
assert data == b"test bytes"
|
|
156
|
+
|
|
157
|
+
if mock_upload_handler.call_count == 0:
|
|
158
|
+
mock_upload_handler.call_count += 1
|
|
159
|
+
return fail_response
|
|
160
|
+
return success_response
|
|
161
|
+
|
|
162
|
+
mock_upload_handler.call_count = 0
|
|
163
|
+
mock_http_client._upload.side_effect = mock_upload_handler
|
|
164
|
+
|
|
165
|
+
client = DiscoveryHttpClient(
|
|
166
|
+
discovery_client=mock_discovery_client,
|
|
167
|
+
http_client=mock_http_client,
|
|
168
|
+
retry_config=RetryConfig(
|
|
169
|
+
max_attempts=2, retry_on_status_codes=frozenset({502})
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
response = await client.upload_with_stream(
|
|
174
|
+
service_name="my-service",
|
|
175
|
+
path="/upload",
|
|
176
|
+
file_name="stream.bin",
|
|
177
|
+
content=b"test bytes",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assert response.is_success is True
|
|
181
|
+
assert mock_http_client._upload.call_count == 2
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Tests for file download support in ByHttpClient."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from by_framework.util.http_client import ByHttpClient, RetryConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_download_streams_response_to_file(tmp_path: Path):
|
|
13
|
+
payload = b"binary payload for download"
|
|
14
|
+
|
|
15
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
16
|
+
assert request.method == "GET"
|
|
17
|
+
assert request.url.path == "/files/archive.bin"
|
|
18
|
+
return httpx.Response(
|
|
19
|
+
status_code=200,
|
|
20
|
+
content=payload,
|
|
21
|
+
headers={"content-type": "application/octet-stream"},
|
|
22
|
+
request=request,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
target_path = tmp_path / "archive.bin"
|
|
26
|
+
transport = httpx.MockTransport(handler)
|
|
27
|
+
|
|
28
|
+
async with ByHttpClient(
|
|
29
|
+
base_url="https://example.com",
|
|
30
|
+
http_client=httpx.AsyncClient(
|
|
31
|
+
transport=transport,
|
|
32
|
+
base_url="https://example.com",
|
|
33
|
+
),
|
|
34
|
+
retry_config=RetryConfig.no_retry(),
|
|
35
|
+
) as client:
|
|
36
|
+
response = await client.download("/files/archive.bin", target_path)
|
|
37
|
+
|
|
38
|
+
assert response.is_success is True
|
|
39
|
+
assert response.status_code == 200
|
|
40
|
+
assert response.data == str(target_path)
|
|
41
|
+
assert target_path.read_bytes() == payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.asyncio
|
|
45
|
+
async def test_upload_single_file(tmp_path: Path):
|
|
46
|
+
file_content = b"my unique file content"
|
|
47
|
+
file_path = tmp_path / "test_upload.txt"
|
|
48
|
+
file_path.write_bytes(file_content)
|
|
49
|
+
|
|
50
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
51
|
+
assert request.method == "POST"
|
|
52
|
+
assert request.url.path == "/upload"
|
|
53
|
+
req_body = await request.aread()
|
|
54
|
+
|
|
55
|
+
# 验证是否上传了真正的文件内容
|
|
56
|
+
assert file_content in req_body
|
|
57
|
+
assert b'filename="test_upload.txt"' in req_body
|
|
58
|
+
assert b'name="file"' in req_body
|
|
59
|
+
assert b'name="field1"' in req_body
|
|
60
|
+
assert b"value1" in req_body
|
|
61
|
+
|
|
62
|
+
return httpx.Response(
|
|
63
|
+
status_code=200,
|
|
64
|
+
json={"status": "uploaded"},
|
|
65
|
+
request=request,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
transport = httpx.MockTransport(handler)
|
|
69
|
+
|
|
70
|
+
async with ByHttpClient(
|
|
71
|
+
base_url="https://example.com",
|
|
72
|
+
http_client=httpx.AsyncClient(
|
|
73
|
+
transport=transport,
|
|
74
|
+
base_url="https://example.com",
|
|
75
|
+
),
|
|
76
|
+
retry_config=RetryConfig.no_retry(),
|
|
77
|
+
) as client:
|
|
78
|
+
response = await client.upload(
|
|
79
|
+
"/upload",
|
|
80
|
+
file_path,
|
|
81
|
+
file_field="file",
|
|
82
|
+
form_fields={"field1": "value1"},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
assert response.is_success is True
|
|
86
|
+
assert response.data == {"status": "uploaded"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_upload_multiple_files(tmp_path: Path):
|
|
91
|
+
file1_content = b"content of file 1"
|
|
92
|
+
file2_content = b"content of file 2"
|
|
93
|
+
file1_path = tmp_path / "file1.txt"
|
|
94
|
+
file2_path = tmp_path / "file2.txt"
|
|
95
|
+
file1_path.write_bytes(file1_content)
|
|
96
|
+
file2_path.write_bytes(file2_content)
|
|
97
|
+
|
|
98
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
99
|
+
assert request.method == "POST"
|
|
100
|
+
req_body = await request.aread()
|
|
101
|
+
|
|
102
|
+
# 验证两个文件的内容和文件名都被正确发送
|
|
103
|
+
assert file1_content in req_body
|
|
104
|
+
assert file2_content in req_body
|
|
105
|
+
assert b'filename="file1.txt"' in req_body
|
|
106
|
+
assert b'filename="file2.txt"' in req_body
|
|
107
|
+
assert b'name="file"' in req_body
|
|
108
|
+
|
|
109
|
+
return httpx.Response(
|
|
110
|
+
status_code=200,
|
|
111
|
+
json={"status": "uploaded_multiple"},
|
|
112
|
+
request=request,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
transport = httpx.MockTransport(handler)
|
|
116
|
+
|
|
117
|
+
async with ByHttpClient(
|
|
118
|
+
base_url="https://example.com",
|
|
119
|
+
http_client=httpx.AsyncClient(
|
|
120
|
+
transport=transport,
|
|
121
|
+
base_url="https://example.com",
|
|
122
|
+
),
|
|
123
|
+
retry_config=RetryConfig.no_retry(),
|
|
124
|
+
) as client:
|
|
125
|
+
response = await client.upload_multiple(
|
|
126
|
+
"/upload",
|
|
127
|
+
[file1_path, file2_path],
|
|
128
|
+
file_field="file",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
assert response.is_success is True
|
|
132
|
+
assert response.data == {"status": "uploaded_multiple"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_upload_with_stream():
|
|
137
|
+
stream_content = b"stream upload data"
|
|
138
|
+
file_name = "stream_file.bin"
|
|
139
|
+
|
|
140
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
141
|
+
assert request.method == "POST"
|
|
142
|
+
req_body = await request.aread()
|
|
143
|
+
|
|
144
|
+
assert stream_content in req_body
|
|
145
|
+
assert f'filename="{file_name}"'.encode() in req_body
|
|
146
|
+
assert b'name="file"' in req_body
|
|
147
|
+
|
|
148
|
+
return httpx.Response(
|
|
149
|
+
status_code=200,
|
|
150
|
+
json={"status": "uploaded_stream"},
|
|
151
|
+
request=request,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
transport = httpx.MockTransport(handler)
|
|
155
|
+
|
|
156
|
+
async with ByHttpClient(
|
|
157
|
+
base_url="https://example.com",
|
|
158
|
+
http_client=httpx.AsyncClient(
|
|
159
|
+
transport=transport,
|
|
160
|
+
base_url="https://example.com",
|
|
161
|
+
),
|
|
162
|
+
retry_config=RetryConfig.no_retry(),
|
|
163
|
+
) as client:
|
|
164
|
+
response = await client.upload_with_stream(
|
|
165
|
+
"/upload",
|
|
166
|
+
file_name,
|
|
167
|
+
stream_content,
|
|
168
|
+
content_type="application/octet-stream",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
assert response.is_success is True
|
|
172
|
+
assert response.data == {"status": "uploaded_stream"}
|