browserfetch 0.9.0__tar.gz → 0.10.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.
- {browserfetch-0.9.0 → browserfetch-0.10.1}/PKG-INFO +2 -1
- {browserfetch-0.9.0 → browserfetch-0.10.1}/browserfetch/__init__.py +78 -32
- browserfetch-0.10.1/browserfetch/__main__.py +36 -0
- {browserfetch-0.9.0 → browserfetch-0.10.1}/browserfetch/browserfetch.js +10 -4
- {browserfetch-0.9.0 → browserfetch-0.10.1}/pyproject.toml +39 -33
- browserfetch-0.10.1/tests/test_init.py +25 -0
- browserfetch-0.9.0/browserfetch/__main__.py +0 -24
- {browserfetch-0.9.0 → browserfetch-0.10.1}/LICENSE +0 -0
- {browserfetch-0.9.0 → browserfetch-0.10.1}/README.rst +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: browserfetch
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: fetch in Python using your browser!
|
|
5
5
|
Keywords: browser,fetch,python,cookies
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -8,6 +8,7 @@ Description-Content-Type: text/x-rst
|
|
|
8
8
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
9
9
|
Requires-Dist: aiohttp
|
|
10
10
|
Requires-Dist: pyperclip
|
|
11
|
+
Requires-Dist: cyclopts
|
|
11
12
|
Project-URL: Homepage, https://github.com/5j9/browserfetch
|
|
12
13
|
|
|
13
14
|
Fetch using your browser.
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
__version__ = '0.
|
|
2
|
-
|
|
1
|
+
__version__ = '0.10.1'
|
|
2
|
+
__all__ = [
|
|
3
|
+
'BrowserError',
|
|
4
|
+
'evaluate',
|
|
5
|
+
'fetch',
|
|
6
|
+
'get',
|
|
7
|
+
'post',
|
|
8
|
+
'Response',
|
|
9
|
+
'start_server',
|
|
10
|
+
]
|
|
3
11
|
import atexit
|
|
4
12
|
from asyncio import (
|
|
5
13
|
AbstractEventLoop,
|
|
@@ -11,20 +19,27 @@ from asyncio import (
|
|
|
11
19
|
)
|
|
12
20
|
from collections import defaultdict
|
|
13
21
|
from dataclasses import dataclass
|
|
22
|
+
from html import escape
|
|
14
23
|
from json import dumps, loads
|
|
15
24
|
from logging import getLogger
|
|
16
|
-
from
|
|
17
|
-
from urllib.parse import urlencode
|
|
25
|
+
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
|
|
18
26
|
|
|
19
27
|
from aiohttp import ClientSession, ClientWebSocketResponse
|
|
20
|
-
from aiohttp.web import
|
|
28
|
+
from aiohttp.web import (
|
|
29
|
+
Application,
|
|
30
|
+
Request,
|
|
31
|
+
Response as _Response,
|
|
32
|
+
RouteTableDef,
|
|
33
|
+
WebSocketResponse,
|
|
34
|
+
)
|
|
21
35
|
from aiohttp.web_runner import AppRunner, TCPSite
|
|
22
36
|
|
|
23
37
|
logger = getLogger(__name__)
|
|
24
38
|
# maps host to its host_ready event or its websocket
|
|
25
|
-
hosts:
|
|
26
|
-
|
|
27
|
-
)
|
|
39
|
+
hosts: defaultdict[
|
|
40
|
+
str, Event | WebSocketResponse | ClientWebSocketResponse
|
|
41
|
+
] = defaultdict(Event)
|
|
42
|
+
|
|
28
43
|
# maps response event id to its response event or response dict
|
|
29
44
|
responses: dict[int, Event | dict] = {}
|
|
30
45
|
|
|
@@ -81,12 +96,15 @@ async def _request(
|
|
|
81
96
|
if body is not None:
|
|
82
97
|
bytes_ += b'\0' + body
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
value = hosts[host]
|
|
100
|
+
match value:
|
|
101
|
+
case Event():
|
|
102
|
+
# wait for the Event to be turned into a websocket response
|
|
103
|
+
await value.wait()
|
|
104
|
+
ws = hosts[host]
|
|
105
|
+
await ws.send_bytes(bytes_) # type: ignore
|
|
106
|
+
case WebSocketResponse() | ClientWebSocketResponse():
|
|
107
|
+
await value.send_bytes(bytes_)
|
|
90
108
|
|
|
91
109
|
try:
|
|
92
110
|
await wait_for(response_ready.wait(), data['timeout'])
|
|
@@ -94,7 +112,8 @@ async def _request(
|
|
|
94
112
|
responses.pop(event_id, None)
|
|
95
113
|
raise
|
|
96
114
|
|
|
97
|
-
return
|
|
115
|
+
# this must return a dict at this point, not an Event
|
|
116
|
+
return responses.pop(event_id) # type: ignore
|
|
98
117
|
|
|
99
118
|
|
|
100
119
|
async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
|
|
@@ -105,15 +124,17 @@ async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
|
|
|
105
124
|
j['body'] = body
|
|
106
125
|
event_id = j.pop('event_id')
|
|
107
126
|
try:
|
|
108
|
-
|
|
127
|
+
# We expect only one response to be recieved for each event,
|
|
128
|
+
# therefore this must be an Event, not a dict.
|
|
129
|
+
response_ready: Event = responses[event_id] # type: ignore
|
|
109
130
|
except KeyError: # lock has reached timeout already
|
|
110
131
|
continue
|
|
111
132
|
responses[event_id] = j
|
|
112
|
-
|
|
133
|
+
response_ready.set()
|
|
113
134
|
|
|
114
135
|
|
|
115
136
|
routes = RouteTableDef()
|
|
116
|
-
PROTOCOL = '
|
|
137
|
+
PROTOCOL = '3'
|
|
117
138
|
|
|
118
139
|
|
|
119
140
|
@routes.get('/ws')
|
|
@@ -121,10 +142,10 @@ async def _(request):
|
|
|
121
142
|
ws = WebSocketResponse()
|
|
122
143
|
await ws.prepare(request)
|
|
123
144
|
|
|
124
|
-
|
|
125
|
-
assert (
|
|
126
|
-
version
|
|
127
|
-
)
|
|
145
|
+
version, _, host = (await ws.receive_str()).partition(' ')
|
|
146
|
+
assert version == PROTOCOL, (
|
|
147
|
+
f'JavaScript protocol version: {version}, expected: {PROTOCOL}'
|
|
148
|
+
)
|
|
128
149
|
logger.info('registering host %s', host)
|
|
129
150
|
|
|
130
151
|
ws_or_e = hosts[host]
|
|
@@ -135,13 +156,13 @@ async def _(request):
|
|
|
135
156
|
try:
|
|
136
157
|
await receive_responses(ws)
|
|
137
158
|
except TypeError:
|
|
138
|
-
logger.info('
|
|
159
|
+
logger.info('WebSocket was closed by browser')
|
|
139
160
|
hosts[host] = Event()
|
|
140
161
|
return ws
|
|
141
162
|
|
|
142
163
|
|
|
143
164
|
@routes.get('/relay')
|
|
144
|
-
async def _(request):
|
|
165
|
+
async def _(request: Request) -> WebSocketResponse:
|
|
145
166
|
ws = WebSocketResponse()
|
|
146
167
|
await ws.prepare(request)
|
|
147
168
|
|
|
@@ -149,13 +170,15 @@ async def _(request):
|
|
|
149
170
|
try:
|
|
150
171
|
bytes_ = await ws.receive_bytes()
|
|
151
172
|
except TypeError: # ws closed
|
|
152
|
-
return
|
|
173
|
+
return ws
|
|
153
174
|
data, null, body = bytes_.partition(b'\0')
|
|
154
175
|
data = loads(data)
|
|
155
176
|
relay_event_id = data['event_id']
|
|
156
177
|
|
|
157
178
|
try:
|
|
158
|
-
r = await _request(
|
|
179
|
+
r: dict = await _request(
|
|
180
|
+
data.pop('host'), data, body if null else None
|
|
181
|
+
)
|
|
159
182
|
except TimeoutError:
|
|
160
183
|
r = {'error': 'TimeoutError in relay'}
|
|
161
184
|
|
|
@@ -164,6 +187,22 @@ async def _(request):
|
|
|
164
187
|
await ws.send_bytes(dumps(r).encode() + b'\0' + body)
|
|
165
188
|
|
|
166
189
|
|
|
190
|
+
@routes.get('/')
|
|
191
|
+
async def _(_) -> _Response:
|
|
192
|
+
hosts_html = '\n'.join(
|
|
193
|
+
[f'<li>{k}: {escape(str(v))}</li>' for k, v in hosts.items()]
|
|
194
|
+
)
|
|
195
|
+
responses_html = '\n'.join(
|
|
196
|
+
[f'<li>{k}: {escape(str(v))}</li>' for k, v in responses.items()]
|
|
197
|
+
)
|
|
198
|
+
return _Response(
|
|
199
|
+
body='<meta charset="utf-8">\n<title>browserfetch</title>\n'
|
|
200
|
+
f'Hosts:\n{hosts_html}\n'
|
|
201
|
+
f'Responses:\n{responses_html}',
|
|
202
|
+
content_type='text/html',
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
167
206
|
async def relay_client(server_host, server_port):
|
|
168
207
|
async with ClientSession() as session:
|
|
169
208
|
relay_url = f'ws://{server_host}:{server_port}/relay'
|
|
@@ -221,7 +260,14 @@ async def fetch(
|
|
|
221
260
|
:return: a dict of response values.
|
|
222
261
|
"""
|
|
223
262
|
if params is not None:
|
|
224
|
-
|
|
263
|
+
parsed_url = urlparse(url)
|
|
264
|
+
url = urlunparse(
|
|
265
|
+
parsed_url._replace(
|
|
266
|
+
query=urlencode(
|
|
267
|
+
parse_qsl(parsed_url.query) + [*params.items()]
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
225
271
|
|
|
226
272
|
d = await _request(
|
|
227
273
|
host,
|
|
@@ -261,14 +307,14 @@ async def post(
|
|
|
261
307
|
url: str,
|
|
262
308
|
*,
|
|
263
309
|
params: dict | None = None,
|
|
264
|
-
data
|
|
310
|
+
data=None,
|
|
265
311
|
form: dict | None = None,
|
|
266
312
|
timeout: int | float = 95,
|
|
267
313
|
options: dict | None = None,
|
|
268
314
|
host: str | None = None,
|
|
269
315
|
) -> Response:
|
|
270
316
|
if options is None:
|
|
271
|
-
options
|
|
317
|
+
options = {'method': 'POST'}
|
|
272
318
|
else:
|
|
273
319
|
options['method'] = 'POST'
|
|
274
320
|
|
|
@@ -281,12 +327,12 @@ async def post(
|
|
|
281
327
|
body = dumps(data).encode()
|
|
282
328
|
headers = options.setdefault('headers', {})
|
|
283
329
|
headers['Content-Type'] = 'application/json'
|
|
284
|
-
|
|
285
|
-
if form is not None:
|
|
286
|
-
assert data is None
|
|
330
|
+
elif form is not None:
|
|
287
331
|
body = urlencode(form).encode()
|
|
288
332
|
headers = options.setdefault('headers', {})
|
|
289
333
|
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
334
|
+
else:
|
|
335
|
+
body = None
|
|
290
336
|
|
|
291
337
|
return await fetch(
|
|
292
338
|
url,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def read_js(host_name_generator: str | None):
|
|
5
|
+
js = (Path(__file__).parent / 'browserfetch.js').read_bytes().decode()
|
|
6
|
+
if host_name_generator is not None:
|
|
7
|
+
return js.replace(
|
|
8
|
+
'async function generateHostName() { return location.host };',
|
|
9
|
+
host_name_generator,
|
|
10
|
+
1,
|
|
11
|
+
)
|
|
12
|
+
return js
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def copyjs(*, host_name_generator: str | None = None):
|
|
16
|
+
"""Copy contents of browserfetch.js to clipboard.
|
|
17
|
+
|
|
18
|
+
`host_name_generator` should be a string containing
|
|
19
|
+
an async JavaScript function named `generateHostName()`. This file's
|
|
20
|
+
contents will be copied into the generated js script and will be used
|
|
21
|
+
to generate a host name for connections. The default
|
|
22
|
+
`generateHostName` function returns `location.host`.
|
|
23
|
+
"""
|
|
24
|
+
from pyperclip import copy
|
|
25
|
+
|
|
26
|
+
copy(read_js(host_name_generator))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == '__main__':
|
|
30
|
+
from cyclopts import App
|
|
31
|
+
|
|
32
|
+
from browserfetch import __version__
|
|
33
|
+
|
|
34
|
+
app = App(version=__version__)
|
|
35
|
+
app.command(copyjs)
|
|
36
|
+
app()
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// @grant GM_registerMenuCommand
|
|
6
6
|
// ==/UserScript==
|
|
7
7
|
// @ts-check
|
|
8
|
-
(() => {
|
|
8
|
+
(async () => {
|
|
9
9
|
/**
|
|
10
10
|
* @param {Uint8Array | null} body
|
|
11
11
|
* @param {Object} req
|
|
@@ -93,13 +93,19 @@
|
|
|
93
93
|
return [blob, JSON.parse(new TextDecoder().decode(jArray))]
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
async function generateHostName() { return location.host };
|
|
97
|
+
var hostName;
|
|
98
|
+
|
|
96
99
|
function connect() {
|
|
97
|
-
var protocol = '
|
|
100
|
+
var protocol = '3'
|
|
98
101
|
var ws = new WebSocket("ws://127.0.0.1:9404/ws");
|
|
99
102
|
ws.binaryType = "arraybuffer";
|
|
100
103
|
|
|
101
|
-
ws.onopen = () => {
|
|
102
|
-
|
|
104
|
+
ws.onopen = async () => {
|
|
105
|
+
if (!hostName) {
|
|
106
|
+
hostName = await generateHostName();
|
|
107
|
+
}
|
|
108
|
+
ws.send(protocol + ' ' + hostName);
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
ws.onclose = () => {
|
|
@@ -1,40 +1,42 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ['flit_core >=3.2,<4']
|
|
3
|
-
build-backend = 'flit_core.buildapi'
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "browserfetch"
|
|
7
|
-
description = "fetch in Python using your browser! "
|
|
8
|
-
keywords = ["browser", "fetch", "python", "cookies"]
|
|
9
|
-
classifiers = [
|
|
10
|
-
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
11
|
-
]
|
|
12
|
-
requires-python = ">=3.11"
|
|
13
|
-
dynamic = ["version"]
|
|
14
|
-
dependencies = [
|
|
15
|
-
"aiohttp",
|
|
16
|
-
"pyperclip",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['flit_core >=3.2,<4']
|
|
3
|
+
build-backend = 'flit_core.buildapi'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "browserfetch"
|
|
7
|
+
description = "fetch in Python using your browser! "
|
|
8
|
+
keywords = ["browser", "fetch", "python", "cookies"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
11
|
+
]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"aiohttp",
|
|
16
|
+
"pyperclip",
|
|
17
|
+
"cyclopts",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[[authors]]
|
|
21
|
+
name = "5j9"
|
|
22
|
+
email = "5j9@users.noreply.github.com"
|
|
23
|
+
|
|
24
|
+
[project.license]
|
|
25
|
+
text = "GNU General Public License v3 (GPLv3)"
|
|
26
|
+
|
|
27
|
+
[project.readme]
|
|
28
|
+
file = "README.rst"
|
|
29
|
+
content-type = "text/x-rst"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/5j9/browserfetch"
|
|
33
|
+
|
|
34
|
+
[tool.ruff]
|
|
34
35
|
line-length = 79
|
|
35
36
|
format.quote-style = 'single'
|
|
36
37
|
lint.isort.combine-as-imports = true
|
|
37
38
|
lint.extend-select = [
|
|
39
|
+
'W605', # invalid-escape-sequence
|
|
38
40
|
'FA', # flake8-future-annotations
|
|
39
41
|
'I', # isort
|
|
40
42
|
'UP', # pyupgrade
|
|
@@ -54,3 +56,7 @@ reportUnnecessaryCast = "warning"
|
|
|
54
56
|
reportUnnecessaryContains = "warning"
|
|
55
57
|
reportUnnecessaryIsInstance = "warning"
|
|
56
58
|
reportUnnecessaryTypeIgnoreComment = true
|
|
59
|
+
reportInvalidStringEscapeSequence = false
|
|
60
|
+
reportConstantRedefinition = 'error'
|
|
61
|
+
reportTypeCommentUsage = 'warning'
|
|
62
|
+
reportUnnecessaryComparison = 'warning'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from browserfetch import fetch
|
|
5
|
+
|
|
6
|
+
request_mock = Mock()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@patch('browserfetch._request', new=request_mock)
|
|
10
|
+
async def test_params():
|
|
11
|
+
with suppress(TypeError):
|
|
12
|
+
await fetch(
|
|
13
|
+
url='http://stackoverflow.com/search?q=question',
|
|
14
|
+
params={'lang': 'en', 'tag': 'python', 'q': True},
|
|
15
|
+
)
|
|
16
|
+
request_mock.assert_called_once_with(
|
|
17
|
+
None, # host
|
|
18
|
+
{
|
|
19
|
+
'action': 'fetch',
|
|
20
|
+
'url': 'http://stackoverflow.com/search?q=question&lang=en&tag=python&q=True',
|
|
21
|
+
'options': None,
|
|
22
|
+
'timeout': 95,
|
|
23
|
+
}, # data
|
|
24
|
+
None, # body
|
|
25
|
+
)
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from pyperclip import copy
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def main():
|
|
8
|
-
parser = ArgumentParser(
|
|
9
|
-
description='The command-line entry for browserfetch.',
|
|
10
|
-
formatter_class=ArgumentDefaultsHelpFormatter,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
parser.add_argument(
|
|
14
|
-
'copyjs', help='copy contents of browserfetch.js to clipboard'
|
|
15
|
-
)
|
|
16
|
-
args = parser.parse_args()
|
|
17
|
-
|
|
18
|
-
if args.copyjs:
|
|
19
|
-
with (Path(__file__).parent / 'browserfetch.js').open() as f:
|
|
20
|
-
copy(f.read())
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if __name__ == '__main__':
|
|
24
|
-
main()
|
|
File without changes
|
|
File without changes
|