requestium 0.4.0__tar.gz → 0.5.1__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.
- {requestium-0.4.0/requestium.egg-info → requestium-0.5.1}/PKG-INFO +73 -13
- {requestium-0.4.0 → requestium-0.5.1}/README.md +5 -2
- requestium-0.5.1/pyproject.toml +145 -0
- {requestium-0.4.0 → requestium-0.5.1}/requestium/__init__.py +1 -0
- requestium-0.5.1/requestium/requestium.py +6 -0
- requestium-0.5.1/requestium/requestium_mixin.py +255 -0
- requestium-0.5.1/requestium/requestium_response.py +34 -0
- requestium-0.5.1/requestium/requestium_session.py +163 -0
- {requestium-0.4.0 → requestium-0.5.1/requestium.egg-info}/PKG-INFO +73 -13
- requestium-0.5.1/requestium.egg-info/SOURCES.txt +17 -0
- requestium-0.5.1/requestium.egg-info/requires.txt +15 -0
- requestium-0.5.1/tests/test_cookies.py +95 -0
- requestium-0.5.1/tests/test_mixin.py +118 -0
- requestium-0.5.1/tests/test_session.py +92 -0
- requestium-0.5.1/tests/test_user_agent.py +34 -0
- requestium-0.4.0/.travis.yml +0 -38
- requestium-0.4.0/MANIFEST.in +0 -5
- requestium-0.4.0/pyproject.toml +0 -20
- requestium-0.4.0/requestium/requestium.py +0 -412
- requestium-0.4.0/requestium.egg-info/SOURCES.txt +0 -15
- requestium-0.4.0/requestium.egg-info/not-zip-safe +0 -1
- requestium-0.4.0/requestium.egg-info/requires.txt +0 -4
- requestium-0.4.0/setup.py +0 -38
- requestium-0.4.0/tox.ini +0 -14
- {requestium-0.4.0 → requestium-0.5.1}/LICENSE +0 -0
- {requestium-0.4.0 → requestium-0.5.1}/requestium.egg-info/dependency_links.txt +0 -0
- {requestium-0.4.0 → requestium-0.5.1}/requestium.egg-info/top_level.txt +0 -0
- {requestium-0.4.0 → requestium-0.5.1}/setup.cfg +0 -0
|
@@ -1,25 +1,85 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: requestium
|
|
3
|
-
Version: 0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
Version: 0.5.1
|
|
4
|
+
Author-email: Joaquin Alori <joaquin@tryolabs.com>
|
|
5
|
+
Maintainer-email: Judson Neer <jkudson.neer@gmail.com>
|
|
6
|
+
License: BSD 3-Clause License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2017, Tryolabs
|
|
9
|
+
All rights reserved.
|
|
10
|
+
|
|
11
|
+
Redistribution and use in source and binary forms, with or without
|
|
12
|
+
modification, are permitted provided that the following conditions are met:
|
|
13
|
+
|
|
14
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
15
|
+
list of conditions and the following disclaimer.
|
|
16
|
+
|
|
17
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
18
|
+
this list of conditions and the following disclaimer in the documentation
|
|
19
|
+
and/or other materials provided with the distribution.
|
|
20
|
+
|
|
21
|
+
* Neither the name of the copyright holder nor the names of its
|
|
22
|
+
contributors may be used to endorse or promote products derived from
|
|
23
|
+
this software without specific prior written permission.
|
|
24
|
+
|
|
25
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
26
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
27
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
28
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
29
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
30
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
31
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
32
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
33
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
34
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
35
|
+
|
|
36
|
+
Project-URL: source, https://github.com/tryolabs/requestium
|
|
37
|
+
Project-URL: download, https://pypi.org/project/requestium/#files
|
|
38
|
+
Project-URL: issues, https://github.com/tryolabs/requestium/issues
|
|
39
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
40
|
Classifier: Intended Audience :: Developers
|
|
41
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
11
47
|
Classifier: Natural Language :: English
|
|
12
|
-
Classifier:
|
|
13
|
-
Classifier:
|
|
14
|
-
Classifier:
|
|
48
|
+
Classifier: Operating System :: MacOS
|
|
49
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
50
|
+
Classifier: Operating System :: POSIX
|
|
51
|
+
Classifier: Operating System :: OS Independent
|
|
52
|
+
Classifier: Environment :: Web Environment
|
|
53
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
54
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
55
|
+
Classifier: Topic :: Software Development :: Testing
|
|
56
|
+
Requires-Python: >=3.10
|
|
15
57
|
Description-Content-Type: text/markdown
|
|
16
58
|
License-File: LICENSE
|
|
59
|
+
Requires-Dist: parsel>=1.7
|
|
60
|
+
Requires-Dist: requests>=2.27
|
|
61
|
+
Requires-Dist: selenium>=4.32
|
|
62
|
+
Requires-Dist: tldextract>=5.3
|
|
63
|
+
Provides-Extra: dev
|
|
64
|
+
Requires-Dist: bandit[sarif]==1.9.2; extra == "dev"
|
|
65
|
+
Requires-Dist: mypy==1.19.1; extra == "dev"
|
|
66
|
+
Requires-Dist: pre-commit~=4.5.0; extra == "dev"
|
|
67
|
+
Requires-Dist: pytest-cov~=7.0.0; extra == "dev"
|
|
68
|
+
Requires-Dist: pytest-xdist~=3.8.0; extra == "dev"
|
|
69
|
+
Requires-Dist: pytest~=9.0.0; extra == "dev"
|
|
70
|
+
Requires-Dist: ruff==0.14.11; extra == "dev"
|
|
71
|
+
Requires-Dist: ssort==0.16.0; extra == "dev"
|
|
72
|
+
Requires-Dist: types-requests==2.32.4.20260107; extra == "dev"
|
|
73
|
+
Dynamic: license-file
|
|
17
74
|
|
|
18
75
|

