Skip to content

Commit 1d64dd9

Browse files
author
dmy.berezovskyi
committed
scripts optimization
1 parent ebbcdce commit 1d64dd9

File tree

8 files changed

+220
-180
lines changed

8 files changed

+220
-180
lines changed

core_driver/driver.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
log = Logger(log_lvl=LogLevel.INFO).get_instance()
1414

1515

16-
def _get_driver_path(driver_type=None):
17-
if driver_type is None:
16+
def _get_project_dir():
17+
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
18+
19+
20+
def _get_driver_path(driver_type):
21+
if not driver_type:
1822
ErrorHandler.raise_error(ErrorType.UNSUPPORTED_DRIVER_TYPE)
1923

20-
project_dir = os.path.dirname(
21-
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22-
)
23-
driver_path = os.path.join(project_dir, "resources", driver_type)
24+
driver_path = os.path.join(_get_project_dir(), "resources", driver_type)
2425

2526
if not os.path.exists(driver_path):
2627
ErrorHandler.raise_error(ErrorType.DRIVER_NOT_FOUND, driver_type)
@@ -43,30 +44,31 @@ def create_driver(self, environment, dr_type):
4344
pass
4445

4546
def get_desired_caps(self, browser="chrome"):
46-
caps = YAMLReader.read_caps(browser)
47-
log.info(f"Capabilities for driver {caps}")
48-
return caps
47+
try:
48+
caps = YAMLReader.read_caps(browser)
49+
log.info(f"Capabilities for {browser} driver: {caps}")
50+
return caps
51+
except Exception as e:
52+
log.error(f"Error reading capabilities for {browser}: {e}")
53+
ErrorHandler.raise_error(ErrorType.CAPABILITY_NOT_FOUND, browser)
4954

5055

5156
class LocalDriver(Driver):
5257
def create_driver(self, environment=None, dr_type="chromedriver"):
53-
"""Tries to use ChromeDriverManager to install the latest driver,
54-
and if it fails, it falls back to a locally stored driver in resources."""
5558
driver = None
59+
options = _init_driver_options(dr_type=dr_type)
5660
try:
5761
driver_path = ChromeDriverManager().install()
58-
options = _init_driver_options(dr_type=dr_type)
5962
driver = webdriver.Chrome(
60-
service=ChromeService(executable_path=driver_path), options=options
61-
)
62-
log.info(
63-
f"Created local Chrome driver with session: {driver.session_id}"
63+
service=ChromeService(executable_path=driver_path),
64+
options=options
6465
)
66+
log.info(f"Local Chrome driver created with session: {driver.session_id}")
6567
except Exception as e:
66-
log.error(f"Failed to create Chrome driver {e}")
68+
log.error(f"ChromeDriverManager failed, falling back to local driver: {e}")
6769
driver = webdriver.Chrome(
6870
service=ChromeService(_get_driver_path(dr_type)),
69-
options=_init_driver_options(dr_type=dr_type),
71+
options=options
7072
)
7173
_configure_driver(driver, environment)
7274
return driver
@@ -84,17 +86,17 @@ def create_driver(self, environment=None, dr_type=None):
8486

8587

8688
class FirefoxDriver(Driver):
87-
def create_driver(self, environment=None, dr_type=None):
89+
def create_driver(self, environment=None, dr_type="firefox"):
90+
driver = None
91+
options = _init_driver_options(dr_type=dr_type)
8892
try:
89-
driver = webdriver.Firefox(options=_init_driver_options(dr_type=dr_type))
90-
log.info(f"Created Firefox driver with session: {driver.session_id}")
93+
driver = webdriver.Firefox(options=options)
94+
log.info(f"Firefox driver created with session: {driver.session_id}")
9195
except Exception as e:
96+
log.error(f"Failed to create Firefox driver, falling back to Chrome: {e}")
9297
driver = webdriver.Chrome(
93-
service=ChromeService(_get_driver_path(dr_type)),
94-
options=_init_driver_options(dr_type=dr_type),
95-
)
96-
log.error(
97-
f"Failed to create Firefox driver, falling back to Chrome: {e}"
98+
service=ChromeService(_get_driver_path("chromedriver")),
99+
options=options
98100
)
99101
_configure_driver(driver, environment)
100102
return driver

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ authors = ["Dmytro Berezovskyi"]
66
readme = "README.md"
77
packages = [
88
{ include = "utils" },
9-
{ include = "scraper" },
10-
{ include = "core_driver" }
9+
{ include = "core_driver" },
10+
{ include = "scraper", from = "utils" },
11+
{ include = "cli.py", from="utils"},
1112
]
1213
[tool.poetry.dependencies]
1314
python = "^3.12"
@@ -26,7 +27,7 @@ setuptools="70.0.0"
2627
ruff="0.6.8"
2728
secure-test-automation="^1.3.1"
2829
colorama="==0.4.6"
29-
typer="==0.15.1"
30+
click="==8.1.8"
3031

