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.
Files changed (28) hide show
  1. {requestium-0.4.0/requestium.egg-info → requestium-0.5.1}/PKG-INFO +73 -13
  2. {requestium-0.4.0 → requestium-0.5.1}/README.md +5 -2
  3. requestium-0.5.1/pyproject.toml +145 -0
  4. {requestium-0.4.0 → requestium-0.5.1}/requestium/__init__.py +1 -0
  5. requestium-0.5.1/requestium/requestium.py +6 -0
  6. requestium-0.5.1/requestium/requestium_mixin.py +255 -0
  7. requestium-0.5.1/requestium/requestium_response.py +34 -0
  8. requestium-0.5.1/requestium/requestium_session.py +163 -0
  9. {requestium-0.4.0 → requestium-0.5.1/requestium.egg-info}/PKG-INFO +73 -13
  10. requestium-0.5.1/requestium.egg-info/SOURCES.txt +17 -0
  11. requestium-0.5.1/requestium.egg-info/requires.txt +15 -0
  12. requestium-0.5.1/tests/test_cookies.py +95 -0
  13. requestium-0.5.1/tests/test_mixin.py +118 -0
  14. requestium-0.5.1/tests/test_session.py +92 -0
  15. requestium-0.5.1/tests/test_user_agent.py +34 -0
  16. requestium-0.4.0/.travis.yml +0 -38
  17. requestium-0.4.0/MANIFEST.in +0 -5
  18. requestium-0.4.0/pyproject.toml +0 -20
  19. requestium-0.4.0/requestium/requestium.py +0 -412
  20. requestium-0.4.0/requestium.egg-info/SOURCES.txt +0 -15
  21. requestium-0.4.0/requestium.egg-info/not-zip-safe +0 -1
  22. requestium-0.4.0/requestium.egg-info/requires.txt +0 -4
  23. requestium-0.4.0/setup.py +0 -38
  24. requestium-0.4.0/tox.ini +0 -14
  25. {requestium-0.4.0 → requestium-0.5.1}/LICENSE +0 -0
  26. {requestium-0.4.0 → requestium-0.5.1}/requestium.egg-info/dependency_links.txt +0 -0
  27. {requestium-0.4.0 → requestium-0.5.1}/requestium.egg-info/top_level.txt +0 -0
  28. {requestium-0.4.0 → requestium-0.5.1}/setup.cfg +0 -0
@@ -1,25 +1,85 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: requestium
3
- Version: 0.4.0
4
- Summary: Adds a Selenium webdriver and parsel's parser to a request's Session object, and makes switching between them seamless. Handles cookie, proxy and header transfer.
5
- Home-page: https://github.com/tryolabs/requestium
6
- Author: Joaquin Alori
7
- Author-email: joaquin@tryolabs.com
8
- License: MIT
9
- Classifier: Development Status :: 3 - Alpha
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: License :: OSI Approved :: MIT License
13
- Classifier: Programming Language :: Python :: 2
14
- Classifier: Programming Language :: Python :: 3
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
  ![Requestium](https://user-images.githubusercontent.com/14966348/32966130-8bb15b00-cbb7-11e7-9faf-85963ec5bd82.png)
19
76
  ========
20
77
 
21
- [![Build Status](https://travis-ci.org/tryolabs/requestium.svg?branch=master)](https://travis-ci.org/tryolabs/requestium)
22
78
  [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
79
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/requestium)](https://pypi.org/project/requestium/)
80
+ [![PyPI](https://img.shields.io/pypi/v/requestium?color=blue)](https://pypi.org/project/requestium/)
81
+ [![Coverage](https://raw.githubusercontent.com/tryolabs/requestium/python-coverage-comment-action-data/badge.svg)](https://htmlpreview.github.io/?https://github.com/tryolabs/requestium/blob/python-coverage-comment-action-data/htmlcov/index.html)
82
+ [![Python checks](https://github.com/tryolabs/requestium/actions/workflows/python.yml/badge.svg)](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 pecifying `headless=True`.
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
  ![Requestium](https://user-images.githubusercontent.com/14966348/32966130-8bb15b00-cbb7-11e7-9faf-85963ec5bd82.png)
2
2
  ========
3
3
 
4
- [![Build Status](https://travis-ci.org/tryolabs/requestium.svg?branch=master)](https://travis-ci.org/tryolabs/requestium)
5
4
  [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/requestium)](https://pypi.org/project/requestium/)
6
+ [![PyPI](https://img.shields.io/pypi/v/requestium?color=blue)](https://pypi.org/project/requestium/)
7
+ [![Coverage](https://raw.githubusercontent.com/tryolabs/requestium/python-coverage-comment-action-data/badge.svg)](https://htmlpreview.github.io/?https://github.com/tryolabs/requestium/blob/python-coverage-comment-action-data/htmlcov/index.html)
8
+ [![Python checks](https://github.com/tryolabs/requestium/actions/workflows/python.yml/badge.svg)](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 pecifying `headless=True`.
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
+ ]
@@ -1,4 +1,5 @@
1
1
  from selenium.common import exceptions # noqa: F401
2
+ from selenium.webdriver.common.by import By # noqa: F401
2
3
  from selenium.webdriver.common.keys import Keys # noqa: F401
3
4
  from selenium.webdriver.support.ui import Select # noqa: F401
4
5
 
@@ -0,0 +1,6 @@
1
+ from .requestium_mixin import ( # noqa: F401
2
+ DriverMixin,
3
+ _ensure_click,
4
+ )
5
+ from .requestium_response import RequestiumResponse # noqa: F401
6
+ from .requestium_session import Session # noqa: F401
@@ -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)