cmdop 0.1.17__tar.gz → 0.1.18__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.
- {cmdop-0.1.17 → cmdop-0.1.18}/PKG-INFO +30 -23
- {cmdop-0.1.17 → cmdop-0.1.18}/README.md +29 -22
- {cmdop-0.1.17 → cmdop-0.1.18}/pyproject.toml +1 -1
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/__init__.py +1 -1
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/session.py +115 -1
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/session.py +47 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/__init__.py +2 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/interaction.py +28 -0
- cmdop-0.1.18/src/cmdop/services/browser/js/scroll.py +224 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/session.py +58 -23
- cmdop-0.1.17/src/cmdop/services/browser/js/scroll.py +0 -133
- {cmdop-0.1.17 → cmdop-0.1.18}/.gitignore +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/LICENSE +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/message_pool.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/py.typed +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.pyi +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2_grpc.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/enums.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/logger.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/retry.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/schema.json +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/enums.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/logger.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/retry.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/schema.json +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/enums.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/logger.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/retry.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/schema.json +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/sync_client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/client.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/config.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/discovery.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/exceptions.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/cleaner.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/formatting.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/logging.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/agent.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/base.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/config.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/extract.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/files.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/terminal.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/py.typed +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/agent.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/base.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/service.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/service.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/core.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/fetch.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/models.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/parsing.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/service.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/extract.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/files.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/terminal.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/base.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/handlers.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/terminal.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/__init__.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/auth.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/base.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/discovery.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/local.py +0 -0
- {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/remote.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmdop
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
4
4
|
Summary: Python SDK for CMDOP agent interaction
|
|
5
5
|
Project-URL: Homepage, https://cmdop.com
|
|
6
6
|
Project-URL: Documentation, https://cmdop.com
|
|
@@ -148,56 +148,63 @@ health: Health = result.output # Typed!
|
|
|
148
148
|
```python
|
|
149
149
|
with client.browser.create_session() as b:
|
|
150
150
|
b.navigate("https://shop.com/products")
|
|
151
|
+
b.close_modal() # Close popups
|
|
151
152
|
|
|
152
|
-
#
|
|
153
|
-
|
|
153
|
+
# BeautifulSoup parsing
|
|
154
|
+
soup = b.soup() # SoupWrapper with chainable API
|
|
155
|
+
for item in soup.select(".product"):
|
|
156
|
+
title = item.select_one("h2").text()
|
|
157
|
+
price = item.attr("data-price")
|
|
154
158
|
|
|
155
|
-
#
|
|
156
|
-
html = b.get_html("[role='feed']")
|
|
157
|
-
soup = b.parse_html(html) # Returns BeautifulSoup object
|
|
158
|
-
|
|
159
|
-
# Scroll & extract pattern
|
|
159
|
+
# Human-like scrolling with random delays
|
|
160
160
|
for _ in range(10):
|
|
161
|
-
|
|
162
|
-
# ... parse
|
|
163
|
-
b.scroll("down",
|
|
164
|
-
b.
|
|
161
|
+
soup = b.soup(".listings")
|
|
162
|
+
# ... parse ...
|
|
163
|
+
b.scroll("down", 700, human_like=True) # Natural micro-scrolls
|
|
164
|
+
b.wait_random(0.8, 1.5) # Random delay
|
|
165
|
+
|
|
166
|
+
# Scroll inside container (Facebook, Twitter feeds)
|
|
167
|
+
b.scroll("down", 800, container="[role='feed']")
|
|
168
|
+
|
|
169
|
+
# Click all "See more" buttons
|
|
170
|
+
b.click_all_by_text("See more")
|
|
165
171
|
|
|
166
172
|
# JS fetch (bypass CORS, inherit cookies)
|
|
167
173
|
data = b.fetch_json("https://api.site.com/v1/items")
|
|
168
|
-
|
|
169
|
-
# Parallel fetch
|
|
170
|
-
results = b.fetch_all({
|
|
171
|
-
"users": "https://api.site.com/v1/users",
|
|
172
|
-
"orders": "https://api.site.com/v1/orders",
|
|
173
|
-
}, credentials=True)
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
| Method | Description |
|
|
177
177
|
|--------|-------------|
|
|
178
178
|
| `navigate(url)` | Go to URL |
|
|
179
179
|
| `click(selector)` | Click element |
|
|
180
|
+
| `click_all_by_text(text, role)` | Click all matching elements |
|
|
180
181
|
| `type(selector, text)` | Type text |
|
|
181
182
|
| `wait_for(selector, ms)` | Wait for element |
|
|
182
183
|
| `wait_seconds(n)` | Sleep |
|
|
184
|
+
| `wait_random(min, max)` | Random sleep |
|
|
183
185
|
| `extract(selector, attr)` | Get text/attr |
|
|
184
186
|
| `get_html(selector)` | Get HTML |
|
|
185
|
-
| `
|
|
187
|
+
| `soup(selector)` | → SoupWrapper |
|
|
186
188
|
| `parse_html(html)` | → BeautifulSoup |
|
|
187
|
-
| `extract_data(item, fields, limit)` | Bulk extract |
|
|
188
189
|
| `fetch_json(url)` | JS fetch → dict |
|
|
189
|
-
| `fetch_all(urls
|
|
190
|
+
| `fetch_all(urls)` | Parallel fetch |
|
|
190
191
|
| `execute_js(code)` | Run async JS |
|
|
191
192
|
| `screenshot()` | PNG bytes |
|
|
192
|
-
| `scroll(dir, amount)` | Scroll page |
|
|
193
|
+
| `scroll(dir, amount, ...)` | Scroll page/container |
|
|
193
194
|
| `scroll_to(selector)` | Scroll to element |
|
|
194
195
|
| `get_scroll_info()` | Position + page size |
|
|
195
|
-
| `infinite_scroll(fn, limit)` | Smart scroll loop |
|
|
196
196
|
| `hover(selector)` | Hover |
|
|
197
197
|
| `select(selector, value)` | Dropdown select |
|
|
198
198
|
| `close_modal()` | Close dialogs |
|
|
199
199
|
| `get/set_cookies()` | Cookie management |
|
|
200
200
|
|
|
201
|
+
**scroll() parameters:**
|
|
202
|
+
- `direction`: "up", "down", "left", "right"
|
|
203
|
+
- `amount`: pixels to scroll
|
|
204
|
+
- `smooth`: animate scroll (default True)
|
|
205
|
+
- `human_like`: random micro-scrolls + variation
|
|
206
|
+
- `container`: CSS selector for scroll container
|
|
207
|
+
|
|
201
208
|
## SDKBaseModel
|
|
202
209
|
|
|
203
210
|
Auto-cleaning Pydantic model for scraped data:
|
|
@@ -107,56 +107,63 @@ health: Health = result.output # Typed!
|
|
|
107
107
|
```python
|
|
108
108
|
with client.browser.create_session() as b:
|
|
109
109
|
b.navigate("https://shop.com/products")
|
|
110
|
+
b.close_modal() # Close popups
|
|
110
111
|
|
|
111
|
-
#
|
|
112
|
-
|
|
112
|
+
# BeautifulSoup parsing
|
|
113
|
+
soup = b.soup() # SoupWrapper with chainable API
|
|
114
|
+
for item in soup.select(".product"):
|
|
115
|
+
title = item.select_one("h2").text()
|
|
116
|
+
price = item.attr("data-price")
|
|
113
117
|
|
|
114
|
-
#
|
|
115
|
-
html = b.get_html("[role='feed']")
|
|
116
|
-
soup = b.parse_html(html) # Returns BeautifulSoup object
|
|
117
|
-
|
|
118
|
-
# Scroll & extract pattern
|
|
118
|
+
# Human-like scrolling with random delays
|
|
119
119
|
for _ in range(10):
|
|
120
|
-
|
|
121
|
-
# ... parse
|
|
122
|
-
b.scroll("down",
|
|
123
|
-
b.
|
|
120
|
+
soup = b.soup(".listings")
|
|
121
|
+
# ... parse ...
|
|
122
|
+
b.scroll("down", 700, human_like=True) # Natural micro-scrolls
|
|
123
|
+
b.wait_random(0.8, 1.5) # Random delay
|
|
124
|
+
|
|
125
|
+
# Scroll inside container (Facebook, Twitter feeds)
|
|
126
|
+
b.scroll("down", 800, container="[role='feed']")
|
|
127
|
+
|
|
128
|
+
# Click all "See more" buttons
|
|
129
|
+
b.click_all_by_text("See more")
|
|
124
130
|
|
|
125
131
|
# JS fetch (bypass CORS, inherit cookies)
|
|
126
132
|
data = b.fetch_json("https://api.site.com/v1/items")
|
|
127
|
-
|
|
128
|
-
# Parallel fetch
|
|
129
|
-
results = b.fetch_all({
|
|
130
|
-
"users": "https://api.site.com/v1/users",
|
|
131
|
-
"orders": "https://api.site.com/v1/orders",
|
|
132
|
-
}, credentials=True)
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
| Method | Description |
|
|
136
136
|
|--------|-------------|
|
|
137
137
|
| `navigate(url)` | Go to URL |
|
|
138
138
|
| `click(selector)` | Click element |
|
|
139
|
+
| `click_all_by_text(text, role)` | Click all matching elements |
|
|
139
140
|
| `type(selector, text)` | Type text |
|
|
140
141
|
| `wait_for(selector, ms)` | Wait for element |
|
|
141
142
|
| `wait_seconds(n)` | Sleep |
|
|
143
|
+
| `wait_random(min, max)` | Random sleep |
|
|
142
144
|
| `extract(selector, attr)` | Get text/attr |
|
|
143
145
|
| `get_html(selector)` | Get HTML |
|
|
144
|
-
| `
|
|
146
|
+
| `soup(selector)` | → SoupWrapper |
|
|
145
147
|
| `parse_html(html)` | → BeautifulSoup |
|
|
146
|
-
| `extract_data(item, fields, limit)` | Bulk extract |
|
|
147
148
|
| `fetch_json(url)` | JS fetch → dict |
|
|
148
|
-
| `fetch_all(urls
|
|
149
|
+
| `fetch_all(urls)` | Parallel fetch |
|
|
149
150
|
| `execute_js(code)` | Run async JS |
|
|
150
151
|
| `screenshot()` | PNG bytes |
|
|
151
|
-
| `scroll(dir, amount)` | Scroll page |
|
|
152
|
+
| `scroll(dir, amount, ...)` | Scroll page/container |
|
|
152
153
|
| `scroll_to(selector)` | Scroll to element |
|
|
153
154
|
| `get_scroll_info()` | Position + page size |
|
|
154
|
-
| `infinite_scroll(fn, limit)` | Smart scroll loop |
|
|
155
155
|
| `hover(selector)` | Hover |
|
|
156
156
|
| `select(selector, value)` | Dropdown select |
|
|
157
157
|
| `close_modal()` | Close dialogs |
|
|
158
158
|
| `get/set_cookies()` | Cookie management |
|
|
159
159
|
|
|
160
|
+
**scroll() parameters:**
|
|
161
|
+
- `direction`: "up", "down", "left", "right"
|
|
162
|
+
- `amount`: pixels to scroll
|
|
163
|
+
- `smooth`: animate scroll (default True)
|
|
164
|
+
- `human_like`: random micro-scrolls + variation
|
|
165
|
+
- `container`: CSS selector for scroll container
|
|
166
|
+
|
|
160
167
|
## SDKBaseModel
|
|
161
168
|
|
|
162
169
|
Auto-cleaning Pydantic model for scraped data:
|
|
@@ -4,9 +4,17 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
7
9
|
from cmdop.services.browser.base.session import BaseSession
|
|
8
|
-
from cmdop.services.browser.models import
|
|
10
|
+
from cmdop.services.browser.models import (
|
|
11
|
+
BrowserCookie,
|
|
12
|
+
BrowserState,
|
|
13
|
+
ScrollInfo,
|
|
14
|
+
ScrollResult,
|
|
15
|
+
)
|
|
9
16
|
from cmdop.services.browser.js import parse_json_result
|
|
17
|
+
from cmdop.services.browser.parsing import parse_html as _parse_html, SoupWrapper
|
|
10
18
|
|
|
11
19
|
if TYPE_CHECKING:
|
|
12
20
|
from cmdop.services.browser.aio.service import AsyncBrowserService
|
|
@@ -158,6 +166,112 @@ class AsyncBrowserSession(BaseSession):
|
|
|
158
166
|
self._session_id, item, fields_json, limit
|
|
159
167
|
)
|
|
160
168
|
|
|
169
|
+
# === HTML Parsing ===
|
|
170
|
+
|
|
171
|
+
async def parse_html(self, html: str | None = None, selector: str | None = None) -> "BeautifulSoup":
|
|
172
|
+
"""Parse HTML with BeautifulSoup."""
|
|
173
|
+
if html is None:
|
|
174
|
+
html = await self.get_html(selector)
|
|
175
|
+
return _parse_html(html)
|
|
176
|
+
|
|
177
|
+
async def soup(self, selector: str | None = None) -> SoupWrapper:
|
|
178
|
+
"""Get page HTML as SoupWrapper."""
|
|
179
|
+
html = await self.get_html(selector)
|
|
180
|
+
return SoupWrapper(html=html)
|
|
181
|
+
|
|
182
|
+
# === Scroll & Navigation ===
|
|
183
|
+
|
|
184
|
+
async def scroll(
|
|
185
|
+
self,
|
|
186
|
+
direction: str = "down",
|
|
187
|
+
amount: int = 500,
|
|
188
|
+
selector: str | None = None,
|
|
189
|
+
smooth: bool = True,
|
|
190
|
+
human_like: bool = False,
|
|
191
|
+
container: str | None = None,
|
|
192
|
+
) -> ScrollResult:
|
|
193
|
+
"""Scroll the page or container."""
|
|
194
|
+
js = self._build_scroll(direction, amount, selector, smooth, human_like, container)
|
|
195
|
+
result = await self.execute_script(js)
|
|
196
|
+
data = parse_json_result(result) or {}
|
|
197
|
+
return ScrollResult(
|
|
198
|
+
success=data.get("success", False),
|
|
199
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
200
|
+
scrolled_by=int(data.get("scrolledBy", 0)),
|
|
201
|
+
at_bottom=data.get("atBottom", False),
|
|
202
|
+
error=data.get("error"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def scroll_to(self, selector: str) -> ScrollResult:
|
|
206
|
+
"""Scroll element into view."""
|
|
207
|
+
return await self.scroll(selector=selector)
|
|
208
|
+
|
|
209
|
+
async def scroll_to_bottom(self) -> ScrollResult:
|
|
210
|
+
"""Scroll to page bottom."""
|
|
211
|
+
js = self._build_scroll_to_bottom()
|
|
212
|
+
result = await self.execute_script(js)
|
|
213
|
+
data = parse_json_result(result) or {}
|
|
214
|
+
return ScrollResult(
|
|
215
|
+
success=data.get("success", False),
|
|
216
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
217
|
+
scrolled_by=int(data.get("scrolledBy", 0)),
|
|
218
|
+
at_bottom=True,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
async def get_scroll_info(self) -> ScrollInfo:
|
|
222
|
+
"""Get current scroll position and page dimensions."""
|
|
223
|
+
js = self._build_get_scroll_info()
|
|
224
|
+
result = await self.execute_script(js)
|
|
225
|
+
data = parse_json_result(result) or {}
|
|
226
|
+
return ScrollInfo(
|
|
227
|
+
scroll_x=int(data.get("scrollX", 0)),
|
|
228
|
+
scroll_y=int(data.get("scrollY", 0)),
|
|
229
|
+
page_height=int(data.get("pageHeight", 0)),
|
|
230
|
+
page_width=int(data.get("pageWidth", 0)),
|
|
231
|
+
viewport_height=int(data.get("viewportHeight", 0)),
|
|
232
|
+
viewport_width=int(data.get("viewportWidth", 0)),
|
|
233
|
+
at_bottom=data.get("atBottom", False),
|
|
234
|
+
at_top=data.get("atTop", True),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# === UI Interaction Helpers ===
|
|
238
|
+
|
|
239
|
+
async def hover(self, selector: str) -> bool:
|
|
240
|
+
"""Hover over element."""
|
|
241
|
+
js = self._build_hover(selector)
|
|
242
|
+
result = await self.execute_script(js)
|
|
243
|
+
data = parse_json_result(result) or {}
|
|
244
|
+
return data.get("success", False)
|
|
245
|
+
|
|
246
|
+
async def select(self, selector: str, value: str | None = None, text: str | None = None) -> dict:
|
|
247
|
+
"""Select option from dropdown."""
|
|
248
|
+
js = self._build_select(selector, value, text)
|
|
249
|
+
result = await self.execute_script(js)
|
|
250
|
+
return parse_json_result(result) or {}
|
|
251
|
+
|
|
252
|
+
async def close_modal(self, selectors: list[str] | None = None) -> bool:
|
|
253
|
+
"""Try to close modal/dialog."""
|
|
254
|
+
js = self._build_close_modal(selectors)
|
|
255
|
+
result = await self.execute_script(js)
|
|
256
|
+
data = parse_json_result(result) or {}
|
|
257
|
+
return data.get("success", False)
|
|
258
|
+
|
|
259
|
+
async def wait_seconds(self, seconds: float) -> None:
|
|
260
|
+
"""Wait for specified seconds."""
|
|
261
|
+
await asyncio.sleep(seconds)
|
|
262
|
+
|
|
263
|
+
async def wait_random(self, min_sec: float = 0.5, max_sec: float = 1.5) -> None:
|
|
264
|
+
"""Wait for random time between min and max seconds."""
|
|
265
|
+
import random
|
|
266
|
+
await asyncio.sleep(min_sec + random.random() * (max_sec - min_sec))
|
|
267
|
+
|
|
268
|
+
async def click_all_by_text(self, text: str, role: str = "button") -> int:
|
|
269
|
+
"""Click all elements containing specific text."""
|
|
270
|
+
js = self._build_click_all_by_text(text, role)
|
|
271
|
+
result = await self.execute_script(js)
|
|
272
|
+
data = parse_json_result(result) or {}
|
|
273
|
+
return data.get("clicked", 0)
|
|
274
|
+
|
|
161
275
|
# === Context Manager ===
|
|
162
276
|
|
|
163
277
|
async def close(self) -> None:
|
|
@@ -10,6 +10,13 @@ from cmdop.services.browser.js import (
|
|
|
10
10
|
build_async_js,
|
|
11
11
|
build_fetch_js,
|
|
12
12
|
build_fetch_all_js,
|
|
13
|
+
build_scroll_js,
|
|
14
|
+
build_scroll_to_bottom_js,
|
|
15
|
+
build_get_scroll_info_js,
|
|
16
|
+
build_hover_js,
|
|
17
|
+
build_select_js,
|
|
18
|
+
build_close_modal_js,
|
|
19
|
+
build_click_all_by_text_js,
|
|
13
20
|
parse_json_result,
|
|
14
21
|
)
|
|
15
22
|
|
|
@@ -70,3 +77,43 @@ class BaseSession(ABC):
|
|
|
70
77
|
def _parse_fetch_all(self, result: Any) -> dict[str, Any]:
|
|
71
78
|
"""Ensure fetch_all returns dict."""
|
|
72
79
|
return result if isinstance(result, dict) else {}
|
|
80
|
+
|
|
81
|
+
# === Scroll helpers ===
|
|
82
|
+
|
|
83
|
+
def _build_scroll(
|
|
84
|
+
self,
|
|
85
|
+
direction: str,
|
|
86
|
+
amount: int,
|
|
87
|
+
selector: str | None,
|
|
88
|
+
smooth: bool,
|
|
89
|
+
human_like: bool,
|
|
90
|
+
container: str | None = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Build scroll JS."""
|
|
93
|
+
return build_scroll_js(direction, amount, selector, smooth, human_like, container)
|
|
94
|
+
|
|
95
|
+
def _build_scroll_to_bottom(self) -> str:
|
|
96
|
+
"""Build scroll to bottom JS."""
|
|
97
|
+
return build_scroll_to_bottom_js()
|
|
98
|
+
|
|
99
|
+
def _build_get_scroll_info(self) -> str:
|
|
100
|
+
"""Build get scroll info JS."""
|
|
101
|
+
return build_get_scroll_info_js()
|
|
102
|
+
|
|
103
|
+
# === Interaction helpers ===
|
|
104
|
+
|
|
105
|
+
def _build_hover(self, selector: str) -> str:
|
|
106
|
+
"""Build hover JS."""
|
|
107
|
+
return build_hover_js(selector)
|
|
108
|
+
|
|
109
|
+
def _build_select(self, selector: str, value: str | None, text: str | None) -> str:
|
|
110
|
+
"""Build select JS."""
|
|
111
|
+
return build_select_js(selector, value, text)
|
|
112
|
+
|
|
113
|
+
def _build_close_modal(self, selectors: list[str] | None) -> str:
|
|
114
|
+
"""Build close modal JS."""
|
|
115
|
+
return build_close_modal_js(selectors)
|
|
116
|
+
|
|
117
|
+
def _build_click_all_by_text(self, text: str, role: str) -> str:
|
|
118
|
+
"""Build click all by text JS."""
|
|
119
|
+
return build_click_all_by_text_js(text, role)
|
|
@@ -28,6 +28,7 @@ from cmdop.services.browser.js.interaction import (
|
|
|
28
28
|
build_hover_js,
|
|
29
29
|
build_select_js,
|
|
30
30
|
build_close_modal_js,
|
|
31
|
+
build_click_all_by_text_js,
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
__all__ = [
|
|
@@ -46,4 +47,5 @@ __all__ = [
|
|
|
46
47
|
"build_hover_js",
|
|
47
48
|
"build_select_js",
|
|
48
49
|
"build_close_modal_js",
|
|
50
|
+
"build_click_all_by_text_js",
|
|
49
51
|
]
|
|
@@ -107,3 +107,31 @@ def build_close_modal_js(
|
|
|
107
107
|
return JSON.stringify({{ success: false, error: 'No modal close button found' }});
|
|
108
108
|
}})()
|
|
109
109
|
"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_click_all_by_text_js(text: str, role: str = "button") -> str:
|
|
113
|
+
"""
|
|
114
|
+
Build JS to click all elements containing specific text.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
text: Text to match (case-insensitive)
|
|
118
|
+
role: Element role to filter (default: "button")
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
JS code that returns { clicked: number }
|
|
122
|
+
"""
|
|
123
|
+
return f"""
|
|
124
|
+
(function() {{
|
|
125
|
+
const elements = document.querySelectorAll('[role="{role}"]');
|
|
126
|
+
let clicked = 0;
|
|
127
|
+
const targetText = "{text}".toLowerCase();
|
|
128
|
+
elements.forEach(el => {{
|
|
129
|
+
const elText = el.textContent.trim().toLowerCase();
|
|
130
|
+
if (elText === targetText || elText.includes(targetText)) {{
|
|
131
|
+
el.click();
|
|
132
|
+
clicked++;
|
|
133
|
+
}}
|
|
134
|
+
}});
|
|
135
|
+
return JSON.stringify({{ clicked: clicked }});
|
|
136
|
+
}})()
|
|
137
|
+
"""
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Scroll JavaScript builders for browser automation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_scroll_js(
|
|
9
|
+
direction: str = "down",
|
|
10
|
+
amount: int = 500,
|
|
11
|
+
selector: str | None = None,
|
|
12
|
+
smooth: bool = True,
|
|
13
|
+
human_like: bool = False,
|
|
14
|
+
container: str | None = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Build JS for scrolling.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
direction: "up", "down", "left", "right"
|
|
21
|
+
amount: Pixels to scroll (ignored if selector provided)
|
|
22
|
+
selector: CSS selector to scroll element into view
|
|
23
|
+
smooth: Use smooth scroll animation (default True)
|
|
24
|
+
human_like: Add random variations for natural scrolling
|
|
25
|
+
container: CSS selector for scroll container (default: window)
|
|
26
|
+
"""
|
|
27
|
+
if selector:
|
|
28
|
+
behavior = "smooth" if smooth else "instant"
|
|
29
|
+
return f"""
|
|
30
|
+
(async function() {{
|
|
31
|
+
const el = document.querySelector("{selector}");
|
|
32
|
+
if (el) {{
|
|
33
|
+
el.scrollIntoView({{ behavior: '{behavior}', block: 'center' }});
|
|
34
|
+
await new Promise(r => setTimeout(r, {300 if smooth else 50}));
|
|
35
|
+
return JSON.stringify({{ success: true, scrollY: window.scrollY }});
|
|
36
|
+
}}
|
|
37
|
+
return JSON.stringify({{ success: false, error: 'Element not found' }});
|
|
38
|
+
}})()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
behavior = "smooth" if smooth else "instant"
|
|
42
|
+
|
|
43
|
+
# Container scroll target
|
|
44
|
+
container_js = f'document.querySelector("{container}")' if container else "null"
|
|
45
|
+
|
|
46
|
+
if human_like:
|
|
47
|
+
# Human-like scroll with random variation and micro-scrolls
|
|
48
|
+
return f"""
|
|
49
|
+
(async function() {{
|
|
50
|
+
const container = {container_js};
|
|
51
|
+
const scrollTarget = container || window;
|
|
52
|
+
const getScrollY = () => container ? container.scrollTop : window.scrollY;
|
|
53
|
+
const getMaxScroll = () => container
|
|
54
|
+
? container.scrollHeight - container.clientHeight
|
|
55
|
+
: document.body.scrollHeight - window.innerHeight;
|
|
56
|
+
|
|
57
|
+
const before = getScrollY();
|
|
58
|
+
const baseAmount = {amount};
|
|
59
|
+
const direction = "{direction}";
|
|
60
|
+
|
|
61
|
+
// Random variation ±15%
|
|
62
|
+
const variation = 0.85 + Math.random() * 0.3;
|
|
63
|
+
const actualAmount = Math.round(baseAmount * variation);
|
|
64
|
+
|
|
65
|
+
// Split into 2-4 micro-scrolls
|
|
66
|
+
const steps = 2 + Math.floor(Math.random() * 3);
|
|
67
|
+
const stepAmount = actualAmount / steps;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < steps; i++) {{
|
|
70
|
+
const stepVariation = 0.8 + Math.random() * 0.4;
|
|
71
|
+
const thisStep = Math.round(stepAmount * stepVariation);
|
|
72
|
+
|
|
73
|
+
let x = 0, y = 0;
|
|
74
|
+
if (direction === "down") y = thisStep;
|
|
75
|
+
else if (direction === "up") y = -thisStep;
|
|
76
|
+
else if (direction === "right") x = thisStep;
|
|
77
|
+
else if (direction === "left") x = -thisStep;
|
|
78
|
+
|
|
79
|
+
if (container) {{
|
|
80
|
+
container.scrollBy({{ left: x, top: y, behavior: '{behavior}' }});
|
|
81
|
+
}} else {{
|
|
82
|
+
window.scrollBy({{ left: x, top: y, behavior: '{behavior}' }});
|
|
83
|
+
}}
|
|
84
|
+
|
|
85
|
+
// Random micro-pause 50-150ms between steps
|
|
86
|
+
if (i < steps - 1) {{
|
|
87
|
+
await new Promise(r => setTimeout(r, 50 + Math.random() * 100));
|
|
88
|
+
}}
|
|
89
|
+
}}
|
|
90
|
+
|
|
91
|
+
// Wait for smooth scroll to finish
|
|
92
|
+
await new Promise(r => setTimeout(r, {250 if smooth else 50}));
|
|
93
|
+
|
|
94
|
+
const after = getScrollY();
|
|
95
|
+
return JSON.stringify({{
|
|
96
|
+
success: true,
|
|
97
|
+
scrollY: after,
|
|
98
|
+
scrolledBy: after - before,
|
|
99
|
+
atBottom: after >= getMaxScroll() - 50
|
|
100
|
+
}});
|
|
101
|
+
}})()
|
|
102
|
+
"""
|
|
103
|
+
else:
|
|
104
|
+
# Standard scroll
|
|
105
|
+
scroll_x = 0
|
|
106
|
+
scroll_y = 0
|
|
107
|
+
if direction == "down":
|
|
108
|
+
scroll_y = amount
|
|
109
|
+
elif direction == "up":
|
|
110
|
+
scroll_y = -amount
|
|
111
|
+
elif direction == "right":
|
|
112
|
+
scroll_x = amount
|
|
113
|
+
elif direction == "left":
|
|
114
|
+
scroll_x = -amount
|
|
115
|
+
|
|
116
|
+
return f"""
|
|
117
|
+
(async function() {{
|
|
118
|
+
const container = {container_js};
|
|
119
|
+
const getScrollY = () => container ? container.scrollTop : window.scrollY;
|
|
120
|
+
const getMaxScroll = () => container
|
|
121
|
+
? container.scrollHeight - container.clientHeight
|
|
122
|
+
: document.body.scrollHeight - window.innerHeight;
|
|
123
|
+
|
|
124
|
+
const before = getScrollY();
|
|
125
|
+
|
|
126
|
+
if (container) {{
|
|
127
|
+
container.scrollBy({{ left: {scroll_x}, top: {scroll_y}, behavior: '{behavior}' }});
|
|
128
|
+
}} else {{
|
|
129
|
+
window.scrollBy({{ left: {scroll_x}, top: {scroll_y}, behavior: '{behavior}' }});
|
|
130
|
+
}}
|
|
131
|
+
|
|
132
|
+
// Wait for scroll to complete
|
|
133
|
+
await new Promise(r => setTimeout(r, {200 if smooth else 30}));
|
|
134
|
+
|
|
135
|
+
const after = getScrollY();
|
|
136
|
+
return JSON.stringify({{
|
|
137
|
+
success: true,
|
|
138
|
+
scrollY: after,
|
|
139
|
+
scrolledBy: after - before,
|
|
140
|
+
atBottom: after >= getMaxScroll() - 50
|
|
141
|
+
}});
|
|
142
|
+
}})()
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_scroll_to_bottom_js() -> str:
|
|
147
|
+
"""Build JS to scroll to page bottom."""
|
|
148
|
+
return """
|
|
149
|
+
(function() {
|
|
150
|
+
const before = window.scrollY;
|
|
151
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
152
|
+
return JSON.stringify({
|
|
153
|
+
success: true,
|
|
154
|
+
scrollY: window.scrollY,
|
|
155
|
+
scrolledBy: window.scrollY - before,
|
|
156
|
+
atBottom: true
|
|
157
|
+
});
|
|
158
|
+
})()
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_infinite_scroll_js(
|
|
163
|
+
seen_keys: list[str],
|
|
164
|
+
key_selector: str = "a[href]",
|
|
165
|
+
key_attr: str = "href",
|
|
166
|
+
container_selector: str = "body",
|
|
167
|
+
) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Build JS for infinite scroll with deduplication.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
seen_keys: List of already seen keys (e.g., URLs)
|
|
173
|
+
key_selector: CSS selector for key elements
|
|
174
|
+
key_attr: Attribute to use as key (default: href)
|
|
175
|
+
container_selector: Container to find elements in
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
JS that returns {new_keys: [...], at_bottom: bool}
|
|
179
|
+
"""
|
|
180
|
+
seen_json = json.dumps(seen_keys)
|
|
181
|
+
return f"""
|
|
182
|
+
(function() {{
|
|
183
|
+
const seen = new Set({seen_json});
|
|
184
|
+
const container = document.querySelector("{container_selector}");
|
|
185
|
+
if (!container) return JSON.stringify({{ new_keys: [], at_bottom: true, error: 'Container not found' }});
|
|
186
|
+
|
|
187
|
+
const elements = container.querySelectorAll("{key_selector}");
|
|
188
|
+
const newKeys = [];
|
|
189
|
+
|
|
190
|
+
elements.forEach(el => {{
|
|
191
|
+
const key = el.getAttribute("{key_attr}") || el.textContent.trim();
|
|
192
|
+
if (key && !seen.has(key)) {{
|
|
193
|
+
seen.add(key);
|
|
194
|
+
newKeys.push(key);
|
|
195
|
+
}}
|
|
196
|
+
}});
|
|
197
|
+
|
|
198
|
+
const atBottom = (window.innerHeight + window.scrollY) >= document.body.scrollHeight - 100;
|
|
199
|
+
|
|
200
|
+
return JSON.stringify({{
|
|
201
|
+
new_keys: newKeys,
|
|
202
|
+
at_bottom: atBottom,
|
|
203
|
+
total_seen: seen.size
|
|
204
|
+
}});
|
|
205
|
+
}})()
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def build_get_scroll_info_js() -> str:
|
|
210
|
+
"""Build JS to get current scroll position and page dimensions."""
|
|
211
|
+
return """
|
|
212
|
+
(function() {
|
|
213
|
+
return JSON.stringify({
|
|
214
|
+
scrollX: window.scrollX,
|
|
215
|
+
scrollY: window.scrollY,
|
|
216
|
+
pageHeight: document.body.scrollHeight,
|
|
217
|
+
pageWidth: document.body.scrollWidth,
|
|
218
|
+
viewportHeight: window.innerHeight,
|
|
219
|
+
viewportWidth: window.innerWidth,
|
|
220
|
+
atBottom: (window.innerHeight + window.scrollY) >= document.body.scrollHeight - 10,
|
|
221
|
+
atTop: window.scrollY <= 10
|
|
222
|
+
});
|
|
223
|
+
})()
|
|
224
|
+
"""
|