pyplaykit 0.1.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.
@@ -0,0 +1,42 @@
1
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ core/base_page.py,sha256=90M-QMTblKIU7RHILzR0ZPFRuVStaQ80cS9mzMERJxA,3978
3
+ core/base_test.py,sha256=xz4bD7V1mFkQ6S-ZEOzv9iQGBqrpRk1SWtyD9Jd2qSQ,779
4
+ core/browser_factory.py,sha256=_uuGdY-e4n39QzMOZDwzlp9R_8jopxCByq980tp2CLU,1067
5
+ core/playwright_manager.py,sha256=oFUBouOclpdSmn7Z2daxfbvdSKBz8EU_9Y9076ixKfo,1302
6
+ dist_pkg/__init__.py,sha256=KVhzkLGG8RdxNk9El-8JruA7L4WBljy7f5JtkgrMhHM,197
7
+ dist_pkg/distribution.py,sha256=RZA-XOGQrBpG6nXnIZeDvri_cRTL-Nv7TBZsevyxPqs,1598
8
+ integrations/__init__.py,sha256=82mpeM9c1NLsbMxylOpjdknnpnavDEePql40Bi3lnxY,466
9
+ integrations/adapters.py,sha256=3tlHQu0UD8mTE1iISpXhly4JzQ_BeYfg9xXZxIHsCXI,5449
10
+ orchestration/__init__.py,sha256=omP4kquqZ2lueb2xBN8Bu1F-zHfqYjsqlvPjQgje9hI,156
11
+ orchestration/planner.py,sha256=BPbfkTpnxaGkeqcdU0Vm-byOfNoOWAgfb2X4m7eqpXw,5933
12
+ pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pages/dashboard_page.py,sha256=2ywYZOV1pcv7GrV9kGdPFmHtq9pE-yNCEycofkZljBE,849
14
+ pages/login_page.py,sha256=KOkRHRDLP5Xj7ClsM1Y1abOlzveoJigpRuXjAJVMXLw,1065
15
+ pages/orangehrm_login_page.py,sha256=h5weajsblILbPKQt6KFYh-vX2BZHAzDSZfj5BhexLic,2849
16
+ pages/saucedemo_inventory_page.py,sha256=ga1MngZPQqAGmA34i8zCRyErFOK-upvoaECh4Zk0GtA,4036
17
+ pages/saucedemo_login_page.py,sha256=X3P_pFfOeMlsRAO53kcQ0G0saVIRzODS_WFIawszL2s,1403
18
+ plugins/__init__.py,sha256=k8WKwbK-5HeQ0Jhgw6fRwRs6OrLym3jHU4iMsSFUaaI,418
19
+ plugins/api_plugin.py,sha256=qOnit0CI1N7BQKrFW4h8v03JUCiBzimw7TmoMdUGwLU,255
20
+ plugins/base.py,sha256=6kkRgGWMBoRaYi5Y7QwBjB72OxxqL5cASmPxED1HsM8,643
21
+ plugins/data_plugin.py,sha256=kihGM5eKblgdzzg7exd0jvNk3RB9YpkhG6P_NIid1NQ,259
22
+ plugins/registry.py,sha256=DoQ-UqRYnACWfn42CshwM6mZG3619HuWC-GCE53v_XE,2783
23
+ plugins/security_plugin.py,sha256=-rxIbVFe2vChLysmdpXN75YiHorb9vrnDuG1bS8LzF4,275
24
+ resilience/__init__.py,sha256=HSIH-phgvPmf_28n0mtEuVmPQT0lHWhjrdDro1LZoM4,222
25
+ resilience/locator_recovery.py,sha256=Dnz0s4hC4YXNEcdv1K-QaP_lEhZ3axTYmUh8-AVl4SI,2920
26
+ utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ utils/api_client.py,sha256=1IxoHGYeSkYfzuUiYmdP9RRkKSEDjhEgBG5rX9ARtpM,4826
28
+ utils/assertion_helper.py,sha256=0u2KNlxY4ZFVtuiVgbP57Uv8SjZ-QnxfHklwcnAydyg,837
29
+ utils/data_loader.py,sha256=h0mpoutvnfMnWwb1344VGxK6MmM_ChEWP_yriyehU2s,1147
30
+ utils/data_validator.py,sha256=H4zCvgN3X6SKoyjxIokPPuH7-RjGVcaP3447oVahB0g,17251
31
+ utils/environment_validator.py,sha256=GQ1BZRGKmgK8pbycISwG5kfy5woxpH1WJPBKfiiteH4,3594
32
+ utils/file_reader.py,sha256=lBd9TbGDJJDzJaDY71XFFHoorfB0pr6MCLg4WcS6v3s,1236
33
+ utils/logger.py,sha256=ExRn7PrgdHfUbS4bEao4WHJsNSD4UcvL_LzbIdq3JSk,973
34
+ utils/observability.py,sha256=-n-rsKL3OP-L3fXrQJ1OZLFCck7REuDN8qryyM1fBHM,6860
35
+ utils/response_validator.py,sha256=tIGN_R9jgUsza6XK7QbxuptV_QAdBRr8ewbDOQ-OLcA,2466
36
+ utils/screenshot_helper.py,sha256=C5oucA5Q4xyB1s-8mX65Im2oaoBSG2tiNV_jtRiq2qc,686
37
+ utils/tdm.py,sha256=0BNeKORfIDlLtcFFIJUew2FVEv5MFBkdTSH106ZOZCE,8215
38
+ utils/yaml_config_reader.py,sha256=neiri3N6QgzDtUD7buzHR7dYWo_2MbW0GE1YRpvPBQ8,4123
39
+ pyplaykit-0.1.0.dist-info/METADATA,sha256=C1zl3R8zedpkQSNTyiWvQQFePlRDovCsROqp_AAXXYg,10975
40
+ pyplaykit-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
41
+ pyplaykit-0.1.0.dist-info/top_level.txt,sha256=DROtyBRVqfRJXUjXj3TYe5SfSTmIVKm16b7YhMD6ZI0,72
42
+ pyplaykit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,8 @@
1
+ core
2
+ dist_pkg
3
+ integrations
4
+ orchestration
5
+ pages
6
+ plugins
7
+ resilience
8
+ utils
resilience/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Resilience utilities for self-healing interaction patterns."""
2
+
3
+ from .locator_recovery import resolve_visible_locator, write_resilience_audit_event
4
+
5
+ __all__ = ["resolve_visible_locator", "write_resilience_audit_event"]
@@ -0,0 +1,72 @@
1
+ """Locator self-healing helpers with fallback chains and DOM retries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+
10
+ def resolve_visible_locator(
11
+ *,
12
+ locator_getter: Callable[[str], Any],
13
+ visible_assertion: Callable[[Any], None],
14
+ primary_selector: str,
15
+ fallback_selectors: list[str] | None = None,
16
+ dom_retry_count: int = 0,
17
+ confidence_decay_per_hop: float = 0.15,
18
+ confidence_decay_per_retry: float = 0.1,
19
+ ) -> tuple[Any, str, int, dict[str, Any]]:
20
+ """Resolve the first visible locator using optional fallback selectors and retry loops."""
21
+ candidates = [primary_selector] + [selector for selector in (fallback_selectors or []) if selector]
22
+ if not candidates:
23
+ raise ValueError("At least one selector must be provided")
24
+
25
+ retries = max(0, int(dom_retry_count))
26
+ hop_decay = max(0.0, float(confidence_decay_per_hop))
27
+ retry_decay = max(0.0, float(confidence_decay_per_retry))
28
+ last_error: Exception | None = None
29
+ attempted_actions: list[dict[str, Any]] = []
30
+
31
+ for attempt in range(retries + 1):
32
+ for selector_index, selector in enumerate(candidates):
33
+ locator = locator_getter(selector)
34
+ try:
35
+ visible_assertion(locator)
36
+ confidence = max(0.0, min(1.0, 1.0 - (selector_index * hop_decay) - (attempt * retry_decay)))
37
+ event = {
38
+ "selected_selector": selector,
39
+ "attempt": attempt,
40
+ "selector_index": selector_index,
41
+ "confidence_score": round(confidence, 4),
42
+ "used_fallback": selector_index > 0,
43
+ "used_retry": attempt > 0,
44
+ "attempted_actions": attempted_actions,
45
+ }
46
+ return locator, selector, attempt, event
47
+ except Exception as error:
48
+ last_error = error
49
+ attempted_actions.append(
50
+ {
51
+ "attempt": attempt,
52
+ "selector": selector,
53
+ "error": str(error),
54
+ }
55
+ )
56
+
57
+ details = ", ".join(candidates)
58
+ raise AssertionError(
59
+ f"Unable to resolve visible element after {retries + 1} attempt(s) using selectors: {details}"
60
+ ) from last_error
61
+
62
+
63
+ def write_resilience_audit_event(audit_log_path: str, event: dict[str, Any]) -> str:
64
+ """Append one resilience audit event as JSON line and return the file path."""
65
+ if not audit_log_path or not str(audit_log_path).strip():
66
+ raise ValueError("audit_log_path must be a non-empty string")
67
+
68
+ output = Path(audit_log_path)
69
+ output.parent.mkdir(parents=True, exist_ok=True)
70
+ with output.open("a", encoding="utf-8") as stream:
71
+ stream.write(json.dumps(event, sort_keys=True) + "\n")
72
+ return str(output.resolve())
utils/__init__.py ADDED
File without changes
utils/api_client.py ADDED
@@ -0,0 +1,125 @@
1
+ """Reusable API client built on Python standard library for framework-level API testing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+ from urllib.error import HTTPError, URLError
10
+ from urllib.parse import urlencode, urljoin
11
+ from urllib.request import Request, urlopen
12
+
13
+
14
+ @dataclass
15
+ class ApiResponse:
16
+ """Normalized API response wrapper for assertions and reporting."""
17
+
18
+ status_code: int
19
+ headers: dict[str, str]
20
+ text: str
21
+ elapsed_ms: float
22
+
23
+ def json(self) -> Any:
24
+ """Parse response body as JSON."""
25
+ return json.loads(self.text) if self.text else {}
26
+
27
+
28
+ class ApiClient:
29
+ """Simple API client supporting retries and JSON payload handling."""
30
+
31
+ def __init__(
32
+ self,
33
+ base_url: str = "",
34
+ default_headers: dict[str, str] | None = None,
35
+ timeout_seconds: int = 30,
36
+ retries: int = 1,
37
+ retry_status_codes: set[int] | None = None,
38
+ logger=None,
39
+ ) -> None:
40
+ self.base_url = base_url
41
+ self.default_headers = default_headers or {}
42
+ self.timeout_seconds = timeout_seconds
43
+ self.retries = retries
44
+ self.retry_status_codes = retry_status_codes or {429, 500, 502, 503, 504}
45
+ self.logger = logger
46
+
47
+ def request(
48
+ self,
49
+ method: str,
50
+ endpoint: str,
51
+ params: dict[str, Any] | None = None,
52
+ headers: dict[str, str] | None = None,
53
+ json_body: dict[str, Any] | list[Any] | None = None,
54
+ data: bytes | None = None,
55
+ ) -> ApiResponse:
56
+ """Send an HTTP request and return a normalized response."""
57
+ url = self._build_url(endpoint, params)
58
+ merged_headers = {**self.default_headers, **(headers or {})}
59
+
60
+ payload = data
61
+ if json_body is not None:
62
+ payload = json.dumps(json_body).encode("utf-8")
63
+ merged_headers.setdefault("Content-Type", "application/json")
64
+
65
+ request = Request(url=url, method=method.upper(), headers=merged_headers, data=payload)
66
+
67
+ attempt = 0
68
+ while attempt <= self.retries:
69
+ attempt += 1
70
+ start = time.perf_counter()
71
+ try:
72
+ if self.logger:
73
+ self.logger.info("API request %s %s (attempt %s)", method.upper(), url, attempt)
74
+ with urlopen(request, timeout=self.timeout_seconds) as response:
75
+ elapsed_ms = (time.perf_counter() - start) * 1000
76
+ body = response.read().decode("utf-8")
77
+ return ApiResponse(
78
+ status_code=response.getcode(),
79
+ headers={k: v for k, v in response.headers.items()},
80
+ text=body,
81
+ elapsed_ms=elapsed_ms,
82
+ )
83
+ except HTTPError as error:
84
+ elapsed_ms = (time.perf_counter() - start) * 1000
85
+ body = error.read().decode("utf-8") if error.fp else ""
86
+ response = ApiResponse(
87
+ status_code=error.code,
88
+ headers={k: v for k, v in error.headers.items()} if error.headers else {},
89
+ text=body,
90
+ elapsed_ms=elapsed_ms,
91
+ )
92
+ if self._should_retry(error.code, attempt):
93
+ continue
94
+ return response
95
+ except URLError as error:
96
+ if attempt <= self.retries:
97
+ continue
98
+ raise RuntimeError(f"Request failed after retries for URL '{url}': {error.reason}") from error
99
+
100
+ raise RuntimeError(f"Request retries exhausted for URL '{url}'.")
101
+
102
+ def get(self, endpoint: str, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) -> ApiResponse:
103
+ """Shortcut for GET requests."""
104
+ return self.request("GET", endpoint, params=params, headers=headers)
105
+
106
+ def post(
107
+ self,
108
+ endpoint: str,
109
+ json_body: dict[str, Any] | list[Any] | None = None,
110
+ headers: dict[str, str] | None = None,
111
+ ) -> ApiResponse:
112
+ """Shortcut for POST requests with optional JSON body."""
113
+ return self.request("POST", endpoint, headers=headers, json_body=json_body)
114
+
115
+ def _build_url(self, endpoint: str, params: dict[str, Any] | None = None) -> str:
116
+ base = self.base_url.rstrip("/") + "/" if self.base_url else ""
117
+ url = urljoin(base, endpoint.lstrip("/")) if base else endpoint
118
+ if not params:
119
+ return url
120
+ query = urlencode(params, doseq=True)
121
+ separator = "&" if "?" in url else "?"
122
+ return f"{url}{separator}{query}"
123
+
124
+ def _should_retry(self, status_code: int, attempt: int) -> bool:
125
+ return attempt <= self.retries and status_code in self.retry_status_codes
@@ -0,0 +1,21 @@
1
+ """Reusable assertion helpers with enterprise-friendly failure messaging."""
2
+
3
+
4
+ def assert_equal(actual, expected, message: str = "") -> None:
5
+ """Assert equality with optional custom message."""
6
+ if actual != expected:
7
+ default_message = f"Expected '{expected}', but found '{actual}'."
8
+ raise AssertionError(message or default_message)
9
+
10
+
11
+ def assert_true(condition: bool, message: str) -> None:
12
+ """Assert condition is true with custom message."""
13
+ if not condition:
14
+ raise AssertionError(message)
15
+
16
+
17
+ def assert_in(member, container, message: str = "") -> None:
18
+ """Assert member exists in container with optional custom message."""
19
+ if member not in container:
20
+ default_message = f"Expected '{member}' to be present in '{container}'."
21
+ raise AssertionError(message or default_message)
utils/data_loader.py ADDED
@@ -0,0 +1,36 @@
1
+ """Data loading utilities for JSON and CSV with environment placeholder support."""
2
+
3
+ import csv
4
+ import json
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ ENV_PATTERN = re.compile(r"^\$\{(?P<name>[A-Z0-9_]+)\}$")
12
+
13
+
14
+ def _resolve_env_placeholders(value: Any) -> Any:
15
+ if isinstance(value, dict):
16
+ return {k: _resolve_env_placeholders(v) for k, v in value.items()}
17
+ if isinstance(value, list):
18
+ return [_resolve_env_placeholders(item) for item in value]
19
+ if isinstance(value, str):
20
+ match = ENV_PATTERN.match(value)
21
+ if match:
22
+ return os.getenv(match.group("name"), "")
23
+ return value
24
+
25
+
26
+ def load_json(file_path: str) -> Any:
27
+ """Load and return JSON content from a file path."""
28
+ with Path(file_path).open("r", encoding="utf-8") as file:
29
+ return _resolve_env_placeholders(json.load(file))
30
+
31
+
32
+ def load_csv(file_path: str) -> list[dict[str, str]]:
33
+ """Load and return CSV rows as list of dictionaries."""
34
+ with Path(file_path).open("r", encoding="utf-8", newline="") as file:
35
+ rows = list(csv.DictReader(file))
36
+ return _resolve_env_placeholders(rows)