3132

3233
[tool.pytest.ini_options]
@@ -61,3 +62,5 @@ filterwarnings = [
6162

6263
[tool.pytest.config]
6364
type = ["local", "firefox", "remote"] # Define a custom command line option for driver types as a list
65+
[tool.poetry.scripts]
66+
psaf = "utils.cli:main"

utils/cli.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import argparse
2+
from pathlib import Path
3+
4+
from utils.scraper.chrome_scraper import ChromePageScraper
5+
6+
7+
def main():
8+
parser = argparse.ArgumentParser(prog="psaf", description="PSAF CLI Tool")
9+
10+
# Subcommand for 'get' operations
11+
subparsers = parser.add_subparsers(dest="command")
12+
13+
# Subcommand for 'get chromedriver'
14+
get_parser = subparsers.add_parser("get", help="Get a specific resource")
15+
get_subparsers = get_parser.add_subparsers(dest="resource")
16+
17+
# Subcommand for 'chromedriver' under 'get'
18+
chromedriver_parser = get_subparsers.add_parser(
19+
"chromedriver", help="Download the ChromeDriver"
20+
)
21+
chromedriver_parser.add_argument(
22+
"--platform",
23+
type=str,
24+
help="Specify the platform (e.g., 'win32', 'linux64').",
25+
)
26+
chromedriver_parser.add_argument(
27+
"--version",
28+
type=str,
29+
help="Specify the version of ChromeDriver.",
30+
)
31+
chromedriver_parser.add_argument(
32+
"--milestone",
33+
type=str,
34+
help="Specify the milestone of ChromeDriver.",
35+
)
36+
chromedriver_parser.add_argument(
37+
"--output-dir",
38+
type=str,
39+
help="Directory to save the downloaded ChromeDriver.",
40+
)
41+
chromedriver_parser.add_argument(
42+
"--extract",
43+
action="store_true",
44+
help="Extract the downloaded ChromeDriver.",
45+
)
46+
47+
args = parser.parse_args()
48+
49+
if args.command == "get" and args.resource == "chromedriver":
50+
output_dir = Path(args.output_dir) if args.output_dir else None
51+
ChromePageScraper.get_chromedriver(
52+
platform=args.platform,
53+
version=args.version,
54+
milestone=args.milestone,
55+
d_dir=output_dir,
56+
is_extracted=args.extract,
57+
)
58+
59+
60+
if __name__ == "__main__":
61+
main()

utils/cli/cli.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

utils/error_handler.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class ErrorType(Enum):
66
EMPTY_URL_ERROR = 2
77
UNSUPPORTED_DRIVER_TYPE = 3
88
DRIVER_NOT_FOUND = 4
9+
CAPABILITY_NOT_FOUND = 5
910

1011

1112
class ErrorHandler:
@@ -14,6 +15,7 @@ class ErrorHandler:
1415
ErrorType.EMPTY_URL_ERROR: "Environment variable is empty or not found",
1516
ErrorType.UNSUPPORTED_DRIVER_TYPE: "Unsupported driver type",
1617
ErrorType.DRIVER_NOT_FOUND: "WebDriver binary not found at ",
18+
ErrorType.CAPABILITY_NOT_FOUND: "Capabilities file not found"
1719
}
1820

1921
@staticmethod

utils/logger.py

Lines changed: 23 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
from enum import Enum
55
from typing import Optional, Callable, Any, Literal
6+
from threading import Lock
67

78

89
class LogLevel(Enum):
@@ -14,23 +15,23 @@ class LogLevel(Enum):
1415

1516
class Singleton(type):
1617
_instances = {}
18+
_lock = Lock() # Ensure thread safety
1719

1820
def __call__(cls, *args, **kwargs) -> Any:
19-
if cls not in cls._instances:
20-
cls._instances[cls] = super().__call__(*args, **kwargs)
21+
with cls._lock:
22+
if cls not in cls._instances:
23+
cls._instances[cls] = super().__call__(*args, **kwargs)
2124
return cls._instances[cls]
2225

2326

