browserfetch 0.7.0__tar.gz → 0.8.0__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.
@@ -1,71 +1,71 @@
1
- Metadata-Version: 2.1
2
- Name: browserfetch
3
- Version: 0.7.0
4
- Summary: fetch in Python using your browser!
5
- License: GNU General Public License v3 (GPLv3)
6
- Project-URL: Homepage, https://github.com/5j9/browserfetch
7
- Keywords: browser,fetch,python,cookies
8
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
- Requires-Python: >=3.11
10
- Description-Content-Type: text/x-rst
11
- License-File: LICENSE
12
- Requires-Dist: aiohttp
13
- Requires-Dist: pyperclip
14
-
15
- Fetch using your browser.
16
-
17
- Let the browser manage cookies for you.
18
-
19
- ⚠️ This project is a very simple implementation. Not tested thoroughly. Consider it a proof of concept.
20
-
21
- Usage
22
- -----
23
- 1. You'll run a Python script containing some code like this:
24
-
25
- .. code-block:: python
26
-
27
- from asyncio import gather, new_event_loop
28
-
29
- from browserfetch import fetch, get, post, run_server
30
-
31
-
32
- async def main():
33
- response1, response2, reponse3 = await gather(
34
- get('https://example.com/path1', params={'a': 1}),
35
- fetch('https://example.com/image.png'),
36
- post('https://example.com/path2', data={'a': 1}),
37
- )
38
- # do stuff with retrieved responses
39
-
40
-
41
- loop = new_event_loop()
42
- loop.create_task(start_server())
43
- loop.run_until_complete(main())
44
-
45
-
46
- 2. Open your browser, goto http://example.com (perhaps solve a captcha and log in).
47
- 3. Copy the contents of `browserfetch.js`_ file and paste it in browser's console. (You can use a browser extensions like violentmonkey_/tampermonkey_ to do this step for you.)
48
-
49
- That's it! Your Python script starts handling requests.
50
- The browser tab should remain open of-coarse.
51
-
52
- The server can handle multiple websocket connections from different websites simultaneously.
53
-
54
- How it works
55
- ------------
56
- ``browserfetch`` communicates with your browser using a websocket. The ``fetch`` function just passes the request to browser and it is the browser that handles the actual request. Response data is sent back to Python using the same WebSocket connection.
57
-
58
- Motivations
59
- -----------
60
- * `browser_cookie3 stopped working on Chrome-based browsers`_. There is a workaround: ShadowCopy, but it requires admin privilege.
61
- * Another issue with browser_cookie's approach is that it retrieves cookies from cookie files, but these files are not updated instantly. Thus, you might have to wait or retry a few times before you can successfully access newly set cookies.
62
- * ShadowCopying and File access are slow and inefficient operations.
63
-
64
- Downsides
65
- ---------
66
- * Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
67
-
68
- .. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
69
- .. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
70
- .. _violentmonkey: https://github.com/violentmonkey/violentmonkey
71
- .. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
1
+ Metadata-Version: 2.1
2
+ Name: browserfetch
3
+ Version: 0.8.0
4
+ Summary: fetch in Python using your browser!
5
+ Keywords: browser,fetch,python,cookies
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/x-rst
8
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
+ Requires-Dist: aiohttp
10
+ Requires-Dist: pyperclip
11
+ Project-URL: Homepage, https://github.com/5j9/browserfetch
12
+
13
+ Fetch using your browser.
14
+
15
+ Let the browser manage cookies for you.
16
+
17
+ ⚠️ Incomplete. Not tested thoroughly. Consider using `Playwright`_, especially for more complex scenarios.
18
+
19
+ Usage
20
+ -----
21
+ 1. You'll run a Python script containing some code like this:
22
+
23
+ .. code-block:: python
24
+
25
+ from asyncio import gather, new_event_loop
26
+
27
+ from browserfetch import fetch, get, post, run_server
28
+
29
+
30
+ async def main():
31
+ response1, response2, reponse3 = await gather(
32
+ get('https://example.com/path1', params={'a': 1}),
33
+ fetch('https://example.com/image.png'),
34
+ post('https://example.com/path2', data={'a': 1}),
35
+ )
36
+ # do stuff with retrieved responses
37
+
38
+
39
+ loop = new_event_loop()
40
+ loop.create_task(start_server())
41
+ loop.run_until_complete(main())
42
+
43
+
44
+ 2. Open your browser, goto http://example.com (perhaps solve a captcha and log in).
45
+ 3. Copy the contents of `browserfetch.js`_ file and paste it in browser's console. (You can use a browser extensions like violentmonkey_/tampermonkey_ to do this step for you.)
46
+
47
+ That's it! Your Python script starts handling requests.
48
+ The browser tab should remain open of-coarse.
49
+
50
+ The server can handle multiple websocket connections from different websites simultaneously.
51
+
52
+ How it works
53
+ ------------
54
+ ``browserfetch`` communicates with your browser using a websocket. The ``fetch`` function just passes the request to browser and it is the browser that handles the actual request. Response data is sent back to Python using the same WebSocket connection.
55
+
56
+ Motivations
57
+ -----------
58
+ * `browser_cookie3 stopped working on Chrome-based browsers`_. There is a workaround: ShadowCopy, but it requires admin privilege.
59
+ * Another issue with browser_cookie's approach is that it retrieves cookies from cookie files, but these files are not updated instantly. Thus, you might have to wait or retry a few times before you can successfully access newly set cookies.
60
+ * ShadowCopying and File access are slow and inefficient operations.
61
+
62
+ Downsides
63
+ ---------
64
+ * Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
65
+
66
+ .. _playwright: https://playwright.dev/python/docs/intro
67
+ .. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
68
+ .. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
69
+ .. _violentmonkey: https://github.com/violentmonkey/violentmonkey
70
+ .. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
71
+
@@ -2,7 +2,7 @@ Fetch using your browser.
2
2
 
