wcp-library 1.6.3__py3-none-any.whl → 1.6.5__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.
wcp_library/__init__.py CHANGED
@@ -1,14 +1,27 @@
1
+ import asyncio
2
+ import logging
3
+ import random
1
4
  import sys
5
+ import time
6
+ from functools import wraps
2
7
  from pathlib import Path
3
- from typing import Generator
8
+ from typing import Callable, Generator, Optional, Type
4
9
 
5
10
  # PyInstaller import
6
11
  import pip_system_certs.wrapt_requests
7
12
  import cryptography.hazmat.primitives.kdf.pbkdf2
8
13
 
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # For retry logic
17
+ MAX_ATTEMPTS = 5
18
+ DELAY = 2
19
+ BACKOFF = 2
20
+ JITTER = 3
21
+
9
22
 
10
23
  # Application Path
11
- if getattr(sys, 'frozen', False):
24
+ if getattr(sys, "frozen", False):
12
25
  application_path = sys.executable
13
26
  application_path = Path(application_path).parent
14
27
  else:
@@ -25,4 +38,94 @@ def divide_chunks(list_obj: list, size: int) -> Generator:
25
38
  """
26
39
 
27
40
  for i in range(0, len(list_obj), size):
28
- yield list_obj[i:i + size]
41
+ yield list_obj[i : i + size]
42
+
43
+
44
+ def retry(
45
+ exceptions: tuple[Type[Exception]],
46
+ max_attempts: Optional[int] = MAX_ATTEMPTS,
47
+ delay: Optional[int] = DELAY,
48
+ backoff: Optional[int] = BACKOFF,
49
+ jitter: Optional[int] = JITTER,
50
+ ) -> Callable:
51
+ """
52
+ Decorator to retry a synchronous function on a specified exception with exponential backoff and jitter.
53
+
54
+ :param exceptions: Tuple of exception types to catch and retry on.
55
+ :param max_attempts: Maximum number of retry attempts.
56
+ :param delay: Initial delay between retries (in seconds).
57
+ :param backoff: Multiplier to increase delay after each failure.
58
+ :param jitter: Maximum number of seconds to add randomly to each delay.
59
+ :return: The decorated function with retry logic.
60
+ """
61
+
62
+ def decorator(func: Callable) -> Callable:
63
+ @wraps(func)
64
+ def wrapper(*args, **kwargs):
65
+ wait_time = delay
66
+
67
+ for attempt in range(0, max_attempts):
68
+ try:
69
+ return func(*args, **kwargs)
70
+ except exceptions as error:
71
+ if attempt == max_attempts:
72
+ logger.error("Retry failed after %d attempts.", max_attempts)
73
+ raise
74
+
75
+ randomized_delay = wait_time + random.uniform(0, jitter)
76
+ logger.warning(
77
+ "Attempt %d failed: %s. Retrying in %.2f seconds...",
78
+ attempt,
79
+ error,
80
+ randomized_delay,
81
+ )
82
+ time.sleep(randomized_delay)
83
+ wait_time *= backoff
84
+ return None
85
+ return wrapper
86
+ return decorator
87
+
88
+
89
+ def async_retry(
90
+ exceptions: tuple[Type[Exception]],
91
+ max_attempts: Optional[int] = MAX_ATTEMPTS,
92
+ delay: Optional[int] = DELAY,
93
+ backoff: Optional[int] = BACKOFF,
94
+ jitter: Optional[int] = JITTER,
95
+ ) -> Callable:
96
+ """
97
+ Decorator to retry an async function on a specified exception with exponential backoff and jitter.
98
+
99
+ :param exceptions: Tuple of exception types to catch and retry on.
100
+ :param max_attempts: Maximum number of retry attempts.
101
+ :param delay: Initial delay between retries (in seconds).
102
+ :param backoff: Multiplier to increase delay after each failure.
103
+ :param jitter: Maximum number of seconds to add randomly to each delay.
104
+ :return: The decorated async function with retry logic.
105
+ """
106
+
107
+ def decorator(func: Callable) -> Callable:
108
+ @wraps(func)
109
+ async def wrapper(*args, **kwargs):
110
+ wait_time = delay
111
+
112
+ for attempt in range(0, max_attempts):
113
+ try:
114
+ return await func(*args, **kwargs)
115
+ except exceptions as error:
116
+ if attempt == max_attempts:
117
+ logger.error("Retry failed after %d attempts.", max_attempts)
118
+ raise
119
+
120
+ randomized_delay = wait_time + random.uniform(0, jitter)
121
+ logger.warning(
122
+ "Attempt %d failed: %s. Retrying in %.2f seconds...",
123
+ attempt,
124
+ error,
125
+ randomized_delay,
126
+ )
127
+ await asyncio.sleep(randomized_delay)
128
+ wait_time *= backoff
129
+ return None
130
+ return wrapper
131
+ return decorator
File without changes
@@ -0,0 +1,299 @@
1
+ """
2
+ This module provides a framework for browser automation using Selenium WebDriver.
3
+
4
+ It defines a base class `BaseSelenium` that encapsulates shared functionality for browser setup,
5
+ option configuration, and lifecycle management. Subclasses for specific browsers—`Chrome`,
6
+ `Firefox`, and `Edge`—extend this base to implement browser-specific driver creation.
7
+
8
+ Additionally, the `Browser` context manager simplifies the use of these classes by managing
9
+ initialization and cleanup of browser sessions.
10
+
11
+ Classes:
12
+ BaseSelenium: Abstract base class for browser automation.
13
+ Chrome: Chrome-specific WebDriver implementation.
14
+ Firefox: Firefox-specific WebDriver implementation.
15
+ Edge: Edge-specific WebDriver implementation.
16
+ Browser: Context manager for browser session lifecycle.
17
+
18
+ Usage:
19
+ with Browser(Firefox) as browser:
20
+ browser.go_to("https://example.com")
21
+ """
22
+
23
+ import logging
24
+ from typing import Dict, Optional
25
+
26
+ from selenium import webdriver
27
+ from selenium.webdriver.chrome.options import Options as ChromeOptions
28
+ from selenium.webdriver.firefox.options import Options as FirefoxOptions
29
+ from selenium.webdriver.edge.options import Options as EdgeOptions
30
+ from yarl import URL
31
+
32
+ from wcp_library.browser_automation.interactions import UIInteractions, WEInteractions
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # +--------------------------------------------------------------------------------------------------------------------------------------------------------+
37
+ # | === Browser options and usage === |
38
+ # +--------------+--------------------------------------------+-----------------------------------------------+--------------------------------------------+
39
+ # | Browser | Description | JSON Configuration | Possible Permutations |
40
+ # +--------------+--------------------------------------------+-----------------------------------------------+--------------------------------------------+
41
+ # | All Browsers | Set browser timeouts (in ms) | {"timeouts": {"implicit": 5000, ...}} | implicit, pageLoad, script |
42
+ # | All Browsers | Name of the browser (e.g., 'chrome', ...) | {"browserName": "chrome"} | chrome, firefox, edge, safari |
43
+ # | All Browsers | Specific version of the browser to use. | {"browserVersion": "latest"} | latest, 91.0, 90.0 |
44
+ # | All Browsers | OS platform (e.g., 'Windows 10', 'Linux') | {"platformName": "Windows 10"} | Windows 10, Linux, macOS |
45
+ # | All Browsers | Strategy for page loads: normal, eager... | {"pageLoadStrategy": "normal"} | normal, eager, none |
46
+ # | All Browsers | Accept self-signed or invalid certs | {"acceptInsecureCerts": true} | true, false |
47
+ # | Chrome | Run browser in headless mode | {"args": ["--headless"]} | --headless |
48
+ # | Chrome | Disable GPU acceleration | {"args": ["--disable-gpu"]} | --disable-gpu |
49
+ # | Chrome | Set experimental options | {"prefs": {"download.default_directory":...}} | profile.default_content_settings.popups... |
50
+ # | Chrome | Set path to Chrome binary | {"binary": "/path/to/chrome"} | /path/to/chrome |
51
+ # | Chrome | Set Chrome extensions | {"extensions": ["/path/to/extension"]} | /path/to/extension |
52
+ # | Chrome | Exclude switches | {"excludeSwitches": ["enable-automation"]} | enable-automation |
53
+ # | Chrome | Use automation extension | {"useAutomationExtension": false} | true, false |
54
+ # | Firefox | Set download folder list | {"prefs": {"browser.download.folderList": 2}} | 1(Download folder), 2(User set directory) |
55
+ # | Firefox | Set download directory | {"prefs": {"browser.download.dir": "/tmp"}} | /tmp |
56
+ # | Firefox | Run Firefox in headless mode | {"args": ["-headless"]} | -headless |
57
+ # | Firefox | Set Firefox log level | {"log": {"level": "trace"}} | trace, debug, info, warn, error |
58
+ # | Firefox | Set Firefox profile | {"profile": "/path/to/profile"} | /path/to/profile |
59
+ # | Firefox | Set path to Firefox binary | {"binary": "/path/to/firefox"} | /path/to/firefox |
60
+ # | Edge | Run Edge in headless mode | {"args": ["--headless"]} | --headless |
61
+ # | Edge | Set path to Edge binary | {"binary": "/path/to/edge"} | /path/to/edge |
62
+ # | Edge | Use Chromium-based Edge | {"useChromium": true} | true, false |
63
+ # | Edge | Set Edge Chromium driver | {"edgeChromiumDriver": "/path/to/driver"} | /path/to/driver |
64
+ # | Chrome/Edge | Set initial window size | {"args": ["--window-size=1920,1080"]} | --window-size=int,int |
65
+ # | Firefox | Launch in private browsing mode | {"args": ["-private"]} | -private |
66
+ # +--------------+--------------------------------------------+-----------------------------------------------+--------------------------------------------+
67
+
68
+
69
+ class BaseSelenium(UIInteractions, WEInteractions):
70
+ """
71
+ Base class for Selenium-based browser automation.
72
+
73
+ This class provides common functionality for initializing and managing Selenium
74
+ WebDriver instances, as well as adding custom options to the WebDriver.
75
+
76
+ :param browser_options: Dictionary containing custom options for the WebDriver.
77
+ :param driver: Selenium WebDriver instance (optional, defaults to None).
78
+ """
79
+
80
+ def __init__(self, browser_options: dict = None):
81
+ self.browser_options = browser_options or {}
82
+ self.driver = None
83
+
84
+ def __enter__(self) -> "BaseSelenium":
85
+ self.driver = self._create_driver()
86
+ super().__init__(self.driver)
87
+ return self
88
+
89
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
90
+ if exc_type:
91
+ logger.error(f"Exception occurred: {exc_type.__name__}: {exc_val}\nTraceback: {exc_tb}")
92
+ if self.driver:
93
+ self.driver.quit()
94
+
95
+ def _create_driver(self) -> webdriver:
96
+ """
97
+ Abstract method to create a Selenium WebDriver instance.
98
+
99
+ This method must be implemented by subclasses to instantiate and return
100
+ a specific browser WebDriver (e.g., Chrome, Firefox, Edge).
101
+
102
+ :return: Selenium WebDriver instance for the specified browser.
103
+ :raises: NotImplementedError
104
+ """
105
+
106
+ raise NotImplementedError("Subclasses must implement this method")
107
+
108
+ def _add_options(
109
+ self, options: ChromeOptions | FirefoxOptions | EdgeOptions
110
+ ) -> None:
111
+ """
112
+ Add custom options to the Selenium WebDriver.
113
+
114
+ This method applies custom options such as headless mode, download paths,
115
+ and command-line arguments to the WebDriver options.
116
+
117
+ :param options: ChromeOptions | FirefoxOptions | EdgeOptions
118
+ """
119
+
120
+ if not self.browser_options:
121
+ return
122
+
123
+ # Apply standard Selenium options
124
+ for key, value in self.browser_options.items():
125
+ if hasattr(options, key) and "args" not in key:
126
+ setattr(options, key, value)
127
+
128
+ # Apply command-line arguments
129
+ args = self.browser_options.get("args", [])
130
+ for arg in args:
131
+ options.add_argument(arg)
132
+
133
+ # Handle download path
134
+ download_path = self.browser_options.get("download_path")
135
+ if download_path:
136
+ if isinstance(options, FirefoxOptions):
137
+ options.set_preference("browser.download.folderList", 2)
138
+ options.set_preference("browser.download.dir", str(download_path))
139
+ options.set_preference(
140
+ "browser.helperApps.neverAsk.saveToDisk", "application/octet-stream"
141
+ )
142
+ elif isinstance(options, (ChromeOptions, EdgeOptions)):
143
+ prefs = {
144
+ "download.default_directory": str(download_path),
145
+ "download.prompt_for_download": False,
146
+ "directory_upgrade": True,
147
+ }
148
+ options.add_experimental_option("prefs", prefs)
149
+
150
+ def go_to(self, url: str | URL):
151
+ """Navigate to the specified URL.
152
+
153
+ :param url: The URL to navigate to.
154
+ :raises RuntimeError: If the WebDriver is not initialized.
155
+ """
156
+
157
+ if self.driver:
158
+ self.driver.get(str(url))
159
+ else:
160
+ raise RuntimeError("WebDriver is not initialized.")
161
+
162
+ def get_url(self) -> str:
163
+ """Get the current URL of the page.
164
+
165
+ :return: The current URL of the page.
166
+ :raises RuntimeError: If the WebDriver is not initialized.
167
+ """
168
+
169
+ if self.driver:
170
+ return self.driver.current_url
171
+ raise RuntimeError("WebDriver is not initialized.")
172
+
173
+ def get_title(self) -> str:
174
+ """Get the title of the current page.
175
+
176
+ :return: The title of the current page.
177
+ :raises RuntimeError: If the WebDriver is not initialized.
178
+ """
179
+
180
+ if self.driver:
181
+ return self.driver.title
182
+ raise RuntimeError("WebDriver is not initialized.")
183
+
184
+ def switch_to_window(
185
+ self, window_handle: Optional[str] = None
186
+ ) -> Optional[Dict[str, list]]:
187
+ """
188
+ Switches the browser context to a new window.
189
+
190
+ If a specific window handle is provided, the driver will switch to that window.
191
+ Otherwise, it will attempt to switch to a newly opened window that is different
192
+ from the current one.
193
+
194
+ :param window_handle: The handle of the window to switch to. If None, the method will search for a new window handle.
195
+ :return: A dictionary containing the original window handle, the new window handle, and a list of all window handles at the time of switching.
196
+ :raises RuntimeError: If the WebDriver is not initialized.
197
+ """
198
+
199
+ if window_handle:
200
+ self.driver.switch_to.window(window_handle)
201
+ return None
202
+
203
+ original_window = self.driver.current_window_handle
204
+ all_windows = self.driver.window_handles
205
+
206
+ for new_window in all_windows:
207
+ if new_window != original_window:
208
+ self.driver.switch_to.window(new_window)
209
+ return {
210
+ "original_window": original_window,
211
+ "new_window": new_window,
212
+ "all_windows": all_windows,
213
+ }
214
+ return None
215
+
216
+ def close_window(self, window_handle: Optional[str] = None) -> None:
217
+ """
218
+ Closes the specified browser window.
219
+
220
+ If a window handle is provided, that window will be closed.
221
+ Otherwise, the currently active window will be closed.
222
+
223
+ :param window_handle: The handle of the window to close. If None, the current window will be closed.
224
+ """
225
+
226
+ self.driver.close(window_handle or self.driver.current_window_handle)
227
+
228
+
229
+ class Firefox(BaseSelenium):
230
+ """
231
+ Class for Firefox browser automation using Selenium.
232
+
233
+ This class extends the BaseSelenium class and provides functionality for creating
234
+ and managing Firefox WebDriver instances.
235
+ """
236
+
237
+ def create_driver(self) -> webdriver.Firefox:
238
+ options = FirefoxOptions()
239
+ self._add_options(options)
240
+ return webdriver.Firefox(options=options)
241
+
242
+
243
+ class Edge(BaseSelenium):
244
+ """
245
+ Class for Edge browser automation using Selenium.
246
+
247
+ This class extends the BaseSelenium class and provides functionality for creating
248
+ and managing Edge WebDriver instances.
249
+ """
250
+
251
+ def create_driver(self) -> webdriver.Edge:
252
+ options = EdgeOptions()
253
+ self._add_options(options)
254
+ return webdriver.Edge(options=options)
255
+
256
+
257
+ class Chrome(BaseSelenium):
258
+ """
259
+ Class for Chrome browser automation using Selenium.
260
+
261
+ This class extends the BaseSelenium class and provides functionality for creating
262
+ and managing Chrome WebDriver instances.
263
+ """
264
+
265
+ def create_driver(self) -> webdriver.Chrome:
266
+ options = ChromeOptions()
267
+ self._add_options(options)
268
+ return webdriver.Chrome(options=options)
269
+
270
+
271
+ class Browser(BaseSelenium):
272
+ """
273
+ Class for managing browser automation using Selenium.
274
+
275
+ This class provides functionality for initializing and managing browser instances
276
+ using the specified browser class and options.
277
+
278
+ It acts as a context manager, allowing for easy setup and teardown of browser sessions.
279
+
280
+ :param browser_class: The class of the browser to be used (e.g., Firefox, Edge, Chrome).
281
+ :param browser_options: Dictionary containing custom options for the browser.
282
+ """
283
+
284
+ def __init__(self, browser_class: type, browser_options: dict = None):
285
+ super().__init__(browser_options)
286
+ self.browser_class = browser_class
287
+ self.browser_options = browser_options or {}
288
+ self.browser_instance = None
289
+
290
+ def __enter__(self) -> BaseSelenium:
291
+ self.browser_instance = self.browser_class(self.browser_options)
292
+ self.browser_instance.driver = self.browser_instance.create_driver()
293
+ return self.browser_instance
294
+
295
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
296
+ if exc_type:
297
+ logger.error(f"Exception occurred: {exc_type.__name__}: {exc_val}\nTraceback: {exc_tb}")
298
+ if self.browser_instance and self.browser_instance.driver:
299
+ self.browser_instance.driver.quit()
@@ -0,0 +1,739 @@
1
+ """
2
+ This module provides classes and methods for interacting with web elements using Selenium WebDriver.
3
+
4
+ The module contains the following classes:
5
+ - Interactions: A base class for common web interactions.
6
+ - UIInteractions: A subclass of Interactions for interacting with web elements using locators.
7
+ - WEInteractions: A subclass of Interactions for interacting with web elements directly.
8
+
9
+ Each class provides methods for performing various web interactions such as navigating to a URL,
10
+ taking screenshots, waiting for elements, clicking buttons, entering text, and more.
11
+ """
12
+
13
+ import logging
14
+ from datetime import datetime
15
+ from io import StringIO
16
+ from typing import List, Optional, Union
17
+
18
+ import pandas as pd
19
+ from selenium.common.exceptions import (
20
+ NoSuchElementException,
21
+ TimeoutException,
22
+ WebDriverException,
23
+ )
24
+ from selenium.webdriver.common.by import By
25
+ from selenium.webdriver.remote.webelement import WebElement
26
+ from selenium.webdriver.support import expected_conditions as EC
27
+ from selenium.webdriver.support.ui import Select, WebDriverWait
28
+
29
+ EXECUTION_ERROR_SCREENSHOT_FOLDER = "P:/Python/RPA/Execution Error Screenshots"
30
+
31
+
32
+ class Interactions:
33
+ """Class for interacting with web elements using Selenium WebDriver.
34
+
35
+ Attributes:
36
+ driver: The Selenium WebDriver instance.
37
+ """
38
+
39
+ def __init__(self, driver):
40
+ self.driver = driver
41
+ logging.basicConfig(level=logging.INFO)
42
+
43
+ def take_screenshot(self, file_path: str):
44
+ """Take a screenshot of the current page and save it to the specified file path.
45
+
46
+ :param file_path: The path where the screenshot will be saved.
47
+ :raises RuntimeError: If the WebDriver is not initialized.
48
+ """
49
+
50
+ if self.driver:
51
+ self.driver.save_screenshot(file_path)
52
+ else:
53
+ raise RuntimeError("WebDriver is not initialized.")
54
+
55
+ def _take_error_screenshot(self) -> None:
56
+ """
57
+ Take a screenshot of the current page and save it to the P drive.
58
+
59
+ :return: None
60
+ :raises RuntimeError: If the WebDriver is not initialized.
61
+ """
62
+
63
+ self.take_screenshot(
64
+ f"{EXECUTION_ERROR_SCREENSHOT_FOLDER}/Failure Screenshot - {datetime.now().strftime('%Y-%m-%d_%H-%M')}.png"
65
+ )
66
+
67
+ def _get_expect_condition_multiple(self, expected_condition: Optional[str]) -> EC:
68
+ """Get the expected condition for multiple elements based on the provided string.
69
+
70
+ :param expected_condition: The expected condition type.
71
+ :return: The expected condition object for multiple elements.
72
+ :raises RuntimeError: If the WebDriver is not initialized.
73
+ """
74
+
75
+ return (
76
+ EC.presence_of_all_elements_located
77
+ if expected_condition == "present"
78
+ else EC.visibility_of_all_elements_located
79
+ )
80
+
81
+ def _get_wait_time(self, wait_time: float) -> float:
82
+ """If a wait time has been specified it is returned. Otherwise, the wait time comes from
83
+ the implicit timeout set when initializing the browser. That value is in milliseconds so
84
+ it is divided by 1000 (WebDriverWait expects a float number in seconds)
85
+
86
+ :param wait_time: The wait time in milliseconds.
87
+ :return: The wait time in seconds.
88
+ :raises RuntimeError: If the WebDriver is not initialized.
89
+ """
90
+
91
+ return (
92
+ wait_time
93
+ or (
94
+ getattr(self, "browser_options", {})
95
+ .get("timeouts", {})
96
+ .get("implicit", 0)
97
+ )
98
+ / 1000
99
+ ) # Timeouts are in ms
100
+
101
+
102
+ class UIInteractions(Interactions):
103
+ """Class for interacting with UI elements using Selenium WebDriver."""
104
+
105
+ def _get_locator(self, locator: str) -> str:
106
+ """Get the locator type based on the provided string.
107
+
108
+ :param locator: The locator type.
109
+ :return: The locator type.
110
+ :raises RuntimeError: If the WebDriver is not initialized.
111
+ """
112
+
113
+ match locator:
114
+ case "id":
115
+ by = By.ID
116
+ case "name":
117
+ by = By.NAME
118
+ case "class":
119
+ by = By.CLASS_NAME
120
+ case "tag":
121
+ by = By.TAG_NAME
122
+ case "xpath":
123
+ by = By.XPATH
124
+ case "link_text":
125
+ by = By.LINK_TEXT
126
+ case "partial_link_text":
127
+ by = By.PARTIAL_LINK_TEXT
128
+ case _:
129
+ by = By.CSS_SELECTOR
130
+ return by
131
+
132
+ def _get_expected_condition(self, expected_condition: Optional[str]) -> EC:
133
+ """Get the expected condition based on the provided string.
134
+
135
+ :param expected_condition: The expected condition type.
136
+ :return: The expected condition object.
137
+ :raises RuntimeError: If the WebDriver is not initialized.
138
+ """
139
+
140
+ match expected_condition:
141
+ case "present":
142
+ expected_condition = EC.presence_of_element_located
143
+ case "visible":
144
+ expected_condition = EC.visibility_of_element_located
145
+ case "selected":
146
+ expected_condition = EC.element_located_to_be_selected
147
+ case "frame_available":
148
+ expected_condition = EC.frame_to_be_available_and_switch_to_it
149
+ case _:
150
+ expected_condition = EC.element_to_be_clickable
151
+ return expected_condition
152
+
153
+ def get_element(
154
+ self,
155
+ element_value: str,
156
+ locator: Optional[str] = None,
157
+ expected_condition: Optional[str] = None,
158
+ wait_time: Optional[float] = 0,
159
+ ) -> WebElement:
160
+ """Get a single WebElement based on the expected condition, locator, and element_value.
161
+
162
+ :param element_value: The expected element value.
163
+ :param locator: The locator type.
164
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
165
+ 'xpath', 'link_text', 'partial_link_text'
166
+ :param expected_condition: The expected condition type.
167
+ Options: 'clickable'(Default), 'present', 'visible',
168
+ 'selected', 'frame_available'
169
+ :param wait_time: Time to wait for the condition.
170
+ :return: The located WebElement.
171
+ :raises RuntimeError: If the WebDriver is not initialized.
172
+ :raises TimeoutException: If the element is not found within the wait time.
173
+ :raises NoSuchElementException: If the element is not found.
174
+ :raises WebDriverException: If a WebDriverException occurs.
175
+ """
176
+
177
+ try:
178
+ return WebDriverWait(self.driver, self._get_wait_time(wait_time)).until(
179
+ self._get_expected_condition(expected_condition)(
180
+ (self._get_locator(locator), element_value)
181
+ )
182
+ )
183
+ except TimeoutException as exc:
184
+ self._take_error_screenshot()
185
+ raise TimeoutException(
186
+ f"Timeout exception for element with locator {locator} and value {element_value}"
187
+ ) from exc
188
+ except NoSuchElementException as exc:
189
+ self._take_error_screenshot()
190
+ raise NoSuchElementException(
191
+ f"Element with locator {locator} and value {element_value} not found."
192
+ ) from exc
193
+ except WebDriverException as exc:
194
+ self._take_error_screenshot()
195
+ raise WebDriverException(f"WebDriverException occurred: {exc}") from exc
196
+
197
+ def get_multiple_elements(
198
+ self,
199
+ element_value: str,
200
+ locator: Optional[str] = None,
201
+ expected_condition: Optional[str] = None,
202
+ wait_time: Optional[float] = 0,
203
+ ) -> List[WebElement]:
204
+ """Get a list of WebElements based on the expected condition, locator, and element_value.
205
+
206
+ :param element_value: The expected element value.
207
+ :param locator: The locator type.
208
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
209
+ 'xpath', 'link_text', 'partial_link_text'
210
+ :param expected_condition: The expected condition type.
211
+ Options: 'clickable'(Default), 'present', 'visible',
212
+ 'selected', 'frame_available'
213
+ :param wait_time: Time to wait for the condition.
214
+ :return: A list of located WebElements.
215
+ :raises RuntimeError: If the WebDriver is not initialized.
216
+ :raises TimeoutException: If the elements are not found within the wait time.
217
+ :raises NoSuchElementException: If the elements are not found.
218
+ :raises WebDriverException: If a WebDriverException occurs.
219
+ """
220
+
221
+ try:
222
+ return WebDriverWait(self.driver, self._get_wait_time(wait_time)).until(
223
+ self._get_expect_condition_multiple(expected_condition)(
224
+ (self._get_locator(locator), element_value)
225
+ )
226
+ )
227
+ except TimeoutException as exc:
228
+ self._take_error_screenshot()
229
+ raise TimeoutException(
230
+ f"Timeout exception for element with locator {locator} and value {element_value} not found."
231
+ ) from exc
232
+ except NoSuchElementException as exc:
233
+ self._take_error_screenshot()
234
+ raise NoSuchElementException(
235
+ f"Element with locator {locator} and value {element_value} not found."
236
+ ) from exc
237
+ except WebDriverException as exc:
238
+ self._take_error_screenshot()
239
+ raise WebDriverException(f"WebDriverException occurred: {exc}") from exc
240
+
241
+ def get_text(
242
+ self,
243
+ element_value: str,
244
+ locator: Optional[str] = None,
245
+ expected_condition: Optional[str] = None,
246
+ ) -> str:
247
+ """Get the text of the WebElement based on the locator and expected condition.
248
+
249
+ :param element_value: The value used to identify the element.
250
+ :param locator: The locator type.
251
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
252
+ 'xpath', 'link_text', 'partial_link_text'
253
+ :param expected_condition: The expected condition type.
254
+ Options: 'clickable'(Default), 'present', 'visible',
255
+ 'selected', 'frame_available'
256
+ :return: The text of the located WebElement.
257
+ :raises RuntimeError: If the WebDriver is not initialized.
258
+ """
259
+
260
+ return self.get_element(element_value, locator, expected_condition).text
261
+
262
+ def get_table(
263
+ self,
264
+ element_value: str,
265
+ locator: Optional[str] = None,
266
+ expected_condition: Optional[str] = None,
267
+ ) -> pd.DataFrame:
268
+ """Get the data from a table element.
269
+
270
+ :param element_value: The value used to identify the element.
271
+ :param locator: The locator type.
272
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
273
+ 'xpath', 'link_text', 'partial_link_text'
274
+ :param expected_condition: The expected condition type.
275
+ Options: 'clickable'(Default), 'present', 'visible',
276
+ 'selected', 'frame_available'
277
+ :return: The data from the table element.
278
+ :raises RuntimeError: If the WebDriver is not initialized.
279
+ """
280
+
281
+ element = self.get_element(element_value, locator, expected_condition)
282
+ return pd.read_html(StringIO(element.get_attribute("outerHTML")))[0]
283
+
284
+ def get_value(
285
+ self,
286
+ element_value: str,
287
+ locator: Optional[str] = None,
288
+ expected_condition: Optional[str] = None,
289
+ ) -> str:
290
+ """Get the value attribute of the WebElement based on the locator and expected condition.
291
+
292
+ :param element_value: The value used to identify the element.
293
+ :param locator: The locator type.
294
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
295
+ 'xpath', 'link_text', 'partial_link_text'
296
+ :param expected_condition: The expected condition type.
297
+ Options: 'clickable'(Default), 'present', 'visible',
298
+ 'selected', 'frame_available'
299
+ :return: The value of the located WebElement.
300
+ :raises RuntimeError: If the WebDriver is not initialized.
301
+ """
302
+
303
+ return self.get_element(
304
+ element_value, locator, expected_condition
305
+ ).get_attribute("value")
306
+
307
+ def press_button(
308
+ self,
309
+ element_value: str,
310
+ locator: Optional[str] = None,
311
+ expected_condition: Optional[str] = None,
312
+ ) -> None:
313
+ """Click on the WebElement based on the locator and expected condition.
314
+
315
+ :param element_value: The value used to identify the element.
316
+ :param locator: The locator type.
317
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
318
+ 'xpath', 'link_text', 'partial_link_text'
319
+ :param expected_condition: The expected condition type.
320
+ Options: 'clickable'(Default), 'present', 'visible',
321
+ 'selected', 'frame_available'
322
+ :return:
323
+ :raises RuntimeError: If the WebDriver is not initialized.
324
+ """
325
+
326
+ element = self.get_element(element_value, locator, expected_condition)
327
+ element.click()
328
+
329
+ def enter_text(
330
+ self,
331
+ text: str,
332
+ element_value: str,
333
+ locator: Optional[str] = None,
334
+ expected_condition: Optional[str] = None,
335
+ ) -> None:
336
+ """Populate the text field with the provided text.
337
+
338
+ :param text: The text to enter.
339
+ :param element_value: The value used to identify the element.
340
+ :param locator: The locator type.
341
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
342
+ 'xpath', 'link_text', 'partial_link_text'
343
+ :param expected_condition: The expected condition type.
344
+ Options: 'clickable'(Default), 'present', 'visible',
345
+ 'selected', 'frame_available'
346
+ :return:
347
+ :raises RuntimeError: If the WebDriver is not initialized.
348
+ """
349
+
350
+ element = self.get_element(element_value, locator, expected_condition)
351
+ element.clear()
352
+ element.send_keys(text)
353
+
354
+ def set_checkbox_state(
355
+ self,
356
+ state: bool,
357
+ element_value: str,
358
+ locator: Optional[str] = None,
359
+ expected_condition: Optional[str] = None,
360
+ ) -> None:
361
+ """Set the state of a checkbox.
362
+
363
+ :param state: The state to set.
364
+ :param element_value: The value used to identify the element.
365
+ :param locator: The locator type.
366
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
367
+ 'xpath', 'link_text', 'partial_link_text'
368
+ :param expected_condition: The expected condition type.
369
+ Options: 'clickable'(Default), 'present', 'visible',
370
+ 'selected', 'frame_available'
371
+ :return:
372
+ :raises RuntimeError: If the WebDriver is not initialized.
373
+ """
374
+
375
+ element = self.get_element(element_value, locator, expected_condition)
376
+ if element.is_selected() != state:
377
+ element.click()
378
+
379
+ def set_select_option(
380
+ self,
381
+ option: str,
382
+ element_value: str,
383
+ select_type: str = None,
384
+ locator: Optional[str] = None,
385
+ expected_condition: Optional[str] = None,
386
+ ) -> None:
387
+ """Select an option from a dropdown.
388
+
389
+ :param option: The option to select. This can be the visible text,
390
+ :param element_value: The value used to identify the element.
391
+ :param select_type: The type of selection to perform.
392
+ :param locator: The locator type.
393
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
394
+ 'xpath', 'link_text', 'partial_link_text'
395
+ :param expected_condition: The expected condition type.
396
+ Options: 'clickable'(Default), 'present', 'visible',
397
+ 'selected', 'frame_available'
398
+ :return:
399
+ :raises RuntimeError: If the WebDriver is not initialized.
400
+ """
401
+
402
+ element = self.get_element(element_value, locator, expected_condition)
403
+ select = Select(element)
404
+ if select_type == "index":
405
+ select.select_by_index(int(option))
406
+ elif select_type == "visible_text":
407
+ select.select_by_visible_text(option)
408
+ else:
409
+ select.select_by_value(option)
410
+
411
+ def web_page_contains(
412
+ self,
413
+ element_value: str,
414
+ locator: Optional[str] = None,
415
+ expected_condition: Optional[str] = None,
416
+ wait_time: Optional[float] = 0,
417
+ ) -> Union[WebElement, bool]:
418
+ """
419
+ Determine whether a web element is present on the page based on
420
+ the provided locator and expected condition.
421
+
422
+ :param element_value: The value used to identify the element.
423
+ :param locator: The locator type.
424
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
425
+ 'xpath', 'link_text', 'partial_link_text'
426
+ :param expected_condition: The expected condition type.
427
+ Options: 'clickable'(Default), 'present', 'visible',
428
+ 'selected', 'frame_available'
429
+ :param wait_time: Time to wait for the condition.
430
+ :return: The located WebElement if found; otherwise, False if the element is not present
431
+ or an exception occurs.
432
+ :raises RuntimeError: If the WebDriver is not initialized.
433
+ """
434
+
435
+ try:
436
+ return self.get_element(
437
+ element_value, locator, expected_condition, wait_time
438
+ )
439
+ except (TimeoutException, NoSuchElementException):
440
+ return False
441
+
442
+ def text_is_present(
443
+ self,
444
+ text: str,
445
+ element_value: str,
446
+ locator: Optional[str] = None,
447
+ text_location: Optional[str] = None,
448
+ wait_time: Optional[float] = 0,
449
+ ) -> bool:
450
+ """
451
+ Checks whether the specified text is present within a web element.
452
+
453
+ :param text: The text to verify within the element.
454
+ :param element_value: The value used to identify the element.
455
+ :param locator: The locator type.
456
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
457
+ 'xpath', 'link_text', 'partial_link_text'
458
+ :param text_location: Where in the element to look for the text.
459
+ Options: 'anywhere'(Default), 'attribute', 'value'
460
+ :param wait_time: Time to wait for the condition.
461
+ :return: True if the text is found within the element, False otherwise.
462
+ :raises RuntimeError: If the WebDriver is not initialized.
463
+ """
464
+
465
+ expected_condition = EC.text_to_be_present_in_element
466
+ if text_location == "attribute":
467
+ expected_condition = EC.text_to_be_present_in_element_attribute
468
+ elif text_location == "value":
469
+ expected_condition = EC.text_to_be_present_in_element_value
470
+
471
+ return WebDriverWait(self.driver, self._get_wait_time(wait_time)).until(
472
+ expected_condition((self._get_locator(locator), element_value), text)
473
+ )
474
+
475
+ def wait_for_element(
476
+ self,
477
+ element_value: str,
478
+ locator: Optional[str] = None,
479
+ expected_condition: Optional[str] = None,
480
+ wait_time: Optional[float] = 0,
481
+ ) -> WebElement:
482
+ """
483
+ Wait for an element to be present based on the locator and expected condition.
484
+
485
+ :param element_value: The value used to identify the element.
486
+ :param locator: The locator type.
487
+ Options: 'css'(Default), 'id', 'name', 'class', 'tag',
488
+ 'xpath', 'link_text', 'partial_link_text'
489
+ :param expected_condition: The expected condition type.
490
+ Options: 'clickable'(Default), 'present', 'visible',
491
+ 'selected', 'frame_available'
492
+ :param wait_time: Time to wait for the element.
493
+ :return: The located WebElement.
494
+ :raises RuntimeError: If the WebDriver is not initialized.
495
+ """
496
+
497
+ return self.get_element(
498
+ element_value, locator, expected_condition, self._get_wait_time(wait_time)
499
+ )
500
+
501
+
502
+ class WEInteractions(Interactions):
503
+ """Class for interacting with web elements directly using WebElement instances."""
504
+
505
+ def _get_expected_condition_we(self, expected_condition: Optional[str] = None):
506
+ """
507
+ Return an expected condition that accepts a WebElement.
508
+
509
+ :param expected_condition: The expected condition type.
510
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
511
+ :return: A Selenium expected condition function.
512
+ :raises RuntimeError: If the WebDriver is not initialized.
513
+ """
514
+
515
+ match expected_condition:
516
+ case "visible":
517
+ expected_condition = EC.visibility_of
518
+ case "invisible":
519
+ expected_condition = EC.invisibility_of_element
520
+ case "selected":
521
+ expected_condition = EC.element_to_be_selected
522
+ case "staleness":
523
+ expected_condition = EC.staleness_of
524
+ case _:
525
+ expected_condition = EC.element_to_be_clickable
526
+ return expected_condition
527
+
528
+ def wait_for_element_we(
529
+ self,
530
+ web_element: WebElement,
531
+ expected_condition: Optional[str] = None,
532
+ wait_time: Optional[float] = 0,
533
+ ) -> WebElement:
534
+ """
535
+ Wait for an element to be present directly using WebElement and expected condition.
536
+
537
+ :param web_element: The WebElement to wait for.
538
+ :param expected_condition: The expected condition type.
539
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
540
+ :param wait_time: Time to wait for the element.
541
+ :return: The WebElement if it meets the expected condition.
542
+ :raises RuntimeError: If the WebDriver is not initialized.
543
+ """
544
+
545
+ condition = self._get_expected_condition_we(expected_condition)
546
+ WebDriverWait(self.driver, self._get_wait_time(wait_time)).until(
547
+ condition(web_element)
548
+ )
549
+ return web_element
550
+
551
+ def get_text_we(
552
+ self,
553
+ web_element: WebElement,
554
+ expected_condition: Optional[str] = None,
555
+ wait_time: Optional[float] = 0,
556
+ ) -> str:
557
+ """
558
+ Get the text of the WebElement directly.
559
+
560
+ :param web_element: The WebElement to get text from.
561
+ :param expected_condition: The expected condition type.
562
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
563
+ :param wait_time: Time to wait for the element.
564
+ :return: The text of the WebElement.
565
+ :raises RuntimeError: If the WebDriver is not initialized.
566
+ """
567
+
568
+ web_element = self.wait_for_element_we(
569
+ web_element, expected_condition, wait_time
570
+ )
571
+ return web_element.text
572
+
573
+ def get_table_we(
574
+ self,
575
+ web_element: WebElement,
576
+ expected_condition: Optional[str] = None,
577
+ wait_time: Optional[float] = 0,
578
+ ) -> pd.DataFrame:
579
+ """
580
+ Get the data from a table element directly.
581
+
582
+ :param web_element: The WebElement representing the table.
583
+ :param expected_condition: The expected condition type.
584
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
585
+ :param wait_time: Time to wait for the element.
586
+ :return: A DataFrame containing the table data.
587
+ :raises RuntimeError: If the WebDriver is not initialized.
588
+ """
589
+
590
+ web_element = self.wait_for_element_we(
591
+ web_element, expected_condition, wait_time
592
+ )
593
+ return pd.read_html(StringIO(web_element.get_attribute("outerHTML")))[0]
594
+
595
+ def get_value_we(
596
+ self,
597
+ web_element: WebElement,
598
+ expected_condition: Optional[str] = None,
599
+ wait_time: Optional[float] = 0,
600
+ ) -> str:
601
+ """
602
+ Get the value attribute of the WebElement directly.
603
+
604
+ :param web_element: The WebElement to get value from.
605
+ :param expected_condition: The expected condition type.
606
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
607
+ :param wait_time: Time to wait for the element.
608
+ :return: The value attribute of the WebElement.
609
+ :raises RuntimeError: If the WebDriver is not initialized.
610
+ """
611
+
612
+ web_element = self.wait_for_element_we(
613
+ web_element, expected_condition, wait_time
614
+ )
615
+ return web_element.get_attribute("value")
616
+
617
+ def press_button_we(
618
+ self,
619
+ web_element: WebElement,
620
+ expected_condition: Optional[str] = None,
621
+ wait_time: Optional[float] = 0,
622
+ ) -> None:
623
+ """Click on the WebElement directly.
624
+
625
+ :param web_element: The WebElement to click.
626
+ :param expected_condition: The expected condition type.
627
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
628
+ :param wait_time: Time to wait for the element.
629
+ :raises RuntimeError: If the WebDriver is not initialized.
630
+ """
631
+
632
+ web_element = self.wait_for_element_we(
633
+ web_element, expected_condition, wait_time
634
+ )
635
+ web_element.click()
636
+
637
+ def enter_text_we(
638
+ self,
639
+ text: str,
640
+ web_element: WebElement,
641
+ expected_condition: Optional[str] = None,
642
+ wait_time: Optional[float] = 0,
643
+ ) -> None:
644
+ """Populate the text field with the provided text directly.
645
+
646
+ :param text: The text to enter.
647
+ :param web_element: The WebElement to populate.
648
+ :param expected_condition: The expected condition type.
649
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
650
+ :param wait_time: Time to wait for the element.
651
+ :raises RuntimeError: If the WebDriver is not initialized.
652
+ """
653
+
654
+ web_element = self.wait_for_element_we(
655
+ web_element, expected_condition, wait_time
656
+ )
657
+ web_element.clear()
658
+ web_element.send_keys(text)
659
+
660
+ def set_checkbox_state_we(
661
+ self,
662
+ state: bool,
663
+ web_element: WebElement,
664
+ expected_condition: Optional[str] = None,
665
+ wait_time: Optional[float] = 0,
666
+ ) -> None:
667
+ """Set the state of a checkbox directly.
668
+
669
+ :param state: The state to set. True to check the checkbox, False to uncheck it.
670
+ :param web_element: The WebElement representing the checkbox.
671
+ :param expected_condition: The expected condition type.
672
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
673
+ :param wait_time: Time to wait for the element.
674
+ :raises RuntimeError: If the WebDriver is not initialized.
675
+ """
676
+
677
+ web_element = self.wait_for_element_we(
678
+ web_element, expected_condition, wait_time
679
+ )
680
+ if web_element.is_selected() != state:
681
+ web_element.click()
682
+
683
+ def set_select_option_we(
684
+ self,
685
+ option: str,
686
+ web_element: WebElement,
687
+ select_type: str = None,
688
+ expected_condition: Optional[str] = None,
689
+ wait_time: Optional[float] = 0,
690
+ ) -> None:
691
+ """Select an option from a dropdown directly.
692
+
693
+ :param option: The option to select.
694
+ This can be the visible text, index, or value of the option.
695
+ Default is by value.
696
+ :param web_element: The WebElement representing the dropdown.
697
+ :param select_type: The type of selection to perform.
698
+ Options: 'value' (default), 'index', 'visible_text'
699
+ :param expected_condition: The expected condition type.
700
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
701
+ :param wait_time: Time to wait for the element.
702
+ :raises RuntimeError: If the WebDriver is not initialized.
703
+ """
704
+
705
+ web_element = self.wait_for_element_we(
706
+ web_element, expected_condition, wait_time
707
+ )
708
+ select = Select(web_element)
709
+ if select_type == "index":
710
+ select.select_by_index(int(option))
711
+ elif select_type == "visible_text":
712
+ select.select_by_visible_text(option)
713
+ else:
714
+ select.select_by_value(option)
715
+
716
+ def web_page_contains_we(
717
+ self,
718
+ web_element: WebElement,
719
+ expected_condition: Optional[str] = None,
720
+ wait_time: Optional[float] = 0,
721
+ ) -> WebElement | bool:
722
+ """Check if the web page contains an element directly using WebElement
723
+ and expected condition.
724
+
725
+ :param web_element: The WebElement to check.
726
+ :param expected_condition: The expected condition type.
727
+ Options: 'clickable'(Default), 'visible', 'selected', 'staleness'
728
+ :param wait_time: Time to wait for the condition.
729
+ :return: The located WebElement if found; otherwise, False if the element is not present
730
+ or an exception occurs.
731
+ :raises RuntimeError: If the WebDriver is not initialized.
732
+ """
733
+
734
+ try:
735
+ return self.wait_for_element_we(
736
+ web_element, expected_condition, self._get_wait_time(wait_time)
737
+ )
738
+ except (TimeoutException, NoSuchElementException):
739
+ return False
wcp_library/emailing.py CHANGED
@@ -32,6 +32,31 @@ def send_email(sender: str, recipients: list, subject: str, message: str=None) -
32
32
  server.quit()
33
33
 
34
34
 
35
+ def send_html_email(sender: str, recipients: list, subject: str, html_content: str) -> None:
36
+ """
37
+ Function to send an HTML email
38
+
39
+ :param sender:
40
+ :param recipients:
41
+ :param subject:
42
+ :param html_content:
43
+ :return:
44
+ """
45
+
46
+ msg = MIMEMultipart('alternative')
47
+ msg['From'] = sender
48
+ msg['To'] = ", ".join(recipients)
49
+ msg['Date'] = formatdate(localtime=True)
50
+ msg['Subject'] = subject
51
+ msg.attach(MIMEText(html_content, 'html'))
52
+
53
+ smtpServer = 'mail.wcap.ca'
54
+ server = smtplib.SMTP(smtpServer, 25)
55
+ server.ehlo()
56
+ server.sendmail(sender, recipients, msg.as_string())
57
+ server.quit()
58
+
59
+
35
60
  def email_reporting(subject: str, message: str) -> None:
36
61
  """
