cmdop 0.1.15__py3-none-any.whl → 0.1.16__py3-none-any.whl
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/__init__.py +14 -10
- cmdop/helpers/__init__.py +4 -10
- cmdop/helpers/cleaner.py +53 -0
- cmdop/helpers/formatting.py +7 -62
- cmdop/logging.py +252 -0
- cmdop/services/browser/__init__.py +33 -6
- cmdop/services/browser/aio/__init__.py +6 -0
- cmdop/services/browser/{async_service.py → aio/service.py} +18 -6
- cmdop/services/browser/{async_session.py → aio/session.py} +76 -24
- cmdop/services/browser/base/__init__.py +6 -0
- cmdop/services/browser/base/service.py +62 -0
- cmdop/services/browser/base/session.py +72 -0
- cmdop/services/browser/js.py +109 -0
- cmdop/services/browser/sync/__init__.py +6 -0
- cmdop/services/browser/{service.py → sync/service.py} +17 -22
- cmdop/services/browser/{session.py → sync/session.py} +68 -26
- {cmdop-0.1.15.dist-info → cmdop-0.1.16.dist-info}/METADATA +138 -33
- {cmdop-0.1.15.dist-info → cmdop-0.1.16.dist-info}/RECORD +20 -13
- cmdop/services/browser/_base.py +0 -109
- {cmdop-0.1.15.dist-info → cmdop-0.1.16.dist-info}/WHEEL +0 -0
- {cmdop-0.1.15.dist-info → cmdop-0.1.16.dist-info}/licenses/LICENSE +0 -0
cmdop/__init__.py
CHANGED
|
@@ -116,14 +116,15 @@ from cmdop.discovery import (
|
|
|
116
116
|
get_online_agents,
|
|
117
117
|
list_agents,
|
|
118
118
|
)
|
|
119
|
-
from cmdop.helpers import
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
from cmdop.helpers import json_to_toon, JsonCleaner
|
|
120
|
+
from cmdop.logging import (
|
|
121
|
+
get_logger,
|
|
122
|
+
setup_logging,
|
|
123
|
+
find_project_root,
|
|
124
|
+
get_log_dir,
|
|
124
125
|
)
|
|
125
126
|
|
|
126
|
-
__version__ = "0.1.
|
|
127
|
+
__version__ = "0.1.16"
|
|
127
128
|
|
|
128
129
|
__all__ = [
|
|
129
130
|
# Version
|
|
@@ -217,8 +218,11 @@ __all__ = [
|
|
|
217
218
|
"BrowserNavigationError",
|
|
218
219
|
"BrowserElementNotFoundError",
|
|
219
220
|
# Helpers
|
|
220
|
-
"
|
|
221
|
-
"
|
|
222
|
-
|
|
223
|
-
"
|
|
221
|
+
"json_to_toon",
|
|
222
|
+
"JsonCleaner",
|
|
223
|
+
# Logging
|
|
224
|
+
"get_logger",
|
|
225
|
+
"setup_logging",
|
|
226
|
+
"find_project_root",
|
|
227
|
+
"get_log_dir",
|
|
224
228
|
]
|
cmdop/helpers/__init__.py
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
"""CMDOP SDK helpers."""
|
|
2
2
|
|
|
3
|
-
from cmdop.helpers.formatting import
|
|
4
|
-
|
|
5
|
-
flatten_json,
|
|
6
|
-
json_to_yaml,
|
|
7
|
-
json_to_text,
|
|
8
|
-
)
|
|
3
|
+
from cmdop.helpers.formatting import json_to_toon
|
|
4
|
+
from cmdop.helpers.cleaner import JsonCleaner
|
|
9
5
|
|
|
10
6
|
__all__ = [
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"json_to_yaml",
|
|
14
|
-
"json_to_text",
|
|
7
|
+
"json_to_toon",
|
|
8
|
+
"JsonCleaner",
|
|
15
9
|
]
|
cmdop/helpers/cleaner.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""JSON cleaning utilities for LLM consumption."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from toon_python import encode as toon_encode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JsonCleaner:
|
|
11
|
+
"""
|
|
12
|
+
Clean JSON and convert to TOON format for LLM.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
cleaner = JsonCleaner(noise_keys={"Photos", "images"})
|
|
16
|
+
toon_text = cleaner.to_toon(data)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
DEFAULT_NOISE_KEYS = frozenset({
|
|
20
|
+
"statusItemTypes",
|
|
21
|
+
"attributes",
|
|
22
|
+
"ordering",
|
|
23
|
+
"updatedDate",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
def __init__(self, noise_keys: set[str] | None = None):
|
|
27
|
+
self.noise_keys = self.DEFAULT_NOISE_KEYS | (noise_keys or set())
|
|
28
|
+
|
|
29
|
+
def compact(self, data: Any) -> Any:
|
|
30
|
+
"""Remove nulls, empty values, noise keys. Simplify {code,title}->title."""
|
|
31
|
+
def _process(obj: Any) -> Any:
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
# Simplify {code, title} pattern
|
|
34
|
+
keys = set(obj.keys())
|
|
35
|
+
if keys <= {"code", "title", "description"} and "title" in keys:
|
|
36
|
+
return obj.get("title") or obj.get("code")
|
|
37
|
+
# Clean dict
|
|
38
|
+
result = {}
|
|
39
|
+
for k, v in obj.items():
|
|
40
|
+
if k in self.noise_keys:
|
|
41
|
+
continue
|
|
42
|
+
cleaned = _process(v)
|
|
43
|
+
if cleaned is not None and cleaned != "" and cleaned != [] and cleaned != {}:
|
|
44
|
+
result[k] = cleaned
|
|
45
|
+
return result
|
|
46
|
+
elif isinstance(obj, list):
|
|
47
|
+
return [_process(item) for item in obj if _process(item) not in (None, "", [], {})]
|
|
48
|
+
return obj
|
|
49
|
+
return _process(data)
|
|
50
|
+
|
|
51
|
+
def to_toon(self, data: Any) -> str:
|
|
52
|
+
"""Compact and convert to TOON format."""
|
|
53
|
+
return toon_encode(self.compact(data))
|
cmdop/helpers/formatting.py
CHANGED
|
@@ -1,70 +1,15 @@
|
|
|
1
1
|
"""JSON formatting utilities."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
from flatten_json import flatten
|
|
5
|
+
from toon_python import encode as toon_encode
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
def json_to_toon(data: dict | list) -> str:
|
|
10
9
|
"""
|
|
11
|
-
JSON to
|
|
10
|
+
Convert JSON to TOON format (saves 30-50% tokens).
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
flat = fmt.flatten({"a": {"b": 1}}) # {"a.b": 1}
|
|
17
|
-
yaml_str = fmt.to_yaml(data)
|
|
18
|
-
text = fmt.to_text(data) # key: value lines
|
|
12
|
+
Example:
|
|
13
|
+
{"name": "Alice", "age": 25} → "name: Alice\\nage: 25"
|
|
19
14
|
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self, separator: str = "."):
|
|
22
|
-
self.separator = separator
|
|
23
|
-
|
|
24
|
-
def flatten(self, data: dict | list) -> dict:
|
|
25
|
-
"""
|
|
26
|
-
Flatten nested JSON to flat dict with dot notation.
|
|
27
|
-
|
|
28
|
-
{"a": {"b": 1}} -> {"a.b": 1}
|
|
29
|
-
"""
|
|
30
|
-
if isinstance(data, list):
|
|
31
|
-
data = {"items": data}
|
|
32
|
-
return flatten(data, self.separator)
|
|
33
|
-
|
|
34
|
-
def to_yaml(self, data: dict | list) -> str:
|
|
35
|
-
"""Convert to YAML string."""
|
|
36
|
-
return yaml.dump(
|
|
37
|
-
data,
|
|
38
|
-
allow_unicode=True,
|
|
39
|
-
default_flow_style=False,
|
|
40
|
-
sort_keys=False,
|
|
41
|
-
indent=2,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
def to_text(self, data: dict | list) -> str:
|
|
45
|
-
"""Convert to flat text (key: value per line)."""
|
|
46
|
-
flat = self.flatten(data)
|
|
47
|
-
lines = []
|
|
48
|
-
for key, value in flat.items():
|
|
49
|
-
if isinstance(value, str):
|
|
50
|
-
val_str = value
|
|
51
|
-
elif value is None:
|
|
52
|
-
val_str = "null"
|
|
53
|
-
elif isinstance(value, bool):
|
|
54
|
-
val_str = "true" if value else "false"
|
|
55
|
-
else:
|
|
56
|
-
val_str = str(value)
|
|
57
|
-
lines.append(f"{key}: {val_str}")
|
|
58
|
-
return "\n".join(lines)
|
|
59
|
-
|
|
60
|
-
def to_json(self, data: dict | list, indent: int = 2) -> str:
|
|
61
|
-
"""Convert to formatted JSON string."""
|
|
62
|
-
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Default instance
|
|
66
|
-
_fmt = JSONFormatter()
|
|
67
|
-
|
|
68
|
-
flatten_json = _fmt.flatten
|
|
69
|
-
json_to_yaml = _fmt.to_yaml
|
|
70
|
-
json_to_text = _fmt.to_text
|
|
15
|
+
return toon_encode(data)
|
cmdop/logging.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Universal Python logger with Rich console output and file persistence.
|
|
3
|
+
|
|
4
|
+
Auto-detects project root by looking for Python project markers:
|
|
5
|
+
- pyproject.toml
|
|
6
|
+
- setup.py
|
|
7
|
+
- requirements.txt
|
|
8
|
+
- .git
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from cmdop.logging import get_logger
|
|
12
|
+
|
|
13
|
+
log = get_logger(__name__)
|
|
14
|
+
log.info("Hello world")
|
|
15
|
+
log.error("Something failed", exc_info=True)
|
|
16
|
+
|
|
17
|
+
# Or with custom settings
|
|
18
|
+
log = get_logger(__name__, level="DEBUG", log_to_file=True)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Literal
|
|
28
|
+
|
|
29
|
+
# Try to import Rich, fallback to standard logging if not available
|
|
30
|
+
try:
|
|
31
|
+
from rich.console import Console
|
|
32
|
+
from rich.logging import RichHandler
|
|
33
|
+
from rich.traceback import install as install_rich_traceback
|
|
34
|
+
|
|
35
|
+
RICH_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
RICH_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"get_logger",
|
|
41
|
+
"setup_logging",
|
|
42
|
+
"find_project_root",
|
|
43
|
+
"get_log_dir",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Project markers to search for (in priority order)
|
|
47
|
+
PROJECT_MARKERS = [
|
|
48
|
+
"pyproject.toml",
|
|
49
|
+
"setup.py",
|
|
50
|
+
"setup.cfg",
|
|
51
|
+
"requirements.txt",
|
|
52
|
+
"Pipfile",
|
|
53
|
+
".git",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
57
|
+
|
|
58
|
+
# Cache for project root
|
|
59
|
+
_project_root_cache: Path | None = None
|
|
60
|
+
_logging_configured: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_project_root(start_path: Path | None = None) -> Path | None:
|
|
64
|
+
"""
|
|
65
|
+
Find Python project root by searching for project markers.
|
|
66
|
+
|
|
67
|
+
Searches upward from start_path (default: cwd) looking for:
|
|
68
|
+
- pyproject.toml
|
|
69
|
+
- setup.py
|
|
70
|
+
- requirements.txt
|
|
71
|
+
- .git
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Path to project root, or None if not found
|
|
75
|
+
"""
|
|
76
|
+
global _project_root_cache
|
|
77
|
+
|
|
78
|
+
if _project_root_cache is not None:
|
|
79
|
+
return _project_root_cache
|
|
80
|
+
|
|
81
|
+
if start_path is None:
|
|
82
|
+
start_path = Path.cwd()
|
|
83
|
+
|
|
84
|
+
path = start_path.resolve()
|
|
85
|
+
|
|
86
|
+
for current in [path] + list(path.parents):
|
|
87
|
+
for marker in PROJECT_MARKERS:
|
|
88
|
+
if (current / marker).exists():
|
|
89
|
+
_project_root_cache = current
|
|
90
|
+
return current
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_log_dir(app_name: str = "app") -> Path:
|
|
96
|
+
"""
|
|
97
|
+
Get log directory path.
|
|
98
|
+
|
|
99
|
+
Priority:
|
|
100
|
+
1. Project root / logs (if project root found)
|
|
101
|
+
2. ~/.local/logs/{app_name} (Linux/macOS)
|
|
102
|
+
3. Current working directory / logs
|
|
103
|
+
|
|
104
|
+
Creates directory if it doesn't exist.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
app_name: Application name for fallback directory
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Path to log directory
|
|
111
|
+
"""
|
|
112
|
+
# Try project root first
|
|
113
|
+
project_root = find_project_root()
|
|
114
|
+
if project_root:
|
|
115
|
+
log_dir = project_root / "logs"
|
|
116
|
+
else:
|
|
117
|
+
# Fallback to home directory
|
|
118
|
+
home = Path.home()
|
|
119
|
+
if sys.platform == "darwin":
|
|
120
|
+
log_dir = home / "Library" / "Logs" / app_name
|
|
121
|
+
elif sys.platform == "win32":
|
|
122
|
+
log_dir = home / "AppData" / "Local" / app_name / "logs"
|
|
123
|
+
else: # Linux and others
|
|
124
|
+
log_dir = home / ".local" / "logs" / app_name
|
|
125
|
+
|
|
126
|
+
# Create directory if needed
|
|
127
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
return log_dir
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def setup_logging(
|
|
132
|
+
level: LogLevel = "INFO",
|
|
133
|
+
log_to_file: bool = True,
|
|
134
|
+
log_to_console: bool = True,
|
|
135
|
+
app_name: str = "app",
|
|
136
|
+
rich_tracebacks: bool = True,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Configure root logger with Rich console and file handlers.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
143
|
+
log_to_file: Whether to write logs to file
|
|
144
|
+
log_to_console: Whether to output to console
|
|
145
|
+
app_name: Application name for log file naming
|
|
146
|
+
rich_tracebacks: Install Rich traceback handler for exceptions
|
|
147
|
+
"""
|
|
148
|
+
global _logging_configured
|
|
149
|
+
|
|
150
|
+
if _logging_configured:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
root_logger = logging.getLogger()
|
|
154
|
+
root_logger.setLevel(getattr(logging, level))
|
|
155
|
+
|
|
156
|
+
# Clear existing handlers
|
|
157
|
+
root_logger.handlers.clear()
|
|
158
|
+
|
|
159
|
+
# Console handler with Rich (if available)
|
|
160
|
+
if log_to_console:
|
|
161
|
+
if RICH_AVAILABLE:
|
|
162
|
+
console = Console(stderr=True)
|
|
163
|
+
console_handler = RichHandler(
|
|
164
|
+
console=console,
|
|
165
|
+
show_time=True,
|
|
166
|
+
show_path=True,
|
|
167
|
+
rich_tracebacks=True,
|
|
168
|
+
tracebacks_show_locals=True,
|
|
169
|
+
markup=True,
|
|
170
|
+
)
|
|
171
|
+
console_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
172
|
+
else:
|
|
173
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
174
|
+
console_handler.setFormatter(
|
|
175
|
+
logging.Formatter(
|
|
176
|
+
"%(asctime)s | %(levelname)-8s | %(name)s - %(message)s",
|
|
177
|
+
datefmt="%H:%M:%S",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
root_logger.addHandler(console_handler)
|
|
181
|
+
|
|
182
|
+
# File handler
|
|
183
|
+
if log_to_file:
|
|
184
|
+
log_dir = get_log_dir(app_name)
|
|
185
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
186
|
+
log_file = log_dir / f"{app_name}_{date_str}.log"
|
|
187
|
+
|
|
188
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
189
|
+
file_handler.setFormatter(
|
|
190
|
+
logging.Formatter(
|
|
191
|
+
"%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d - %(message)s",
|
|
192
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
root_logger.addHandler(file_handler)
|
|
196
|
+
|
|
197
|
+
# Install Rich tracebacks globally
|
|
198
|
+
if rich_tracebacks and RICH_AVAILABLE:
|
|
199
|
+
install_rich_traceback(show_locals=True, suppress=[])
|
|
200
|
+
|
|
201
|
+
_logging_configured = True
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_logger(
|
|
205
|
+
name: str | None = None,
|
|
206
|
+
level: LogLevel | None = None,
|
|
207
|
+
log_to_file: bool = True,
|
|
208
|
+
app_name: str = "app",
|
|
209
|
+
) -> logging.Logger:
|
|
210
|
+
"""
|
|
211
|
+
Get a configured logger instance.
|
|
212
|
+
|
|
213
|
+
On first call, sets up the logging system with Rich console output
|
|
214
|
+
and file persistence.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
name: Logger name (typically __name__)
|
|
218
|
+
level: Override log level (default: INFO)
|
|
219
|
+
log_to_file: Whether to write to log file
|
|
220
|
+
app_name: Application name for log file
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Configured logger instance
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
log = get_logger(__name__)
|
|
227
|
+
log.info("Starting process")
|
|
228
|
+
log.debug("Debug details: %s", data)
|
|
229
|
+
log.error("Failed!", exc_info=True)
|
|
230
|
+
"""
|
|
231
|
+
# Setup logging system on first call
|
|
232
|
+
setup_logging(
|
|
233
|
+
level=level or "INFO",
|
|
234
|
+
log_to_file=log_to_file,
|
|
235
|
+
app_name=app_name,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
logger = logging.getLogger(name)
|
|
239
|
+
|
|
240
|
+
# Override level if specified
|
|
241
|
+
if level:
|
|
242
|
+
logger.setLevel(getattr(logging, level))
|
|
243
|
+
|
|
244
|
+
return logger
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Convenience aliases
|
|
248
|
+
debug = lambda msg, *args, **kw: get_logger().debug(msg, *args, **kw)
|
|
249
|
+
info = lambda msg, *args, **kw: get_logger().info(msg, *args, **kw)
|
|
250
|
+
warning = lambda msg, *args, **kw: get_logger().warning(msg, *args, **kw)
|
|
251
|
+
error = lambda msg, *args, **kw: get_logger().error(msg, *args, **kw)
|
|
252
|
+
critical = lambda msg, *args, **kw: get_logger().critical(msg, *args, **kw)
|
|
@@ -1,17 +1,44 @@
|
|
|
1
|
-
"""Browser service
|
|
1
|
+
"""Browser automation service.
|
|
2
|
+
|
|
3
|
+
Structure:
|
|
4
|
+
browser/
|
|
5
|
+
models.py - Data models (BrowserCookie, BrowserState)
|
|
6
|
+
js.py - JavaScript code builders
|
|
7
|
+
base/ - Base classes (BaseSession, BaseServiceMixin)
|
|
8
|
+
sync/ - Sync implementation (BrowserSession, BrowserService)
|
|
9
|
+
aio/ - Async implementation (AsyncBrowserSession, AsyncBrowserService)
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
# Sync
|
|
13
|
+
with client.browser.create_session() as session:
|
|
14
|
+
session.navigate("https://example.com")
|
|
15
|
+
data = session.fetch_all(urls, headers={"Accept": "application/json"})
|
|
16
|
+
|
|
17
|
+
# Async
|
|
18
|
+
async with client.browser.create_session() as session:
|
|
19
|
+
await session.navigate("https://example.com")
|
|
20
|
+
data = await session.fetch_all(urls, headers={"Accept": "application/json"})
|
|
21
|
+
"""
|
|
2
22
|
|
|
3
23
|
from cmdop.services.browser.models import BrowserCookie, BrowserState, raise_browser_error
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from cmdop.services.browser.
|
|
7
|
-
from cmdop.services.browser.
|
|
24
|
+
|
|
25
|
+
# Sync
|
|
26
|
+
from cmdop.services.browser.sync.session import BrowserSession
|
|
27
|
+
from cmdop.services.browser.sync.service import BrowserService
|
|
28
|
+
|
|
29
|
+
# Async
|
|
30
|
+
from cmdop.services.browser.aio.session import AsyncBrowserSession
|
|
31
|
+
from cmdop.services.browser.aio.service import AsyncBrowserService
|
|
8
32
|
|
|
9
33
|
__all__ = [
|
|
34
|
+
# Models
|
|
10
35
|
"BrowserCookie",
|
|
11
36
|
"BrowserState",
|
|
37
|
+
"raise_browser_error",
|
|
38
|
+
# Sync
|
|
12
39
|
"BrowserSession",
|
|
13
40
|
"BrowserService",
|
|
41
|
+
# Async
|
|
14
42
|
"AsyncBrowserSession",
|
|
15
43
|
"AsyncBrowserService",
|
|
16
|
-
"raise_browser_error",
|
|
17
44
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Asynchronous browser service."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -6,17 +6,16 @@ import json
|
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from cmdop.services.base import BaseService
|
|
9
|
+
from cmdop.services.browser.base.service import BaseServiceMixin, cookie_to_pb, pb_to_cookie
|
|
9
10
|
from cmdop.services.browser.models import BrowserCookie, BrowserState, raise_browser_error
|
|
10
|
-
from cmdop.services.browser.async_session import AsyncBrowserSession
|
|
11
|
-
from cmdop.services.browser._base import cookie_to_pb, pb_to_cookie
|
|
12
11
|
|
|
13
12
|
if TYPE_CHECKING:
|
|
14
13
|
from cmdop.transport.base import BaseTransport
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
class AsyncBrowserService(BaseService):
|
|
16
|
+
class AsyncBrowserService(BaseService, BaseServiceMixin):
|
|
18
17
|
"""
|
|
19
|
-
|
|
18
|
+
Asynchronous browser service.
|
|
20
19
|
|
|
21
20
|
Example:
|
|
22
21
|
async with client.browser.create_session() as session:
|
|
@@ -36,6 +35,8 @@ class AsyncBrowserService(BaseService):
|
|
|
36
35
|
self._stub = TerminalStreamingServiceStub(self._async_channel)
|
|
37
36
|
return self._stub
|
|
38
37
|
|
|
38
|
+
# === Session Management ===
|
|
39
|
+
|
|
39
40
|
async def create_session(
|
|
40
41
|
self,
|
|
41
42
|
start_url: str | None = None,
|
|
@@ -44,8 +45,9 @@ class AsyncBrowserService(BaseService):
|
|
|
44
45
|
headless: bool = False,
|
|
45
46
|
width: int = 1280,
|
|
46
47
|
height: int = 800,
|
|
47
|
-
) -> AsyncBrowserSession:
|
|
48
|
+
) -> "AsyncBrowserSession":
|
|
48
49
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserCreateSessionRequest
|
|
50
|
+
from cmdop.services.browser.aio.session import AsyncBrowserSession
|
|
49
51
|
|
|
50
52
|
request = BrowserCreateSessionRequest(
|
|
51
53
|
provider=provider,
|
|
@@ -71,6 +73,8 @@ class AsyncBrowserService(BaseService):
|
|
|
71
73
|
if not response.success:
|
|
72
74
|
raise RuntimeError(f"Failed to close browser session: {response.error}")
|
|
73
75
|
|
|
76
|
+
# === Navigation & Interaction ===
|
|
77
|
+
|
|
74
78
|
async def navigate(self, session_id: str, url: str, timeout_ms: int = 30000) -> str:
|
|
75
79
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserNavigateRequest
|
|
76
80
|
|
|
@@ -132,6 +136,8 @@ class AsyncBrowserService(BaseService):
|
|
|
132
136
|
|
|
133
137
|
return response.found
|
|
134
138
|
|
|
139
|
+
# === Extraction ===
|
|
140
|
+
|
|
135
141
|
async def extract(
|
|
136
142
|
self, session_id: str, selector: str, attr: str | None = None, limit: int = 100
|
|
137
143
|
) -> list[str]:
|
|
@@ -194,6 +200,8 @@ class AsyncBrowserService(BaseService):
|
|
|
194
200
|
|
|
195
201
|
return response.text
|
|
196
202
|
|
|
203
|
+
# === JavaScript ===
|
|
204
|
+
|
|
197
205
|
async def execute_script(self, session_id: str, script: str) -> str:
|
|
198
206
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserExecuteScriptRequest
|
|
199
207
|
|
|
@@ -209,6 +217,8 @@ class AsyncBrowserService(BaseService):
|
|
|
209
217
|
|
|
210
218
|
return response.result
|
|
211
219
|
|
|
220
|
+
# === State & Cookies ===
|
|
221
|
+
|
|
212
222
|
async def screenshot(self, session_id: str, full_page: bool = False) -> bytes:
|
|
213
223
|
from cmdop._generated.rpc_messages.browser_pb2 import BrowserScreenshotRequest
|
|
214
224
|
|
|
@@ -265,6 +275,8 @@ class AsyncBrowserService(BaseService):
|
|
|
265
275
|
|
|
266
276
|
return [pb_to_cookie(c) for c in response.cookies]
|
|
267
277
|
|
|
278
|
+
# === Parser helpers ===
|
|
279
|
+
|
|
268
280
|
async def validate_selectors(
|
|
269
281
|
self, session_id: str, item: str, fields: dict[str, str]
|
|
270
282
|
) -> dict:
|