cmdop 0.1.16__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.18/PKG-INFO +256 -0
- cmdop-0.1.18/README.md +215 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/pyproject.toml +6 -1
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/__init__.py +1 -1
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/session.py +115 -1
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/session.py +47 -0
- cmdop-0.1.18/src/cmdop/services/browser/js/__init__.py +51 -0
- cmdop-0.1.18/src/cmdop/services/browser/js/core.py +38 -0
- cmdop-0.1.16/src/cmdop/services/browser/js.py → cmdop-0.1.18/src/cmdop/services/browser/js/fetch.py +1 -34
- cmdop-0.1.18/src/cmdop/services/browser/js/interaction.py +137 -0
- cmdop-0.1.18/src/cmdop/services/browser/js/scroll.py +224 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/models.py +32 -0
- cmdop-0.1.18/src/cmdop/services/browser/parsing.py +104 -0
- cmdop-0.1.18/src/cmdop/services/browser/sync/session.py +467 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/discovery.py +25 -1
- cmdop-0.1.16/PKG-INFO +0 -464
- cmdop-0.1.16/README.md +0 -426
- cmdop-0.1.16/src/cmdop/services/browser/sync/session.py +0 -171
- {cmdop-0.1.16 → cmdop-0.1.18}/.gitignore +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/LICENSE +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/message_pool.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/py.typed +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.pyi +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2_grpc.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/enums.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/logger.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/models.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/models.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/retry.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/schema.json +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/enums.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/logger.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/retry.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/schema.json +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/models.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/models.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/enums.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/logger.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/retry.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/schema.json +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/models.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/sync_client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/client.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/config.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/discovery.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/exceptions.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/cleaner.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/formatting.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/logging.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/agent.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/base.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/config.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/extract.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/files.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/terminal.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/py.typed +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/agent.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/base.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/service.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/service.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/sync/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/sync/service.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/extract.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/files.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/terminal.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/base.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/handlers.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/terminal.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/__init__.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/auth.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/base.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/local.py +0 -0
- {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/remote.py +0 -0
cmdop-0.1.18/PKG-INFO
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmdop
|
|
3
|
+
Version: 0.1.18
|
|
4
|
+
Summary: Python SDK for CMDOP agent interaction
|
|
5
|
+
Project-URL: Homepage, https://cmdop.com
|
|
6
|
+
Project-URL: Documentation, https://cmdop.com
|
|
7
|
+
Project-URL: Repository, https://github.com/markolofsen/cmdop-client
|
|
8
|
+
Author: CMDOP Team
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,automation,cmdop,terminal
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: beautifulsoup4>=4.12.0
|
|
23
|
+
Requires-Dist: grpcio>=1.60.0
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: lxml>=5.0.0
|
|
26
|
+
Requires-Dist: protobuf>=4.25.0
|
|
27
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
28
|
+
Requires-Dist: pydantic>=2.5.0
|
|
29
|
+
Requires-Dist: rich>=13.0.0
|
|
30
|
+
Requires-Dist: toon-python>=0.1.2
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-grpc-aio>=0.3.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# cmdop
|
|
43
|
+
|
|
44
|
+
**Any machine. One API.**
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from cmdop import CMDOPClient
|
|
48
|
+
|
|
49
|
+
with CMDOPClient.remote(api_key="cmd_xxx") as server:
|
|
50
|
+
server.terminal.execute("docker restart app")
|
|
51
|
+
server.files.write("/etc/nginx/nginx.conf", new_config)
|
|
52
|
+
logs = server.files.read("/var/log/app.log")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
No SSH. No VPN. No open ports.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## How
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Your Code ──── Cloud Relay ──── Agent (on server)
|
|
63
|
+
│
|
|
64
|
+
Outbound only, works through any NAT/firewall
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Agent connects out. Your code connects to relay. Done.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install cmdop
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from cmdop import CMDOPClient, AsyncCMDOPClient
|
|
79
|
+
|
|
80
|
+
# Remote (via cloud relay)
|
|
81
|
+
with CMDOPClient.remote(api_key="cmd_xxx") as client:
|
|
82
|
+
client.files.list("/home")
|
|
83
|
+
|
|
84
|
+
# Local (direct IPC)
|
|
85
|
+
with CMDOPClient.local() as client:
|
|
86
|
+
client.terminal.execute("ls -la")
|
|
87
|
+
|
|
88
|
+
# Async
|
|
89
|
+
async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
|
|
90
|
+
await client.files.read("/etc/hostname")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Terminal
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
session = server.terminal.create()
|
|
99
|
+
server.terminal.send_input(session.session_id, "kubectl get pods\n")
|
|
100
|
+
output = server.terminal.get_history(session.session_id)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
| Method | Description |
|
|
104
|
+
|--------|-------------|
|
|
105
|
+
| `create(shell)` | Start session |
|
|
106
|
+
| `send_input(id, data)` | Send commands |
|
|
107
|
+
| `get_history(id)` | Get output |
|
|
108
|
+
| `resize(id, cols, rows)` | Resize |
|
|
109
|
+
| `send_signal(id, signal)` | SIGINT/SIGTERM |
|
|
110
|
+
| `close(id)` | End session |
|
|
111
|
+
|
|
112
|
+
## Files
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
server.files.list("/var/log")
|
|
116
|
+
server.files.read("/etc/nginx/nginx.conf")
|
|
117
|
+
server.files.write("/tmp/config.json", b'{"key": "value"}')
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Method | Description |
|
|
121
|
+
|--------|-------------|
|
|
122
|
+
| `list(path)` | List dir |
|
|
123
|
+
| `read(path)` | Read file |
|
|
124
|
+
| `write(path, content)` | Write file |
|
|
125
|
+
| `delete(path)` | Delete |
|
|
126
|
+
| `copy/move(src, dst)` | Copy/Move |
|
|
127
|
+
| `mkdir(path)` | Create dir |
|
|
128
|
+
| `info(path)` | Metadata |
|
|
129
|
+
|
|
130
|
+
## Agent
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pydantic import BaseModel
|
|
134
|
+
|
|
135
|
+
class Health(BaseModel):
|
|
136
|
+
status: str
|
|
137
|
+
cpu: float
|
|
138
|
+
issues: list[str]
|
|
139
|
+
|
|
140
|
+
result = server.agent.run("Check server health", output_schema=Health)
|
|
141
|
+
health: Health = result.output # Typed!
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Browser
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
with client.browser.create_session() as b:
|
|
150
|
+
b.navigate("https://shop.com/products")
|
|
151
|
+
b.close_modal() # Close popups
|
|
152
|
+
|
|
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")
|
|
158
|
+
|
|
159
|
+
# Human-like scrolling with random delays
|
|
160
|
+
for _ in range(10):
|
|
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")
|
|
171
|
+
|
|
172
|
+
# JS fetch (bypass CORS, inherit cookies)
|
|
173
|
+
data = b.fetch_json("https://api.site.com/v1/items")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Method | Description |
|
|
177
|
+
|--------|-------------|
|
|
178
|
+
| `navigate(url)` | Go to URL |
|
|
179
|
+
| `click(selector)` | Click element |
|
|
180
|
+
| `click_all_by_text(text, role)` | Click all matching elements |
|
|
181
|
+
| `type(selector, text)` | Type text |
|
|
182
|
+
| `wait_for(selector, ms)` | Wait for element |
|
|
183
|
+
| `wait_seconds(n)` | Sleep |
|
|
184
|
+
| `wait_random(min, max)` | Random sleep |
|
|
185
|
+
| `extract(selector, attr)` | Get text/attr |
|
|
186
|
+
| `get_html(selector)` | Get HTML |
|
|
187
|
+
| `soup(selector)` | → SoupWrapper |
|
|
188
|
+
| `parse_html(html)` | → BeautifulSoup |
|
|
189
|
+
| `fetch_json(url)` | JS fetch → dict |
|
|
190
|
+
| `fetch_all(urls)` | Parallel fetch |
|
|
191
|
+
| `execute_js(code)` | Run async JS |
|
|
192
|
+
| `screenshot()` | PNG bytes |
|
|
193
|
+
| `scroll(dir, amount, ...)` | Scroll page/container |
|
|
194
|
+
| `scroll_to(selector)` | Scroll to element |
|
|
195
|
+
| `get_scroll_info()` | Position + page size |
|
|
196
|
+
| `hover(selector)` | Hover |
|
|
197
|
+
| `select(selector, value)` | Dropdown select |
|
|
198
|
+
| `close_modal()` | Close dialogs |
|
|
199
|
+
| `get/set_cookies()` | Cookie management |
|
|
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
|
+
|
|
208
|
+
## SDKBaseModel
|
|
209
|
+
|
|
210
|
+
Auto-cleaning Pydantic model for scraped data:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from cmdop import SDKBaseModel
|
|
214
|
+
|
|
215
|
+
class Product(SDKBaseModel):
|
|
216
|
+
__base_url__ = "https://shop.com"
|
|
217
|
+
name: str = "" # " iPhone 15 \n" → "iPhone 15"
|
|
218
|
+
price: int = 0 # "$1,299.00" → 1299
|
|
219
|
+
rating: float = 0 # "4.5 stars" → 4.5
|
|
220
|
+
url: str = "" # "/p/123" → "https://shop.com/p/123"
|
|
221
|
+
|
|
222
|
+
products = Product.from_list(raw["items"]) # Auto dedupe + filter
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Utilities
|
|
228
|
+
|
|
229
|
+
**Logging:**
|
|
230
|
+
```python
|
|
231
|
+
from cmdop import get_logger
|
|
232
|
+
log = get_logger(__name__)
|
|
233
|
+
log.info("Starting") # Rich console + auto file logging
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**TOON Format (30-50% token savings):**
|
|
237
|
+
```python
|
|
238
|
+
from cmdop import json_to_toon, JsonCleaner
|
|
239
|
+
toon = json_to_toon({"name": "Alice", "age": 25})
|
|
240
|
+
# → "name: Alice\nage: 25"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Requirements
|
|
246
|
+
|
|
247
|
+
- Python 3.10+
|
|
248
|
+
- CMDOP agent on target
|
|
249
|
+
|
|
250
|
+
## Links
|
|
251
|
+
|
|
252
|
+
[cmdop.com](https://cmdop.com)
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT
|
cmdop-0.1.18/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# cmdop
|
|
2
|
+
|
|
3
|
+
**Any machine. One API.**
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from cmdop import CMDOPClient
|
|
7
|
+
|
|
8
|
+
with CMDOPClient.remote(api_key="cmd_xxx") as server:
|
|
9
|
+
server.terminal.execute("docker restart app")
|
|
10
|
+
server.files.write("/etc/nginx/nginx.conf", new_config)
|
|
11
|
+
logs = server.files.read("/var/log/app.log")
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
No SSH. No VPN. No open ports.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## How
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Your Code ──── Cloud Relay ──── Agent (on server)
|
|
22
|
+
│
|
|
23
|
+
Outbound only, works through any NAT/firewall
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Agent connects out. Your code connects to relay. Done.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install cmdop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from cmdop import CMDOPClient, AsyncCMDOPClient
|
|
38
|
+
|
|
39
|
+
# Remote (via cloud relay)
|
|
40
|
+
with CMDOPClient.remote(api_key="cmd_xxx") as client:
|
|
41
|
+
client.files.list("/home")
|
|
42
|
+
|
|
43
|
+
# Local (direct IPC)
|
|
44
|
+
with CMDOPClient.local() as client:
|
|
45
|
+
client.terminal.execute("ls -la")
|
|
46
|
+
|
|
47
|
+
# Async
|
|
48
|
+
async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
|
|
49
|
+
await client.files.read("/etc/hostname")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Terminal
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
session = server.terminal.create()
|
|
58
|
+
server.terminal.send_input(session.session_id, "kubectl get pods\n")
|
|
59
|
+
output = server.terminal.get_history(session.session_id)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Method | Description |
|
|
63
|
+
|--------|-------------|
|
|
64
|
+
| `create(shell)` | Start session |
|
|
65
|
+
| `send_input(id, data)` | Send commands |
|
|
66
|
+
| `get_history(id)` | Get output |
|
|
67
|
+
| `resize(id, cols, rows)` | Resize |
|
|
68
|
+
| `send_signal(id, signal)` | SIGINT/SIGTERM |
|
|
69
|
+
| `close(id)` | End session |
|
|
70
|
+
|
|
71
|
+
## Files
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
server.files.list("/var/log")
|
|
75
|
+
server.files.read("/etc/nginx/nginx.conf")
|
|
76
|
+
server.files.write("/tmp/config.json", b'{"key": "value"}')
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
| Method | Description |
|
|
80
|
+
|--------|-------------|
|
|
81
|
+
| `list(path)` | List dir |
|
|
82
|
+
| `read(path)` | Read file |
|
|
83
|
+
| `write(path, content)` | Write file |
|
|
84
|
+
| `delete(path)` | Delete |
|
|
85
|
+
| `copy/move(src, dst)` | Copy/Move |
|
|
86
|
+
| `mkdir(path)` | Create dir |
|
|
87
|
+
| `info(path)` | Metadata |
|
|
88
|
+
|
|
89
|
+
## Agent
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pydantic import BaseModel
|
|
93
|
+
|
|
94
|
+
class Health(BaseModel):
|
|
95
|
+
status: str
|
|
96
|
+
cpu: float
|
|
97
|
+
issues: list[str]
|
|
98
|
+
|
|
99
|
+
result = server.agent.run("Check server health", output_schema=Health)
|
|
100
|
+
health: Health = result.output # Typed!
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Browser
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
with client.browser.create_session() as b:
|
|
109
|
+
b.navigate("https://shop.com/products")
|
|
110
|
+
b.close_modal() # Close popups
|
|
111
|
+
|
|
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")
|
|
117
|
+
|
|
118
|
+
# Human-like scrolling with random delays
|
|
119
|
+
for _ in range(10):
|
|
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")
|
|
130
|
+
|
|
131
|
+
# JS fetch (bypass CORS, inherit cookies)
|
|
132
|
+
data = b.fetch_json("https://api.site.com/v1/items")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Method | Description |
|
|
136
|
+
|--------|-------------|
|
|
137
|
+
| `navigate(url)` | Go to URL |
|
|
138
|
+
| `click(selector)` | Click element |
|
|
139
|
+
| `click_all_by_text(text, role)` | Click all matching elements |
|
|
140
|
+
| `type(selector, text)` | Type text |
|
|
141
|
+
| `wait_for(selector, ms)` | Wait for element |
|
|
142
|
+
| `wait_seconds(n)` | Sleep |
|
|
143
|
+
| `wait_random(min, max)` | Random sleep |
|
|
144
|
+
| `extract(selector, attr)` | Get text/attr |
|
|
145
|
+
| `get_html(selector)` | Get HTML |
|
|
146
|
+
| `soup(selector)` | → SoupWrapper |
|
|
147
|
+
| `parse_html(html)` | → BeautifulSoup |
|
|
148
|
+
| `fetch_json(url)` | JS fetch → dict |
|
|
149
|
+
| `fetch_all(urls)` | Parallel fetch |
|
|
150
|
+
| `execute_js(code)` | Run async JS |
|
|
151
|
+
| `screenshot()` | PNG bytes |
|
|
152
|
+
| `scroll(dir, amount, ...)` | Scroll page/container |
|
|
153
|
+
| `scroll_to(selector)` | Scroll to element |
|
|
154
|
+
| `get_scroll_info()` | Position + page size |
|
|
155
|
+
| `hover(selector)` | Hover |
|
|
156
|
+
| `select(selector, value)` | Dropdown select |
|
|
157
|
+
| `close_modal()` | Close dialogs |
|
|
158
|
+
| `get/set_cookies()` | Cookie management |
|
|
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
|
+
|
|
167
|
+
## SDKBaseModel
|
|
168
|
+
|
|
169
|
+
Auto-cleaning Pydantic model for scraped data:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from cmdop import SDKBaseModel
|
|
173
|
+
|
|
174
|
+
class Product(SDKBaseModel):
|
|
175
|
+
__base_url__ = "https://shop.com"
|
|
176
|
+
name: str = "" # " iPhone 15 \n" → "iPhone 15"
|
|
177
|
+
price: int = 0 # "$1,299.00" → 1299
|
|
178
|
+
rating: float = 0 # "4.5 stars" → 4.5
|
|
179
|
+
url: str = "" # "/p/123" → "https://shop.com/p/123"
|
|
180
|
+
|
|
181
|
+
products = Product.from_list(raw["items"]) # Auto dedupe + filter
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Utilities
|
|
187
|
+
|
|
188
|
+
**Logging:**
|
|
189
|
+
```python
|
|
190
|
+
from cmdop import get_logger
|
|
191
|
+
log = get_logger(__name__)
|
|
192
|
+
log.info("Starting") # Rich console + auto file logging
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**TOON Format (30-50% token savings):**
|
|
196
|
+
```python
|
|
197
|
+
from cmdop import json_to_toon, JsonCleaner
|
|
198
|
+
toon = json_to_toon({"name": "Alice", "age": 25})
|
|
199
|
+
# → "name: Alice\nage: 25"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Requirements
|
|
205
|
+
|
|
206
|
+
- Python 3.10+
|
|
207
|
+
- CMDOP agent on target
|
|
208
|
+
|
|
209
|
+
## Links
|
|
210
|
+
|
|
211
|
+
[cmdop.com](https://cmdop.com)
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "cmdop"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.18"
|
|
4
4
|
description = "Python SDK for CMDOP agent interaction"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -32,6 +32,9 @@ dependencies = [
|
|
|
32
32
|
"toon-python>=0.1.2",
|
|
33
33
|
# Logging
|
|
34
34
|
"rich>=13.0.0",
|
|
35
|
+
# HTML parsing (browser SDK)
|
|
36
|
+
"beautifulsoup4>=4.12.0",
|
|
37
|
+
"lxml>=5.0.0",
|
|
35
38
|
]
|
|
36
39
|
|
|
37
40
|
[project.optional-dependencies]
|
|
@@ -44,6 +47,8 @@ dev = [
|
|
|
44
47
|
"ruff>=0.1.0",
|
|
45
48
|
# For proto generation (still uses grpcio-tools with betterproto plugin)
|
|
46
49
|
"grpcio-tools>=1.60.0",
|
|
50
|
+
# HTML parsing for tests
|
|
51
|
+
"beautifulsoup4>=4.12.0",
|
|
47
52
|
]
|
|
48
53
|
|
|
49
54
|
[project.urls]
|
|
@@ -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)
|