cucu 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cucu might be problematic. Click here for more details.

Files changed (83) hide show
  1. cucu/__init__.py +38 -0
  2. cucu/ansi_parser.py +58 -0
  3. cucu/behave_tweaks.py +196 -0
  4. cucu/browser/__init__.py +0 -0
  5. cucu/browser/core.py +80 -0
  6. cucu/browser/frames.py +106 -0
  7. cucu/browser/selenium.py +323 -0
  8. cucu/browser/selenium_tweaks.py +27 -0
  9. cucu/cli/__init__.py +3 -0
  10. cucu/cli/core.py +788 -0
  11. cucu/cli/run.py +207 -0
  12. cucu/cli/steps.py +137 -0
  13. cucu/cli/thread_dumper.py +55 -0
  14. cucu/config.py +440 -0
  15. cucu/edgedriver_autoinstaller/README.md +1 -0
  16. cucu/edgedriver_autoinstaller/__init__.py +37 -0
  17. cucu/edgedriver_autoinstaller/utils.py +231 -0
  18. cucu/environment.py +283 -0
  19. cucu/external/jquery/jquery-3.5.1.min.js +2 -0
  20. cucu/formatter/__init__.py +0 -0
  21. cucu/formatter/cucu.py +261 -0
  22. cucu/formatter/json.py +321 -0
  23. cucu/formatter/junit.py +289 -0
  24. cucu/fuzzy/__init__.py +3 -0
  25. cucu/fuzzy/core.py +107 -0
  26. cucu/fuzzy/fuzzy.js +253 -0
  27. cucu/helpers.py +875 -0
  28. cucu/hooks.py +205 -0
  29. cucu/language_server/__init__.py +3 -0
  30. cucu/language_server/core.py +114 -0
  31. cucu/lint/__init__.py +0 -0
  32. cucu/lint/linter.py +397 -0
  33. cucu/lint/rules/format.yaml +125 -0
  34. cucu/logger.py +113 -0
  35. cucu/matcher/__init__.py +0 -0
  36. cucu/matcher/core.py +30 -0
  37. cucu/page_checks.py +63 -0
  38. cucu/reporter/__init__.py +3 -0
  39. cucu/reporter/external/bootstrap.min.css +7 -0
  40. cucu/reporter/external/bootstrap.min.js +7 -0
  41. cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
  42. cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
  43. cucu/reporter/external/jquery-3.5.1.min.js +2 -0
  44. cucu/reporter/external/jquery.dataTables.min.js +192 -0
  45. cucu/reporter/external/popper.min.js +5 -0
  46. cucu/reporter/favicon.png +0 -0
  47. cucu/reporter/html.py +452 -0
  48. cucu/reporter/templates/feature.html +72 -0
  49. cucu/reporter/templates/flat.html +48 -0
  50. cucu/reporter/templates/index.html +49 -0
  51. cucu/reporter/templates/layout.html +109 -0
  52. cucu/reporter/templates/scenario.html +200 -0
  53. cucu/steps/__init__.py +27 -0
  54. cucu/steps/base_steps.py +88 -0
  55. cucu/steps/browser_steps.py +337 -0
  56. cucu/steps/button_steps.py +91 -0
  57. cucu/steps/checkbox_steps.py +111 -0
  58. cucu/steps/command_steps.py +181 -0
  59. cucu/steps/comment_steps.py +17 -0
  60. cucu/steps/draggable_steps.py +168 -0
  61. cucu/steps/dropdown_steps.py +467 -0
  62. cucu/steps/file_input_steps.py +80 -0
  63. cucu/steps/filesystem_steps.py +144 -0
  64. cucu/steps/flow_control_steps.py +198 -0
  65. cucu/steps/image_steps.py +37 -0
  66. cucu/steps/input_steps.py +301 -0
  67. cucu/steps/link_steps.py +63 -0
  68. cucu/steps/menuitem_steps.py +39 -0
  69. cucu/steps/platform_steps.py +29 -0
  70. cucu/steps/radio_steps.py +187 -0
  71. cucu/steps/step_utils.py +55 -0
  72. cucu/steps/tab_steps.py +68 -0
  73. cucu/steps/table_steps.py +437 -0
  74. cucu/steps/tables.js +28 -0
  75. cucu/steps/text_steps.py +78 -0
  76. cucu/steps/variable_steps.py +100 -0
  77. cucu/steps/webserver_steps.py +40 -0
  78. cucu/utils.py +269 -0
  79. cucu-1.0.0.dist-info/METADATA +424 -0
  80. cucu-1.0.0.dist-info/RECORD +83 -0
  81. cucu-1.0.0.dist-info/WHEEL +4 -0
  82. cucu-1.0.0.dist-info/entry_points.txt +2 -0
  83. cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