3
3
  Let the browser manage cookies for you.
4
4
 
5
- ⚠️ This project is a very simple implementation. Not tested thoroughly. Consider it a proof of concept.
5
+ ⚠️ Incomplete. Not tested thoroughly. Consider using `Playwright`_, especially for more complex scenarios.
6
6
 
7
7
  Usage
8
8
  -----
@@ -51,6 +51,7 @@ Downsides
51
51
  ---------
52
52
  * Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
53
53
 
54
+ .. _playwright: https://playwright.dev/python/docs/intro
54
55
  .. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
55
56
  .. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
56
57
  .. _violentmonkey: https://github.com/violentmonkey/violentmonkey
@@ -1,13 +1,13 @@
1
- __version__ = '0.7.0'
1
+ __version__ = '0.8.0'
2
2
 
3
3
  import atexit
4
4
  from asyncio import (
5
5
  AbstractEventLoop,
6
6
  CancelledError,
7
7
  Event,
8
+ Task,
8
9
  get_running_loop,
9
10
  wait_for,
10
- Task,
11
11
  )
12
12
  from collections import defaultdict
13
13
  from dataclasses import dataclass
@@ -22,9 +22,9 @@ from aiohttp.web_runner import AppRunner, TCPSite
22
22
 
23
23
  logger = getLogger(__name__)
24
24
  # maps host to its host_ready event or its websocket
25
- hosts: dict[
26
- str, Event | WebSocketResponse | ClientWebSocketResponse
27
- ] = defaultdict(Event)
25
+ hosts: dict[str, Event | WebSocketResponse | ClientWebSocketResponse] = (
26
+ defaultdict(Event)
27
+ )
28
28
  # maps response event id to its response event or response dict
29
29
  responses: dict[int, Event | dict] = {}
30
30
 
@@ -113,6 +113,7 @@ async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
113
113
 
114
114
 
115
115
  routes = RouteTableDef()
116
+ PROTOCOL = '2'
116
117
 
117
118
 
118
119
  @routes.get('/ws')
@@ -120,7 +121,10 @@ async def _(request):
120
121
  ws = WebSocketResponse()
121
122
  await ws.prepare(request)
122
123
 
123
- host = await ws.receive_str()
124
+ host, _, version = (await ws.receive_str()).partition(' ')
125
+ assert (
126
+ version == PROTOCOL
127
+ ), f'JavaScript protocol version: {version}, expected: {PROTOCOL}'
124
128
  logger.info('registering host %s', host)
125
129
 
126
130
  ws_or_e = hosts[host]
@@ -183,6 +187,18 @@ async def relay_client(server_host, server_port):
183
187
  return
184
188
 
185
189
 