37
62
  Function to email the reporting team from the Python email
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wcp-library
3
- Version: 1.6.3
3
+ Version: 1.6.5
4
4
  Summary: Common utilites for internal development at WCP
5
5
  Author: Mitch-Petersen
6
6
  Author-email: mitch.petersen@wcap.ca
@@ -1,4 +1,7 @@
1
- wcp_library/__init__.py,sha256=bdNwVRQugYLeKlJFj3kE0U1LvqlK1O0k8CjF-_xqrEE,625
1
+ wcp_library/__init__.py,sha256=kfv5jNFXKn_iHjGUOp_UDtqAFiogpVRcn4Tpmkho52o,4242
2
+ wcp_library/browser_automation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ wcp_library/browser_automation/browser.py,sha256=06xKu_lLifRzfj1TBlpZmKUeBoMqa7ye8kj0Aoy27uI,14551
4
+ wcp_library/browser_automation/interactions.py,sha256=YTYyII81M9qoVU_iYXoF5vHCc1PSj60hStPU-nT-qyA,28941
2
5
  wcp_library/credentials/__init__.py,sha256=cfClJuFs9E3o_-5IPSKVJrkhQTiC3S2HhnZdrab9L6U,1856
3
6
  wcp_library/credentials/_credential_manager_asynchronous.py,sha256=A7OJkBsNJ5fzNKoMih8GyLCIQytLViQgwccOrtdBOL8,6392
