requestium 0.5.1__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.
- requestium/__init__.py +6 -0
- requestium/requestium.py +6 -0
- requestium/requestium_mixin.py +255 -0
- requestium/requestium_response.py +34 -0
- requestium/requestium_session.py +163 -0
- requestium-0.5.1.dist-info/METADATA +376 -0
- requestium-0.5.1.dist-info/RECORD +10 -0
- requestium-0.5.1.dist-info/WHEEL +5 -0
- requestium-0.5.1.dist-info/licenses/LICENSE +29 -0
- requestium-0.5.1.dist-info/top_level.txt +1 -0
requestium/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from selenium.common import exceptions # noqa: F401
|
|
2
|
+
from selenium.webdriver.common.by import By # noqa: F401
|
|
3
|
+
from selenium.webdriver.common.keys import Keys # noqa: F401
|
|
4
|
+
from selenium.webdriver.support.ui import Select # noqa: F401
|
|
5
|
+
|
|
6
|
+
from .requestium import Session # noqa: F401
|
requestium/requestium.py
ADDED
|
@@ -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)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import types
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import tldextract
|
|
9
|
+
from selenium import webdriver
|
|
10
|
+
from selenium.common import InvalidCookieDomainException
|
|
11
|
+
from selenium.webdriver import ChromeService
|
|
12
|
+
|
|
13
|
+
from .requestium_mixin import DriverMixin
|
|
14
|
+
from .requestium_response import RequestiumResponse
|
|
15
|
+
|
|
16
|
+
RequestiumChrome = type("RequestiumChrome", (DriverMixin, webdriver.Chrome), {})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Session(requests.Session):
|
|
20
|
+
"""
|
|
21
|
+
Class that adds a Selenium Webdriver and helper methods to a Requests Session.
|
|
22
|
+
|
|
23
|
+
This session class is a normal Requests Session that has the ability to switch back
|
|
24
|
+
and forth between this session and a webdriver, allowing us to run js when needed.
|
|
25
|
+
|
|
26
|
+
Cookie transfer is done with the 'transfer' methods.
|
|
27
|
+
|
|
28
|
+
Header and proxy transfer is done only one time when the driver process starts.
|
|
29
|
+
|
|
30
|
+
Some useful helper methods and object wrappings have been added.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def _start_chrome_browser(self, headless: bool | None = False): # noqa C901
|
|
34
|
+
# TODO @joaqo: Transfer of proxies and headers.
|
|
35
|
+
# https://github.com/tryolabs/requestium/issues/96
|
|
36
|
+
# Not currently supported by chromedriver. Choosing not to use plug-ins
|
|
37
|
+
# for this as I don't want to worry about the extra dependencies and
|
|
38
|
+
# plug-ins don't work in headless mode. :-(
|
|
39
|
+
chrome_options = webdriver.ChromeOptions()
|
|
40
|
+
|
|
41
|
+
if headless:
|
|
42
|
+
chrome_options.add_argument("headless=new")
|
|
43
|
+
|
|
44
|
+
if "binary_location" in self.webdriver_options:
|
|
45
|
+
chrome_options.binary_location = self.webdriver_options["binary_location"]
|
|
46
|
+
|
|
47
|
+
if "arguments" in self.webdriver_options:
|
|
48
|
+
if isinstance(self.webdriver_options["arguments"], list):
|
|
49
|
+
for arg in self.webdriver_options["arguments"]:
|
|
50
|
+
chrome_options.add_argument(arg)
|
|
51
|
+
else:
|
|
52
|
+
msg = f"'arguments' option must be a list, but got {type(self.webdriver_options['arguments']).__name__}"
|
|
53
|
+
raise TypeError(msg)
|
|
54
|
+
|
|
55
|
+
if "extensions" in self.webdriver_options and isinstance(self.webdriver_options["extensions"], list):
|
|
56
|
+
for arg in self.webdriver_options["extensions"]:
|
|
57
|
+
chrome_options.add_extension(arg)
|
|
58
|
+
|
|
59
|
+
if "prefs" in self.webdriver_options:
|
|
60
|
+
prefs = self.webdriver_options["prefs"]
|
|
61
|
+
chrome_options.add_experimental_option("prefs", prefs)
|
|
62
|
+
|
|
63
|
+
experimental_options = self.webdriver_options.get("experimental_options")
|
|
64
|
+
if isinstance(experimental_options, dict):
|
|
65
|
+
for name, value in experimental_options.items():
|
|
66
|
+
chrome_options.add_experimental_option(name, value)
|
|
67
|
+
|
|
68
|
+
# Selenium updated webdriver.Chrome's arg and kwargs, to accept options, service, keep_alive
|
|
69
|
+
# since ChromeService is the only object where webdriver_path is mapped to executable_path, it must be
|
|
70
|
+
# initialized and passed in as a kwarg to RequestiumChrome so it can be passed in as a kwarg
|
|
71
|
+
# when passed into webdriver.Chrome in super(DriverMixin, self).__init__(*args, **kwargs)
|
|
72
|
+
service = ChromeService(executable_path=self.webdriver_path)
|
|
73
|
+
return RequestiumChrome(service=service, options=chrome_options, default_timeout=self.default_timeout)
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
webdriver_path: str | None = None,
|
|
79
|
+
headless: bool | None = None,
|
|
80
|
+
default_timeout: float = 5,
|
|
81
|
+
webdriver_options: dict[str, Any] | None = None,
|
|
82
|
+
driver: DriverMixin | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
super().__init__()
|
|
85
|
+
|
|
86
|
+
if webdriver_options is None:
|
|
87
|
+
webdriver_options = {}
|
|
88
|
+
|
|
89
|
+
self.webdriver_path = webdriver_path
|
|
90
|
+
self.default_timeout = default_timeout
|
|
91
|
+
self.webdriver_options = webdriver_options
|
|
92
|
+
self._driver = driver
|
|
93
|
+
self._last_requests_url: str | None = None
|
|
94
|
+
|
|
95
|
+
if not self._driver:
|
|
96
|
+
self._driver_initializer = functools.partial(self._start_chrome_browser, headless=headless)
|
|
97
|
+
else:
|
|
98
|
+
for name in DriverMixin.__dict__:
|
|
99
|
+
name_private = name.startswith("__") and name.endswith("__")
|
|
100
|
+
name_function = isinstance(DriverMixin.__dict__[name], types.FunctionType)
|
|
101
|
+
name_in_driver = name in dir(self._driver)
|
|
102
|
+
if name_private or not name_function or name_in_driver:
|
|
103
|
+
continue
|
|
104
|
+
self._driver.__dict__[name] = DriverMixin.__dict__[name].__get__(self._driver)
|
|
105
|
+
self._driver.default_timeout = self.default_timeout
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def driver(self) -> DriverMixin:
|
|
109
|
+
if self._driver is None:
|
|
110
|
+
self._driver = self._driver_initializer()
|
|
111
|
+
return self._driver
|
|
112
|
+
|
|
113
|
+
def transfer_session_cookies_to_driver(self, domain: str | None = None) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Copy the Session's cookies into the webdriver.
|
|
116
|
+
|
|
117
|
+
Using the 'domain' parameter we choose the cookies we wish to transfer, we only
|
|
118
|
+
transfer the cookies which belong to that domain. The domain defaults to our last visited
|
|
119
|
+
site if not provided.
|
|
120
|
+
"""
|
|
121
|
+
if not domain and self._last_requests_url:
|
|
122
|
+
domain = tldextract.extract(self._last_requests_url).top_domain_under_public_suffix
|
|
123
|
+
|
|
124
|
+
if not domain:
|
|
125
|
+
msg = "Trying to transfer cookies to selenium without specifying a domain and without having visited any page in the current session"
|
|
126
|
+
raise InvalidCookieDomainException(msg)
|
|
127
|
+
|
|
128
|
+
# Transfer cookies
|
|
129
|
+
for c in [c for c in self.cookies if domain in c.domain]:
|
|
130
|
+
cookie = {"name": c.name, "value": c.value, "path": c.path, "expiry": c.expires, "domain": c.domain}
|
|
131
|
+
|
|
132
|
+
self.driver.ensure_add_cookie({k: v for k, v in cookie.items() if v is not None})
|
|
133
|
+
|
|
134
|
+
def transfer_driver_cookies_to_session(self, *, copy_user_agent: bool | None = True) -> None:
|
|
135
|
+
if copy_user_agent:
|
|
136
|
+
self.copy_user_agent_from_driver()
|
|
137
|
+
|
|
138
|
+
for cookie in self.driver.get_cookies():
|
|
139
|
+
self.cookies.set(cookie["name"], cookie["value"], domain=cookie["domain"])
|
|
140
|
+
|
|
141
|
+
def get(self, *args, **kwargs) -> RequestiumResponse:
|
|
142
|
+
resp = super().get(*args, **kwargs)
|
|
143
|
+
self._last_requests_url = resp.url
|
|
144
|
+
return RequestiumResponse(resp)
|
|
145
|
+
|
|
146
|
+
def post(self, *args, **kwargs) -> RequestiumResponse:
|
|
147
|
+
resp = super().post(*args, **kwargs)
|
|
148
|
+
self._last_requests_url = resp.url
|
|
149
|
+
return RequestiumResponse(resp)
|
|
150
|
+
|
|
151
|
+
def put(self, *args, **kwargs) -> RequestiumResponse:
|
|
152
|
+
resp = super().put(*args, **kwargs)
|
|
153
|
+
self._last_requests_url = resp.url
|
|
154
|
+
return RequestiumResponse(resp)
|
|
155
|
+
|
|
156
|
+
def copy_user_agent_from_driver(self) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Update requests' session user-agent with the driver's user agent.
|
|
159
|
+
|
|
160
|
+
This method will start the browser process if its not already running.
|
|
161
|
+
"""
|
|
162
|
+
selenium_user_agent = self.driver.execute_script("return navigator.userAgent;")
|
|
163
|
+
self.headers.update({"user-agent": selenium_user_agent})
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: requestium
|
|
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
|
|
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
|
|
47
|
+
Classifier: Natural Language :: English
|
|
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
|
|
57
|
+
Description-Content-Type: text/markdown
|
|
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
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
========
|
|
77
|
+
|
|
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)
|
|
83
|
+
|
|
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.
|
|
85
|
+
|
|
86
|
+
The library was created for writing web automation scripts that are written using mostly Requests but that are able to seamlessly switch to Selenium for the JavaScript heavy parts of the website, while maintaining the session.
|
|
87
|
+
|
|
88
|
+
Requestium adds independent improvements to both Requests and Selenium, and every new feature is lazily evaluated, so its useful even if writing scripts that use only Requests or Selenium.
|
|
89
|
+
|
|
90
|
+
Read more about the motivation behind creating this library in this [blopost](https://tryolabs.com/blog/2017/11/22/requestium-integration-layer-requests-selenium-web-automation/).
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
- Enables switching between a Requests' Session and a Selenium webdriver while maintaining the current web session.
|
|
94
|
+
- Integrates Parsel's parser into the library, making xpath, css, and regex much cleaner to write.
|
|
95
|
+
- Improves Selenium's handling of dynamically loading elements.
|
|
96
|
+
- Makes cookie handling more flexible in Selenium.
|
|
97
|
+
- Makes clicking elements in Selenium more reliable.
|
|
98
|
+
- Supports Chromedriver natively plus adding a custom webdriver.
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
```bash
|
|
102
|
+
pip install requestium
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You should then download your preferred Selenium webdriver if you plan to use the Selenium part of Requestium, such as [Chromedriver](https://sites.google.com/a/chromium.org/chromedriver/).
|
|
106
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
First create a session as you would do on Requests, and optionally add arguments for the web-driver if you plan to use one.
|
|
109
|
+
```python
|
|
110
|
+
from requestium import Session, Keys
|
|
111
|
+
|
|
112
|
+
options = {'arguments': ['headless']}
|
|
113
|
+
s = Session(webdriver_path='./chromedriver', default_timeout=15, webdriver_options=options)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Since headless mode is common, there's a shortcut for it by specifying `headless=True`.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from requestium import Session, Keys
|
|
120
|
+
|
|
121
|
+
s = Session(webdriver_path='./chromedriver' headless=True)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
You can also create a Selenium webdriver outside Requestium and have it use that instead:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from selenium import webdriver
|
|
128
|
+
from requestium import Session, Keys
|
|
129
|
+
|
|
130
|
+
firefox_driver = webdriver.Firefox()
|
|
131
|
+
|
|
132
|
+
s = Session(driver=firefox_driver)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
You can also specify a 3rd party Chrome webdriver class and use it by specifying the `browser` argument as well. This will allow, for example, to use [Selenium-Wire](https://github.com/wkeeling/selenium-wire) to get XHR requests of a web page:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from seleniumwire import webdriver
|
|
139
|
+
from requestium import Session, Keys
|
|
140
|
+
|
|
141
|
+
seleniumwire_driver = webdriver.Chrome()
|
|
142
|
+
|
|
143
|
+
s = Session(webdriver_path='./chromedriver', driver=seleniumwire_driver)
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
You don't need to parse the response, it is done automatically when calling xpath, css or re.
|
|
148
|
+
```python
|
|
149
|
+
title = s.get('http://samplesite.com').xpath('//title/text()').extract_first(default='Default Title')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Regex require less boilerplate when compared to Python's standard `re` module.
|
|
153
|
+
```python
|
|
154
|
+
response = s.get('http://samplesite.com/sample_path')
|
|
155
|
+
|
|
156
|
+
# Extracts the first match
|
|
157
|
+
identifier = response.re_first(r'ID_\d\w\d', default='ID_1A1')
|
|
158
|
+
|
|
159
|
+
# Extracts all matches as a list
|
|
160
|
+
users = response.re(r'user_\d\d\d')
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The Session object is just a regular Requests's session object, so you can use all of its methods.
|
|
164
|
+
```python
|
|
165
|
+
s.post('http://www.samplesite.com/sample', data={'field1': 'data1'})
|
|
166
|
+
s.proxies.update({'http': 'http://10.11.4.254:3128', 'https': 'https://10.11.4.252:3128'})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
And you can switch to using the Selenium webdriver to run any js code.
|
|
170
|
+
```python
|
|
171
|
+
s.transfer_session_cookies_to_driver() # You can maintain the session if needed
|
|
172
|
+
s.driver.get('http://www.samplesite.com/sample/process')
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The driver object is a Selenium webdriver object, so you can use any of the normal selenium methods plus new methods added by Requestium.
|
|
176
|
+
```python
|
|
177
|
+
s.driver.find_element("xpath", "//input[@class='user_name']").send_keys('James Bond', Keys.ENTER)
|
|
178
|
+
|
|
179
|
+
# New methods which wait for element to load instead of failing, useful for single page web apps
|
|
180
|
+
s.driver.ensure_element("xpath", "//div[@attribute='button']").click()
|
|
181
|
+
s.driver.ensure_element_by_xpath("//div[@attribute='button']").click()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Requestium also adds xpath, css, and re methods to the Selenium driver object.
|
|
185
|
+
```python
|
|
186
|
+
if s.driver.re(r'ID_\d\w\d some_pattern'):
|
|
187
|
+
print('Found it!')
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
And finally you can switch back to using Requests.
|
|
191
|
+
```python
|
|
192
|
+
s.transfer_driver_cookies_to_session()
|
|
193
|
+
s.post('http://www.samplesite.com/sample2', data={'key1': 'value1'})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Selenium workarounds
|
|
197
|
+
Requestium adds several 'ensure' methods to the driver object, as Selenium is known to be very finicky about selecting elements and cookie handling.
|
|
198
|
+
|
|
199
|
+
### Wait for element
|
|
200
|
+
The `ensure_element` and `ensure_element_by_` methods wait for the element to be loaded in the browser and returns it as soon as it loads. They're named after Selenium's `find_element` and `find_element_by_` methods (which immediately raise an exception if they can't find the element).
|
|
201
|
+
|
|
202
|
+
Requestium can wait for an element to be in any of the following states:
|
|
203
|
+
- present (default)
|
|
204
|
+
- clickable
|
|
205
|
+
- visible
|
|
206
|
+
- invisible (useful for things like waiting for *loading...* gifs to disappear)
|
|
207
|
+
|
|
208
|
+
These methods are very useful for single page web apps where the site is dynamically changing its elements. We usually end up completely replacing our `find_element` and `find_element_by_` calls with `ensure_element` and `ensure_element_by_` calls as they are more flexible.
|
|
209
|
+
|
|
210
|
+
Elements you get using these methods have the new `ensure_click` method which makes the click less prone to failure. This helps with getting through a lot of the problems with Selenium clicking.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
s.driver.ensure_element("xpath", "//li[@class='b1']", state='clickable', timeout=5).ensure_click()
|
|
214
|
+
|
|
215
|
+
# === We also added these methods named in accordance to Selenium's api design ===
|
|
216
|
+
# ensure_element_by_id
|
|
217
|
+
# ensure_element_by_name
|
|
218
|
+
# ensure_element_by_xpath
|
|
219
|
+
# ensure_element_by_link_text
|
|
220
|
+
# ensure_element_by_partial_link_text
|
|
221
|
+
# ensure_element_by_tag_name
|
|
222
|
+
# ensure_element_by_class_name
|
|
223
|
+
# ensure_element_by_css_selector
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Add cookie
|
|
227
|
+
The `ensure_add_cookie` method makes adding cookies much more robust. Selenium needs the browser to be at the cookie's domain before being able to add the cookie, this method offers several workarounds for this. If the browser is not in the cookies domain, it GETs the domain before adding the cookie. It also allows you to override the domain before adding it, and avoid making this GET. The domain can be overridden to `''`, this sets the cookie's domain to whatever domain the driver is currently in.
|
|
228
|
+
|
|
229
|
+
If it can't add the cookie it tries to add it with a less restrictive domain (Eg.: home.site.com -> site.com) before failing.
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
cookie = {"domain": "www.site.com",
|
|
233
|
+
"secure": false,
|
|
234
|
+
"value": "sd2451dgd13",
|
|
235
|
+
"expiry": 1516824855.759154,
|
|
236
|
+
"path": "/",
|
|
237
|
+
"httpOnly": true,
|
|
238
|
+
"name": "sessionid"}
|
|
239
|
+
s.driver.ensure_add_cookie(cookie, override_domain='')
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Considerations
|
|
243
|
+
New features are lazily evaluated, meaning:
|
|
244
|
+
- The Selenium webdriver process is only started if you call the driver object. So if you don't need to use the webdriver, you could use the library with no overhead. Very useful if you just want to use the library for its integration with Parsel.
|
|
245
|
+
- Parsing of the responses is only done if you call the `xpath`, `css`, or `re` methods of the response. So again there is no overhead if you don't need to use this feature.
|
|
246
|
+
|
|
247
|
+
A byproduct of this is that the Selenium webdriver could be used just as a tool to ease in the development of regular Requests code: You can start writing your script using just the Requests' session, and at the last step of the script (the one you are currently working on) transfer the session to the Chrome webdriver. This way, a Chrome process starts in your machine, and acts as a real time "visor" for the last step of your code. You can see in what state your session is currently in, inspect it with Chrome's excellent inspect tools, and decide what's the next step your session object should take. Very useful to try code in an IPython interpreter and see how the site reacts in real time.
|
|
248
|
+
|
|
249
|
+
When `transfer_driver_cookies_to_session` is called, Requestium automatically updates your Requests session user-agent to match that of the browser used in Selenium. This doesn't happen when running Requests without having switched from a Selenium session first though. So if you just want to run Requests but want it to use your browser's user agent instead of the default one (which sites love to block), just run:
|
|
250
|
+
```python
|
|
251
|
+
s.copy_user_agent_from_driver()
|
|
252
|
+
```
|
|
253
|
+
Take into account that doing this will launch a browser process.
|
|
254
|
+
|
|
255
|
+
Note: The Selenium Chrome webdriver doesn't support automatic transfer of proxies from the Session to the Webdriver at the moment.
|
|
256
|
+
|
|
257
|
+
## Comparison with Requests + Selenium + lxml
|
|
258
|
+
A silly working example of a script that runs on Reddit. We'll then show how it compares to using Requests + Selenium + lxml instead of Requestium.
|
|
259
|
+
|
|
260
|
+
### Using Requestium
|
|
261
|
+
```python
|
|
262
|
+
from requestium import Session, Keys
|
|
263
|
+
|
|
264
|
+
# If you want requestium to type your username in the browser for you, write it in here:
|
|
265
|
+
reddit_user_name = ''
|
|
266
|
+
|
|
267
|
+
s = Session('./chromedriver', default_timeout=15)
|
|
268
|
+
s.driver.get('http://reddit.com')
|
|
269
|
+
s.driver.find_element("xpath", "//a[@href='https://www.reddit.com/login']").click()
|
|
270
|
+
|
|
271
|
+
print('Waiting for elements to load...')
|
|
272
|
+
s.driver.ensure_element("class name", "desktop-onboarding-sign-up__form-toggler",
|
|
273
|
+
state='visible').click()
|
|
274
|
+
|
|
275
|
+
if reddit_user_name:
|
|
276
|
+
s.driver.ensure_element('id', 'user_login').send_keys(reddit_user_name)
|
|
277
|
+
s.driver.ensure_element('id', 'passwd_login').send_keys(Keys.BACKSPACE)
|
|
278
|
+
print('Please log-in in the chrome browser')
|
|
279
|
+
|
|
280
|
+
s.driver.ensure_element("class name", "desktop-onboarding__title", timeout=60, state='invisible')
|
|
281
|
+
print('Thanks!')
|
|
282
|
+
|
|
283
|
+
if not reddit_user_name:
|
|
284
|
+
reddit_user_name = s.driver.xpath("//span[@class='user']//text()").extract_first()
|
|
285
|
+
|
|
286
|
+
if reddit_user_name:
|
|
287
|
+
s.transfer_driver_cookies_to_session()
|
|
288
|
+
response = s.get("https://www.reddit.com/user/{}/".format(reddit_user_name))
|
|
289
|
+
cmnt_karma = response.xpath("//span[@class='karma comment-karma']//text()").extract_first()
|
|
290
|
+
reddit_golds_given = response.re_first(r"(\d+) gildings given out")
|
|
291
|
+
print("Comment karma: {}".format(cmnt_karma))
|
|
292
|
+
print("Reddit golds given: {}".format(reddit_golds_given))
|
|
293
|
+
else:
|
|
294
|
+
print("Couldn't get user name")
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Using Requests + Selenium + lxml
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
import re
|
|
301
|
+
from lxml import etree
|
|
302
|
+
from requests import Session
|
|
303
|
+
from selenium import webdriver
|
|
304
|
+
from selenium.common.exceptions import TimeoutException
|
|
305
|
+
from selenium.webdriver.common.keys import Keys
|
|
306
|
+
from selenium.webdriver.common.by import By
|
|
307
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
308
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
309
|
+
|
|
310
|
+
# If you want requestium to type your username in the browser for you, write it in here:
|
|
311
|
+
reddit_user_name = ''
|
|
312
|
+
|
|
313
|
+
driver = webdriver.Chrome('./chromedriver')
|
|
314
|
+
driver.get('http://reddit.com')
|
|
315
|
+
driver.find_element("xpath", "//a[@href='https://www.reddit.com/login']").click()
|
|
316
|
+
|
|
317
|
+
print('Waiting for elements to load...')
|
|
318
|
+
WebDriverWait(driver, 15).until(
|
|
319
|
+
EC.visibility_of_element_located((By.CLASS_NAME, "desktop-onboarding-sign-up__form-toggler"))
|
|
320
|
+
).click()
|
|
321
|
+
|
|
322
|
+
if reddit_user_name:
|
|
323
|
+
WebDriverWait(driver, 15).until(
|
|
324
|
+
EC.presence_of_element_located((By.ID, 'user_login'))
|
|
325
|
+
).send_keys(reddit_user_name)
|
|
326
|
+
driver.find_element('id', 'passwd_login').send_keys(Keys.BACKSPACE)
|
|
327
|
+
print('Please log-in in the chrome browser')
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
WebDriverWait(driver, 3).until(
|
|
331
|
+
EC.presence_of_element_located((By.CLASS_NAME, "desktop-onboarding__title"))
|
|
332
|
+
)
|
|
333
|
+
except TimeoutException:
|
|
334
|
+
pass
|
|
335
|
+
WebDriverWait(driver, 60).until(
|
|
336
|
+
EC.invisibility_of_element_located((By.CLASS_NAME, "desktop-onboarding__title"))
|
|
337
|
+
)
|
|
338
|
+
print('Thanks!')
|
|
339
|
+
|
|
340
|
+
if not reddit_user_name:
|
|
341
|
+
tree = etree.HTML(driver.page_source)
|
|
342
|
+
try:
|
|
343
|
+
reddit_user_name = tree.xpath("//span[@class='user']//text()")[0]
|
|
344
|
+
except IndexError:
|
|
345
|
+
reddit_user_name = None
|
|
346
|
+
|
|
347
|
+
if reddit_user_name:
|
|
348
|
+
s = Session()
|
|
349
|
+
# Reddit will think we are a bot if we have the wrong user agent
|
|
350
|
+
selenium_user_agent = driver.execute_script("return navigator.userAgent;")
|
|
351
|
+
s.headers.update({"user-agent": selenium_user_agent})
|
|
352
|
+
for cookie in driver.get_cookies():
|
|
353
|
+
s.cookies.set(cookie['name'], cookie['value'], domain=cookie['domain'])
|
|
354
|
+
response = s.get("https://www.reddit.com/user/{}/".format(reddit_user_name))
|
|
355
|
+
try:
|
|
356
|
+
cmnt_karma = etree.HTML(response.content).xpath(
|
|
357
|
+
"//span[@class='karma comment-karma']//text()")[0]
|
|
358
|
+
except IndexError:
|
|
359
|
+
cmnt_karma = None
|
|
360
|
+
match = re.search(r"(\d+) gildings given out", str(response.content))
|
|
361
|
+
if match:
|
|
362
|
+
reddit_golds_given = match.group(1)
|
|
363
|
+
else:
|
|
364
|
+
reddit_golds_given = None
|
|
365
|
+
print("Comment karma: {}".format(cmnt_karma))
|
|
366
|
+
print("Reddit golds given: {}".format(reddit_golds_given))
|
|
367
|
+
else:
|
|
368
|
+
print("Couldn't get user name")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Similar Projects
|
|
372
|
+
This project intends to be a drop-in replacement of Requests' Session object, with added functionality. If your use case is a drop in replacement for a Selenium webdriver, but that also has some of Requests' functionality, [Selenium-Requests](https://github.com/cryzed/Selenium-Requests) does just that.
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
Copyright © 2018, [Tryolabs](https://tryolabs.com/). Released under the [BSD 3-Clause](https://github.com/tryolabs/requestium/blob/master/LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
requestium/__init__.py,sha256=BEjUrsq_EjA5osONG3zj3n_x2B2SgemDqY1XHYRkfsQ,283
|
|
2
|
+
requestium/requestium.py,sha256=nsSQEmVFIcRHz0YvoepB3deXy46B06eqrv339O4FLWs,204
|
|
3
|
+
requestium/requestium_mixin.py,sha256=6tdyRPX1qvP_BlZAaKLtTMKHGTFWRku0Ot459gFMwZM,12754
|
|
4
|
+
requestium/requestium_response.py,sha256=5nta1i3x2Ya4tZiYGPznr15Guc36j3p3q8B5p1bddc0,1154
|
|
5
|
+
requestium/requestium_session.py,sha256=Ia5PlswYZQXwwzRkqxqzz2IIHFX4RCSQ8dR3a6lozKU,7051
|
|
6
|
+
requestium-0.5.1.dist-info/licenses/LICENSE,sha256=_KRtksFg3PbSlgNIn1UoPfJu-VboyW2Phdo3oPyosCk,1508
|
|
7
|
+
requestium-0.5.1.dist-info/METADATA,sha256=WWKGloqUs6s2pOIuUT4sJT5-tedCAjTjLDewG3RW0Yg,18231
|
|
8
|
+
requestium-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
requestium-0.5.1.dist-info/top_level.txt,sha256=6lHJKsZ2tUqzyHVJWBZkIAq0OX8aTbMCTwqCd0yHdV0,11
|
|
10
|
+
requestium-0.5.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017, Tryolabs
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requestium
|