2427
class Logger(metaclass=Singleton):
25-
def __init__(self,
26-
log_lvl: LogLevel = LogLevel.INFO,
27-
log_target: Literal["console", "file", "both"] = "console") -> None:
28-
"""Logger
29-
:param log_lvl: LogLevel
30-
:param log_target: Target for logs (store to file, display to console or both(file and console))
31-
"""
28+
def __init__(
29+
self,
30+
log_lvl: LogLevel = LogLevel.INFO,
31+
log_target: Literal["console", "file", "both"] = "console",
32+
) -> None:
3233
self._log = logging.getLogger("selenium")
33-
self._log.setLevel(LogLevel.INFO.value)
34+
self._log.setLevel(log_lvl.value)
3435
self.log_file = self._create_log_file()
3536
self.log_target = log_target
3637
self._initialize_logging(log_lvl)
@@ -42,9 +43,7 @@ def _create_log_file(self) -> str:
4243
)
4344

4445
try:
45-
os.makedirs(
46-
log_directory, exist_ok=True
47-
) # Create directory if it doesn't exist
46+
os.makedirs(log_directory, exist_ok=True)
4847
except Exception as e:
4948
raise RuntimeError(
5049
f"Failed to create log directory '{log_directory}': {e}"
@@ -56,14 +55,12 @@ def _initialize_logging(self, log_lvl: LogLevel) -> None:
5655
formatter = logging.Formatter(
5756
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
5857
)
59-
# Lot to file or both file and console
6058
if self.log_target in ("file", "both"):
61-
fh = logging.FileHandler(self.log_file, mode="w")
59+
fh = logging.FileHandler(self.log_file, mode="a")
6260
fh.setFormatter(formatter)
6361
fh.setLevel(log_lvl.value)
6462
self._log.addHandler(fh)
6563

66-
# Log to console or both file and console
6764
if self.log_target in ("console", "both"):
6865
ch = logging.StreamHandler()
6966
ch.setFormatter(formatter)
@@ -73,61 +70,30 @@ def _initialize_logging(self, log_lvl: LogLevel) -> None:
7370
def get_instance(self) -> logging.Logger:
7471
return self._log
7572

76-
def annotate(
77-
self, message: str, level: Literal["info", "warn", "debug", "error"]
78-
) -> None:
79-
"""Log a message at the specified level."""
80-
if level == "info":
73+
def annotate(self, message: str, level: LogLevel) -> None:
74+
if level == LogLevel.INFO:
8175
self._log.info(message)
82-
elif level == "warn":
76+
elif level == LogLevel.WARNING:
8377
self._log.warning(message)
84-
elif level == "debug":
78+
elif level == LogLevel.DEBUG:
8579
self._log.debug(message)
86-
elif level == "error":
80+
elif level == LogLevel.ERROR:
8781
self._log.error(message)
8882
else:
8983
raise ValueError(f"Invalid log level: {level}")
9084

9185

92-
def log(
93-
data: Optional[str] = None,
94-
level: Literal["info", "warn", "debug", "error"] = "info",
95-
) -> Callable:
96-
"""Decorator to log the current method's execution.
97-
98-
:param data: Custom log message to use if no docstring is provided.
99-
:param level: Level of the logs, e.g., info, warn, debug, error.
100-
"""
86+
def log(data: Optional[str] = None, level: LogLevel = LogLevel.INFO) -> Callable:
10187
logger_instance = Logger() # Get the singleton instance of Logger
10288

10389
def decorator(func: Callable) -> Callable:
10490
def wrapper(self, *args, **kwargs) -> Any:
105-
# Get the method's docstring
10691
method_docs = format_method_doc_str(func.__doc__)
107-
108-
if method_docs is None and data is None:
109-
raise ValueError(
110-
f"No documentation available for :: {func.__name__}"
111-
)
112-
113-
# Construct the parameter string for logging
114-
params_str = ", ".join(repr(arg) for arg in args)
115-
kwargs_str = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
116-
all_params_str = ", ".join(filter(None, [params_str, kwargs_str]))
117-
118-
logs = (
119-
method_docs
120-
+ f" Method :: {func.__name__}()"
121-
+ f" with parameters: {all_params_str}"
122-
if method_docs
123-
else data
124-
+ f" Method :: {func.__name__}()"
125-
+ f" with parameters: {all_params_str}"
92+
log_message = (
93+
(method_docs or data or "")
94+
+ f" Method :: {func.__name__}() with parameters: {', '.join(map(str, args))}"
12695
)
127-
128-
logger_instance.annotate(logs, level)
129-
130-
# Call the original method, passing *args and **kwargs
96+
logger_instance.annotate(log_message, level)
13197
return func(self, *args, **kwargs)
13298

13399
return wrapper
@@ -136,7 +102,6 @@ def wrapper(self, *args, **kwargs) -> Any:
136102

137103

138104
def format_method_doc_str(doc_str: Optional[str]) -> Optional[str]:
139-
"""Add a dot to the docs string if it doesn't exist."""
140105
if doc_str and not doc_str.endswith("."):
141106
return doc_str + "."
142107
return doc_str

0 commit comments

Comments
 (0)