4
7
  wcp_library/credentials/_credential_manager_synchronous.py,sha256=QUF3OOUgx0gu2PKFykkB69C-Kf_-9lyo2RIVA2JYvyc,5764
@@ -7,7 +10,7 @@ wcp_library/credentials/ftp.py,sha256=lSjOlfU68j9PsOCaoWJSdjbR1QmjTxjVmSxOpMdMG7
7
10
  wcp_library/credentials/internet.py,sha256=z9tihFddTJcX2pt-iHFP_rkCoheLhTdFgn4ksc2sYYE,2239
8
11
  wcp_library/credentials/oracle.py,sha256=OV02op95LNUT21cNDM4zDbD63C1ldiq--vYofDpU-zU,2869
9
12
  wcp_library/credentials/postgres.py,sha256=B5qNPKYzuzOJZ5S4M3MCN4xUelvZ6AgyR6QJc4z0Cgo,2573
10
- wcp_library/emailing.py,sha256=xqNly6Tmj-pvwl5bdys3gauZFDd4SuWCQYzGFNemv2Q,2496
13
+ wcp_library/emailing.py,sha256=g6g9ZHIMBq6fzUFEjhB54WnJOj0cZFVbjLsB29zAVlY,3135
11
14
  wcp_library/ftp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
15
  wcp_library/ftp/ftp.py,sha256=Zg8WMYfzUGnpqimLjtCuKyrtNGtv0Syupgi-rh_al00,3985