cucu/config.py ADDED
@@ -0,0 +1,440 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import socket
5
+
6
+ import yaml
7
+
8
+
9
+ class Config(dict):
10
+ # only match variables {...}
11
+ __VARIABLE_REGEX = re.compile(r"\{(?<!\\{)([^{}]+)\}(?<!\\})")
12
+
13
+ def __init__(self, **kwargs):
14
+ self.update(**kwargs)
15
+ self.resolving = False
16
+ self.defined_variables = {}
17
+ self.variable_lookups = {}
18
+
19
+ def define(self, name, description, default=None):
20
+ """
21
+ used to define variables and set their default so that we can then
22
+ provide the info using the `cucu vars` command for test writers to know
23
+ where and how to use various built-in variables.
24
+
25
+ parameters:
26
+ name(string): the name of the variable
27
+ description(string): a succinct description of variables purpose
28
+ default(string): an optional default to set the variable to
29
+ """
30
+ self.defined_variables[name] = {
31
+ "description": description,
32
+ "default": default,
33
+ }
34
+ # set the default
35
+ self.__setitem__(name, default)
36
+
37
+ def escape(self, string):
38
+ """
39
+ utility method used to escape strings that would be otherwise problematic
40
+ if the values were passed around as is in cucu steps.
41
+ """
42
+ if isinstance(string, str):
43
+ string = string.replace("{", "\\{")
44
+ string = string.replace("}", "\\}")
45
+
46
+ return string
47
+
48
+ def __getitem__(self, key):
49
+ try:
50
+ # environment always takes precedence
51
+ value = os.environ.get(key)
52
+
53
+ if value is None:
54
+ value = dict.__getitem__(self, key)
55
+
56
+ return value
57
+ except KeyError:
58
+ return None
59
+
60
+ def __setitem__(self, key, val):
61
+ dict.__setitem__(self, key, val)
62
+
63
+ def get(self, key, default=None):
64
+ try:
65
+ for regex, lookup in self.variable_lookups.items():
66
+ if regex.match(key):
67
+ return lookup(key)
68
+
69
+ return self[key]
70
+ except KeyError:
71
+ return default
72
+
73
+ def update(self, **kwargs):
74
+ for k, v in kwargs.items():
75
+ self[k] = v
76
+
77
+ def bool(self, key):
78
+ return self[key] in [True, "True", "true", "yes", "enabled"]
79
+
80
+ def true(self, key):
81
+ return self.bool(key)
82
+
83
+ def false(self, key):
84
+ return not self.true(key)
85
+
86
+ def load(self, filepath):
87
+ """
88
+ loads configuration values from a YAML file at the filepath provided
89
+ """
90
+ config = yaml.safe_load(open(filepath, "rb"))
91
+
92
+ if config:
93
+ for key in config.keys():
94
+ if key == "CUCU_SECRETS": # security: only add restrictions
95
+ vals = self.get(key, "").split(",")
96
+ vals = vals + config[key].split(",")
97
+ self[key] = ",".join(set(vals) ^ {""})
98
+ else:
99
+ self[key] = config[key]
100
+
101
+ def load_cucurc_files(self, filepath):
102
+ """
103
+ load in order the ~/.cucurc.yml and then subsequent config files
104
+ starting from the current working directory to the filepath provided
105
+ """
106
+
107
+ # load the ~/.cucurc.yml first
108
+ home_cucurc_filepath = os.path.join(
109
+ os.path.expanduser("~"), ".cucurc.yml"
110
+ )
111
+ if os.path.exists(home_cucurc_filepath):
112
+ self.load(home_cucurc_filepath)
113
+
114
+ filepath = os.path.abspath(filepath)
115
+ if os.path.isfile(filepath):
116
+ basename = os.path.dirname(filepath)
117
+ else:
118
+ basename = filepath
119
+
120
+ # create the inverse list of the directories starting from cwd to the one
121
+ # containing the feature file we want to run to load the cucrc.yml files in
122
+ # the correct order
123
+ dirnames = [os.getcwd()]
124
+ while basename != os.getcwd():
125
+ dirnames.append(basename)
126
+ basename = os.path.dirname(basename)
127
+
128
+ for dirname in dirnames:
129
+ cucurc_filepath = os.path.join(dirname, "cucurc.yml")
130
+ if os.path.exists(cucurc_filepath):
131
+ self.load(cucurc_filepath)
132
+
133
+ def expand(self, string):
134
+ """
135
+ find any variable references and return an dictionary of all of the
136
+ variable names and values within the string provided.
137
+
138
+ returns:
139
+ a dictionary of all of the variable names found in the string
140
+ provided with the exact value of the variable at runtime.
141
+ """
142
+ references = {}
143
+
144
+ variables = re.findall("{([^{}]+)}", string)
145
+
146
+ for variable in variables:
147
+ value = self.resolve(f"{{{variable}}}")
148
+
149
+ # if it didn't resolve to anything then
150
+ if value:
151
+ value = str(value).replace("\n", "\\n")
152
+ value = value[:80] + "..." * (len(value) > 80)
153
+ else:
154
+ value = None
155
+
156
+ references[variable] = value
157
+
158
+ return references
159
+
160
+ def hide_secrets(self, text: str | bytes):
161
+ secret_keys = [x for x in self.get("CUCU_SECRETS", "").split(",") if x]
162
+ secret_values = [self.get(x) for x in secret_keys if self.get(x)]
163
+ secret_values = [x for x in secret_values if isinstance(x, str)]
164
+
165
+ is_bytes = isinstance(text, bytes)
166
+ if is_bytes:
167
+ text = text.decode()
168
+
169
+ result = None
170
+ if text.startswith("{"):
171
+ try:
172
+ result = self._hide_secrets_json(secret_values, text)
173
+ except Exception as e:
174
+ print(
175
+ f"Couldn't parse json, falling back to text processing: {e}"
176
+ )
177
+
178
+ if result is None:
179
+ result = self._hide_secrets_text(secret_values, text)
180
+
181
+ if is_bytes:
182
+ result = result.encode()
183
+
184
+ return result
185
+
186
+ def _hide_secrets_json(self, secret_values, text):
187
+ data = json.loads(text)
188
+
189
+ def hide_node(value, parent, key):
190
+ if not isinstance(value, str):
191
+ return value
192
+
193
+ if (
194
+ key == "name"
195
+ and isinstance(parent, dict)
196
+ and parent.get("keyword", "") in ["Feature", "Scenario"]
197
+ ):
198
+ return value
199
+
200
+ return self._hide_secrets_text(secret_values, value)
201
+
202
+ leaf_map(data, hide_node)
203
+ return json.dumps(data, indent=2, sort_keys=True)
204
+
205
+ def _hide_secrets_text(self, secret_values, text):
206
+ lines = text.split("\n")
207
+
208
+ for x in range(len(lines)):
209
+ # here's where we can hide secrets
210
+ for value in secret_values:
211
+ lines[x] = lines[x].replace(value, "*" * len(value))
212
+
213
+ return "\n".join(lines)
214
+
215
+ def resolve(self, string):
216
+ """
217
+ resolve any variable references {...} in the string provided using
218
+ the values currently stored in the config object. We also unescape any
219
+ characters preceding by a backslash as to allow for test writers to
220
+ escape special characters
221
+ """
222
+ if isinstance(string, str):
223
+ previouses = [None]
224
+ while previouses[-1] != string:
225
+ # if any of the previous iterations of replacing variables looks
226
+ # like the exact string we have now then break out of the loop
227
+ # as there's an infinite recursion of variable replacing values
228
+ if string in previouses:
229
+ raise RuntimeError("infinite replacement loop detected")
230
+
231
+ previouses.append(string)
232
+
233
+ for var_name in Config.__VARIABLE_REGEX.findall(string):
234
+ value = self.get(var_name)
235
+
236
+ if value is None:
237
+ value = ""
238
+ # print directly to the output stream, which was taken over in behave_tweaks
239
+ print(f'WARNING variable "{var_name}" is undefined')
240
+
241
+ string = string.replace("{" + var_name + "}", str(value))
242
+
243
+ # we are only going to allow escaping of { and " characters for the
244
+ # time being as they're part of the language:
245
+ #
246
+ # * " are used around step arguments
247
+ # * {} are used by variable references
248
+ #
249
+ # regex below uses negative lookbehind to assert we're not replacing
250
+ # any escapes that have the backslash itself escaped.
251
+ #
252
+ string = re.sub(r"(?<!\\)\\{", "{", string)
253
+ string = re.sub(r"(?<!\\)\\}", "}", string)
254
+ string = re.sub(r'(?<!\\)\\"', '"', string)
255
+
256
+ return string
257
+
258
+ def snapshot(self):
259
+ """
260
+ make a shallow copy of the current config values which can later be
261
+ restored using the `restore` method.
262
+ """
263
+ self.snapshot_data = self.copy()
264
+
265
+ def restore(self):
266
+ """
267
+ restore a previous `snapshot`
268
+ """
269
+ self.clear()
270
+ self.update(**self.snapshot_data)
271
+
272
+ def register_custom_variable_handling(self, regex, lookup):
273
+ """
274
+ register a regex to match variable names on and allow the lookup
275
+ function provided to do the handling of the resolution of the variable
276
+ name.
277
+
278
+ parameters:
279
+ regex(string): regular expression to match on any config variable
280
+ name when doing lookups
281
+ lookup(func): a function that accepts a variable name to return its
282
+ value at runtime.
283
+
284
+ def lookup(name):
285
+ return [value of the variable at runtime]
286
+ """
287
+ self.variable_lookups[re.compile(regex)] = lookup
288
+
289
+ def to_yaml_without_secrets(self):
290
+ omit_keys = [
291
+ "CWD",
292
+ "STDOUT",
293
+ "STDERR",
294
+ "SCENARIO_DOWNLOADS_DIR",
295
+ "SCENARIO_LOGS_DIR",
296
+ "SCENARIO_RESULTS_DIR",
297
+ ]
298
+ secret_keys = [x for x in self.get("CUCU_SECRETS", "").split(",") if x]
299
+ keys = [
300
+ k
301
+ for k in self.keys()
302
+ if not k.startswith("__")
303
+ and k not in secret_keys
304
+ and k not in omit_keys
305
+ ]
306
+
307
+ config = {k: self[k] for k in sorted(keys)}
308
+ for k, v in config.items():
309
+ if isinstance(v, str):
310
+ v = self.hide_secrets(v)
311
+ # fix leading '*' issue
312
+ if v.startswith("*"):
313
+ v = f"'{v}'"
314
+ config[k] = v
315
+
316
+ return yaml.dump(config)
317
+
318
+
319
+ # global config object
320
+ CONFIG = Config()
321
+
322
+
323
+ def _get_local_address():
324
+ """
325
+ internal method to get the current host address
326
+ """
327
+ google_dns_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
328
+ google_dns_socket.connect(("8.8.8.8", 80))
329
+ return google_dns_socket.getsockname()[0]
330
+
331
+
332
+ CONFIG.define(
333
+ "HOST_ADDRESS",
334
+ "host address of the current machine cucu is running on",
335
+ default=_get_local_address(),
336
+ )
337
+ CONFIG.define(
338
+ "CWD",
339
+ "the current working directory of the cucu process",
340
+ default=os.getcwd(),
341
+ )
342
+ CONFIG.define(
343
+ "CUCU_SECRETS",
344
+ "a comma separated list of VARIABLE names that we want to hide "
345
+ "their values in the various outputs of cucu by replacing with asterisks",
346
+ default="",
347
+ )
348
+ CONFIG.define(
349
+ "CUCU_SHORT_UI_RETRY_AFTER_S",
350
+ "the amount of time to wait between retries in seconds for non-wait ui steps",
351
+ default=0.25,
352
+ )
353
+ CONFIG.define(
354
+ "CUCU_SHORT_UI_WAIT_TIMEOUT_S",
355
+ "the total amount of wait time in seconds for non-wait ui steps",
356
+ default=2.25,
357
+ )
358
+ CONFIG.define(
359
+ "CUCU_STEP_WAIT_TIMEOUT_S",
360
+ "the total amount of wait time in seconds for `wait for` steps",
361
+ default=20.0,
362
+ )
363
+ CONFIG.define(
364
+ "CUCU_STEP_RETRY_AFTER_S",
365
+ "the amount of time to wait between retries in seconds for `wait for` steps",
366
+ default=0.5,
367
+ )
368
+ CONFIG.define(
369
+ "CUCU_KEEP_BROWSER_ALIVE",
370
+ "when set to true we'll reuse the browser between scenario runs",
371
+ default=False,
372
+ )
373
+ CONFIG.define(
374
+ "CUCU_BROWSER_WINDOW_HEIGHT",
375
+ "the browser window height when running browser tests",
376
+ default=768,
377
+ )
378
+ CONFIG.define(
379
+ "CUCU_BROWSER_WINDOW_WIDTH",
380
+ "the browser window width when running browser tests",
381
+ default=1366,
382
+ )
383
+ CONFIG.define(
384
+ "CUCU_BROWSER_DOWNLOADS_DIR",
385
+ "the browser download directory when running browser tests",
386
+ default="/tmp/cucu-browser-downloads",
387
+ )
388
+ CONFIG.define(
389
+ "CUCU_SOCKET_DEFAULT_TIMEOUT_S",
390
+ "the default timeout (seconds) for socket connect/read in cucu",
391
+ default=10,
392
+ )
393
+ CONFIG.define(
394
+ "CUCU_SELENIUM_DEFAULT_TIMEOUT_S",
395
+ "the default timeout (seconds) for selenium connect/read in selenium",
396
+ default=10,
397
+ )
398
+ CONFIG.define(
399
+ "CUCU_MONITOR_PNG",
400
+ "when set to a filename `cucu` will update the image to match "
401
+ "the exact image step at runtime.",
402
+ default=".monitor.png",
403
+ )
404
+ CONFIG.define(
405
+ "CUCU_LINT_RULES_PATH",
406
+ "comma separated list of paths to load cucu lint rules from .yaml files",
407
+ default="",
408
+ )
409
+ CONFIG.define(
410
+ "CUCU_JUNIT_WITH_STACKTRACE",
411
+ "when set to 'true' results in stacktraces showing in the JUnit XML failure output",
412
+ default="false",
413
+ )
414
+ CONFIG.define(
415
+ "CUCU_SKIP_HIGHLIGHT_BORDER",
416
+ "when set to 'True' skips adding a border to highlight found element in screenshots",
417
+ default=True,
418
+ )
419
+
420
+
421
+ # define re_map here instead of in utils.py to avoid circular import
422
+ def leaf_map(data, value_func, parent=None, key=None):
423
+ """
424
+ Utility to apply a map function recursively to a dict or list.
425
+
426
+ Args:
427
+ data: The dict or list or value to use
428
+ value_func: Callable function that accepts data and parent
429
+ parent: The parent object (or None)
430
+ """
431
+ if isinstance(data, dict):
432
+ for key, value in data.items():
433
+ data[key] = leaf_map(value, value_func, data, key)
434
+ return data
435
+ elif isinstance(data, list):
436
+ for x, value in enumerate(data):
437
+ data[x] = leaf_map(value, value_func, data, key)
438
+ return data
439
+ else:
440
+ return value_func(data, parent, key)
@@ -0,0 +1 @@
1
+ taken and adapted from https://github.com/celedev97/python-edgedriver-autoinstaller
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+
3
+ import os
4
+ import logging
5
+ from . import utils
6
+
7
+
8
+ def install(cwd=False):
9
+ """
10
+ Appends the directory of the edgedriver binary file to PATH.
11
+
12
+ :param cwd: Flag indicating whether to download to current working directory
13
+ :return: The file path of edgedriver
14
+ """
15
+ edgedriver_filepath = utils.download_edgedriver(cwd)
16
+ if not edgedriver_filepath:
17
+ logging.debug("Can not download edgedriver.")
18
+ return
19
+ edgedriver_dir = os.path.dirname(edgedriver_filepath)
20
+ if "PATH" not in os.environ:
21
+ os.environ["PATH"] = edgedriver_dir
22
+ elif edgedriver_dir not in os.environ["PATH"]:
23
+ os.environ["PATH"] = (
24
+ edgedriver_dir
25
+ + utils.get_variable_separator()
26
+ + os.environ["PATH"]
27
+ )
28
+ return edgedriver_filepath
29
+
30
+
31
+ def get_edge_version():
32
+ """
33
+ Get installed version of edge on client
34
+
35
+ :return: The version of edge
36
+ """
37
+ return utils.get_edge_version()
@@ -0,0 +1,231 @@
1
+ # coding: utf-8
2
+ """
3
+ Helper functions for filename and URL generation.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import xml.etree.ElementTree as elemTree
12
+ import zipfile
13
+ from io import BytesIO
14
+
15
+ import requests
16
+
17
+ __author__ = "Yeongbin Jo <iam.yeongbin.jo@gmail.com>"
18
+
19
+
20
+ def get_edgedriver_filename():
21
+ """
22
+ Returns the filename of the binary for the current platform.
23
+ :return: Binary filename
24
+ """
25
+ if sys.platform.startswith("win"):
26
+ return "msedgedriver.exe"
27
+ return "msedgedriver"
28
+
29
+
30
+ def get_variable_separator():
31
+ """
32
+ Returns the environment variable separator for the current platform.
33
+ :return: Environment variable separator
34
+ """
35
+ if sys.platform.startswith("win"):
36
+ return ";"
37
+ return ":"
38
+
39
+
40
+ def get_platform_architecture():
41
+ if sys.platform.startswith("linux") and sys.maxsize > 2**32:
42
+ platform = "linux"
43
+ architecture = "64"
44
+ elif sys.platform == "darwin":
45
+ platform = "mac"
46
+ architecture = "64"
47
+ elif sys.platform.startswith("win"):
48
+ platform = "win"
49
+ architecture = "32"
50
+ else:
51
+ raise RuntimeError(
52
+ "Could not determine edgedriver download URL for this platform."
53
+ )
54
+ return platform, architecture
55
+
56
+
57
+ def get_edgedriver_url(version):
58
+ """
59
+ Generates the download URL for current platform , architecture and the given version.
60
+ Supports Linux, MacOS and Windows.
61
+ :param version: edgedriver version string
62
+ :return: Download URL for edgedriver
63
+ """
64
+ base_url = "https://msedgedriver.azureedge.net//"
65
+ platform, architecture = get_platform_architecture()
66
+ return (
67
+ base_url + version + "/edgedriver_" + platform + architecture + ".zip"
68
+ )
69
+
70
+
71
+ def find_binary_in_path(filename):
72
+ """
73
+ Searches for a binary named `filename` in the current PATH. If an executable is found, its absolute path is returned
74
+ else None.
75
+ :param filename: Filename of the binary
76
+ :return: Absolute path or None
77
+ """
78
+ if "PATH" not in os.environ:
79
+ return None
80
+ for directory in os.environ["PATH"].split(get_variable_separator()):
81
+ binary = os.path.abspath(os.path.join(directory, filename))
82
+ if os.path.isfile(binary) and os.access(binary, os.X_OK):
83
+ return binary
84
+ return None
85
+
86
+
87
+ def check_version(binary, required_version):
88
+ try:
89
+ version = subprocess.check_output([binary, "-v"])
90
+ version = re.match(r".*?([\d.]+).*?", version.decode("utf-8"))[1]
91
+ if version == required_version:
92
+ return True
93
+ except Exception:
94
+ return False
95
+ return False
96
+
97
+
98
+ def get_edge_version():
99
+ """
100
+ :return: the version of edge installed on client
101
+ """
102
+ platform, _ = get_platform_architecture()
103
+ if platform == "linux":
104
+ return # Edge for linux still doesn't exists
105
+ elif platform == "mac":
106
+ process = subprocess.Popen(
107
+ [
108
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
109
+ "--version",
110
+ ],
111
+ stdout=subprocess.PIPE,
112
+ )
113
+ version = (
114
+ process.communicate()[0]
115
+ .decode("UTF-8")
116
+ .replace("Microsoft Edge", "")
117
+ .strip()
118
+ )
119
+ elif platform == "win":
120
+ process = subprocess.Popen(
121
+ [
122
+ "reg",
123
+ "query",
124
+ "HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\BLBeacon",
125
+ "/v",
126
+ "version",
127
+ ],
128
+ stdout=subprocess.PIPE,
129
+ stderr=subprocess.DEVNULL,
130
+ stdin=subprocess.DEVNULL,
131
+ )
132
+ version = process.communicate()[0].decode("UTF-8").strip().split()[-1]
133
+ else:
134
+ return
135
+ return version
136
+
137
+
138
+ def get_major_version(version):
139
+ """
140
+ :param version: the version of edge
141
+ :return: the major version of edge
142
+ """
143
+ return version.split(".")[0]
144
+
145
+
146
+ def get_matched_edgedriver_version(version):
147
+ """
148
+ :param version: the version of edge
149
+ :return: the version of edgedriver
150
+ """
151
+ doc = requests.get(
152
+ "https://msedgedriver.azureedge.net/",
153
+ headers={"accept-encoding": "gzip, deflate, br"},
154
+ ).text
155
+ root = elemTree.fromstring(doc)
156
+ for k in root.iter("Name"):
157
+ if k.text.find(get_major_version(version) + ".") == 0:
158
+ return k.text.split("/")[0]
159
+ return
160
+
161
+
162
+ def get_edgedriver_path():
163
+ """
164
+ :return: path of the edgedriver binary
165
+ """
166
+ return os.path.abspath(os.path.dirname(__file__))
167
+
168
+
169
+ def print_edgedriver_path():
170
+ """
171
+ Print the path of the edgedriver binary.
172
+ """
173
+ print(get_edgedriver_path())
174
+
175
+
176
+ def download_edgedriver(cwd=False):
177
+ """
178
+ Downloads, unzips and installs edgedriver.
179
+ If a edgedriver binary is found in PATH it will be copied, otherwise downloaded.
180
+
181
+ :param cwd: Flag indicating whether to download to current working directory
182
+ :return: The file path of edgedriver
183
+ """
184
+ edge_version = get_edge_version()
185
+ if not edge_version:
186
+ logging.debug("edge is not installed.")
187
+ return
188
+ edgedriver_version = get_matched_edgedriver_version(edge_version)
189
+ if not edgedriver_version:
190
+ logging.debug(
191
+ "Can not find edgedriver for currently installed edge version."
192
+ )
193
+ return
194
+ major_version = get_major_version(edgedriver_version)
195
+
196
+ if cwd:
197
+ edgedriver_dir = os.path.join(
198
+ os.path.abspath(os.getcwd()), major_version
199
+ )
200
+ else:
201
+ edgedriver_dir = os.path.join(
202
+ os.path.abspath(os.path.dirname(__file__)), major_version
203
+ )
204
+ edgedriver_filename = get_edgedriver_filename()
205
+ edgedriver_filepath = os.path.join(edgedriver_dir, edgedriver_filename)
206
+ if not os.path.isfile(edgedriver_filepath) or not check_version(
207
+ edgedriver_filepath, edgedriver_version
208
+ ):
209
+ logging.debug(f"Downloading edgedriver ({edgedriver_version})...")
210
+ if not os.path.isdir(edgedriver_dir):
211
+ os.makedirs(edgedriver_dir)
212
+ url = get_edgedriver_url(version=edgedriver_version)
213
+ try:
214
+ response = requests.get(url)
215
+ if response.status_code != 200:
216
+ raise Exception("URL Not Found")
217
+ except Exception:
218
+ raise RuntimeError(f"Failed to download edgedriver archive: {url}")
219
+ archive = BytesIO(response.content)
220
+ with zipfile.ZipFile(archive) as zip_file:
221
+ zip_file.extract(edgedriver_filename, edgedriver_dir)
222
+ else:
223
+ logging.debug("edgedriver is already installed.")
224
+ if not os.access(edgedriver_filepath, os.X_OK):
225
+ os.chmod(edgedriver_filepath, 0o744)
226
+ return edgedriver_filepath
227
+
228
+
229
+ if __name__ == "__main__":
230
+ print(get_edge_version())
231
+ print(download_edgedriver())