190
+ async def evaluate(
191
+ string: str,
192
+ host: str,
193
+ timeout: int | float = 95,
194
+ ):
195
+ """Evaluate string in browser context and return JSON.stringify(result)."""
196
+ d = await _request(
197
+ host, {'action': 'eval', 'string': string, 'timeout': timeout}, None
198
+ )
199
+ return d['result']
200
+
201
+
186
202
  async def fetch(
187
203
  url: str,
188
204
  *,
@@ -209,6 +225,7 @@ async def fetch(
209
225
  d = await _request(
210
226
  host,
211
227
  {
228
+ 'action': 'fetch',
212
229
  'url': url,
213
230
  'options': options,
214
231
  'timeout': timeout,
@@ -288,7 +305,7 @@ def _shutdown_server(loop: AbstractEventLoop):
288
305
 
289
306
 
290
307
  def _cancel_relay_task(loop: AbstractEventLoop, task: Task):
291
- logger.info(f'cancelling relay task')
308
+ logger.info('cancelling relay task')
292
309
  task.cancel()
293
310
  try:
294
311
  loop.run_until_complete(task)
@@ -0,0 +1,120 @@
1
+ // ==UserScript==
2
+ // @name browserfetch
3
+ // @namespace https://github.com/5j9/browserfetch
4
+ // @match https://example.com/
5
+ // @grant GM_registerMenuCommand
6
+ // ==/UserScript==
7
+ (() => {
8
+ /**
9
+ * @param {Blob} body
10
+ * @param {j} Object
11
+ * @returns {Promise<Blob>}
12
+ */
13
+ async function doFetch(req, body) {
14
+ var returnData, response;
15
+ var options = req['options'] || {};
16
+
17
+ if (req['timeout']) {
18
+ options.signal = AbortSignal.timeout(req['timeout'] * 1000);
19
+ }
20
+
21
+ if (body !== null) {
22
+ options.body = body;
23
+ }
24
+
25
+ try {
26
+ var r = await fetch(req['url'], options);
27
+ returnData = {
28
+ 'event_id': req['event_id'],
29
+ 'headers': Object.fromEntries([...r.headers]),
30
+ 'ok': r.ok,
31
+ 'redirected': r.redirected,
32
+ 'status': r.status,
33
+ 'status_text': r.statusText,
34
+ 'type': r.type,
35
+ 'url': r.url
36
+ };
37
+ response = await r.blob();
38
+ } catch (err) {
39
+ returnData = {
40
+ 'event_id': req['event_id'],
41
+ 'error': err.toString()
42
+ };
43
+ response = "";
44
+ };
45
+ return new Blob([new TextEncoder().encode(JSON.stringify(returnData)), "\0", response]);
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {String} s
51
+ * @returns {Promise<Uint8Array>}
52
+ */
53
+ async function doEval(req) {
54
+ var evalled, resp;
55
+ try {
56
+ evalled = eval(req['string']);
57
+ resp = {'result': evalled, 'event_id': req['event_id']};
58
+ } catch (err) {
59
+ resp = {'result': err.toString(), 'event_id': req['event_id']};
60
+ }
61
+ return new TextEncoder().encode(JSON.stringify(resp));
62
+ }
63
+
64
+ /**
65
+ *
66
+ * @param {ArrayBuffer} d
67
+ * @returns {Array.<{binaryPart: Uint8Array, jsonPart: Object}>}
68
+ */
69
+ function parseData(d) {
70
+ var blob, jArray;
71
+ var dArray = new Uint8Array(d);
72
+ var nullIndex = dArray.indexOf(0);
73
+ if (nullIndex === -1) {
74
+ blob = null;
75
+ jArray = dArray;
76
+ } else {
77
+ blob = dArray.slice(nullIndex + 1);
78
+ jArray = dArray.slice(0, nullIndex)
79
+ }
80
+
81
+ return [blob, JSON.parse(new TextDecoder().decode(jArray))]
82
+ }
83
+
84
+ function connect() {
85
+ var protocol = '2'
86
+ var ws = new WebSocket("ws://127.0.0.1:9404/ws");
87
+ ws.binaryType = "arraybuffer";
88
+
89
+ ws.onopen = () => {
90
+ ws.send(location.host + ' ' + protocol);
91
+ }
92
+
93
+ ws.onclose = () => {
94
+ console.error('browserfetch: WebSocket was closed; will retry in 5 seconds');
95
+ setTimeout(connect, 5000);
96
+ };
97
+
98
+ ws.onmessage = async (evt) => {
99
+ var result, j, b;
100
+ [b, j] = parseData(evt.data);
101
+ switch (j['action']) {
102
+ case 'fetch':
103
+ result = await doFetch(j, b);
104
+ break;
105
+ case 'eval':
106
+ result = await doEval(j);
107
+ }
108
+ ws.send(result);
109
+ }
110
+ };
111
+
112
+ if (window.GM_registerMenuCommand) {
113
+ GM_registerMenuCommand(
114
+ 'connect to browserfetch',
115
+ connect
116
+ );
117
+ } else {
118
+ connect();
119
+ }
120
+ })();
@@ -1,9 +1,6 @@
1
1
  [build-system]
2
- requires = [
3
- "setuptools>=66.1.0",
4
- "wheel",
5
- ]
6
- build-backend = "setuptools.build_meta"
2
+ requires = ['flit_core >=3.2,<4']
3
+ build-backend = 'flit_core.buildapi'
7
4
 
8
5
  [project]
9
6
  name = "browserfetch"
@@ -33,18 +30,18 @@ content-type = "text/x-rst"
33
30
  [project.urls]
34
31
  Homepage = "https://github.com/5j9/browserfetch"
35
32
 
36
- [tool.setuptools]
37
- packages = ["browserfetch"]
38
- zip-safe = false
39
- include-package-data = false
40
-
41
- [tool.setuptools.dynamic.version]
42
- attr = "browserfetch.__version__"
43
-
44
- [tool.setuptools.package-data]
45
- browserfetch = ["*.js"]
46
-
47
33
  [tool.ruff]
48
- format.quote-style = 'single'
49
- line-length = 79
50
- isort.combine-as-imports = true
34
+ line-length = 79
35
+ format.quote-style = 'single'
36
+ lint.isort.combine-as-imports = true
37
+ lint.extend-select = [
38
+ 'I', # isort
39
+ 'UP', # pyupgrade
40
+ ]
41
+ lint.ignore = [
42
+ 'UP027', # list comprehensions are faster than generator expressions
43
+ 'E721', # Do not compare types, use `isinstance()`
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ addopts = '--quiet --tb=short'
@@ -1,75 +0,0 @@
1
- // ==UserScript==
2
- // @name browserfetch
3
- // @namespace https://github.com/5j9/browserfetch
4
- // @match https://example.com/
5
- // @grant GM_registerMenuCommand
6
- // ==/UserScript==
7
- (() => {
8
- function connect() {
9
- var ws = new WebSocket("ws://127.0.0.1:9404/ws");
10
- ws.binaryType = "arraybuffer";
11
-
12
- ws.onopen = () => {
13
- ws.send(location.host);
14
- }
15
-
16
- ws.onclose = function () {
17
- console.error('browserfetch: WebSocket was closed; will retry in 5 seconds');
18
- setTimeout(connect, 5000);
19
- };
20
-
21
- ws.onmessage = async (evt) => {
22
- var returnData, responseBlob, body, jArray;
23
- var requestArray = new Uint8Array(evt.data);
24
- var nullIndex = requestArray.indexOf(0);
25
- if (nullIndex === -1) {
26
- body = null;
27
- jArray = requestArray;
28
- } else {
29
- body = requestArray.slice(nullIndex + 1);
30
- jArray = requestArray.slice(0, nullIndex)
31
- }
32
- var j = JSON.parse(new TextDecoder().decode(jArray));
33
- var options = j['options'] || {};
34
-
35
- if (j['timeout']) {
36
- options.signal = AbortSignal.timeout(j['timeout'] * 1000);
37
- }
38
-
39
- if (body !== null) {
40
- options.body = body;
41
- }
42
-
43
- try {
44
- var r = await fetch(j['url'], options);
45
- returnData = {
46
- 'event_id': j['event_id'],
47
- 'headers': Object.fromEntries([...r.headers]),
48
- 'ok': r.ok,
49
- 'redirected': r.redirected,
50
- 'status': r.status,
51
- 'status_text': r.statusText,
52
- 'type': r.type,
53
- 'url': r.url
54
- };
55
- responseBlob = await r.blob();
56
- } catch (err) {
57
- returnData = {
58
- 'event_id': j['event_id'],
59
- 'error': err.toString()
60
- };
61
- responseBlob = "";
62
- };
63
- ws.send(new Blob([new TextEncoder().encode(JSON.stringify(returnData)), "\0", responseBlob]));
64
- }
65
- };
66
-
67
- if (window.GM_registerMenuCommand) {
68
- GM_registerMenuCommand(
69
- 'connect to browserfetch',
70
- connect
71
- );
72
- } else {
73
- connect();
74
- }
75
- })();
@@ -1,71 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: browserfetch
3
- Version: 0.7.0
4
- Summary: fetch in Python using your browser!
5
- License: GNU General Public License v3 (GPLv3)
6
- Project-URL: Homepage, https://github.com/5j9/browserfetch
7
- Keywords: browser,fetch,python,cookies
8
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
- Requires-Python: >=3.11
10
- Description-Content-Type: text/x-rst
11
- License-File: LICENSE
12
- Requires-Dist: aiohttp
13
- Requires-Dist: pyperclip
14
-
15
- Fetch using your browser.
16
-
17
- Let the browser manage cookies for you.
18
-
19
- ⚠️ This project is a very simple implementation. Not tested thoroughly. Consider it a proof of concept.
20
-
21
- Usage
22
- -----
23
- 1. You'll run a Python script containing some code like this:
24
-
25
- .. code-block:: python
26
-
27
- from asyncio import gather, new_event_loop
28
-
29
- from browserfetch import fetch, get, post, run_server
30
-
31
-
32
- async def main():
33
- response1, response2, reponse3 = await gather(
34
- get('https://example.com/path1', params={'a': 1}),
35
- fetch('https://example.com/image.png'),
36
- post('https://example.com/path2', data={'a': 1}),
37
- )
38
- # do stuff with retrieved responses
39
-
40
-
41
- loop = new_event_loop()
42
- loop.create_task(start_server())
43
- loop.run_until_complete(main())
44
-
45
-
46
- 2. Open your browser, goto http://example.com (perhaps solve a captcha and log in).
47
- 3. Copy the contents of `browserfetch.js`_ file and paste it in browser's console. (You can use a browser extensions like violentmonkey_/tampermonkey_ to do this step for you.)
48
-
49
- That's it! Your Python script starts handling requests.
50
- The browser tab should remain open of-coarse.
51
-
52
- The server can handle multiple websocket connections from different websites simultaneously.
53
-
54
- How it works
55
- ------------
56
- ``browserfetch`` communicates with your browser using a websocket. The ``fetch`` function just passes the request to browser and it is the browser that handles the actual request. Response data is sent back to Python using the same WebSocket connection.
57
-
58
- Motivations
59
- -----------
60
- * `browser_cookie3 stopped working on Chrome-based browsers`_. There is a workaround: ShadowCopy, but it requires admin privilege.
61
- * Another issue with browser_cookie's approach is that it retrieves cookies from cookie files, but these files are not updated instantly. Thus, you might have to wait or retry a few times before you can successfully access newly set cookies.
62
- * ShadowCopying and File access are slow and inefficient operations.
63
-
64
- Downsides
65
- ---------
66
- * Setting up ``browserfetch`` is more cumbersome since it requires running a Python server and also injecting a small script into the webpage. Using ``browser_cookie3`` might be a better choice if there are many websites that you need to communicate with.
67
-
68
- .. _`browser_cookie3 stopped working on Chrome-based browsers`: https://github.com/borisbabic/browser_cookie3/issues/180
69
- .. _tampermonkey: https://github.com/Tampermonkey/tampermonkey
70
- .. _violentmonkey: https://github.com/violentmonkey/violentmonkey
71
- .. _browserfetch.js: https://github.com/5j9/browserfetch/blob/master/browserfetch/browserfetch.js
@@ -1,12 +0,0 @@
1
- LICENSE
2
- README.rst
3
- pyproject.toml
4
- browserfetch/__init__.py
5
- browserfetch/__main__.py
6
- browserfetch/browserfetch.js
7
- browserfetch.egg-info/PKG-INFO
8
- browserfetch.egg-info/SOURCES.txt
9
- browserfetch.egg-info/dependency_links.txt
10
- browserfetch.egg-info/not-zip-safe
11
- browserfetch.egg-info/requires.txt
12
- browserfetch.egg-info/top_level.txt
@@ -1,2 +0,0 @@
1
- aiohttp
2
- pyperclip
@@ -1 +0,0 @@
1
- browserfetch
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
File without changes