browserfetch 0.6.2__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.6.2
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,7 +1,14 @@
1
- __version__ = '0.6.2'
1
+ __version__ = '0.8.0'
2
2
 
3
3
  import atexit
4
- from asyncio import CancelledError, Event, Task, get_running_loop, wait_for
4
+ from asyncio import (
5
+ AbstractEventLoop,
6
+ CancelledError,
7
+ Event,
8
+ Task,
9
+ get_running_loop,
10
+ wait_for,
11
+ )
5
12
  from collections import defaultdict
6
13
  from dataclasses import dataclass
7
14
  from json import dumps, loads
@@ -15,9 +22,9 @@ from aiohttp.web_runner import AppRunner, TCPSite
15
22
 
16
23
  logger = getLogger(__name__)
17
24
  # maps host to its host_ready event or its websocket
18
- hosts: dict[
19
- str, Event | WebSocketResponse | ClientWebSocketResponse
20
- ] = defaultdict(Event)
25
+ hosts: dict[str, Event | WebSocketResponse | ClientWebSocketResponse] = (
26
+ defaultdict(Event)
27
+ )
21
28
  # maps response event id to its response event or response dict
22
29
  responses: dict[int, Event | dict] = {}
23
30
 
@@ -106,6 +113,7 @@ async def receive_responses(ws: WebSocketResponse | ClientWebSocketResponse):
106
113
 
107
114
 
108
115
  routes = RouteTableDef()
116
+ PROTOCOL = '2'
109
117
 
110
118
 
111
119
  @routes.get('/ws')
@@ -113,7 +121,10 @@ async def _(request):
113
121
  ws = WebSocketResponse()
114
122
  await ws.prepare(request)
115
123
 
116
- 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}'
117
128
  logger.info('registering host %s', host)
118
129
 
119
130
  ws_or_e = hosts[host]
@@ -165,10 +176,29 @@ async def relay_client(server_host, server_port):
165
176
  try:
166
177
  await receive_responses(ws)
167
178
  except TypeError:
168
- logger.info('relay server WebSocket was closed')
179
+ logger.info('relay WebSocket was closed')
180
+ hosts.default_factory = Event
181
+ for host, ws_or_e in hosts.items():
182
+ if isinstance(ws_or_e, Event):
183
+ ws_or_e.clear()
184
+ else:
185
+ hosts[host] = Event()
186
+ await start_server(host=_host, port=_port)
169
187
  return
170
188
 
171
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
+
172
202
  async def fetch(
173
203
  url: str,
174
204
  *,
@@ -195,6 +225,7 @@ async def fetch(
195
225
  d = await _request(
196
226
  host,
197
227
  {
228
+ 'action': 'fetch',
198
229
  'url': url,
199
230
  'options': options,
200
231
  'timeout': timeout,
@@ -268,12 +299,13 @@ app.add_routes(routes)
268
299
  app_runner = AppRunner(app)
269
300
 
270
301
 
271
- def shutdown_server(loop):
302
+ def _shutdown_server(loop: AbstractEventLoop):
272
303
  logger.info('waiting for app_runner.cleanup()')
273
304
  loop.run_until_complete(app_runner.cleanup())
274
305
 
275
306
 
276
- def shutdown_relay_client(loop, task: Task):
307
+ def _cancel_relay_task(loop: AbstractEventLoop, task: Task):
308
+ logger.info('cancelling relay task')
277
309
  task.cancel()
278
310
  try:
279
311
  loop.run_until_complete(task)
@@ -282,10 +314,13 @@ def shutdown_relay_client(loop, task: Task):
282
314
 
283
315
 
284
316
  _server = False
317
+ _host = '127.0.0.1'
318
+ _port = 9404
285
319
 
286
320
 
287
- async def start_server(*, host='127.0.0.1', port=9404):
288
- global _server
321
+ async def start_server(*, host=_host, port=_port):
322
+ global _server, _host, _port
323
+ _host, _port = host, port
289
324
  loop = get_running_loop()
290
325
  await app_runner.setup()
291
326
  site = TCPSite(app_runner, host, port)
@@ -298,8 +333,8 @@ async def start_server(*, host='127.0.0.1', port=9404):
298
333
  e,
299
334
  )
300
335
  relay_task = loop.create_task(relay_client(host, port))
301
- atexit.register(shutdown_relay_client, loop, relay_task)
336
+ atexit.register(_cancel_relay_task, loop, relay_task)
302
337
  else:
303
338
  _server = True
304
- atexit.register(shutdown_server, loop)
339
+ atexit.register(_shutdown_server, loop)
305
340
  logger.info('server started at http://%s:%s', host, port)
@@ -20,5 +20,5 @@ def main():
20
20
  copy(f.read())
21
21
 
22
22
 
23
- if __name__ == "__main__":
23
+ if __name__ == '__main__':
24
24
  main()
@@ -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,14 +1,6 @@
1
1
  [build-system]
2
- requires = [
3
- "setuptools>=66.1.0",
4
- "wheel",
5
- ]
6
- build-backend = "setuptools.build_meta"
7
-
8
- [tool.isort]
9
- profile = "black"
10
- line_length = 79
11
- combine_as_imports = true
2
+ requires = ['flit_core >=3.2,<4']
3
+ build-backend = 'flit_core.buildapi'
12
4
 
13
5
  [project]
14
6
  name = "browserfetch"
@@ -38,13 +30,18 @@ content-type = "text/x-rst"
38
30
  [project.urls]
39
31
  Homepage = "https://github.com/5j9/browserfetch"
40
32
 
41
- [tool.setuptools]
42
- packages = ["browserfetch"]
43
- zip-safe = false
44
- include-package-data = false
45
-
46
- [tool.setuptools.dynamic.version]
47
- attr = "browserfetch.__version__"
48
-
49
- [tool.setuptools.package-data]
50
- browserfetch = ["*.js"]
33
+ [tool.ruff]
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.6.2
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