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.
Files changed (47) hide show
  1. adapters/__init__.py +14 -0
  2. adapters/base.py +34 -0
  3. adapters/browser_adapter.py +300 -0
  4. adapters/dom_scripts.py +518 -0
  5. adapters/envelope_builder.py +108 -0
  6. adapters/live_session_manager.py +326 -0
  7. adapters/upload_adapter.py +77 -0
  8. builder.py +83 -0
  9. cli.py +341 -0
  10. config/__init__.py +23 -0
  11. config/settings.py +339 -0
  12. constants/__init__.py +165 -0
  13. constants/contracts.py +95 -0
  14. constants/selectors.py +27 -0
  15. constants/timeouts.py +25 -0
  16. core/__init__.py +45 -0
  17. core/exceptions.py +33 -0
  18. core/models.py +273 -0
  19. engine.py +199 -0
  20. exceptions.py +23 -0
  21. mcp_server.py +347 -0
  22. models.py +29 -0
  23. orchestration/__init__.py +19 -0
  24. orchestration/context.py +71 -0
  25. orchestration/flow_orchestrator.py +358 -0
  26. orchestration/session_coordinator.py +88 -0
  27. services/__init__.py +17 -0
  28. services/auth_navigation_service.py +129 -0
  29. services/field_interaction_service.py +174 -0
  30. services/form_filler_service.py +106 -0
  31. services/inspection_formatter_service.py +68 -0
  32. services/inspection_service.py +40 -0
  33. services/reactive_interaction_service.py +174 -0
  34. supervisor/__init__.py +13 -0
  35. supervisor/client.py +171 -0
  36. supervisor/contracts.py +62 -0
  37. supervisor/master_daemon.py +303 -0
  38. supervisor/shell.py +194 -0
  39. supervisor/worker_process.py +400 -0
  40. validators/__init__.py +9 -0
  41. validators/form_validator.py +28 -0
  42. validators/payload_validator.py +25 -0
  43. webpilot_engine-2.0.0.dist-info/METADATA +299 -0
  44. webpilot_engine-2.0.0.dist-info/RECORD +47 -0
  45. webpilot_engine-2.0.0.dist-info/WHEEL +5 -0
  46. webpilot_engine-2.0.0.dist-info/entry_points.txt +6 -0
  47. 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