webpilot-engine 2.0.0__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.
- adapters/__init__.py +14 -0
- adapters/base.py +34 -0
- adapters/browser_adapter.py +300 -0
- adapters/dom_scripts.py +518 -0
- adapters/envelope_builder.py +108 -0
- adapters/live_session_manager.py +326 -0
- adapters/upload_adapter.py +77 -0
- builder.py +83 -0
- cli.py +341 -0
- config/__init__.py +23 -0
- config/settings.py +339 -0
- constants/__init__.py +165 -0
- constants/contracts.py +95 -0
- constants/selectors.py +27 -0
- constants/timeouts.py +25 -0
- core/__init__.py +45 -0
- core/exceptions.py +33 -0
- core/models.py +273 -0
- engine.py +199 -0
- exceptions.py +23 -0
- mcp_server.py +347 -0
- models.py +29 -0
- orchestration/__init__.py +19 -0
- orchestration/context.py +71 -0
- orchestration/flow_orchestrator.py +358 -0
- orchestration/session_coordinator.py +88 -0
- services/__init__.py +17 -0
- services/auth_navigation_service.py +129 -0
- services/field_interaction_service.py +174 -0
- services/form_filler_service.py +106 -0
- services/inspection_formatter_service.py +68 -0
- services/inspection_service.py +40 -0
- services/reactive_interaction_service.py +174 -0
- supervisor/__init__.py +13 -0
- supervisor/client.py +171 -0
- supervisor/contracts.py +62 -0
- supervisor/master_daemon.py +303 -0
- supervisor/shell.py +194 -0
- supervisor/worker_process.py +400 -0
- validators/__init__.py +9 -0
- validators/form_validator.py +28 -0
- validators/payload_validator.py +25 -0
- webpilot_engine-2.0.0.dist-info/METADATA +299 -0
- webpilot_engine-2.0.0.dist-info/RECORD +47 -0
- webpilot_engine-2.0.0.dist-info/WHEEL +5 -0
- webpilot_engine-2.0.0.dist-info/entry_points.txt +6 -0
- webpilot_engine-2.0.0.dist-info/top_level.txt +14 -0
config/settings.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""Dedicated centralized configuration and settings module.
|
|
2
|
+
|
|
3
|
+
Enforces strict resolution precedence:
|
|
4
|
+
1. Environment variables (os.environ)
|
|
5
|
+
2. .env file
|
|
6
|
+
3. Dedicated JSON configuration files (settings.json, device_profile.json)
|
|
7
|
+
|
|
8
|
+
Zero hardcoded fallbacks in source code: coordinates, IPs, ports, device models,
|
|
9
|
+
and timeouts are exclusively sourced from configuration.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from exceptions import ConfigurationError
|
|
20
|
+
from models import DeviceProfile, ProxyConfig
|
|
21
|
+
|
|
22
|
+
# Project root and configuration paths
|
|
23
|
+
PROJECT_ROOT: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
24
|
+
CONFIG_DIR: str = os.path.join(PROJECT_ROOT, "config")
|
|
25
|
+
DEFAULT_SETTINGS_JSON: str = os.path.join(CONFIG_DIR, "settings.json")
|
|
26
|
+
DEFAULT_SETTINGS_EXAMPLE_JSON: str = os.path.join(CONFIG_DIR, "settings.example.json")
|
|
27
|
+
DEFAULT_DEVICE_PROFILE_JSON: str = os.path.join(CONFIG_DIR, "device_profile.json")
|
|
28
|
+
DEFAULT_DEVICE_PROFILE_EXAMPLE_JSON: str = os.path.join(CONFIG_DIR, "device_profile.example.json")
|
|
29
|
+
DEFAULT_ENV_FILE: str = os.path.join(PROJECT_ROOT, ".env")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_env_file(filepath: str) -> Dict[str, str]:
|
|
33
|
+
"""Parse a .env file into a dictionary without modifying os.environ."""
|
|
34
|
+
if not os.path.isfile(filepath):
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
env_vars: Dict[str, str] = {}
|
|
38
|
+
try:
|
|
39
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
40
|
+
for raw_line in f:
|
|
41
|
+
line = raw_line.strip()
|
|
42
|
+
if not line or line.startswith("#"):
|
|
43
|
+
continue
|
|
44
|
+
if "=" in line:
|
|
45
|
+
k, v = line.split("=", 1)
|
|
46
|
+
k = k.strip()
|
|
47
|
+
v = v.strip()
|
|
48
|
+
# Strip matching surrounding quotes
|
|
49
|
+
if len(v) >= 2 and ((v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'"))):
|
|
50
|
+
v = v[1:-1]
|
|
51
|
+
env_vars[k] = v
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
raise ConfigurationError(f"Failed to parse .env file at {filepath}: {exc}") from exc
|
|
54
|
+
|
|
55
|
+
return env_vars
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_json_file(primary_path: str, fallback_path: Optional[str] = None) -> Dict[str, Any]:
|
|
59
|
+
"""Loads a JSON configuration file, falling back to a template if primary is absent."""
|
|
60
|
+
target_path = primary_path
|
|
61
|
+
if not os.path.isfile(target_path):
|
|
62
|
+
if fallback_path and os.path.isfile(fallback_path):
|
|
63
|
+
target_path = fallback_path
|
|
64
|
+
else:
|
|
65
|
+
return {}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with open(target_path, "r", encoding="utf-8") as f:
|
|
69
|
+
data = json.load(f)
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
return data
|
|
72
|
+
raise ConfigurationError(f"JSON config at {target_path} must be an object/dict")
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
raise ConfigurationError(f"Failed to read JSON config at {target_path}: {exc}") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ValueResolver:
|
|
78
|
+
"""Resolves values following strict precedence: os.environ -> .env -> JSON."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, env_data: Dict[str, str], json_data: Dict[str, Any], example_json_data: Dict[str, Any]):
|
|
81
|
+
self.env_data = env_data
|
|
82
|
+
self.json_data = json_data
|
|
83
|
+
self.example_json_data = example_json_data
|
|
84
|
+
|
|
85
|
+
def resolve(
|
|
86
|
+
self,
|
|
87
|
+
json_key: str,
|
|
88
|
+
env_var: str,
|
|
89
|
+
default: Any = None,
|
|
90
|
+
cast_type: Optional[type] = None
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""Resolve a setting by priority."""
|
|
93
|
+
# 1. os.environ
|
|
94
|
+
if env_var in os.environ and os.environ[env_var] != "":
|
|
95
|
+
raw_val = os.environ[env_var]
|
|
96
|
+
return self._cast(raw_val, cast_type)
|
|
97
|
+
|
|
98
|
+
# 2. .env file
|
|
99
|
+
if env_var in self.env_data and self.env_data[env_var] != "":
|
|
100
|
+
raw_val = self.env_data[env_var]
|
|
101
|
+
return self._cast(raw_val, cast_type)
|
|
102
|
+
|
|
103
|
+
# 3. Dedicated JSON config
|
|
104
|
+
if json_key in self.json_data and self.json_data[json_key] is not None:
|
|
105
|
+
raw_val = self.json_data[json_key]
|
|
106
|
+
return self._cast(raw_val, cast_type)
|
|
107
|
+
|
|
108
|
+
# 4. Dedicated JSON example template
|
|
109
|
+
if json_key in self.example_json_data and self.example_json_data[json_key] is not None:
|
|
110
|
+
raw_val = self.example_json_data[json_key]
|
|
111
|
+
return self._cast(raw_val, cast_type)
|
|
112
|
+
|
|
113
|
+
# 5. Default
|
|
114
|
+
return default
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _cast(val: Any, cast_type: Optional[type]) -> Any:
|
|
118
|
+
if cast_type is None or val is None:
|
|
119
|
+
return val
|
|
120
|
+
if cast_type == bool:
|
|
121
|
+
if isinstance(val, bool):
|
|
122
|
+
return val
|
|
123
|
+
s = str(val).strip().lower()
|
|
124
|
+
return s in ("1", "true", "yes", "on", "t")
|
|
125
|
+
try:
|
|
126
|
+
return cast_type(val)
|
|
127
|
+
except (ValueError, TypeError) as exc:
|
|
128
|
+
raise ConfigurationError(f"Unable to cast value {val!r} to {cast_type.__name__}: {exc}") from exc
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class Settings:
|
|
133
|
+
"""Fully resolved runtime configuration."""
|
|
134
|
+
|
|
135
|
+
headless: bool
|
|
136
|
+
default_timeout_ms: int
|
|
137
|
+
form_ready_timeout_ms: int
|
|
138
|
+
submission_wait_ms: int
|
|
139
|
+
expand_section_wait_ms: int
|
|
140
|
+
screenshots_dir: str
|
|
141
|
+
cookies_path: Optional[str]
|
|
142
|
+
data_path: Optional[str]
|
|
143
|
+
settings_path: str
|
|
144
|
+
device_profile_path: str
|
|
145
|
+
proxy: Optional[ProxyConfig] = None
|
|
146
|
+
geolocation_latitude: Optional[float] = None
|
|
147
|
+
geolocation_longitude: Optional[float] = None
|
|
148
|
+
geolocation_accuracy: Optional[float] = None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
_SETTINGS_CACHE: Optional[Settings] = None
|
|
152
|
+
_DEVICE_PROFILE_CACHE: Optional[DeviceProfile] = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def reset_settings_cache() -> None:
|
|
156
|
+
"""Clear cached settings and device profile singletons."""
|
|
157
|
+
global _SETTINGS_CACHE, _DEVICE_PROFILE_CACHE
|
|
158
|
+
_SETTINGS_CACHE = None
|
|
159
|
+
_DEVICE_PROFILE_CACHE = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_settings(
|
|
163
|
+
env_file: Optional[str] = None,
|
|
164
|
+
settings_json_path: Optional[str] = None,
|
|
165
|
+
reload: bool = False
|
|
166
|
+
) -> Settings:
|
|
167
|
+
"""Build or retrieve the resolved Settings singleton."""
|
|
168
|
+
global _SETTINGS_CACHE
|
|
169
|
+
if _SETTINGS_CACHE is not None and not reload:
|
|
170
|
+
return _SETTINGS_CACHE
|
|
171
|
+
|
|
172
|
+
target_env = env_file or os.environ.get("APP_ENV_FILE") or DEFAULT_ENV_FILE
|
|
173
|
+
env_data = parse_env_file(target_env)
|
|
174
|
+
|
|
175
|
+
target_settings_path = (
|
|
176
|
+
settings_json_path
|
|
177
|
+
or os.environ.get("APP_SETTINGS_PATH")
|
|
178
|
+
or env_data.get("APP_SETTINGS_PATH")
|
|
179
|
+
or DEFAULT_SETTINGS_JSON
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
json_data = load_json_file(target_settings_path, DEFAULT_SETTINGS_EXAMPLE_JSON)
|
|
183
|
+
example_json_data = load_json_file(DEFAULT_SETTINGS_EXAMPLE_JSON)
|
|
184
|
+
|
|
185
|
+
resolver = ValueResolver(env_data, json_data, example_json_data)
|
|
186
|
+
|
|
187
|
+
# Proxy resolution
|
|
188
|
+
proxy_server = resolver.resolve("proxy_server", "APP_PROXY_SERVER", cast_type=str)
|
|
189
|
+
if not proxy_server and isinstance(json_data.get("proxy"), dict):
|
|
190
|
+
proxy_server = json_data["proxy"].get("server")
|
|
191
|
+
proxy_user = resolver.resolve("proxy_username", "APP_PROXY_USERNAME", cast_type=str)
|
|
192
|
+
if not proxy_user and isinstance(json_data.get("proxy"), dict):
|
|
193
|
+
proxy_user = json_data["proxy"].get("username")
|
|
194
|
+
proxy_pass = resolver.resolve("proxy_password", "APP_PROXY_PASSWORD", cast_type=str)
|
|
195
|
+
if not proxy_pass and isinstance(json_data.get("proxy"), dict):
|
|
196
|
+
proxy_pass = json_data["proxy"].get("password")
|
|
197
|
+
|
|
198
|
+
proxy_cfg = ProxyConfig(server=proxy_server, username=proxy_user, password=proxy_pass) if proxy_server else None
|
|
199
|
+
|
|
200
|
+
# Geolocation resolution
|
|
201
|
+
geo_lat = resolver.resolve("geolocation_latitude", "APP_GEOLOCATION_LATITUDE", cast_type=float)
|
|
202
|
+
if geo_lat is None and isinstance(json_data.get("geolocation"), dict):
|
|
203
|
+
geo_lat = json_data["geolocation"].get("latitude")
|
|
204
|
+
geo_lon = resolver.resolve("geolocation_longitude", "APP_GEOLOCATION_LONGITUDE", cast_type=float)
|
|
205
|
+
if geo_lon is None and isinstance(json_data.get("geolocation"), dict):
|
|
206
|
+
geo_lon = json_data["geolocation"].get("longitude")
|
|
207
|
+
geo_acc = resolver.resolve("geolocation_accuracy", "APP_GEOLOCATION_ACCURACY", default=10.0, cast_type=float)
|
|
208
|
+
if geo_acc is None and isinstance(json_data.get("geolocation"), dict):
|
|
209
|
+
geo_acc = json_data["geolocation"].get("accuracy", 10.0)
|
|
210
|
+
|
|
211
|
+
# Device profile path
|
|
212
|
+
dev_path = resolver.resolve(
|
|
213
|
+
"device_profile_path",
|
|
214
|
+
"APP_DEVICE_PROFILE_PATH",
|
|
215
|
+
default=DEFAULT_DEVICE_PROFILE_JSON,
|
|
216
|
+
cast_type=str
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
settings = Settings(
|
|
220
|
+
headless=resolver.resolve("headless", "APP_HEADLESS", default=True, cast_type=bool),
|
|
221
|
+
default_timeout_ms=resolver.resolve("default_timeout_ms", "APP_DEFAULT_TIMEOUT_MS", default=45000, cast_type=int),
|
|
222
|
+
form_ready_timeout_ms=resolver.resolve("form_ready_timeout_ms", "APP_FORM_READY_TIMEOUT_MS", default=10000, cast_type=int),
|
|
223
|
+
submission_wait_ms=resolver.resolve("submission_wait_ms", "APP_SUBMISSION_WAIT_MS", default=8000, cast_type=int),
|
|
224
|
+
expand_section_wait_ms=resolver.resolve("expand_section_wait_ms", "APP_EXPAND_SECTION_WAIT_MS", default=800, cast_type=int),
|
|
225
|
+
screenshots_dir=resolver.resolve("screenshots_dir", "APP_SCREENSHOTS_DIR", default="screenshots", cast_type=str),
|
|
226
|
+
cookies_path=resolver.resolve("cookies_path", "APP_COOKIES_PATH", cast_type=str),
|
|
227
|
+
data_path=resolver.resolve("data_path", "APP_DATA_PATH", default=None, cast_type=str),
|
|
228
|
+
settings_path=target_settings_path,
|
|
229
|
+
device_profile_path=dev_path,
|
|
230
|
+
proxy=proxy_cfg,
|
|
231
|
+
geolocation_latitude=float(geo_lat) if geo_lat is not None else None,
|
|
232
|
+
geolocation_longitude=float(geo_lon) if geo_lon is not None else None,
|
|
233
|
+
geolocation_accuracy=float(geo_acc) if geo_acc is not None else None,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
_SETTINGS_CACHE = settings
|
|
237
|
+
return settings
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def load_device_profile(
|
|
241
|
+
profile_path: Optional[str] = None,
|
|
242
|
+
env_file: Optional[str] = None,
|
|
243
|
+
reload: bool = False
|
|
244
|
+
) -> DeviceProfile:
|
|
245
|
+
"""Loads and resolves DeviceProfile adhering strictly to 3-tier precedence.
|
|
246
|
+
|
|
247
|
+
Order:
|
|
248
|
+
1. os.environ
|
|
249
|
+
2. .env file
|
|
250
|
+
3. Dedicated JSON file (device_profile.json or device_profile.example.json)
|
|
251
|
+
"""
|
|
252
|
+
global _DEVICE_PROFILE_CACHE
|
|
253
|
+
if _DEVICE_PROFILE_CACHE is not None and not reload and profile_path is None:
|
|
254
|
+
return _DEVICE_PROFILE_CACHE
|
|
255
|
+
|
|
256
|
+
target_env = env_file or os.environ.get("APP_ENV_FILE") or DEFAULT_ENV_FILE
|
|
257
|
+
env_data = parse_env_file(target_env)
|
|
258
|
+
|
|
259
|
+
if profile_path is not None:
|
|
260
|
+
if not os.path.isfile(profile_path):
|
|
261
|
+
raise ConfigurationError(f"Specified device profile file not found: '{profile_path}'")
|
|
262
|
+
target_file = profile_path
|
|
263
|
+
example_fallback = None
|
|
264
|
+
else:
|
|
265
|
+
target_file = (
|
|
266
|
+
os.environ.get("APP_DEVICE_PROFILE_PATH")
|
|
267
|
+
or env_data.get("APP_DEVICE_PROFILE_PATH")
|
|
268
|
+
or DEFAULT_DEVICE_PROFILE_JSON
|
|
269
|
+
)
|
|
270
|
+
example_fallback = DEFAULT_DEVICE_PROFILE_EXAMPLE_JSON
|
|
271
|
+
|
|
272
|
+
json_data = load_json_file(target_file, example_fallback)
|
|
273
|
+
example_data = load_json_file(example_fallback) if example_fallback else {}
|
|
274
|
+
|
|
275
|
+
if not json_data and not example_data:
|
|
276
|
+
raise ConfigurationError(
|
|
277
|
+
f"Device profile configuration not found at '{target_file}'"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
resolver = ValueResolver(env_data, json_data, example_data)
|
|
281
|
+
|
|
282
|
+
# Resolve viewport
|
|
283
|
+
vp_w = resolver.resolve("viewport_width", "DEVICE_VIEWPORT_WIDTH", cast_type=int)
|
|
284
|
+
if vp_w is None and isinstance(json_data.get("viewport"), dict):
|
|
285
|
+
vp_w = json_data["viewport"].get("width")
|
|
286
|
+
if vp_w is None and isinstance(example_data.get("viewport"), dict):
|
|
287
|
+
vp_w = example_data["viewport"].get("width")
|
|
288
|
+
|
|
289
|
+
vp_h = resolver.resolve("viewport_height", "DEVICE_VIEWPORT_HEIGHT", cast_type=int)
|
|
290
|
+
if vp_h is None and isinstance(json_data.get("viewport"), dict):
|
|
291
|
+
vp_h = json_data["viewport"].get("height")
|
|
292
|
+
if vp_h is None and isinstance(example_data.get("viewport"), dict):
|
|
293
|
+
vp_h = example_data["viewport"].get("height")
|
|
294
|
+
|
|
295
|
+
# Resolve geolocation
|
|
296
|
+
geo_lat = resolver.resolve("geolocation_latitude", "DEVICE_GEOLOCATION_LATITUDE", cast_type=float)
|
|
297
|
+
if geo_lat is None and isinstance(json_data.get("geolocation"), dict):
|
|
298
|
+
geo_lat = json_data["geolocation"].get("latitude")
|
|
299
|
+
geo_lon = resolver.resolve("geolocation_longitude", "DEVICE_GEOLOCATION_LONGITUDE", cast_type=float)
|
|
300
|
+
if geo_lon is None and isinstance(json_data.get("geolocation"), dict):
|
|
301
|
+
geo_lon = json_data["geolocation"].get("longitude")
|
|
302
|
+
geo_acc = resolver.resolve("geolocation_accuracy", "DEVICE_GEOLOCATION_ACCURACY", cast_type=float)
|
|
303
|
+
if geo_acc is None and isinstance(json_data.get("geolocation"), dict):
|
|
304
|
+
geo_acc = json_data["geolocation"].get("accuracy")
|
|
305
|
+
|
|
306
|
+
# Required attributes check: must exist in profile configuration
|
|
307
|
+
device_name = resolver.resolve("device_name", "DEVICE_NAME", cast_type=str)
|
|
308
|
+
user_agent = resolver.resolve("user_agent", "DEVICE_USER_AGENT", cast_type=str)
|
|
309
|
+
|
|
310
|
+
if not user_agent:
|
|
311
|
+
raise ConfigurationError(
|
|
312
|
+
f"Required 'user_agent' could not be resolved from environment or profile at {target_file}"
|
|
313
|
+
)
|
|
314
|
+
if not vp_w or not vp_h:
|
|
315
|
+
raise ConfigurationError(
|
|
316
|
+
f"Required viewport dimensions (width, height) missing in configuration at {target_file}"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
profile = DeviceProfile(
|
|
320
|
+
device_name=str(device_name or "Standard Profile"),
|
|
321
|
+
user_agent=str(user_agent),
|
|
322
|
+
viewport_width=int(vp_w),
|
|
323
|
+
viewport_height=int(vp_h),
|
|
324
|
+
device_scale_factor=resolver.resolve("device_scale_factor", "DEVICE_SCALE_FACTOR", default=1.0, cast_type=float),
|
|
325
|
+
is_mobile=resolver.resolve("is_mobile", "DEVICE_IS_MOBILE", default=False, cast_type=bool),
|
|
326
|
+
has_touch=resolver.resolve("has_touch", "DEVICE_HAS_TOUCH", default=False, cast_type=bool),
|
|
327
|
+
locale=resolver.resolve("locale", "DEVICE_LOCALE", default="en-US", cast_type=str),
|
|
328
|
+
timezone_id=resolver.resolve("timezone_id", "DEVICE_TIMEZONE", default="UTC", cast_type=str),
|
|
329
|
+
geolocation_latitude=float(geo_lat) if geo_lat is not None else None,
|
|
330
|
+
geolocation_longitude=float(geo_lon) if geo_lon is not None else None,
|
|
331
|
+
geolocation_accuracy=float(geo_acc) if geo_acc is not None else None,
|
|
332
|
+
permissions=list(resolver.resolve("permissions", "DEVICE_PERMISSIONS", default=[])),
|
|
333
|
+
platform=resolver.resolve("platform", "DEVICE_PLATFORM", default="Win32", cast_type=str),
|
|
334
|
+
vendor=resolver.resolve("vendor", "DEVICE_VENDOR", default="Google Inc.", cast_type=str),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if profile_path is None:
|
|
338
|
+
_DEVICE_PROFILE_CACHE = profile
|
|
339
|
+
return profile
|
constants/__init__.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Constants package for universal web applier."""
|
|
2
|
+
|
|
3
|
+
from constants.timeouts import (
|
|
4
|
+
DEFAULT_NAV_TIMEOUT_MS,
|
|
5
|
+
DEFAULT_FORM_READY_TIMEOUT_MS,
|
|
6
|
+
DEFAULT_SUBMIT_WAIT_MS,
|
|
7
|
+
DEFAULT_EXPAND_SECTION_WAIT_MS,
|
|
8
|
+
PAUSE_DOM_CONTENT_LOADED_MS,
|
|
9
|
+
PAUSE_PORTAL_DROPDOWN_CLICK_MS,
|
|
10
|
+
PAUSE_PORTAL_APPLY_CLICK_MS,
|
|
11
|
+
PAUSE_LOGIN_STABILIZATION_MS,
|
|
12
|
+
PAUSE_INPUT_SET_MS,
|
|
13
|
+
PAUSE_RETRY_FALLBACK_MS,
|
|
14
|
+
PAUSE_PICKLIST_EXPAND_MS,
|
|
15
|
+
PAUSE_PICKLIST_SCROLL_STEP_MS,
|
|
16
|
+
PAUSE_PICKLIST_CLOSE_MS,
|
|
17
|
+
PAUSE_UPLOAD_DIRECT_MS,
|
|
18
|
+
PAUSE_UPLOAD_CHOOSER_MS,
|
|
19
|
+
PAUSE_REACTIVE_STABILIZE_MS,
|
|
20
|
+
PAUSE_MICRO_WAIT_MS,
|
|
21
|
+
PAUSE_MODAL_DISMISS_MS,
|
|
22
|
+
)
|
|
23
|
+
from constants.selectors import (
|
|
24
|
+
SELECTOR_SECTION_BUTTON,
|
|
25
|
+
SELECTOR_SECTION_CONTAINER,
|
|
26
|
+
SELECTOR_SECTION_CONTENT,
|
|
27
|
+
SELECTOR_PORTAL_DROPDOWN,
|
|
28
|
+
SELECTOR_PORTAL_APPLY_BUTTON,
|
|
29
|
+
SELECTOR_FILE_INPUT_GENERIC,
|
|
30
|
+
SELECTOR_CUSTOM_UPLOAD_TRIGGERS,
|
|
31
|
+
SELECTOR_PICKLIST_INPUTS,
|
|
32
|
+
SELECTOR_MODAL_DIALOGS,
|
|
33
|
+
SELECTOR_VALIDATION_ERRORS,
|
|
34
|
+
)
|
|
35
|
+
from constants.contracts import (
|
|
36
|
+
SCHEMA_KEY_TITLE,
|
|
37
|
+
SCHEMA_KEY_URL,
|
|
38
|
+
SCHEMA_KEY_SECTIONS,
|
|
39
|
+
SCHEMA_KEY_INPUTS,
|
|
40
|
+
SCHEMA_KEY_DROPDOWNS,
|
|
41
|
+
SCHEMA_KEY_CHOICES,
|
|
42
|
+
SCHEMA_KEY_FILE_UPLOADS,
|
|
43
|
+
SCHEMA_KEY_ALREADY_UPLOADED,
|
|
44
|
+
SCHEMA_KEY_BUTTONS,
|
|
45
|
+
FIELD_KEY_ID,
|
|
46
|
+
FIELD_KEY_LABEL,
|
|
47
|
+
FIELD_KEY_TYPE,
|
|
48
|
+
FIELD_KEY_REQUIRED,
|
|
49
|
+
FIELD_KEY_VALUE,
|
|
50
|
+
FIELD_KEY_TRIGGER_ID,
|
|
51
|
+
FIELD_KEY_SAMPLE_OPTIONS,
|
|
52
|
+
FIELD_KEY_OPTIONS_COUNT,
|
|
53
|
+
CHOICE_KEY_QUESTION,
|
|
54
|
+
CHOICE_KEY_TYPE,
|
|
55
|
+
CHOICE_KEY_REQUIRED,
|
|
56
|
+
CHOICE_KEY_OPTIONS,
|
|
57
|
+
CHOICE_KEY_SELECTED,
|
|
58
|
+
BUTTON_KEY_ID,
|
|
59
|
+
BUTTON_KEY_TEXT,
|
|
60
|
+
BUTTON_KEY_ACTION,
|
|
61
|
+
WIDGET_TYPE_TEXT,
|
|
62
|
+
WIDGET_TYPE_SELECT,
|
|
63
|
+
WIDGET_TYPE_PICKLIST,
|
|
64
|
+
WIDGET_TYPE_RADIO,
|
|
65
|
+
WIDGET_TYPE_CHECKBOX,
|
|
66
|
+
WIDGET_TYPE_CUSTOM_RADIO,
|
|
67
|
+
WIDGET_TYPE_BUTTON,
|
|
68
|
+
BUTTON_ACTION_DEFAULT,
|
|
69
|
+
OUTCOME_KEY_SUCCESS,
|
|
70
|
+
OUTCOME_KEY_TARGET,
|
|
71
|
+
OUTCOME_KEY_REDIRECTED,
|
|
72
|
+
OUTCOME_KEY_NEW_WINDOW,
|
|
73
|
+
OUTCOME_KEY_NEW_URL,
|
|
74
|
+
OUTCOME_KEY_MODAL_OPENED,
|
|
75
|
+
OUTCOME_KEY_MODAL_TEXT,
|
|
76
|
+
OUTCOME_KEY_NEW_INPUTS_COUNT,
|
|
77
|
+
OUTCOME_KEY_SCHEMA,
|
|
78
|
+
OUTCOME_KEY_ALERTS,
|
|
79
|
+
SUMMARY_KEY_CONFIRMED,
|
|
80
|
+
SUMMARY_KEY_UNCONFIRMED,
|
|
81
|
+
SUMMARY_KEY_FAILED,
|
|
82
|
+
SCHEMA_KEYS,
|
|
83
|
+
OUTCOME_KEYS,
|
|
84
|
+
SUMMARY_KEYS,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
"DEFAULT_NAV_TIMEOUT_MS",
|
|
89
|
+
"DEFAULT_FORM_READY_TIMEOUT_MS",
|
|
90
|
+
"DEFAULT_SUBMIT_WAIT_MS",
|
|
91
|
+
"DEFAULT_EXPAND_SECTION_WAIT_MS",
|
|
92
|
+
"PAUSE_DOM_CONTENT_LOADED_MS",
|
|
93
|
+
"PAUSE_PORTAL_DROPDOWN_CLICK_MS",
|
|
94
|
+
"PAUSE_PORTAL_APPLY_CLICK_MS",
|
|
95
|
+
"PAUSE_LOGIN_STABILIZATION_MS",
|
|
96
|
+
"PAUSE_INPUT_SET_MS",
|
|
97
|
+
"PAUSE_RETRY_FALLBACK_MS",
|
|
98
|
+
"PAUSE_PICKLIST_EXPAND_MS",
|
|
99
|
+
"PAUSE_PICKLIST_SCROLL_STEP_MS",
|
|
100
|
+
"PAUSE_PICKLIST_CLOSE_MS",
|
|
101
|
+
"PAUSE_UPLOAD_DIRECT_MS",
|
|
102
|
+
"PAUSE_UPLOAD_CHOOSER_MS",
|
|
103
|
+
"PAUSE_REACTIVE_STABILIZE_MS",
|
|
104
|
+
"PAUSE_MICRO_WAIT_MS",
|
|
105
|
+
"PAUSE_MODAL_DISMISS_MS",
|
|
106
|
+
"SELECTOR_SECTION_BUTTON",
|
|
107
|
+
"SELECTOR_SECTION_CONTAINER",
|
|
108
|
+
"SELECTOR_SECTION_CONTENT",
|
|
109
|
+
"SELECTOR_PORTAL_DROPDOWN",
|
|
110
|
+
"SELECTOR_PORTAL_APPLY_BUTTON",
|
|
111
|
+
"SELECTOR_FILE_INPUT_GENERIC",
|
|
112
|
+
"SELECTOR_CUSTOM_UPLOAD_TRIGGERS",
|
|
113
|
+
"SELECTOR_PICKLIST_INPUTS",
|
|
114
|
+
"SELECTOR_MODAL_DIALOGS",
|
|
115
|
+
"SELECTOR_VALIDATION_ERRORS",
|
|
116
|
+
"SCHEMA_KEY_TITLE",
|
|
117
|
+
"SCHEMA_KEY_URL",
|
|
118
|
+
"SCHEMA_KEY_SECTIONS",
|
|
119
|
+
"SCHEMA_KEY_INPUTS",
|
|
120
|
+
"SCHEMA_KEY_DROPDOWNS",
|
|
121
|
+
"SCHEMA_KEY_CHOICES",
|
|
122
|
+
"SCHEMA_KEY_FILE_UPLOADS",
|
|
123
|
+
"SCHEMA_KEY_ALREADY_UPLOADED",
|
|
124
|
+
"SCHEMA_KEY_BUTTONS",
|
|
125
|
+
"FIELD_KEY_ID",
|
|
126
|
+
"FIELD_KEY_LABEL",
|
|
127
|
+
"FIELD_KEY_TYPE",
|
|
128
|
+
"FIELD_KEY_REQUIRED",
|
|
129
|
+
"FIELD_KEY_VALUE",
|
|
130
|
+
"FIELD_KEY_TRIGGER_ID",
|
|
131
|
+
"FIELD_KEY_SAMPLE_OPTIONS",
|
|
132
|
+
"FIELD_KEY_OPTIONS_COUNT",
|
|
133
|
+
"CHOICE_KEY_QUESTION",
|
|
134
|
+
"CHOICE_KEY_TYPE",
|
|
135
|
+
"CHOICE_KEY_REQUIRED",
|
|
136
|
+
"CHOICE_KEY_OPTIONS",
|
|
137
|
+
"CHOICE_KEY_SELECTED",
|
|
138
|
+
"BUTTON_KEY_ID",
|
|
139
|
+
"BUTTON_KEY_TEXT",
|
|
140
|
+
"BUTTON_KEY_ACTION",
|
|
141
|
+
"WIDGET_TYPE_TEXT",
|
|
142
|
+
"WIDGET_TYPE_SELECT",
|
|
143
|
+
"WIDGET_TYPE_PICKLIST",
|
|
144
|
+
"WIDGET_TYPE_RADIO",
|
|
145
|
+
"WIDGET_TYPE_CHECKBOX",
|
|
146
|
+
"WIDGET_TYPE_CUSTOM_RADIO",
|
|
147
|
+
"WIDGET_TYPE_BUTTON",
|
|
148
|
+
"BUTTON_ACTION_DEFAULT",
|
|
149
|
+
"OUTCOME_KEY_SUCCESS",
|
|
150
|
+
"OUTCOME_KEY_TARGET",
|
|
151
|
+
"OUTCOME_KEY_REDIRECTED",
|
|
152
|
+
"OUTCOME_KEY_NEW_WINDOW",
|
|
153
|
+
"OUTCOME_KEY_NEW_URL",
|
|
154
|
+
"OUTCOME_KEY_MODAL_OPENED",
|
|
155
|
+
"OUTCOME_KEY_MODAL_TEXT",
|
|
156
|
+
"OUTCOME_KEY_NEW_INPUTS_COUNT",
|
|
157
|
+
"OUTCOME_KEY_SCHEMA",
|
|
158
|
+
"OUTCOME_KEY_ALERTS",
|
|
159
|
+
"SUMMARY_KEY_CONFIRMED",
|
|
160
|
+
"SUMMARY_KEY_UNCONFIRMED",
|
|
161
|
+
"SUMMARY_KEY_FAILED",
|
|
162
|
+
"SCHEMA_KEYS",
|
|
163
|
+
"OUTCOME_KEYS",
|
|
164
|
+
"SUMMARY_KEYS",
|
|
165
|
+
]
|
constants/contracts.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Single Source of Truth (SSOT) for data contracts, schema keys, and wire models."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
# --- Inspection Schema Contract Keys ---
|
|
6
|
+
SCHEMA_KEY_TITLE: Final[str] = "title"
|
|
7
|
+
SCHEMA_KEY_URL: Final[str] = "url"
|
|
8
|
+
SCHEMA_KEY_SECTIONS: Final[str] = "sections"
|
|
9
|
+
SCHEMA_KEY_INPUTS: Final[str] = "inputs"
|
|
10
|
+
SCHEMA_KEY_DROPDOWNS: Final[str] = "dropdowns"
|
|
11
|
+
SCHEMA_KEY_CHOICES: Final[str] = "choices"
|
|
12
|
+
SCHEMA_KEY_FILE_UPLOADS: Final[str] = "file_uploads"
|
|
13
|
+
SCHEMA_KEY_ALREADY_UPLOADED: Final[str] = "already_uploaded_files"
|
|
14
|
+
SCHEMA_KEY_BUTTONS: Final[str] = "buttons"
|
|
15
|
+
|
|
16
|
+
# --- Field / Element Property Keys ---
|
|
17
|
+
FIELD_KEY_ID: Final[str] = "id"
|
|
18
|
+
FIELD_KEY_LABEL: Final[str] = "label"
|
|
19
|
+
FIELD_KEY_TYPE: Final[str] = "type"
|
|
20
|
+
FIELD_KEY_REQUIRED: Final[str] = "required"
|
|
21
|
+
FIELD_KEY_VALUE: Final[str] = "value"
|
|
22
|
+
FIELD_KEY_TRIGGER_ID: Final[str] = "trigger_id"
|
|
23
|
+
FIELD_KEY_SAMPLE_OPTIONS: Final[str] = "sample_options"
|
|
24
|
+
FIELD_KEY_OPTIONS_COUNT: Final[str] = "options_count"
|
|
25
|
+
|
|
26
|
+
# --- Choice Group Property Keys ---
|
|
27
|
+
CHOICE_KEY_QUESTION: Final[str] = "question"
|
|
28
|
+
CHOICE_KEY_TYPE: Final[str] = "type"
|
|
29
|
+
CHOICE_KEY_REQUIRED: Final[str] = "required"
|
|
30
|
+
CHOICE_KEY_OPTIONS: Final[str] = "options"
|
|
31
|
+
CHOICE_KEY_SELECTED: Final[str] = "selected"
|
|
32
|
+
|
|
33
|
+
# --- Action Button Property Keys ---
|
|
34
|
+
BUTTON_KEY_ID: Final[str] = "id"
|
|
35
|
+
BUTTON_KEY_TEXT: Final[str] = "text"
|
|
36
|
+
BUTTON_KEY_ACTION: Final[str] = "action"
|
|
37
|
+
|
|
38
|
+
# --- Widget Type Identifiers ---
|
|
39
|
+
WIDGET_TYPE_TEXT: Final[str] = "text"
|
|
40
|
+
WIDGET_TYPE_SELECT: Final[str] = "select"
|
|
41
|
+
WIDGET_TYPE_PICKLIST: Final[str] = "picklist"
|
|
42
|
+
WIDGET_TYPE_RADIO: Final[str] = "radio"
|
|
43
|
+
WIDGET_TYPE_CHECKBOX: Final[str] = "checkbox"
|
|
44
|
+
WIDGET_TYPE_CUSTOM_RADIO: Final[str] = "custom_radio"
|
|
45
|
+
WIDGET_TYPE_BUTTON: Final[str] = "button"
|
|
46
|
+
BUTTON_ACTION_DEFAULT: Final[str] = "action"
|
|
47
|
+
|
|
48
|
+
# --- Reactive Action Outcome Keys ---
|
|
49
|
+
OUTCOME_KEY_SUCCESS: Final[str] = "success"
|
|
50
|
+
OUTCOME_KEY_TARGET: Final[str] = "target"
|
|
51
|
+
OUTCOME_KEY_REDIRECTED: Final[str] = "redirected"
|
|
52
|
+
OUTCOME_KEY_NEW_WINDOW: Final[str] = "new_window"
|
|
53
|
+
OUTCOME_KEY_NEW_URL: Final[str] = "new_url"
|
|
54
|
+
OUTCOME_KEY_MODAL_OPENED: Final[str] = "modal_opened"
|
|
55
|
+
OUTCOME_KEY_MODAL_TEXT: Final[str] = "modal_text"
|
|
56
|
+
OUTCOME_KEY_NEW_INPUTS_COUNT: Final[str] = "new_inputs_count"
|
|
57
|
+
OUTCOME_KEY_SCHEMA: Final[str] = "schema"
|
|
58
|
+
OUTCOME_KEY_ALERTS: Final[str] = "alerts"
|
|
59
|
+
|
|
60
|
+
# --- Fill Summary Keys ---
|
|
61
|
+
SUMMARY_KEY_CONFIRMED: Final[str] = "confirmed"
|
|
62
|
+
SUMMARY_KEY_UNCONFIRMED: Final[str] = "unconfirmed"
|
|
63
|
+
SUMMARY_KEY_FAILED: Final[str] = "failed"
|
|
64
|
+
|
|
65
|
+
# --- Contract Key Sets ---
|
|
66
|
+
SCHEMA_KEYS: Final[set] = {
|
|
67
|
+
SCHEMA_KEY_TITLE,
|
|
68
|
+
SCHEMA_KEY_URL,
|
|
69
|
+
SCHEMA_KEY_SECTIONS,
|
|
70
|
+
SCHEMA_KEY_INPUTS,
|
|
71
|
+
SCHEMA_KEY_DROPDOWNS,
|
|
72
|
+
SCHEMA_KEY_CHOICES,
|
|
73
|
+
SCHEMA_KEY_FILE_UPLOADS,
|
|
74
|
+
SCHEMA_KEY_ALREADY_UPLOADED,
|
|
75
|
+
SCHEMA_KEY_BUTTONS,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
OUTCOME_KEYS: Final[set] = {
|
|
79
|
+
OUTCOME_KEY_SUCCESS,
|
|
80
|
+
OUTCOME_KEY_TARGET,
|
|
81
|
+
OUTCOME_KEY_REDIRECTED,
|
|
82
|
+
OUTCOME_KEY_NEW_WINDOW,
|
|
83
|
+
OUTCOME_KEY_NEW_URL,
|
|
84
|
+
OUTCOME_KEY_MODAL_OPENED,
|
|
85
|
+
OUTCOME_KEY_MODAL_TEXT,
|
|
86
|
+
OUTCOME_KEY_NEW_INPUTS_COUNT,
|
|
87
|
+
OUTCOME_KEY_SCHEMA,
|
|
88
|
+
OUTCOME_KEY_ALERTS,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
SUMMARY_KEYS: Final[set] = {
|
|
92
|
+
SUMMARY_KEY_CONFIRMED,
|
|
93
|
+
SUMMARY_KEY_UNCONFIRMED,
|
|
94
|
+
SUMMARY_KEY_FAILED,
|
|
95
|
+
}
|
constants/selectors.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CSS and XPath selector constants for form elements and framework widgets."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
# Form section and accordion toggle selectors
|
|
6
|
+
SELECTOR_SECTION_BUTTON: Final[str] = "button.rcmFormSectionTopBar, .rcmFormSectionTopBar"
|
|
7
|
+
SELECTOR_SECTION_CONTAINER: Final[str] = ".rcmFormSection, [class*='rcmFormSection']"
|
|
8
|
+
SELECTOR_SECTION_CONTENT: Final[str] = ".sectionContent, [class*='sectionContent']"
|
|
9
|
+
|
|
10
|
+
# Job portal landing transition selectors
|
|
11
|
+
SELECTOR_PORTAL_DROPDOWN: Final[str] = "button.dropdown-toggle, .dropdown-toggle, [data-toggle='dropdown']"
|
|
12
|
+
SELECTOR_PORTAL_APPLY_BUTTON: Final[str] = ".dialogApplyBtn"
|
|
13
|
+
|
|
14
|
+
# File input and upload trigger selectors
|
|
15
|
+
SELECTOR_FILE_INPUT_GENERIC: Final[str] = "input[type='file']"
|
|
16
|
+
SELECTOR_CUSTOM_UPLOAD_TRIGGERS: Final[str] = (
|
|
17
|
+
"input[type='file'], .sfFileUpload, .fileUploadWrapper, [role='button'][id*='_attach']"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Enterprise picklist / Combobox selectors
|
|
21
|
+
SELECTOR_PICKLIST_INPUTS: Final[str] = "input[role='combobox'], input[id$=':_input']"
|
|
22
|
+
SELECTOR_MODAL_DIALOGS: Final[str] = ".modal:visible, [role='dialog']:visible, .dialog:visible, .pop-up:visible"
|
|
23
|
+
|
|
24
|
+
# Error banners and validation notice selectors
|
|
25
|
+
SELECTOR_VALIDATION_ERRORS: Final[str] = (
|
|
26
|
+
".alert-danger, .error, .errorMessage, .sf-error, [role='alert'], .validation-summary-errors, .has-error"
|
|
27
|
+
)
|
constants/timeouts.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Timing and delay constants for browser actions and stabilization."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
# Default timeouts in milliseconds
|
|
6
|
+
DEFAULT_NAV_TIMEOUT_MS: Final[int] = 45000
|
|
7
|
+
DEFAULT_FORM_READY_TIMEOUT_MS: Final[int] = 10000
|
|
8
|
+
DEFAULT_SUBMIT_WAIT_MS: Final[int] = 8000
|
|
9
|
+
DEFAULT_EXPAND_SECTION_WAIT_MS: Final[int] = 800
|
|
10
|
+
|
|
11
|
+
# Granular stabilization pauses in milliseconds
|
|
12
|
+
PAUSE_DOM_CONTENT_LOADED_MS: Final[int] = 3000
|
|
13
|
+
PAUSE_PORTAL_DROPDOWN_CLICK_MS: Final[int] = 1000
|
|
14
|
+
PAUSE_PORTAL_APPLY_CLICK_MS: Final[int] = 7000
|
|
15
|
+
PAUSE_LOGIN_STABILIZATION_MS: Final[int] = 6000
|
|
16
|
+
PAUSE_INPUT_SET_MS: Final[int] = 150
|
|
17
|
+
PAUSE_RETRY_FALLBACK_MS: Final[int] = 200
|
|
18
|
+
PAUSE_PICKLIST_EXPAND_MS: Final[int] = 400
|
|
19
|
+
PAUSE_PICKLIST_SCROLL_STEP_MS: Final[int] = 450
|
|
20
|
+
PAUSE_PICKLIST_CLOSE_MS: Final[int] = 100
|
|
21
|
+
PAUSE_UPLOAD_DIRECT_MS: Final[int] = 3000
|
|
22
|
+
PAUSE_UPLOAD_CHOOSER_MS: Final[int] = 4000
|
|
23
|
+
PAUSE_REACTIVE_STABILIZE_MS: Final[int] = 3000
|
|
24
|
+
PAUSE_MICRO_WAIT_MS: Final[int] = 100
|
|
25
|
+
PAUSE_MODAL_DISMISS_MS: Final[int] = 200
|