13
16
  wcp_library/ftp/sftp.py,sha256=hykXGLGdxe7DYAxFdTwjPjTEOYuIpSMyK3NOiTQNUK0,4176
@@ -20,6 +23,6 @@ wcp_library/sql/__init__.py,sha256=f0JyVyFyUjueQ1XdVmCgmRjzkFENmFKl3BOzrvv3i1I,2
20
23
  wcp_library/sql/oracle.py,sha256=GpqUn7srm4mGxeJAU06tHaMR37zxASSdMZJ3h8KTC-U,19240
21
24
  wcp_library/sql/postgres.py,sha256=cLOurfmad3WqJcE7FiSxUyzo_0patpfMcTfQAoiteuU,18525
22
25
  wcp_library/time.py,sha256=ktSzhK7SZnGPNXobFexnhFGQAcriLCJQKxnO0fed8fQ,1740
23
- wcp_library-1.6.3.dist-info/METADATA,sha256=gxtA32HYQk2IqRVKtnTLOQS3WDJvxP_5-HWNCVg2whw,1393
24
- wcp_library-1.6.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
25
- wcp_library-1.6.3.dist-info/RECORD,,
26
+ wcp_library-1.6.5.dist-info/METADATA,sha256=CzgY0bCG4iFsZJhG4Y6Ff2mIaEgJX8QXmBqFqGtO988,1393
27
+ wcp_library-1.6.5.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
+ wcp_library-1.6.5.dist-info/RECORD,,