waitless 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- waitless-0.2.0/LICENSE +21 -0
- {waitless-0.1.0 → waitless-0.2.0}/PKG-INFO +9 -7
- {waitless-0.1.0 → waitless-0.2.0}/README.md +6 -6
- {waitless-0.1.0 → waitless-0.2.0}/pyproject.toml +1 -1
- {waitless-0.1.0 → waitless-0.2.0}/waitless/config.py +14 -14
- {waitless-0.1.0 → waitless-0.2.0}/waitless/engine.py +3 -1
- {waitless-0.1.0 → waitless-0.2.0}/waitless/instrumentation.py +35 -2
- {waitless-0.1.0 → waitless-0.2.0}/waitless/selenium_integration.py +66 -6
- {waitless-0.1.0 → waitless-0.2.0}/waitless/signals.py +34 -5
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/PKG-INFO +9 -7
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/SOURCES.txt +1 -0
- {waitless-0.1.0 → waitless-0.2.0}/setup.cfg +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless/__init__.py +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless/__main__.py +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless/diagnostics.py +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless/exceptions.py +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/dependency_links.txt +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/entry_points.txt +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/requires.txt +0 -0
- {waitless-0.1.0 → waitless-0.2.0}/waitless.egg-info/top_level.txt +0 -0
waitless-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dhiraj Das
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
5
|
Author-email: Dhiraj Das <dhirajdas.66@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,9 +22,11 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
28
|
Requires-Dist: selenium>=4.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# Waitless
|
|
30
32
|
|
|
@@ -93,12 +95,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
93
95
|
from waitless import stabilize, StabilizationConfig
|
|
94
96
|
|
|
95
97
|
config = StabilizationConfig(
|
|
96
|
-
timeout=
|
|
97
|
-
|
|
98
|
-
network_idle_threshold=
|
|
99
|
-
animation_detection=True,
|
|
100
|
-
strictness='normal',
|
|
101
|
-
debug_mode=True
|
|
98
|
+
timeout=10, # Max wait time (seconds)
|
|
99
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
100
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
101
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
102
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
103
|
+
debug_mode=True # Enable logging
|
|
102
104
|
)
|
|
103
105
|
|
|
104
106
|
driver = stabilize(driver, config=config)
|
|
@@ -65,12 +65,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
65
65
|
from waitless import stabilize, StabilizationConfig
|
|
66
66
|
|
|
67
67
|
config = StabilizationConfig(
|
|
68
|
-
timeout=
|
|
69
|
-
|
|
70
|
-
network_idle_threshold=
|
|
71
|
-
animation_detection=True,
|
|
72
|
-
strictness='normal',
|
|
73
|
-
debug_mode=True
|
|
68
|
+
timeout=10, # Max wait time (seconds)
|
|
69
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
70
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
71
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
72
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
73
|
+
debug_mode=True # Enable logging
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
driver = stabilize(driver, config=config)
|
|
@@ -22,18 +22,16 @@ class StabilizationConfig:
|
|
|
22
22
|
Consider lowering to 5s for faster feedback loops.
|
|
23
23
|
|
|
24
24
|
dom_settle_time: Time (seconds) DOM must be quiet to be considered stable.
|
|
25
|
-
Default 0.1s (100ms).
|
|
25
|
+
Default 0.1s (100ms). This is a FALLBACK - mutation rate
|
|
26
|
+
is the primary check for DOM stability.
|
|
27
|
+
|
|
28
|
+
mutation_rate_threshold: Maximum mutations per second for DOM to be stable.
|
|
29
|
+
Default 50/sec. This allows animated sites (typewriter
|
|
30
|
+
effects ~30/sec) while catching loading bursts (100+/sec).
|
|
26
31
|
|
|
27
32
|
network_idle_threshold: Maximum pending requests allowed for stability.
|
|
28
|
-
Default
|
|
29
|
-
|
|
30
|
-
⚠️ WARNING: Many apps have background traffic:
|
|
31
|
-
- Analytics calls
|
|
32
|
-
- Long polling
|
|
33
|
-
- Feature flags
|
|
34
|
-
- WebSocket heartbeats
|
|
35
|
-
|
|
36
|
-
If your tests timeout frequently, try setting this to 1-2.
|
|
33
|
+
Default 2 (allows background analytics/polling).
|
|
34
|
+
Set to 0 for strict mode if all requests must complete.
|
|
37
35
|
|
|
38
36
|
animation_detection: Whether to wait for CSS animations/transitions.
|
|
39
37
|
Default True. Disable for apps with infinite animations.
|
|
@@ -42,9 +40,9 @@ class StabilizationConfig:
|
|
|
42
40
|
Default True in 'strict' mode.
|
|
43
41
|
|
|
44
42
|
strictness: Overall strictness level.
|
|
45
|
-
- 'strict': All signals must be stable (recommended)
|
|
46
|
-
- 'normal': DOM + Network only (
|
|
47
|
-
- 'relaxed': DOM only (fastest
|
|
43
|
+
- 'strict': All signals must be stable (recommended for CI)
|
|
44
|
+
- 'normal': DOM + Network only (default)
|
|
45
|
+
- 'relaxed': DOM only (fastest)
|
|
48
46
|
|
|
49
47
|
debug_mode: Enable verbose logging for troubleshooting.
|
|
50
48
|
Default False.
|
|
@@ -58,7 +56,8 @@ class StabilizationConfig:
|
|
|
58
56
|
|
|
59
57
|
timeout: float = 10.0
|
|
60
58
|
dom_settle_time: float = 0.1
|
|
61
|
-
|
|
59
|
+
mutation_rate_threshold: float = 50.0 # mutations/sec - allows animations
|
|
60
|
+
network_idle_threshold: int = 2 # Allow background traffic
|
|
62
61
|
animation_detection: bool = True
|
|
63
62
|
layout_stability: bool = True
|
|
64
63
|
strictness: StrictnessLevel = 'normal'
|
|
@@ -118,6 +117,7 @@ class StabilizationConfig:
|
|
|
118
117
|
current = {
|
|
119
118
|
'timeout': self.timeout,
|
|
120
119
|
'dom_settle_time': self.dom_settle_time,
|
|
120
|
+
'mutation_rate_threshold': self.mutation_rate_threshold,
|
|
121
121
|
'network_idle_threshold': self.network_idle_threshold,
|
|
122
122
|
'animation_detection': self.animation_detection,
|
|
123
123
|
'layout_stability': self.layout_stability,
|
|
@@ -53,6 +53,7 @@ class StabilizationEngine:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
self._last_status: Optional[StabilityStatus] = None
|
|
56
|
+
self._last_browser_state: Optional[Dict[str, Any]] = None
|
|
56
57
|
self._last_blocking_factors: Dict[str, Any] = {}
|
|
57
58
|
self._timeline: list = []
|
|
58
59
|
|
|
@@ -175,6 +176,7 @@ class StabilizationEngine:
|
|
|
175
176
|
status = self.evaluator.evaluate(browser_state, current_time)
|
|
176
177
|
last_status = status
|
|
177
178
|
self._last_status = status
|
|
179
|
+
self._last_browser_state = browser_state # Store for diagnostics
|
|
178
180
|
|
|
179
181
|
if status.is_stable:
|
|
180
182
|
self._debug(f"UI stable after {elapsed:.2f}s")
|
|
@@ -260,7 +262,7 @@ class StabilizationEngine:
|
|
|
260
262
|
'network_idle_threshold': self.config.network_idle_threshold,
|
|
261
263
|
'animation_detection': self.config.animation_detection,
|
|
262
264
|
},
|
|
263
|
-
'last_status': self.
|
|
265
|
+
'last_status': self._last_browser_state, # Raw browser state with mutation_rate, etc.
|
|
264
266
|
'blocking_factors': self._last_blocking_factors,
|
|
265
267
|
'timeline': self._timeline[-50:],
|
|
266
268
|
'instrumented': self._instrumented,
|
|
@@ -68,11 +68,26 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
68
68
|
|
|
69
69
|
// ===== MUTATION OBSERVER =====
|
|
70
70
|
|
|
71
|
+
// Rolling window for mutation rate calculation
|
|
72
|
+
_mutationTimestamps: [],
|
|
73
|
+
_mutationWindowMs: 1000, // 1 second window for rate calculation
|
|
74
|
+
|
|
71
75
|
_setupMutationObserver: function() {
|
|
72
76
|
var self = this;
|
|
73
77
|
var observer = new MutationObserver(function(mutations) {
|
|
74
|
-
|
|
75
|
-
self.
|
|
78
|
+
var now = Date.now();
|
|
79
|
+
self.lastMutationTime = now;
|
|
80
|
+
|
|
81
|
+
// Add to rolling window
|
|
82
|
+
self._mutationTimestamps.push(now);
|
|
83
|
+
|
|
84
|
+
// Remove old timestamps outside the window
|
|
85
|
+
var cutoff = now - self._mutationWindowMs;
|
|
86
|
+
while (self._mutationTimestamps.length > 0 && self._mutationTimestamps[0] < cutoff) {
|
|
87
|
+
self._mutationTimestamps.shift();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
self._log('DOM mutation', { count: mutations.length, rate: self.getMutationRate() });
|
|
76
91
|
});
|
|
77
92
|
|
|
78
93
|
observer.observe(document.documentElement || document.body, {
|
|
@@ -85,6 +100,23 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
85
100
|
this._observers.push(observer);
|
|
86
101
|
},
|
|
87
102
|
|
|
103
|
+
// Calculate mutations per second from rolling window
|
|
104
|
+
getMutationRate: function() {
|
|
105
|
+
var now = Date.now();
|
|
106
|
+
var cutoff = now - this._mutationWindowMs;
|
|
107
|
+
|
|
108
|
+
// Count mutations in the last second
|
|
109
|
+
var count = 0;
|
|
110
|
+
for (var i = 0; i < this._mutationTimestamps.length; i++) {
|
|
111
|
+
if (this._mutationTimestamps[i] > cutoff) {
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Return rate per second
|
|
117
|
+
return count;
|
|
118
|
+
},
|
|
119
|
+
|
|
88
120
|
// ===== NETWORK INTERCEPTORS =====
|
|
89
121
|
|
|
90
122
|
_setupNetworkInterceptors: function() {
|
|
@@ -246,6 +278,7 @@ INSTRUMENTATION_SCRIPT = """
|
|
|
246
278
|
stable: this.isStable(),
|
|
247
279
|
pending_requests: this.pendingRequests,
|
|
248
280
|
last_mutation_time: this.lastMutationTime,
|
|
281
|
+
mutation_rate: this.getMutationRate(), // mutations per second
|
|
249
282
|
active_animations: this.activeAnimations + this.activeTransitions,
|
|
250
283
|
layout_shifting: this.layoutShifting,
|
|
251
284
|
pending_request_details: this.pendingRequestDetails.slice(),
|
|
@@ -116,14 +116,74 @@ class StabilizedWebDriver:
|
|
|
116
116
|
return attr
|
|
117
117
|
|
|
118
118
|
def _stabilized_find_element(self, *args, **kwargs) -> StabilizedWebElement:
|
|
119
|
-
"""
|
|
120
|
-
element
|
|
121
|
-
|
|
119
|
+
"""
|
|
120
|
+
Find element with automatic waiting.
|
|
121
|
+
|
|
122
|
+
This method:
|
|
123
|
+
1. Waits for page stability first
|
|
124
|
+
2. Tries to find the element
|
|
125
|
+
3. If element not found, retries until timeout
|
|
126
|
+
|
|
127
|
+
This eliminates the need for explicit WebDriverWait in test code.
|
|
128
|
+
"""
|
|
129
|
+
import time
|
|
130
|
+
from selenium.common.exceptions import NoSuchElementException
|
|
131
|
+
|
|
132
|
+
timeout = self._engine.config.timeout
|
|
133
|
+
poll_interval = self._engine.config.poll_interval
|
|
134
|
+
start_time = time.time()
|
|
135
|
+
last_exception = None
|
|
136
|
+
|
|
137
|
+
while (time.time() - start_time) < timeout:
|
|
138
|
+
# Wait for page stability first
|
|
139
|
+
try:
|
|
140
|
+
self._engine.wait_for_stability()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass # Continue trying to find element
|
|
143
|
+
|
|
144
|
+
# Try to find the element
|
|
145
|
+
try:
|
|
146
|
+
element = self._driver.find_element(*args, **kwargs)
|
|
147
|
+
return StabilizedWebElement(element, self._engine)
|
|
148
|
+
except NoSuchElementException as e:
|
|
149
|
+
last_exception = e
|
|
150
|
+
# Element not found - wait and retry
|
|
151
|
+
time.sleep(poll_interval)
|
|
152
|
+
|
|
153
|
+
# Timeout reached - raise the last exception
|
|
154
|
+
if last_exception:
|
|
155
|
+
raise last_exception
|
|
156
|
+
raise NoSuchElementException(f"Element not found within {timeout}s: {args}")
|
|
122
157
|
|
|
123
158
|
def _stabilized_find_elements(self, *args, **kwargs) -> List[StabilizedWebElement]:
|
|
124
|
-
"""
|
|
125
|
-
elements
|
|
126
|
-
|
|
159
|
+
"""
|
|
160
|
+
Find elements with automatic waiting.
|
|
161
|
+
|
|
162
|
+
Similar to find_element but returns list (may be empty).
|
|
163
|
+
"""
|
|
164
|
+
import time
|
|
165
|
+
|
|
166
|
+
timeout = self._engine.config.timeout
|
|
167
|
+
poll_interval = self._engine.config.poll_interval
|
|
168
|
+
start_time = time.time()
|
|
169
|
+
|
|
170
|
+
while (time.time() - start_time) < timeout:
|
|
171
|
+
# Wait for stability first
|
|
172
|
+
try:
|
|
173
|
+
self._engine.wait_for_stability()
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
# Try to find elements
|
|
178
|
+
elements = self._driver.find_elements(*args, **kwargs)
|
|
179
|
+
if elements:
|
|
180
|
+
return [StabilizedWebElement(el, self._engine) for el in elements]
|
|
181
|
+
|
|
182
|
+
# No elements found - wait and retry
|
|
183
|
+
time.sleep(poll_interval)
|
|
184
|
+
|
|
185
|
+
# Return empty list if nothing found
|
|
186
|
+
return []
|
|
127
187
|
|
|
128
188
|
@property
|
|
129
189
|
def unwrapped(self) -> 'WebDriver':
|
|
@@ -154,11 +154,34 @@ class SignalEvaluator:
|
|
|
154
154
|
)
|
|
155
155
|
|
|
156
156
|
def _evaluate_dom(self, state: Dict[str, Any], current_time: float) -> Signal:
|
|
157
|
-
"""
|
|
157
|
+
"""
|
|
158
|
+
Evaluate DOM mutation activity using MUTATION RATE.
|
|
159
|
+
|
|
160
|
+
Key insight: Animated sites have steady ~30-50 mutations/sec (typewriter, particles).
|
|
161
|
+
Loading bursts have 100+ mutations/sec. We consider stable when rate is LOW, not zero.
|
|
162
|
+
|
|
163
|
+
Primary check: mutation_rate <= threshold (50/sec default)
|
|
164
|
+
Fallback: time since last mutation (for older browsers)
|
|
165
|
+
"""
|
|
166
|
+
mutation_rate = state.get('mutation_rate')
|
|
158
167
|
last_mutation = state.get('last_mutation_time', 0)
|
|
159
|
-
time_since_mutation = (current_time * 1000) - last_mutation # Convert to ms
|
|
160
|
-
threshold_ms = self.config.dom_settle_time * 1000
|
|
161
168
|
|
|
169
|
+
# Primary: Use mutation rate if available
|
|
170
|
+
if mutation_rate is not None:
|
|
171
|
+
threshold = self.config.mutation_rate_threshold
|
|
172
|
+
is_stable = mutation_rate <= threshold
|
|
173
|
+
return Signal(
|
|
174
|
+
signal_type=SignalType.DOM_MUTATIONS,
|
|
175
|
+
state=SignalState.STABLE if is_stable else SignalState.UNSTABLE,
|
|
176
|
+
value=mutation_rate,
|
|
177
|
+
threshold=threshold,
|
|
178
|
+
is_mandatory=True,
|
|
179
|
+
details=f"Mutation rate: {mutation_rate:.0f}/sec (threshold: {threshold:.0f}/sec)",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Fallback: Use time since last mutation
|
|
183
|
+
time_since_mutation = (current_time * 1000) - last_mutation
|
|
184
|
+
threshold_ms = self.config.dom_settle_time * 1000
|
|
162
185
|
is_stable = time_since_mutation >= threshold_ms
|
|
163
186
|
|
|
164
187
|
return Signal(
|
|
@@ -187,9 +210,15 @@ class SignalEvaluator:
|
|
|
187
210
|
)
|
|
188
211
|
|
|
189
212
|
def _evaluate_animations(self, state: Dict[str, Any]) -> Signal:
|
|
190
|
-
"""
|
|
213
|
+
"""
|
|
214
|
+
Evaluate CSS animation/transition activity.
|
|
215
|
+
|
|
216
|
+
Only mandatory in 'strict' mode. In 'normal' and 'relaxed' modes,
|
|
217
|
+
animations are cosmetic and don't block interaction.
|
|
218
|
+
"""
|
|
191
219
|
active = state.get('active_animations', 0)
|
|
192
|
-
|
|
220
|
+
# Only mandatory in strict mode - animations are usually cosmetic
|
|
221
|
+
is_mandatory = self.config.strictness == 'strict'
|
|
193
222
|
is_stable = active == 0
|
|
194
223
|
|
|
195
224
|
return Signal(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: waitless
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Eliminate explicit waits in UI automation by detecting true UI stability
|
|
5
5
|
Author-email: Dhiraj Das <dhirajdas.66@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -22,9 +22,11 @@ Classifier: Topic :: Software Development :: Testing
|
|
|
22
22
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
23
|
Requires-Python: >=3.9
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
25
26
|
Provides-Extra: dev
|
|
26
27
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
27
28
|
Requires-Dist: selenium>=4.0; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
28
30
|
|
|
29
31
|
# Waitless
|
|
30
32
|
|
|
@@ -93,12 +95,12 @@ When you interact, waitless ensures the page is truly ready.
|
|
|
93
95
|
from waitless import stabilize, StabilizationConfig
|
|
94
96
|
|
|
95
97
|
config = StabilizationConfig(
|
|
96
|
-
timeout=
|
|
97
|
-
|
|
98
|
-
network_idle_threshold=
|
|
99
|
-
animation_detection=True,
|
|
100
|
-
strictness='normal',
|
|
101
|
-
debug_mode=True
|
|
98
|
+
timeout=10, # Max wait time (seconds)
|
|
99
|
+
mutation_rate_threshold=50, # mutations/sec considered stable (allows animations)
|
|
100
|
+
network_idle_threshold=2, # Max pending requests (allows background traffic)
|
|
101
|
+
animation_detection=True, # Track CSS animations (non-blocking in normal mode)
|
|
102
|
+
strictness='normal', # 'strict' | 'normal' | 'relaxed'
|
|
103
|
+
debug_mode=True # Enable logging
|
|
102
104
|
)
|
|
103
105
|
|
|
104
106
|
driver = stabilize(driver, config=config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|