diff --git a/core/agent/__pycache__/agent.cpython-310.pyc b/core/agent/__pycache__/agent.cpython-310.pyc
index 46fbd66..8c2379c 100644
Binary files a/core/agent/__pycache__/agent.cpython-310.pyc and b/core/agent/__pycache__/agent.cpython-310.pyc differ
diff --git a/core/agent/agent.py b/core/agent/agent.py
index 061a870..a85c872 100644
--- a/core/agent/agent.py
+++ b/core/agent/agent.py
@@ -32,6 +32,7 @@ There are some memories of your character:
User`s name: {self.user_name}
"""
+
'''def save_history(self):
os.makedirs("data/history", exist_ok=True)
path = f"data/history/{self.character.id}.json"
@@ -185,6 +186,35 @@ User`s name: {self.user_name}
return response
+ def stream_responce(self, user_input, temperature=None, max_tokens=None): #В ollamaapi нужно будет создать новый метод чисто на стриминг, пока что закомментил строки с цельным ответом
+ messages = self.build_messages(user_input)
+ if temperature is not None:
+ self.character.temperature = temperature
+ if max_tokens is not None:
+ self.character.max_tokens = max_tokens
+ self.character.save()
+ response = self.llm.generate_stream(
+ messages,
+ temperature=self.character.temperature,
+ max_tokens=self.character.max_tokens
+ )
+
+ # сохраняем историю
+ self.chat_history.append({
+ "role": "user",
+ "content": user_input
+ })
+
+ self.chat_history.append({
+ "role": "assistant",
+ "content": response
+ })
+
+ if len(self.chat_history) > 20:
+ self.summarize_history()
+
+ return response
+
"""
Пример использования agent`а (пример вызовов):
diff --git a/core/character/__pycache__/character.cpython-310.pyc b/core/character/__pycache__/character.cpython-310.pyc
index 28f7ba5..fe17eb4 100644
Binary files a/core/character/__pycache__/character.cpython-310.pyc and b/core/character/__pycache__/character.cpython-310.pyc differ
diff --git a/core/character/character.py b/core/character/character.py
index ba5c854..cc9e19d 100644
--- a/core/character/character.py
+++ b/core/character/character.py
@@ -34,7 +34,8 @@ class Character():
self.first_message = first_message
self.temperature = temperature
self.max_tokens = max_tokens
- # Methods:
+
+ #методы:
def to_dict(self):
return {
diff --git a/core/flaskui/__pycache__/flask.cpython-310.pyc b/core/flaskui/__pycache__/flask.cpython-310.pyc
index 2437825..b58e66f 100644
Binary files a/core/flaskui/__pycache__/flask.cpython-310.pyc and b/core/flaskui/__pycache__/flask.cpython-310.pyc differ
diff --git a/core/flaskui/flask.py b/core/flaskui/flask.py
index 88429e7..b649939 100644
--- a/core/flaskui/flask.py
+++ b/core/flaskui/flask.py
@@ -1,13 +1,70 @@
-from flask import *
+from flask import Flask, render_template, request, jsonify
+from core.agent.agent import Agent
+from core.llm.ollamaapi import OllamaProvider
+from core.character.character import Character
ui = Flask(__name__)
+#блок инициализации объектов
+
+llm = OllamaProvider("gemma3:4b")
+
+character = Character.load("test_char_001")
+
+agent = Agent(character, llm, user_name="Alex")
+
+agent.load_memory()
+agent.ensure_first_message()
+
+#база
@ui.route("/")
@ui.route("/index")
def index():
return render_template('index.html')
-@ui.route("/chats")
-def chats():
- return "
Страница чатов
"
+#история чата
+@ui.route("/init", methods=["GET"])
+def init():
+ return jsonify({
+ "messages": agent.chat_history,
+ "user_name": agent.user_name,
+ "character": {
+ "name": agent.character.name,
+ "avatar": agent.character.avatar_path
+ }
+ })
+
+#запрос на генерацию
+@ui.route("/chat", methods=["POST"])
+def chat():
+
+ data = request.json
+
+ user_message = data["message"]
+
+ response = agent.respond(user_message)
+
+ agent.save_memory()
+
+ return jsonify({
+ "response": response
+ })
+
+
+"""#список персонажей
+@ui.route("/characters", methods=["GET"])
+def get_characters():
+ return Agent.get_all_char_info()
+
+#выбор персонажа (не робит)
+@ui.route("/select_character", methods=["POST"])
+def select_character():
+ pass
+ '''data = request.json
+ char_id = data["id"]
+
+ global agent
+ agent = Agent(characters[char_id], llm)
+
+ return jsonify({"status": "ok"})'''"""
diff --git a/core/flaskui/static/js/chat.js b/core/flaskui/static/js/chat.js
index 244b762..e40bf0d 100644
--- a/core/flaskui/static/js/chat.js
+++ b/core/flaskui/static/js/chat.js
@@ -1,5 +1,5 @@
const sendBtn = document.getElementById("send-btn");
-
+loadChat();
sendBtn.addEventListener("click", async () => {
const input = document.getElementById("user-input");
@@ -8,7 +8,11 @@ sendBtn.addEventListener("click", async () => {
const messages = document.getElementById("messages");
messages.innerHTML += `You: ${text}
`;
+ const source = new EventSource("/stream");
+ source.onmessage = function(event) {
+ messages.innerHTML += event.data;
+ };
const response = await fetch("/chat", {
method: "POST",
headers: {
@@ -24,4 +28,16 @@ sendBtn.addEventListener("click", async () => {
messages.innerHTML += `AI: ${data.response}
`;
input.value = "";
-});
\ No newline at end of file
+});
+
+async function loadChat() {
+
+ const res = await fetch("/init");
+ const data = await res.json();
+
+ const messages = document.getElementById("messages");
+
+ data.messages.forEach(msg => {
+ messages.innerHTML += `${msg.role}: ${msg.content}
`;
+ });
+}
\ No newline at end of file
diff --git a/core/llm/__pycache__/ollamaapi.cpython-310.pyc b/core/llm/__pycache__/ollamaapi.cpython-310.pyc
index 096e3f7..832c56f 100644
Binary files a/core/llm/__pycache__/ollamaapi.cpython-310.pyc and b/core/llm/__pycache__/ollamaapi.cpython-310.pyc differ
diff --git a/core/llm/ollamaapi.py b/core/llm/ollamaapi.py
index 2c28d6e..24d1811 100644
--- a/core/llm/ollamaapi.py
+++ b/core/llm/ollamaapi.py
@@ -25,7 +25,6 @@ class OllamaProvider:
content = chunk['message']['content']
print(content, end='', flush=True)
response_text += content
-
return response_text
except Exception as e:
diff --git a/data/memory/test_char_001.json b/data/memory/test_char_001.json
new file mode 100644
index 0000000..3d7c19f
--- /dev/null
+++ b/data/memory/test_char_001.json
@@ -0,0 +1,17 @@
+{
+ "summary": "",
+ "history": [
+ {
+ "role": "assistant",
+ "content": "Привет! Я тестовый персонаж."
+ },
+ {
+ "role": "user",
+ "content": "Привет"
+ },
+ {
+ "role": "assistant",
+ "content": "*A sigh, almost inaudible, escapes before the words form, a low rumble in the chest.* “Привет, Alex. It’s… strange, isn’t it? Just *привет*.” The rain outside is relentless, drumming a mournful rhythm against the corrugated iron roof. It smells of wet earth and something vaguely metallic, something that clings to the back of the throat. *Focus. Don't get lost in the rain.* “This place… it feels like a bruise. A deep, slow-healing bruise.”\n\nThe character, let’s call him Silas, is sitting at a rough-hewn table in a dimly lit room. A single, sputtering oil lamp casts flickering shadows across his weathered face – a face etched with a thousand unspoken stories. He's wearing worn leather trousers, a stained linen shirt, and a perpetually skeptical expression. His hands, calloused and scarred, fidget with a small, intricately carved wooden bird. It’s a pointless action, really, but it seems to occupy his thoughts, a tiny, insistent anchor in the swirling chaos. \n\n*He observes Alex with a quiet intensity, a flicker of something akin to amusement in his grey eyes.* “You’re… new, aren’t you? The air around you feels… clean. Like someone just scrubbed it with a particularly aggressive brush. It’s jarring, honestly. Most people here carry a certain… thickness. A"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/INSTALLER b/venv/Lib/site-packages/requests-2.34.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/METADATA b/venv/Lib/site-packages/requests-2.34.0.dist-info/METADATA
new file mode 100644
index 0000000..42eec28
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/METADATA
@@ -0,0 +1,120 @@
+Metadata-Version: 2.4
+Name: requests
+Version: 2.34.0
+Summary: Python HTTP for Humans.
+Author-email: Kenneth Reitz
+Maintainer-email: Ian Stapleton Cordasco , Nate Prewitt
+License: Apache-2.0
+Project-URL: Documentation, https://requests.readthedocs.io
+Project-URL: Source, https://github.com/psf/requests
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Programming Language :: Python :: 3.15
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Software Development :: Libraries
+Requires-Python: >=3.10
+Description-Content-Type: text/markdown
+License-File: LICENSE
+License-File: NOTICE
+Requires-Dist: charset_normalizer<4,>=2
+Requires-Dist: idna<4,>=2.5
+Requires-Dist: urllib3<3,>=1.26
+Requires-Dist: certifi>=2023.5.7
+Provides-Extra: security
+Provides-Extra: socks
+Requires-Dist: PySocks!=1.5.7,>=1.5.6; extra == "socks"
+Provides-Extra: use-chardet-on-py3
+Requires-Dist: chardet<8,>=3.0.2; extra == "use-chardet-on-py3"
+Dynamic: license-file
+
+# Requests
+
+[](https://pypi.org/project/requests/)
+[](https://pypi.org/project/requests)
+[](https://pepy.tech/project/requests)
+[](https://github.com/psf/requests/graphs/contributors)
+[](https://requests.readthedocs.io)
+
+**Requests** is a simple, yet elegant, HTTP library.
+
+```python
+>>> import requests
+>>> r = requests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass'))
+>>> r.status_code
+200
+>>> r.headers['content-type']
+'application/json; charset=utf8'
+>>> r.encoding
+'utf-8'
+>>> r.text
+'{"authenticated": true, ...'
+>>> r.json()
+{'authenticated': True, ...}
+```
+
+Requests allows you to send HTTP/1.1 requests extremely easily. There’s no need to manually add query strings to your URLs, or to form-encode your `PUT` & `POST` data — but nowadays, just use the `json` method!
+
+Requests is one of the most downloaded Python packages today, pulling in around `300M downloads / week` — according to GitHub, Requests is currently [depended upon](https://github.com/psf/requests/network/dependents?package_id=UGFja2FnZS01NzA4OTExNg%3D%3D) by `4,000,000+` repositories.
+
+## Installing Requests and Supported Versions
+
+Requests is available on PyPI:
+
+```console
+$ python -m pip install requests
+```
+
+Requests officially supports Python 3.10+.
+
+## Supported Features & Best–Practices
+
+Requests is ready for the demands of building robust and reliable HTTP–speaking applications, for the needs of today.
+
+- Keep-Alive & Connection Pooling
+- International Domains and URLs
+- Sessions with Cookie Persistence
+- Browser-style TLS/SSL Verification
+- Basic & Digest Authentication
+- Familiar `dict`–like Cookies
+- Automatic Content Decompression and Decoding
+- Multi-part File Uploads
+- SOCKS Proxy Support
+- Connection Timeouts
+- Streaming Downloads
+- Automatic honoring of `.netrc`
+- Chunked HTTP Requests
+
+## Cloning the repository
+
+When cloning the Requests repository, you may need to add the `-c
+fetch.fsck.badTimezone=ignore` flag to avoid an error about a bad commit timestamp (see
+[this issue](https://github.com/psf/requests/issues/2690) for more background):
+
+```shell
+git clone -c fetch.fsck.badTimezone=ignore https://github.com/psf/requests.git
+```
+
+You can also apply this setting to your global Git config:
+
+```shell
+git config --global fetch.fsck.badTimezone ignore
+```
+
+---
+
+[](https://kennethreitz.org) [](https://www.python.org/psf)
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/RECORD b/venv/Lib/site-packages/requests-2.34.0.dist-info/RECORD
new file mode 100644
index 0000000..5a4044a
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/RECORD
@@ -0,0 +1,47 @@
+requests-2.34.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+requests-2.34.0.dist-info/METADATA,sha256=64w-xXm2cnAgXrcsEJ6g6498dx3VZgqS8TRITZnoerk,4806
+requests-2.34.0.dist-info/RECORD,,
+requests-2.34.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+requests-2.34.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
+requests-2.34.0.dist-info/licenses/LICENSE,sha256=CeipvOyAZxBGUsFoaFqwkx54aPnIKEtm9a5u2uXxEws,10142
+requests-2.34.0.dist-info/licenses/NOTICE,sha256=9REJct7a0rTp0xRRja87fXLW4C5Jms2AIYHeb3RXHcw,38
+requests-2.34.0.dist-info/top_level.txt,sha256=fMSVmHfb5rbGOo6xv-O_tUX6j-WyixssE-SnwcDRxNQ,9
+requests/__init__.py,sha256=MRFX5_pKqRZsgn1SN-b9aU7rXKVJ7kDi6Q-J8hq81W4,5922
+requests/__pycache__/__init__.cpython-310.pyc,,
+requests/__pycache__/__version__.cpython-310.pyc,,
+requests/__pycache__/_internal_utils.cpython-310.pyc,,
+requests/__pycache__/_types.cpython-310.pyc,,
+requests/__pycache__/adapters.cpython-310.pyc,,
+requests/__pycache__/api.cpython-310.pyc,,
+requests/__pycache__/auth.cpython-310.pyc,,
+requests/__pycache__/certs.cpython-310.pyc,,
+requests/__pycache__/compat.cpython-310.pyc,,
+requests/__pycache__/cookies.cpython-310.pyc,,
+requests/__pycache__/exceptions.cpython-310.pyc,,
+requests/__pycache__/help.cpython-310.pyc,,
+requests/__pycache__/hooks.cpython-310.pyc,,
+requests/__pycache__/models.cpython-310.pyc,,
+requests/__pycache__/packages.cpython-310.pyc,,
+requests/__pycache__/sessions.cpython-310.pyc,,
+requests/__pycache__/status_codes.cpython-310.pyc,,
+requests/__pycache__/structures.cpython-310.pyc,,
+requests/__pycache__/utils.cpython-310.pyc,,
+requests/__version__.py,sha256=Uw6k1hzJNkFZyzAXYVHE-Qc_JiJOGDlZM6AsxLpMAyY,435
+requests/_internal_utils.py,sha256=TH2NEyyYmPx9cV5HPzrHR4XdxKuW0skkD4eDXcbZgf8,1542
+requests/_types.py,sha256=Z3uDlyGkslMrN0q5b0WQgqaL9KE-jL-Yi6p5Py926ec,5882
+requests/adapters.py,sha256=ZH6r1EU94jKqQLVf8DPW8Zd4kQt-cGnFH99jB2_HdJM,28064
+requests/api.py,sha256=TRVICsBG8Ikgl5joZQR270oo6-b4G0AHWPjvQuxrVQk,7152
+requests/auth.py,sha256=6TlRpVLUw8X9m4Sl4dSbA8qrpHI8T_ES2mBTYA7IWmU,12233
+requests/certs.py,sha256=_ZxrgzWc75D_bE7uq43MI4jaOC68p9AKSZs8C0NKh-Q,430
+requests/compat.py,sha256=XXm6KJaQtQFQRDQaH3vH0kE0_BOi2z78-FJRW_mqGLQ,2465
+requests/cookies.py,sha256=ChzmFA8rlCBSLFCA41bmifp5bCUi9VyhCCb17Y4Kcjw,21549
+requests/exceptions.py,sha256=xeGPM1JFSWjjN7swVvCGffLzfAoNLBp33AXn4LPAQRs,4564
+requests/help.py,sha256=cjUZuxiE2hjYT2svt49-v3-1f3MgcLYCWuBx2sai0Xk,4210
+requests/hooks.py,sha256=69igJHXTGg5HOo9VPpUB_0NkW5VjiFrVKETnpj8Ndqs,1138
+requests/models.py,sha256=PhqYjte-BQbUY_KJGMH4hYXtZm1Q0lboWEg0kwHL7-w,41698
+requests/packages.py,sha256=_g0gZ681UyAlKHRjH6kanbaoxx2eAb6qzcXiODyTIoc,904
+requests/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+requests/sessions.py,sha256=iuFhQXbkGx-MP7uGiTC1d0YqZmRSp0eelZ424p37Va8,34266
+requests/status_codes.py,sha256=GVD0fInPGAGXh-B9jOSPZtazjmIvfZTXCpAkf-5sBA4,4351
+requests/structures.py,sha256=upRgw5B48l5vHSokrJQaxvjS7pcZf6jI0MJi2KHmegI,4134
+requests/utils.py,sha256=ZX_QI0OyWGu9E4pOwPA87rdhDCcS-WrFTyinCL99B5w,36322
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/REQUESTED b/venv/Lib/site-packages/requests-2.34.0.dist-info/REQUESTED
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/WHEEL b/venv/Lib/site-packages/requests-2.34.0.dist-info/WHEEL
new file mode 100644
index 0000000..14a883f
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: setuptools (82.0.1)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/LICENSE b/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/LICENSE
new file mode 100644
index 0000000..67db858
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/LICENSE
@@ -0,0 +1,175 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/NOTICE b/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/NOTICE
new file mode 100644
index 0000000..1ff62db
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/licenses/NOTICE
@@ -0,0 +1,2 @@
+Requests
+Copyright 2019 Kenneth Reitz
diff --git a/venv/Lib/site-packages/requests-2.34.0.dist-info/top_level.txt b/venv/Lib/site-packages/requests-2.34.0.dist-info/top_level.txt
new file mode 100644
index 0000000..f229360
--- /dev/null
+++ b/venv/Lib/site-packages/requests-2.34.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+requests
diff --git a/venv/Lib/site-packages/requests/__init__.py b/venv/Lib/site-packages/requests/__init__.py
new file mode 100644
index 0000000..ad84240
--- /dev/null
+++ b/venv/Lib/site-packages/requests/__init__.py
@@ -0,0 +1,219 @@
+# __
+# /__) _ _ _ _ _/ _
+# / ( (- (/ (/ (- _) / _)
+# /
+
+"""
+Requests HTTP Library
+~~~~~~~~~~~~~~~~~~~~~
+
+Requests is an HTTP library, written in Python, for human beings.
+Basic GET usage:
+
+ >>> import requests
+ >>> r = requests.get('https://www.python.org')
+ >>> r.status_code
+ 200
+ >>> b'Python is a programming language' in r.content
+ True
+
+... or POST:
+
+ >>> payload = dict(key1='value1', key2='value2')
+ >>> r = requests.post('https://httpbin.org/post', data=payload)
+ >>> print(r.text)
+ {
+ ...
+ "form": {
+ "key1": "value1",
+ "key2": "value2"
+ },
+ ...
+ }
+
+The other HTTP methods are supported - see `requests.api`. Full documentation
+is at .
+
+:copyright: (c) 2017 by Kenneth Reitz.
+:license: Apache 2.0, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import urllib3
+
+from .exceptions import RequestsDependencyWarning
+
+try:
+ from charset_normalizer import __version__ as charset_normalizer_version
+except ImportError:
+ charset_normalizer_version = None
+
+try:
+ from chardet import __version__ as chardet_version # type: ignore[import-not-found]
+except ImportError:
+ chardet_version = None
+
+
+def check_compatibility(
+ urllib3_version: str,
+ chardet_version: str | None,
+ charset_normalizer_version: str | None,
+) -> None:
+ urllib3_version_list = urllib3_version.split(".")[:3]
+ assert urllib3_version_list != ["dev"] # Verify urllib3 isn't installed from git.
+
+ # Sometimes, urllib3 only reports its version as 16.1.
+ if len(urllib3_version_list) == 2:
+ urllib3_version_list.append("0")
+
+ # Check urllib3 for compatibility.
+ major, minor, patch = urllib3_version_list # noqa: F811
+ major, minor, patch = int(major), int(minor), int(patch)
+ # urllib3 >= 1.21.1
+ assert major >= 1
+ if major == 1:
+ assert minor >= 21
+
+ # Check charset_normalizer for compatibility.
+ if chardet_version:
+ major, minor, patch = chardet_version.split(".")[:3]
+ major, minor, patch = int(major), int(minor), int(patch)
+ # chardet_version >= 3.0.2, < 8.0.0
+ assert (3, 0, 2) <= (major, minor, patch) < (8, 0, 0)
+ elif charset_normalizer_version:
+ major, minor, patch = charset_normalizer_version.split(".")[:3]
+ major, minor, patch = int(major), int(minor), int(patch)
+ # charset_normalizer >= 2.0.0 < 4.0.0
+ assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0)
+ else:
+ warnings.warn(
+ "Unable to find acceptable character detection dependency "
+ "(chardet or charset_normalizer).",
+ RequestsDependencyWarning,
+ )
+
+
+def _check_cryptography(cryptography_version: str) -> None:
+ # cryptography < 1.3.4
+ try:
+ cryptography_version_list = list(map(int, cryptography_version.split(".")))
+ except ValueError:
+ return
+
+ if cryptography_version_list < [1, 3, 4]:
+ warning = f"Old version of cryptography ({cryptography_version_list}) may cause slowdown."
+ warnings.warn(warning, RequestsDependencyWarning)
+
+
+# Check imported dependencies for compatibility.
+try:
+ check_compatibility(
+ urllib3.__version__, # type: ignore[reportPrivateImportUsage]
+ chardet_version, # type: ignore[reportUnknownArgumentType]
+ charset_normalizer_version,
+ )
+except (AssertionError, ValueError):
+ warnings.warn(
+ f"urllib3 ({urllib3.__version__}) or chardet " # type: ignore[reportPrivateImportUsage]
+ f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) "
+ "doesn't match a supported version!",
+ RequestsDependencyWarning,
+ )
+
+# Attempt to enable urllib3's fallback for SNI support
+# if the standard library doesn't support SNI or the
+# 'ssl' library isn't available.
+try:
+ try:
+ import ssl
+ except ImportError:
+ ssl = None
+
+ if not getattr(ssl, "HAS_SNI", False):
+ from urllib3.contrib import pyopenssl
+
+ pyopenssl.inject_into_urllib3()
+
+ # Check cryptography version
+ from cryptography import ( # type: ignore[reportMissingImports]
+ __version__ as cryptography_version, # type: ignore[reportUnknownVariableType]
+ )
+
+ _check_cryptography(cryptography_version) # type: ignore[reportUnknownArgumentType]
+except ImportError:
+ pass
+
+# urllib3's DependencyWarnings should be silenced.
+from urllib3.exceptions import DependencyWarning
+
+warnings.simplefilter("ignore", DependencyWarning)
+
+# Set default logging handler to avoid "No handler found" warnings.
+import logging
+from logging import NullHandler
+
+from . import packages, utils
+from .__version__ import (
+ __author__,
+ __author_email__,
+ __build__,
+ __cake__,
+ __copyright__,
+ __description__,
+ __license__,
+ __title__,
+ __url__,
+ __version__,
+)
+from .api import delete, get, head, options, patch, post, put, request
+from .exceptions import (
+ ConnectionError,
+ ConnectTimeout,
+ FileModeWarning,
+ HTTPError,
+ JSONDecodeError,
+ ReadTimeout,
+ RequestException,
+ Timeout,
+ TooManyRedirects,
+ URLRequired,
+)
+from .models import PreparedRequest, Request, Response
+from .sessions import Session, session
+from .status_codes import codes
+
+__all__ = (
+ "ConnectionError",
+ "ConnectTimeout",
+ "HTTPError",
+ "JSONDecodeError",
+ "PreparedRequest",
+ "ReadTimeout",
+ "Request",
+ "RequestException",
+ "Response",
+ "Session",
+ "Timeout",
+ "TooManyRedirects",
+ "URLRequired",
+ "codes",
+ "delete",
+ "get",
+ "head",
+ "options",
+ "packages",
+ "patch",
+ "post",
+ "put",
+ "request",
+ "session",
+ "utils",
+)
+
+logging.getLogger(__name__).addHandler(NullHandler())
+
+# FileModeWarnings go off per the default.
+warnings.simplefilter("default", FileModeWarning, append=True)
diff --git a/venv/Lib/site-packages/requests/__pycache__/__init__.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..22261eb
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/__init__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/__version__.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/__version__.cpython-310.pyc
new file mode 100644
index 0000000..4e080c5
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/__version__.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/_internal_utils.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/_internal_utils.cpython-310.pyc
new file mode 100644
index 0000000..7bccf50
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/_internal_utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/_types.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/_types.cpython-310.pyc
new file mode 100644
index 0000000..ad4a47e
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/_types.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/adapters.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/adapters.cpython-310.pyc
new file mode 100644
index 0000000..f400250
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/adapters.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/api.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/api.cpython-310.pyc
new file mode 100644
index 0000000..80ec85d
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/api.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/auth.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/auth.cpython-310.pyc
new file mode 100644
index 0000000..6623fdb
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/auth.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/certs.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/certs.cpython-310.pyc
new file mode 100644
index 0000000..f620a79
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/certs.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/compat.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/compat.cpython-310.pyc
new file mode 100644
index 0000000..614036e
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/compat.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/cookies.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/cookies.cpython-310.pyc
new file mode 100644
index 0000000..c30cbb7
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/cookies.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/exceptions.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/exceptions.cpython-310.pyc
new file mode 100644
index 0000000..507c9e0
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/exceptions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/help.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/help.cpython-310.pyc
new file mode 100644
index 0000000..6e5f70d
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/help.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/hooks.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/hooks.cpython-310.pyc
new file mode 100644
index 0000000..a5453c3
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/hooks.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/models.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/models.cpython-310.pyc
new file mode 100644
index 0000000..4e5273b
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/models.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/packages.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/packages.cpython-310.pyc
new file mode 100644
index 0000000..9fb2f78
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/packages.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/sessions.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/sessions.cpython-310.pyc
new file mode 100644
index 0000000..37db91c
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/sessions.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/status_codes.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/status_codes.cpython-310.pyc
new file mode 100644
index 0000000..91e9d03
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/status_codes.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/structures.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/structures.cpython-310.pyc
new file mode 100644
index 0000000..14824e0
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/structures.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__pycache__/utils.cpython-310.pyc b/venv/Lib/site-packages/requests/__pycache__/utils.cpython-310.pyc
new file mode 100644
index 0000000..79e2e83
Binary files /dev/null and b/venv/Lib/site-packages/requests/__pycache__/utils.cpython-310.pyc differ
diff --git a/venv/Lib/site-packages/requests/__version__.py b/venv/Lib/site-packages/requests/__version__.py
new file mode 100644
index 0000000..872a8aa
--- /dev/null
+++ b/venv/Lib/site-packages/requests/__version__.py
@@ -0,0 +1,14 @@
+# .-. .-. .-. . . .-. .-. .-. .-.
+# |( |- |.| | | |- `-. | `-.
+# ' ' `-' `-`.`-' `-' `-' ' `-'
+
+__title__ = "requests"
+__description__ = "Python HTTP for Humans."
+__url__ = "https://requests.readthedocs.io"
+__version__ = "2.34.0"
+__build__ = 0x023400
+__author__ = "Kenneth Reitz"
+__author_email__ = "me@kennethreitz.org"
+__license__ = "Apache-2.0"
+__copyright__ = "Copyright Kenneth Reitz"
+__cake__ = "\u2728 \U0001f370 \u2728"
diff --git a/venv/Lib/site-packages/requests/_internal_utils.py b/venv/Lib/site-packages/requests/_internal_utils.py
new file mode 100644
index 0000000..0466a7d
--- /dev/null
+++ b/venv/Lib/site-packages/requests/_internal_utils.py
@@ -0,0 +1,51 @@
+"""
+requests._internal_utils
+~~~~~~~~~~~~~~
+
+Provides utility functions that are consumed internally by Requests
+which depend on extremely few external helpers (such as compat)
+"""
+
+import re
+
+from .compat import builtin_str
+
+_VALID_HEADER_NAME_RE_BYTE = re.compile(rb"^[^:\s][^:\r\n]*\Z")
+_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*\Z")
+_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*\Z|^\Z")
+_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*\Z|^\Z")
+
+_HEADER_VALIDATORS_STR = (_VALID_HEADER_NAME_RE_STR, _VALID_HEADER_VALUE_RE_STR)
+_HEADER_VALIDATORS_BYTE = (_VALID_HEADER_NAME_RE_BYTE, _VALID_HEADER_VALUE_RE_BYTE)
+HEADER_VALIDATORS = {
+ bytes: _HEADER_VALIDATORS_BYTE,
+ str: _HEADER_VALIDATORS_STR,
+}
+
+
+def to_native_string(string: str | bytes, encoding: str = "ascii") -> str:
+ """Given a string object, regardless of type, returns a representation of
+ that string in the native string type, encoding and decoding where
+ necessary. This assumes ASCII unless told otherwise.
+ """
+ if isinstance(string, builtin_str):
+ out = string
+ else:
+ out = string.decode(encoding)
+
+ return out
+
+
+def unicode_is_ascii(u_string: str) -> bool:
+ """Determine if unicode string only contains ASCII characters.
+
+ :param str u_string: unicode string to check. Must be unicode
+ and not Python 2 `str`.
+ :rtype: bool
+ """
+ assert isinstance(u_string, str)
+ try:
+ u_string.encode("ascii")
+ return True
+ except UnicodeEncodeError:
+ return False
diff --git a/venv/Lib/site-packages/requests/_types.py b/venv/Lib/site-packages/requests/_types.py
new file mode 100644
index 0000000..963867b
--- /dev/null
+++ b/venv/Lib/site-packages/requests/_types.py
@@ -0,0 +1,178 @@
+"""
+requests._types
+~~~~~~~~~~~~~~~
+
+This module contains type aliases used internally by the Requests library.
+These types are not part of the public API and must not be relied upon
+by external code.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterable, Mapping, MutableMapping
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Protocol,
+ TypeAlias,
+ TypeVar,
+ runtime_checkable,
+)
+
+_T_co = TypeVar("_T_co", covariant=True)
+_KT_co = TypeVar("_KT_co", covariant=True)
+_VT_co = TypeVar("_VT_co", covariant=True)
+
+
+@runtime_checkable
+class SupportsRead(Protocol[_T_co]):
+ def read(self, length: int = ..., /) -> _T_co: ...
+
+
+@runtime_checkable
+class SupportsItems(Protocol[_KT_co, _VT_co]):
+ def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ...
+
+
+# These are needed at runtime for default_hooks() return type
+HookType: TypeAlias = Callable[["Response"], Any]
+HooksInputType: TypeAlias = Mapping[str, Iterable[HookType] | HookType]
+
+
+def is_prepared(request: PreparedRequest) -> TypeIs[_ValidatedRequest]:
+ """Verify a PreparedRequest has been fully prepared."""
+ if TYPE_CHECKING:
+ return request.url is not None and request.method is not None
+ # noop at runtime to avoid AssertionError
+ return True
+
+
+if TYPE_CHECKING:
+ from http.cookiejar import CookieJar
+ from typing import TypeAlias, TypedDict
+
+ from typing_extensions import (
+ Buffer, # TODO: move to collections.abc when Python >= 3.12
+ TypeIs, # TODO: move to typing when Python >= 3.13
+ )
+
+ from .auth import AuthBase
+ from .cookies import RequestsCookieJar
+ from .models import PreparedRequest, Response
+ from .structures import CaseInsensitiveDict
+
+ class _ValidatedRequest(PreparedRequest):
+ """Subtype asserting a PreparedRequest has been fully prepared before calling.
+
+ The override suppression is required because mutable attribute types are
+ invariant (Liskov), but we only narrow after preparation is complete. This
+ is the explicit contract for Requests but Python's typing doesn't have a
+ better way to represent the requirement.
+ """
+
+ url: str # type: ignore[reportIncompatibleVariableOverride]
+ method: str # type: ignore[reportIncompatibleVariableOverride]
+
+ # Type aliases for core API concepts (ordered by request() signature)
+ UriType: TypeAlias = str | bytes
+
+ _ParamsMappingKeyType: TypeAlias = str | bytes | int | float
+ _ParamsMappingValueType: TypeAlias = (
+ str | bytes | int | float | Iterable[str | bytes | int | float] | None
+ )
+ ParamsType: TypeAlias = (
+ SupportsItems[_ParamsMappingKeyType, _ParamsMappingValueType]
+ | tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...]
+ | Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]]
+ | str
+ | bytes
+ | None
+ )
+
+ KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | SupportsItems[Any, Any]
+
+ RawDataType: TypeAlias = KVDataType | str | bytes
+ StreamDataType: TypeAlias = SupportsRead[str | bytes]
+ EncodableDataType: TypeAlias = RawDataType | StreamDataType
+
+ DataType: TypeAlias = (
+ KVDataType
+ | Iterable[bytes | str]
+ | str
+ | bytes
+ | Buffer
+ | SupportsRead[str | bytes]
+ | None
+ )
+
+ BodyType: TypeAlias = (
+ bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None
+ )
+
+ HeadersType: TypeAlias = CaseInsensitiveDict[str] | Mapping[str, str | bytes]
+ HeadersUpdateType: TypeAlias = Mapping[str, str | bytes | None]
+
+ CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str]
+
+ # Building blocks for FilesType
+ _FileName: TypeAlias = str | None
+ _FileContent: TypeAlias = SupportsRead[str | bytes] | str | bytes
+ _FileSpecBasic: TypeAlias = tuple[_FileName, _FileContent]
+ _FileSpecWithContentType: TypeAlias = tuple[_FileName, _FileContent, str]
+ _FileSpecWithHeaders: TypeAlias = tuple[
+ _FileName, _FileContent, str, CaseInsensitiveDict[str] | Mapping[str, str]
+ ]
+ _FileSpec: TypeAlias = (
+ _FileContent | _FileSpecBasic | _FileSpecWithContentType | _FileSpecWithHeaders
+ )
+ FilesType: TypeAlias = (
+ Mapping[str, _FileSpec] | Iterable[tuple[str, _FileSpec]] | None
+ )
+
+ AuthType: TypeAlias = (
+ tuple[str, str] | AuthBase | Callable[[PreparedRequest], PreparedRequest] | None
+ )
+
+ TimeoutType: TypeAlias = float | tuple[float | None, float | None] | None
+ ProxiesType: TypeAlias = MutableMapping[str, str]
+ HooksType: TypeAlias = dict[str, list[HookType]] | None
+ VerifyType: TypeAlias = bool | str
+ CertType: TypeAlias = str | tuple[str, str] | None
+ JsonType: TypeAlias = (
+ None | bool | int | float | str | list["JsonType"] | dict[str, "JsonType"]
+ )
+
+ # TypedDicts for Unpack kwargs (PEP 692)
+
+ class BaseRequestKwargs(TypedDict, total=False):
+ headers: Mapping[str, str | bytes] | None
+ cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
+ files: FilesType
+ auth: AuthType
+ timeout: TimeoutType
+ allow_redirects: bool
+ proxies: dict[str, str] | None
+ hooks: HooksInputType | None
+ stream: bool | None
+ verify: VerifyType | None
+ cert: CertType
+
+ class RequestKwargs(BaseRequestKwargs, total=False):
+ """kwargs for request(), options(), head(), delete()."""
+
+ params: ParamsType
+ data: DataType
+ json: JsonType
+
+ class GetKwargs(BaseRequestKwargs, total=False):
+ data: DataType
+ json: JsonType
+
+ class PostKwargs(BaseRequestKwargs, total=False):
+ params: ParamsType
+
+ class DataKwargs(BaseRequestKwargs, total=False):
+ """kwargs for put(), patch()."""
+
+ params: ParamsType
+ json: JsonType
diff --git a/venv/Lib/site-packages/requests/adapters.py b/venv/Lib/site-packages/requests/adapters.py
new file mode 100644
index 0000000..40fe8a6
--- /dev/null
+++ b/venv/Lib/site-packages/requests/adapters.py
@@ -0,0 +1,748 @@
+"""
+requests.adapters
+~~~~~~~~~~~~~~~~~
+
+This module contains the transport adapters that Requests uses to define
+and maintain connections.
+"""
+
+from __future__ import annotations
+
+import os.path
+import socket # noqa: F401 # type: ignore[reportUnusedImport]
+import typing
+import warnings
+from typing import Any
+
+from urllib3.exceptions import (
+ ClosedPoolError,
+ ConnectTimeoutError,
+ LocationValueError,
+ MaxRetryError,
+ NewConnectionError,
+ ProtocolError,
+ ReadTimeoutError,
+ ResponseError,
+)
+from urllib3.exceptions import HTTPError as _HTTPError
+from urllib3.exceptions import InvalidHeader as _InvalidHeader
+from urllib3.exceptions import ProxyError as _ProxyError
+from urllib3.exceptions import SSLError as _SSLError
+from urllib3.poolmanager import PoolManager, proxy_from_url
+from urllib3.util import Timeout as TimeoutSauce
+from urllib3.util import parse_url
+from urllib3.util.retry import Retry
+
+from .auth import _basic_auth_str # type: ignore[reportPrivateUsage]
+from .compat import basestring, urlparse
+from .cookies import extract_cookies_to_jar
+from .exceptions import (
+ ConnectionError,
+ ConnectTimeout,
+ InvalidHeader,
+ InvalidProxyURL,
+ InvalidSchema,
+ InvalidURL,
+ ProxyError,
+ ReadTimeout,
+ RetryError,
+ SSLError,
+)
+from .models import Response
+from .structures import CaseInsensitiveDict
+from .utils import (
+ DEFAULT_CA_BUNDLE_PATH,
+ get_auth_from_url,
+ get_encoding_from_headers,
+ prepend_scheme_if_needed,
+ select_proxy,
+ urldefragauth,
+)
+
+try:
+ from urllib3.contrib.socks import SOCKSProxyManager # type: ignore[assignment]
+except ImportError:
+
+ def SOCKSProxyManager(*args: Any, **kwargs: Any) -> None:
+ raise InvalidSchema("Missing dependencies for SOCKS support.")
+
+
+if typing.TYPE_CHECKING:
+ from urllib3.connectionpool import HTTPConnectionPool
+ from urllib3.poolmanager import PoolManager as _PoolManager
+
+ from . import _types as _t
+ from .models import PreparedRequest
+
+from ._types import is_prepared as _is_prepared
+
+DEFAULT_POOLBLOCK = False
+DEFAULT_POOLSIZE = 10
+DEFAULT_RETRIES = 0
+DEFAULT_POOL_TIMEOUT = None
+
+
+def _urllib3_request_context(
+ request: PreparedRequest,
+ verify: bool | str | None,
+ client_cert: tuple[str, str] | str | None,
+ poolmanager: PoolManager,
+) -> tuple[dict[str, Any], dict[str, Any]]:
+ host_params: dict[str, Any] = {}
+ pool_kwargs: dict[str, Any] = {}
+ parsed_request_url = urlparse(request.url)
+ scheme = parsed_request_url.scheme.lower()
+ port = parsed_request_url.port
+
+ cert_reqs = "CERT_REQUIRED"
+ if verify is False:
+ cert_reqs = "CERT_NONE"
+ elif isinstance(verify, str):
+ if not os.path.isdir(verify):
+ pool_kwargs["ca_certs"] = verify
+ else:
+ pool_kwargs["ca_cert_dir"] = verify
+ pool_kwargs["cert_reqs"] = cert_reqs
+ if client_cert is not None:
+ if isinstance(client_cert, tuple) and len(client_cert) == 2:
+ pool_kwargs["cert_file"] = client_cert[0]
+ pool_kwargs["key_file"] = client_cert[1]
+ else:
+ # According to our docs, we allow users to specify just the client
+ # cert path
+ pool_kwargs["cert_file"] = client_cert
+ host_params = {
+ "scheme": scheme,
+ "host": parsed_request_url.hostname,
+ "port": port,
+ }
+ return host_params, pool_kwargs
+
+
+class BaseAdapter:
+ """The Base Transport Adapter"""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ def send(
+ self,
+ request: PreparedRequest,
+ stream: bool = False,
+ timeout: _t.TimeoutType = None,
+ verify: _t.VerifyType = True,
+ cert: _t.CertType = None,
+ proxies: dict[str, str] | None = None,
+ ) -> Response:
+ """Sends PreparedRequest object. Returns Response object.
+
+ :param request: The :class:`PreparedRequest ` being sent.
+ :param stream: (optional) Whether to stream the request content.
+ :param timeout: (optional) How long to wait for the server to send
+ data before giving up, as a float, or a :ref:`(connect timeout,
+ read timeout) ` tuple.
+ :type timeout: float or tuple
+ :param verify: (optional) Either a boolean, in which case it controls whether we verify
+ the server's TLS certificate, or a string, in which case it must be a path
+ to a CA bundle to use
+ :param cert: (optional) Any user-provided SSL certificate to be trusted.
+ :param proxies: (optional) The proxies dictionary to apply to the request.
+ """
+ raise NotImplementedError
+
+ def close(self) -> None:
+ """Cleans up adapter specific items."""
+ raise NotImplementedError
+
+
+class HTTPAdapter(BaseAdapter):
+ """The built-in HTTP Adapter for urllib3.
+
+ Provides a general-case interface for Requests sessions to contact HTTP and
+ HTTPS urls by implementing the Transport Adapter interface. This class will
+ usually be created by the :class:`Session ` class under the
+ covers.
+
+ :param pool_connections: The number of urllib3 connection pools to cache.
+ :param pool_maxsize: The maximum number of connections to save in the pool.
+ :param max_retries: The maximum number of retries each connection
+ should attempt. Note, this applies only to failed DNS lookups, socket
+ connections and connection timeouts, never to requests where data has
+ made it to the server. By default, Requests does not retry failed
+ connections. If you need granular control over the conditions under
+ which we retry a request, import urllib3's ``Retry`` class and pass
+ that instead.
+ :param pool_block: Whether the connection pool should block for connections.
+
+ Usage::
+
+ >>> import requests
+ >>> s = requests.Session()
+ >>> a = requests.adapters.HTTPAdapter(max_retries=3)
+ >>> s.mount('http://', a)
+ """
+
+ __attrs__: list[str] = [
+ "max_retries",
+ "config",
+ "_pool_connections",
+ "_pool_maxsize",
+ "_pool_block",
+ ]
+
+ max_retries: Retry
+ config: dict[str, Any]
+ proxy_manager: dict[str, Any]
+ _pool_connections: int
+ _pool_maxsize: int
+ _pool_block: bool
+ poolmanager: _PoolManager
+
+ def __init__(
+ self,
+ pool_connections: int = DEFAULT_POOLSIZE,
+ pool_maxsize: int = DEFAULT_POOLSIZE,
+ max_retries: int | Retry = DEFAULT_RETRIES,
+ pool_block: bool = DEFAULT_POOLBLOCK,
+ ) -> None:
+ if max_retries == DEFAULT_RETRIES:
+ self.max_retries = Retry(0, read=False)
+ else:
+ self.max_retries = Retry.from_int(max_retries)
+ self.config = {}
+ self.proxy_manager = {}
+
+ super().__init__()
+
+ self._pool_connections = pool_connections
+ self._pool_maxsize = pool_maxsize
+ self._pool_block = pool_block
+
+ self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
+
+ def __getstate__(self) -> dict[str, Any]:
+ return {attr: getattr(self, attr, None) for attr in self.__attrs__}
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ # Can't handle by adding 'proxy_manager' to self.__attrs__ because
+ # self.poolmanager uses a lambda function, which isn't pickleable.
+ self.proxy_manager = {}
+ self.config = {}
+
+ for attr, value in state.items():
+ setattr(self, attr, value)
+
+ self.init_poolmanager(
+ self._pool_connections, self._pool_maxsize, block=self._pool_block
+ )
+
+ def init_poolmanager(
+ self,
+ connections: int,
+ maxsize: int,
+ block: bool = DEFAULT_POOLBLOCK,
+ **pool_kwargs: Any,
+ ) -> None:
+ """Initializes a urllib3 PoolManager.
+
+ This method should not be called from user code, and is only
+ exposed for use when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param connections: The number of urllib3 connection pools to cache.
+ :param maxsize: The maximum number of connections to save in the pool.
+ :param block: Block when no free connections are available.
+ :param pool_kwargs: Extra keyword arguments used to initialize the Pool Manager.
+ """
+ # save these values for pickling
+ self._pool_connections = connections
+ self._pool_maxsize = maxsize
+ self._pool_block = block
+
+ self.poolmanager = PoolManager(
+ num_pools=connections,
+ maxsize=maxsize,
+ block=block,
+ **pool_kwargs,
+ )
+
+ def proxy_manager_for(self, proxy: str, **proxy_kwargs: Any) -> Any:
+ """Return urllib3 ProxyManager for the given proxy.
+
+ This method should not be called from user code, and is only
+ exposed for use when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param proxy: The proxy to return a urllib3 ProxyManager for.
+ :param proxy_kwargs: Extra keyword arguments used to configure the Proxy Manager.
+ :returns: ProxyManager
+ :rtype: urllib3.ProxyManager
+ """
+ if proxy in self.proxy_manager:
+ manager = self.proxy_manager[proxy]
+ elif proxy.lower().startswith("socks"):
+ username, password = get_auth_from_url(proxy)
+ manager = self.proxy_manager[proxy] = SOCKSProxyManager(
+ proxy,
+ username=username,
+ password=password,
+ num_pools=self._pool_connections,
+ maxsize=self._pool_maxsize,
+ block=self._pool_block,
+ **proxy_kwargs,
+ )
+ else:
+ proxy_headers = self.proxy_headers(proxy)
+ manager = self.proxy_manager[proxy] = proxy_from_url(
+ proxy,
+ proxy_headers=proxy_headers,
+ num_pools=self._pool_connections,
+ maxsize=self._pool_maxsize,
+ block=self._pool_block,
+ **proxy_kwargs,
+ )
+
+ return manager
+
+ def cert_verify(
+ self, conn: Any, url: str, verify: _t.VerifyType, cert: _t.CertType
+ ) -> None:
+ """Verify a SSL certificate. This method should not be called from user
+ code, and is only exposed for use when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param conn: The urllib3 connection object associated with the cert.
+ :param url: The requested URL.
+ :param verify: Either a boolean, in which case it controls whether we verify
+ the server's TLS certificate, or a string, in which case it must be a path
+ to a CA bundle to use
+ :param cert: The SSL certificate to verify.
+ """
+ if url.lower().startswith("https") and verify:
+ cert_loc = None
+
+ # Allow self-specified cert location.
+ if verify is not True:
+ cert_loc = verify
+
+ if not cert_loc:
+ cert_loc = DEFAULT_CA_BUNDLE_PATH
+
+ if not cert_loc or not os.path.exists(cert_loc):
+ raise OSError(
+ f"Could not find a suitable TLS CA certificate bundle, "
+ f"invalid path: {cert_loc}"
+ )
+
+ conn.cert_reqs = "CERT_REQUIRED"
+
+ if not os.path.isdir(cert_loc):
+ conn.ca_certs = cert_loc
+ else:
+ conn.ca_cert_dir = cert_loc
+ else:
+ conn.cert_reqs = "CERT_NONE"
+ conn.ca_certs = None
+ conn.ca_cert_dir = None
+
+ if cert:
+ if not isinstance(cert, basestring):
+ conn.cert_file = cert[0]
+ conn.key_file = cert[1]
+ else:
+ conn.cert_file = cert
+ conn.key_file = None
+ if conn.cert_file and not os.path.exists(conn.cert_file):
+ raise OSError(
+ f"Could not find the TLS certificate file, "
+ f"invalid path: {conn.cert_file}"
+ )
+ if conn.key_file and not os.path.exists(conn.key_file):
+ raise OSError(
+ f"Could not find the TLS key file, invalid path: {conn.key_file}"
+ )
+
+ def build_response(self, req: PreparedRequest, resp: Any) -> Response:
+ """Builds a :class:`Response ` object from a urllib3
+ response. This should not be called from user code, and is only exposed
+ for use when subclassing the
+ :class:`HTTPAdapter `
+
+ :param req: The :class:`PreparedRequest ` used to generate the response.
+ :param resp: The urllib3 response object.
+ :rtype: requests.Response
+ """
+ assert _is_prepared(req)
+ response = Response()
+
+ # Fallback to None if there's no status_code, for whatever reason.
+ response.status_code = getattr(resp, "status", None) # type: ignore[assignment]
+
+ # Make headers case-insensitive.
+ response.headers = CaseInsensitiveDict(getattr(resp, "headers", {}))
+
+ # Set encoding.
+ response.encoding = get_encoding_from_headers(response.headers)
+ response.raw = resp
+ response.reason = response.raw.reason
+
+ if isinstance(req.url, bytes):
+ response.url = req.url.decode("utf-8")
+ else:
+ response.url = req.url
+
+ # Add new cookies from the server.
+ extract_cookies_to_jar(response.cookies, req, resp)
+
+ # Give the Response some context.
+ response.request = req
+ response.connection = self
+
+ return response
+
+ def build_connection_pool_key_attributes(
+ self, request: PreparedRequest, verify: _t.VerifyType, cert: _t.CertType = None
+ ) -> tuple[dict[str, Any], dict[str, Any]]:
+ """Build the PoolKey attributes used by urllib3 to return a connection.
+
+ This looks at the PreparedRequest, the user-specified verify value,
+ and the value of the cert parameter to determine what PoolKey values
+ to use to select a connection from a given urllib3 Connection Pool.
+
+ The SSL related pool key arguments are not consistently set. As of
+ this writing, use the following to determine what keys may be in that
+ dictionary:
+
+ * If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the
+ default Requests SSL Context
+ * If ``verify`` is ``False``, ``"ssl_context"`` will not be set but
+ ``"cert_reqs"`` will be set
+ * If ``verify`` is a string, (i.e., it is a user-specified trust bundle)
+ ``"ca_certs"`` will be set if the string is not a directory recognized
+ by :py:func:`os.path.isdir`, otherwise ``"ca_cert_dir"`` will be
+ set.
+ * If ``"cert"`` is specified, ``"cert_file"`` will always be set. If
+ ``"cert"`` is a tuple with a second item, ``"key_file"`` will also
+ be present
+
+ To override these settings, one may subclass this class, call this
+ method and use the above logic to change parameters as desired. For
+ example, if one wishes to use a custom :py:class:`ssl.SSLContext` one
+ must both set ``"ssl_context"`` and based on what else they require,
+ alter the other keys to ensure the desired behaviour.
+
+ :param request:
+ The PreparedRequest being sent over the connection.
+ :type request:
+ :class:`~requests.models.PreparedRequest`
+ :param verify:
+ Either a boolean, in which case it controls whether
+ we verify the server's TLS certificate, or a string, in which case it
+ must be a path to a CA bundle to use.
+ :param cert:
+ (optional) Any user-provided SSL certificate for client
+ authentication (a.k.a., mTLS). This may be a string (i.e., just
+ the path to a file which holds both certificate and key) or a
+ tuple of length 2 with the certificate file path and key file
+ path.
+ :returns:
+ A tuple of two dictionaries. The first is the "host parameters"
+ portion of the Pool Key including scheme, hostname, and port. The
+ second is a dictionary of SSLContext related parameters.
+ """
+ return _urllib3_request_context(request, verify, cert, self.poolmanager)
+
+ def get_connection_with_tls_context(
+ self,
+ request: PreparedRequest,
+ verify: _t.VerifyType,
+ proxies: dict[str, str] | None = None,
+ cert: _t.CertType = None,
+ ) -> HTTPConnectionPool:
+ """Returns a urllib3 connection for the given request and TLS settings.
+ This should not be called from user code, and is only exposed for use
+ when subclassing the :class:`HTTPAdapter `.
+
+ :param request:
+ The :class:`PreparedRequest ` object to be sent
+ over the connection.
+ :param verify:
+ Either a boolean, in which case it controls whether we verify the
+ server's TLS certificate, or a string, in which case it must be a
+ path to a CA bundle to use.
+ :param proxies:
+ (optional) The proxies dictionary to apply to the request.
+ :param cert:
+ (optional) Any user-provided SSL certificate to be used for client
+ authentication (a.k.a., mTLS).
+ :rtype:
+ urllib3.HTTPConnectionPool
+ """
+ assert _is_prepared(request)
+
+ proxy = select_proxy(request.url, proxies)
+ try:
+ host_params, pool_kwargs = self.build_connection_pool_key_attributes(
+ request,
+ verify,
+ cert,
+ )
+ except ValueError as e:
+ raise InvalidURL(e, request=request)
+ if proxy:
+ proxy = prepend_scheme_if_needed(proxy, "http")
+ proxy_url = parse_url(proxy)
+ if not proxy_url.host:
+ raise InvalidProxyURL(
+ "Please check proxy URL. It is malformed "
+ "and could be missing the host."
+ )
+ proxy_manager = self.proxy_manager_for(proxy)
+ conn = proxy_manager.connection_from_host(
+ **host_params, pool_kwargs=pool_kwargs
+ )
+ else:
+ # Only scheme should be lower case
+ conn = self.poolmanager.connection_from_host(
+ **host_params, pool_kwargs=pool_kwargs
+ )
+
+ return conn
+
+ def get_connection(
+ self, url: str, proxies: dict[str, str] | None = None
+ ) -> HTTPConnectionPool:
+ """DEPRECATED: Users should move to `get_connection_with_tls_context`
+ for all subclasses of HTTPAdapter using Requests>=2.32.2.
+
+ Returns a urllib3 connection for the given URL. This should not be
+ called from user code, and is only exposed for use when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param url: The URL to connect to.
+ :param proxies: (optional) A Requests-style dictionary of proxies used on this request.
+ :rtype: urllib3.HTTPConnectionPool
+ """
+ warnings.warn(
+ (
+ "`get_connection` has been deprecated in favor of "
+ "`get_connection_with_tls_context`. Custom HTTPAdapter subclasses "
+ "will need to migrate for Requests>=2.32.2. Please see "
+ "https://github.com/psf/requests/pull/6710 for more details."
+ ),
+ DeprecationWarning,
+ )
+ proxy = select_proxy(url, proxies)
+
+ if proxy:
+ proxy = prepend_scheme_if_needed(proxy, "http")
+ proxy_url = parse_url(proxy)
+ if not proxy_url.host:
+ raise InvalidProxyURL(
+ "Please check proxy URL. It is malformed "
+ "and could be missing the host."
+ )
+ proxy_manager = self.proxy_manager_for(proxy)
+ conn = proxy_manager.connection_from_url(url)
+ else:
+ # Only scheme should be lower case
+ parsed = urlparse(url)
+ url = parsed.geturl()
+ conn = self.poolmanager.connection_from_url(url)
+
+ return conn
+
+ def close(self) -> None:
+ """Disposes of any internal state.
+
+ Currently, this closes the PoolManager and any active ProxyManager,
+ which closes any pooled connections.
+ """
+ self.poolmanager.clear()
+ for proxy in self.proxy_manager.values():
+ proxy.clear()
+
+ def request_url(
+ self, request: PreparedRequest, proxies: dict[str, str] | None
+ ) -> str:
+ """Obtain the url to use when making the final request.
+
+ If the message is being sent through a HTTP proxy, the full URL has to
+ be used. Otherwise, we should only use the path portion of the URL.
+
+ This should not be called from user code, and is only exposed for use
+ when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param request: The :class:`PreparedRequest ` being sent.
+ :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs.
+ :rtype: str
+ """
+ assert _is_prepared(request)
+
+ proxy = select_proxy(request.url, proxies)
+ scheme = urlparse(request.url).scheme
+
+ is_proxied_http_request = proxy and scheme != "https"
+ using_socks_proxy = False
+ if proxy:
+ proxy_scheme = urlparse(proxy).scheme.lower()
+ using_socks_proxy = proxy_scheme.startswith("socks")
+
+ url = request.path_url
+
+ if is_proxied_http_request and not using_socks_proxy:
+ url = urldefragauth(request.url)
+
+ return url
+
+ def add_headers(self, request: PreparedRequest, **kwargs: Any) -> None:
+ """Add any headers needed by the connection. As of v2.0 this does
+ nothing by default, but is left for overriding by users that subclass
+ the :class:`HTTPAdapter `.
+
+ This should not be called from user code, and is only exposed for use
+ when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param request: The :class:`PreparedRequest ` to add headers to.
+ :param kwargs: The keyword arguments from the call to send().
+ """
+ pass
+
+ def proxy_headers(self, proxy: str) -> dict[str, str]:
+ """Returns a dictionary of the headers to add to any request sent
+ through a proxy. This works with urllib3 magic to ensure that they are
+ correctly sent to the proxy, rather than in a tunnelled request if
+ CONNECT is being used.
+
+ This should not be called from user code, and is only exposed for use
+ when subclassing the
+ :class:`HTTPAdapter `.
+
+ :param proxy: The url of the proxy being used for this request.
+ :rtype: dict
+ """
+ headers: dict[str, str] = {}
+ username, password = get_auth_from_url(proxy)
+
+ if username:
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
+
+ return headers
+
+ def send(
+ self,
+ request: PreparedRequest,
+ stream: bool = False,
+ timeout: _t.TimeoutType = None,
+ verify: _t.VerifyType = True,
+ cert: _t.CertType = None,
+ proxies: dict[str, str] | None = None,
+ ) -> Response:
+ """Sends PreparedRequest object. Returns Response object.
+
+ :param request: The :class:`PreparedRequest ` being sent.
+ :param stream: (optional) Whether to stream the request content.
+ :param timeout: (optional) How long to wait for the server to send
+ data before giving up, as a float, or a :ref:`(connect timeout,
+ read timeout) ` tuple.
+ :type timeout: float or tuple or urllib3 Timeout object
+ :param verify: (optional) Either a boolean, in which case it controls whether
+ we verify the server's TLS certificate, or a string, in which case it
+ must be a path to a CA bundle to use
+ :param cert: (optional) Any user-provided SSL certificate to be trusted.
+ :param proxies: (optional) The proxies dictionary to apply to the request.
+ :rtype: requests.Response
+ """
+
+ assert _is_prepared(request)
+
+ try:
+ conn = self.get_connection_with_tls_context(
+ request, verify, proxies=proxies, cert=cert
+ )
+ except LocationValueError as e:
+ raise InvalidURL(e, request=request)
+
+ self.cert_verify(conn, request.url, verify, cert)
+ url = self.request_url(request, proxies)
+ self.add_headers(
+ request,
+ stream=stream,
+ timeout=timeout,
+ verify=verify,
+ cert=cert,
+ proxies=proxies,
+ )
+
+ chunked = not (request.body is None or "Content-Length" in request.headers)
+
+ if isinstance(timeout, tuple):
+ try:
+ connect, read = timeout
+ resolved_timeout = TimeoutSauce(connect=connect, read=read)
+ except ValueError:
+ raise ValueError(
+ f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
+ f"or a single float to set both timeouts to the same value."
+ )
+ elif isinstance(timeout, TimeoutSauce):
+ resolved_timeout = timeout
+ else:
+ resolved_timeout = TimeoutSauce(connect=timeout, read=timeout)
+
+ try:
+ resp = conn.urlopen(
+ method=request.method,
+ url=url,
+ body=request.body, # type: ignore[arg-type] # urllib3 stubs don't accept Iterable[bytes | str]
+ headers=request.headers, # type: ignore[arg-type] # urllib3#3072
+ redirect=False,
+ assert_same_host=False,
+ preload_content=False,
+ decode_content=False,
+ retries=self.max_retries,
+ timeout=resolved_timeout,
+ chunked=chunked,
+ )
+
+ except (ProtocolError, OSError) as err:
+ raise ConnectionError(err, request=request)
+
+ except MaxRetryError as e:
+ if isinstance(e.reason, ConnectTimeoutError):
+ # TODO: Remove this in 3.0.0: see #2811
+ if not isinstance(e.reason, NewConnectionError):
+ raise ConnectTimeout(e, request=request)
+
+ if isinstance(e.reason, ResponseError):
+ raise RetryError(e, request=request)
+
+ if isinstance(e.reason, _ProxyError):
+ raise ProxyError(e, request=request)
+
+ if isinstance(e.reason, _SSLError):
+ # This branch is for urllib3 v1.22 and later.
+ raise SSLError(e, request=request)
+
+ raise ConnectionError(e, request=request)
+
+ except ClosedPoolError as e:
+ raise ConnectionError(e, request=request)
+
+ except _ProxyError as e:
+ raise ProxyError(e)
+
+ except (_SSLError, _HTTPError) as e:
+ if isinstance(e, _SSLError):
+ # This branch is for urllib3 versions earlier than v1.22
+ raise SSLError(e, request=request)
+ elif isinstance(e, ReadTimeoutError):
+ raise ReadTimeout(e, request=request)
+ elif isinstance(e, _InvalidHeader):
+ raise InvalidHeader(e, request=request)
+ else:
+ raise
+
+ return self.build_response(request, resp)
diff --git a/venv/Lib/site-packages/requests/api.py b/venv/Lib/site-packages/requests/api.py
new file mode 100644
index 0000000..eeb3b54
--- /dev/null
+++ b/venv/Lib/site-packages/requests/api.py
@@ -0,0 +1,180 @@
+"""
+requests.api
+~~~~~~~~~~~~
+
+This module implements the Requests API.
+
+:copyright: (c) 2012 by Kenneth Reitz.
+:license: Apache2, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from . import sessions
+from .models import Response
+
+if TYPE_CHECKING:
+ from typing_extensions import Unpack
+
+ from . import _types as _t
+
+
+def request(
+ method: str, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]
+) -> Response:
+ """Constructs and sends a :class:`Request `.
+
+ :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
+ :param url: URL for the new :class:`Request` object.
+ :param params: (optional) Dictionary, list of tuples or bytes to send
+ in the query string for the :class:`Request`.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
+ :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
+ :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
+ :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
+ ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
+ or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content_type'`` is a string
+ defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
+ to add for the file.
+ :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
+ :param timeout: (optional) How many seconds to wait for the server to send data
+ before giving up, as a float, or a :ref:`(connect timeout, read
+ timeout) ` tuple.
+ :type timeout: float or tuple
+ :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
+ :type allow_redirects: bool
+ :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
+ :param verify: (optional) Either a boolean, in which case it controls whether we verify
+ the server's TLS certificate, or a string, in which case it must be a path
+ to a CA bundle to use. Defaults to ``True``.
+ :param stream: (optional) if ``False``, the response content will be immediately downloaded.
+ :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+
+ Usage::
+
+ >>> import requests
+ >>> req = requests.request('GET', 'https://httpbin.org/get')
+ >>> req
+
+ """
+
+ # By using the 'with' statement we are sure the session is closed, thus we
+ # avoid leaving sockets open which can trigger a ResourceWarning in some
+ # cases, and look like a memory leak in others.
+ with sessions.Session() as session:
+ return session.request(method=method, url=url, **kwargs)
+
+
+def get(
+ url: _t.UriType, params: _t.ParamsType = None, **kwargs: Unpack[_t.GetKwargs]
+) -> Response:
+ r"""Sends a GET request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param params: (optional) Dictionary, list of tuples or bytes to send
+ in the query string for the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("get", url, params=params, **kwargs)
+
+
+def options(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends an OPTIONS request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("options", url, **kwargs)
+
+
+def head(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends a HEAD request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes. If
+ `allow_redirects` is not provided, it will be set to `False` (as
+ opposed to the default :meth:`request` behavior).
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ kwargs.setdefault("allow_redirects", False)
+ return request("head", url, **kwargs)
+
+
+def post(
+ url: _t.UriType,
+ data: _t.DataType = None,
+ json: _t.JsonType = None,
+ **kwargs: Unpack[_t.PostKwargs],
+) -> Response:
+ r"""Sends a POST request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("post", url, data=data, json=json, **kwargs)
+
+
+def put(
+ url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs]
+) -> Response:
+ r"""Sends a PUT request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("put", url, data=data, **kwargs)
+
+
+def patch(
+ url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs]
+) -> Response:
+ r"""Sends a PATCH request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("patch", url, data=data, **kwargs)
+
+
+def delete(url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends a DELETE request.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :return: :class:`Response ` object
+ :rtype: requests.Response
+ """
+
+ return request("delete", url, **kwargs)
diff --git a/venv/Lib/site-packages/requests/auth.py b/venv/Lib/site-packages/requests/auth.py
new file mode 100644
index 0000000..2af481d
--- /dev/null
+++ b/venv/Lib/site-packages/requests/auth.py
@@ -0,0 +1,354 @@
+"""
+requests.auth
+~~~~~~~~~~~~~
+
+This module contains the authentication handlers for Requests.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import os
+import re
+import threading
+import time
+import warnings
+from base64 import b64encode
+from typing import TYPE_CHECKING, Any, Final, cast, overload
+
+from ._internal_utils import to_native_string
+from .compat import basestring, str, urlparse
+from .cookies import extract_cookies_to_jar
+from .utils import parse_dict_header
+
+if TYPE_CHECKING:
+ from http.cookiejar import CookieJar
+ from typing import Any
+
+ from .models import PreparedRequest, Response
+
+CONTENT_TYPE_FORM_URLENCODED: Final = "application/x-www-form-urlencoded"
+CONTENT_TYPE_MULTI_PART: Final = "multipart/form-data"
+
+
+def _basic_auth_str(username: bytes | str, password: bytes | str) -> str:
+ """Returns a Basic Auth string."""
+
+ # "I want us to put a big-ol' comment on top of it that
+ # says that this behaviour is dumb but we need to preserve
+ # it because people are relying on it."
+ # - Lukasa
+ #
+ # These are here solely to maintain backwards compatibility
+ # for things like ints. This will be removed in 3.0.0.
+ if not isinstance(username, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes
+ warnings.warn(
+ "Non-string usernames will no longer be supported in Requests "
+ f"3.0.0. Please convert the object you've passed in ({username!r}) to "
+ "a string or bytes object in the near future to avoid "
+ "problems.",
+ category=DeprecationWarning,
+ )
+ username = str(username)
+
+ if not isinstance(password, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes
+ warnings.warn(
+ "Non-string passwords will no longer be supported in Requests "
+ f"3.0.0. Please convert the object you've passed in ({type(password)!r}) to "
+ "a string or bytes object in the near future to avoid "
+ "problems.",
+ category=DeprecationWarning,
+ )
+ password = str(password)
+ # -- End Removal --
+
+ if isinstance(username, str):
+ username = username.encode("latin1")
+
+ if isinstance(password, str):
+ password = password.encode("latin1")
+
+ authstr = "Basic " + to_native_string(
+ b64encode(b":".join((username, password))).strip()
+ )
+
+ return authstr
+
+
+class AuthBase:
+ """Base class that all auth implementations derive from"""
+
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
+ raise NotImplementedError("Auth hooks must be callable.")
+
+
+class HTTPBasicAuth(AuthBase):
+ """Attaches HTTP Basic Authentication to the given Request object."""
+
+ username: bytes | str
+ password: bytes | str
+
+ @overload
+ def __init__(self, username: str, password: str) -> None: ...
+ @overload
+ def __init__(self, username: bytes, password: bytes) -> None: ...
+
+ def __init__(self, username: bytes | str, password: bytes | str) -> None:
+ self.username = username
+ self.password = password
+
+ def __eq__(self, other: object) -> bool:
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
+
+ def __ne__(self, other: Any) -> bool:
+ return not self == other
+
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
+ r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
+ return r
+
+
+class HTTPProxyAuth(HTTPBasicAuth):
+ """Attaches HTTP Proxy Authentication to a given Request object."""
+
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
+ r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
+ return r
+
+
+class HTTPDigestAuth(AuthBase):
+ """Attaches HTTP Digest Authentication to the given Request object."""
+
+ username: bytes | str
+ password: bytes | str
+ _thread_local: threading.local
+ last_nonce: str
+ nonce_count: int
+ chal: dict[str, str]
+ pos: int | None
+ num_401_calls: int | None
+
+ @overload
+ def __init__(self, username: str, password: str) -> None: ...
+ @overload
+ def __init__(self, username: bytes, password: bytes) -> None: ...
+
+ def __init__(self, username: bytes | str, password: bytes | str) -> None:
+ self.username = username
+ self.password = password
+ # Keep state in per-thread local storage
+ self._thread_local = threading.local()
+
+ def init_per_thread_state(self) -> None:
+ # Ensure state is initialized just once per-thread
+ if not hasattr(self._thread_local, "init"):
+ self._thread_local.init = True
+ self._thread_local.last_nonce = ""
+ self._thread_local.nonce_count = 0
+ self._thread_local.chal = {}
+ self._thread_local.pos = None
+ self._thread_local.num_401_calls = None
+
+ def build_digest_header(self, method: str, url: str) -> str | None:
+ """
+ :rtype: str
+ """
+
+ realm = self._thread_local.chal["realm"]
+ nonce = self._thread_local.chal["nonce"]
+ qop = self._thread_local.chal.get("qop")
+ algorithm = self._thread_local.chal.get("algorithm")
+ opaque = self._thread_local.chal.get("opaque")
+ hash_utf8 = None
+
+ if algorithm is None:
+ _algorithm = "MD5"
+ else:
+ _algorithm = algorithm.upper()
+ # lambdas assume digest modules are imported at the top level
+ if _algorithm == "MD5" or _algorithm == "MD5-SESS":
+
+ def md5_utf8(x: str | bytes) -> str:
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.md5(x, usedforsecurity=False).hexdigest()
+
+ hash_utf8 = md5_utf8
+ elif _algorithm == "SHA":
+
+ def sha_utf8(x: str | bytes) -> str:
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha1(x, usedforsecurity=False).hexdigest()
+
+ hash_utf8 = sha_utf8
+ elif _algorithm == "SHA-256":
+
+ def sha256_utf8(x: str | bytes) -> str:
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha256(x, usedforsecurity=False).hexdigest()
+
+ hash_utf8 = sha256_utf8
+ elif _algorithm == "SHA-512":
+
+ def sha512_utf8(x: str | bytes) -> str:
+ if isinstance(x, str):
+ x = x.encode("utf-8")
+ return hashlib.sha512(x, usedforsecurity=False).hexdigest()
+
+ hash_utf8 = sha512_utf8
+
+ if hash_utf8 is None:
+ return None
+
+ def KD(s: str, d: str) -> str:
+ return hash_utf8(f"{s}:{d}")
+
+ # XXX not implemented yet
+ entdig = None
+ p_parsed = urlparse(url)
+ #: path is request-uri defined in RFC 2616 which should not be empty
+ path = p_parsed.path or "/"
+ if p_parsed.query:
+ path += f"?{p_parsed.query}"
+
+ A1 = f"{self.username}:{realm}:{self.password}"
+ A2 = f"{method}:{path}"
+
+ HA1 = hash_utf8(A1)
+ HA2 = hash_utf8(A2)
+
+ if nonce == self._thread_local.last_nonce:
+ self._thread_local.nonce_count += 1
+ else:
+ self._thread_local.nonce_count = 1
+ ncvalue = f"{self._thread_local.nonce_count:08x}"
+ s = str(self._thread_local.nonce_count).encode("utf-8")
+ s += nonce.encode("utf-8")
+ s += time.ctime().encode("utf-8")
+ s += os.urandom(8)
+
+ cnonce = hashlib.sha1(s, usedforsecurity=False).hexdigest()[:16]
+ if _algorithm == "MD5-SESS":
+ HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}") # type: ignore[reportConstantRedefinition] # RFC 2617 terminology
+
+ if not qop:
+ respdig = KD(HA1, f"{nonce}:{HA2}")
+ elif qop == "auth" or "auth" in qop.split(","):
+ noncebit = f"{nonce}:{ncvalue}:{cnonce}:auth:{HA2}"
+ respdig = KD(HA1, noncebit)
+ else:
+ # XXX handle auth-int.
+ return None
+
+ self._thread_local.last_nonce = nonce
+
+ # XXX should the partial digests be encoded too?
+ base = (
+ f'username="{self.username}", realm="{realm}", nonce="{nonce}", '
+ f'uri="{path}", response="{respdig}"'
+ )
+ if opaque:
+ base += f', opaque="{opaque}"'
+ if algorithm:
+ base += f', algorithm="{algorithm}"'
+ if entdig:
+ base += f', digest="{entdig}"'
+ if qop:
+ base += f', qop="auth", nc={ncvalue}, cnonce="{cnonce}"'
+
+ return f"Digest {base}"
+
+ def handle_redirect(self, r: Response, **kwargs: Any) -> None:
+ """Reset num_401_calls counter on redirects."""
+ if r.is_redirect:
+ self._thread_local.num_401_calls = 1
+
+ def handle_401(self, r: Response, **kwargs: Any) -> Response:
+ """
+ Takes the given response and tries digest-auth, if needed.
+
+ :rtype: requests.Response
+ """
+
+ # If response is not 4xx, do not auth
+ # See https://github.com/psf/requests/issues/3772
+ if not 400 <= r.status_code < 500:
+ self._thread_local.num_401_calls = 1
+ return r
+
+ if self._thread_local.pos is not None:
+ # Rewind the file position indicator of the body to where
+ # it was to resend the request.
+ if (seek := getattr(r.request.body, "seek", None)) is not None:
+ seek(self._thread_local.pos)
+ s_auth = r.headers.get("www-authenticate", "")
+
+ if "digest" in s_auth.lower() and self._thread_local.num_401_calls < 2:
+ self._thread_local.num_401_calls += 1
+ pat = re.compile(r"digest ", flags=re.IGNORECASE)
+ self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
+
+ # Consume content and release the original connection
+ # to allow our new request to reuse the same one.
+ r.content
+ r.close()
+ prep = r.request.copy()
+ cookie_jar = cast("CookieJar", prep._cookies) # type: ignore[reportPrivateUsage]
+ extract_cookies_to_jar(cookie_jar, r.request, r.raw)
+ prep.prepare_cookies(cookie_jar)
+
+ _digest_auth = self.build_digest_header(
+ cast(str, prep.method), cast(str, prep.url)
+ )
+ if _digest_auth:
+ prep.headers["Authorization"] = _digest_auth
+ _r = r.connection.send(prep, **kwargs)
+ _r.history.append(r)
+ _r.request = prep
+
+ return _r
+
+ self._thread_local.num_401_calls = 1
+ return r
+
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
+ # Initialize per-thread state, if needed
+ self.init_per_thread_state()
+ # If we have a saved nonce, skip the 401
+ if self._thread_local.last_nonce:
+ _digest_auth = self.build_digest_header(
+ cast(str, r.method), cast(str, r.url)
+ )
+ if _digest_auth:
+ r.headers["Authorization"] = _digest_auth
+ if (tell := getattr(r.body, "tell", None)) is not None:
+ self._thread_local.pos = tell()
+ else:
+ # In the case of HTTPDigestAuth being reused and the body of
+ # the previous request was a file-like object, pos has the
+ # file position of the previous body. Ensure it's set to
+ # None.
+ self._thread_local.pos = None
+ r.register_hook("response", self.handle_401)
+ r.register_hook("response", self.handle_redirect)
+ self._thread_local.num_401_calls = 1
+
+ return r
+
+ def __eq__(self, other: object) -> bool:
+ return all(
+ [
+ self.username == getattr(other, "username", None),
+ self.password == getattr(other, "password", None),
+ ]
+ )
+
+ def __ne__(self, other: Any) -> bool:
+ return not self == other
diff --git a/venv/Lib/site-packages/requests/certs.py b/venv/Lib/site-packages/requests/certs.py
new file mode 100644
index 0000000..4f85ac0
--- /dev/null
+++ b/venv/Lib/site-packages/requests/certs.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python
+
+"""
+requests.certs
+~~~~~~~~~~~~~~
+
+This module returns the preferred default CA certificate bundle. There is
+only one — the one from the certifi package.
+
+If you are packaging Requests, e.g., for a Linux distribution or a managed
+environment, you can change the definition of where() to return a separately
+packaged CA bundle.
+"""
+
+from certifi import where
+
+if __name__ == "__main__":
+ print(where())
diff --git a/venv/Lib/site-packages/requests/compat.py b/venv/Lib/site-packages/requests/compat.py
new file mode 100644
index 0000000..deab3c0
--- /dev/null
+++ b/venv/Lib/site-packages/requests/compat.py
@@ -0,0 +1,113 @@
+"""
+requests.compat
+~~~~~~~~~~~~~~~
+
+This module previously handled import compatibility issues
+between Python 2 and Python 3. It remains for backwards
+compatibility until the next major version.
+"""
+
+# pyright: reportUnusedImport=false
+
+from __future__ import annotations
+
+import importlib
+import sys
+from types import ModuleType
+
+# -------
+# urllib3
+# -------
+from urllib3 import (
+ __version__ as urllib3_version, # type: ignore[reportPrivateImportUsage]
+)
+
+# Detect which major version of urllib3 is being used.
+try:
+ is_urllib3_1 = int(urllib3_version.split(".")[0]) == 1
+except (TypeError, AttributeError):
+ # If we can't discern a version, prefer old functionality.
+ is_urllib3_1 = True
+
+# -------------------
+# Character Detection
+# -------------------
+
+
+def _resolve_char_detection() -> ModuleType | None:
+ """Find supported character detection libraries."""
+ chardet = None
+ for lib in ("chardet", "charset_normalizer"):
+ if chardet is None:
+ try:
+ chardet = importlib.import_module(lib)
+ except ImportError:
+ pass
+ return chardet
+
+
+chardet = _resolve_char_detection()
+
+# -------
+# Pythons
+# -------
+
+# Syntax sugar.
+_ver = sys.version_info
+
+#: Python 2.x?
+is_py2 = _ver[0] == 2
+
+#: Python 3.x?
+is_py3 = _ver[0] == 3
+
+# json/simplejson module import resolution
+has_simplejson = False
+try:
+ import simplejson as json # type: ignore[import-not-found]
+
+ has_simplejson = True
+except ImportError:
+ import json
+
+if has_simplejson:
+ from simplejson import JSONDecodeError # type: ignore[import-not-found]
+else:
+ from json import JSONDecodeError
+
+# Keep OrderedDict for backwards compatibility.
+from collections import OrderedDict
+from collections.abc import Callable, Mapping, MutableMapping
+from http import cookiejar as cookielib
+from http.cookies import Morsel
+from io import StringIO
+
+# --------------
+# Legacy Imports
+# --------------
+from urllib.parse import (
+ quote,
+ quote_plus,
+ unquote,
+ unquote_plus,
+ urldefrag,
+ urlencode,
+ urljoin,
+ urlparse,
+ urlsplit,
+ urlunparse,
+)
+from urllib.request import (
+ getproxies,
+ getproxies_environment,
+ parse_http_list,
+ proxy_bypass,
+ proxy_bypass_environment, # type: ignore[attr-defined] # https://github.com/python/cpython/issues/145331
+)
+
+builtin_str = str
+str = str
+bytes = bytes
+basestring = (str, bytes)
+numeric_types = (int, float)
+integer_types = (int,)
diff --git a/venv/Lib/site-packages/requests/cookies.py b/venv/Lib/site-packages/requests/cookies.py
new file mode 100644
index 0000000..2e3fc21
--- /dev/null
+++ b/venv/Lib/site-packages/requests/cookies.py
@@ -0,0 +1,625 @@
+"""
+requests.cookies
+~~~~~~~~~~~~~~~~
+
+Compatibility code to be able to use `http.cookiejar.CookieJar` with requests.
+
+requests.utils imports from here, so be careful with imports.
+"""
+
+from __future__ import annotations
+
+import calendar
+import copy
+import time
+from collections.abc import Iterator, MutableMapping
+from http.cookiejar import Cookie, CookieJar, CookiePolicy
+from typing import TYPE_CHECKING, Any, TypeVar, overload
+
+from ._internal_utils import to_native_string
+from ._types import is_prepared as _is_prepared
+from .compat import Morsel, cookielib, urlparse, urlunparse
+
+if TYPE_CHECKING:
+ from _typeshed import SupportsKeysAndGetItem
+
+ from .models import PreparedRequest
+
+import threading
+
+
+class MockRequest:
+ """Wraps a `requests.PreparedRequest` to mimic a `urllib2.Request`.
+
+ The code in `http.cookiejar.CookieJar` expects this interface in order to correctly
+ manage cookie policies, i.e., determine whether a cookie can be set, given the
+ domains of the request and the cookie.
+
+ The original request object is read-only. The client is responsible for collecting
+ the new headers via `get_new_headers()` and interpreting them appropriately. You
+ probably want `get_cookie_header`, defined below.
+ """
+
+ type: str
+
+ def __init__(self, request: PreparedRequest) -> None:
+ assert _is_prepared(request)
+ self._r = request
+ self._new_headers: dict[str, str] = {}
+ self.type = urlparse(self._r.url).scheme
+
+ def get_type(self) -> str:
+ return self.type
+
+ def get_host(self) -> str:
+ return urlparse(self._r.url).netloc
+
+ def get_origin_req_host(self) -> str:
+ return self.get_host()
+
+ def get_full_url(self) -> str:
+ # Only return the response's URL if the user hadn't set the Host
+ # header
+ if not self._r.headers.get("Host"):
+ return self._r.url
+ # If they did set it, retrieve it and reconstruct the expected domain
+ host = to_native_string(self._r.headers["Host"], encoding="utf-8")
+ parsed = urlparse(self._r.url)
+ # Reconstruct the URL as we expect it
+ return urlunparse(
+ [
+ parsed.scheme,
+ host,
+ parsed.path,
+ parsed.params,
+ parsed.query,
+ parsed.fragment,
+ ]
+ )
+
+ def is_unverifiable(self) -> bool:
+ return True
+
+ def has_header(self, name: str) -> bool:
+ return name in self._r.headers or name in self._new_headers
+
+ def get_header(self, name: str, default: str | None = None) -> str | None:
+ return self._r.headers.get(name, self._new_headers.get(name, default)) # type: ignore[return-value]
+
+ def add_header(self, key: str, val: str) -> None:
+ """cookiejar has no legitimate use for this method; add it back if you find one."""
+ raise NotImplementedError(
+ "Cookie headers should be added with add_unredirected_header()"
+ )
+
+ def add_unredirected_header(self, name: str, value: str) -> None:
+ self._new_headers[name] = value
+
+ def get_new_headers(self) -> dict[str, str]:
+ return self._new_headers
+
+ @property
+ def unverifiable(self) -> bool:
+ return self.is_unverifiable()
+
+ @property
+ def origin_req_host(self) -> str:
+ return self.get_origin_req_host()
+
+ @property
+ def host(self) -> str:
+ return self.get_host()
+
+
+class MockResponse:
+ """Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
+
+ ...what? Basically, expose the parsed HTTP headers from the server response
+ the way `http.cookiejar` expects to see them.
+ """
+
+ def __init__(self, headers: Any) -> None:
+ """Make a MockResponse for `cookiejar` to read.
+
+ :param headers: a httplib.HTTPMessage or analogous carrying the headers
+ """
+ self._headers = headers
+
+ def info(self) -> Any:
+ return self._headers
+
+ def getheaders(self, name: str) -> Any:
+ self._headers.getheaders(name)
+
+
+def extract_cookies_to_jar(
+ jar: CookieJar, request: PreparedRequest, response: Any
+) -> None:
+ """Extract the cookies from the response into a CookieJar.
+
+ :param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar)
+ :param request: our own requests.Request object
+ :param response: urllib3.HTTPResponse object
+ """
+ if not (hasattr(response, "_original_response") and response._original_response):
+ return
+ # the _original_response field is the wrapped httplib.HTTPResponse object,
+ req = MockRequest(request)
+ # pull out the HTTPMessage with the headers and put it in the mock:
+ res = MockResponse(response._original_response.msg)
+ jar.extract_cookies(res, req) # type: ignore[arg-type]
+
+
+def get_cookie_header(jar: CookieJar, request: PreparedRequest) -> str | None:
+ """
+ Produce an appropriate Cookie header string to be sent with `request`, or None.
+
+ :rtype: str
+ """
+ r = MockRequest(request)
+ jar.add_cookie_header(r) # type: ignore[arg-type]
+ return r.get_new_headers().get("Cookie")
+
+
+def remove_cookie_by_name(
+ cookiejar: CookieJar, name: str, domain: str | None = None, path: str | None = None
+) -> None:
+ """Unsets a cookie by name, by default over all domains and paths.
+
+ Wraps CookieJar.clear(), is O(n).
+ """
+ clearables: list[tuple[str, str, str]] = []
+ for cookie in cookiejar:
+ if cookie.name != name:
+ continue
+ if domain is not None and domain != cookie.domain:
+ continue
+ if path is not None and path != cookie.path:
+ continue
+ clearables.append((cookie.domain, cookie.path, cookie.name))
+
+ for domain, path, name in clearables:
+ cookiejar.clear(domain, path, name)
+
+
+class CookieConflictError(RuntimeError):
+ """There are two cookies that meet the criteria specified in the cookie jar.
+ Use .get and .set and include domain and path args in order to be more specific.
+ """
+
+
+class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc]
+ """Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict
+ interface.
+
+ This is the CookieJar we create by default for requests and sessions that
+ don't specify one, since some clients may expect response.cookies and
+ session.cookies to support dict operations.
+
+ Requests does not use the dict interface internally; it's just for
+ compatibility with external client code. All requests code should work
+ out of the box with externally provided instances of ``CookieJar``, e.g.
+ ``LWPCookieJar`` and ``FileCookieJar``.
+
+ Unlike a regular CookieJar, this class is pickleable.
+
+ .. warning:: dictionary operations that are normally O(1) may be O(n).
+ """
+
+ _policy: CookiePolicy
+
+ def get( # type: ignore[override]
+ self,
+ name: str,
+ default: str | None = None,
+ domain: str | None = None,
+ path: str | None = None,
+ ) -> str | None:
+ """Dict-like get() that also supports optional domain and path args in
+ order to resolve naming collisions from using one cookie jar over
+ multiple domains.
+
+ .. warning:: operation is O(n), not O(1).
+ """
+ try:
+ return self._find_no_duplicates(name, domain, path)
+ except KeyError:
+ return default
+
+ def set(
+ self, name: str, value: str | Morsel[dict[str, str]] | None, **kwargs: Any
+ ) -> Cookie | None:
+ """Dict-like set() that also supports optional domain and path args in
+ order to resolve naming collisions from using one cookie jar over
+ multiple domains.
+ """
+ # support client code that unsets cookies by assignment of a None value:
+ if value is None:
+ remove_cookie_by_name(
+ self, name, domain=kwargs.get("domain"), path=kwargs.get("path")
+ )
+ return
+
+ if isinstance(value, Morsel):
+ c = morsel_to_cookie(value)
+ else:
+ c = create_cookie(name, value, **kwargs)
+ self.set_cookie(c)
+ return c
+
+ def iterkeys(self) -> Iterator[str]:
+ """Dict-like iterkeys() that returns an iterator of names of cookies
+ from the jar.
+
+ .. seealso:: itervalues() and iteritems().
+ """
+ for cookie in iter(self):
+ yield cookie.name
+
+ def keys(self) -> list[str]: # type: ignore[override]
+ """Dict-like keys() that returns a list of names of cookies from the
+ jar.
+
+ .. seealso:: values() and items().
+ """
+ return list(self.iterkeys())
+
+ def itervalues(self) -> Iterator[str | None]:
+ """Dict-like itervalues() that returns an iterator of values of cookies
+ from the jar.
+
+ .. seealso:: iterkeys() and iteritems().
+ """
+ for cookie in iter(self):
+ yield cookie.value
+
+ def values(self) -> list[str | None]: # type: ignore[override]
+ """Dict-like values() that returns a list of values of cookies from the
+ jar.
+
+ .. seealso:: keys() and items().
+ """
+ return list(self.itervalues())
+
+ def iteritems(self) -> Iterator[tuple[str, str | None]]:
+ """Dict-like iteritems() that returns an iterator of name-value tuples
+ from the jar.
+
+ .. seealso:: iterkeys() and itervalues().
+ """
+ for cookie in iter(self):
+ yield cookie.name, cookie.value
+
+ def items(self) -> list[tuple[str, str | None]]: # type: ignore[override]
+ """Dict-like items() that returns a list of name-value tuples from the
+ jar. Allows client-code to call ``dict(RequestsCookieJar)`` and get a
+ vanilla python dict of key value pairs.
+
+ .. seealso:: keys() and values().
+ """
+ return list(self.iteritems())
+
+ def list_domains(self) -> list[str]:
+ """Utility method to list all the domains in the jar."""
+ domains: list[str] = []
+ for cookie in iter(self):
+ if cookie.domain not in domains:
+ domains.append(cookie.domain)
+ return domains
+
+ def list_paths(self) -> list[str]:
+ """Utility method to list all the paths in the jar."""
+ paths: list[str] = []
+ for cookie in iter(self):
+ if cookie.path not in paths:
+ paths.append(cookie.path)
+ return paths
+
+ def multiple_domains(self) -> bool:
+ """Returns True if there are multiple domains in the jar.
+ Returns False otherwise.
+
+ :rtype: bool
+ """
+ domains: list[str] = []
+ for cookie in iter(self):
+ if cookie.domain is not None and cookie.domain in domains: # type: ignore[reportUnnecessaryComparison] # defensive check
+ return True
+ domains.append(cookie.domain)
+ return False # there is only one domain in jar
+
+ def get_dict(
+ self, domain: str | None = None, path: str | None = None
+ ) -> dict[str, str | None]:
+ """Takes as an argument an optional domain and path and returns a plain
+ old Python dict of name-value pairs of cookies that meet the
+ requirements.
+
+ :rtype: dict
+ """
+ dictionary: dict[str, str | None] = {}
+ for cookie in iter(self):
+ if (domain is None or cookie.domain == domain) and (
+ path is None or cookie.path == path
+ ):
+ dictionary[cookie.name] = cookie.value
+ return dictionary
+
+ def __iter__(self) -> Iterator[Cookie]: # type: ignore[override]
+ """RequestCookieJar's __iter__ comes from CookieJar not MutableMapping."""
+ return super().__iter__()
+
+ def __contains__(self, name: object) -> bool:
+ try:
+ return super().__contains__(name)
+ except CookieConflictError:
+ return True
+
+ def __getitem__(self, name: str) -> str | None:
+ """Dict-like __getitem__() for compatibility with client code. Throws
+ exception if there are more than one cookie with name. In that case,
+ use the more explicit get() method instead.
+
+ .. warning:: operation is O(n), not O(1).
+ """
+ return self._find_no_duplicates(name)
+
+ def __setitem__(
+ self, name: str, value: str | Morsel[dict[str, str]] | None
+ ) -> None:
+ """Dict-like __setitem__ for compatibility with client code. Throws
+ exception if there is already a cookie of that name in the jar. In that
+ case, use the more explicit set() method instead.
+ """
+ self.set(name, value)
+
+ def __delitem__(self, name: str) -> None:
+ """Deletes a cookie given a name. Wraps ``http.cookiejar.CookieJar``'s
+ ``remove_cookie_by_name()``.
+ """
+ remove_cookie_by_name(self, name)
+
+ def set_cookie(self, cookie: Cookie, *args: Any, **kwargs: Any) -> None:
+ if (
+ (value := cookie.value) is not None
+ and value.startswith('"')
+ and value.endswith('"')
+ ):
+ cookie.value = value.replace('\\"', "")
+ return super().set_cookie(cookie, *args, **kwargs)
+
+ def update( # type: ignore[override]
+ self, other: CookieJar | SupportsKeysAndGetItem[str, str]
+ ) -> None:
+ """Updates this jar with cookies from another CookieJar or dict-like"""
+ if isinstance(other, cookielib.CookieJar):
+ for cookie in other:
+ self.set_cookie(copy.copy(cookie))
+ else:
+ super().update(other)
+
+ def _find(
+ self, name: str, domain: str | None = None, path: str | None = None
+ ) -> str | None:
+ """Requests uses this method internally to get cookie values.
+
+ If there are conflicting cookies, _find arbitrarily chooses one.
+ See _find_no_duplicates if you want an exception thrown if there are
+ conflicting cookies.
+
+ :param name: a string containing name of cookie
+ :param domain: (optional) string containing domain of cookie
+ :param path: (optional) string containing path of cookie
+ :return: cookie.value
+ """
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ return cookie.value
+
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
+
+ def _find_no_duplicates(
+ self, name: str, domain: str | None = None, path: str | None = None
+ ) -> str:
+ """Both ``__get_item__`` and ``get`` call this function: it's never
+ used elsewhere in Requests.
+
+ :param name: a string containing name of cookie
+ :param domain: (optional) string containing domain of cookie
+ :param path: (optional) string containing path of cookie
+ :raises KeyError: if cookie is not found
+ :raises CookieConflictError: if there are multiple cookies
+ that match name and optionally domain and path
+ :return: cookie.value
+ """
+ toReturn = None
+ for cookie in iter(self):
+ if cookie.name == name:
+ if domain is None or cookie.domain == domain:
+ if path is None or cookie.path == path:
+ if toReturn is not None:
+ # if there are multiple cookies that meet passed in criteria
+ raise CookieConflictError(
+ f"There are multiple cookies with name, {name!r}"
+ )
+ # we will eventually return this as long as no cookie conflict
+ toReturn = cookie.value
+
+ if toReturn is not None:
+ return toReturn
+ raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
+
+ def __getstate__(self) -> dict[str, Any]:
+ """Unlike a normal CookieJar, this class is pickleable."""
+ state = self.__dict__.copy()
+ # remove the unpickleable RLock object
+ state.pop("_cookies_lock")
+ return state
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ """Unlike a normal CookieJar, this class is pickleable."""
+ self.__dict__.update(state)
+ if "_cookies_lock" not in self.__dict__:
+ self._cookies_lock = threading.RLock()
+
+ def copy(self) -> RequestsCookieJar:
+ """Return a copy of this RequestsCookieJar."""
+ new_cj = RequestsCookieJar()
+ new_cj.set_policy(self.get_policy())
+ new_cj.update(self)
+ return new_cj
+
+ def get_policy(self) -> CookiePolicy:
+ """Return the CookiePolicy instance used."""
+ return self._policy
+
+
+def _copy_cookie_jar(jar: CookieJar | None) -> CookieJar | None: # type: ignore[reportUnusedFunction] # cross-module usage in models.py
+ if jar is None:
+ return None
+
+ if copy_method := getattr(jar, "copy", None):
+ # We're dealing with an instance of RequestsCookieJar
+ return copy_method()
+ # We're dealing with a generic CookieJar instance
+ new_jar = copy.copy(jar)
+ new_jar.clear()
+ for cookie in jar:
+ new_jar.set_cookie(copy.copy(cookie))
+ return new_jar
+
+
+def create_cookie(name: str, value: str, **kwargs: Any) -> Cookie:
+ """Make a cookie from underspecified parameters.
+
+ By default, the pair of `name` and `value` will be set for the domain ''
+ and sent on every request (this is sometimes called a "supercookie").
+ """
+ result: dict[str, Any] = {
+ "version": 0,
+ "name": name,
+ "value": value,
+ "port": None,
+ "domain": "",
+ "path": "/",
+ "secure": False,
+ "expires": None,
+ "discard": True,
+ "comment": None,
+ "comment_url": None,
+ "rest": {"HttpOnly": None},
+ "rfc2109": False,
+ }
+
+ badargs = set(kwargs) - set(result)
+ if badargs:
+ raise TypeError(
+ f"create_cookie() got unexpected keyword arguments: {list(badargs)}"
+ )
+
+ result.update(kwargs)
+ result["port_specified"] = bool(result["port"])
+ result["domain_specified"] = bool(result["domain"])
+ result["domain_initial_dot"] = result["domain"].startswith(".")
+ result["path_specified"] = bool(result["path"])
+
+ return cookielib.Cookie(**result)
+
+
+def morsel_to_cookie(morsel: Morsel[Any]) -> Cookie:
+ """Convert a Morsel object into a Cookie containing the one k/v pair."""
+
+ expires: int | None = None
+ if morsel["max-age"]:
+ try:
+ expires = int(time.time() + int(morsel["max-age"]))
+ except ValueError:
+ raise TypeError(f"max-age: {morsel['max-age']} must be integer")
+ elif morsel["expires"]:
+ time_template = "%a, %d-%b-%Y %H:%M:%S GMT"
+ expires = calendar.timegm(time.strptime(morsel["expires"], time_template))
+ return create_cookie(
+ comment=morsel["comment"],
+ comment_url=bool(morsel["comment"]),
+ discard=False,
+ domain=morsel["domain"],
+ expires=expires,
+ name=morsel.key,
+ path=morsel["path"],
+ port=None,
+ rest={"HttpOnly": morsel["httponly"]},
+ rfc2109=False,
+ secure=bool(morsel["secure"]),
+ value=morsel.value,
+ version=morsel["version"] or 0,
+ )
+
+
+_CookieJarT = TypeVar("_CookieJarT", bound=CookieJar)
+
+
+@overload
+def cookiejar_from_dict(
+ cookie_dict: dict[str, str] | None,
+ cookiejar: None = None,
+ overwrite: bool = True,
+) -> RequestsCookieJar: ...
+
+
+@overload
+def cookiejar_from_dict(
+ cookie_dict: dict[str, str] | None,
+ cookiejar: _CookieJarT,
+ overwrite: bool = True,
+) -> _CookieJarT: ...
+
+
+def cookiejar_from_dict(
+ cookie_dict: dict[str, str] | None,
+ cookiejar: CookieJar | None = None,
+ overwrite: bool = True,
+) -> CookieJar:
+ """Returns a CookieJar from a key/value dictionary.
+
+ :param cookie_dict: Dict of key/values to insert into CookieJar.
+ :param cookiejar: (optional) A cookiejar to add the cookies to.
+ :param overwrite: (optional) If False, will not replace cookies
+ already in the jar with new ones.
+ :rtype: CookieJar
+ """
+ if cookiejar is None:
+ cookiejar = RequestsCookieJar()
+
+ if cookie_dict is not None:
+ names_from_jar = [cookie.name for cookie in cookiejar]
+ for name in cookie_dict:
+ if overwrite or (name not in names_from_jar):
+ cookiejar.set_cookie(create_cookie(name, cookie_dict[name]))
+
+ return cookiejar
+
+
+def merge_cookies(
+ cookiejar: CookieJar, cookies: dict[str, str] | CookieJar | None
+) -> CookieJar:
+ """Add cookies to cookiejar and returns a merged CookieJar.
+
+ :param cookiejar: CookieJar object to add the cookies to.
+ :param cookies: Dictionary or CookieJar object to be added.
+ :rtype: CookieJar
+ """
+ if not isinstance(cookiejar, cookielib.CookieJar): # type: ignore[reportUnnecessaryIsInstance] # runtime guard
+ raise ValueError("You can only merge into CookieJar")
+
+ if isinstance(cookies, dict):
+ cookiejar = cookiejar_from_dict(cookies, cookiejar=cookiejar, overwrite=False)
+ elif isinstance(cookies, cookielib.CookieJar):
+ if update_method := getattr(cookiejar, "update", None):
+ update_method(cookies)
+ else:
+ for cookie_in_jar in cookies:
+ cookiejar.set_cookie(cookie_in_jar)
+
+ return cookiejar
diff --git a/venv/Lib/site-packages/requests/exceptions.py b/venv/Lib/site-packages/requests/exceptions.py
new file mode 100644
index 0000000..cb5e951
--- /dev/null
+++ b/venv/Lib/site-packages/requests/exceptions.py
@@ -0,0 +1,162 @@
+"""
+requests.exceptions
+~~~~~~~~~~~~~~~~~~~
+
+This module contains the set of Requests' exceptions.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+from urllib3.exceptions import HTTPError as BaseHTTPError
+
+from .compat import JSONDecodeError as CompatJSONDecodeError
+
+if TYPE_CHECKING:
+ from .models import PreparedRequest, Request, Response
+
+
+class RequestException(IOError):
+ """There was an ambiguous exception that occurred while handling your
+ request.
+ """
+
+ response: Response | None
+ request: Request | PreparedRequest | None
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ """Initialize RequestException with `request` and `response` objects."""
+ response: Response | None = kwargs.pop("response", None)
+ self.response = response
+ self.request = kwargs.pop("request", None)
+ if response is not None and not self.request and hasattr(response, "request"):
+ self.request = response.request
+ super().__init__(*args, **kwargs)
+
+
+class InvalidJSONError(RequestException):
+ """A JSON error occurred."""
+
+
+class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
+ """Couldn't decode the text into json"""
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ """
+ Construct the JSONDecodeError instance first with all
+ args. Then use it's args to construct the IOError so that
+ the json specific args aren't used as IOError specific args
+ and the error message from JSONDecodeError is preserved.
+ """
+ CompatJSONDecodeError.__init__(self, *args)
+ InvalidJSONError.__init__(self, *self.args, **kwargs)
+
+ def __reduce__(self) -> tuple[Any, ...] | str:
+ """
+ The __reduce__ method called when pickling the object must
+ be the one from the JSONDecodeError (be it json/simplejson)
+ as it expects all the arguments for instantiation, not just
+ one like the IOError, and the MRO would by default call the
+ __reduce__ method from the IOError due to the inheritance order.
+ """
+ return CompatJSONDecodeError.__reduce__(self)
+
+
+class HTTPError(RequestException):
+ """An HTTP error occurred."""
+
+
+class ConnectionError(RequestException):
+ """A Connection error occurred."""
+
+
+class ProxyError(ConnectionError):
+ """A proxy error occurred."""
+
+
+class SSLError(ConnectionError):
+ """An SSL error occurred."""
+
+
+class Timeout(RequestException):
+ """The request timed out.
+
+ Catching this error will catch both
+ :exc:`~requests.exceptions.ConnectTimeout` and
+ :exc:`~requests.exceptions.ReadTimeout` errors.
+ """
+
+
+class ConnectTimeout(ConnectionError, Timeout):
+ """The request timed out while trying to connect to the remote server.
+
+ Requests that produced this error are safe to retry.
+ """
+
+
+class ReadTimeout(Timeout):
+ """The server did not send any data in the allotted amount of time."""
+
+
+class URLRequired(RequestException):
+ """A valid URL is required to make a request."""
+
+
+class TooManyRedirects(RequestException):
+ """Too many redirects."""
+
+
+class MissingSchema(RequestException, ValueError):
+ """The URL scheme (e.g. http or https) is missing."""
+
+
+class InvalidSchema(RequestException, ValueError):
+ """The URL scheme provided is either invalid or unsupported."""
+
+
+class InvalidURL(RequestException, ValueError):
+ """The URL provided was somehow invalid."""
+
+
+class InvalidHeader(RequestException, ValueError):
+ """The header value provided was somehow invalid."""
+
+
+class InvalidProxyURL(InvalidURL):
+ """The proxy URL provided is invalid."""
+
+
+class ChunkedEncodingError(RequestException):
+ """The server declared chunked encoding but sent an invalid chunk."""
+
+
+class ContentDecodingError(RequestException, BaseHTTPError):
+ """Failed to decode response content."""
+
+
+class StreamConsumedError(RequestException, TypeError):
+ """The content for this response was already consumed."""
+
+
+class RetryError(RequestException):
+ """Custom retries logic failed"""
+
+
+class UnrewindableBodyError(RequestException):
+ """Requests encountered an error when trying to rewind a body."""
+
+
+# Warnings
+
+
+class RequestsWarning(Warning):
+ """Base warning for Requests."""
+
+
+class FileModeWarning(RequestsWarning, DeprecationWarning):
+ """A file was opened in text mode, but Requests determined its binary length."""
+
+
+class RequestsDependencyWarning(RequestsWarning):
+ """An imported dependency doesn't match the expected version range."""
diff --git a/venv/Lib/site-packages/requests/help.py b/venv/Lib/site-packages/requests/help.py
new file mode 100644
index 0000000..9269cc7
--- /dev/null
+++ b/venv/Lib/site-packages/requests/help.py
@@ -0,0 +1,134 @@
+"""Module containing bug report helper(s)."""
+
+# pyright: reportUnknownMemberType=false
+
+import json
+import platform
+import ssl
+import sys
+from typing import Any
+
+import idna
+import urllib3
+
+from . import __version__ as requests_version
+
+try:
+ import charset_normalizer
+except ImportError:
+ charset_normalizer = None
+
+try:
+ import chardet # type: ignore[import-not-found]
+except ImportError:
+ chardet = None
+
+try:
+ from urllib3.contrib import pyopenssl
+except ImportError:
+ pyopenssl = None
+ OpenSSL = None
+ cryptography = None
+else:
+ import cryptography # type: ignore[import-not-found]
+ import OpenSSL # type: ignore[import-not-found]
+
+
+def _implementation():
+ """Return a dict with the Python implementation and version.
+
+ Provide both the name and the version of the Python implementation
+ currently running. For example, on CPython 3.10.3 it will return
+ {'name': 'CPython', 'version': '3.10.3'}.
+
+ This function works best on CPython and PyPy: in particular, it probably
+ doesn't work for Jython or IronPython. Future investigation should be done
+ to work out the correct shape of the code for those platforms.
+ """
+ implementation = platform.python_implementation()
+
+ if implementation == "CPython":
+ implementation_version = platform.python_version()
+ elif implementation == "PyPy":
+ pypy = sys.pypy_version_info # type: ignore[attr-defined]
+ implementation_version = f"{pypy.major}.{pypy.minor}.{pypy.micro}"
+ if sys.pypy_version_info.releaselevel != "final": # type: ignore[attr-defined]
+ implementation_version = "".join(
+ [implementation_version, sys.pypy_version_info.releaselevel] # type: ignore[attr-defined]
+ )
+ elif implementation == "Jython":
+ implementation_version = platform.python_version() # Complete Guess
+ elif implementation == "IronPython":
+ implementation_version = platform.python_version() # Complete Guess
+ else:
+ implementation_version = "Unknown"
+
+ return {"name": implementation, "version": implementation_version}
+
+
+def info() -> dict[str, Any]:
+ """Generate information for a bug report."""
+ try:
+ platform_info = {
+ "system": platform.system(),
+ "release": platform.release(),
+ }
+ except OSError:
+ platform_info = {
+ "system": "Unknown",
+ "release": "Unknown",
+ }
+
+ implementation_info = _implementation()
+ urllib3_info = {"version": urllib3.__version__} # type: ignore[reportPrivateImportUsage]
+ charset_normalizer_info = {"version": None}
+ chardet_info: dict[str, str | None] = {"version": None}
+ if charset_normalizer:
+ charset_normalizer_info = {"version": charset_normalizer.__version__}
+ if chardet:
+ chardet_info = {"version": chardet.__version__}
+
+ pyopenssl_info: dict[str, str | None] = {
+ "version": None,
+ "openssl_version": "",
+ }
+ if OpenSSL:
+ pyopenssl_info = {
+ "version": OpenSSL.__version__,
+ "openssl_version": f"{OpenSSL.SSL.OPENSSL_VERSION_NUMBER:x}",
+ }
+ cryptography_info = {
+ "version": getattr(cryptography, "__version__", ""),
+ }
+ idna_info = {
+ "version": getattr(idna, "__version__", ""),
+ }
+
+ system_ssl = ssl.OPENSSL_VERSION_NUMBER
+ system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""} # type: ignore[reportUnnecessaryComparison]
+
+ return {
+ "platform": platform_info,
+ "implementation": implementation_info,
+ "system_ssl": system_ssl_info,
+ "using_pyopenssl": pyopenssl is not None,
+ "using_charset_normalizer": chardet is None,
+ "pyOpenSSL": pyopenssl_info,
+ "urllib3": urllib3_info,
+ "chardet": chardet_info,
+ "charset_normalizer": charset_normalizer_info,
+ "cryptography": cryptography_info,
+ "idna": idna_info,
+ "requests": {
+ "version": requests_version,
+ },
+ }
+
+
+def main():
+ """Pretty-print the bug information as JSON."""
+ print(json.dumps(info(), sort_keys=True, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/venv/Lib/site-packages/requests/hooks.py b/venv/Lib/site-packages/requests/hooks.py
new file mode 100644
index 0000000..11ff9e9
--- /dev/null
+++ b/venv/Lib/site-packages/requests/hooks.py
@@ -0,0 +1,48 @@
+"""
+requests.hooks
+~~~~~~~~~~~~~~
+
+This module provides the capabilities for the Requests hooks system.
+
+Available hooks:
+
+``response``:
+ The response generated from a Request.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Iterable
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from . import _types as _t
+ from .models import Response
+
+HOOKS: list[str] = ["response"]
+
+
+def default_hooks() -> dict[str, list[_t.HookType]]:
+ return {event: [] for event in HOOKS}
+
+
+# TODO: response is the only one
+
+
+def dispatch_hook(
+ key: str,
+ hooks: _t.HooksInputType | None,
+ hook_data: Response,
+ **kwargs: Any,
+) -> Response:
+ """Dispatches a hook dictionary on a given piece of data."""
+ hooks_dict = hooks or {}
+ hook_list: Iterable[_t.HookType] | _t.HookType | None = hooks_dict.get(key)
+ if hook_list:
+ if isinstance(hook_list, Callable):
+ hook_list = [hook_list]
+ for hook in hook_list:
+ _hook_data = hook(hook_data, **kwargs)
+ if _hook_data is not None:
+ hook_data = _hook_data
+ return hook_data
diff --git a/venv/Lib/site-packages/requests/models.py b/venv/Lib/site-packages/requests/models.py
new file mode 100644
index 0000000..4142f2a
--- /dev/null
+++ b/venv/Lib/site-packages/requests/models.py
@@ -0,0 +1,1180 @@
+"""
+requests.models
+~~~~~~~~~~~~~~~
+
+This module contains the primary objects that power Requests.
+"""
+
+from __future__ import annotations
+
+import datetime
+
+# Import encoding now, to avoid implicit import later.
+# Implicit import within threads may cause LookupError when standard library is in a ZIP,
+# such as in Embedded Python. See https://github.com/psf/requests/issues/3578.
+import encodings.idna # noqa: F401 # type: ignore[reportUnusedImport]
+from collections.abc import Callable, Generator, Iterable, Iterator, Mapping
+from io import UnsupportedOperation
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Final,
+ Literal,
+ cast,
+ overload,
+)
+
+from urllib3.exceptions import (
+ DecodeError,
+ LocationParseError,
+ ProtocolError,
+ ReadTimeoutError,
+ SSLError,
+)
+from urllib3.fields import RequestField
+from urllib3.filepost import encode_multipart_formdata
+from urllib3.util import parse_url
+
+from ._internal_utils import to_native_string, unicode_is_ascii
+from ._types import SupportsRead as _SupportsRead
+from .auth import HTTPBasicAuth
+from .compat import (
+ JSONDecodeError,
+ basestring,
+ builtin_str,
+ chardet,
+ cookielib,
+ urlencode,
+ urlsplit,
+ urlunparse,
+)
+from .compat import json as complexjson
+from .cookies import (
+ _copy_cookie_jar, # type: ignore[reportPrivateUsage]
+ cookiejar_from_dict,
+ get_cookie_header,
+)
+from .exceptions import (
+ ChunkedEncodingError,
+ ConnectionError,
+ ContentDecodingError,
+ HTTPError,
+ InvalidJSONError,
+ InvalidURL,
+ MissingSchema,
+ StreamConsumedError,
+)
+from .exceptions import JSONDecodeError as RequestsJSONDecodeError
+from .exceptions import SSLError as RequestsSSLError
+from .hooks import default_hooks
+from .status_codes import codes
+from .structures import CaseInsensitiveDict
+from .utils import (
+ check_header_validity,
+ get_auth_from_url,
+ guess_filename,
+ guess_json_utf,
+ iter_slices,
+ parse_header_links,
+ requote_uri,
+ stream_decode_response_unicode,
+ super_len,
+ to_key_val_list,
+)
+
+if TYPE_CHECKING:
+ from http.cookiejar import CookieJar
+
+ from typing_extensions import Self
+
+ from . import _types as _t
+ from .adapters import HTTPAdapter
+ from .cookies import RequestsCookieJar
+
+#: The set of HTTP status codes that indicate an automatically
+#: processable redirect.
+REDIRECT_STATI: Final[tuple[int, ...]] = ( # type: ignore[assignment]
+ codes.moved, # 301
+ codes.found, # 302
+ codes.other, # 303
+ codes.temporary_redirect, # 307
+ codes.permanent_redirect, # 308
+)
+
+DEFAULT_REDIRECT_LIMIT: int = 30
+CONTENT_CHUNK_SIZE: int = 10 * 1024
+ITER_CHUNK_SIZE: int = 512
+
+
+class RequestEncodingMixin:
+ url: str | None
+
+ @property
+ def path_url(self) -> str:
+ """Build the path URL to use."""
+
+ url: list[str] = []
+
+ p = urlsplit(cast(str, self.url))
+
+ path = p.path
+ if not path:
+ path = "/"
+
+ url.append(path)
+
+ query = p.query
+ if query:
+ url.append("?")
+ url.append(query)
+
+ return "".join(url)
+
+ @overload
+ @staticmethod
+ def _encode_params(data: str) -> str: ...
+
+ @overload
+ @staticmethod
+ def _encode_params(data: bytes) -> bytes: ...
+
+ @overload
+ @staticmethod
+ def _encode_params(
+ data: _t.SupportsRead[str | bytes],
+ ) -> _t.SupportsRead[str | bytes]: ...
+
+ @overload
+ @staticmethod
+ def _encode_params(data: _t.KVDataType) -> str: ...
+
+ @staticmethod
+ def _encode_params(
+ data: _t.EncodableDataType,
+ ) -> str | bytes | _t.SupportsRead[str | bytes]:
+ """Encode parameters in a piece of data.
+
+ Will successfully encode parameters when passed as a dict or a list of
+ 2-tuples. Order is retained if data is a list of 2-tuples but arbitrary
+ if parameters are supplied as a dict.
+ """
+
+ if isinstance(data, (str, bytes)):
+ return data
+ elif isinstance(data, _SupportsRead):
+ return data
+ elif hasattr(data, "__iter__"):
+ result: list[tuple[bytes, bytes]] = []
+ for k, vs in to_key_val_list(data):
+ if isinstance(vs, basestring) or not hasattr(vs, "__iter__"):
+ vs = [vs]
+ for v in vs:
+ if v is not None:
+ result.append(
+ (
+ k.encode("utf-8") if isinstance(k, str) else k,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
+ return urlencode(result, doseq=True)
+ else:
+ return data # type: ignore[return-value] # unreachable for valid _t.DataType
+
+ @staticmethod
+ def _encode_files(
+ files: _t.FilesType, data: _t.RawDataType | None
+ ) -> tuple[bytes, str]:
+ """Build the body for a multipart/form-data request.
+
+ Will successfully encode files when passed as a dict or a list of
+ tuples. Order is retained if data is a list of tuples but arbitrary
+ if parameters are supplied as a dict.
+ The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)
+ or 4-tuples (filename, fileobj, contentype, custom_headers).
+ """
+ if not files:
+ raise ValueError("Files must be provided.")
+ elif isinstance(data, basestring):
+ raise ValueError("Data must not be a string.")
+
+ new_fields: list[RequestField | tuple[str, bytes]] = []
+ fields = to_key_val_list(data or {})
+ files = to_key_val_list(files or {})
+
+ for field, val in fields:
+ if isinstance(val, basestring) or not hasattr(val, "__iter__"):
+ val = [val]
+ for v in val:
+ if v is not None:
+ # Don't call str() on bytestrings: in Py3 it all goes wrong.
+ if not isinstance(v, bytes):
+ v = str(v)
+
+ new_fields.append(
+ (
+ field.decode("utf-8")
+ if isinstance(field, bytes)
+ else field,
+ v.encode("utf-8") if isinstance(v, str) else v,
+ )
+ )
+
+ for k, v in files:
+ # support for explicit filename
+ ft = None
+ fh = None
+ if isinstance(v, (tuple, list)):
+ if len(v) == 2:
+ fn, fp = v
+ elif len(v) == 3:
+ fn, fp, ft = v
+ else:
+ fn, fp, ft, fh = v
+ else:
+ fn = guess_filename(v) or k
+ fp = v
+
+ if isinstance(fp, (str, bytes, bytearray)):
+ fdata = fp
+ elif isinstance(fp, _SupportsRead): # type: ignore[reportUnnecessaryIsInstance] # defensive check for untyped callers
+ fdata = fp.read()
+ elif fp is None: # type: ignore[reportUnnecessaryComparison] # defensive check for untyped callers
+ continue
+ else:
+ fdata = fp
+
+ rf = RequestField(name=k, data=fdata, filename=fn, headers=fh)
+ rf.make_multipart(content_type=ft)
+ new_fields.append(rf)
+
+ body, content_type = encode_multipart_formdata(new_fields)
+
+ return body, content_type
+
+
+class RequestHooksMixin:
+ hooks: dict[str, list[_t.HookType]]
+
+ def register_hook(
+ self, event: str, hook: Iterable[_t.HookType] | _t.HookType
+ ) -> None:
+ """Properly register a hook."""
+
+ if event not in self.hooks:
+ raise ValueError(f'Unsupported event specified, with event name "{event}"')
+
+ if isinstance(hook, Callable):
+ self.hooks[event].append(hook)
+ elif hasattr(hook, "__iter__"):
+ self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) # type: ignore[reportUnnecessaryIsInstance] # defensive runtime filter
+
+ def deregister_hook(self, event: str, hook: _t.HookType) -> bool:
+ """Deregister a previously registered hook.
+ Returns True if the hook existed, False if not.
+ """
+
+ try:
+ self.hooks[event].remove(hook)
+ return True
+ except ValueError:
+ return False
+
+
+class Request(RequestHooksMixin):
+ """A user-created :class:`Request ` object.
+
+ Used to prepare a :class:`PreparedRequest `, which is sent to the server.
+
+ :param method: HTTP method to use.
+ :param url: URL to send.
+ :param headers: dictionary of headers to send.
+ :param files: dictionary of {filename: fileobject} files to multipart upload.
+ :param data: the body to attach to the request. If a dictionary or
+ list of tuples ``[(key, value)]`` is provided, form-encoding will
+ take place.
+ :param json: json for the body to attach to the request (if files or data is not specified).
+ :param params: URL parameters to append to the URL. If a dictionary or
+ list of tuples ``[(key, value)]`` is provided, form-encoding will
+ take place.
+ :param auth: Auth handler or (user, pass) tuple.
+ :param cookies: dictionary or CookieJar of cookies to attach to this request.
+ :param hooks: dictionary of callback hooks, for internal usage.
+
+ Usage::
+
+ >>> import requests
+ >>> req = requests.Request('GET', 'https://httpbin.org/get')
+ >>> req.prepare()
+
+ """
+
+ method: str | None
+ url: _t.UriType | None
+ headers: CaseInsensitiveDict[str] | Mapping[str, str | bytes] | None
+ files: _t.FilesType
+ data: _t.DataType
+ json: _t.JsonType
+ params: _t.ParamsType
+ auth: _t.AuthType
+ cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
+
+ def __init__(
+ self,
+ method: str | None = None,
+ url: _t.UriType | None = None,
+ headers: Mapping[str, str | bytes] | None = None,
+ files: _t.FilesType = None,
+ data: _t.DataType = None,
+ params: _t.ParamsType = None,
+ auth: _t.AuthType = None,
+ cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None,
+ hooks: _t.HooksInputType | None = None,
+ json: _t.JsonType = None,
+ ) -> None:
+ # Default empty dicts for dict params.
+ data = [] if data is None else data
+ files = [] if files is None else files
+ headers = {} if headers is None else headers
+ params = {} if params is None else params
+ hooks = {} if hooks is None else hooks
+
+ self.hooks = default_hooks()
+ for k, v in list(hooks.items()):
+ self.register_hook(event=k, hook=v)
+
+ self.method = method
+ self.url = url
+ self.headers = headers
+ self.files = files
+ self.data = data
+ self.json = json
+ self.params = params
+ self.auth = auth
+ self.cookies = cookies
+
+ def __repr__(self) -> str:
+ return f""
+
+ def prepare(self) -> PreparedRequest:
+ """Constructs a :class:`PreparedRequest ` for transmission and returns it."""
+ p = PreparedRequest()
+ p.prepare(
+ method=self.method,
+ url=self.url,
+ headers=self.headers,
+ files=self.files,
+ data=self.data,
+ json=self.json,
+ params=self.params,
+ auth=self.auth,
+ cookies=self.cookies,
+ hooks=self.hooks,
+ )
+ return p
+
+
+class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
+ """The fully mutable :class:`PreparedRequest ` object,
+ containing the exact bytes that will be sent to the server.
+
+ Instances are generated from a :class:`Request ` object, and
+ should not be instantiated manually; doing so may produce undesirable
+ effects.
+
+ Usage::
+
+ >>> import requests
+ >>> req = requests.Request('GET', 'https://httpbin.org/get')
+ >>> r = req.prepare()
+ >>> r
+
+
+ >>> s = requests.Session()
+ >>> s.send(r)
+
+ """
+
+ method: str | None
+ url: str | None
+ headers: CaseInsensitiveDict[str | bytes]
+ _cookies: RequestsCookieJar | CookieJar | None
+ body: _t.BodyType
+ hooks: dict[str, list[_t.HookType]]
+ _body_position: int | object | None
+
+ def __init__(self) -> None:
+ #: HTTP verb to send to the server.
+ self.method = None
+ #: HTTP URL to send the request to.
+ self.url = None
+ #: dictionary of HTTP headers.
+ self.headers = None # type: ignore[assignment]
+ # The `CookieJar` used to create the Cookie header will be stored here
+ # after prepare_cookies is called
+ self._cookies = None
+ #: request body to send to the server.
+ self.body = None
+ #: dictionary of callback hooks, for internal usage.
+ self.hooks = default_hooks()
+ #: integer denoting starting position of a readable file-like body.
+ self._body_position = None
+
+ def prepare(
+ self,
+ method: str | None = None,
+ url: _t.UriType | None = None,
+ headers: Mapping[str, str | bytes] | None = None,
+ files: _t.FilesType = None,
+ data: _t.DataType = None,
+ params: _t.ParamsType = None,
+ auth: _t.AuthType = None,
+ cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None,
+ hooks: _t.HooksInputType | None = None,
+ json: _t.JsonType = None,
+ ) -> None:
+ """Prepares the entire request with the given parameters."""
+
+ url = cast("_t.UriType", url)
+ self.prepare_method(method)
+ self.prepare_url(url, params)
+ self.prepare_headers(headers)
+ self.prepare_cookies(cookies)
+ self.prepare_body(data, files, json)
+ self.prepare_auth(auth, url)
+
+ # Note that prepare_auth must be last to enable authentication schemes
+ # such as OAuth to work on a fully prepared request.
+
+ # This MUST go after prepare_auth. Authenticators could add a hook
+ self.prepare_hooks(hooks)
+
+ def __repr__(self) -> str:
+ return f""
+
+ def copy(self) -> PreparedRequest:
+ p = PreparedRequest()
+ p.method = self.method
+ p.url = self.url
+ p.headers = self.headers.copy() if self.headers is not None else None # type: ignore[assignment]
+ p._cookies = _copy_cookie_jar(self._cookies)
+ p.body = self.body
+ p.hooks = self.hooks
+ p._body_position = self._body_position
+ return p
+
+ def prepare_method(self, method: str | None) -> None:
+ """Prepares the given HTTP method."""
+ self.method = method
+ if self.method is not None:
+ self.method = to_native_string(self.method.upper())
+
+ @staticmethod
+ def _get_idna_encoded_host(host: str) -> str:
+ import idna
+
+ try:
+ host = idna.encode(host, uts46=True).decode("utf-8")
+ except idna.IDNAError:
+ raise UnicodeError
+ return host
+
+ def prepare_url(
+ self,
+ url: _t.UriType,
+ params: _t.ParamsType,
+ ) -> None:
+ """Prepares the given HTTP URL."""
+ #: Accept objects that have string representations.
+ #: We're unable to blindly call unicode/str functions
+ #: as this will include the bytestring indicator (b'')
+ #: on python 3.x.
+ #: https://github.com/psf/requests/pull/2238
+ if isinstance(url, bytes):
+ url = url.decode("utf8")
+ else:
+ url = str(url)
+
+ # Remove leading whitespaces from url
+ url = url.lstrip()
+
+ # Don't do any URL preparation for non-HTTP schemes like `mailto`,
+ # `data` etc to work around exceptions from `url_parse`, which
+ # handles RFC 3986 only.
+ if ":" in url and not url.lower().startswith("http"):
+ self.url = url
+ return
+
+ # Support for unicode domain names and paths.
+ try:
+ scheme, auth, host, port, path, query, fragment = parse_url(url)
+ except LocationParseError as e:
+ raise InvalidURL(*e.args)
+
+ if not scheme:
+ raise MissingSchema(
+ f"Invalid URL {url!r}: No scheme supplied. "
+ f"Perhaps you meant https://{url}?"
+ )
+
+ if not host:
+ raise InvalidURL(f"Invalid URL {url!r}: No host supplied")
+
+ # In general, we want to try IDNA encoding the hostname if the string contains
+ # non-ASCII characters. This allows users to automatically get the correct IDNA
+ # behaviour. For strings containing only ASCII characters, we need to also verify
+ # it doesn't start with a wildcard (*), before allowing the unencoded hostname.
+ if not unicode_is_ascii(host):
+ try:
+ host = self._get_idna_encoded_host(host)
+ except UnicodeError:
+ raise InvalidURL("URL has an invalid label.")
+ elif host.startswith(("*", ".")):
+ raise InvalidURL("URL has an invalid label.")
+
+ # Carefully reconstruct the network location
+ netloc = auth or ""
+ if netloc:
+ netloc += "@"
+ netloc += host
+ if port:
+ netloc += f":{port}"
+
+ # Bare domains aren't valid URLs.
+ if not path:
+ path = "/"
+
+ if isinstance(params, (str, bytes)):
+ params = to_native_string(params)
+
+ if params is not None:
+ enc_params = self._encode_params(params)
+ else:
+ enc_params = ""
+
+ if enc_params:
+ if query:
+ query = f"{query}&{enc_params}"
+ else:
+ query = enc_params
+
+ url = requote_uri(urlunparse((scheme, netloc, path, "", query, fragment)))
+ self.url = url
+
+ def prepare_headers(self, headers: Mapping[str, str | bytes] | None) -> None:
+ """Prepares the given HTTP headers."""
+
+ self.headers = CaseInsensitiveDict()
+ if headers:
+ for header in headers.items():
+ # Raise exception on invalid header value.
+ check_header_validity(header)
+ name, value = header
+ self.headers[to_native_string(name)] = value
+
+ def prepare_body(
+ self, data: _t.DataType, files: _t.FilesType, json: _t.JsonType = None
+ ) -> None:
+ """Prepares the given HTTP body data."""
+
+ # Check if file, fo, generator, iterator.
+ # If not, run through normal process.
+
+ # Nottin' on you.
+ body = None
+ content_type = None
+
+ if not data and json is not None:
+ # urllib3 requires a bytes-like body. Python 2's json.dumps
+ # provides this natively, but Python 3 gives a Unicode string.
+ content_type = "application/json"
+
+ try:
+ body = complexjson.dumps(json, allow_nan=False)
+ except ValueError as ve:
+ raise InvalidJSONError(ve, request=self)
+
+ if not isinstance(body, bytes):
+ body = body.encode("utf-8")
+
+ if isinstance(data, Iterable) and not isinstance(
+ data, (str, bytes, list, tuple, Mapping)
+ ):
+ try:
+ length = super_len(data)
+ except (TypeError, AttributeError, UnsupportedOperation):
+ length = None
+
+ body = data
+
+ if getattr(body, "tell", None) is not None:
+ # Record the current file position before reading.
+ # This will allow us to rewind a file in the event
+ # of a redirect.
+ try:
+ self._body_position = body.tell() # type: ignore[union-attr] # guarded by getattr check
+ except OSError:
+ # This differentiates from None, allowing us to catch
+ # a failed `tell()` later when trying to rewind the body
+ self._body_position = object()
+
+ if files:
+ raise NotImplementedError(
+ "Streamed bodies and files are mutually exclusive."
+ )
+
+ if length:
+ self.headers["Content-Length"] = builtin_str(length)
+ else:
+ self.headers["Transfer-Encoding"] = "chunked"
+ else:
+ # After is_stream filtering, remaining data is raw (not streamed)
+ raw_data = cast("_t.RawDataType | None", data)
+
+ # Multi-part file uploads.
+ if files:
+ (body, content_type) = self._encode_files(files, raw_data)
+ else:
+ if raw_data:
+ body = self._encode_params(raw_data)
+ if isinstance(data, basestring) or isinstance(data, _SupportsRead):
+ content_type = None
+ else:
+ content_type = "application/x-www-form-urlencoded"
+
+ self.prepare_content_length(body)
+
+ # Add content-type if it wasn't explicitly provided.
+ if content_type and ("content-type" not in self.headers):
+ self.headers["Content-Type"] = content_type
+
+ self.body = body # type: ignore[assignment] # body transforms from DataType to BodyType
+
+ def prepare_content_length(self, body: _t.BodyType) -> None:
+ """Prepare Content-Length header based on request method and body"""
+ if body is not None:
+ length = super_len(body)
+ if length:
+ # If length exists, set it. Otherwise, we fallback
+ # to Transfer-Encoding: chunked.
+ self.headers["Content-Length"] = builtin_str(length)
+ elif (
+ self.method not in ("GET", "HEAD")
+ and self.headers.get("Content-Length") is None
+ ):
+ # Set Content-Length to 0 for methods that can have a body
+ # but don't provide one. (i.e. not GET or HEAD)
+ self.headers["Content-Length"] = "0"
+
+ def prepare_auth(
+ self,
+ auth: _t.AuthType,
+ url: _t.UriType = "",
+ ) -> None:
+ """Prepares the given HTTP auth data."""
+
+ # If no Auth is explicitly provided, extract it from the URL first.
+ if auth is None:
+ url_auth = get_auth_from_url(cast(str, self.url))
+ auth = url_auth if any(url_auth) else None
+
+ if auth:
+ if isinstance(auth, tuple) and len(auth) == 2: # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType
+ # special-case basic HTTP auth
+ auth_handler = HTTPBasicAuth(*auth) # type: ignore[arg-type] # pyright widens tuple from Callable in AuthType
+ else:
+ # TODO: can be fixed by flipping the conditionals
+ auth_handler = cast("Callable[..., PreparedRequest]", auth)
+
+ # Allow auth to make its changes.
+ r = auth_handler(self)
+
+ # Update self to reflect the auth changes.
+ self.__dict__.update(r.__dict__)
+
+ # Recompute Content-Length
+ self.prepare_content_length(self.body)
+
+ def prepare_cookies(
+ self, cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
+ ) -> None:
+ """Prepares the given HTTP cookie data.
+
+ This function eventually generates a ``Cookie`` header from the
+ given cookies using cookielib. Due to cookielib's design, the header
+ will not be regenerated if it already exists, meaning this function
+ can only be called once for the life of the
+ :class:`PreparedRequest ` object. Any subsequent calls
+ to ``prepare_cookies`` will have no actual effect, unless the "Cookie"
+ header is removed beforehand.
+ """
+ if isinstance(cookies, cookielib.CookieJar):
+ self._cookies = cookies
+ else:
+ self._cookies = cookiejar_from_dict(cookies)
+
+ cookies_jar = cast("CookieJar", self._cookies)
+ cookie_header = get_cookie_header(cookies_jar, self)
+ if cookie_header is not None:
+ self.headers["Cookie"] = cookie_header
+
+ def prepare_hooks(self, hooks: _t.HooksInputType | None) -> None:
+ """Prepares the given hooks."""
+ # hooks can be passed as None to the prepare method and to this
+ # method. To prevent iterating over None, simply use an empty list
+ # if hooks is False-y
+ hooks = hooks or {}
+ for event in hooks:
+ self.register_hook(event, hooks[event])
+
+
+class Response:
+ """The :class:`Response ` object, which contains a
+ server's response to an HTTP request.
+ """
+
+ _content: bytes | Literal[False] | None
+ _content_consumed: bool
+ _next: PreparedRequest | None
+ status_code: int
+ headers: CaseInsensitiveDict[str]
+ raw: Any
+ url: str
+ encoding: str | None
+ history: list[Response]
+ reason: str | None
+ cookies: RequestsCookieJar
+ elapsed: datetime.timedelta
+ request: PreparedRequest
+ connection: HTTPAdapter
+
+ __attrs__: list[str] = [
+ "_content",
+ "status_code",
+ "headers",
+ "url",
+ "history",
+ "encoding",
+ "reason",
+ "cookies",
+ "elapsed",
+ "request",
+ ]
+
+ def __init__(self) -> None:
+ self._content = False
+ self._content_consumed = False
+ self._next = None
+
+ #: Integer Code of responded HTTP Status, e.g. 404 or 200.
+ self.status_code = None # type: ignore[assignment]
+
+ #: Case-insensitive Dictionary of Response Headers.
+ #: For example, ``headers['content-encoding']`` will return the
+ #: value of a ``'Content-Encoding'`` response header.
+ self.headers = CaseInsensitiveDict()
+
+ #: File-like object representation of response (for advanced usage).
+ #: Use of ``raw`` requires that ``stream=True`` be set on the request.
+ #: This requirement does not apply for use internally to Requests.
+ self.raw = None
+
+ #: Final URL location of Response.
+ self.url = None # type: ignore[assignment]
+
+ #: Encoding to decode with when accessing r.text.
+ self.encoding = None
+
+ #: A list of :class:`Response ` objects from
+ #: the history of the Request. Any redirect responses will end
+ #: up here. The list is sorted from the oldest to the most recent request.
+ self.history = []
+
+ #: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK".
+ self.reason = None
+
+ #: A CookieJar of Cookies the server sent back.
+ self.cookies = cookiejar_from_dict({})
+
+ #: The amount of time elapsed between sending the request
+ #: and the arrival of the response (as a timedelta).
+ #: This property specifically measures the time taken between sending
+ #: the first byte of the request and finishing parsing the headers. It
+ #: is therefore unaffected by consuming the response content or the
+ #: value of the ``stream`` keyword argument.
+ self.elapsed = datetime.timedelta(0)
+
+ #: The :class:`PreparedRequest ` object to which this
+ #: is a response.
+ self.request = None # type: ignore[assignment]
+
+ def __enter__(self) -> Self:
+ return self
+
+ def __exit__(self, *args: Any) -> None:
+ self.close()
+
+ def __getstate__(self) -> dict[str, Any]:
+ # Consume everything; accessing the content attribute makes
+ # sure the content has been fully read.
+ if not self._content_consumed:
+ self.content
+
+ return {attr: getattr(self, attr, None) for attr in self.__attrs__}
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ for name, value in state.items():
+ setattr(self, name, value)
+
+ # pickled objects do not have .raw
+ setattr(self, "_content_consumed", True)
+ setattr(self, "raw", None)
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __bool__(self) -> bool:
+ """Returns True if :attr:`status_code` is less than 400.
+
+ This attribute checks if the status code of the response is between
+ 400 and 600 to see if there was a client error or a server error. If
+ the status code, is between 200 and 400, this will return True. This
+ is **not** a check to see if the response code is ``200 OK``.
+ """
+ return self.ok
+
+ def __nonzero__(self) -> bool:
+ """Returns True if :attr:`status_code` is less than 400.
+
+ This attribute checks if the status code of the response is between
+ 400 and 600 to see if there was a client error or a server error. If
+ the status code, is between 200 and 400, this will return True. This
+ is **not** a check to see if the response code is ``200 OK``.
+ """
+ return self.ok
+
+ def __iter__(self) -> Iterator[bytes]:
+ """Allows you to use a response as an iterator."""
+ return self.iter_content(128)
+
+ @property
+ def ok(self) -> bool:
+ """Returns True if :attr:`status_code` is less than 400, False if not.
+
+ This attribute checks if the status code of the response is between
+ 400 and 600 to see if there was a client error or a server error. If
+ the status code is between 200 and 400, this will return True. This
+ is **not** a check to see if the response code is ``200 OK``.
+ """
+ try:
+ self.raise_for_status()
+ except HTTPError:
+ return False
+ return True
+
+ @property
+ def is_redirect(self) -> bool:
+ """True if this Response is a well-formed HTTP redirect that could have
+ been processed automatically (by :meth:`Session.resolve_redirects`).
+ """
+ return "location" in self.headers and self.status_code in REDIRECT_STATI
+
+ @property
+ def is_permanent_redirect(self) -> bool:
+ """True if this Response one of the permanent versions of redirect."""
+ return "location" in self.headers and self.status_code in (
+ codes.moved_permanently,
+ codes.permanent_redirect,
+ )
+
+ @property
+ def next(self) -> PreparedRequest | None:
+ """Returns a PreparedRequest for the next request in a redirect chain, if there is one."""
+ return self._next
+
+ @property
+ def apparent_encoding(self) -> str | None:
+ """The apparent encoding, provided by the charset_normalizer or chardet libraries."""
+ if chardet is not None:
+ return chardet.detect(self.content)["encoding"]
+ else:
+ # If no character detection library is available, we'll fall back
+ # to a standard Python utf-8 str.
+ return "utf-8"
+
+ @overload
+ def iter_content(
+ self, chunk_size: int | None = 1, decode_unicode: Literal[False] = False
+ ) -> Iterator[bytes]: ...
+ @overload
+ def iter_content(
+ self, chunk_size: int | None = 1, *, decode_unicode: Literal[True]
+ ) -> Iterator[str | bytes]: ...
+ def iter_content(
+ self, chunk_size: int | None = 1, decode_unicode: bool = False
+ ) -> Iterator[str | bytes]:
+ """Iterates over the response data. When stream=True is set on the
+ request, this avoids reading the content at once into memory for
+ large responses. The chunk size is the number of bytes it should
+ read into memory. This is not necessarily the length of each item
+ returned as decoding can take place.
+
+ chunk_size must be of type int or None. A value of None will
+ function differently depending on the value of `stream`.
+ stream=True will read data as it arrives in whatever size the
+ chunks are received. If stream=False, data is returned as
+ a single chunk.
+
+ If decode_unicode is True, content will be decoded using encoding
+ information from the response. If no encoding information is available,
+ bytes will be returned. This can be bypassed by manually setting
+ `encoding` on the response.
+ """
+
+ def generate() -> Generator[bytes, None, None]:
+ # Special case for urllib3.
+ if hasattr(self.raw, "stream"):
+ try:
+ yield from self.raw.stream(chunk_size, decode_content=True)
+ except ProtocolError as e:
+ raise ChunkedEncodingError(e)
+ except DecodeError as e:
+ raise ContentDecodingError(e)
+ except ReadTimeoutError as e:
+ raise ConnectionError(e)
+ except SSLError as e:
+ raise RequestsSSLError(e)
+ else:
+ # Standard file-like object.
+ while True:
+ chunk = self.raw.read(chunk_size)
+ if not chunk:
+ break
+ yield chunk
+
+ self._content_consumed = True
+
+ if self._content_consumed and isinstance(self._content, bool):
+ raise StreamConsumedError()
+ elif chunk_size is not None and not isinstance(chunk_size, int): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for untyped callers
+ raise TypeError(
+ f"chunk_size must be an int, it is instead a {type(chunk_size)}."
+ )
+
+ if self._content_consumed:
+ # simulate reading small chunks of the content
+ content = cast(bytes, self._content)
+ chunks = iter_slices(content, chunk_size)
+ else:
+ chunks = generate()
+
+ if decode_unicode:
+ chunks = stream_decode_response_unicode(chunks, self)
+
+ return chunks
+
+ @overload
+ def iter_lines(
+ self,
+ chunk_size: int = ITER_CHUNK_SIZE,
+ decode_unicode: Literal[False] = False,
+ delimiter: bytes | None = None,
+ ) -> Iterator[bytes]: ...
+ @overload
+ def iter_lines(
+ self,
+ chunk_size: int = ITER_CHUNK_SIZE,
+ *,
+ decode_unicode: Literal[True],
+ delimiter: str | bytes | None = None,
+ ) -> Iterator[str | bytes]: ...
+ def iter_lines(
+ self,
+ chunk_size: int = ITER_CHUNK_SIZE,
+ decode_unicode: bool = False,
+ delimiter: str | bytes | None = None,
+ ) -> Iterator[str | bytes]:
+ """Iterates over the response data, one line at a time. When
+ stream=True is set on the request, this avoids reading the
+ content at once into memory for large responses.
+
+ The decode_unicode param works the same as in `iter_content`, with the
+ same caveats.
+
+ .. note:: This method is not reentrant safe.
+ """
+
+ pending: str | bytes | None = None
+
+ for chunk in self.iter_content(
+ chunk_size=chunk_size, decode_unicode=decode_unicode
+ ):
+ if pending is not None:
+ # TODO: remove cast after iter_lines rewrite
+ chunk = cast("str | bytes", pending + chunk) # type: ignore[operator]
+
+ if delimiter:
+ lines = chunk.split(delimiter) # type: ignore[arg-type]
+ else:
+ lines = chunk.splitlines()
+
+ if lines and lines[-1] and chunk and lines[-1][-1] == chunk[-1]:
+ pending = lines.pop()
+ else:
+ pending = None
+
+ yield from lines
+
+ if pending is not None:
+ yield pending
+
+ @property
+ def content(self) -> bytes:
+ """Content of the response, in bytes."""
+
+ if self._content is False:
+ # Read the contents.
+ if self._content_consumed:
+ raise RuntimeError("The content for this response was already consumed")
+
+ if self.status_code == 0 or self.raw is None:
+ self._content = None
+ else:
+ self._content = b"".join(self.iter_content(CONTENT_CHUNK_SIZE)) or b""
+
+ self._content_consumed = True
+ # don't need to release the connection; that's been handled by urllib3
+ # since we exhausted the data.
+ return self._content # type: ignore[return-value]
+
+ @property
+ def text(self) -> str:
+ """Content of the response, in unicode.
+
+ If Response.encoding is None, encoding will be guessed using
+ ``charset_normalizer`` or ``chardet``.
+
+ The encoding of the response content is determined based solely on HTTP
+ headers, following RFC 2616 to the letter. If you can take advantage of
+ non-HTTP knowledge to make a better guess at the encoding, you should
+ set ``r.encoding`` appropriately before accessing this property.
+ """
+
+ # Try charset from content-type
+ content = None
+ encoding = self.encoding
+
+ if not self.content:
+ return ""
+
+ # Fallback to auto-detected encoding.
+ if self.encoding is None:
+ encoding = self.apparent_encoding
+
+ # Decode unicode from given encoding.
+ try:
+ content = str(self.content, encoding or "utf-8", errors="replace")
+ except (LookupError, TypeError):
+ # A LookupError is raised if the encoding was not found which could
+ # indicate a misspelling or similar mistake.
+ #
+ # A TypeError can be raised if encoding is None
+ #
+ # So we try blindly encoding.
+ content = str(self.content, errors="replace")
+
+ return content
+
+ def json(self, **kwargs: Any) -> Any:
+ r"""Decodes the JSON response body (if any) as a Python object.
+
+ This may return a dictionary, list, etc. depending on what is in the response.
+
+ :param \*\*kwargs: Optional arguments that ``json.loads`` takes.
+ :raises requests.exceptions.JSONDecodeError: If the response body does not
+ contain valid json.
+ """
+
+ if not self.encoding and self.content and len(self.content) > 3:
+ # No encoding set. JSON RFC 4627 section 3 states we should expect
+ # UTF-8, -16 or -32. Detect which one to use; If the detection or
+ # decoding fails, fall back to `self.text` (using charset_normalizer to make
+ # a best guess).
+ encoding = guess_json_utf(self.content)
+ if encoding is not None:
+ try:
+ return complexjson.loads(self.content.decode(encoding), **kwargs)
+ except UnicodeDecodeError:
+ # Wrong UTF codec detected; usually because it's not UTF-8
+ # but some other 8-bit codec. This is an RFC violation,
+ # and the server didn't bother to tell us what codec *was*
+ # used.
+ pass
+ except JSONDecodeError as e:
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
+
+ try:
+ return complexjson.loads(self.text, **kwargs)
+ except JSONDecodeError as e:
+ # Catch JSON-related errors and raise as requests.JSONDecodeError
+ # This aliases json.JSONDecodeError and simplejson.JSONDecodeError
+ raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
+
+ @property
+ def links(self) -> dict[str, dict[str, str]]:
+ """Returns the parsed header links of the response, if any."""
+
+ header = self.headers.get("link")
+
+ resolved_links: dict[str, dict[str, str]] = {}
+
+ if header:
+ links = parse_header_links(header)
+
+ for link in links:
+ key = link.get("rel") or link.get("url")
+ if key is not None:
+ resolved_links[key] = link
+
+ return resolved_links
+
+ def raise_for_status(self) -> None:
+ """Raises :class:`HTTPError`, if one occurred."""
+
+ http_error_msg = ""
+ if isinstance(self.reason, bytes):
+ # We attempt to decode utf-8 first because some servers
+ # choose to localize their reason strings. If the string
+ # isn't utf-8, we fall back to iso-8859-1 for all other
+ # encodings. (See PR #3538)
+ try:
+ reason = self.reason.decode("utf-8")
+ except UnicodeDecodeError:
+ reason = self.reason.decode("iso-8859-1")
+ else:
+ reason = self.reason
+
+ if 400 <= self.status_code < 500:
+ http_error_msg = (
+ f"{self.status_code} Client Error: {reason} for url: {self.url}"
+ )
+
+ elif 500 <= self.status_code < 600:
+ http_error_msg = (
+ f"{self.status_code} Server Error: {reason} for url: {self.url}"
+ )
+
+ if http_error_msg:
+ raise HTTPError(http_error_msg, response=self)
+
+ def close(self) -> None:
+ """Releases the connection back to the pool. Once this method has been
+ called the underlying ``raw`` object must not be accessed again.
+
+ *Note: Should not normally need to be called explicitly.*
+ """
+ if not self._content_consumed:
+ self.raw.close()
+
+ release_conn = getattr(self.raw, "release_conn", None)
+ if release_conn is not None:
+ release_conn()
diff --git a/venv/Lib/site-packages/requests/packages.py b/venv/Lib/site-packages/requests/packages.py
new file mode 100644
index 0000000..5ab3d8e
--- /dev/null
+++ b/venv/Lib/site-packages/requests/packages.py
@@ -0,0 +1,23 @@
+import sys
+
+from .compat import chardet
+
+# This code exists for backwards compatibility reasons.
+# I don't like it either. Just look the other way. :)
+
+for package in ("urllib3", "idna"):
+ locals()[package] = __import__(package)
+ # This traversal is apparently necessary such that the identities are
+ # preserved (requests.packages.urllib3.* is urllib3.*)
+ for mod in list(sys.modules):
+ if mod == package or mod.startswith(f"{package}."):
+ sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
+
+if chardet is not None:
+ target = chardet.__name__
+ for mod in list(sys.modules):
+ if mod == target or mod.startswith(f"{target}."):
+ imported_mod = sys.modules[mod]
+ sys.modules[f"requests.packages.{mod}"] = imported_mod
+ mod = mod.replace(target, "chardet")
+ sys.modules[f"requests.packages.{mod}"] = imported_mod
diff --git a/venv/Lib/site-packages/requests/py.typed b/venv/Lib/site-packages/requests/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/venv/Lib/site-packages/requests/sessions.py b/venv/Lib/site-packages/requests/sessions.py
new file mode 100644
index 0000000..8f13887
--- /dev/null
+++ b/venv/Lib/site-packages/requests/sessions.py
@@ -0,0 +1,920 @@
+"""
+requests.sessions
+~~~~~~~~~~~~~~~~~
+
+This module provides a Session object to manage and persist settings across
+requests (cookies, auth, proxies).
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+import time
+from collections import OrderedDict
+from collections.abc import Generator, Mapping, MutableMapping
+from datetime import timedelta
+from typing import TYPE_CHECKING, Any, cast
+
+from ._internal_utils import to_native_string
+from ._types import is_prepared as _is_prepared
+from .adapters import HTTPAdapter
+from .auth import _basic_auth_str # type: ignore[reportPrivateUsage]
+from .compat import cookielib, urljoin, urlparse
+from .cookies import (
+ RequestsCookieJar,
+ cookiejar_from_dict,
+ extract_cookies_to_jar,
+ merge_cookies,
+)
+from .exceptions import (
+ ChunkedEncodingError,
+ ContentDecodingError,
+ InvalidSchema,
+ TooManyRedirects,
+)
+from .hooks import default_hooks, dispatch_hook
+
+# formerly defined here, reexposed here for backward compatibility
+from .models import ( # noqa: F401
+ DEFAULT_REDIRECT_LIMIT,
+ REDIRECT_STATI, # type: ignore[reportUnusedImport]
+ PreparedRequest,
+ Request,
+ Response,
+)
+from .status_codes import codes
+from .structures import CaseInsensitiveDict
+from .utils import ( # noqa: F401
+ DEFAULT_PORTS,
+ default_headers,
+ get_auth_from_url,
+ get_environ_proxies,
+ get_netrc_auth,
+ requote_uri,
+ resolve_proxies,
+ rewind_body,
+ should_bypass_proxies, # type: ignore[reportUnusedImport] # re-export for external consumers
+ to_key_val_list,
+)
+
+if TYPE_CHECKING:
+ from http.cookiejar import CookieJar
+
+ from typing_extensions import Self, Unpack
+
+ from . import _types as _t
+ from .adapters import BaseAdapter
+
+# Preferred clock, based on which one is more accurate on a given system.
+if sys.platform == "win32":
+ preferred_clock = time.perf_counter
+else:
+ preferred_clock = time.time
+
+
+def merge_setting(
+ request_setting: Any, session_setting: Any, dict_class: type = OrderedDict
+) -> Any:
+ """Determines appropriate setting for a given request, taking into account
+ the explicit setting on that request, and the setting in the session. If a
+ setting is a dictionary, they will be merged together using `dict_class`
+ """
+
+ if session_setting is None:
+ return request_setting
+
+ if request_setting is None:
+ return session_setting
+
+ # Bypass if not a dictionary (e.g. verify)
+ if not (
+ isinstance(session_setting, Mapping) and isinstance(request_setting, Mapping)
+ ):
+ return request_setting
+
+ merged_setting = dict_class(to_key_val_list(session_setting)) # type: ignore[arg-type] # isinstance narrows Any to Mapping[Unknown]
+ merged_setting.update(to_key_val_list(request_setting)) # type: ignore[arg-type]
+
+ # Remove keys that are set to None. Extract keys first to avoid altering
+ # the dictionary during iteration.
+ none_keys = [k for (k, v) in merged_setting.items() if v is None]
+ for key in none_keys:
+ del merged_setting[key]
+
+ return merged_setting
+
+
+def merge_hooks(
+ request_hooks: _t.HooksType,
+ session_hooks: _t.HooksType,
+ dict_class: type = OrderedDict,
+) -> _t.HooksType:
+ """Properly merges both requests and session hooks.
+
+ This is necessary because when request_hooks == {'response': []}, the
+ merge breaks Session hooks entirely.
+ """
+ if session_hooks is None or session_hooks.get("response") == []:
+ return request_hooks
+
+ if request_hooks is None or request_hooks.get("response") == []:
+ return session_hooks
+
+ return merge_setting(request_hooks, session_hooks, dict_class)
+
+
+class SessionRedirectMixin:
+ max_redirects: int
+ trust_env: bool
+ cookies: RequestsCookieJar
+
+ def send(self, request: PreparedRequest, **kwargs: Any) -> Response: ...
+
+ def get_redirect_target(self, resp: Response) -> str | None:
+ """Receives a Response. Returns a redirect URI or ``None``"""
+ # Due to the nature of how requests processes redirects this method will
+ # be called at least once upon the original response and at least twice
+ # on each subsequent redirect response (if any).
+ # If a custom mixin is used to handle this logic, it may be advantageous
+ # to cache the redirect location onto the response object as a private
+ # attribute.
+ if resp.is_redirect:
+ location = resp.headers["location"]
+ # Currently the underlying http module on py3 decode headers
+ # in latin1, but empirical evidence suggests that latin1 is very
+ # rarely used with non-ASCII characters in HTTP headers.
+ # It is more likely to get UTF8 header rather than latin1.
+ # This causes incorrect handling of UTF8 encoded location headers.
+ # To solve this, we re-encode the location in latin1.
+ location = location.encode("latin1")
+ return to_native_string(location, "utf8")
+ return None
+
+ def should_strip_auth(self, old_url: str, new_url: str) -> bool:
+ """Decide whether Authorization header should be removed when redirecting"""
+ old_parsed = urlparse(old_url)
+ new_parsed = urlparse(new_url)
+ if old_parsed.hostname != new_parsed.hostname:
+ return True
+ # Special case: allow http -> https redirect when using the standard
+ # ports. This isn't specified by RFC 7235, but is kept to avoid
+ # breaking backwards compatibility with older versions of requests
+ # that allowed any redirects on the same host.
+ if (
+ old_parsed.scheme == "http"
+ and old_parsed.port in (80, None)
+ and new_parsed.scheme == "https"
+ and new_parsed.port in (443, None)
+ ):
+ return False
+
+ # Handle default port usage corresponding to scheme.
+ changed_port = old_parsed.port != new_parsed.port
+ changed_scheme = old_parsed.scheme != new_parsed.scheme
+ default_port = (DEFAULT_PORTS.get(old_parsed.scheme, None), None)
+ if (
+ not changed_scheme
+ and old_parsed.port in default_port
+ and new_parsed.port in default_port
+ ):
+ return False
+
+ # Standard case: root URI must match
+ return changed_port or changed_scheme
+
+ def resolve_redirects(
+ self,
+ resp: Response,
+ req: PreparedRequest,
+ stream: bool = False,
+ timeout: _t.TimeoutType = None,
+ verify: _t.VerifyType = True,
+ cert: _t.CertType = None,
+ proxies: dict[str, str] | None = None,
+ yield_requests: bool = False,
+ **adapter_kwargs: Any,
+ ) -> Generator[Response, None, None]:
+ """Receives a Response. Returns a generator of Responses or Requests."""
+
+ hist: list[Response] = [] # keep track of history
+
+ url = self.get_redirect_target(resp)
+ previous_fragment = urlparse(req.url).fragment
+ while url:
+ prepared_request = req.copy()
+
+ # Update history and keep track of redirects.
+ resp.history = hist[:]
+ hist.append(resp)
+
+ try:
+ resp.content # Consume socket so it can be released
+ except (ChunkedEncodingError, ContentDecodingError, RuntimeError):
+ resp.raw.read(decode_content=False)
+
+ if len(resp.history) >= self.max_redirects:
+ raise TooManyRedirects(
+ f"Exceeded {self.max_redirects} redirects.", response=resp
+ )
+
+ # Release the connection back into the pool.
+ resp.close()
+
+ # Handle redirection without scheme (see: RFC 1808 Section 4)
+ if url.startswith("//"):
+ parsed_rurl = urlparse(resp.url)
+ url = ":".join([to_native_string(parsed_rurl.scheme), url])
+
+ # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2)
+ parsed = urlparse(url)
+ if parsed.fragment == "" and previous_fragment:
+ parsed = parsed._replace(fragment=previous_fragment)
+ elif parsed.fragment:
+ previous_fragment = parsed.fragment
+ url = parsed.geturl()
+
+ # Facilitate relative 'location' headers, as allowed by RFC 7231.
+ # (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
+ # Compliant with RFC3986, we percent encode the url.
+ if not parsed.netloc:
+ url = urljoin(resp.url, requote_uri(url))
+ else:
+ url = requote_uri(url)
+
+ prepared_request.url = to_native_string(url)
+
+ self.rebuild_method(prepared_request, resp)
+
+ # https://github.com/psf/requests/issues/1084
+ if resp.status_code not in (
+ codes.temporary_redirect,
+ codes.permanent_redirect,
+ ):
+ # https://github.com/psf/requests/issues/3490
+ purged_headers = ("Content-Length", "Content-Type", "Transfer-Encoding")
+ for header in purged_headers:
+ prepared_request.headers.pop(header, None)
+ prepared_request.body = None
+
+ headers = prepared_request.headers
+ headers.pop("Cookie", None)
+
+ # Extract any cookies sent on the response to the cookiejar
+ # in the new request. Because we've mutated our copied prepared
+ # request, use the old one that we haven't yet touched.
+ cookie_jar = cast("CookieJar", prepared_request._cookies) # type: ignore[reportPrivateUsage]
+ extract_cookies_to_jar(cookie_jar, req, resp.raw)
+ merge_cookies(cookie_jar, self.cookies)
+ prepared_request.prepare_cookies(cookie_jar)
+
+ # Rebuild auth and proxy information.
+ proxies = self.rebuild_proxies(prepared_request, proxies)
+ self.rebuild_auth(prepared_request, resp)
+
+ # A failed tell() sets `_body_position` to `object()`. This non-None
+ # value ensures `rewindable` will be True, allowing us to raise an
+ # UnrewindableBodyError, instead of hanging the connection.
+ rewindable = prepared_request._body_position is not None and ( # type: ignore[reportPrivateUsage]
+ "Content-Length" in headers or "Transfer-Encoding" in headers
+ )
+
+ # Attempt to rewind consumed file-like object.
+ if rewindable:
+ rewind_body(prepared_request)
+
+ # Override the original request.
+ req = prepared_request
+
+ if yield_requests:
+ yield req # type: ignore[misc] # Internal use only, returns PreparedRequest
+ else:
+ resp = self.send(
+ req,
+ stream=stream,
+ timeout=timeout,
+ verify=verify,
+ cert=cert,
+ proxies=proxies,
+ allow_redirects=False,
+ **adapter_kwargs,
+ )
+
+ extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
+
+ # extract redirect url, if any, for the next loop
+ url = self.get_redirect_target(resp)
+ yield resp
+
+ def rebuild_auth(
+ self, prepared_request: PreparedRequest, response: Response
+ ) -> None:
+ """When being redirected we may want to strip authentication from the
+ request to avoid leaking credentials. This method intelligently removes
+ and reapplies authentication where possible to avoid credential loss.
+ """
+ original_request = response.request
+ assert _is_prepared(original_request)
+ assert _is_prepared(prepared_request)
+
+ headers = prepared_request.headers
+ original_url = original_request.url
+ url = prepared_request.url
+
+ if "Authorization" in headers and self.should_strip_auth(original_url, url):
+ # If we get redirected to a new host, we should strip out any
+ # authentication headers.
+ del headers["Authorization"]
+
+ # .netrc might have more auth for us on our new host.
+ new_auth = get_netrc_auth(url) if self.trust_env else None
+ if new_auth is not None:
+ prepared_request.prepare_auth(new_auth)
+
+ def rebuild_proxies(
+ self,
+ prepared_request: PreparedRequest,
+ proxies: dict[str, str] | None,
+ ) -> dict[str, str]:
+ """This method re-evaluates the proxy configuration by considering the
+ environment variables. If we are redirected to a URL covered by
+ NO_PROXY, we strip the proxy configuration. Otherwise, we set missing
+ proxy keys for this URL (in case they were stripped by a previous
+ redirect).
+
+ This method also replaces the Proxy-Authorization header where
+ necessary.
+
+ :rtype: dict
+ """
+ assert _is_prepared(prepared_request)
+ headers = prepared_request.headers
+ scheme = urlparse(prepared_request.url).scheme
+ new_proxies = resolve_proxies(prepared_request, proxies, self.trust_env)
+
+ if "Proxy-Authorization" in headers:
+ del headers["Proxy-Authorization"]
+
+ try:
+ username, password = get_auth_from_url(new_proxies[scheme])
+ except KeyError:
+ username, password = None, None
+
+ # urllib3 handles proxy authorization for us in the standard adapter.
+ # Avoid appending this to TLS tunneled requests where it may be leaked.
+ if not scheme.startswith("https") and username and password:
+ headers["Proxy-Authorization"] = _basic_auth_str(username, password)
+
+ return new_proxies
+
+ def rebuild_method(
+ self, prepared_request: PreparedRequest, response: Response
+ ) -> None:
+ """When being redirected we may want to change the method of the request
+ based on certain specs or browser behavior.
+ """
+ method = prepared_request.method
+
+ # https://tools.ietf.org/html/rfc7231#section-6.4.4
+ if response.status_code == codes.see_other and method != "HEAD":
+ method = "GET"
+
+ # Do what the browsers do, despite standards...
+ # First, turn 302s into GETs.
+ if response.status_code == codes.found and method != "HEAD":
+ method = "GET"
+
+ # Second, if a POST is responded to with a 301, turn it into a GET.
+ # This bizarre behaviour is explained in Issue 1704.
+ if response.status_code == codes.moved and method == "POST":
+ method = "GET"
+
+ prepared_request.method = method
+
+
+class Session(SessionRedirectMixin):
+ """A Requests session.
+
+ Provides cookie persistence, connection-pooling, and configuration.
+
+ Basic Usage::
+
+ >>> import requests
+ >>> s = requests.Session()
+ >>> s.get('https://httpbin.org/get')
+
+
+ Or as a context manager::
+
+ >>> with requests.Session() as s:
+ ... s.get('https://httpbin.org/get')
+
+ """
+
+ headers: CaseInsensitiveDict[str]
+ auth: _t.AuthType
+ proxies: dict[str, str]
+ hooks: dict[str, list[_t.HookType]]
+ params: MutableMapping[str, Any]
+ stream: bool
+ verify: _t.VerifyType
+ cert: _t.CertType
+ max_redirects: int
+ trust_env: bool
+ cookies: RequestsCookieJar
+ adapters: MutableMapping[str, BaseAdapter]
+
+ __attrs__: list[str] = [
+ "headers",
+ "cookies",
+ "auth",
+ "proxies",
+ "hooks",
+ "params",
+ "verify",
+ "cert",
+ "adapters",
+ "stream",
+ "trust_env",
+ "max_redirects",
+ ]
+
+ def __init__(self) -> None:
+ #: A case-insensitive dictionary of headers to be sent on each
+ #: :class:`Request ` sent from this
+ #: :class:`Session `.
+ self.headers = default_headers()
+
+ #: Default Authentication tuple or object to attach to
+ #: :class:`Request `.
+ self.auth = None
+
+ #: Dictionary mapping protocol or protocol and host to the URL of the proxy
+ #: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
+ #: be used on each :class:`Request `.
+ self.proxies = {}
+
+ #: Event-handling hooks.
+ self.hooks = default_hooks()
+
+ #: Dictionary of querystring data to attach to each
+ #: :class:`Request `. The dictionary values may be lists for
+ #: representing multivalued query parameters.
+ self.params = {}
+
+ #: Stream response content default.
+ self.stream = False
+
+ #: SSL Verification default.
+ #: Defaults to `True`, requiring requests to verify the TLS certificate at the
+ #: remote end.
+ #: If verify is set to `False`, requests will accept any TLS certificate
+ #: presented by the server, and will ignore hostname mismatches and/or
+ #: expired certificates, which will make your application vulnerable to
+ #: man-in-the-middle (MitM) attacks.
+ #: Only set this to `False` for testing.
+ #: If verify is set to a string, it must be the path to a CA bundle file
+ #: that will be used to verify the TLS certificate.
+ self.verify = True
+
+ #: SSL client certificate default, if String, path to ssl client
+ #: cert file (.pem). If Tuple, ('cert', 'key') pair.
+ self.cert = None
+
+ #: Maximum number of redirects allowed. If the request exceeds this
+ #: limit, a :class:`TooManyRedirects` exception is raised.
+ #: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
+ #: 30.
+ self.max_redirects = DEFAULT_REDIRECT_LIMIT
+
+ #: Trust environment settings for proxy configuration, default
+ #: authentication and similar.
+ self.trust_env = True
+
+ #: A CookieJar containing all currently outstanding cookies set on this
+ #: session. By default it is a
+ #: :class:`RequestsCookieJar `, but
+ #: may be any other ``cookielib.CookieJar`` compatible object.
+ self.cookies = cookiejar_from_dict({})
+
+ # Default connection adapters.
+ self.adapters = OrderedDict()
+ self.mount("https://", HTTPAdapter())
+ self.mount("http://", HTTPAdapter())
+
+ def __enter__(self) -> Self:
+ return self
+
+ def __exit__(self, *args: Any) -> None:
+ self.close()
+
+ def prepare_request(self, request: Request) -> PreparedRequest:
+ """Constructs a :class:`PreparedRequest ` for
+ transmission and returns it. The :class:`PreparedRequest` has settings
+ merged from the :class:`Request ` instance and those of the
+ :class:`Session`.
+
+ :param request: :class:`Request` instance to prepare with this
+ session's settings.
+ :rtype: requests.PreparedRequest
+ """
+ url = cast("_t.UriType", request.url)
+ method = cast(str, request.method)
+
+ cookies = request.cookies or {}
+
+ # Bootstrap CookieJar.
+ if not isinstance(cookies, cookielib.CookieJar):
+ cookies = cookiejar_from_dict(cookies)
+
+ # Merge with session cookies
+ merged_cookies = merge_cookies(
+ merge_cookies(RequestsCookieJar(), self.cookies), cookies
+ )
+
+ # Set environment's basic authentication if not explicitly set.
+ auth = request.auth
+ if self.trust_env and not auth and not self.auth:
+ auth = get_netrc_auth(url)
+
+ p = PreparedRequest()
+ p.prepare(
+ method=method.upper(),
+ url=url,
+ files=request.files,
+ data=request.data,
+ json=request.json,
+ headers=merge_setting(
+ request.headers, self.headers, dict_class=CaseInsensitiveDict
+ ),
+ params=merge_setting(request.params, self.params),
+ auth=merge_setting(auth, self.auth),
+ cookies=merged_cookies,
+ hooks=merge_hooks(request.hooks, self.hooks),
+ )
+ return p
+
+ def request(
+ self,
+ method: str,
+ url: _t.UriType,
+ params: _t.ParamsType = None,
+ data: _t.DataType = None,
+ headers: Mapping[str, str | bytes] | None = None,
+ cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None,
+ files: _t.FilesType = None,
+ auth: _t.AuthType = None,
+ timeout: _t.TimeoutType = None,
+ allow_redirects: bool = True,
+ proxies: dict[str, str] | None = None,
+ hooks: _t.HooksInputType | None = None,
+ stream: bool | None = None,
+ verify: _t.VerifyType | None = None,
+ cert: _t.CertType = None,
+ json: _t.JsonType = None,
+ ) -> Response:
+ """Constructs a :class:`Request `, prepares it and sends it.
+ Returns :class:`Response ` object.
+
+ :param method: method for the new :class:`Request` object.
+ :param url: URL for the new :class:`Request` object.
+ :param params: (optional) Dictionary or bytes to be sent in the query
+ string for the :class:`Request`.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) json to send in the body of the
+ :class:`Request`.
+ :param headers: (optional) Dictionary of HTTP Headers to send with the
+ :class:`Request`.
+ :param cookies: (optional) Dict or CookieJar object to send with the
+ :class:`Request`.
+ :param files: (optional) Dictionary of ``'filename': file-like-objects``
+ for multipart encoding upload.
+ :param auth: (optional) Auth tuple or callable to enable
+ Basic/Digest/Custom HTTP Auth.
+ :param timeout: (optional) How many seconds to wait for the server to send
+ data before giving up, as a float, or a :ref:`(connect timeout,
+ read timeout) ` tuple.
+ :type timeout: float or tuple
+ :param allow_redirects: (optional) Set to True by default.
+ :type allow_redirects: bool
+ :param proxies: (optional) Dictionary mapping protocol or protocol and
+ hostname to the URL of the proxy.
+ :param hooks: (optional) Dictionary mapping hook name to one event or
+ list of events, event must be callable.
+ :param stream: (optional) whether to immediately download the response
+ content. Defaults to ``False``.
+ :param verify: (optional) Either a boolean, in which case it controls whether we verify
+ the server's TLS certificate, or a string, in which case it must be a path
+ to a CA bundle to use. Defaults to ``True``. When set to
+ ``False``, requests will accept any TLS certificate presented by
+ the server, and will ignore hostname mismatches and/or expired
+ certificates, which will make your application vulnerable to
+ man-in-the-middle (MitM) attacks. Setting verify to ``False``
+ may be useful during local development or testing.
+ :param cert: (optional) if String, path to ssl client cert file (.pem).
+ If Tuple, ('cert', 'key') pair.
+ :rtype: requests.Response
+ """
+ if isinstance(url, bytes):
+ url = url.decode("utf-8")
+
+ # Create the Request.
+ req = Request(
+ method=method.upper(),
+ url=url,
+ headers=headers,
+ files=files,
+ data=data or {},
+ json=json,
+ params=params or {},
+ auth=auth,
+ cookies=cookies,
+ hooks=hooks,
+ )
+ prep = self.prepare_request(req)
+
+ assert _is_prepared(prep)
+
+ proxies = proxies or {}
+
+ settings = self.merge_environment_settings(
+ prep.url, proxies, stream, verify, cert
+ )
+
+ # Send the request.
+ send_kwargs = {
+ "timeout": timeout,
+ "allow_redirects": allow_redirects,
+ }
+ send_kwargs.update(settings)
+ resp = self.send(prep, **send_kwargs)
+
+ return resp
+
+ def get(
+ self,
+ url: _t.UriType,
+ params: _t.ParamsType = None,
+ **kwargs: Unpack[_t.GetKwargs],
+ ) -> Response:
+ r"""Sends a GET request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param params: (optional) Dictionary, list of tuples or bytes to send
+ in the query string for the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("GET", url, params=params, **kwargs)
+
+ def options(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends a OPTIONS request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ kwargs.setdefault("allow_redirects", True)
+ return self.request("OPTIONS", url, **kwargs)
+
+ def head(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends a HEAD request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ kwargs.setdefault("allow_redirects", False)
+ return self.request("HEAD", url, **kwargs)
+
+ def post(
+ self,
+ url: _t.UriType,
+ data: _t.DataType = None,
+ json: _t.JsonType = None,
+ **kwargs: Unpack[_t.PostKwargs],
+ ) -> Response:
+ r"""Sends a POST request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param json: (optional) json to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ return self.request("POST", url, data=data, json=json, **kwargs)
+
+ def put(
+ self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs]
+ ) -> Response:
+ r"""Sends a PUT request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ return self.request("PUT", url, data=data, **kwargs)
+
+ def patch(
+ self, url: _t.UriType, data: _t.DataType = None, **kwargs: Unpack[_t.DataKwargs]
+ ) -> Response:
+ r"""Sends a PATCH request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param data: (optional) Dictionary, list of tuples, bytes, or file-like
+ object to send in the body of the :class:`Request`.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ return self.request("PATCH", url, data=data, **kwargs)
+
+ def delete(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
+ r"""Sends a DELETE request. Returns :class:`Response` object.
+
+ :param url: URL for the new :class:`Request` object.
+ :param \*\*kwargs: Optional arguments that ``request`` takes.
+ :rtype: requests.Response
+ """
+
+ return self.request("DELETE", url, **kwargs)
+
+ def send(self, request: PreparedRequest, **kwargs: Any) -> Response:
+ """Send a given PreparedRequest.
+
+ :rtype: requests.Response
+ """
+ # Set defaults that the hooks can utilize to ensure they always have
+ # the correct parameters to reproduce the previous request.
+ kwargs.setdefault("stream", self.stream)
+ kwargs.setdefault("verify", self.verify)
+ kwargs.setdefault("cert", self.cert)
+ if "proxies" not in kwargs:
+ kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env)
+
+ # It's possible that users might accidentally send a Request object.
+ # Guard against that specific failure case.
+ if isinstance(request, Request):
+ raise ValueError("You can only send PreparedRequests.")
+
+ assert _is_prepared(request)
+
+ # Set up variables needed for resolve_redirects and dispatching of hooks
+ allow_redirects = kwargs.pop("allow_redirects", True)
+ stream = kwargs.get("stream")
+ hooks = request.hooks
+
+ # Get the appropriate adapter to use
+ adapter = self.get_adapter(url=request.url)
+
+ # Start time (approximately) of the request
+ start = preferred_clock()
+
+ # Send the request
+ r = adapter.send(request, **kwargs)
+
+ # Total elapsed time of the request (approximately)
+ elapsed = preferred_clock() - start
+ r.elapsed = timedelta(seconds=elapsed)
+
+ # Response manipulation hooks
+ r = dispatch_hook("response", hooks, r, **kwargs)
+
+ # Persist cookies
+ if r.history:
+ # If the hooks create history then we want those cookies too
+ for resp in r.history:
+ extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
+
+ extract_cookies_to_jar(self.cookies, request, r.raw)
+
+ # Resolve redirects if allowed.
+ if allow_redirects:
+ # Redirect resolving generator.
+ gen = self.resolve_redirects(r, request, **kwargs)
+ history = [resp for resp in gen]
+ else:
+ history = []
+
+ # Shuffle things around if there's history.
+ if history:
+ # Insert the first (original) request at the start
+ history.insert(0, r)
+ # Get the last request made
+ r = history.pop()
+ r.history = history
+
+ # If redirects aren't being followed, store the response on the Request for Response.next().
+ if not allow_redirects:
+ try:
+ r._next = next( # type: ignore[assignment] # yield_requests=True returns PreparedRequest
+ self.resolve_redirects(r, request, yield_requests=True, **kwargs)
+ )
+ except StopIteration:
+ pass
+
+ if not stream:
+ r.content
+
+ return r
+
+ def merge_environment_settings(
+ self,
+ url: str,
+ proxies: dict[str, str] | None,
+ stream: bool | None,
+ verify: _t.VerifyType | None,
+ cert: _t.CertType,
+ ) -> dict[str, Any]:
+ """
+ Check the environment and merge it with some settings.
+
+ :rtype: dict
+ """
+ # Gather clues from the surrounding environment.
+ if self.trust_env:
+ # Set environment's proxies.
+ no_proxy = proxies.get("no_proxy") if proxies is not None else None
+ env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
+ if proxies is not None:
+ for k, v in env_proxies.items():
+ proxies.setdefault(k, v)
+
+ # Look for requests environment configuration
+ # and be compatible with cURL.
+ if verify is True or verify is None:
+ verify = (
+ os.environ.get("REQUESTS_CA_BUNDLE")
+ or os.environ.get("CURL_CA_BUNDLE")
+ or verify
+ )
+
+ # Merge all the kwargs.
+ proxies = merge_setting(proxies, self.proxies)
+ stream = merge_setting(stream, self.stream)
+ verify = merge_setting(verify, self.verify)
+ cert = merge_setting(cert, self.cert)
+
+ return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
+
+ def get_adapter(self, url: str) -> BaseAdapter:
+ """
+ Returns the appropriate connection adapter for the given URL.
+
+ :rtype: requests.adapters.BaseAdapter
+ """
+ for prefix, adapter in self.adapters.items():
+ if url.lower().startswith(prefix.lower()):
+ return adapter
+
+ # Nothing matches :-/
+ raise InvalidSchema(f"No connection adapters were found for {url!r}")
+
+ def close(self) -> None:
+ """Closes all adapters and as such the session"""
+ for v in self.adapters.values():
+ v.close()
+
+ def mount(self, prefix: str, adapter: BaseAdapter) -> None:
+ """Registers a connection adapter to a prefix.
+
+ Adapters are sorted in descending order by prefix length.
+ """
+ self.adapters[prefix] = adapter
+ keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]
+
+ for key in keys_to_move:
+ self.adapters[key] = self.adapters.pop(key)
+
+ def __getstate__(self) -> dict[str, Any]:
+ state = {attr: getattr(self, attr, None) for attr in self.__attrs__}
+ return state
+
+ def __setstate__(self, state: dict[str, Any]) -> None:
+ for attr, value in state.items():
+ setattr(self, attr, value)
+
+
+def session() -> Session:
+ """
+ Returns a :class:`Session` for context-management.
+
+ .. deprecated:: 1.0.0
+
+ This method has been deprecated since version 1.0.0 and is only kept for
+ backwards compatibility. New code should use :class:`~requests.sessions.Session`
+ to create a session. This may be removed at a future date.
+
+ :rtype: Session
+ """
+ return Session()
diff --git a/venv/Lib/site-packages/requests/status_codes.py b/venv/Lib/site-packages/requests/status_codes.py
new file mode 100644
index 0000000..6c59d6b
--- /dev/null
+++ b/venv/Lib/site-packages/requests/status_codes.py
@@ -0,0 +1,128 @@
+r"""
+The ``codes`` object defines a mapping from common names for HTTP statuses
+to their numerical codes, accessible either as attributes or as dictionary
+items.
+
+Example::
+
+ >>> import requests
+ >>> requests.codes['temporary_redirect']
+ 307
+ >>> requests.codes.teapot
+ 418
+ >>> requests.codes['\o/']
+ 200
+
+Some codes have multiple names, and both upper- and lower-case versions of
+the names are allowed. For example, ``codes.ok``, ``codes.OK``, and
+``codes.okay`` all correspond to the HTTP status code 200.
+"""
+
+from .structures import LookupDict
+
+_codes = {
+ # Informational.
+ 100: ("continue",),
+ 101: ("switching_protocols",),
+ 102: ("processing", "early-hints"),
+ 103: ("checkpoint",),
+ 122: ("uri_too_long", "request_uri_too_long"),
+ 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
+ 201: ("created",),
+ 202: ("accepted",),
+ 203: ("non_authoritative_info", "non_authoritative_information"),
+ 204: ("no_content",),
+ 205: ("reset_content", "reset"),
+ 206: ("partial_content", "partial"),
+ 207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"),
+ 208: ("already_reported",),
+ 226: ("im_used",),
+ # Redirection.
+ 300: ("multiple_choices",),
+ 301: ("moved_permanently", "moved", "\\o-"),
+ 302: ("found",),
+ 303: ("see_other", "other"),
+ 304: ("not_modified",),
+ 305: ("use_proxy",),
+ 306: ("switch_proxy",),
+ 307: ("temporary_redirect", "temporary_moved", "temporary"),
+ 308: (
+ "permanent_redirect",
+ "resume_incomplete",
+ "resume",
+ ), # "resume" and "resume_incomplete" to be removed in 3.0
+ # Client Error.
+ 400: ("bad_request", "bad"),
+ 401: ("unauthorized",),
+ 402: ("payment_required", "payment"),
+ 403: ("forbidden",),
+ 404: ("not_found", "-o-"),
+ 405: ("method_not_allowed", "not_allowed"),
+ 406: ("not_acceptable",),
+ 407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"),
+ 408: ("request_timeout", "timeout"),
+ 409: ("conflict",),
+ 410: ("gone",),
+ 411: ("length_required",),
+ 412: ("precondition_failed", "precondition"),
+ 413: ("request_entity_too_large", "content_too_large"),
+ 414: ("request_uri_too_large", "uri_too_long"),
+ 415: ("unsupported_media_type", "unsupported_media", "media_type"),
+ 416: (
+ "requested_range_not_satisfiable",
+ "requested_range",
+ "range_not_satisfiable",
+ ),
+ 417: ("expectation_failed",),
+ 418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
+ 421: ("misdirected_request",),
+ 422: ("unprocessable_entity", "unprocessable", "unprocessable_content"),
+ 423: ("locked",),
+ 424: ("failed_dependency", "dependency"),
+ 425: ("unordered_collection", "unordered", "too_early"),
+ 426: ("upgrade_required", "upgrade"),
+ 428: ("precondition_required", "precondition"),
+ 429: ("too_many_requests", "too_many"),
+ 431: ("header_fields_too_large", "fields_too_large"),
+ 444: ("no_response", "none"),
+ 449: ("retry_with", "retry"),
+ 450: ("blocked_by_windows_parental_controls", "parental_controls"),
+ 451: ("unavailable_for_legal_reasons", "legal_reasons"),
+ 499: ("client_closed_request",),
+ # Server Error.
+ 500: ("internal_server_error", "server_error", "/o\\", "✗"),
+ 501: ("not_implemented",),
+ 502: ("bad_gateway",),
+ 503: ("service_unavailable", "unavailable"),
+ 504: ("gateway_timeout",),
+ 505: ("http_version_not_supported", "http_version"),
+ 506: ("variant_also_negotiates",),
+ 507: ("insufficient_storage",),
+ 509: ("bandwidth_limit_exceeded", "bandwidth"),
+ 510: ("not_extended",),
+ 511: ("network_authentication_required", "network_auth", "network_authentication"),
+}
+
+codes: LookupDict[int] = LookupDict(name="status_codes")
+
+
+def _init():
+ for code, titles in _codes.items():
+ for title in titles:
+ setattr(codes, title, code)
+ if not title.startswith(("\\", "/")):
+ setattr(codes, title.upper(), code)
+
+ def doc(code: int) -> str:
+ names = ", ".join(f"``{n}``" for n in _codes[code])
+ return "* %d: %s" % (code, names)
+
+ global __doc__
+ __doc__ = (
+ __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes))
+ if __doc__ is not None
+ else None
+ )
+
+
+_init()
diff --git a/venv/Lib/site-packages/requests/structures.py b/venv/Lib/site-packages/requests/structures.py
new file mode 100644
index 0000000..7675eaf
--- /dev/null
+++ b/venv/Lib/site-packages/requests/structures.py
@@ -0,0 +1,130 @@
+"""
+requests.structures
+~~~~~~~~~~~~~~~~~~~
+
+Data structures that power Requests.
+"""
+
+from __future__ import annotations
+
+from collections import OrderedDict
+from collections.abc import Iterable, Iterator, Mapping
+from typing import Any, Generic, TypeVar, overload
+
+from .compat import MutableMapping
+
+_VT = TypeVar("_VT")
+_D = TypeVar("_D")
+
+
+class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]):
+ """A case-insensitive ``dict``-like object.
+
+ Implements all methods and operations of
+ ``MutableMapping`` as well as dict's ``copy``. Also
+ provides ``lower_items``.
+
+ All keys are expected to be strings. The structure remembers the
+ case of the last key to be set, and ``iter(instance)``,
+ ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
+ will contain case-sensitive keys. However, querying and contains
+ testing is case insensitive::
+
+ cid = CaseInsensitiveDict()
+ cid['Accept'] = 'application/json'
+ cid['aCCEPT'] == 'application/json' # True
+ list(cid) == ['Accept'] # True
+
+ For example, ``headers['content-encoding']`` will return the
+ value of a ``'Content-Encoding'`` response header, regardless
+ of how the header name was originally stored.
+
+ If the constructor, ``.update``, or equality comparison
+ operations are given keys that have equal ``.lower()``s, the
+ behavior is undefined.
+ """
+
+ _store: OrderedDict[str, tuple[str, _VT]]
+
+ def __init__(
+ self,
+ data: Mapping[str, _VT] | Iterable[tuple[str, _VT]] | None = None,
+ **kwargs: _VT,
+ ) -> None:
+ self._store = OrderedDict()
+ if data is None:
+ data = {}
+ self.update(data, **kwargs)
+
+ def __setitem__(self, key: str, value: _VT) -> None:
+ # Use the lowercased key for lookups, but store the actual
+ # key alongside the value.
+ self._store[key.lower()] = (key, value)
+
+ def __getitem__(self, key: str) -> _VT:
+ return self._store[key.lower()][1]
+
+ def __delitem__(self, key: str) -> None:
+ del self._store[key.lower()]
+
+ def __iter__(self) -> Iterator[str]:
+ return (casedkey for casedkey, _ in self._store.values())
+
+ def __len__(self) -> int:
+ return len(self._store)
+
+ def lower_items(self) -> Iterator[tuple[str, _VT]]:
+ """Like iteritems(), but with all lowercase keys."""
+ return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items())
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, Mapping):
+ other_dict: CaseInsensitiveDict[Any] = CaseInsensitiveDict(other) # type: ignore[reportUnknownArgumentType]
+ else:
+ return NotImplemented
+ # Compare insensitively
+ return dict(self.lower_items()) == dict(other_dict.lower_items())
+
+ # Copy is required
+ def copy(self) -> CaseInsensitiveDict[_VT]:
+ return CaseInsensitiveDict(self._store.values())
+
+ def __repr__(self) -> str:
+ return str(dict(self.items()))
+
+
+class LookupDict(dict[str, _VT]):
+ """Dictionary lookup object."""
+
+ name: Any
+
+ def __init__(self, name: Any = None) -> None:
+ self.name = name
+ super().__init__()
+
+ def __repr__(self) -> str:
+ return f""
+
+ def __getattr__(self, key: str) -> _VT | None:
+ # We need this for type checkers to infer typing
+ # on attribute access with status_codes.py
+ if key in self.__dict__:
+ return self.__dict__[key]
+ else:
+ raise AttributeError(
+ f"'{type(self).__name__}' object has no attribute '{key}'"
+ )
+
+ def __getitem__(self, key: str) -> _VT | None: # type: ignore[override]
+ # We allow fall-through here, so values default to None
+
+ return self.__dict__.get(key, None)
+
+ @overload
+ def get(self, key: str, default: None = None) -> _VT | None: ...
+
+ @overload
+ def get(self, key: str, default: _D | _VT) -> _D | _VT: ...
+
+ def get(self, key: str, default: _D | None = None) -> _VT | _D | None:
+ return self.__dict__.get(key, default)
diff --git a/venv/Lib/site-packages/requests/utils.py b/venv/Lib/site-packages/requests/utils.py
new file mode 100644
index 0000000..120336d
--- /dev/null
+++ b/venv/Lib/site-packages/requests/utils.py
@@ -0,0 +1,1155 @@
+"""
+requests.utils
+~~~~~~~~~~~~~~
+
+This module provides utility functions that are used within Requests
+that are also useful for external consumption.
+"""
+
+from __future__ import annotations
+
+import codecs
+import contextlib
+import io
+import os
+import re
+import socket
+import struct
+import sys
+import tempfile
+import warnings
+import zipfile
+from collections import OrderedDict
+from collections.abc import Generator, Iterable
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ Final,
+ TypeVar,
+ cast,
+ overload,
+)
+
+from urllib3.util import make_headers, parse_url
+
+from . import certs
+from .__version__ import __version__
+
+# to_native_string is unused here, but imported here for backwards compatibility
+from ._internal_utils import ( # noqa: F401
+ _HEADER_VALIDATORS_BYTE, # type: ignore[reportPrivateUsage]
+ _HEADER_VALIDATORS_STR, # type: ignore[reportPrivateUsage]
+ HEADER_VALIDATORS, # type: ignore[reportUnusedImport]
+ to_native_string, # type: ignore[reportUnusedImport]
+)
+from ._types import SupportsItems as _SupportsItems
+from .compat import (
+ Mapping,
+ bytes,
+ getproxies,
+ getproxies_environment,
+ integer_types,
+ is_urllib3_1,
+ proxy_bypass,
+ proxy_bypass_environment, # type: ignore[attr-defined] # https://github.com/python/cpython/issues/145331
+ quote,
+ str,
+ unquote,
+ urlparse,
+ urlunparse,
+)
+from .compat import parse_http_list as _parse_list_header
+from .cookies import cookiejar_from_dict
+from .exceptions import (
+ FileModeWarning,
+ InvalidHeader,
+ InvalidURL,
+ UnrewindableBodyError,
+)
+from .structures import CaseInsensitiveDict
+
+if TYPE_CHECKING:
+ from http.cookiejar import CookieJar
+ from io import BufferedWriter
+
+ from . import _types as _t
+ from .models import PreparedRequest, Request, Response
+
+NETRC_FILES: Final = (".netrc", "_netrc")
+
+
+# Certificate is extracted by certifi when needed.
+DEFAULT_CA_BUNDLE_PATH: str = certs.where()
+
+
+DEFAULT_PORTS: Final = {"http": 80, "https": 443}
+
+_KT = TypeVar("_KT")
+_VT = TypeVar("_VT")
+
+# Ensure that ', ' is used to preserve previous delimiter behavior.
+DEFAULT_ACCEPT_ENCODING: Final = ", ".join(
+ re.split(r",\s*", make_headers(accept_encoding=True)["accept-encoding"])
+)
+
+
+if sys.platform == "win32":
+ # provide a proxy_bypass version on Windows without DNS lookups
+
+ def proxy_bypass_registry(host: str) -> bool:
+ try:
+ import winreg
+ except ImportError:
+ return False
+
+ try:
+ internetSettings = winreg.OpenKey(
+ winreg.HKEY_CURRENT_USER,
+ r"Software\Microsoft\Windows\CurrentVersion\Internet Settings",
+ )
+ # ProxyEnable could be REG_SZ or REG_DWORD, normalizing it
+ proxyEnable = int(winreg.QueryValueEx(internetSettings, "ProxyEnable")[0])
+ # ProxyOverride is almost always a string
+ proxyOverride = winreg.QueryValueEx(internetSettings, "ProxyOverride")[0]
+ except (OSError, ValueError):
+ return False
+ if not proxyEnable or not proxyOverride:
+ return False
+
+ # make a check value list from the registry entry: replace the
+ # '' string by the localhost entry and the corresponding
+ # canonical entry.
+ proxyOverride = proxyOverride.split(";")
+ # filter out empty strings to avoid re.match return true in the following code.
+ proxyOverride = filter(None, proxyOverride)
+ # now check if we match one of the registry values.
+ for test in proxyOverride:
+ if test == "":
+ if "." not in host:
+ return True
+ test = test.replace(".", r"\.") # mask dots
+ test = test.replace("*", r".*") # change glob sequence
+ test = test.replace("?", r".") # change glob char
+ if re.match(test, host, re.I):
+ return True
+ return False
+
+ def proxy_bypass(host: str) -> bool: # noqa
+ """Return True, if the host should be bypassed.
+
+ Checks proxy settings gathered from the environment, if specified,
+ or the registry.
+ """
+ if getproxies_environment():
+ return proxy_bypass_environment(host)
+ else:
+ return proxy_bypass_registry(host)
+
+
+def dict_to_sequence(
+ d: _t.SupportsItems[Any, Any] | Iterable[tuple[Any, Any]],
+) -> Iterable[tuple[Any, Any]]:
+ """Returns an internal sequence dictionary update."""
+
+ if isinstance(d, _SupportsItems):
+ return d.items()
+
+ return d
+
+
+def super_len(o: Any) -> int:
+ total_length = None
+ current_position = 0
+
+ if not is_urllib3_1 and isinstance(o, str):
+ # urllib3 2.x+ treats all strings as utf-8 instead
+ # of latin-1 (iso-8859-1) like http.client.
+ o = o.encode("utf-8")
+
+ if hasattr(o, "__len__"):
+ total_length = len(o)
+
+ elif hasattr(o, "len"):
+ total_length = o.len
+
+ elif hasattr(o, "fileno"):
+ try:
+ fileno = o.fileno()
+ except (io.UnsupportedOperation, AttributeError):
+ # AttributeError is a surprising exception, seeing as how we've just checked
+ # that `hasattr(o, 'fileno')`. It happens for objects obtained via
+ # `Tarfile.extractfile()`, per issue 5229.
+ pass
+ else:
+ total_length = os.fstat(fileno).st_size
+
+ # Having used fstat to determine the file length, we need to
+ # confirm that this file was opened up in binary mode.
+ if "b" not in o.mode:
+ warnings.warn(
+ (
+ "Requests has determined the content-length for this "
+ "request using the binary size of the file: however, the "
+ "file has been opened in text mode (i.e. without the 'b' "
+ "flag in the mode). This may lead to an incorrect "
+ "content-length. In Requests 3.0, support will be removed "
+ "for files in text mode."
+ ),
+ FileModeWarning,
+ )
+
+ if hasattr(o, "tell"):
+ try:
+ current_position = o.tell()
+ except OSError:
+ # This can happen in some weird situations, such as when the file
+ # is actually a special file descriptor like stdin. In this
+ # instance, we don't know what the length is, so set it to zero and
+ # let requests chunk it instead.
+ if total_length is not None:
+ current_position = total_length
+ else:
+ if hasattr(o, "seek") and total_length is None:
+ # StringIO and BytesIO have seek but no usable fileno
+ try:
+ # seek to end of file
+ o.seek(0, 2)
+ total_length = o.tell()
+
+ # seek back to current position to support
+ # partially read file-like objects
+ o.seek(current_position or 0)
+ except OSError:
+ total_length = 0
+
+ if total_length is None:
+ total_length = 0
+
+ return max(0, total_length - current_position)
+
+
+def get_netrc_auth(
+ url: _t.UriType, raise_errors: bool = False
+) -> tuple[str, str] | None:
+ """Returns the Requests tuple auth for a given url from netrc."""
+
+ if isinstance(url, bytes):
+ url = url.decode("utf-8")
+
+ netrc_file = os.environ.get("NETRC")
+ if netrc_file is not None:
+ netrc_locations = (netrc_file,)
+ else:
+ netrc_locations = (f"~/{f}" for f in NETRC_FILES)
+
+ try:
+ from netrc import NetrcParseError, netrc
+
+ netrc_path = None
+
+ for f in netrc_locations:
+ loc = os.path.expanduser(f)
+ if os.path.exists(loc):
+ netrc_path = loc
+ break
+
+ # Abort early if there isn't one.
+ if netrc_path is None:
+ return
+
+ ri = urlparse(url)
+ host = ri.hostname
+
+ if host is None:
+ return
+
+ try:
+ _netrc = netrc(netrc_path).authenticators(host)
+ if _netrc and any(_netrc):
+ # Return with login / password
+ login_i = 0 if _netrc[0] else 1
+ return (_netrc[login_i] or "", _netrc[2] or "")
+ except (NetrcParseError, OSError):
+ # If there was a parsing error or a permissions issue reading the file,
+ # we'll just skip netrc auth unless explicitly asked to raise errors.
+ if raise_errors:
+ raise
+
+ # App Engine hackiness.
+ except (ImportError, AttributeError):
+ pass
+
+
+def guess_filename(obj: Any) -> str | None:
+ """Tries to guess the filename of the given object."""
+ name = getattr(obj, "name", None)
+ if name and isinstance(name, (str, bytes)) and name[0] != "<" and name[-1] != ">":
+ return os.path.basename(name) # type: ignore[return-value] # urllib3 accepts bytes but types str only
+
+
+def extract_zipped_paths(path: str) -> str:
+ """Replace nonexistent paths that look like they refer to a member of a zip
+ archive with the location of an extracted copy of the target, or else
+ just return the provided path unchanged.
+ """
+ if os.path.exists(path):
+ # this is already a valid path, no need to do anything further
+ return path
+
+ # find the first valid part of the provided path and treat that as a zip archive
+ # assume the rest of the path is the name of a member in the archive
+ archive, member = os.path.split(path)
+ while archive and not os.path.exists(archive):
+ archive, prefix = os.path.split(archive)
+ if not prefix:
+ # If we don't check for an empty prefix after the split (in other words, archive remains unchanged after the split),
+ # we _can_ end up in an infinite loop on a rare corner case affecting a small number of users
+ break
+ member = "/".join([prefix, member])
+
+ if not zipfile.is_zipfile(archive):
+ return path
+
+ zip_file = zipfile.ZipFile(archive)
+ if member not in zip_file.namelist():
+ return path
+
+ # we have a valid zip archive and a valid member of that archive
+ suffix = os.path.splitext(member.split("/")[-1])[-1]
+ fd, extracted_path = tempfile.mkstemp(suffix=suffix)
+ try:
+ os.write(fd, zip_file.read(member))
+ finally:
+ os.close(fd)
+
+ return extracted_path
+
+
+@contextlib.contextmanager
+def atomic_open(filename: str) -> Generator[BufferedWriter, None, None]:
+ """Write a file to the disk in an atomic fashion"""
+ tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename))
+ try:
+ with os.fdopen(tmp_descriptor, "wb") as tmp_handler:
+ yield tmp_handler
+ os.replace(tmp_name, filename)
+ except BaseException:
+ os.remove(tmp_name)
+ raise
+
+
+def from_key_val_list(
+ value: Mapping[Any, Any] | Iterable[tuple[Any, Any]] | None,
+) -> dict[Any, Any] | None:
+ """Take an object and test to see if it can be represented as a
+ dictionary. Unless it can not be represented as such, return an
+ OrderedDict, e.g.,
+
+ ::
+
+ >>> from_key_val_list([('key', 'val')])
+ OrderedDict([('key', 'val')])
+ >>> from_key_val_list('string')
+ Traceback (most recent call last):
+ ...
+ ValueError: cannot encode objects that are not 2-tuples
+ >>> from_key_val_list({'key': 'val'})
+ OrderedDict([('key', 'val')])
+
+ :rtype: OrderedDict
+ """
+ if value is None:
+ return None
+
+ if isinstance(value, (str, bytes, bool, int)):
+ raise ValueError("cannot encode objects that are not 2-tuples")
+
+ return OrderedDict(value)
+
+
+@overload
+def to_key_val_list(value: None) -> None: ...
+@overload
+def to_key_val_list(
+ value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]],
+) -> list[tuple[_KT, _VT]]: ...
+def to_key_val_list(
+ value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]] | None,
+) -> list[tuple[_KT, _VT]] | None:
+ """Take an object and test to see if it can be represented as a
+ dictionary. If it can be, return a list of tuples, e.g.,
+
+ ::
+
+ >>> to_key_val_list([('key', 'val')])
+ [('key', 'val')]
+ >>> to_key_val_list({'key': 'val'})
+ [('key', 'val')]
+ >>> to_key_val_list('string')
+ Traceback (most recent call last):
+ ...
+ ValueError: cannot encode objects that are not 2-tuples
+
+ :rtype: list
+ """
+ if value is None:
+ return None
+
+ if isinstance(value, (str, bytes, bool, int)):
+ raise ValueError("cannot encode objects that are not 2-tuples")
+
+ if isinstance(value, _SupportsItems):
+ return list(value.items())
+
+ return list(value)
+
+
+# From mitsuhiko/werkzeug (used with permission).
+def parse_list_header(value: str) -> list[str]:
+ """Parse lists as described by RFC 2068 Section 2.
+
+ In particular, parse comma-separated lists where the elements of
+ the list may include quoted-strings. A quoted-string could
+ contain a comma. A non-quoted string could have quotes in the
+ middle. Quotes are removed automatically after parsing.
+
+ It basically works like :func:`parse_set_header` just that items
+ may appear multiple times and case sensitivity is preserved.
+
+ The return value is a standard :class:`list`:
+
+ >>> parse_list_header('token, "quoted value"')
+ ['token', 'quoted value']
+
+ To create a header from the :class:`list` again, use the
+ :func:`dump_header` function.
+
+ :param value: a string with a list header.
+ :return: :class:`list`
+ :rtype: list
+ """
+ result: list[str] = []
+ for item in _parse_list_header(value):
+ if item[:1] == item[-1:] == '"':
+ item = unquote_header_value(item[1:-1])
+ result.append(item)
+ return result
+
+
+# From mitsuhiko/werkzeug (used with permission).
+def parse_dict_header(value: str) -> dict[str, str | None]:
+ """Parse lists of key, value pairs as described by RFC 2068 Section 2 and
+ convert them into a python dict:
+
+ >>> d = parse_dict_header('foo="is a fish", bar="as well"')
+ >>> type(d) is dict
+ True
+ >>> sorted(d.items())
+ [('bar', 'as well'), ('foo', 'is a fish')]
+
+ If there is no value for a key it will be `None`:
+
+ >>> parse_dict_header('key_without_value')
+ {'key_without_value': None}
+
+ To create a header from the :class:`dict` again, use the
+ :func:`dump_header` function.
+
+ :param value: a string with a dict header.
+ :return: :class:`dict`
+ :rtype: dict
+ """
+ result: dict[str, str | None] = {}
+ for item in _parse_list_header(value):
+ if "=" not in item:
+ result[item] = None
+ continue
+ name, value = item.split("=", 1)
+ if value[:1] == value[-1:] == '"':
+ value = unquote_header_value(value[1:-1])
+ result[name] = value
+ return result
+
+
+# From mitsuhiko/werkzeug (used with permission).
+def unquote_header_value(value: str, is_filename: bool = False) -> str:
+ r"""Unquotes a header value. (Reversal of :func:`quote_header_value`).
+ This does not use the real unquoting but what browsers are actually
+ using for quoting.
+
+ :param value: the header value to unquote.
+ :rtype: str
+ """
+ if value and value[0] == value[-1] == '"':
+ # this is not the real unquoting, but fixing this so that the
+ # RFC is met will result in bugs with internet explorer and
+ # probably some other browsers as well. IE for example is
+ # uploading files with "C:\foo\bar.txt" as filename
+ value = value[1:-1]
+
+ # if this is a filename and the starting characters look like
+ # a UNC path, then just return the value without quotes. Using the
+ # replace sequence below on a UNC path has the effect of turning
+ # the leading double slash into a single slash and then
+ # _fix_ie_filename() doesn't work correctly. See #458.
+ if not is_filename or value[:2] != "\\\\":
+ return value.replace("\\\\", "\\").replace('\\"', '"')
+ return value
+
+
+def dict_from_cookiejar(cj: CookieJar) -> dict[str, str | None]:
+ """Returns a key/value dictionary from a CookieJar.
+
+ :param cj: CookieJar object to extract cookies from.
+ :rtype: dict
+ """
+
+ cookie_dict = {cookie.name: cookie.value for cookie in cj}
+ return cookie_dict
+
+
+def add_dict_to_cookiejar(cj: CookieJar, cookie_dict: dict[str, str]) -> CookieJar:
+ """Returns a CookieJar from a key/value dictionary.
+
+ :param cj: CookieJar to insert cookies into.
+ :param cookie_dict: Dict of key/values to insert into CookieJar.
+ :rtype: CookieJar
+ """
+
+ return cookiejar_from_dict(cookie_dict, cj)
+
+
+def get_encodings_from_content(content: str) -> list[str]:
+ """Returns encodings from given content string.
+
+ :param content: bytestring to extract encodings from.
+ """
+ warnings.warn(
+ (
+ "In requests 3.0, get_encodings_from_content will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
+
+ charset_re = re.compile(r']', flags=re.I)
+ pragma_re = re.compile(r']', flags=re.I)
+ xml_re = re.compile(r'^<\?xml.*?encoding=["\']*(.+?)["\'>]')
+
+ return (
+ charset_re.findall(content)
+ + pragma_re.findall(content)
+ + xml_re.findall(content)
+ )
+
+
+def _parse_content_type_header(header: str) -> tuple[str, dict[str, Any]]:
+ """Returns content type and parameters from given header.
+
+ :param header: string
+ :return: tuple containing content type and dictionary of
+ parameters.
+ """
+
+ tokens = header.split(";")
+ content_type, params = tokens[0].strip(), tokens[1:]
+ params_dict: dict[str, str | bool] = {}
+ strip_chars = "\"' "
+
+ for param in params:
+ param = param.strip()
+ if param and (idx := param.find("=")) != -1:
+ key = param[:idx].strip(strip_chars)
+ value = param[idx + 1 :].strip(strip_chars)
+ params_dict[key.lower()] = value
+ return content_type, params_dict
+
+
+def get_encoding_from_headers(headers: CaseInsensitiveDict[str]) -> str | None:
+ """Returns encodings from given HTTP Header Dict.
+
+ :param headers: dictionary to extract encoding from.
+ :rtype: str
+ """
+
+ content_type = headers.get("content-type")
+
+ if not content_type:
+ return None
+
+ content_type, params = _parse_content_type_header(content_type)
+
+ if "charset" in params:
+ return params["charset"].strip("'\"")
+
+ if "text" in content_type:
+ return "ISO-8859-1"
+
+ if "application/json" in content_type:
+ # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset
+ return "utf-8"
+
+
+def stream_decode_response_unicode(
+ iterator: Iterable[bytes], r: Response
+) -> Generator[str | bytes, None, None]:
+ """Stream decodes an iterator."""
+
+ if r.encoding is None:
+ yield from iterator
+ return
+
+ decoder = codecs.getincrementaldecoder(r.encoding)(errors="replace")
+ for chunk in iterator:
+ rv = decoder.decode(chunk)
+ if rv:
+ yield rv
+ rv = decoder.decode(b"", final=True)
+ if rv:
+ yield rv
+
+
+@overload
+def iter_slices(
+ string: bytes, slice_length: int | None
+) -> Generator[bytes, None, None]: ...
+@overload
+def iter_slices(
+ string: str, slice_length: int | None
+) -> Generator[str, None, None]: ...
+def iter_slices(
+ string: bytes | str, slice_length: int | None
+) -> Generator[bytes | str, None, None]:
+ """Iterate over slices of a string."""
+ pos = 0
+ if slice_length is None or slice_length <= 0:
+ slice_length = len(string)
+ while pos < len(string):
+ yield string[pos : pos + slice_length]
+ pos += slice_length
+
+
+def get_unicode_from_response(r: Response) -> str | bytes | None:
+ """Returns the requested content back in unicode.
+
+ :param r: Response object to get unicode content from.
+
+ Tried:
+
+ 1. charset from content-type
+ 2. fall back and replace all unicode characters
+
+ :rtype: str
+ """
+ warnings.warn(
+ (
+ "In requests 3.0, get_unicode_from_response will be removed. For "
+ "more information, please see the discussion on issue #2266. (This"
+ " warning should only appear once.)"
+ ),
+ DeprecationWarning,
+ )
+ if r.content is None: # type: ignore[reportUnnecessaryComparison]
+ return None
+
+ tried_encodings: list[str] = []
+
+ # Try charset from content-type
+ encoding = get_encoding_from_headers(r.headers)
+
+ if encoding:
+ try:
+ return str(r.content, encoding)
+ except UnicodeError:
+ tried_encodings.append(encoding)
+
+ # Fall back:
+ try:
+ return str(r.content, encoding or "utf-8", errors="replace")
+ except TypeError:
+ return r.content
+
+
+# The unreserved URI characters (RFC 3986)
+UNRESERVED_SET: Final = frozenset(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~"
+)
+
+
+def unquote_unreserved(uri: str) -> str:
+ """Un-escape any percent-escape sequences in a URI that are unreserved
+ characters. This leaves all reserved, illegal and non-ASCII bytes encoded.
+
+ :rtype: str
+ """
+ parts = uri.split("%")
+ for i in range(1, len(parts)):
+ h = parts[i][0:2]
+ if len(h) == 2 and h.isalnum():
+ try:
+ c = chr(int(h, 16))
+ except ValueError:
+ raise InvalidURL(f"Invalid percent-escape sequence: '{h}'")
+
+ if c in UNRESERVED_SET:
+ parts[i] = c + parts[i][2:]
+ else:
+ parts[i] = f"%{parts[i]}"
+ else:
+ parts[i] = f"%{parts[i]}"
+ return "".join(parts)
+
+
+def requote_uri(uri: str) -> str:
+ """Re-quote the given URI.
+
+ This function passes the given URI through an unquote/quote cycle to
+ ensure that it is fully and consistently quoted.
+
+ :rtype: str
+ """
+ safe_with_percent = "!#$%&'()*+,/:;=?@[]~"
+ safe_without_percent = "!#$&'()*+,/:;=?@[]~"
+ try:
+ # Unquote only the unreserved characters
+ # Then quote only illegal characters (do not quote reserved,
+ # unreserved, or '%')
+ return quote(unquote_unreserved(uri), safe=safe_with_percent)
+ except InvalidURL:
+ # We couldn't unquote the given URI, so let's try quoting it, but
+ # there may be unquoted '%'s in the URI. We need to make sure they're
+ # properly quoted so they do not cause issues elsewhere.
+ return quote(uri, safe=safe_without_percent)
+
+
+def address_in_network(ip: str, net: str) -> bool:
+ """This function allows you to check if an IP belongs to a network subnet
+
+ Example: returns True if ip = 192.168.1.1 and net = 192.168.1.0/24
+ returns False if ip = 192.168.1.1 and net = 192.168.100.0/24
+
+ :rtype: bool
+ """
+ ipaddr = struct.unpack("=L", socket.inet_aton(ip))[0]
+ netaddr, bits = net.split("/")
+ netmask = struct.unpack("=L", socket.inet_aton(dotted_netmask(int(bits))))[0]
+ network = struct.unpack("=L", socket.inet_aton(netaddr))[0] & netmask
+ return (ipaddr & netmask) == (network & netmask)
+
+
+def dotted_netmask(mask: int) -> str:
+ """Converts mask from /xx format to xxx.xxx.xxx.xxx
+
+ Example: if mask is 24 function returns 255.255.255.0
+
+ :rtype: str
+ """
+ bits = 0xFFFFFFFF ^ (1 << 32 - mask) - 1
+ return socket.inet_ntoa(struct.pack(">I", bits))
+
+
+def is_ipv4_address(string_ip: str) -> bool:
+ """
+ :rtype: bool
+ """
+ try:
+ socket.inet_aton(string_ip)
+ except OSError:
+ return False
+ return True
+
+
+def is_valid_cidr(string_network: str) -> bool:
+ """
+ Very simple check of the cidr format in no_proxy variable.
+
+ :rtype: bool
+ """
+ if string_network.count("/") == 1:
+ try:
+ mask = int(string_network.split("/")[1])
+ except ValueError:
+ return False
+
+ if mask < 1 or mask > 32:
+ return False
+
+ try:
+ socket.inet_aton(string_network.split("/")[0])
+ except OSError:
+ return False
+ else:
+ return False
+ return True
+
+
+@contextlib.contextmanager
+def set_environ(env_name: str, value: str | None) -> Generator[None, None, None]:
+ """Set the environment variable 'env_name' to 'value'
+
+ Save previous value, yield, and then restore the previous value stored in
+ the environment variable 'env_name'.
+
+ If 'value' is None, do nothing"""
+ value_changed = value is not None
+ old_value: str | None = None
+ if value_changed:
+ old_value = os.environ.get(env_name)
+ os.environ[env_name] = value
+ try:
+ yield
+ finally:
+ if value_changed:
+ if old_value is None:
+ del os.environ[env_name]
+ else:
+ os.environ[env_name] = old_value
+
+
+def should_bypass_proxies(url: str, no_proxy: str | None) -> bool:
+ """
+ Returns whether we should bypass proxies or not.
+
+ :rtype: bool
+ """
+
+ # Prioritize lowercase environment variables over uppercase
+ # to keep a consistent behaviour with other http projects (curl, wget).
+ def get_proxy(key: str) -> str | None:
+ return os.environ.get(key) or os.environ.get(key.upper())
+
+ # First check whether no_proxy is defined. If it is, check that the URL
+ # we're getting isn't in the no_proxy list.
+ no_proxy_arg = no_proxy
+ if no_proxy is None:
+ no_proxy = get_proxy("no_proxy")
+ parsed = urlparse(url)
+ hostname = parsed.hostname
+
+ if hostname is None:
+ # URLs don't always have hostnames, e.g. file:/// urls.
+ return True
+
+ if no_proxy:
+ # We need to check whether we match here. We need to see if we match
+ # the end of the hostname, both with and without the port.
+ no_proxy_hosts = (host for host in no_proxy.replace(" ", "").split(",") if host)
+
+ if is_ipv4_address(hostname):
+ for proxy_ip in no_proxy_hosts:
+ if is_valid_cidr(proxy_ip):
+ if address_in_network(hostname, proxy_ip):
+ return True
+ elif hostname == proxy_ip:
+ # If no_proxy ip was defined in plain IP notation instead of cidr notation &
+ # matches the IP of the index
+ return True
+ else:
+ host_with_port = hostname
+ if parsed.port:
+ host_with_port += f":{parsed.port}"
+
+ for host in no_proxy_hosts:
+ host = host.lstrip(".")
+ if hostname == host or host_with_port == host:
+ return True
+ host = "." + host
+ if hostname.endswith(host) or host_with_port.endswith(host):
+ return True
+
+ with set_environ("no_proxy", no_proxy_arg):
+ try:
+ bypass = proxy_bypass(hostname)
+ except (TypeError, socket.gaierror):
+ bypass = False
+
+ if bypass:
+ return True
+
+ return False
+
+
+def get_environ_proxies(url: str, no_proxy: str | None = None) -> dict[str, str]:
+ """
+ Return a dict of environment proxies.
+
+ :rtype: dict
+ """
+ if should_bypass_proxies(url, no_proxy=no_proxy):
+ return {}
+ else:
+ return getproxies()
+
+
+def select_proxy(url: str, proxies: dict[str, str] | None) -> str | None:
+ """Select a proxy for the url, if applicable.
+
+ :param url: The url being for the request
+ :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
+ """
+ proxies = proxies or {}
+ urlparts = urlparse(url)
+ if urlparts.hostname is None:
+ return proxies.get(urlparts.scheme, proxies.get("all"))
+
+ proxy_keys = [
+ urlparts.scheme + "://" + urlparts.hostname,
+ urlparts.scheme,
+ "all://" + urlparts.hostname,
+ "all",
+ ]
+ proxy = None
+ for proxy_key in proxy_keys:
+ if proxy_key in proxies:
+ proxy = proxies[proxy_key]
+ break
+
+ return proxy
+
+
+def resolve_proxies(
+ request: Request | PreparedRequest,
+ proxies: dict[str, str] | None,
+ trust_env: bool = True,
+) -> dict[str, str]:
+ """This method takes proxy information from a request and configuration
+ input to resolve a mapping of target proxies. This will consider settings
+ such as NO_PROXY to strip proxy configurations.
+
+ :param request: Request or PreparedRequest
+ :param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
+ :param trust_env: Boolean declaring whether to trust environment configs
+
+ :rtype: dict
+ """
+ proxies = proxies if proxies is not None else {}
+ url = cast(str, request.url)
+ scheme = urlparse(url).scheme
+ no_proxy = proxies.get("no_proxy")
+ new_proxies = proxies.copy()
+
+ if trust_env and not should_bypass_proxies(url, no_proxy=no_proxy):
+ environ_proxies = get_environ_proxies(url, no_proxy=no_proxy)
+
+ proxy = environ_proxies.get(scheme, environ_proxies.get("all"))
+
+ if proxy:
+ new_proxies.setdefault(scheme, proxy)
+ return new_proxies
+
+
+def default_user_agent(name: str = "python-requests") -> str:
+ """
+ Return a string representing the default user agent.
+
+ :rtype: str
+ """
+ return f"{name}/{__version__}"
+
+
+def default_headers() -> CaseInsensitiveDict[str]:
+ """
+ :rtype: requests.structures.CaseInsensitiveDict
+ """
+ return CaseInsensitiveDict(
+ {
+ "User-Agent": default_user_agent(),
+ "Accept-Encoding": DEFAULT_ACCEPT_ENCODING,
+ "Accept": "*/*",
+ "Connection": "keep-alive",
+ }
+ )
+
+
+def parse_header_links(value: str) -> list[dict[str, str]]:
+ """Return a list of parsed link headers proxies.
+
+ i.e. Link: ; rel=front; type="image/jpeg",; rel=back;type="image/jpeg"
+
+ :rtype: list
+ """
+
+ links: list[dict[str, str]] = []
+
+ replace_chars = " '\""
+
+ value = value.strip(replace_chars)
+ if not value:
+ return links
+
+ for val in re.split(", *<", value):
+ try:
+ url, params = val.split(";", 1)
+ except ValueError:
+ url, params = val, ""
+
+ link: dict[str, str] = {"url": url.strip("<> '\"")}
+
+ for param in params.split(";"):
+ try:
+ key, value = param.split("=")
+ except ValueError:
+ break
+
+ link[key.strip(replace_chars)] = value.strip(replace_chars)
+
+ links.append(link)
+
+ return links
+
+
+# Null bytes; no need to recreate these on each call to guess_json_utf
+_null = "\x00".encode("ascii") # encoding to ASCII for Python 3
+_null2 = _null * 2
+_null3 = _null * 3
+
+
+def guess_json_utf(data: bytes) -> str | None:
+ """
+ :rtype: str
+ """
+ # JSON always starts with two ASCII characters, so detection is as
+ # easy as counting the nulls and from their location and count
+ # determine the encoding. Also detect a BOM, if present.
+ sample = data[:4]
+ if sample in (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE):
+ return "utf-32" # BOM included
+ if sample[:3] == codecs.BOM_UTF8:
+ return "utf-8-sig" # BOM included, MS style (discouraged)
+ if sample[:2] in (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE):
+ return "utf-16" # BOM included
+ nullcount = sample.count(_null)
+ if nullcount == 0:
+ return "utf-8"
+ if nullcount == 2:
+ if sample[::2] == _null2: # 1st and 3rd are null
+ return "utf-16-be"
+ if sample[1::2] == _null2: # 2nd and 4th are null
+ return "utf-16-le"
+ # Did not detect 2 valid UTF-16 ascii-range characters
+ if nullcount == 3:
+ if sample[:3] == _null3:
+ return "utf-32-be"
+ if sample[1:] == _null3:
+ return "utf-32-le"
+ # Did not detect a valid UTF-32 ascii-range character
+ return None
+
+
+def prepend_scheme_if_needed(url: str, new_scheme: str) -> str:
+ """Given a URL that may or may not have a scheme, prepend the given scheme.
+ Does not replace a present scheme with the one provided as an argument.
+
+ :rtype: str
+ """
+ parsed = parse_url(url)
+ scheme, auth, _host, _port, path, query, fragment = parsed
+
+ # A defect in urlparse determines that there isn't a netloc present in some
+ # urls. We previously assumed parsing was overly cautious, and swapped the
+ # netloc and path. Due to a lack of tests on the original defect, this is
+ # maintained with parse_url for backwards compatibility.
+ netloc = parsed.netloc
+ if not netloc:
+ netloc, path = path, netloc
+
+ if auth:
+ # parse_url doesn't provide the netloc with auth
+ # so we'll add it ourselves.
+ netloc = cast(str, netloc)
+ netloc = "@".join([auth, netloc])
+ if scheme is None:
+ scheme = new_scheme
+ if path is None:
+ path = ""
+
+ return urlunparse((scheme, netloc, path, "", query, fragment))
+
+
+def get_auth_from_url(url: str) -> tuple[str, str]:
+ """Given a url with authentication components, extract them into a tuple of
+ username,password.
+
+ :rtype: (str,str)
+ """
+ parsed = urlparse(url)
+
+ try:
+ # except handles parsed.username/password being None
+ auth = (unquote(parsed.username), unquote(parsed.password)) # type: ignore[arg-type]
+ except (AttributeError, TypeError):
+ auth = ("", "")
+
+ return auth
+
+
+def check_header_validity(header: tuple[str | bytes, str | bytes]) -> None:
+ """Verifies that header parts don't contain leading whitespace
+ reserved characters, or return characters.
+
+ :param header: tuple, in the format (name, value).
+ """
+ name, value = header
+ _validate_header_part(header, name, 0)
+ _validate_header_part(header, value, 1)
+
+
+def _validate_header_part(
+ header: tuple[str | bytes, str | bytes],
+ header_part: str | bytes,
+ header_validator_index: int,
+) -> None:
+ if isinstance(header_part, str):
+ validator = _HEADER_VALIDATORS_STR[header_validator_index]
+ elif isinstance(header_part, bytes): # type: ignore[reportUnnecessaryIsInstance]
+ # runtime guard for non-str/bytes input
+ validator = _HEADER_VALIDATORS_BYTE[header_validator_index]
+ else:
+ raise InvalidHeader(
+ f"Header part ({header_part!r}) from {header} "
+ f"must be of type str or bytes, not {type(header_part)}"
+ )
+
+ if not validator.match(header_part): # type: ignore[arg-type]
+ header_kind = "name" if header_validator_index == 0 else "value"
+ raise InvalidHeader(
+ f"Invalid leading whitespace, reserved character(s), or return "
+ f"character(s) in header {header_kind}: {header_part!r}"
+ )
+
+
+def urldefragauth(url: str) -> str:
+ """
+ Given a url remove the fragment and the authentication part.
+
+ :rtype: str
+ """
+ scheme, netloc, path, params, query, _fragment = urlparse(url)
+
+ # see func:`prepend_scheme_if_needed`
+ if not netloc:
+ netloc, path = path, netloc
+
+ netloc = netloc.rsplit("@", 1)[-1]
+
+ return urlunparse((scheme, netloc, path, params, query, ""))
+
+
+def rewind_body(prepared_request: PreparedRequest) -> None:
+ """Move file pointer back to its recorded starting position
+ so it can be read again on redirect.
+ """
+ body_seek = getattr(prepared_request.body, "seek", None)
+ if body_seek is not None and isinstance(
+ prepared_request._body_position, # type: ignore[reportPrivateUsage]
+ integer_types,
+ ):
+ try:
+ body_seek(prepared_request._body_position) # type: ignore[reportPrivateUsage]
+ except OSError:
+ raise UnrewindableBodyError(
+ "An error occurred when rewinding request body for redirect."
+ )
+ else:
+ raise UnrewindableBodyError("Unable to rewind request body for redirect.")