|
|
19
76
|
========
|
|
20
77
|
|
|
21
|
-
[](https://travis-ci.org/tryolabs/requestium)
|
|
22
78
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
79
|
+
[](https://pypi.org/project/requestium/)
|
|
80
|
+
[](https://pypi.org/project/requestium/)
|
|
81
|
+
[](https://htmlpreview.github.io/?https://github.com/tryolabs/requestium/blob/python-coverage-comment-action-data/htmlcov/index.html)
|
|
82
|
+
[](https://github.com/tryolabs/requestium/actions/workflows/python.yml)
|
|
23
83
|
|
|
24
84
|
Requestium is a Python library that merges the power of [Requests](https://github.com/requests/requests), [Selenium](https://github.com/SeleniumHQ/selenium), and [Parsel](https://github.com/scrapy/parsel) into a single integrated tool for automatizing web actions.
|
|
25
85
|
|
|
@@ -53,7 +113,7 @@ options = {'arguments': ['headless']}
|
|
|
53
113
|
s = Session(webdriver_path='./chromedriver', default_timeout=15, webdriver_options=options)
|
|
54
114
|
```
|
|
55
115
|
|
|
56
|
-
Since headless mode is common, there's a shortcut for it by
|
|
116
|
+
Since headless mode is common, there's a shortcut for it by specifying `headless=True`.
|
|
57
117
|
|
|
58
118
|
```python
|
|
59
119
|
from requestium import Session, Keys
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|

|
|
2
2
|
========
|
|
3
3
|
|
|
4
|
-
[](https://travis-ci.org/tryolabs/requestium)
|
|
5
4
|
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
5
|
+
[](https://pypi.org/project/requestium/)
|
|
6
|
+
[](https://pypi.org/project/requestium/)
|
|
7
|
+
[](https://htmlpreview.github.io/?https://github.com/tryolabs/requestium/blob/python-coverage-comment-action-data/htmlcov/index.html)
|
|
8
|
+
[](https://github.com/tryolabs/requestium/actions/workflows/python.yml)
|
|
6
9
|
|
|
7
10
|
Requestium is a Python library that merges the power of [Requests](https://github.com/requests/requests), [Selenium](https://github.com/SeleniumHQ/selenium), and [Parsel](https://github.com/scrapy/parsel) into a single integrated tool for automatizing web actions.
|
|
8
11
|
|
|
@@ -36,7 +39,7 @@ options = {'arguments': ['headless']}
|
|
|
36
39
|
s = Session(webdriver_path='./chromedriver', default_timeout=15, webdriver_options=options)
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
Since headless mode is common, there's a shortcut for it by
|
|
42
|
+
Since headless mode is common, there's a shortcut for it by specifying `headless=True`.
|
|
40
43
|
|
|
41
44
|
```python
|
|
42
45
|
from requestium import Session, Keys
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "requestium"
|
|
3
|
+
version = "0.5.1"
|
|
4
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = { file = "LICENSE" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Joaquin Alori", email = "joaquin@tryolabs.com" }
|
|
9
|
+
]
|
|
10
|
+
maintainers = [
|
|
11
|
+
{ name = "Judson Neer", email = "jkudson.neer@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: BSD License",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Natural Language :: English",
|
|
23
|
+
"Operating System :: MacOS",
|
|
24
|
+
"Operating System :: Microsoft :: Windows",
|
|
25
|
+
"Operating System :: POSIX",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Environment :: Web Environment",
|
|
28
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Topic :: Software Development :: Testing",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"parsel>=1.7",
|
|
34
|
+
"requests>=2.27",
|
|
35
|
+
"selenium>=4.32",
|
|
36
|
+
"tldextract>=5.3",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
source = "https://github.com/tryolabs/requestium"
|
|
41
|
+
download = "https://pypi.org/project/requestium/#files"
|
|
42
|
+
issues = "https://github.com/tryolabs/requestium/issues"
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = [
|
|
46
|
+
"bandit[sarif]==1.9.2",
|
|
47
|
+
"mypy==1.19.1",
|
|
48
|
+
"pre-commit~=4.5.0",
|
|
49
|
+
"pytest-cov~=7.0.0",
|
|
50
|
+
"pytest-xdist~=3.8.0",
|
|
51
|
+
"pytest~=9.0.0",
|
|
52
|
+
"ruff==0.14.11",
|
|
53
|
+
"ssort==0.16.0",
|
|
54
|
+
"types-requests==2.32.4.20260107",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 160
|
|
61
|
+
include = [
|
|
62
|
+
"requestium/**/*.py",
|
|
63
|
+
"tests/**/*.py",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
extend-select = [
|
|
68
|
+
"A", # flake8-builtins (A)
|
|
69
|
+
"ANN", # flake8-annotations (ANN)
|
|
70
|
+
"ARG", # flake8-unused-arguments (ARG)
|
|
71
|
+
"B", # flake8-bugbear (B)
|
|
72
|
+
"BLE", # flake8-blind-except (BLE)
|
|
73
|
+
"C4", # flake8-comprehensions (C4)
|
|
74
|
+
"C90", # mccabe (C90)
|
|
75
|
+
"D", # pydocstyle (D)
|
|
76
|
+
"DTZ", # flake8-datetimez (DTZ)
|
|
77
|
+
"E", # pycodestyle-error (E)
|
|
78
|
+
"EM", # flake8-errmsg (EM)
|
|
79
|
+
"EXE", # flake8-executable (EXE)
|
|
80
|
+
"F", # Pyflakes (F)
|
|
81
|
+
"FA", # flake8-future-annotations (FA)
|
|
82
|
+
"FBT", # flake8-boolean-trap (FBT)
|
|
83
|
+
"FIX", # flake8-fixme (FIX)
|
|
84
|
+
"G", # flake8-logging-format (G)
|
|
85
|
+
"I", # isort (I)
|
|
86
|
+
"ICN", # flake8-import-conventions (ICN)
|
|
87
|
+
"INP", # flake8-no-pep420 (INP)
|
|
88
|
+
"INT", # flake8-gettext (INT)
|
|
89
|
+
"ISC", # flake8-implicit-str-concat (ISC)
|
|
90
|
+
"LOG", # flake8-logging (LOG)
|
|
91
|
+
"N", # pep8-naming (N)
|
|
92
|
+
"PIE", # flake8-pie (PIE)
|
|
93
|
+
"PL", # Pylint (PL)
|
|
94
|
+
"PT", # flake8-pytest-style (PT)
|
|
95
|
+
"PTH", # flake8-use-pathlib (PTH)
|
|
96
|
+
"Q", # flake8-quotes (Q)
|
|
97
|
+
"RET", # flake8-return (RET)
|
|
98
|
+
"RSE", # flake8-raise (RSE)
|
|
99
|
+
"RUF", # Ruff-specific rules (RUF)
|
|
100
|
+
"SIM", # flake8-simplify (SIM)
|
|
101
|
+
"SLF", # flake8-self (SLF)
|
|
102
|
+
"T10", # flake8-debugger (T10)
|
|
103
|
+
"T20", # flake8-print (T20)
|
|
104
|
+
"TC", # flake8-type-checking (TC)
|
|
105
|
+
"TD", # flake8-todos (TD)
|
|
106
|
+
"TID", # flake8-tidy-imports (TID)
|
|
107
|
+
"UP", # pyupgrade (UP)
|
|
108
|
+
"W", # pycodestyle-warning (W)
|
|
109
|
+
"YTT", # flake8-2020 (YTT)
|
|
110
|
+
]
|
|
111
|
+
ignore = [
|
|
112
|
+
"ANN002", # missing-type-args (ANN002)
|
|
113
|
+
"ANN003", # missing-type-kwargs (ANN003)
|
|
114
|
+
"D100", # undocumented-public-module (D100)
|
|
115
|
+
"D102", # undocumented-public-method (D102)
|
|
116
|
+
"D103", # undocumented-public-function (D103)
|
|
117
|
+
"D104", # undocumented-public-package (D104)
|
|
118
|
+
"D107", # undocumented-public-init (D107)
|
|
119
|
+
"D203", # incorrect-blank-line-before-class (D203)
|
|
120
|
+
"D212", # multi-line-summary-first-line (D212)
|
|
121
|
+
"FIX002", # line-contains-todo (FIX002)
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
[tool.ruff.lint.per-file-ignores]
|
|
125
|
+
"**/{tests,docs}/*" = [
|
|
126
|
+
"SLF001", # private-member-access (SLF001)
|
|
127
|
+
"PLR2004", # magic-value-comparison (PLR2004)
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
[tool.pytest]
|
|
131
|
+
addopts = ["-n", "auto", "--cov=requestium", "--no-cov-on-fail"]
|
|
132
|
+
testpaths = [
|
|
133
|
+
"tests",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[tool.coverage]
|
|
137
|
+
|
|
138
|
+
[tool.coverage.run]
|
|
139
|
+
branch = true
|
|
140
|
+
relative_files = true
|
|
141
|
+
|
|
142
|
+
[tool.coverage.report]
|
|
143
|
+
exclude_also = [
|
|
144
|
+
"logger\\.",
|
|
145
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import tldextract
|
|
9
|
+
from parsel.selector import Selector, SelectorList
|
|
10
|
+
from selenium.common.exceptions import NoSuchWindowException, WebDriverException
|
|
11
|
+
from selenium.webdriver.common.by import By, ByType
|
|
12
|
+
from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
|
|
13
|
+
from selenium.webdriver.support import expected_conditions
|
|
14
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from selenium.webdriver.remote.webelement import WebElement
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
DEFAULT_TIMEOUT: float = 0.5
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_click(self: WebElement) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Ensure a click gets made, because Selenium can be a bit buggy about clicks.
|
|
26
|
+
|
|
27
|
+
This method gets added to the selenium element returned in '__ensure_element_by_xpath'.
|
|
28
|
+
We should probably add it to more selenium methods, such as all the 'find**' methods though.
|
|
29
|
+
|
|
30
|
+
I wrote this method out of frustration with chromedriver and its problems with clicking
|
|
31
|
+
items that need to be scrolled to in order to be clickable. In '__ensure_element_by_xpath' we
|
|
32
|
+
scroll to the item before returning it, but chrome has some problems if it doesn't get some
|
|
33
|
+
time to scroll to the item. This method ensures chromes gets enough time to scroll to the item
|
|
34
|
+
before clicking it. I tried SEVERAL more 'correct' methods to get around this, but none of them
|
|
35
|
+
worked 100% of the time (waiting for the element to be 'clickable' does not work).
|
|
36
|
+
"""
|
|
37
|
+
# We ensure the element is scrolled into the middle of the viewport to ensure that
|
|
38
|
+
# it is clickable. There are two main ways an element may not be clickable:
|
|
39
|
+
# - It is outside of the viewport
|
|
40
|
+
# - It is under a banner or toolbar
|
|
41
|
+
# This script solves both cases
|
|
42
|
+
script = (
|
|
43
|
+
"var viewPortHeight = Math.max("
|
|
44
|
+
"document.documentElement.clientHeight, window.innerHeight || 0);"
|
|
45
|
+
"var elementTop = arguments[0].getBoundingClientRect().top;"
|
|
46
|
+
"window.scrollBy(0, elementTop-(viewPortHeight/2));"
|
|
47
|
+
)
|
|
48
|
+
self.parent.execute_script(script, self) # parent = the webdriver
|
|
49
|
+
|
|
50
|
+
exception_message = ""
|
|
51
|
+
for _ in range(10):
|
|
52
|
+
try:
|
|
53
|
+
self.click()
|
|
54
|
+
return
|
|
55
|
+
except WebDriverException as e:
|
|
56
|
+
exception_message = str(e)
|
|
57
|
+
time.sleep(0.2)
|
|
58
|
+
msg = f"Couldn't click item after trying 10 times, got error message: \n{exception_message}"
|
|
59
|
+
raise WebDriverException(msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DriverMixin(RemoteWebDriver):
|
|
63
|
+
"""Provides helper methods to our driver classes."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
66
|
+
self.default_timeout = kwargs.pop("default_timeout", DEFAULT_TIMEOUT)
|
|
67
|
+
super().__init__(*args, **kwargs)
|
|
68
|
+
|
|
69
|
+
def try_add_cookie(self, cookie: dict[str, Any]) -> bool:
|
|
70
|
+
"""
|
|
71
|
+
Attempt to add the cookie.
|
|
72
|
+
|
|
73
|
+
Suppress any errors, and simply detect success or failure.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
self.add_cookie(cookie)
|
|
77
|
+
except WebDriverException as e:
|
|
78
|
+
if e.msg and not e.msg.__contains__("Couldn't add the following cookie to the webdriver"):
|
|
79
|
+
raise WebDriverException from e
|
|
80
|
+
return self.is_cookie_in_driver(cookie)
|
|
81
|
+
|
|
82
|
+
def ensure_add_cookie(self, cookie: dict[str, Any], override_domain: str | None = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Add a cookie to the driver and check to ensure it has been added.
|
|
85
|
+
|
|
86
|
+
Selenium needs the driver to be currently at the domain of the cookie
|
|
87
|
+
before allowing you to add it, so we need to get through this limitation.
|
|
88
|
+
|
|
89
|
+
The cookie parameter is a dict which must contain the keys (name, value, domain) and
|
|
90
|
+
may contain the keys (path, expiry).
|
|
91
|
+
|
|
92
|
+
We first check that we aren't currently in the cookie's domain, if we aren't, we GET
|
|
93
|
+
the cookie's domain and then add the cookies to the driver.
|
|
94
|
+
|
|
95
|
+
We can override the cookie's domain using 'override_domain'. The use for this
|
|
96
|
+
parameter is that sometimes GETting the cookie's domain redirects you to a different
|
|
97
|
+
sub domain, and therefore adding the cookie fails. So sometimes the user may
|
|
98
|
+
need to override the cookie's domain to a less strict one, Eg.: 'site.com' instead of
|
|
99
|
+
'home.site.com', in this way even if the site redirects us to a subdomain, the cookie will
|
|
100
|
+
stick. If you set the domain to '', the cookie gets added with whatever domain the browser
|
|
101
|
+
is currently at (at least in chrome it does), so this ensures the cookie gets added.
|
|
102
|
+
|
|
103
|
+
It also retries adding the cookie with a more permissive domain if it fails in the first
|
|
104
|
+
try, and raises an exception if that fails. The standard selenium behaviour in this case
|
|
105
|
+
was to not do anything, which was very hard to debug.
|
|
106
|
+
"""
|
|
107
|
+
if override_domain:
|
|
108
|
+
cookie["domain"] = override_domain
|
|
109
|
+
|
|
110
|
+
cookie_domain = cookie["domain"] if cookie["domain"][0] != "." else cookie["domain"][1:]
|
|
111
|
+
try:
|
|
112
|
+
browser_domain = tldextract.extract(self.current_url).fqdn
|
|
113
|
+
except (AttributeError, NoSuchWindowException):
|
|
114
|
+
browser_domain = ""
|
|
115
|
+
if cookie_domain not in browser_domain:
|
|
116
|
+
# TODO @joaqo: Check if hardcoding 'http' causes trouble.
|
|
117
|
+
# https://github.com/tryolabs/requestium/issues/97
|
|
118
|
+
# Consider using a new proxy for this next request to not cause an anomalous
|
|
119
|
+
# request. This way their server sees our ip address as continuously having the
|
|
120
|
+
# same cookies and not have a request mid-session with no cookies
|
|
121
|
+
self.get("http://" + cookie_domain)
|
|
122
|
+
|
|
123
|
+
cookie_added = self.try_add_cookie(cookie)
|
|
124
|
+
|
|
125
|
+
# If we fail adding the cookie, retry with a more permissive domain
|
|
126
|
+
if not cookie_added:
|
|
127
|
+
cookie["domain"] = tldextract.extract(cookie["domain"]).top_domain_under_public_suffix
|
|
128
|
+
cookie_added = self.try_add_cookie(cookie)
|
|
129
|
+
if not cookie_added:
|
|
130
|
+
msg = f"Couldn't add the following cookie to the webdriver: {cookie}"
|
|
131
|
+
raise WebDriverException(msg)
|
|
132
|
+
|
|
133
|
+
def is_cookie_in_driver(self, cookie: dict[str, Any]) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
We check that the cookie is correctly added to the driver.
|
|
136
|
+
|
|
137
|
+
We only compare name, value and domain, as the rest can produce false negatives.
|
|
138
|
+
We are a bit lenient when comparing domains.
|
|
139
|
+
"""
|
|
140
|
+
for driver_cookie in self.get_cookies():
|
|
141
|
+
name_matches = cookie["name"] == driver_cookie["name"]
|
|
142
|
+
value_matches = cookie["value"] == driver_cookie["value"]
|
|
143
|
+
domain_matches = driver_cookie["domain"] in (cookie["domain"], "." + cookie["domain"])
|
|
144
|
+
if name_matches and value_matches and domain_matches:
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def ensure_element_by_id(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
149
|
+
return self.ensure_element(By.ID, selector, state, timeout)
|
|
150
|
+
|
|
151
|
+
def ensure_element_by_name(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
152
|
+
return self.ensure_element(By.NAME, selector, state, timeout)
|
|
153
|
+
|
|
154
|
+
def ensure_element_by_xpath(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
155
|
+
return self.ensure_element(By.XPATH, selector, state, timeout)
|
|
156
|
+
|
|
157
|
+
def ensure_element_by_link_text(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
158
|
+
return self.ensure_element(By.LINK_TEXT, selector, state, timeout)
|
|
159
|
+
|
|
160
|
+
def ensure_element_by_partial_link_text(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
161
|
+
return self.ensure_element(By.PARTIAL_LINK_TEXT, selector, state, timeout)
|
|
162
|
+
|
|
163
|
+
def ensure_element_by_tag_name(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
164
|
+
return self.ensure_element(By.TAG_NAME, selector, state, timeout)
|
|
165
|
+
|
|
166
|
+
def ensure_element_by_class_name(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
167
|
+
return self.ensure_element(By.CLASS_NAME, selector, state, timeout)
|
|
168
|
+
|
|
169
|
+
def ensure_element_by_css_selector(self, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
170
|
+
return self.ensure_element(By.CSS_SELECTOR, selector, state, timeout)
|
|
171
|
+
|
|
172
|
+
def ensure_element(self, locator: ByType | str, selector: str, state: str | None = "present", timeout: float | None = None) -> WebElement | None:
|
|
173
|
+
"""
|
|
174
|
+
Wait until an element appears or disappears in the browser.
|
|
175
|
+
|
|
176
|
+
The webdriver runs in parallel with our scripts, so we must wait for it everytime it
|
|
177
|
+
runs javascript. Selenium automatically waits till a page loads when GETing it,
|
|
178
|
+
but it doesn't do this when it runs javascript and makes AJAX requests.
|
|
179
|
+
So we must explicitly wait in that case.
|
|
180
|
+
|
|
181
|
+
The 'locator' argument defines what strategy we use to search for the element.
|
|
182
|
+
It expects standard names from the By class in selenium.webdriver.common.by.
|
|
183
|
+
https://www.selenium.dev/selenium/docs/api/py/webdriver/selenium.webdriver.common.by.html
|
|
184
|
+
|
|
185
|
+
The 'state' argument allows us to chose between waiting for the element to be visible,
|
|
186
|
+
clickable, present, or invisible. Presence is more inclusive, but sometimes we want to
|
|
187
|
+
know if the element is visible. Careful, its not always intuitive what Selenium considers
|
|
188
|
+
to be a visible element. We can also wait for it to be clickable, although this method
|
|
189
|
+
is a bit buggy in selenium, an element can be 'clickable' according to selenium and
|
|
190
|
+
still fail when we try to click it.
|
|
191
|
+
|
|
192
|
+
More info at: http://selenium-python.readthedocs.io/waits.html
|
|
193
|
+
"""
|
|
194
|
+
locators_compatibility = {
|
|
195
|
+
"link_text": By.LINK_TEXT,
|
|
196
|
+
"partial_link_text": By.PARTIAL_LINK_TEXT,
|
|
197
|
+
"tag_name": By.TAG_NAME,
|
|
198
|
+
"class_name": By.CLASS_NAME,
|
|
199
|
+
"css_selector": By.CSS_SELECTOR,
|
|
200
|
+
}
|
|
201
|
+
if locator in locators_compatibility:
|
|
202
|
+
warnings.warn(
|
|
203
|
+
"""
|
|
204
|
+
Support for locator strategy names with underscores is deprecated.
|
|
205
|
+
Use strategies from Selenium's By class (importable from selenium.webdriver.common.by).
|
|
206
|
+
""",
|
|
207
|
+
DeprecationWarning,
|
|
208
|
+
stacklevel=2,
|
|
209
|
+
)
|
|
210
|
+
locator = locators_compatibility[locator]
|
|
211
|
+
|
|
212
|
+
if not timeout:
|
|
213
|
+
timeout = self.default_timeout or DEFAULT_TIMEOUT
|
|
214
|
+
|
|
215
|
+
if state == "visible":
|
|
216
|
+
element = WebDriverWait(self, timeout).until(expected_conditions.visibility_of_element_located((locator, selector)))
|
|
217
|
+
elif state == "clickable":
|
|
218
|
+
element = WebDriverWait(self, timeout).until(expected_conditions.element_to_be_clickable((locator, selector)))
|
|
219
|
+
elif state == "present":
|
|
220
|
+
element = WebDriverWait(self, timeout).until(expected_conditions.presence_of_element_located((locator, selector)))
|
|
221
|
+
elif state == "invisible":
|
|
222
|
+
WebDriverWait(self, timeout).until(expected_conditions.invisibility_of_element_located((locator, selector)))
|
|
223
|
+
element = None
|
|
224
|
+
else:
|
|
225
|
+
msg = f"The 'state' argument must be 'visible', 'clickable', 'present' or 'invisible', not '{state}'"
|
|
226
|
+
raise ValueError(msg)
|
|
227
|
+
|
|
228
|
+
# We add this method to our element to provide a more robust click. Chromedriver
|
|
229
|
+
# sometimes needs some time before it can click an item, specially if it needs to
|
|
230
|
+
# scroll into it first. This method ensures clicks don't fail because of this.
|
|
231
|
+
if element:
|
|
232
|
+
element.ensure_click = functools.partial(_ensure_click, element) # type: ignore[attr-defined]
|
|
233
|
+
return element
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def selector(self) -> Selector:
|
|
237
|
+
"""
|
|
238
|
+
Returns the current state of the browser in a Selector.
|
|
239
|
+
|
|
240
|
+
We re-parse the site on each xpath, css, re call because we are running a web browser
|
|
241
|
+
and the site may change between calls
|
|
242
|
+
"""
|
|
243
|
+
return Selector(text=self.page_source)
|
|
244
|
+
|
|
245
|
+
def xpath(self, *args, **kwargs) -> SelectorList[Selector]:
|
|
246
|
+
return self.selector.xpath(*args, **kwargs)
|
|
247
|
+
|
|
248
|
+
def css(self, *args, **kwargs) -> SelectorList[Selector]:
|
|
249
|
+
return self.selector.css(*args, **kwargs)
|
|
250
|
+
|
|
251
|
+
def re(self, *args, **kwargs) -> list[str]:
|
|
252
|
+
return self.selector.re(*args, **kwargs)
|
|
253
|
+
|
|
254
|
+
def re_first(self, *args, **kwargs) -> str | None:
|
|
255
|
+
return self.selector.re_first(*args, **kwargs)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from parsel.selector import Selector, SelectorList
|
|
5
|
+
from requests import Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequestiumResponse(requests.Response):
|
|
9
|
+
"""Adds xpath, css, and regex methods to a normal requests response object."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, response: Response) -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.__class__ = type(response.__class__.__name__, (self.__class__, response.__class__), response.__dict__)
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def selector(self) -> Selector:
|
|
17
|
+
"""
|
|
18
|
+
Returns the response text in a Selector.
|
|
19
|
+
|
|
20
|
+
We re-parse the text on each xpath, css, re call in case the encoding has changed.
|
|
21
|
+
"""
|
|
22
|
+
return Selector(text=self.text)
|
|
23
|
+
|
|
24
|
+
def xpath(self, *args, **kwargs) -> SelectorList[Selector]:
|
|
25
|
+
return self.selector.xpath(*args, **kwargs)
|
|
26
|
+
|
|
27
|
+
def css(self, *args, **kwargs) -> SelectorList[Selector]:
|
|
28
|
+
return self.selector.css(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def re(self, *args, **kwargs) -> list[str]:
|
|
31
|
+
return self.selector.re(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
def re_first(self, *args, **kwargs) -> str | None:
|
|
34
|
+
return self.selector.re_first(*args, **kwargs)
|