Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae7ef63b4 | ||
|
|
5460f467b0 | ||
|
|
6af0b94158 | ||
|
|
8f8b212de8 | ||
|
|
8068356288 | ||
|
|
1f6589ec3a | ||
|
|
414f0513c3 | ||
|
|
ded32878c0 | ||
|
|
69f8484704 | ||
|
|
b17c61b618 | ||
|
|
f361ead047 | ||
|
|
d38495c906 | ||
|
|
4c800e9aea | ||
|
|
23953c0c87 | ||
|
|
4ed3d1b320 | ||
|
|
d44b67eb55 | ||
|
|
74c56d5ffb | ||
|
|
d64b9ad4bf | ||
|
|
6f66281a1d | ||
|
|
6f205ff422 | ||
|
|
661970d171 | ||
|
|
fbd0eb3732 | ||
|
|
1190afd14f | ||
|
|
e50e594529 | ||
|
|
3be097d450 | ||
|
|
a634611dd1 | ||
|
|
c4367f231b | ||
|
|
cd90742ed9 | ||
|
|
6e83187b8f | ||
|
|
84d10f0be8 | ||
|
|
b7b549b545 | ||
|
|
e511bc7277 | ||
|
|
5691f59613 | ||
|
|
2144213c30 | ||
|
|
6404f345e5 | ||
|
|
0b401c76b6 | ||
|
|
86b378d3f6 | ||
|
|
a4f9a5999b | ||
|
|
3816cfa1ab | ||
|
|
b684dcb9bb | ||
|
|
dc9dbdfb34 | ||
|
|
25340ebad0 | ||
|
|
fd628095d7 | ||
|
|
e8d2c015ee | ||
|
|
eb173bc819 | ||
|
|
d60f4773b1 | ||
|
|
f64a0d782b | ||
|
|
04d750509b | ||
|
|
f7822f7c41 | ||
|
|
561e4b6889 | ||
|
|
8f6cda9969 | ||
|
|
27e0981962 | ||
|
|
6ec76b4a36 | ||
|
|
f43f750ee1 | ||
|
|
9450dd51fb | ||
|
|
93bf5331a7 | ||
|
|
79f4df84cf | ||
|
|
b294b08fe3 | ||
|
|
5f3ff9b9e4 | ||
|
|
514c1623fe | ||
|
|
d2f6bdecc8 | ||
|
|
a044b020de | ||
|
|
16df2a0917 | ||
|
|
fe2063be0c | ||
|
|
4b0b1a3e9f | ||
|
|
185f587a78 | ||
|
|
ef439eb779 | ||
|
|
cbce031327 | ||
|
|
6360477c52 | ||
|
|
111d2b7779 | ||
|
|
f0198e6dfc | ||
|
|
bc7dd0fc4d | ||
|
|
4443b1a847 | ||
|
|
389eea58df | ||
|
|
7407309c8a | ||
|
|
bc04dfd6da | ||
|
|
66d21cb07b | ||
|
|
8b9bc8fc0f | ||
|
|
e331a288f3 | ||
|
|
753fd08c5e | ||
|
|
774a0b837a | ||
|
|
9c72a41bec | ||
|
|
ebf7190679 | ||
|
|
0e4ae38f0c | ||
|
|
d568f47278 | ||
|
|
b2a1d33f57 | ||
|
|
392b01f26e | ||
|
|
27f8b40d5c | ||
|
|
4bd79e3973 | ||
|
|
da9113c046 | ||
|
|
a2ce3d8ace | ||
|
|
5e7cb71cd7 | ||
|
|
00600b38f4 | ||
|
|
5f338446f9 | ||
|
|
47914226c2 | ||
|
|
1b40fdd004 | ||
|
|
514b3f2345 | ||
|
|
dedee0f456 | ||
|
|
7112775514 | ||
|
|
733b2018a3 | ||
|
|
0c2bbe7398 | ||
|
|
32e26af3af | ||
|
|
421b8733cf | ||
|
|
968863678b | ||
|
|
0b950d8e97 | ||
|
|
42eaeb4da8 | ||
|
|
49f8968867 | ||
|
|
8e398336f8 | ||
|
|
5f33bddabd | ||
|
|
ca78ca9611 | ||
|
|
8e5ac8e0eb | ||
|
|
6c88b6b09f | ||
|
|
cb2c800518 | ||
|
|
a389aaad45 | ||
|
|
f8bec2f7ca | ||
|
|
7029833289 | ||
|
|
6e4134b204 | ||
|
|
420d16bc7e | ||
|
|
3c8decb92f | ||
|
|
5a270e2a95 | ||
|
|
7d8df1df57 | ||
|
|
1c49660b84 |
81
.github/AI_POLICY.md
vendored
Normal file
81
.github/AI_POLICY.md
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
# Generative AI / LLM Policy
|
||||
|
||||
We appreciate that we can't realistically police how you author your pull requests, which includes whether you employ large-language model (LLM)-based development tools.
|
||||
So, we don't.
|
||||
|
||||
However, due to both legal and human reasons, we have to establish boundaries.
|
||||
|
||||
> [!CAUTION]
|
||||
> **TL;DR:**
|
||||
> - We take the responsibility for this project very seriously and we expect you to take your responsibility for your contributions seriously, too.
|
||||
> This used to be a given, but it changed now that a pull request is just one prompt away.
|
||||
>
|
||||
> - Every contribution has to be backed by a human who unequivocally owns the copyright for all changes.
|
||||
> No LLM bots in `Co-authored-by:`s.
|
||||
>
|
||||
> - DoS-by-slop leads to a permanent ban.
|
||||
>
|
||||
> - Absolutely **no** unsupervised agentic tools like OpenClaw.
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> By submitting a pull request, you certify that:
|
||||
>
|
||||
> - You are the author of the contribution or have the legal right to submit it.
|
||||
> - You either hold the copyright to the changes or have explicit legal authorization to contribute them under this project's license.
|
||||
> - You understand the code.
|
||||
> - You accept full responsibility for it.
|
||||
|
||||
|
||||
## Legal
|
||||
|
||||
There is ongoing legal uncertainty regarding the copyright status of LLM-generated works and their provenance.
|
||||
Since we do not have a formal [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_license_agreement) (CLA), you retain your copyright to your changes to this project.
|
||||
|
||||
Therefore, allowing contributions by LLMs has unpredictable consequences for the copyright status of this project – even when leaving aside possible copyright violations due to plagiarism.
|
||||
|
||||
|
||||
## Human
|
||||
|
||||
As the makers of software that is used by millions of people worldwide and with a reputation for high-quality maintenance, we take our responsibility to our users very seriously.
|
||||
No matter what LLM vendors or boosters on LinkedIn tell you, we have to manually review every change before merging, because it's **our responsibility** to keep the project stable.
|
||||
|
||||
Please understand that by opening low-quality pull requests you're not helping anyone.
|
||||
Worse, you're [poisoning the open source ecosystem](https://lwn.net/Articles/1058266/) that was precarious even before the arrival of LLM tools.
|
||||
Having to wade through plausible-looking-but-low-quality pull requests and trying to determine which ones are legit is extremely demoralizing and has already burned out many good maintainers.
|
||||
|
||||
Put bluntly, we have no time or interest to become part of your vibe coding loop where you drop LLM slop at our door, we spend time and energy to review it, and you just feed it back into the LLM for another iteration.
|
||||
|
||||
This dynamic is especially pernicious because it poisons the well for mentoring new contributors which we are committed to.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
In practice, this means:
|
||||
|
||||
- Pull requests that have an LLM product listed as co-author can't be merged and will be closed without further discussion.
|
||||
We cannot risk the copyright status of this project.
|
||||
|
||||
If you used LLM tools during development, you may still submit – but you must remove any LLM co-author tags and take full ownership of every line.
|
||||
|
||||
- By submitting a pull request, **you** take full **technical and legal** responsibility for the contents of the pull request and promise that **you** hold the copyright for the changes submitted.
|
||||
|
||||
"An LLM wrote it" is **not** an acceptable response to questions or critique.
|
||||
**If you cannot explain and defend the changes you submit, do not submit them** and open a high-quality bug report/feature request instead.
|
||||
|
||||
- Accounts that exercise bot-like behavior – like automated mass pull requests – will be permanently banned, whether they belong to a human or not.
|
||||
|
||||
- Do **not** post LLM-generated review comments – we can prompt LLMs ourselves should we desire their wisdom.
|
||||
Do **not** post summaries unless you've fact-checked them and take responsibility for 100% of their content.
|
||||
Remember that *all* LLM output *looks* **plausible**.
|
||||
When using these tools, it's **your** responsibility to ensure that it's also **correct** and has a reasonable signal-to-noise ratio.
|
||||
|
||||
---
|
||||
|
||||
# Credits, Attribution
|
||||
|
||||
The original version of this can be found in [pyca/service-identity][] and is
|
||||
used with permission from Hynek Schlawack.
|
||||
|
||||
[pyca/service-identity]:
|
||||
https://github.com/pyca/service-identity/blob/fa91bf55cfda64145aa3d202cc84059befb98af4/.github/AI_POLICY.md
|
||||
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Restrict all files related to deploying to
|
||||
# require lead maintainer approval.
|
||||
|
||||
.github/workflows/ @nateprewitt @sigmavirus24
|
||||
.github/CODEOWNERS @nateprewitt @sigmavirus24
|
||||
src/requests/__version__.py @nateprewitt @sigmavirus24
|
||||
HISTORY.md @nateprewitt @sigmavirus24
|
||||
pyproject.toml @nateprewitt @sigmavirus24
|
||||
14
.github/CONTRIBUTING.md
vendored
14
.github/CONTRIBUTING.md
vendored
@@ -1,7 +1,8 @@
|
||||
# Contribution Guidelines
|
||||
|
||||
Before opening any issues or proposing any pull requests, please read
|
||||
our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/).
|
||||
our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/)
|
||||
as well as our [AI Policy](/.github/AI_POLICY.md).
|
||||
|
||||
To get the greatest chance of helpful responses, please also observe the
|
||||
following additional notes.
|
||||
@@ -52,3 +53,14 @@ Please be aware of the following things when filing bug reports:
|
||||
If you do not provide all of these things, it will take us much longer to
|
||||
fix your problem. If we ask you to clarify these and you never respond, we
|
||||
will close your issue without fixing it.
|
||||
|
||||
## Good Pull Requests
|
||||
|
||||
1. Avoid raising duplicate pull requests. *Please* use the GitHub pull request
|
||||
search feature to check whether someone else has already filed a pull
|
||||
request.
|
||||
2. When filing a pull request, always include tests that fail without your
|
||||
changes thus preventing the fix from regressing.
|
||||
3. Always run the test suite locally
|
||||
4. Write commit messages that explain _why_ a change was made beyond "Fixes
|
||||
#NNNN". Why is this the right fix? What other options were ruled out? etc.
|
||||
|
||||
25
.github/dependabot.yml
vendored
25
.github/dependabot.yml
vendored
@@ -1,11 +1,30 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
# Ignore all patch releases as we can manually
|
||||
# upgrade if we run into a bug and need a fix.
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "pre-commit"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
pre-commit:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
7
.github/workflows/close-issues.yml
vendored
7
.github/workflows/close-issues.yml
vendored
@@ -5,11 +5,12 @@ on:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
close_qa:
|
||||
permissions:
|
||||
issues: write
|
||||
if: github.event.label.name == 'actions/autoclose-qa'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -22,6 +23,8 @@ jobs:
|
||||
--reason completed
|
||||
gh issue lock $ISSUE_URL --reason off_topic
|
||||
close_feature_request:
|
||||
permissions:
|
||||
issues: write
|
||||
if: github.event.label.name == 'actions/autoclose-feat'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
9
.github/workflows/codeql-analysis.yml
vendored
9
.github/workflows/codeql-analysis.yml
vendored
@@ -32,11 +32,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
with:
|
||||
languages: "python"
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -56,7 +57,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -70,4 +71,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3.29.0
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
|
||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -11,10 +11,14 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Run pre-commit
|
||||
uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0
|
||||
run: |
|
||||
python -m pip install pre-commit==4.6.0
|
||||
pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
|
||||
2
.github/workflows/lock-issues.yml
vendored
2
.github/workflows/lock-issues.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
if: github.repository_owner == 'psf'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@d42e5f49803f3c4e14ffee0378e31481265dda22 # v5.0.0
|
||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||
with:
|
||||
issue-lock-inactive-days: 90
|
||||
pr-lock-inactive-days: 90
|
||||
|
||||
78
.github/workflows/publish.yml
vendored
78
.github/workflows/publish.yml
vendored
@@ -4,6 +4,12 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test-pypi-only:
|
||||
description: "Publish to Test PyPI only"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,43 +18,30 @@ jobs:
|
||||
build:
|
||||
name: "Build dists"
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: "publish"
|
||||
outputs:
|
||||
hashes: ${{ steps.hash.outputs.hashes }}
|
||||
artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout repository"
|
||||
uses: "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Setup Python"
|
||||
uses: "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065"
|
||||
uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: python -m pip install build==0.8.0
|
||||
run: python -m pip install build==1.4.0
|
||||
|
||||
- name: "Build dists"
|
||||
run: |
|
||||
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \
|
||||
python -m build
|
||||
|
||||
- name: "Generate hashes"
|
||||
id: hash
|
||||
run: |
|
||||
cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)"
|
||||
|
||||
- name: "Upload dists"
|
||||
uses: "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"
|
||||
uses: "actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"
|
||||
id: upload-artifact
|
||||
with:
|
||||
name: "dist"
|
||||
@@ -56,38 +49,47 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
provenance:
|
||||
needs: [build]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
id-token: write # Needed to access the workflow's OIDC identity.
|
||||
uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0"
|
||||
with:
|
||||
base64-subjects: "${{ needs.build.outputs.hashes }}"
|
||||
upload-assets: true
|
||||
compile-generator: true # Workaround for https://github.com/slsa-framework/slsa-github-generator/issues/1163
|
||||
|
||||
publish:
|
||||
name: "Publish"
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: ["build", "provenance"]
|
||||
needs: ["build"]
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: "publish"
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Download dists"
|
||||
uses: "actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0" # v5.0.0
|
||||
uses: "actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" # v8.0.0
|
||||
with:
|
||||
artifact-ids: ${{ needs.build.outputs.artifact-id }}
|
||||
path: "dist/"
|
||||
|
||||
- name: "Publish dists to PyPI"
|
||||
uses: "pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc"
|
||||
uses: "pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b" # v1.14.0
|
||||
with:
|
||||
attestations: true
|
||||
|
||||
publish-test-pypi:
|
||||
name: "Publish to Test PyPI"
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
needs: ["build"]
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: "testpypi"
|
||||
|
||||
steps:
|
||||
- name: "Download dists"
|
||||
uses: "actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3" # v8.0.0
|
||||
with:
|
||||
artifact-ids: ${{ needs.build.outputs.artifact-id }}
|
||||
path: "dist/"
|
||||
|
||||
- name: "Publish dists to Test PyPI"
|
||||
uses: "pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b" # v1.14.0
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
attestations: true
|
||||
|
||||
30
.github/workflows/run-tests.yml
vendored
30
.github/workflows/run-tests.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev", "pypy-3.10", "pypy-3.11"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15-dev", "pypy-3.11"]
|
||||
os: [ubuntu-22.04, macOS-latest, windows-latest]
|
||||
# Pypy-3.11 can't install openssl-sys with rust
|
||||
# which prevents us from testing in GHA.
|
||||
@@ -20,14 +20,18 @@ jobs:
|
||||
- { python-version: "pypy-3.11", os: "windows-latest" }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
allow-prereleases: true
|
||||
- name: Install dependencies
|
||||
env:
|
||||
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: ${{ matrix.python-version == '3.15-dev' && '1' || '' }}
|
||||
run: |
|
||||
make
|
||||
- name: Run tests
|
||||
@@ -41,11 +45,13 @@ jobs:
|
||||
fail-fast: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
- name: 'Set up Python 3.9'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
python-version: '3.9'
|
||||
persist-credentials: false
|
||||
- name: 'Set up Python 3.10'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
make
|
||||
@@ -61,11 +67,13 @@ jobs:
|
||||
fail-fast: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
- name: 'Set up Python 3.9'
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
python-version: '3.9'
|
||||
persist-credentials: false
|
||||
- name: 'Set up Python 3.10'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
make
|
||||
|
||||
32
.github/workflows/typecheck.yml
vendored
Normal file
32
.github/workflows/typecheck.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Type Check
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.14"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install pip==26.0.1
|
||||
python -m pip install -e . --group typecheck
|
||||
|
||||
- name: Run pyright
|
||||
run: python -m pyright src/requests/
|
||||
24
.github/workflows/zizmor.yml
vendored
Normal file
24
.github/workflows/zizmor.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Sourced from https://github.com/zizmorcore/zizmor-action
|
||||
name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
|
||||
@@ -2,27 +2,20 @@ exclude: 'docs/|ext/'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-case-conflict
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: [--fix=auto]
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.16.4
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.7.0
|
||||
hooks:
|
||||
- id: black
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
exclude: tests/test_lowlevel.py
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.10.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
|
||||
14
AUTHORS.rst
14
AUTHORS.rst
@@ -1,17 +1,17 @@
|
||||
Requests was lovingly created by Kenneth Reitz.
|
||||
|
||||
Keepers of the Crystals
|
||||
```````````````````````
|
||||
Requests Maintainers
|
||||
````````````````````
|
||||
|
||||
- Ian Stapleton Cordasco <graffatcolmingov@gmail.com> `@sigmavirus24 <https://github.com/sigmavirus24>`_.
|
||||
- Nate Prewitt `@nateprewitt <https://github.com/nateprewitt>`_.
|
||||
- Seth M. Larson `@sethmlarson <https://github.com/sethmlarson>`_.
|
||||
|
||||
Previous Keepers of Crystals
|
||||
````````````````````````````
|
||||
Previous Maintainers
|
||||
````````````````````
|
||||
|
||||
- Kenneth Reitz <me@kennethreitz.org> `@kennethreitz <https://github.com/kennethreitz>`_, reluctant Keeper of the Master Crystal.
|
||||
- Cory Benfield <cory@lukasa.co.uk> `@lukasa <https://github.com/lukasa>`_
|
||||
- Ian Cordasco <graffatcolmingov@gmail.com> `@sigmavirus24 <https://github.com/sigmavirus24>`_.
|
||||
|
||||
- Seth M. Larson `@sethmlarson <https://github.com/sethmlarson>`_.
|
||||
|
||||
Patches and Suggestions
|
||||
```````````````````````
|
||||
|
||||
90
HISTORY.md
90
HISTORY.md
@@ -6,6 +6,96 @@ dev
|
||||
|
||||
- \[Short description of non-trivial change.\]
|
||||
|
||||
|
||||
2.34.2 (2026-05-14)
|
||||
-------------------
|
||||
- Moved `headers` input type back to `Mapping` to avoid invariance issues
|
||||
with `MutableMapping` and inferred dict types. Users calling
|
||||
`Request.headers.update()` may need to narrow typing in their code. (#7441)
|
||||
|
||||
|
||||
2.34.1 (2026-05-13)
|
||||
-------------------
|
||||
|
||||
**Bugfixes**
|
||||
- Widened `json` input type from `dict` and `list` to `Mapping`
|
||||
and `Sequence`. (#7436)
|
||||
- Changed `headers` input type to MutableMapping and removed `None` from
|
||||
`Request.headers` typing to improve handling for users. (#7431)
|
||||
- `Response.reason` moved from `str | None` to `str` to improve handling
|
||||
for users. (#7437)
|
||||
- Fixed a bug where some bodies with custom `__getattr__` implementations
|
||||
weren't being properly detected as Iterables. (#7433)
|
||||
|
||||
|
||||
2.34.0 (2026-05-11)
|
||||
-------------------
|
||||
|
||||
**Announcements**
|
||||
- Requests 2.34.0 introduces inline types, replacing those provided by
|
||||
typeshed. Public API types should be fully compatible with mypy, pyright,
|
||||
and ty. We believe types are comprehensive but if you find issues, please
|
||||
report them to the pinned tracking issue.
|
||||
|
||||
Special thanks to @bastimeyer, @cthoyt, @edgarrmondragon, and @srittau for
|
||||
helping review and test the types ahead of the release. (#7272)
|
||||
|
||||
**Improvements**
|
||||
- Digest Auth hashing algorithms have added `usedforsecurity=False` to clarify
|
||||
security considerations. (#7310)
|
||||
- Requests added support for Python 3.15 based on beta1. Downstream projects
|
||||
should be able to start testing prior to its release in October. (#7422)
|
||||
- Requests added support for Python 3.14t. (#7419)
|
||||
|
||||
**Bugfixes**
|
||||
- ``Response.history`` no longer contains a reference to itself, preventing
|
||||
accidental looping when traversing the history list. (#7328)
|
||||
- Requests no longer performs greedy matching on no_proxy domains. The
|
||||
proxy_bypass implementation has been updated with CPython's fix from
|
||||
bpo-39057. (#7427)
|
||||
- Requests no longer incorrectly strips duplicate leading slashes in
|
||||
URI paths. This should address user issues with specific presigned
|
||||
URLs. Note the full fix requires urllib3 2.7.0+. (#7315)
|
||||
|
||||
|
||||
2.33.1 (2026-03-30)
|
||||
-------------------
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed test cleanup for CVE-2026-25645 to avoid leaving unnecessary
|
||||
files in the tmp directory. (#7305)
|
||||
- Fixed Content-Type header parsing for malformed values. (#7309)
|
||||
- Improved error consistency for malformed header values. (#7308)
|
||||
|
||||
2.33.0 (2026-03-25)
|
||||
-------------------
|
||||
|
||||
**Announcements**
|
||||
- 📣 Requests is adding inline types. If you have a typed code base that
|
||||
uses Requests, please take a look at #7271. Give it a try, and report
|
||||
any gaps or feedback you may have in the issue. 📣
|
||||
|
||||
**Security**
|
||||
- CVE-2026-25645 ``requests.utils.extract_zipped_paths`` now extracts
|
||||
contents to a non-deterministic location to prevent malicious file
|
||||
replacement. This does not affect default usage of Requests, only
|
||||
applications calling the utility function directly.
|
||||
|
||||
**Improvements**
|
||||
- Migrated to a PEP 517 build system using setuptools. (#7012)
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed an issue where an empty netrc entry could cause
|
||||
malformed authentication to be applied to Requests on
|
||||
Python 3.11+. (#7205)
|
||||
|
||||
**Deprecations**
|
||||
- Dropped support for Python 3.9 following its end of support. (#7196)
|
||||
|
||||
**Documentation**
|
||||
- Various typo fixes and doc improvements.
|
||||
|
||||
|
||||
2.32.5 (2025-08-18)
|
||||
-------------------
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
@@ -19,11 +25,7 @@
|
||||
|
||||
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 `30M downloads / week`— according to GitHub, Requests is currently [depended upon](https://github.com/psf/requests/network/dependents?package_id=UGFja2FnZS01NzA4OTExNg%3D%3D) by `1,000,000+` repositories. You may certainly put your trust in this code.
|
||||
|
||||
[](https://pepy.tech/project/requests)
|
||||
[](https://pypi.org/project/requests)
|
||||
[](https://github.com/psf/requests/graphs/contributors)
|
||||
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
|
||||
|
||||
@@ -33,7 +35,7 @@ Requests is available on PyPI:
|
||||
$ python -m pip install requests
|
||||
```
|
||||
|
||||
Requests officially supports Python 3.9+.
|
||||
Requests officially supports Python 3.10+.
|
||||
|
||||
## Supported Features & Best–Practices
|
||||
|
||||
@@ -53,10 +55,6 @@ Requests is ready for the demands of building robust and reliable HTTP–speakin
|
||||
- Automatic honoring of `.netrc`
|
||||
- Chunked HTTP Requests
|
||||
|
||||
## API Reference and User Guide available on [Read the Docs](https://requests.readthedocs.io)
|
||||
|
||||
[](https://requests.readthedocs.io)
|
||||
|
||||
## Cloning the repository
|
||||
|
||||
When cloning the Requests repository, you may need to add the `-c
|
||||
|
||||
@@ -9,7 +9,7 @@ BUILDDIR = _build
|
||||
|
||||
# User-friendly check for sphinx-build
|
||||
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
|
||||
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/)
|
||||
endif
|
||||
|
||||
# Internal variables.
|
||||
|
||||
187
docs/_static/custom.css
vendored
187
docs/_static/custom.css
vendored
@@ -1,177 +1,12 @@
|
||||
body > div.document > div.sphinxsidebar > div > form > table > tbody > tr:nth-child(2) > td > select {
|
||||
width: 100%!important;
|
||||
}
|
||||
|
||||
#python27 > a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Carbon by BuySellAds */
|
||||
#carbonads {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin: 1.5em 0 2em;
|
||||
padding: 1em;
|
||||
border: solid 1px #cccccc;
|
||||
border-radius: 2px;
|
||||
background-color: #eeeeee;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#carbonads a {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
#carbonads span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.carbon-img {
|
||||
display: block;
|
||||
margin: 0 auto 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.carbon-text {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.carbon-poweredby {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Native CPC by BuySellAds */
|
||||
|
||||
#native-ribbon #_custom_ {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-shadow: 0 -1px 4px 1px hsla(0, 0%, 0%, .15);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu,
|
||||
Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
transition: all .25s ease-in-out;
|
||||
transform: translateY(calc(100% - 35px));
|
||||
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
#native-ribbon #_custom_:hover {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.native-img {
|
||||
margin-right: 20px;
|
||||
max-height: 50px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.native-sponsor {
|
||||
margin: 10px 20px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
transition: all .3s ease-in-out;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
#native-ribbon #_custom_:hover .native-sponsor {
|
||||
margin: 0 20px;
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
.native-flex {
|
||||
display: flex;
|
||||
padding: 10px 20px 25px;
|
||||
text-decoration: none;
|
||||
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.native-main {
|
||||
display: flex;
|
||||
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.native-details {
|
||||
display: flex;
|
||||
margin-right: 30px;
|
||||
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.native-company {
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.native-desc {
|
||||
letter-spacing: 1px;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.native-cta {
|
||||
padding: 10px 14px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 6px 13px 0 hsla(0, 0%, 0%, .15);
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
transition: all .3s ease-in-out;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.native-cta:hover {
|
||||
box-shadow: none;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 320px) and (max-width: 759px) {
|
||||
.native-flex {
|
||||
padding: 5px 5px 15px;
|
||||
flex-direction: column;
|
||||
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.native-img {
|
||||
margin: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.native-details {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.native-main {
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.native-cta {
|
||||
display: none;
|
||||
}
|
||||
img.logo {margin-left: -20px!important;}
|
||||
|
||||
div#searchbox h3 {text-transform: capitalize;}
|
||||
|
||||
div.document {width: 1008px;}
|
||||
|
||||
div.highlight pre {padding: 11px 14px;}
|
||||
|
||||
@media screen and (max-width: 1008px) {
|
||||
div.document {width: 100%!important;}
|
||||
div.highlight pre {margin-right: -30px;}
|
||||
}
|
||||
|
||||
80
docs/_templates/hacks.html
vendored
80
docs/_templates/hacks.html
vendored
@@ -1,80 +0,0 @@
|
||||
<!-- Alabaster (krTheme++) Hacks -->
|
||||
|
||||
<!-- CSS Adjustments (I'm very picky.) -->
|
||||
<style type="text/css">
|
||||
|
||||
/* Rezzy requires precise alignment. */
|
||||
img.logo {margin-left: -20px!important;}
|
||||
|
||||
/* "Quick Search" should be capitalized. */
|
||||
div#searchbox h3 {text-transform: capitalize;}
|
||||
|
||||
/* Make the document a little wider, less code is cut-off. */
|
||||
div.document {width: 1008px;}
|
||||
|
||||
/* Much-improved spacing around code blocks. */
|
||||
div.highlight pre {padding: 11px 14px;}
|
||||
|
||||
/* Remain Responsive! */
|
||||
@media screen and (max-width: 1008px) {
|
||||
div.sphinxsidebar {display: none;}
|
||||
div.document {width: 100%!important;}
|
||||
|
||||
/* Have code blocks escape the document right-margin. */
|
||||
div.highlight pre {margin-right: -30px;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Native CPC by BuySellAds -->
|
||||
|
||||
<script type="text/javascript" src="//m.servedby-buysellads.com/monetization.js"></script>
|
||||
|
||||
<div id="native-ribbon">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
_bsa.init('custom', 'CK7D62JU', 'placement:pythonrequestsorg',
|
||||
{
|
||||
target: '#native-ribbon',
|
||||
template: `
|
||||
<div class="native-sponsor">Sponsored by ##company## — Learn More</div>
|
||||
<a href="##link##" class="native-flex">
|
||||
<style>
|
||||
#native-ribbon #_custom_ {
|
||||
background: linear-gradient(-30deg, ##backgroundColor##E5, ##backgroundColor##E5 45%, ##backgroundColor## 45%) #fff;
|
||||
}
|
||||
|
||||
.native-details,
|
||||
.native-sponsor,
|
||||
.native-bsa {
|
||||
color: ##textColor## !important;
|
||||
}
|
||||
|
||||
.native-details:hover {
|
||||
color: ##textColorHover## !important;
|
||||
}
|
||||
|
||||
.native-cta {
|
||||
color: ##ctaTextColor##;
|
||||
background-color: ##ctaBackgroundColor##;
|
||||
}
|
||||
|
||||
.native-cta:hover {
|
||||
color: ##ctaTextColorHover##;
|
||||
background-color: ##ctaBackgroundColorHover##;
|
||||
}
|
||||
</style>
|
||||
<div class="native-main">
|
||||
<img class="native-img" src="##logo##">
|
||||
<div class="native-details">
|
||||
<span class="native-company">##title##</span>
|
||||
<span class="native-desc">##description##</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="native-cta">##callToAction##</span>
|
||||
</a>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
);
|
||||
</script>
|
||||
33
docs/_templates/sidebar.html
vendored
Normal file
33
docs/_templates/sidebar.html
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<p class="logo">
|
||||
<a href="{{ pathto(root_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" alt="{{ _('Requests logo') }}" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=psf&repo=requests&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{{ _('Requests is an elegant and simple HTTP library for Python, built for human beings.') }}
|
||||
</p>
|
||||
|
||||
<h3>{{ _('Useful Links') }}</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto('user/quickstart') }}">{{ _('Quickstart') }}</a></li>
|
||||
<li><a href="{{ pathto('user/advanced') }}">{{ _('Advanced Usage') }}</a></li>
|
||||
<li><a href="{{ pathto('api') }}">{{ _('API Reference') }}</a></li>
|
||||
<li><a href="{{ pathto('community/updates') + '#release-history' }}">{{ _('Release History') }}</a></li>
|
||||
<li><a href="{{ pathto('dev/contributing') }}">{{ _('Contributors Guide') }}</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="{{ pathto('community/recommended') }}">{{ _('Recommended Packages and Extensions') }}</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="https://github.com/psf/requests">{{ _('Requests @ GitHub') }}</a></li>
|
||||
<li><a href="https://pypi.org/project/requests/">{{ _('Requests @ PyPI') }}</a></li>
|
||||
<li><a href="https://github.com/psf/requests/issues">{{ _('Issue Tracker') }}</a></li>
|
||||
</ul>
|
||||
37
docs/_templates/sidebarintro.html
vendored
37
docs/_templates/sidebarintro.html
vendored
@@ -1,37 +0,0 @@
|
||||
<p class="logo">
|
||||
<a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/requests-sidebar.png', 1) }}" alt="Requests logo" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=psf&repo=requests&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Requests is an elegant and simple HTTP library for Python, built for
|
||||
human beings.
|
||||
</p>
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto('user/quickstart') }}">Quickstart</a></li>
|
||||
<li><a href="{{ pathto('user/advanced') }}">Advanced Usage</a></li>
|
||||
<li><a href="{{ pathto('api') }}">API Reference</a></li>
|
||||
<li><a href="{{ pathto('community/updates') + '#release-history' }}">Release History</a></li>
|
||||
<li><a href="{{ pathto('dev/contributing') }}">Contributors Guide</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="{{ pathto('community/recommended') }}">Recommended Packages and Extensions</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="https://github.com/psf/requests">Requests @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/requests/">Requests @ PyPI</a></li>
|
||||
<li><a href="https://github.com/psf/requests/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
|
||||
<div id="native-ribbon">
|
||||
</div>
|
||||
30
docs/_templates/sidebarlogo.html
vendored
30
docs/_templates/sidebarlogo.html
vendored
@@ -1,30 +0,0 @@
|
||||
<p>
|
||||
<iframe src="https://ghbtns.com/github-btn.html?user=psf&repo=requests&type=watch&count=true&size=large"
|
||||
allowtransparency="true" frameborder="0" scrolling="0" width="200px" height="35px"></iframe>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Requests is an elegant and simple HTTP library for Python, built for
|
||||
human beings. You are currently looking at the documentation of the
|
||||
development release.
|
||||
</p>
|
||||
|
||||
<h3>Useful Links</h3>
|
||||
<ul>
|
||||
<li><a href="{{ pathto('user/quickstart') }}">Quickstart</a></li>
|
||||
<li><a href="{{ pathto('user/advanced') }}">Advanced Usage</a></li>
|
||||
<li><a href="{{ pathto('api') }}">API Reference</a></li>
|
||||
<li><a href="{{ pathto('community/updates') + '#release-history' }}">Release History</a></li>
|
||||
<li><a href="{{ pathto('dev/contributing') }}">Contributors Guide</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="{{ pathto('community/recommended') }}">Recommended Packages and Extensions</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="https://github.com/psf/requests">Requests @ GitHub</a></li>
|
||||
<li><a href="https://pypi.org/project/requests/">Requests @ PyPI</a></li>
|
||||
<li><a href="https://github.com/psf/requests/issues">Issue Tracker</a></li>
|
||||
</ul>
|
||||
|
||||
@@ -55,6 +55,7 @@ Lower-Level Classes
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
:exclude-members: is_permanent_redirect
|
||||
|
||||
|
||||
Lower-Lower-Level Classes
|
||||
|
||||
@@ -30,7 +30,7 @@ Why not Httplib2?
|
||||
-----------------
|
||||
|
||||
Chris Adams gave an excellent summary on
|
||||
`Hacker News <http://news.ycombinator.com/item?id=2884406>`_:
|
||||
`Hacker News <https://news.ycombinator.com/item?id=2884406>`_:
|
||||
|
||||
httplib2 is part of why you should use requests: it's far more respectable
|
||||
as a client but not as well documented and it still takes way too much code
|
||||
@@ -44,8 +44,8 @@ Chris Adams gave an excellent summary on
|
||||
Disclosure: I'm listed in the requests AUTHORS file but can claim credit
|
||||
for, oh, about 0.0001% of the awesomeness.
|
||||
|
||||
1. http://code.google.com/p/httplib2/issues/detail?id=96 is a good example:
|
||||
an annoying bug which affect many people, there was a fix available for
|
||||
1. https://code.google.com/p/httplib2/issues/detail?id=96 is a good example:
|
||||
an annoying bug that affected many people, there was a fix available for
|
||||
months, which worked great when I applied it in a fork and pounded a couple
|
||||
TB of data through it, but it took over a year to make it into trunk and
|
||||
even longer to make it onto PyPI where any other project which required "
|
||||
|
||||
@@ -7,4 +7,4 @@ Articles & Talks
|
||||
- `Issac Kelly's 'Consuming Web APIs' talk <https://issackelly.github.io/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
|
||||
- `Blog post about Requests via Yum <https://arunsag.wordpress.com/2011/08/17/new-package-python-requests-http-for-humans/>`_
|
||||
- `Russian blog post introducing Requests <https://habr.com/post/126262/>`_
|
||||
- `Sending JSON in Requests <http://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_
|
||||
- `Sending JSON in Requests <https://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_
|
||||
|
||||
17
docs/conf.py
17
docs/conf.py
@@ -54,11 +54,11 @@ source_suffix = ".rst"
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
root_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u"Requests"
|
||||
copyright = u'MMXVIX. A Kenneth Reitz Project'
|
||||
copyright = u"Kenneth Reitz and contributors"
|
||||
author = u"Kenneth Reitz"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
@@ -126,7 +126,7 @@ html_theme = "alabaster"
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
"show_powered_by": False,
|
||||
"github_user": "requests",
|
||||
"github_user": "psf",
|
||||
"github_repo": "requests",
|
||||
"github_banner": True,
|
||||
"show_related": False,
|
||||
@@ -172,14 +172,13 @@ html_use_smartypants = False
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
"index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"],
|
||||
"index": ["sidebar.html", "sourcelink.html", "searchbox.html"],
|
||||
"**": [
|
||||
"sidebarlogo.html",
|
||||
"sidebar.html",
|
||||
"localtoc.html",
|
||||
"relations.html",
|
||||
"sourcelink.html",
|
||||
"searchbox.html",
|
||||
"hacks.html",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -247,7 +246,7 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, "Requests.tex", u"Requests Documentation", u"Kenneth Reitz", "manual")
|
||||
(root_doc, "Requests.tex", u"Requests Documentation", u"Kenneth Reitz", "manual")
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@@ -275,7 +274,7 @@ latex_documents = [
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [(master_doc, "requests", u"Requests Documentation", [author], 1)]
|
||||
man_pages = [(root_doc, "requests", u"Requests Documentation", [author], 1)]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
@@ -288,7 +287,7 @@ man_pages = [(master_doc, "requests", u"Requests Documentation", [author], 1)]
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
root_doc,
|
||||
"Requests",
|
||||
u"Requests Documentation",
|
||||
author,
|
||||
|
||||
@@ -14,7 +14,7 @@ getting a feel for how contributing to this project works. If you have any
|
||||
questions, feel free to reach out to either `Nate Prewitt`_, `Ian Cordasco`_,
|
||||
or `Seth Michael Larson`_, the primary maintainers.
|
||||
|
||||
.. _Ian Cordasco: http://www.coglib.com/~icordasc/
|
||||
.. _Ian Cordasco: https://www.coglib.com/~icordasc/
|
||||
.. _Nate Prewitt: https://www.nateprewitt.com/
|
||||
.. _Seth Michael Larson: https://sethmlarson.dev/
|
||||
|
||||
@@ -132,8 +132,8 @@ files and a semi-formal, yet friendly and approachable, prose style.
|
||||
When presenting Python code, use single-quoted strings (``'hello'`` instead of
|
||||
``"hello"``).
|
||||
|
||||
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
|
||||
.. _Sphinx: http://sphinx-doc.org/index.html
|
||||
.. _reStructuredText: https://docutils.sourceforge.net/rst.html
|
||||
.. _Sphinx: https://www.sphinx-doc.org/en/master/
|
||||
|
||||
|
||||
.. _bug-reports:
|
||||
|
||||
@@ -9,21 +9,25 @@ Requests: HTTP for Humans™
|
||||
Release v\ |version|. (:ref:`Installation <install>`)
|
||||
|
||||
|
||||
.. image:: https://static.pepy.tech/badge/requests/month
|
||||
:target: https://pepy.tech/project/requests
|
||||
:alt: Requests Downloads Per Month Badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/requests.svg
|
||||
.. image:: https://img.shields.io/pypi/v/requests.svg?maxAge=86400
|
||||
:target: https://pypi.org/project/requests/
|
||||
:alt: License Badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/wheel/requests.svg
|
||||
:target: https://pypi.org/project/requests/
|
||||
:alt: Wheel Support Badge
|
||||
:alt: PyPI Version Badge
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/requests.svg
|
||||
:target: https://pypi.org/project/requests/
|
||||
:alt: Python Version Support Badge
|
||||
:alt: Supported Versions Badge
|
||||
|
||||
.. image:: https://static.pepy.tech/badge/requests/month
|
||||
:target: https://pepy.tech/project/requests
|
||||
:alt: Downloads Per Month Badge
|
||||
|
||||
.. image:: https://img.shields.io/github/contributors/psf/requests.svg
|
||||
:target: https://github.com/psf/requests/graphs/contributors
|
||||
:alt: Contributors Badge
|
||||
|
||||
.. image:: https://readthedocs.org/projects/requests/badge/?version=latest
|
||||
:target: https://requests.readthedocs.io
|
||||
:alt: Documentation Badge
|
||||
|
||||
**Requests** is an elegant and simple HTTP library for Python, built for human beings.
|
||||
|
||||
@@ -72,7 +76,7 @@ Requests is ready for today's web.
|
||||
- Chunked Requests
|
||||
- ``.netrc`` Support
|
||||
|
||||
Requests officially supports Python 3.9+, and runs great on PyPy.
|
||||
Requests officially supports Python 3.10+, and runs great on PyPy.
|
||||
|
||||
|
||||
The User Guide
|
||||
|
||||
@@ -65,7 +65,7 @@ if errorlevel 9009 (
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
echo.https://www.sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
|
||||
@@ -467,6 +467,7 @@ Let's print some request method arguments at runtime::
|
||||
You can add multiple hooks to a single request. Let's call two hooks at once::
|
||||
|
||||
>>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]})
|
||||
https://httpbin.org/
|
||||
>>> r.hook_called
|
||||
True
|
||||
|
||||
@@ -677,9 +678,7 @@ In addition to basic HTTP proxies, Requests also supports proxies using the
|
||||
SOCKS protocol. This is an optional feature that requires that additional
|
||||
third-party libraries be installed before use.
|
||||
|
||||
You can get the dependencies for this feature from ``pip``:
|
||||
|
||||
.. code-block:: bash
|
||||
You can get the dependencies for this feature from ``pip``::
|
||||
|
||||
$ python -m pip install 'requests[socks]'
|
||||
|
||||
|
||||
@@ -271,7 +271,9 @@ use the same key::
|
||||
},
|
||||
...
|
||||
}
|
||||
>>> r1.text == r2.text
|
||||
>>> # httpbin may embed non-deterministic metadata,
|
||||
>>> # so we only compare our submitted data here.
|
||||
>>> r1.json()['form'] == r2.json()['form']
|
||||
True
|
||||
|
||||
There are times that you may want to send data that is not form-encoded. If
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 363 KiB |
BIN
ext/ss.png
BIN
ext/ss.png
Binary file not shown.
|
Before Width: | Height: | Size: 363 KiB |
123
pyproject.toml
123
pyproject.toml
@@ -1,10 +1,125 @@
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
src_paths = ["src/requests", "test"]
|
||||
honor_noqa = true
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "requests"
|
||||
description = "Python HTTP for Humans."
|
||||
readme = "README.md"
|
||||
license = {text = "Apache-2.0"}
|
||||
authors = [
|
||||
{ name = "Kenneth Reitz", email = "me@kennethreitz.org" },
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Ian Stapleton Cordasco", email="graffatcolmingov@gmail.com"},
|
||||
{name = "Nate Prewitt", email="nate.prewitt@gmail.com"}
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"charset_normalizer>=2,<4",
|
||||
"idna>=2.5,<4",
|
||||
"urllib3>=1.26,<3",
|
||||
"certifi>=2023.5.7"
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python :: 3.15",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Programming Language :: Python :: Free Threading :: 2 - Beta",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Software Development :: Libraries"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://requests.readthedocs.io"
|
||||
Source = "https://github.com/psf/requests"
|
||||
|
||||
[project.optional-dependencies]
|
||||
security = []
|
||||
socks = ["PySocks>=1.5.6, !=1.5.7"]
|
||||
use_chardet_on_py3 = ["chardet>=3.0.2,<8"]
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
"requests[socks]",
|
||||
"pytest-httpbin==2.1.0",
|
||||
"httpbin~=0.10.0",
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
"pytest-xdist",
|
||||
"pytest>=3",
|
||||
"trustme",
|
||||
]
|
||||
typecheck = [
|
||||
"pyright",
|
||||
"typing_extensions",
|
||||
"chardet",
|
||||
"cryptography",
|
||||
"PyOpenSSL",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
license-files = ["LICENSE", "NOTICE"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
version = {attr = "requests.__version__.__version__"}
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
src = ["src/requests", "tests"]
|
||||
exclude = ["docs/", "ext/"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"T10", # flake8-debugger (replaces debug-statements hook)
|
||||
]
|
||||
ignore = ["E203", "E501", "UP031"]
|
||||
per-file-ignores = {"src/requests/__init__.py" = ["E402", "F401"], "src/requests/compat.py" = ["E402", "F401"], "tests/compat.py" = ["F401"]}
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["requests"]
|
||||
|
||||
[tool.ruff.format]
|
||||
# Use black-compatible formatting
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "--doctest-modules"
|
||||
doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS"
|
||||
minversion = "6.2"
|
||||
testpaths = ["tests"]
|
||||
|
||||
|
||||
[tool.pyright]
|
||||
include = ["src/requests"]
|
||||
typeCheckingMode = "strict"
|
||||
# linting rules unrelated to type checking
|
||||
reportPrivateUsage = false
|
||||
reportPrivateImportUsage = false
|
||||
reportUnnecessaryIsInstance = false
|
||||
reportUnusedImport = false # duplicate of Ruff's F401 rule
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-e .[socks]
|
||||
pytest>=2.8.0,<9
|
||||
pytest>=2.8.0,<10
|
||||
pytest-cov
|
||||
pytest-httpbin==2.1.0
|
||||
httpbin~=0.10.0
|
||||
|
||||
17
setup.cfg
17
setup.cfg
@@ -1,17 +0,0 @@
|
||||
[metadata]
|
||||
license_file = LICENSE
|
||||
provides-extra =
|
||||
socks
|
||||
use_chardet_on_py3
|
||||
requires-dist =
|
||||
certifi>=2017.4.17
|
||||
charset_normalizer>=2,<4
|
||||
idna>=2.5,<4
|
||||
urllib3>=1.21.1,<3
|
||||
|
||||
[flake8]
|
||||
ignore = E203, E501, W503
|
||||
per-file-ignores =
|
||||
src/requests/__init__.py:E402, F401
|
||||
src/requests/compat.py:E402, F401
|
||||
tests/compat.py:F401
|
||||
108
setup.py
108
setup.py
@@ -1,107 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from codecs import open
|
||||
|
||||
if sys.version_info < (3, 10): # noqa: UP036
|
||||
sys.stderr.write("Requests requires Python 3.10 or later.\n")
|
||||
sys.exit(1)
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
CURRENT_PYTHON = sys.version_info[:2]
|
||||
REQUIRED_PYTHON = (3, 9)
|
||||
|
||||
if CURRENT_PYTHON < REQUIRED_PYTHON:
|
||||
sys.stderr.write(
|
||||
"""
|
||||
==========================
|
||||
Unsupported Python version
|
||||
==========================
|
||||
This version of Requests requires at least Python {}.{}, but
|
||||
you're trying to install it on Python {}.{}. To resolve this,
|
||||
consider upgrading to a supported Python version.
|
||||
|
||||
If you can't upgrade your Python version, you'll need to
|
||||
pin to an older version of Requests (<2.32.0).
|
||||
""".format(
|
||||
*(REQUIRED_PYTHON + CURRENT_PYTHON)
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# 'setup.py publish' shortcut.
|
||||
if sys.argv[-1] == "publish":
|
||||
os.system("python setup.py sdist bdist_wheel")
|
||||
os.system("twine upload dist/*")
|
||||
sys.exit()
|
||||
|
||||
requires = [
|
||||
"charset_normalizer>=2,<4",
|
||||
"idna>=2.5,<4",
|
||||
"urllib3>=1.21.1,<3",
|
||||
"certifi>=2017.4.17",
|
||||
]
|
||||
test_requirements = [
|
||||
"pytest-httpbin==2.1.0",
|
||||
"pytest-cov",
|
||||
"pytest-mock",
|
||||
"pytest-xdist",
|
||||
"PySocks>=1.5.6, !=1.5.7",
|
||||
"pytest>=3",
|
||||
]
|
||||
|
||||
about = {}
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
with open(os.path.join(here, "src", "requests", "__version__.py"), "r", "utf-8") as f:
|
||||
exec(f.read(), about)
|
||||
|
||||
with open("README.md", "r", "utf-8") as f:
|
||||
readme = f.read()
|
||||
|
||||
setup(
|
||||
name=about["__title__"],
|
||||
version=about["__version__"],
|
||||
description=about["__description__"],
|
||||
long_description=readme,
|
||||
long_description_content_type="text/markdown",
|
||||
author=about["__author__"],
|
||||
author_email=about["__author_email__"],
|
||||
url=about["__url__"],
|
||||
packages=["requests"],
|
||||
package_data={"": ["LICENSE", "NOTICE"]},
|
||||
package_dir={"": "src"},
|
||||
include_package_data=True,
|
||||
python_requires=">=3.9",
|
||||
install_requires=requires,
|
||||
license=about["__license__"],
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
tests_require=test_requirements,
|
||||
extras_require={
|
||||
"security": [],
|
||||
"socks": ["PySocks>=1.5.6, !=1.5.7"],
|
||||
"use_chardet_on_py3": ["chardet>=3.0.2,<6"],
|
||||
},
|
||||
project_urls={
|
||||
"Documentation": "https://requests.readthedocs.io",
|
||||
"Source": "https://github.com/psf/requests",
|
||||
},
|
||||
)
|
||||
setup()
|
||||
|
||||
@@ -38,6 +38,8 @@ is at <https://requests.readthedocs.io>.
|
||||
:license: Apache 2.0, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
|
||||
import urllib3
|
||||
@@ -55,16 +57,20 @@ except ImportError:
|
||||
chardet_version = None
|
||||
|
||||
|
||||
def check_compatibility(urllib3_version, chardet_version, charset_normalizer_version):
|
||||
urllib3_version = urllib3_version.split(".")
|
||||
assert urllib3_version != ["dev"] # Verify urllib3 isn't installed from git.
|
||||
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) == 2:
|
||||
urllib3_version.append("0")
|
||||
if len(urllib3_version_list) == 2:
|
||||
urllib3_version_list.append("0")
|
||||
|
||||
# Check urllib3 for compatibility.
|
||||
major, minor, patch = urllib3_version # noqa: F811
|
||||
major, minor, patch = urllib3_version_list # noqa: F811
|
||||
major, minor, patch = int(major), int(minor), int(patch)
|
||||
# urllib3 >= 1.21.1
|
||||
assert major >= 1
|
||||
@@ -75,8 +81,8 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
|
||||
if chardet_version:
|
||||
major, minor, patch = chardet_version.split(".")[:3]
|
||||
major, minor, patch = int(major), int(minor), int(patch)
|
||||
# chardet_version >= 3.0.2, < 6.0.0
|
||||
assert (3, 0, 2) <= (major, minor, patch) < (6, 0, 0)
|
||||
# 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)
|
||||
@@ -90,31 +96,30 @@ def check_compatibility(urllib3_version, chardet_version, charset_normalizer_ver
|
||||
)
|
||||
|
||||
|
||||
def _check_cryptography(cryptography_version):
|
||||
def _check_cryptography(cryptography_version: str) -> None:
|
||||
# cryptography < 1.3.4
|
||||
try:
|
||||
cryptography_version = list(map(int, cryptography_version.split(".")))
|
||||
cryptography_version_list = list(map(int, cryptography_version.split(".")))
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if cryptography_version < [1, 3, 4]:
|
||||
warning = "Old version of cryptography ({}) may cause slowdown.".format(
|
||||
cryptography_version
|
||||
)
|
||||
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__, chardet_version, charset_normalizer_version
|
||||
urllib3.__version__,
|
||||
chardet_version,
|
||||
charset_normalizer_version,
|
||||
)
|
||||
except (AssertionError, ValueError):
|
||||
warnings.warn(
|
||||
"urllib3 ({}) or chardet ({})/charset_normalizer ({}) doesn't match a supported "
|
||||
"version!".format(
|
||||
urllib3.__version__, chardet_version, charset_normalizer_version
|
||||
),
|
||||
f"urllib3 ({urllib3.__version__}) or chardet "
|
||||
f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) "
|
||||
"doesn't match a supported version!",
|
||||
RequestsDependencyWarning,
|
||||
)
|
||||
|
||||
@@ -133,7 +138,9 @@ try:
|
||||
pyopenssl.inject_into_urllib3()
|
||||
|
||||
# Check cryptography version
|
||||
from cryptography import __version__ as cryptography_version
|
||||
from cryptography import (
|
||||
__version__ as cryptography_version,
|
||||
)
|
||||
|
||||
_check_cryptography(cryptography_version)
|
||||
except ImportError:
|
||||
@@ -178,6 +185,34 @@ 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.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
__title__ = "requests"
|
||||
__description__ = "Python HTTP for Humans."
|
||||
__url__ = "https://requests.readthedocs.io"
|
||||
__version__ = "2.32.5"
|
||||
__build__ = 0x023205
|
||||
__version__ = "2.34.2"
|
||||
__build__ = 0x023402
|
||||
__author__ = "Kenneth Reitz"
|
||||
__author_email__ = "me@kennethreitz.org"
|
||||
__license__ = "Apache-2.0"
|
||||
|
||||
@@ -5,14 +5,15 @@ 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]*$")
|
||||
_VALID_HEADER_NAME_RE_STR = re.compile(r"^[^:\s][^:\r\n]*$")
|
||||
_VALID_HEADER_VALUE_RE_BYTE = re.compile(rb"^\S[^\r\n]*$|^$")
|
||||
_VALID_HEADER_VALUE_RE_STR = re.compile(r"^\S[^\r\n]*$|^$")
|
||||
_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)
|
||||
@@ -22,7 +23,7 @@ HEADER_VALIDATORS = {
|
||||
}
|
||||
|
||||
|
||||
def to_native_string(string, encoding="ascii"):
|
||||
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.
|
||||
@@ -35,7 +36,7 @@ def to_native_string(string, encoding="ascii"):
|
||||
return out
|
||||
|
||||
|
||||
def unicode_is_ascii(u_string):
|
||||
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
|
||||
|
||||
188
src/requests/_types.py
Normal file
188
src/requests/_types.py
Normal file
@@ -0,0 +1,188 @@
|
||||
"""
|
||||
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, Sequence
|
||||
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: ...
|
||||
|
||||
|
||||
def has_read(obj: Any) -> TypeIs[SupportsRead[str | bytes]]:
|
||||
"""Check if obj supports read, including __getattr__ based proxies."""
|
||||
return isinstance(obj, SupportsRead) or hasattr(obj, "read")
|
||||
|
||||
|
||||
@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 = 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
|
||||
| Sequence["JsonType"]
|
||||
| Mapping[str, "JsonType"]
|
||||
)
|
||||
|
||||
# TypedDicts for Unpack kwargs (PEP 692)
|
||||
|
||||
class BaseRequestKwargs(TypedDict, total=False):
|
||||
headers: HeadersType
|
||||
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
|
||||
@@ -6,22 +6,27 @@ 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
|
||||
import typing
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
from urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
|
||||
from urllib3.exceptions import HTTPError as _HTTPError
|
||||
from urllib3.exceptions import InvalidHeader as _InvalidHeader
|
||||
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 ReadTimeoutError, ResponseError
|
||||
from urllib3.exceptions import SSLError as _SSLError
|
||||
from urllib3.poolmanager import PoolManager, proxy_from_url
|
||||
from urllib3.util import Timeout as TimeoutSauce
|
||||
@@ -47,7 +52,6 @@ from .models import Response
|
||||
from .structures import CaseInsensitiveDict
|
||||
from .utils import (
|
||||
DEFAULT_CA_BUNDLE_PATH,
|
||||
extract_zipped_paths,
|
||||
get_auth_from_url,
|
||||
get_encoding_from_headers,
|
||||
prepend_scheme_if_needed,
|
||||
@@ -56,16 +60,21 @@ from .utils import (
|
||||
)
|
||||
|
||||
try:
|
||||
from urllib3.contrib.socks import SOCKSProxyManager
|
||||
from urllib3.contrib.socks import SOCKSProxyManager # type: ignore[assignment]
|
||||
except ImportError:
|
||||
|
||||
def SOCKSProxyManager(*args, **kwargs):
|
||||
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
|
||||
@@ -74,13 +83,13 @@ DEFAULT_POOL_TIMEOUT = None
|
||||
|
||||
|
||||
def _urllib3_request_context(
|
||||
request: "PreparedRequest",
|
||||
verify: "bool | str | None",
|
||||
client_cert: "typing.Tuple[str, str] | str | None",
|
||||
poolmanager: "PoolManager",
|
||||
) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])":
|
||||
host_params = {}
|
||||
pool_kwargs = {}
|
||||
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
|
||||
@@ -113,12 +122,18 @@ def _urllib3_request_context(
|
||||
class BaseAdapter:
|
||||
"""The Base Transport Adapter"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def send(
|
||||
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
|
||||
):
|
||||
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 <PreparedRequest>` being sent.
|
||||
@@ -135,7 +150,7 @@ class BaseAdapter:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Cleans up adapter specific items."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -167,7 +182,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
>>> s.mount('http://', a)
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
__attrs__: list[str] = [
|
||||
"max_retries",
|
||||
"config",
|
||||
"_pool_connections",
|
||||
@@ -175,13 +190,21 @@ class HTTPAdapter(BaseAdapter):
|
||||
"_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=DEFAULT_POOLSIZE,
|
||||
pool_maxsize=DEFAULT_POOLSIZE,
|
||||
max_retries=DEFAULT_RETRIES,
|
||||
pool_block=DEFAULT_POOLBLOCK,
|
||||
):
|
||||
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:
|
||||
@@ -197,10 +220,10 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
|
||||
|
||||
def __getstate__(self):
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
return {attr: getattr(self, attr, None) for attr in self.__attrs__}
|
||||
|
||||
def __setstate__(self, state):
|
||||
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 = {}
|
||||
@@ -214,8 +237,12 @@ class HTTPAdapter(BaseAdapter):
|
||||
)
|
||||
|
||||
def init_poolmanager(
|
||||
self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs
|
||||
):
|
||||
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
|
||||
@@ -239,7 +266,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
**pool_kwargs,
|
||||
)
|
||||
|
||||
def proxy_manager_for(self, proxy, **proxy_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
|
||||
@@ -277,7 +304,9 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
return manager
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
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 <requests.adapters.HTTPAdapter>`.
|
||||
@@ -297,7 +326,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
cert_loc = verify
|
||||
|
||||
if not cert_loc:
|
||||
cert_loc = extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
|
||||
cert_loc = DEFAULT_CA_BUNDLE_PATH
|
||||
|
||||
if not cert_loc or not os.path.exists(cert_loc):
|
||||
raise OSError(
|
||||
@@ -333,7 +362,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
f"Could not find the TLS key file, invalid path: {conn.key_file}"
|
||||
)
|
||||
|
||||
def build_response(self, req, resp):
|
||||
def build_response(self, req: PreparedRequest, resp: Any) -> Response:
|
||||
"""Builds a :class:`Response <requests.Response>` object from a urllib3
|
||||
response. This should not be called from user code, and is only exposed
|
||||
for use when subclassing the
|
||||
@@ -343,10 +372,11 @@ class HTTPAdapter(BaseAdapter):
|
||||
: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)
|
||||
response.status_code = getattr(resp, "status", None) # type: ignore[assignment]
|
||||
|
||||
# Make headers case-insensitive.
|
||||
response.headers = CaseInsensitiveDict(getattr(resp, "headers", {}))
|
||||
@@ -370,7 +400,9 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
return response
|
||||
|
||||
def build_connection_pool_key_attributes(self, request, verify, cert=None):
|
||||
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,
|
||||
@@ -400,7 +432,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
alter the other keys to ensure the desired behaviour.
|
||||
|
||||
:param request:
|
||||
The PreparedReqest being sent over the connection.
|
||||
The PreparedRequest being sent over the connection.
|
||||
:type request:
|
||||
:class:`~requests.models.PreparedRequest`
|
||||
:param verify:
|
||||
@@ -420,7 +452,13 @@ class HTTPAdapter(BaseAdapter):
|
||||
"""
|
||||
return _urllib3_request_context(request, verify, cert, self.poolmanager)
|
||||
|
||||
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
|
||||
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 <requests.adapters.HTTPAdapter>`.
|
||||
@@ -438,8 +476,10 @@ class HTTPAdapter(BaseAdapter):
|
||||
(optional) Any user-provided SSL certificate to be used for client
|
||||
authentication (a.k.a., mTLS).
|
||||
:rtype:
|
||||
urllib3.ConnectionPool
|
||||
urllib3.HTTPConnectionPool
|
||||
"""
|
||||
assert _is_prepared(request)
|
||||
|
||||
proxy = select_proxy(request.url, proxies)
|
||||
try:
|
||||
host_params, pool_kwargs = self.build_connection_pool_key_attributes(
|
||||
@@ -469,7 +509,9 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
return conn
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
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.
|
||||
|
||||
@@ -479,7 +521,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
:param url: The URL to connect to.
|
||||
:param proxies: (optional) A Requests-style dictionary of proxies used on this request.
|
||||
:rtype: urllib3.ConnectionPool
|
||||
:rtype: urllib3.HTTPConnectionPool
|
||||
"""
|
||||
warnings.warn(
|
||||
(
|
||||
@@ -510,7 +552,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
return conn
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Disposes of any internal state.
|
||||
|
||||
Currently, this closes the PoolManager and any active ProxyManager,
|
||||
@@ -520,7 +562,9 @@ class HTTPAdapter(BaseAdapter):
|
||||
for proxy in self.proxy_manager.values():
|
||||
proxy.clear()
|
||||
|
||||
def request_url(self, request, proxies):
|
||||
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
|
||||
@@ -534,6 +578,8 @@ class HTTPAdapter(BaseAdapter):
|
||||
: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
|
||||
|
||||
@@ -544,15 +590,13 @@ class HTTPAdapter(BaseAdapter):
|
||||
using_socks_proxy = proxy_scheme.startswith("socks")
|
||||
|
||||
url = request.path_url
|
||||
if url.startswith("//"): # Don't confuse urllib3
|
||||
url = f"/{url.lstrip('/')}"
|
||||
|
||||
if is_proxied_http_request and not using_socks_proxy:
|
||||
url = urldefragauth(request.url)
|
||||
|
||||
return url
|
||||
|
||||
def add_headers(self, request, **kwargs):
|
||||
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 <requests.adapters.HTTPAdapter>`.
|
||||
@@ -566,7 +610,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
"""
|
||||
pass
|
||||
|
||||
def proxy_headers(self, proxy):
|
||||
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
|
||||
@@ -579,7 +623,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
:param proxy: The url of the proxy being used for this request.
|
||||
:rtype: dict
|
||||
"""
|
||||
headers = {}
|
||||
headers: dict[str, str] = {}
|
||||
username, password = get_auth_from_url(proxy)
|
||||
|
||||
if username:
|
||||
@@ -588,8 +632,14 @@ class HTTPAdapter(BaseAdapter):
|
||||
return headers
|
||||
|
||||
def send(
|
||||
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
|
||||
):
|
||||
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 <PreparedRequest>` being sent.
|
||||
@@ -606,6 +656,8 @@ class HTTPAdapter(BaseAdapter):
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
assert _is_prepared(request)
|
||||
|
||||
try:
|
||||
conn = self.get_connection_with_tls_context(
|
||||
request, verify, proxies=proxies, cert=cert
|
||||
@@ -629,29 +681,29 @@ class HTTPAdapter(BaseAdapter):
|
||||
if isinstance(timeout, tuple):
|
||||
try:
|
||||
connect, read = timeout
|
||||
timeout = TimeoutSauce(connect=connect, read=read)
|
||||
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):
|
||||
pass
|
||||
resolved_timeout = timeout
|
||||
else:
|
||||
timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
resolved_timeout = TimeoutSauce(connect=timeout, read=timeout)
|
||||
|
||||
try:
|
||||
resp = conn.urlopen(
|
||||
method=request.method,
|
||||
url=url,
|
||||
body=request.body,
|
||||
headers=request.headers,
|
||||
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=timeout,
|
||||
timeout=resolved_timeout,
|
||||
chunked=chunked,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,10 +8,22 @@ This module implements the Requests API.
|
||||
: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, url, **kwargs):
|
||||
def request(
|
||||
method: str, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]
|
||||
) -> Response:
|
||||
"""Constructs and sends a :class:`Request <Request>`.
|
||||
|
||||
:param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
|
||||
@@ -59,7 +71,9 @@ def request(method, url, **kwargs):
|
||||
return session.request(method=method, url=url, **kwargs)
|
||||
|
||||
|
||||
def get(url, params=None, **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.
|
||||
@@ -73,7 +87,7 @@ def get(url, params=None, **kwargs):
|
||||
return request("get", url, params=params, **kwargs)
|
||||
|
||||
|
||||
def options(url, **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.
|
||||
@@ -85,7 +99,7 @@ def options(url, **kwargs):
|
||||
return request("options", url, **kwargs)
|
||||
|
||||
|
||||
def head(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.
|
||||
@@ -100,7 +114,12 @@ def head(url, **kwargs):
|
||||
return request("head", url, **kwargs)
|
||||
|
||||
|
||||
def post(url, data=None, json=None, **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.
|
||||
@@ -115,7 +134,9 @@ def post(url, data=None, json=None, **kwargs):
|
||||
return request("post", url, data=data, json=json, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **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.
|
||||
@@ -130,7 +151,9 @@ def put(url, data=None, **kwargs):
|
||||
return request("put", url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **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.
|
||||
@@ -145,7 +168,7 @@ def patch(url, data=None, **kwargs):
|
||||
return request("patch", url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **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.
|
||||
|
||||
@@ -5,6 +5,8 @@ requests.auth
|
||||
This module contains the authentication handlers for Requests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
@@ -12,17 +14,24 @@ 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
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"
|
||||
CONTENT_TYPE_MULTI_PART = "multipart/form-data"
|
||||
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, password):
|
||||
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
|
||||
@@ -32,22 +41,22 @@ def _basic_auth_str(username, password):
|
||||
#
|
||||
# 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):
|
||||
if not isinstance(username, basestring): # runtime guard for non-str/bytes
|
||||
warnings.warn(
|
||||
"Non-string usernames will no longer be supported in Requests "
|
||||
"3.0.0. Please convert the object you've passed in ({!r}) to "
|
||||
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.".format(username),
|
||||
"problems.",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
username = str(username)
|
||||
|
||||
if not isinstance(password, basestring):
|
||||
if not isinstance(password, basestring): # runtime guard for non-str/bytes
|
||||
warnings.warn(
|
||||
"Non-string passwords will no longer be supported in Requests "
|
||||
"3.0.0. Please convert the object you've passed in ({!r}) to "
|
||||
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.".format(type(password)),
|
||||
"problems.",
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
password = str(password)
|
||||
@@ -69,18 +78,26 @@ def _basic_auth_str(username, password):
|
||||
class AuthBase:
|
||||
"""Base class that all auth implementations derive from"""
|
||||
|
||||
def __call__(self, r):
|
||||
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."""
|
||||
|
||||
def __init__(self, username, password):
|
||||
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):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.username == getattr(other, "username", None),
|
||||
@@ -88,10 +105,10 @@ class HTTPBasicAuth(AuthBase):
|
||||
]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self == other
|
||||
|
||||
def __call__(self, r):
|
||||
def __call__(self, r: PreparedRequest) -> PreparedRequest:
|
||||
r.headers["Authorization"] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
@@ -99,7 +116,7 @@ class HTTPBasicAuth(AuthBase):
|
||||
class HTTPProxyAuth(HTTPBasicAuth):
|
||||
"""Attaches HTTP Proxy Authentication to a given Request object."""
|
||||
|
||||
def __call__(self, r):
|
||||
def __call__(self, r: PreparedRequest) -> PreparedRequest:
|
||||
r.headers["Proxy-Authorization"] = _basic_auth_str(self.username, self.password)
|
||||
return r
|
||||
|
||||
@@ -107,13 +124,27 @@ class HTTPProxyAuth(HTTPBasicAuth):
|
||||
class HTTPDigestAuth(AuthBase):
|
||||
"""Attaches HTTP Digest Authentication to the given Request object."""
|
||||
|
||||
def __init__(self, username, password):
|
||||
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):
|
||||
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
|
||||
@@ -123,7 +154,7 @@ class HTTPDigestAuth(AuthBase):
|
||||
self._thread_local.pos = None
|
||||
self._thread_local.num_401_calls = None
|
||||
|
||||
def build_digest_header(self, method, url):
|
||||
def build_digest_header(self, method: str, url: str) -> str | None:
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
@@ -142,42 +173,43 @@ class HTTPDigestAuth(AuthBase):
|
||||
# lambdas assume digest modules are imported at the top level
|
||||
if _algorithm == "MD5" or _algorithm == "MD5-SESS":
|
||||
|
||||
def md5_utf8(x):
|
||||
def md5_utf8(x: str | bytes) -> str:
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.md5(x).hexdigest()
|
||||
return hashlib.md5(x, usedforsecurity=False).hexdigest()
|
||||
|
||||
hash_utf8 = md5_utf8
|
||||
elif _algorithm == "SHA":
|
||||
|
||||
def sha_utf8(x):
|
||||
def sha_utf8(x: str | bytes) -> str:
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha1(x).hexdigest()
|
||||
return hashlib.sha1(x, usedforsecurity=False).hexdigest()
|
||||
|
||||
hash_utf8 = sha_utf8
|
||||
elif _algorithm == "SHA-256":
|
||||
|
||||
def sha256_utf8(x):
|
||||
def sha256_utf8(x: str | bytes) -> str:
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha256(x).hexdigest()
|
||||
return hashlib.sha256(x, usedforsecurity=False).hexdigest()
|
||||
|
||||
hash_utf8 = sha256_utf8
|
||||
elif _algorithm == "SHA-512":
|
||||
|
||||
def sha512_utf8(x):
|
||||
def sha512_utf8(x: str | bytes) -> str:
|
||||
if isinstance(x, str):
|
||||
x = x.encode("utf-8")
|
||||
return hashlib.sha512(x).hexdigest()
|
||||
return hashlib.sha512(x, usedforsecurity=False).hexdigest()
|
||||
|
||||
hash_utf8 = sha512_utf8
|
||||
|
||||
KD = lambda s, d: hash_utf8(f"{s}:{d}") # noqa:E731
|
||||
|
||||
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)
|
||||
@@ -202,9 +234,9 @@ class HTTPDigestAuth(AuthBase):
|
||||
s += time.ctime().encode("utf-8")
|
||||
s += os.urandom(8)
|
||||
|
||||
cnonce = hashlib.sha1(s).hexdigest()[:16]
|
||||
cnonce = hashlib.sha1(s, usedforsecurity=False).hexdigest()[:16]
|
||||
if _algorithm == "MD5-SESS":
|
||||
HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}")
|
||||
HA1 = hash_utf8(f"{HA1}:{nonce}:{cnonce}") # type: ignore[reportConstantRedefinition] # RFC 2617 terminology
|
||||
|
||||
if not qop:
|
||||
respdig = KD(HA1, f"{nonce}:{HA2}")
|
||||
@@ -233,12 +265,12 @@ class HTTPDigestAuth(AuthBase):
|
||||
|
||||
return f"Digest {base}"
|
||||
|
||||
def handle_redirect(self, r, **kwargs):
|
||||
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, **kwargs):
|
||||
def handle_401(self, r: Response, **kwargs: Any) -> Response:
|
||||
"""
|
||||
Takes the given response and tries digest-auth, if needed.
|
||||
|
||||
@@ -254,7 +286,8 @@ class HTTPDigestAuth(AuthBase):
|
||||
if self._thread_local.pos is not None:
|
||||
# Rewind the file position indicator of the body to where
|
||||
# it was to resend the request.
|
||||
r.request.body.seek(self._thread_local.pos)
|
||||
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:
|
||||
@@ -267,12 +300,15 @@ class HTTPDigestAuth(AuthBase):
|
||||
r.content
|
||||
r.close()
|
||||
prep = r.request.copy()
|
||||
extract_cookies_to_jar(prep._cookies, r.request, r.raw)
|
||||
prep.prepare_cookies(prep._cookies)
|
||||
cookie_jar = cast("CookieJar", prep._cookies)
|
||||
extract_cookies_to_jar(cookie_jar, r.request, r.raw)
|
||||
prep.prepare_cookies(cookie_jar)
|
||||
|
||||
prep.headers["Authorization"] = self.build_digest_header(
|
||||
prep.method, prep.url
|
||||
_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
|
||||
@@ -282,15 +318,19 @@ class HTTPDigestAuth(AuthBase):
|
||||
self._thread_local.num_401_calls = 1
|
||||
return r
|
||||
|
||||
def __call__(self, 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:
|
||||
r.headers["Authorization"] = self.build_digest_header(r.method, r.url)
|
||||
try:
|
||||
self._thread_local.pos = r.body.tell()
|
||||
except AttributeError:
|
||||
_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
|
||||
@@ -302,7 +342,7 @@ class HTTPDigestAuth(AuthBase):
|
||||
|
||||
return r
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.username == getattr(other, "username", None),
|
||||
@@ -310,5 +350,5 @@ class HTTPDigestAuth(AuthBase):
|
||||
]
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
return not self == other
|
||||
|
||||
@@ -11,6 +11,7 @@ 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__":
|
||||
|
||||
@@ -7,8 +7,14 @@ 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
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# -------
|
||||
# urllib3
|
||||
@@ -27,7 +33,7 @@ except (TypeError, AttributeError):
|
||||
# -------------------
|
||||
|
||||
|
||||
def _resolve_char_detection():
|
||||
def _resolve_char_detection() -> ModuleType | None:
|
||||
"""Find supported character detection libraries."""
|
||||
chardet = None
|
||||
for lib in ("chardet", "charset_normalizer"):
|
||||
@@ -39,7 +45,10 @@ def _resolve_char_detection():
|
||||
return chardet
|
||||
|
||||
|
||||
chardet = _resolve_char_detection()
|
||||
if TYPE_CHECKING:
|
||||
import chardet
|
||||
else:
|
||||
chardet = _resolve_char_detection()
|
||||
|
||||
# -------
|
||||
# Pythons
|
||||
@@ -57,14 +66,14 @@ is_py3 = _ver[0] == 3
|
||||
# json/simplejson module import resolution
|
||||
has_simplejson = False
|
||||
try:
|
||||
import simplejson as json
|
||||
import simplejson as json # type: ignore[import-not-found]
|
||||
|
||||
has_simplejson = True
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
if has_simplejson:
|
||||
from simplejson import JSONDecodeError
|
||||
from simplejson import JSONDecodeError # type: ignore[import-not-found]
|
||||
else:
|
||||
from json import JSONDecodeError
|
||||
|
||||
@@ -95,7 +104,7 @@ from urllib.request import (
|
||||
getproxies_environment,
|
||||
parse_http_list,
|
||||
proxy_bypass,
|
||||
proxy_bypass_environment,
|
||||
proxy_bypass_environment, # type: ignore[attr-defined] # https://github.com/python/cpython/issues/145331
|
||||
)
|
||||
|
||||
builtin_str = str
|
||||
|
||||
@@ -7,21 +7,29 @@ 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 .compat import Morsel, MutableMapping, cookielib, urlparse, urlunparse
|
||||
from ._types import is_prepared as _is_prepared
|
||||
from .compat import Morsel, cookielib, urlparse, urlunparse
|
||||
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
import dummy_threading as threading
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import SupportsKeysAndGetItem
|
||||
|
||||
from .models import PreparedRequest
|
||||
|
||||
import threading
|
||||
|
||||
|
||||
class MockRequest:
|
||||
"""Wraps a `requests.Request` to mimic a `urllib2.Request`.
|
||||
"""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
|
||||
@@ -32,21 +40,24 @@ class MockRequest:
|
||||
probably want `get_cookie_header`, defined below.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
type: str
|
||||
|
||||
def __init__(self, request: PreparedRequest) -> None:
|
||||
assert _is_prepared(request)
|
||||
self._r = request
|
||||
self._new_headers = {}
|
||||
self._new_headers: dict[str, str] = {}
|
||||
self.type = urlparse(self._r.url).scheme
|
||||
|
||||
def get_type(self):
|
||||
def get_type(self) -> str:
|
||||
return self.type
|
||||
|
||||
def get_host(self):
|
||||
def get_host(self) -> str:
|
||||
return urlparse(self._r.url).netloc
|
||||
|
||||
def get_origin_req_host(self):
|
||||
def get_origin_req_host(self) -> str:
|
||||
return self.get_host()
|
||||
|
||||
def get_full_url(self):
|
||||
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"):
|
||||
@@ -66,37 +77,37 @@ class MockRequest:
|
||||
]
|
||||
)
|
||||
|
||||
def is_unverifiable(self):
|
||||
def is_unverifiable(self) -> bool:
|
||||
return True
|
||||
|
||||
def has_header(self, name):
|
||||
def has_header(self, name: str) -> bool:
|
||||
return name in self._r.headers or name in self._new_headers
|
||||
|
||||
def get_header(self, name, default=None):
|
||||
return self._r.headers.get(name, self._new_headers.get(name, default))
|
||||
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, val):
|
||||
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, value):
|
||||
def add_unredirected_header(self, name: str, value: str) -> None:
|
||||
self._new_headers[name] = value
|
||||
|
||||
def get_new_headers(self):
|
||||
def get_new_headers(self) -> dict[str, str]:
|
||||
return self._new_headers
|
||||
|
||||
@property
|
||||
def unverifiable(self):
|
||||
def unverifiable(self) -> bool:
|
||||
return self.is_unverifiable()
|
||||
|
||||
@property
|
||||
def origin_req_host(self):
|
||||
def origin_req_host(self) -> str:
|
||||
return self.get_origin_req_host()
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
def host(self) -> str:
|
||||
return self.get_host()
|
||||
|
||||
|
||||
@@ -107,21 +118,23 @@ class MockResponse:
|
||||
the way `http.cookiejar` expects to see them.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
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):
|
||||
def info(self) -> Any:
|
||||
return self._headers
|
||||
|
||||
def getheaders(self, name):
|
||||
def getheaders(self, name: str) -> Any:
|
||||
self._headers.getheaders(name)
|
||||
|
||||
|
||||
def extract_cookies_to_jar(jar, request, response):
|
||||
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)
|
||||
@@ -134,26 +147,28 @@ def extract_cookies_to_jar(jar, request, response):
|
||||
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)
|
||||
jar.extract_cookies(res, req) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def get_cookie_header(jar, request):
|
||||
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)
|
||||
jar.add_cookie_header(r) # type: ignore[arg-type]
|
||||
return r.get_new_headers().get("Cookie")
|
||||
|
||||
|
||||
def remove_cookie_by_name(cookiejar, name, domain=None, path=None):
|
||||
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 = []
|
||||
clearables: list[tuple[str, str, str]] = []
|
||||
for cookie in cookiejar:
|
||||
if cookie.name != name:
|
||||
continue
|
||||
@@ -173,7 +188,7 @@ class CookieConflictError(RuntimeError):
|
||||
"""
|
||||
|
||||
|
||||
class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc]
|
||||
"""Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict
|
||||
interface.
|
||||
|
||||
@@ -191,7 +206,15 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
.. warning:: dictionary operations that are normally O(1) may be O(n).
|
||||
"""
|
||||
|
||||
def get(self, name, default=None, domain=None, path=None):
|
||||
_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.
|
||||
@@ -203,7 +226,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def set(self, name, value, **kwargs):
|
||||
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.
|
||||
@@ -222,7 +247,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
self.set_cookie(c)
|
||||
return c
|
||||
|
||||
def iterkeys(self):
|
||||
def iterkeys(self) -> Iterator[str]:
|
||||
"""Dict-like iterkeys() that returns an iterator of names of cookies
|
||||
from the jar.
|
||||
|
||||
@@ -231,7 +256,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
for cookie in iter(self):
|
||||
yield cookie.name
|
||||
|
||||
def keys(self):
|
||||
def keys(self) -> list[str]: # type: ignore[override]
|
||||
"""Dict-like keys() that returns a list of names of cookies from the
|
||||
jar.
|
||||
|
||||
@@ -239,7 +264,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""
|
||||
return list(self.iterkeys())
|
||||
|
||||
def itervalues(self):
|
||||
def itervalues(self) -> Iterator[str | None]:
|
||||
"""Dict-like itervalues() that returns an iterator of values of cookies
|
||||
from the jar.
|
||||
|
||||
@@ -248,7 +273,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
for cookie in iter(self):
|
||||
yield cookie.value
|
||||
|
||||
def values(self):
|
||||
def values(self) -> list[str | None]: # type: ignore[override]
|
||||
"""Dict-like values() that returns a list of values of cookies from the
|
||||
jar.
|
||||
|
||||
@@ -256,7 +281,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""
|
||||
return list(self.itervalues())
|
||||
|
||||
def iteritems(self):
|
||||
def iteritems(self) -> Iterator[tuple[str, str | None]]:
|
||||
"""Dict-like iteritems() that returns an iterator of name-value tuples
|
||||
from the jar.
|
||||
|
||||
@@ -265,7 +290,7 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
for cookie in iter(self):
|
||||
yield cookie.name, cookie.value
|
||||
|
||||
def items(self):
|
||||
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.
|
||||
@@ -274,43 +299,45 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""
|
||||
return list(self.iteritems())
|
||||
|
||||
def list_domains(self):
|
||||
def list_domains(self) -> list[str]:
|
||||
"""Utility method to list all the domains in the jar."""
|
||||
domains = []
|
||||
domains: list[str] = []
|
||||
for cookie in iter(self):
|
||||
if cookie.domain not in domains:
|
||||
domains.append(cookie.domain)
|
||||
return domains
|
||||
|
||||
def list_paths(self):
|
||||
def list_paths(self) -> list[str]:
|
||||
"""Utility method to list all the paths in the jar."""
|
||||
paths = []
|
||||
paths: list[str] = []
|
||||
for cookie in iter(self):
|
||||
if cookie.path not in paths:
|
||||
paths.append(cookie.path)
|
||||
return paths
|
||||
|
||||
def multiple_domains(self):
|
||||
def multiple_domains(self) -> bool:
|
||||
"""Returns True if there are multiple domains in the jar.
|
||||
Returns False otherwise.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
domains = []
|
||||
domains: list[str] = []
|
||||
for cookie in iter(self):
|
||||
if cookie.domain is not None and cookie.domain in domains:
|
||||
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=None, path=None):
|
||||
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 = {}
|
||||
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
|
||||
@@ -318,13 +345,17 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
dictionary[cookie.name] = cookie.value
|
||||
return dictionary
|
||||
|
||||
def __contains__(self, name):
|
||||
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):
|
||||
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.
|
||||
@@ -333,29 +364,33 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""
|
||||
return self._find_no_duplicates(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
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):
|
||||
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, *args, **kwargs):
|
||||
def set_cookie(self, cookie: Cookie, *args: Any, **kwargs: Any) -> None:
|
||||
if (
|
||||
hasattr(cookie.value, "startswith")
|
||||
and cookie.value.startswith('"')
|
||||
and cookie.value.endswith('"')
|
||||
(value := cookie.value) is not None
|
||||
and value.startswith('"')
|
||||
and value.endswith('"')
|
||||
):
|
||||
cookie.value = cookie.value.replace('\\"', "")
|
||||
cookie.value = value.replace('\\"', "")
|
||||
return super().set_cookie(cookie, *args, **kwargs)
|
||||
|
||||
def update(self, other):
|
||||
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:
|
||||
@@ -363,7 +398,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
else:
|
||||
super().update(other)
|
||||
|
||||
def _find(self, name, domain=None, path=None):
|
||||
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.
|
||||
@@ -383,7 +420,9 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
|
||||
raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
|
||||
|
||||
def _find_no_duplicates(self, name, domain=None, path=None):
|
||||
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.
|
||||
|
||||
@@ -408,42 +447,42 @@ class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
# we will eventually return this as long as no cookie conflict
|
||||
toReturn = cookie.value
|
||||
|
||||
if toReturn:
|
||||
if toReturn is not None:
|
||||
return toReturn
|
||||
raise KeyError(f"name={name!r}, domain={domain!r}, path={path!r}")
|
||||
|
||||
def __getstate__(self):
|
||||
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):
|
||||
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):
|
||||
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):
|
||||
def get_policy(self) -> CookiePolicy:
|
||||
"""Return the CookiePolicy instance used."""
|
||||
return self._policy
|
||||
|
||||
|
||||
def _copy_cookie_jar(jar):
|
||||
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 hasattr(jar, "copy"):
|
||||
if copy_method := getattr(jar, "copy", None):
|
||||
# We're dealing with an instance of RequestsCookieJar
|
||||
return jar.copy()
|
||||
return copy_method()
|
||||
# We're dealing with a generic CookieJar instance
|
||||
new_jar = copy.copy(jar)
|
||||
new_jar.clear()
|
||||
@@ -452,13 +491,13 @@ def _copy_cookie_jar(jar):
|
||||
return new_jar
|
||||
|
||||
|
||||
def create_cookie(name, value, **kwargs):
|
||||
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 = {
|
||||
result: dict[str, Any] = {
|
||||
"version": 0,
|
||||
"name": name,
|
||||
"value": value,
|
||||
@@ -489,10 +528,10 @@ def create_cookie(name, value, **kwargs):
|
||||
return cookielib.Cookie(**result)
|
||||
|
||||
|
||||
def morsel_to_cookie(morsel):
|
||||
def morsel_to_cookie(morsel: Morsel[Any]) -> Cookie:
|
||||
"""Convert a Morsel object into a Cookie containing the one k/v pair."""
|
||||
|
||||
expires = None
|
||||
expires: int | None = None
|
||||
if morsel["max-age"]:
|
||||
try:
|
||||
expires = int(time.time() + int(morsel["max-age"]))
|
||||
@@ -518,7 +557,30 @@ def morsel_to_cookie(morsel):
|
||||
)
|
||||
|
||||
|
||||
def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
|
||||
_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.
|
||||
@@ -539,22 +601,24 @@ def cookiejar_from_dict(cookie_dict, cookiejar=None, overwrite=True):
|
||||
return cookiejar
|
||||
|
||||
|
||||
def merge_cookies(cookiejar, cookies):
|
||||
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):
|
||||
if not isinstance(cookiejar, cookielib.CookieJar): # 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):
|
||||
try:
|
||||
cookiejar.update(cookies)
|
||||
except AttributeError:
|
||||
if update_method := getattr(cookiejar, "update", None):
|
||||
update_method(cookies)
|
||||
else:
|
||||
for cookie_in_jar in cookies:
|
||||
cookiejar.set_cookie(cookie_in_jar)
|
||||
|
||||
|
||||
@@ -4,23 +4,34 @@ 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.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
response: Response | None
|
||||
request: Request | PreparedRequest | None
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize RequestException with `request` and `response` objects."""
|
||||
response = kwargs.pop("response", None)
|
||||
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 = self.response.request
|
||||
self.request = response.request
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -31,7 +42,7 @@ class InvalidJSONError(RequestException):
|
||||
class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
|
||||
"""Couldn't decode the text into json"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
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
|
||||
@@ -41,7 +52,7 @@ class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
|
||||
CompatJSONDecodeError.__init__(self, *args)
|
||||
InvalidJSONError.__init__(self, *self.args, **kwargs)
|
||||
|
||||
def __reduce__(self):
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import idna
|
||||
import urllib3
|
||||
@@ -47,14 +48,11 @@ def _implementation():
|
||||
if implementation == "CPython":
|
||||
implementation_version = platform.python_version()
|
||||
elif implementation == "PyPy":
|
||||
implementation_version = "{}.{}.{}".format(
|
||||
sys.pypy_version_info.major,
|
||||
sys.pypy_version_info.minor,
|
||||
sys.pypy_version_info.micro,
|
||||
)
|
||||
if sys.pypy_version_info.releaselevel != "final":
|
||||
pypy = sys.pypy_version_info # type: ignore[attr-defined]
|
||||
implementation_version = f"{pypy.major}.{pypy.minor}.{pypy.micro}" # pyright: ignore[reportUnknownMemberType]
|
||||
if sys.pypy_version_info.releaselevel != "final": # type: ignore[attr-defined]
|
||||
implementation_version = "".join(
|
||||
[implementation_version, sys.pypy_version_info.releaselevel]
|
||||
[implementation_version, sys.pypy_version_info.releaselevel] # type: ignore[attr-defined]
|
||||
)
|
||||
elif implementation == "Jython":
|
||||
implementation_version = platform.python_version() # Complete Guess
|
||||
@@ -66,7 +64,7 @@ def _implementation():
|
||||
return {"name": implementation, "version": implementation_version}
|
||||
|
||||
|
||||
def info():
|
||||
def info() -> dict[str, Any]:
|
||||
"""Generate information for a bug report."""
|
||||
try:
|
||||
platform_info = {
|
||||
@@ -82,13 +80,13 @@ def info():
|
||||
implementation_info = _implementation()
|
||||
urllib3_info = {"version": urllib3.__version__}
|
||||
charset_normalizer_info = {"version": None}
|
||||
chardet_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 = {
|
||||
pyopenssl_info: dict[str, str | None] = {
|
||||
"version": None,
|
||||
"openssl_version": "",
|
||||
}
|
||||
@@ -105,7 +103,7 @@ def info():
|
||||
}
|
||||
|
||||
system_ssl = ssl.OPENSSL_VERSION_NUMBER
|
||||
system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""}
|
||||
system_ssl_info = {"version": f"{system_ssl:x}" if system_ssl is not None else ""} # type: ignore[reportUnnecessaryComparison]
|
||||
|
||||
return {
|
||||
"platform": platform_info,
|
||||
|
||||
@@ -9,24 +9,39 @@ Available hooks:
|
||||
``response``:
|
||||
The response generated from a Request.
|
||||
"""
|
||||
HOOKS = ["response"]
|
||||
|
||||
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():
|
||||
def default_hooks() -> dict[str, list[_t.HookType]]:
|
||||
return {event: [] for event in HOOKS}
|
||||
|
||||
|
||||
# TODO: response is the only one
|
||||
|
||||
|
||||
def dispatch_hook(key, hooks, hook_data, **kwargs):
|
||||
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 = hooks or {}
|
||||
hooks = hooks.get(key)
|
||||
if hooks:
|
||||
if hasattr(hooks, "__call__"):
|
||||
hooks = [hooks]
|
||||
for hook in hooks:
|
||||
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
|
||||
|
||||
@@ -5,13 +5,24 @@ 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
|
||||
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,
|
||||
@@ -24,20 +35,25 @@ from urllib3.fields import RequestField
|
||||
from urllib3.filepost import encode_multipart_formdata
|
||||
from urllib3.util import parse_url
|
||||
|
||||
from . import _types as _t
|
||||
from ._internal_utils import to_native_string, unicode_is_ascii
|
||||
from .auth import HTTPBasicAuth
|
||||
from .compat import (
|
||||
Callable,
|
||||
JSONDecodeError,
|
||||
Mapping,
|
||||
basestring,
|
||||
builtin_str,
|
||||
chardet,
|
||||
cookielib,
|
||||
urlencode,
|
||||
urlsplit,
|
||||
urlunparse,
|
||||
)
|
||||
from .compat import json as complexjson
|
||||
from .compat import urlencode, urlsplit, urlunparse
|
||||
from .cookies import _copy_cookie_jar, cookiejar_from_dict, get_cookie_header
|
||||
from .cookies import (
|
||||
_copy_cookie_jar,
|
||||
cookiejar_from_dict,
|
||||
get_cookie_header,
|
||||
)
|
||||
from .exceptions import (
|
||||
ChunkedEncodingError,
|
||||
ConnectionError,
|
||||
@@ -45,11 +61,11 @@ from .exceptions import (
|
||||
HTTPError,
|
||||
InvalidJSONError,
|
||||
InvalidURL,
|
||||
MissingSchema,
|
||||
StreamConsumedError,
|
||||
)
|
||||
from .exceptions import JSONDecodeError as RequestsJSONDecodeError
|
||||
from .exceptions import MissingSchema
|
||||
from .exceptions import SSLError as RequestsSSLError
|
||||
from .exceptions import StreamConsumedError
|
||||
from .hooks import default_hooks
|
||||
from .status_codes import codes
|
||||
from .structures import CaseInsensitiveDict
|
||||
@@ -66,9 +82,17 @@ from .utils import (
|
||||
to_key_val_list,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from http.cookiejar import CookieJar
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
from .adapters import HTTPAdapter
|
||||
from .cookies import RequestsCookieJar
|
||||
|
||||
#: The set of HTTP status codes that indicate an automatically
|
||||
#: processable redirect.
|
||||
REDIRECT_STATI = (
|
||||
REDIRECT_STATI: Final[tuple[int, ...]] = ( # type: ignore[assignment]
|
||||
codes.moved, # 301
|
||||
codes.found, # 302
|
||||
codes.other, # 303
|
||||
@@ -76,19 +100,21 @@ REDIRECT_STATI = (
|
||||
codes.permanent_redirect, # 308
|
||||
)
|
||||
|
||||
DEFAULT_REDIRECT_LIMIT = 30
|
||||
CONTENT_CHUNK_SIZE = 10 * 1024
|
||||
ITER_CHUNK_SIZE = 512
|
||||
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):
|
||||
def path_url(self) -> str:
|
||||
"""Build the path URL to use."""
|
||||
|
||||
url = []
|
||||
url: list[str] = []
|
||||
|
||||
p = urlsplit(self.url)
|
||||
p = urlsplit(cast(str, self.url))
|
||||
|
||||
path = p.path
|
||||
if not path:
|
||||
@@ -103,8 +129,28 @@ class RequestEncodingMixin:
|
||||
|
||||
return "".join(url)
|
||||
|
||||
@overload
|
||||
@staticmethod
|
||||
def _encode_params(data):
|
||||
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
|
||||
@@ -114,10 +160,10 @@ class RequestEncodingMixin:
|
||||
|
||||
if isinstance(data, (str, bytes)):
|
||||
return data
|
||||
elif hasattr(data, "read"):
|
||||
elif _t.has_read(data):
|
||||
return data
|
||||
elif hasattr(data, "__iter__"):
|
||||
result = []
|
||||
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]
|
||||
@@ -131,10 +177,12 @@ class RequestEncodingMixin:
|
||||
)
|
||||
return urlencode(result, doseq=True)
|
||||
else:
|
||||
return data
|
||||
return data # type: ignore[return-value] # unreachable for valid _t.DataType
|
||||
|
||||
@staticmethod
|
||||
def _encode_files(files, data):
|
||||
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
|
||||
@@ -148,7 +196,7 @@ class RequestEncodingMixin:
|
||||
elif isinstance(data, basestring):
|
||||
raise ValueError("Data must not be a string.")
|
||||
|
||||
new_fields = []
|
||||
new_fields: list[RequestField | tuple[str, bytes]] = []
|
||||
fields = to_key_val_list(data or {})
|
||||
files = to_key_val_list(files or {})
|
||||
|
||||
@@ -187,9 +235,9 @@ class RequestEncodingMixin:
|
||||
|
||||
if isinstance(fp, (str, bytes, bytearray)):
|
||||
fdata = fp
|
||||
elif hasattr(fp, "read"):
|
||||
elif _t.has_read(fp):
|
||||
fdata = fp.read()
|
||||
elif fp is None:
|
||||
elif fp is None: # defensive check for untyped callers
|
||||
continue
|
||||
else:
|
||||
fdata = fp
|
||||
@@ -204,7 +252,11 @@ class RequestEncodingMixin:
|
||||
|
||||
|
||||
class RequestHooksMixin:
|
||||
def register_hook(self, event, hook):
|
||||
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:
|
||||
@@ -213,9 +265,11 @@ class RequestHooksMixin:
|
||||
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))
|
||||
self.hooks[event].extend(
|
||||
h for h in hook if isinstance(h, Callable)
|
||||
) # defensive runtime filter
|
||||
|
||||
def deregister_hook(self, event, hook):
|
||||
def deregister_hook(self, event: str, hook: _t.HookType) -> bool:
|
||||
"""Deregister a previously registered hook.
|
||||
Returns True if the hook existed, False if not.
|
||||
"""
|
||||
@@ -255,19 +309,30 @@ class Request(RequestHooksMixin):
|
||||
<PreparedRequest [GET]>
|
||||
"""
|
||||
|
||||
hooks: dict[str, list[_t.HookType]]
|
||||
method: str | None
|
||||
url: _t.UriType | None
|
||||
headers: Mapping[str, str | bytes]
|
||||
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=None,
|
||||
url=None,
|
||||
headers=None,
|
||||
files=None,
|
||||
data=None,
|
||||
params=None,
|
||||
auth=None,
|
||||
cookies=None,
|
||||
hooks=None,
|
||||
json=None,
|
||||
):
|
||||
method: str | None = None,
|
||||
url: _t.UriType | None = None,
|
||||
headers: _t.HeadersType = 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
|
||||
@@ -289,10 +354,10 @@ class Request(RequestHooksMixin):
|
||||
self.auth = auth
|
||||
self.cookies = cookies
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Request [{self.method}]>"
|
||||
|
||||
def prepare(self):
|
||||
def prepare(self) -> PreparedRequest:
|
||||
"""Constructs a :class:`PreparedRequest <PreparedRequest>` for transmission and returns it."""
|
||||
p = PreparedRequest()
|
||||
p.prepare(
|
||||
@@ -331,13 +396,21 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
<Response [200]>
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
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
|
||||
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
|
||||
@@ -350,19 +423,20 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
|
||||
def prepare(
|
||||
self,
|
||||
method=None,
|
||||
url=None,
|
||||
headers=None,
|
||||
files=None,
|
||||
data=None,
|
||||
params=None,
|
||||
auth=None,
|
||||
cookies=None,
|
||||
hooks=None,
|
||||
json=None,
|
||||
):
|
||||
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)
|
||||
@@ -376,28 +450,28 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
# This MUST go after prepare_auth. Authenticators could add a hook
|
||||
self.prepare_hooks(hooks)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<PreparedRequest [{self.method}]>"
|
||||
|
||||
def copy(self):
|
||||
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
|
||||
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):
|
||||
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):
|
||||
def _get_idna_encoded_host(host: str) -> str:
|
||||
import idna
|
||||
|
||||
try:
|
||||
@@ -406,7 +480,11 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
raise UnicodeError
|
||||
return host
|
||||
|
||||
def prepare_url(self, url, params):
|
||||
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
|
||||
@@ -470,17 +548,21 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
if isinstance(params, (str, bytes)):
|
||||
params = to_native_string(params)
|
||||
|
||||
enc_params = self._encode_params(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, None, query, fragment]))
|
||||
url = requote_uri(urlunparse((scheme, netloc, path, "", query, fragment)))
|
||||
self.url = url
|
||||
|
||||
def prepare_headers(self, headers):
|
||||
def prepare_headers(self, headers: Mapping[str, str | bytes] | None) -> None:
|
||||
"""Prepares the given HTTP headers."""
|
||||
|
||||
self.headers = CaseInsensitiveDict()
|
||||
@@ -491,7 +573,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
name, value = header
|
||||
self.headers[to_native_string(name)] = value
|
||||
|
||||
def prepare_body(self, data, files, json=None):
|
||||
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.
|
||||
@@ -514,14 +598,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
if not isinstance(body, bytes):
|
||||
body = body.encode("utf-8")
|
||||
|
||||
is_stream = all(
|
||||
[
|
||||
hasattr(data, "__iter__"),
|
||||
not isinstance(data, (basestring, list, tuple, Mapping)),
|
||||
]
|
||||
)
|
||||
|
||||
if is_stream:
|
||||
# data that proxies attributes to underlying objects needs hasattr
|
||||
is_iterable = isinstance(data, Iterable) or hasattr(data, "__iter__")
|
||||
if is_iterable and not isinstance(data, (str, bytes, list, tuple, Mapping)):
|
||||
try:
|
||||
length = super_len(data)
|
||||
except (TypeError, AttributeError, UnsupportedOperation):
|
||||
@@ -534,7 +613,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
# This will allow us to rewind a file in the event
|
||||
# of a redirect.
|
||||
try:
|
||||
self._body_position = body.tell()
|
||||
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
|
||||
@@ -550,13 +629,16 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
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, data)
|
||||
(body, content_type) = self._encode_files(files, raw_data)
|
||||
else:
|
||||
if data:
|
||||
body = self._encode_params(data)
|
||||
if isinstance(data, basestring) or hasattr(data, "read"):
|
||||
if raw_data:
|
||||
body = self._encode_params(raw_data)
|
||||
if isinstance(data, basestring) or _t.has_read(data):
|
||||
content_type = None
|
||||
else:
|
||||
content_type = "application/x-www-form-urlencoded"
|
||||
@@ -567,9 +649,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
if content_type and ("content-type" not in self.headers):
|
||||
self.headers["Content-Type"] = content_type
|
||||
|
||||
self.body = body
|
||||
self.body = body # type: ignore[assignment] # body transforms from DataType to BodyType
|
||||
|
||||
def prepare_content_length(self, body):
|
||||
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)
|
||||
@@ -585,21 +667,28 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
# but don't provide one. (i.e. not GET or HEAD)
|
||||
self.headers["Content-Length"] = "0"
|
||||
|
||||
def prepare_auth(self, auth, url=""):
|
||||
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(self.url)
|
||||
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:
|
||||
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 = HTTPBasicAuth(*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(self)
|
||||
r = auth_handler(self)
|
||||
|
||||
# Update self to reflect the auth changes.
|
||||
self.__dict__.update(r.__dict__)
|
||||
@@ -607,7 +696,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
# Recompute Content-Length
|
||||
self.prepare_content_length(self.body)
|
||||
|
||||
def prepare_cookies(self, cookies):
|
||||
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
|
||||
@@ -623,16 +714,17 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
|
||||
else:
|
||||
self._cookies = cookiejar_from_dict(cookies)
|
||||
|
||||
cookie_header = get_cookie_header(self._cookies, self)
|
||||
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):
|
||||
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 []
|
||||
hooks = hooks or {}
|
||||
for event in hooks:
|
||||
self.register_hook(event, hooks[event])
|
||||
|
||||
@@ -642,7 +734,22 @@ class Response:
|
||||
server's response to an HTTP request.
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
_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
|
||||
cookies: RequestsCookieJar
|
||||
elapsed: datetime.timedelta
|
||||
request: PreparedRequest
|
||||
connection: HTTPAdapter
|
||||
|
||||
__attrs__: list[str] = [
|
||||
"_content",
|
||||
"status_code",
|
||||
"headers",
|
||||
@@ -655,13 +762,13 @@ class Response:
|
||||
"request",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
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
|
||||
self.status_code = None # type: ignore[assignment]
|
||||
|
||||
#: Case-insensitive Dictionary of Response Headers.
|
||||
#: For example, ``headers['content-encoding']`` will return the
|
||||
@@ -674,7 +781,7 @@ class Response:
|
||||
self.raw = None
|
||||
|
||||
#: Final URL location of Response.
|
||||
self.url = None
|
||||
self.url = None # type: ignore[assignment]
|
||||
|
||||
#: Encoding to decode with when accessing r.text.
|
||||
self.encoding = None
|
||||
@@ -685,7 +792,7 @@ class Response:
|
||||
self.history = []
|
||||
|
||||
#: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK".
|
||||
self.reason = None
|
||||
self.reason = None # type: ignore[assignment]
|
||||
|
||||
#: A CookieJar of Cookies the server sent back.
|
||||
self.cookies = cookiejar_from_dict({})
|
||||
@@ -700,15 +807,15 @@ class Response:
|
||||
|
||||
#: The :class:`PreparedRequest <PreparedRequest>` object to which this
|
||||
#: is a response.
|
||||
self.request = None
|
||||
self.request = None # type: ignore[assignment]
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.close()
|
||||
|
||||
def __getstate__(self):
|
||||
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:
|
||||
@@ -716,7 +823,7 @@ class Response:
|
||||
|
||||
return {attr: getattr(self, attr, None) for attr in self.__attrs__}
|
||||
|
||||
def __setstate__(self, state):
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
for name, value in state.items():
|
||||
setattr(self, name, value)
|
||||
|
||||
@@ -724,10 +831,10 @@ class Response:
|
||||
setattr(self, "_content_consumed", True)
|
||||
setattr(self, "raw", None)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Response [{self.status_code}]>"
|
||||
|
||||
def __bool__(self):
|
||||
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
|
||||
@@ -737,7 +844,7 @@ class Response:
|
||||
"""
|
||||
return self.ok
|
||||
|
||||
def __nonzero__(self):
|
||||
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
|
||||
@@ -747,12 +854,12 @@ class Response:
|
||||
"""
|
||||
return self.ok
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[bytes]:
|
||||
"""Allows you to use a response as an iterator."""
|
||||
return self.iter_content(128)
|
||||
|
||||
@property
|
||||
def ok(self):
|
||||
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
|
||||
@@ -767,14 +874,14 @@ class Response:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_redirect(self):
|
||||
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):
|
||||
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,
|
||||
@@ -782,12 +889,12 @@ class Response:
|
||||
)
|
||||
|
||||
@property
|
||||
def next(self):
|
||||
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):
|
||||
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"]
|
||||
@@ -796,7 +903,17 @@ class Response:
|
||||
# to a standard Python utf-8 str.
|
||||
return "utf-8"
|
||||
|
||||
def iter_content(self, chunk_size=1, decode_unicode=False):
|
||||
@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
|
||||
@@ -809,11 +926,13 @@ class Response:
|
||||
chunks are received. If stream=False, data is returned as
|
||||
a single chunk.
|
||||
|
||||
If decode_unicode is True, content will be decoded using the best
|
||||
available encoding based on the response.
|
||||
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():
|
||||
def generate() -> Generator[bytes, None, None]:
|
||||
# Special case for urllib3.
|
||||
if hasattr(self.raw, "stream"):
|
||||
try:
|
||||
@@ -838,42 +957,67 @@ class Response:
|
||||
|
||||
if self._content_consumed and isinstance(self._content, bool):
|
||||
raise StreamConsumedError()
|
||||
elif chunk_size is not None and not isinstance(chunk_size, int):
|
||||
elif chunk_size is not None and not isinstance(
|
||||
chunk_size, int
|
||||
): # runtime guard for untyped callers
|
||||
raise TypeError(
|
||||
f"chunk_size must be an int, it is instead a {type(chunk_size)}."
|
||||
)
|
||||
# simulate reading small chunks of the content
|
||||
reused_chunks = iter_slices(self._content, chunk_size)
|
||||
|
||||
stream_chunks = generate()
|
||||
|
||||
chunks = reused_chunks if self._content_consumed else stream_chunks
|
||||
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=ITER_CHUNK_SIZE, decode_unicode=False, delimiter=None
|
||||
):
|
||||
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 = None
|
||||
pending: str | bytes | None = None
|
||||
|
||||
for chunk in self.iter_content(
|
||||
chunk_size=chunk_size, decode_unicode=decode_unicode
|
||||
):
|
||||
if pending is not None:
|
||||
chunk = pending + chunk
|
||||
# TODO: remove cast after iter_lines rewrite
|
||||
chunk = cast("str | bytes", pending + chunk)
|
||||
|
||||
if delimiter:
|
||||
lines = chunk.split(delimiter)
|
||||
lines = chunk.split(delimiter) # type: ignore[arg-type]
|
||||
else:
|
||||
lines = chunk.splitlines()
|
||||
|
||||
@@ -888,7 +1032,7 @@ class Response:
|
||||
yield pending
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
def content(self) -> bytes:
|
||||
"""Content of the response, in bytes."""
|
||||
|
||||
if self._content is False:
|
||||
@@ -904,10 +1048,10 @@ class Response:
|
||||
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
|
||||
return self._content # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
def text(self) -> str:
|
||||
"""Content of the response, in unicode.
|
||||
|
||||
If Response.encoding is None, encoding will be guessed using
|
||||
@@ -932,7 +1076,7 @@ class Response:
|
||||
|
||||
# Decode unicode from given encoding.
|
||||
try:
|
||||
content = str(self.content, encoding, errors="replace")
|
||||
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.
|
||||
@@ -944,7 +1088,7 @@ class Response:
|
||||
|
||||
return content
|
||||
|
||||
def json(self, **kwargs):
|
||||
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.
|
||||
@@ -980,23 +1124,24 @@ class Response:
|
||||
raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
|
||||
|
||||
@property
|
||||
def links(self):
|
||||
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 = {}
|
||||
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")
|
||||
resolved_links[key] = link
|
||||
if key is not None:
|
||||
resolved_links[key] = link
|
||||
|
||||
return resolved_links
|
||||
|
||||
def raise_for_status(self):
|
||||
def raise_for_status(self) -> None:
|
||||
"""Raises :class:`HTTPError`, if one occurred."""
|
||||
|
||||
http_error_msg = ""
|
||||
@@ -1025,7 +1170,7 @@ class Response:
|
||||
if http_error_msg:
|
||||
raise HTTPError(http_error_msg, response=self)
|
||||
|
||||
def close(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.
|
||||
|
||||
|
||||
0
src/requests/py.typed
Normal file
0
src/requests/py.typed
Normal file
@@ -5,16 +5,22 @@ 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
|
||||
from .compat import Mapping, cookielib, urljoin, urlparse
|
||||
from .compat import cookielib, urljoin, urlparse
|
||||
from .cookies import (
|
||||
RequestsCookieJar,
|
||||
cookiejar_from_dict,
|
||||
@@ -35,6 +41,7 @@ from .models import ( # noqa: F401
|
||||
REDIRECT_STATI,
|
||||
PreparedRequest,
|
||||
Request,
|
||||
Response,
|
||||
)
|
||||
from .status_codes import codes
|
||||
from .structures import CaseInsensitiveDict
|
||||
@@ -47,10 +54,18 @@ from .utils import ( # noqa: F401
|
||||
requote_uri,
|
||||
resolve_proxies,
|
||||
rewind_body,
|
||||
should_bypass_proxies,
|
||||
should_bypass_proxies, # 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
|
||||
@@ -58,7 +73,9 @@ else:
|
||||
preferred_clock = time.time
|
||||
|
||||
|
||||
def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
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`
|
||||
@@ -76,8 +93,8 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
):
|
||||
return request_setting
|
||||
|
||||
merged_setting = dict_class(to_key_val_list(session_setting))
|
||||
merged_setting.update(to_key_val_list(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.
|
||||
@@ -88,7 +105,11 @@ def merge_setting(request_setting, session_setting, dict_class=OrderedDict):
|
||||
return merged_setting
|
||||
|
||||
|
||||
def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
|
||||
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
|
||||
@@ -104,7 +125,13 @@ def merge_hooks(request_hooks, session_hooks, dict_class=OrderedDict):
|
||||
|
||||
|
||||
class SessionRedirectMixin:
|
||||
def get_redirect_target(self, resp):
|
||||
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
|
||||
@@ -124,7 +151,7 @@ class SessionRedirectMixin:
|
||||
return to_native_string(location, "utf8")
|
||||
return None
|
||||
|
||||
def should_strip_auth(self, old_url, new_url):
|
||||
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)
|
||||
@@ -158,19 +185,19 @@ class SessionRedirectMixin:
|
||||
|
||||
def resolve_redirects(
|
||||
self,
|
||||
resp,
|
||||
req,
|
||||
stream=False,
|
||||
timeout=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
proxies=None,
|
||||
yield_requests=False,
|
||||
**adapter_kwargs,
|
||||
):
|
||||
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 = [] # keep track of history
|
||||
hist: list[Response] = [] # keep track of history
|
||||
|
||||
url = self.get_redirect_target(resp)
|
||||
previous_fragment = urlparse(req.url).fragment
|
||||
@@ -178,9 +205,8 @@ class SessionRedirectMixin:
|
||||
prepared_request = req.copy()
|
||||
|
||||
# Update history and keep track of redirects.
|
||||
# resp.history must ignore the original request in this loop
|
||||
resp.history = hist[:]
|
||||
hist.append(resp)
|
||||
resp.history = hist[1:]
|
||||
|
||||
try:
|
||||
resp.content # Consume socket so it can be released
|
||||
@@ -237,9 +263,10 @@ class SessionRedirectMixin:
|
||||
# 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.
|
||||
extract_cookies_to_jar(prepared_request._cookies, req, resp.raw)
|
||||
merge_cookies(prepared_request._cookies, self.cookies)
|
||||
prepared_request.prepare_cookies(prepared_request._cookies)
|
||||
cookie_jar = cast("CookieJar", prepared_request._cookies)
|
||||
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)
|
||||
@@ -260,7 +287,7 @@ class SessionRedirectMixin:
|
||||
req = prepared_request
|
||||
|
||||
if yield_requests:
|
||||
yield req
|
||||
yield req # type: ignore[misc] # Internal use only, returns PreparedRequest
|
||||
else:
|
||||
resp = self.send(
|
||||
req,
|
||||
@@ -279,17 +306,22 @@ class SessionRedirectMixin:
|
||||
url = self.get_redirect_target(resp)
|
||||
yield resp
|
||||
|
||||
def rebuild_auth(self, prepared_request, response):
|
||||
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(
|
||||
response.request.url, 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"]
|
||||
@@ -299,7 +331,11 @@ class SessionRedirectMixin:
|
||||
if new_auth is not None:
|
||||
prepared_request.prepare_auth(new_auth)
|
||||
|
||||
def rebuild_proxies(self, prepared_request, proxies):
|
||||
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
|
||||
@@ -311,6 +347,7 @@ class SessionRedirectMixin:
|
||||
|
||||
: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)
|
||||
@@ -330,7 +367,9 @@ class SessionRedirectMixin:
|
||||
|
||||
return new_proxies
|
||||
|
||||
def rebuild_method(self, prepared_request, response):
|
||||
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.
|
||||
"""
|
||||
@@ -372,7 +411,20 @@ class Session(SessionRedirectMixin):
|
||||
<Response [200]>
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
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",
|
||||
@@ -387,7 +439,7 @@ class Session(SessionRedirectMixin):
|
||||
"max_redirects",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
#: A case-insensitive dictionary of headers to be sent on each
|
||||
#: :class:`Request <Request>` sent from this
|
||||
#: :class:`Session <Session>`.
|
||||
@@ -421,6 +473,8 @@ class Session(SessionRedirectMixin):
|
||||
#: 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
|
||||
@@ -448,13 +502,13 @@ class Session(SessionRedirectMixin):
|
||||
self.mount("https://", HTTPAdapter())
|
||||
self.mount("http://", HTTPAdapter())
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.close()
|
||||
|
||||
def prepare_request(self, request):
|
||||
def prepare_request(self, request: Request) -> PreparedRequest:
|
||||
"""Constructs a :class:`PreparedRequest <PreparedRequest>` for
|
||||
transmission and returns it. The :class:`PreparedRequest` has settings
|
||||
merged from the :class:`Request <Request>` instance and those of the
|
||||
@@ -464,6 +518,9 @@ class Session(SessionRedirectMixin):
|
||||
session's settings.
|
||||
:rtype: requests.PreparedRequest
|
||||
"""
|
||||
url = cast("_t.UriType", request.url)
|
||||
method = cast(str, request.method)
|
||||
|
||||
cookies = request.cookies or {}
|
||||
|
||||
# Bootstrap CookieJar.
|
||||
@@ -478,12 +535,12 @@ class Session(SessionRedirectMixin):
|
||||
# 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(request.url)
|
||||
auth = get_netrc_auth(url)
|
||||
|
||||
p = PreparedRequest()
|
||||
p.prepare(
|
||||
method=request.method.upper(),
|
||||
url=request.url,
|
||||
method=method.upper(),
|
||||
url=url,
|
||||
files=request.files,
|
||||
data=request.data,
|
||||
json=request.json,
|
||||
@@ -499,23 +556,23 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
def request(
|
||||
self,
|
||||
method,
|
||||
url,
|
||||
params=None,
|
||||
data=None,
|
||||
headers=None,
|
||||
cookies=None,
|
||||
files=None,
|
||||
auth=None,
|
||||
timeout=None,
|
||||
allow_redirects=True,
|
||||
proxies=None,
|
||||
hooks=None,
|
||||
stream=None,
|
||||
verify=None,
|
||||
cert=None,
|
||||
json=None,
|
||||
):
|
||||
method: str,
|
||||
url: _t.UriType,
|
||||
params: _t.ParamsType = None,
|
||||
data: _t.DataType = None,
|
||||
headers: _t.HeadersType = 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 <Request>`, prepares it and sends it.
|
||||
Returns :class:`Response <Response>` object.
|
||||
|
||||
@@ -559,6 +616,9 @@ class Session(SessionRedirectMixin):
|
||||
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(),
|
||||
@@ -574,6 +634,8 @@ class Session(SessionRedirectMixin):
|
||||
)
|
||||
prep = self.prepare_request(req)
|
||||
|
||||
assert _is_prepared(prep)
|
||||
|
||||
proxies = proxies or {}
|
||||
|
||||
settings = self.merge_environment_settings(
|
||||
@@ -590,18 +652,25 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return resp
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
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, **kwargs)
|
||||
return self.request("GET", url, params=params, **kwargs)
|
||||
|
||||
def options(self, url, **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.
|
||||
@@ -612,7 +681,7 @@ class Session(SessionRedirectMixin):
|
||||
kwargs.setdefault("allow_redirects", True)
|
||||
return self.request("OPTIONS", url, **kwargs)
|
||||
|
||||
def head(self, 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.
|
||||
@@ -623,7 +692,13 @@ class Session(SessionRedirectMixin):
|
||||
kwargs.setdefault("allow_redirects", False)
|
||||
return self.request("HEAD", url, **kwargs)
|
||||
|
||||
def post(self, url, data=None, json=None, **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.
|
||||
@@ -636,7 +711,9 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return self.request("POST", url, data=data, json=json, **kwargs)
|
||||
|
||||
def put(self, url, data=None, **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.
|
||||
@@ -648,7 +725,9 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return self.request("PUT", url, data=data, **kwargs)
|
||||
|
||||
def patch(self, url, data=None, **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.
|
||||
@@ -660,7 +739,7 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return self.request("PATCH", url, data=data, **kwargs)
|
||||
|
||||
def delete(self, url, **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.
|
||||
@@ -670,7 +749,7 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return self.request("DELETE", url, **kwargs)
|
||||
|
||||
def send(self, request, **kwargs):
|
||||
def send(self, request: PreparedRequest, **kwargs: Any) -> Response:
|
||||
"""Send a given PreparedRequest.
|
||||
|
||||
:rtype: requests.Response
|
||||
@@ -688,6 +767,8 @@ class Session(SessionRedirectMixin):
|
||||
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")
|
||||
@@ -736,7 +817,7 @@ class Session(SessionRedirectMixin):
|
||||
# If redirects aren't being followed, store the response on the Request for Response.next().
|
||||
if not allow_redirects:
|
||||
try:
|
||||
r._next = next(
|
||||
r._next = next( # type: ignore[assignment] # yield_requests=True returns PreparedRequest
|
||||
self.resolve_redirects(r, request, yield_requests=True, **kwargs)
|
||||
)
|
||||
except StopIteration:
|
||||
@@ -747,7 +828,14 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return r
|
||||
|
||||
def merge_environment_settings(self, url, proxies, stream, verify, cert):
|
||||
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.
|
||||
|
||||
@@ -758,8 +846,9 @@ class Session(SessionRedirectMixin):
|
||||
# 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)
|
||||
for k, v in env_proxies.items():
|
||||
proxies.setdefault(k, v)
|
||||
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.
|
||||
@@ -778,7 +867,7 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
return {"proxies": proxies, "stream": stream, "verify": verify, "cert": cert}
|
||||
|
||||
def get_adapter(self, url):
|
||||
def get_adapter(self, url: str) -> BaseAdapter:
|
||||
"""
|
||||
Returns the appropriate connection adapter for the given URL.
|
||||
|
||||
@@ -791,12 +880,12 @@ class Session(SessionRedirectMixin):
|
||||
# Nothing matches :-/
|
||||
raise InvalidSchema(f"No connection adapters were found for {url!r}")
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Closes all adapters and as such the session"""
|
||||
for v in self.adapters.values():
|
||||
v.close()
|
||||
|
||||
def mount(self, prefix, adapter):
|
||||
def mount(self, prefix: str, adapter: BaseAdapter) -> None:
|
||||
"""Registers a connection adapter to a prefix.
|
||||
|
||||
Adapters are sorted in descending order by prefix length.
|
||||
@@ -807,16 +896,16 @@ class Session(SessionRedirectMixin):
|
||||
for key in keys_to_move:
|
||||
self.adapters[key] = self.adapters.pop(key)
|
||||
|
||||
def __getstate__(self):
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = {attr: getattr(self, attr, None) for attr in self.__attrs__}
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
for attr, value in state.items():
|
||||
setattr(self, attr, value)
|
||||
|
||||
|
||||
def session():
|
||||
def session() -> Session:
|
||||
"""
|
||||
Returns a :class:`Session` for context-management.
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ _codes = {
|
||||
511: ("network_authentication_required", "network_auth", "network_authentication"),
|
||||
}
|
||||
|
||||
codes = LookupDict(name="status_codes")
|
||||
codes: LookupDict[int] = LookupDict(name="status_codes")
|
||||
|
||||
|
||||
def _init():
|
||||
@@ -113,7 +113,7 @@ def _init():
|
||||
if not title.startswith(("\\", "/")):
|
||||
setattr(codes, title.upper(), code)
|
||||
|
||||
def doc(code):
|
||||
def doc(code: int) -> str:
|
||||
names = ", ".join(f"``{n}``" for n in _codes[code])
|
||||
return "* %d: %s" % (code, names)
|
||||
|
||||
|
||||
@@ -5,12 +5,19 @@ 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 Mapping, MutableMapping
|
||||
from .compat import MutableMapping
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
_D = TypeVar("_D")
|
||||
|
||||
|
||||
class CaseInsensitiveDict(MutableMapping):
|
||||
class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]):
|
||||
"""A case-insensitive ``dict``-like object.
|
||||
|
||||
Implements all methods and operations of
|
||||
@@ -37,63 +44,87 @@ class CaseInsensitiveDict(MutableMapping):
|
||||
behavior is undefined.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, **kwargs):
|
||||
_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, value):
|
||||
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):
|
||||
def __getitem__(self, key: str) -> _VT:
|
||||
return self._store[key.lower()][1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self._store[key.lower()]
|
||||
|
||||
def __iter__(self):
|
||||
return (casedkey for casedkey, mappedvalue in self._store.values())
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return (casedkey for casedkey, _ in self._store.values())
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._store)
|
||||
|
||||
def lower_items(self):
|
||||
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):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, Mapping):
|
||||
other = CaseInsensitiveDict(other)
|
||||
other_dict: CaseInsensitiveDict[Any] = CaseInsensitiveDict(other) # type: ignore[reportUnknownArgumentType]
|
||||
else:
|
||||
return NotImplemented
|
||||
# Compare insensitively
|
||||
return dict(self.lower_items()) == dict(other.lower_items())
|
||||
return dict(self.lower_items()) == dict(other_dict.lower_items())
|
||||
|
||||
# Copy is required
|
||||
def copy(self):
|
||||
def copy(self) -> CaseInsensitiveDict[_VT]:
|
||||
return CaseInsensitiveDict(self._store.values())
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return str(dict(self.items()))
|
||||
|
||||
|
||||
class LookupDict(dict):
|
||||
class LookupDict(dict[str, _VT]):
|
||||
"""Dictionary lookup object."""
|
||||
|
||||
def __init__(self, name=None):
|
||||
name: Any
|
||||
|
||||
def __init__(self, name: Any = None) -> None:
|
||||
self.name = name
|
||||
super().__init__()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<lookup '{self.name}'>"
|
||||
|
||||
def __getitem__(self, key):
|
||||
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)
|
||||
|
||||
def get(self, key, default=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)
|
||||
|
||||
@@ -6,6 +6,8 @@ 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
|
||||
@@ -18,6 +20,15 @@ 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
|
||||
|
||||
@@ -31,25 +42,23 @@ from ._internal_utils import ( # noqa: F401
|
||||
HEADER_VALIDATORS,
|
||||
to_native_string,
|
||||
)
|
||||
from ._types import SupportsItems as _SupportsItems
|
||||
from .compat import (
|
||||
Mapping,
|
||||
basestring,
|
||||
bytes,
|
||||
getproxies,
|
||||
getproxies_environment,
|
||||
integer_types,
|
||||
is_urllib3_1,
|
||||
)
|
||||
from .compat import parse_http_list as _parse_list_header
|
||||
from .compat import (
|
||||
proxy_bypass,
|
||||
proxy_bypass_environment,
|
||||
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,
|
||||
@@ -59,14 +68,27 @@ from .exceptions import (
|
||||
)
|
||||
from .structures import CaseInsensitiveDict
|
||||
|
||||
NETRC_FILES = (".netrc", "_netrc")
|
||||
if TYPE_CHECKING:
|
||||
from http.cookiejar import CookieJar
|
||||
from io import BufferedWriter
|
||||
|
||||
DEFAULT_CA_BUNDLE_PATH = certs.where()
|
||||
from . import _types as _t
|
||||
from .models import PreparedRequest, Request, Response
|
||||
|
||||
DEFAULT_PORTS = {"http": 80, "https": 443}
|
||||
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 = ", ".join(
|
||||
DEFAULT_ACCEPT_ENCODING: Final = ", ".join(
|
||||
re.split(r",\s*", make_headers(accept_encoding=True)["accept-encoding"])
|
||||
)
|
||||
|
||||
@@ -74,7 +96,7 @@ DEFAULT_ACCEPT_ENCODING = ", ".join(
|
||||
if sys.platform == "win32":
|
||||
# provide a proxy_bypass version on Windows without DNS lookups
|
||||
|
||||
def proxy_bypass_registry(host):
|
||||
def proxy_bypass_registry(host: str) -> bool:
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
@@ -112,7 +134,7 @@ if sys.platform == "win32":
|
||||
return True
|
||||
return False
|
||||
|
||||
def proxy_bypass(host): # noqa
|
||||
def proxy_bypass(host: str) -> bool: # noqa
|
||||
"""Return True, if the host should be bypassed.
|
||||
|
||||
Checks proxy settings gathered from the environment, if specified,
|
||||
@@ -124,16 +146,18 @@ if sys.platform == "win32":
|
||||
return proxy_bypass_registry(host)
|
||||
|
||||
|
||||
def dict_to_sequence(d):
|
||||
def dict_to_sequence(
|
||||
d: _t.SupportsItems[Any, Any] | Iterable[tuple[Any, Any]],
|
||||
) -> Iterable[tuple[Any, Any]]:
|
||||
"""Returns an internal sequence dictionary update."""
|
||||
|
||||
if hasattr(d, "items"):
|
||||
d = d.items()
|
||||
if isinstance(d, _SupportsItems):
|
||||
return d.items()
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def super_len(o):
|
||||
def super_len(o: Any) -> int:
|
||||
total_length = None
|
||||
current_position = 0
|
||||
|
||||
@@ -204,9 +228,14 @@ def super_len(o):
|
||||
return max(0, total_length - current_position)
|
||||
|
||||
|
||||
def get_netrc_auth(url, raise_errors=False):
|
||||
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,)
|
||||
@@ -231,12 +260,15 @@ def get_netrc_auth(url, raise_errors=False):
|
||||
ri = urlparse(url)
|
||||
host = ri.hostname
|
||||
|
||||
if host is None:
|
||||
return
|
||||
|
||||
try:
|
||||
_netrc = netrc(netrc_path).authenticators(host)
|
||||
if _netrc:
|
||||
if _netrc and any(_netrc):
|
||||
# Return with login / password
|
||||
login_i = 0 if _netrc[0] else 1
|
||||
return (_netrc[login_i], _netrc[2])
|
||||
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.
|
||||
@@ -248,14 +280,14 @@ def get_netrc_auth(url, raise_errors=False):
|
||||
pass
|
||||
|
||||
|
||||
def guess_filename(obj):
|
||||
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, basestring) and name[0] != "<" and name[-1] != ">":
|
||||
return os.path.basename(name)
|
||||
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):
|
||||
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.
|
||||
@@ -283,17 +315,18 @@ def extract_zipped_paths(path):
|
||||
return path
|
||||
|
||||
# we have a valid zip archive and a valid member of that archive
|
||||
tmp = tempfile.gettempdir()
|
||||
extracted_path = os.path.join(tmp, member.split("/")[-1])
|
||||
if not os.path.exists(extracted_path):
|
||||
# use read + write to avoid the creating nested folders, we only want the file, avoids mkdir racing condition
|
||||
with atomic_open(extracted_path) as file_handler:
|
||||
file_handler.write(zip_file.read(member))
|
||||
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):
|
||||
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:
|
||||
@@ -305,7 +338,9 @@ def atomic_open(filename):
|
||||
raise
|
||||
|
||||
|
||||
def from_key_val_list(value):
|
||||
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.,
|
||||
@@ -332,7 +367,15 @@ def from_key_val_list(value):
|
||||
return OrderedDict(value)
|
||||
|
||||
|
||||
def to_key_val_list(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.,
|
||||
|
||||
@@ -355,14 +398,14 @@ def to_key_val_list(value):
|
||||
if isinstance(value, (str, bytes, bool, int)):
|
||||
raise ValueError("cannot encode objects that are not 2-tuples")
|
||||
|
||||
if isinstance(value, Mapping):
|
||||
value = value.items()
|
||||
if isinstance(value, _SupportsItems):
|
||||
return list(value.items())
|
||||
|
||||
return list(value)
|
||||
|
||||
|
||||
# From mitsuhiko/werkzeug (used with permission).
|
||||
def parse_list_header(value):
|
||||
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
|
||||
@@ -385,7 +428,7 @@ def parse_list_header(value):
|
||||
:return: :class:`list`
|
||||
:rtype: list
|
||||
"""
|
||||
result = []
|
||||
result: list[str] = []
|
||||
for item in _parse_list_header(value):
|
||||
if item[:1] == item[-1:] == '"':
|
||||
item = unquote_header_value(item[1:-1])
|
||||
@@ -394,7 +437,7 @@ def parse_list_header(value):
|
||||
|
||||
|
||||
# From mitsuhiko/werkzeug (used with permission).
|
||||
def parse_dict_header(value):
|
||||
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:
|
||||
|
||||
@@ -416,7 +459,7 @@ def parse_dict_header(value):
|
||||
:return: :class:`dict`
|
||||
:rtype: dict
|
||||
"""
|
||||
result = {}
|
||||
result: dict[str, str | None] = {}
|
||||
for item in _parse_list_header(value):
|
||||
if "=" not in item:
|
||||
result[item] = None
|
||||
@@ -429,7 +472,7 @@ def parse_dict_header(value):
|
||||
|
||||
|
||||
# From mitsuhiko/werkzeug (used with permission).
|
||||
def unquote_header_value(value, is_filename=False):
|
||||
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.
|
||||
@@ -454,7 +497,7 @@ def unquote_header_value(value, is_filename=False):
|
||||
return value
|
||||
|
||||
|
||||
def dict_from_cookiejar(cj):
|
||||
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.
|
||||
@@ -465,7 +508,7 @@ def dict_from_cookiejar(cj):
|
||||
return cookie_dict
|
||||
|
||||
|
||||
def add_dict_to_cookiejar(cj, 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.
|
||||
@@ -476,7 +519,7 @@ def add_dict_to_cookiejar(cj, cookie_dict):
|
||||
return cookiejar_from_dict(cookie_dict, cj)
|
||||
|
||||
|
||||
def get_encodings_from_content(content):
|
||||
def get_encodings_from_content(content: str) -> list[str]:
|
||||
"""Returns encodings from given content string.
|
||||
|
||||
:param content: bytestring to extract encodings from.
|
||||
@@ -501,32 +544,29 @@ def get_encodings_from_content(content):
|
||||
)
|
||||
|
||||
|
||||
def _parse_content_type_header(header):
|
||||
"""Returns content type and parameters from given header
|
||||
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
|
||||
parameters.
|
||||
"""
|
||||
|
||||
tokens = header.split(";")
|
||||
content_type, params = tokens[0].strip(), tokens[1:]
|
||||
params_dict = {}
|
||||
items_to_strip = "\"' "
|
||||
params_dict: dict[str, str | bool] = {}
|
||||
strip_chars = "\"' "
|
||||
|
||||
for param in params:
|
||||
param = param.strip()
|
||||
if param:
|
||||
key, value = param, True
|
||||
index_of_equals = param.find("=")
|
||||
if index_of_equals != -1:
|
||||
key = param[:index_of_equals].strip(items_to_strip)
|
||||
value = param[index_of_equals + 1 :].strip(items_to_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):
|
||||
def get_encoding_from_headers(headers: CaseInsensitiveDict[str]) -> str | None:
|
||||
"""Returns encodings from given HTTP Header Dict.
|
||||
|
||||
:param headers: dictionary to extract encoding from.
|
||||
@@ -551,7 +591,9 @@ def get_encoding_from_headers(headers):
|
||||
return "utf-8"
|
||||
|
||||
|
||||
def stream_decode_response_unicode(iterator, r):
|
||||
def stream_decode_response_unicode(
|
||||
iterator: Iterable[bytes], r: Response
|
||||
) -> Generator[str | bytes, None, None]:
|
||||
"""Stream decodes an iterator."""
|
||||
|
||||
if r.encoding is None:
|
||||
@@ -568,7 +610,17 @@ def stream_decode_response_unicode(iterator, r):
|
||||
yield rv
|
||||
|
||||
|
||||
def iter_slices(string, slice_length):
|
||||
@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:
|
||||
@@ -578,7 +630,7 @@ def iter_slices(string, slice_length):
|
||||
pos += slice_length
|
||||
|
||||
|
||||
def get_unicode_from_response(r):
|
||||
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.
|
||||
@@ -598,8 +650,10 @@ def get_unicode_from_response(r):
|
||||
),
|
||||
DeprecationWarning,
|
||||
)
|
||||
if r.content is None: # type: ignore[reportUnnecessaryComparison]
|
||||
return None
|
||||
|
||||
tried_encodings = []
|
||||
tried_encodings: list[str] = []
|
||||
|
||||
# Try charset from content-type
|
||||
encoding = get_encoding_from_headers(r.headers)
|
||||
@@ -612,18 +666,18 @@ def get_unicode_from_response(r):
|
||||
|
||||
# Fall back:
|
||||
try:
|
||||
return str(r.content, encoding, errors="replace")
|
||||
return str(r.content, encoding or "utf-8", errors="replace")
|
||||
except TypeError:
|
||||
return r.content
|
||||
|
||||
|
||||
# The unreserved URI characters (RFC 3986)
|
||||
UNRESERVED_SET = frozenset(
|
||||
UNRESERVED_SET: Final = frozenset(
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + "0123456789-._~"
|
||||
)
|
||||
|
||||
|
||||
def unquote_unreserved(uri):
|
||||
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.
|
||||
|
||||
@@ -647,7 +701,7 @@ def unquote_unreserved(uri):
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def requote_uri(uri):
|
||||
def requote_uri(uri: str) -> str:
|
||||
"""Re-quote the given URI.
|
||||
|
||||
This function passes the given URI through an unquote/quote cycle to
|
||||
@@ -669,7 +723,7 @@ def requote_uri(uri):
|
||||
return quote(uri, safe=safe_without_percent)
|
||||
|
||||
|
||||
def address_in_network(ip, net):
|
||||
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
|
||||
@@ -684,7 +738,7 @@ def address_in_network(ip, net):
|
||||
return (ipaddr & netmask) == (network & netmask)
|
||||
|
||||
|
||||
def dotted_netmask(mask):
|
||||
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
|
||||
@@ -695,7 +749,7 @@ def dotted_netmask(mask):
|
||||
return socket.inet_ntoa(struct.pack(">I", bits))
|
||||
|
||||
|
||||
def is_ipv4_address(string_ip):
|
||||
def is_ipv4_address(string_ip: str) -> bool:
|
||||
"""
|
||||
:rtype: bool
|
||||
"""
|
||||
@@ -706,7 +760,7 @@ def is_ipv4_address(string_ip):
|
||||
return True
|
||||
|
||||
|
||||
def is_valid_cidr(string_network):
|
||||
def is_valid_cidr(string_network: str) -> bool:
|
||||
"""
|
||||
Very simple check of the cidr format in no_proxy variable.
|
||||
|
||||
@@ -731,7 +785,7 @@ def is_valid_cidr(string_network):
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def set_environ(env_name, value):
|
||||
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
|
||||
@@ -739,6 +793,7 @@ def set_environ(env_name, value):
|
||||
|
||||
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
|
||||
@@ -752,7 +807,7 @@ def set_environ(env_name, value):
|
||||
os.environ[env_name] = old_value
|
||||
|
||||
|
||||
def should_bypass_proxies(url, no_proxy):
|
||||
def should_bypass_proxies(url: str, no_proxy: str | None) -> bool:
|
||||
"""
|
||||
Returns whether we should bypass proxies or not.
|
||||
|
||||
@@ -761,7 +816,7 @@ def should_bypass_proxies(url, no_proxy):
|
||||
|
||||
# Prioritize lowercase environment variables over uppercase
|
||||
# to keep a consistent behaviour with other http projects (curl, wget).
|
||||
def get_proxy(key):
|
||||
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
|
||||
@@ -770,40 +825,42 @@ def should_bypass_proxies(url, no_proxy):
|
||||
if no_proxy is None:
|
||||
no_proxy = get_proxy("no_proxy")
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
|
||||
if parsed.hostname is None:
|
||||
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 = (host for host in no_proxy.replace(" ", "").split(",") if host)
|
||||
no_proxy_hosts = (host for host in no_proxy.replace(" ", "").split(",") if host)
|
||||
|
||||
if is_ipv4_address(parsed.hostname):
|
||||
for proxy_ip in no_proxy:
|
||||
if is_ipv4_address(hostname):
|
||||
for proxy_ip in no_proxy_hosts:
|
||||
if is_valid_cidr(proxy_ip):
|
||||
if address_in_network(parsed.hostname, proxy_ip):
|
||||
if address_in_network(hostname, proxy_ip):
|
||||
return True
|
||||
elif parsed.hostname == proxy_ip:
|
||||
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 = parsed.hostname
|
||||
host_with_port = hostname
|
||||
if parsed.port:
|
||||
host_with_port += f":{parsed.port}"
|
||||
|
||||
for host in no_proxy:
|
||||
if parsed.hostname.endswith(host) or host_with_port.endswith(host):
|
||||
# The URL does match something in no_proxy, so we don't want
|
||||
# to apply the proxies on this URL.
|
||||
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):
|
||||
# parsed.hostname can be `None` in cases such as a file URI.
|
||||
try:
|
||||
bypass = proxy_bypass(parsed.hostname)
|
||||
bypass = proxy_bypass(hostname)
|
||||
except (TypeError, socket.gaierror):
|
||||
bypass = False
|
||||
|
||||
@@ -813,7 +870,7 @@ def should_bypass_proxies(url, no_proxy):
|
||||
return False
|
||||
|
||||
|
||||
def get_environ_proxies(url, no_proxy=None):
|
||||
def get_environ_proxies(url: str, no_proxy: str | None = None) -> dict[str, str]:
|
||||
"""
|
||||
Return a dict of environment proxies.
|
||||
|
||||
@@ -825,7 +882,7 @@ def get_environ_proxies(url, no_proxy=None):
|
||||
return getproxies()
|
||||
|
||||
|
||||
def select_proxy(url, proxies):
|
||||
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
|
||||
@@ -851,7 +908,11 @@ def select_proxy(url, proxies):
|
||||
return proxy
|
||||
|
||||
|
||||
def resolve_proxies(request, proxies, trust_env=True):
|
||||
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.
|
||||
@@ -863,7 +924,7 @@ def resolve_proxies(request, proxies, trust_env=True):
|
||||
:rtype: dict
|
||||
"""
|
||||
proxies = proxies if proxies is not None else {}
|
||||
url = request.url
|
||||
url = cast(str, request.url)
|
||||
scheme = urlparse(url).scheme
|
||||
no_proxy = proxies.get("no_proxy")
|
||||
new_proxies = proxies.copy()
|
||||
@@ -878,7 +939,7 @@ def resolve_proxies(request, proxies, trust_env=True):
|
||||
return new_proxies
|
||||
|
||||
|
||||
def default_user_agent(name="python-requests"):
|
||||
def default_user_agent(name: str = "python-requests") -> str:
|
||||
"""
|
||||
Return a string representing the default user agent.
|
||||
|
||||
@@ -887,7 +948,7 @@ def default_user_agent(name="python-requests"):
|
||||
return f"{name}/{__version__}"
|
||||
|
||||
|
||||
def default_headers():
|
||||
def default_headers() -> CaseInsensitiveDict[str]:
|
||||
"""
|
||||
:rtype: requests.structures.CaseInsensitiveDict
|
||||
"""
|
||||
@@ -901,7 +962,7 @@ def default_headers():
|
||||
)
|
||||
|
||||
|
||||
def parse_header_links(value):
|
||||
def parse_header_links(value: str) -> list[dict[str, str]]:
|
||||
"""Return a list of parsed link headers proxies.
|
||||
|
||||
i.e. Link: <http:/.../front.jpeg>; rel=front; type="image/jpeg",<http://.../back.jpeg>; rel=back;type="image/jpeg"
|
||||
@@ -909,7 +970,7 @@ def parse_header_links(value):
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
links = []
|
||||
links: list[dict[str, str]] = []
|
||||
|
||||
replace_chars = " '\""
|
||||
|
||||
@@ -923,7 +984,7 @@ def parse_header_links(value):
|
||||
except ValueError:
|
||||
url, params = val, ""
|
||||
|
||||
link = {"url": url.strip("<> '\"")}
|
||||
link: dict[str, str] = {"url": url.strip("<> '\"")}
|
||||
|
||||
for param in params.split(";"):
|
||||
try:
|
||||
@@ -944,7 +1005,7 @@ _null2 = _null * 2
|
||||
_null3 = _null * 3
|
||||
|
||||
|
||||
def guess_json_utf(data):
|
||||
def guess_json_utf(data: bytes) -> str | None:
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
@@ -976,14 +1037,14 @@ def guess_json_utf(data):
|
||||
return None
|
||||
|
||||
|
||||
def prepend_scheme_if_needed(url, new_scheme):
|
||||
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
|
||||
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
|
||||
@@ -996,6 +1057,7 @@ def prepend_scheme_if_needed(url, new_scheme):
|
||||
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
|
||||
@@ -1005,7 +1067,7 @@ def prepend_scheme_if_needed(url, new_scheme):
|
||||
return urlunparse((scheme, netloc, path, "", query, fragment))
|
||||
|
||||
|
||||
def get_auth_from_url(url):
|
||||
def get_auth_from_url(url: str) -> tuple[str, str]:
|
||||
"""Given a url with authentication components, extract them into a tuple of
|
||||
username,password.
|
||||
|
||||
@@ -1014,14 +1076,15 @@ def get_auth_from_url(url):
|
||||
parsed = urlparse(url)
|
||||
|
||||
try:
|
||||
auth = (unquote(parsed.username), unquote(parsed.password))
|
||||
# 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):
|
||||
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.
|
||||
|
||||
@@ -1032,10 +1095,15 @@ def check_header_validity(header):
|
||||
_validate_header_part(header, value, 1)
|
||||
|
||||
|
||||
def _validate_header_part(header, header_part, header_validator_index):
|
||||
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):
|
||||
# runtime guard for non-str/bytes input
|
||||
validator = _HEADER_VALIDATORS_BYTE[header_validator_index]
|
||||
else:
|
||||
raise InvalidHeader(
|
||||
@@ -1043,7 +1111,7 @@ def _validate_header_part(header, header_part, header_validator_index):
|
||||
f"must be of type str or bytes, not {type(header_part)}"
|
||||
)
|
||||
|
||||
if not validator.match(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 "
|
||||
@@ -1051,13 +1119,13 @@ def _validate_header_part(header, header_part, header_validator_index):
|
||||
)
|
||||
|
||||
|
||||
def urldefragauth(url):
|
||||
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)
|
||||
scheme, netloc, path, params, query, _fragment = urlparse(url)
|
||||
|
||||
# see func:`prepend_scheme_if_needed`
|
||||
if not netloc:
|
||||
@@ -1068,13 +1136,14 @@ def urldefragauth(url):
|
||||
return urlunparse((scheme, netloc, path, params, query, ""))
|
||||
|
||||
|
||||
def rewind_body(prepared_request):
|
||||
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, integer_types
|
||||
prepared_request._body_position,
|
||||
integer_types,
|
||||
):
|
||||
try:
|
||||
body_seek(prepared_request._body_position)
|
||||
|
||||
@@ -22,6 +22,15 @@ def prepare_url(value):
|
||||
return inner
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_proxy_environ(monkeypatch):
|
||||
"""Remove proxy related environment variables for every test."""
|
||||
proxy_vars = ("http_proxy", "https_proxy", "no_proxy", "ftp_proxy", "all_proxy")
|
||||
for var in proxy_vars:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.delenv(var.upper(), raising=False)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def httpbin(httpbin):
|
||||
return prepare_url(httpbin)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import requests.adapters
|
||||
|
||||
|
||||
def test_request_url_trims_leading_path_separators():
|
||||
def test_request_url_handles_leading_path_separators():
|
||||
"""See also https://github.com/psf/requests/issues/6643."""
|
||||
a = requests.adapters.HTTPAdapter()
|
||||
p = requests.Request(method="GET", url="http://127.0.0.1:10000//v:h").prepare()
|
||||
assert "/v:h" == a.request_url(p, {})
|
||||
assert "//v:h" == a.request_url(p, {})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from tests.testserver.server import Server, consume_socket_content
|
||||
|
||||
import requests
|
||||
from requests.compat import JSONDecodeError
|
||||
from tests.testserver.server import Server, consume_socket_content
|
||||
|
||||
from .utils import override_environ
|
||||
|
||||
|
||||
@@ -44,9 +44,11 @@ from requests.exceptions import (
|
||||
ReadTimeout,
|
||||
RequestException,
|
||||
RetryError,
|
||||
Timeout,
|
||||
TooManyRedirects,
|
||||
UnrewindableBodyError,
|
||||
)
|
||||
from requests.exceptions import SSLError as RequestsSSLError
|
||||
from requests.exceptions import Timeout, TooManyRedirects, UnrewindableBodyError
|
||||
from requests.hooks import default_hooks
|
||||
from requests.models import PreparedRequest, urlencode
|
||||
from requests.sessions import SessionRedirectMixin
|
||||
@@ -215,6 +217,14 @@ class TestRequests:
|
||||
assert r.history[0].status_code == 302
|
||||
assert r.history[0].is_redirect
|
||||
|
||||
def test_redirect_history_no_self_reference(self, httpbin):
|
||||
r = requests.get(httpbin("redirect", "3"))
|
||||
assert r.status_code == 200
|
||||
assert len(r.history) == 3
|
||||
for i, resp in enumerate(r.history):
|
||||
assert resp not in resp.history
|
||||
assert resp.history == r.history[:i]
|
||||
|
||||
def test_HTTP_307_ALLOW_REDIRECT_POST(self, httpbin):
|
||||
r = requests.post(
|
||||
httpbin("redirect-to"),
|
||||
@@ -576,7 +586,7 @@ class TestRequests:
|
||||
("http://doesnotexist.google.com", ConnectionError),
|
||||
# Connecting to an invalid port should raise a ConnectionError
|
||||
("http://localhost:1", ConnectionError),
|
||||
# Inputing a URL that cannot be parsed should raise an InvalidURL error
|
||||
# Inputting a URL that cannot be parsed should raise an InvalidURL error
|
||||
("http://fe80::5054:ff:fe5a:fc0", InvalidURL),
|
||||
),
|
||||
)
|
||||
@@ -727,7 +737,7 @@ class TestRequests:
|
||||
|
||||
try:
|
||||
# Should use netrc
|
||||
# Make sure that we don't use the example.com credentails
|
||||
# Make sure that we don't use the example.com credentials
|
||||
# for the request
|
||||
r = requests.get(url)
|
||||
assert r.status_code == 200
|
||||
@@ -960,20 +970,18 @@ class TestRequests:
|
||||
INVALID_PATH = "/garbage"
|
||||
with pytest.raises(IOError) as e:
|
||||
requests.get(httpbin_secure(), verify=INVALID_PATH)
|
||||
assert str(
|
||||
e.value
|
||||
) == "Could not find a suitable TLS CA certificate bundle, invalid path: {}".format(
|
||||
INVALID_PATH
|
||||
assert (
|
||||
str(e.value)
|
||||
== f"Could not find a suitable TLS CA certificate bundle, invalid path: {INVALID_PATH}"
|
||||
)
|
||||
|
||||
def test_invalid_ssl_certificate_files(self, httpbin_secure):
|
||||
INVALID_PATH = "/garbage"
|
||||
with pytest.raises(IOError) as e:
|
||||
requests.get(httpbin_secure(), cert=INVALID_PATH)
|
||||
assert str(
|
||||
e.value
|
||||
) == "Could not find the TLS certificate file, invalid path: {}".format(
|
||||
INVALID_PATH
|
||||
assert (
|
||||
str(e.value)
|
||||
== f"Could not find the TLS certificate file, invalid path: {INVALID_PATH}"
|
||||
)
|
||||
|
||||
with pytest.raises(IOError) as e:
|
||||
@@ -1079,6 +1087,32 @@ class TestRequests:
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_post_named_tempfile(self, httpbin):
|
||||
with tempfile.NamedTemporaryFile(mode="w+") as f:
|
||||
f.write("named temp file contents\n")
|
||||
f.seek(0)
|
||||
r = requests.post(
|
||||
httpbin("post"),
|
||||
files={"file": f},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["files"]["file"] == "named temp file contents\n"
|
||||
|
||||
def test_post_getattr_proxy_read_only(self, httpbin):
|
||||
|
||||
class ReadProxy:
|
||||
def __init__(self):
|
||||
self._file = io.BytesIO(b"streamed body")
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name == "__iter__":
|
||||
raise AttributeError(name)
|
||||
return getattr(self._file, name)
|
||||
|
||||
r = requests.post(httpbin("post"), data=ReadProxy())
|
||||
assert r.status_code == 200
|
||||
assert r.json()["data"] == "streamed body"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data",
|
||||
(
|
||||
@@ -1791,6 +1825,11 @@ class TestRequests:
|
||||
{"fo\r\no": "bar"},
|
||||
{"fo\n\ro": "bar"},
|
||||
{"fo\no": "bar"},
|
||||
{"foo": "bar\n"},
|
||||
{"foo\n": "bar"},
|
||||
{"foo": "bar\r\n"},
|
||||
{"foo": "\n"},
|
||||
{"foo": "\r\n"},
|
||||
),
|
||||
)
|
||||
def test_header_no_return_chars(self, httpbin, invalid_header):
|
||||
@@ -2060,6 +2099,21 @@ class TestRequests:
|
||||
|
||||
assert "Unable to rewind request body" in str(e)
|
||||
|
||||
def test_getattr_proxy_stream_follows_redirect(self, httpbin):
|
||||
"""Ensure stream wrappers that don't implement __iter__ directly are still detected."""
|
||||
|
||||
class AttrProxy:
|
||||
def __init__(self):
|
||||
self._file = io.BytesIO(b"data")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._file, name)
|
||||
|
||||
r = requests.post(
|
||||
httpbin("redirect-to?url=/post&status_code=307"), data=AttrProxy()
|
||||
)
|
||||
assert r.json()["data"] == "data"
|
||||
|
||||
def _patch_adapter_gzipped_redirect(self, session, url):
|
||||
adapter = session.get_adapter(url=url)
|
||||
org_build_response = adapter.build_response
|
||||
@@ -2460,7 +2514,6 @@ class TestMorselToCookieExpires:
|
||||
|
||||
|
||||
class TestMorselToCookieMaxAge:
|
||||
|
||||
"""Tests for morsel_to_cookie when morsel contains max-age."""
|
||||
|
||||
def test_max_age_valid_int(self):
|
||||
@@ -2567,6 +2620,7 @@ class RedirectSession(SessionRedirectMixin):
|
||||
def build_response(self):
|
||||
request = self.calls[-1].args[0]
|
||||
r = requests.Response()
|
||||
r.url = request.url
|
||||
|
||||
try:
|
||||
r.status_code = int(self.redirects.pop(0))
|
||||
|
||||
@@ -76,3 +76,16 @@ class TestLookupDict:
|
||||
@get_item_parameters
|
||||
def test_get(self, key, value):
|
||||
assert self.lookup_dict.get(key) == value
|
||||
|
||||
def test_hasattr(self):
|
||||
assert hasattr(self.lookup_dict, "bad_gateway") is True
|
||||
assert hasattr(self.lookup_dict, "not_a_key") is False
|
||||
|
||||
def test_getattr(self):
|
||||
assert getattr(self.lookup_dict, "bad_gateway") == 502
|
||||
with pytest.raises(AttributeError):
|
||||
getattr(self.lookup_dict, "not_a_key")
|
||||
|
||||
@get_item_parameters
|
||||
def test_getattr_default(self, key, value):
|
||||
assert getattr(self.lookup_dict, key, None) == value
|
||||
|
||||
@@ -3,9 +3,9 @@ import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from tests.testserver.server import Server
|
||||
|
||||
import requests
|
||||
from tests.testserver.server import Server
|
||||
|
||||
|
||||
class TestTestServer:
|
||||
@@ -42,7 +42,7 @@ class TestTestServer:
|
||||
def test_text_response(self):
|
||||
"""the text_response_server sends the given text"""
|
||||
server = Server.text_response_server(
|
||||
"HTTP/1.1 200 OK\r\n" "Content-Length: 6\r\n" "\r\nroflol"
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nroflol"
|
||||
)
|
||||
|
||||
with server as (host, port):
|
||||
|
||||
@@ -170,6 +170,16 @@ class TestGetNetrcAuth:
|
||||
auth = get_netrc_auth("http://example.com:@evil.com/'")
|
||||
assert auth is None
|
||||
|
||||
def test_empty_default_credentials_ignored(self, tmp_path, monkeypatch):
|
||||
"""Empty default credentials should not be returned."""
|
||||
netrc_path = tmp_path / ".netrc"
|
||||
monkeypatch.setenv("NETRC", str(netrc_path))
|
||||
with open(netrc_path, "w") as f:
|
||||
f.write("machine example.com login user password pass\ndefault\n")
|
||||
|
||||
auth = get_netrc_auth("http://httpbin.org/")
|
||||
assert auth is None
|
||||
|
||||
|
||||
class TestToKeyValList:
|
||||
@pytest.mark.parametrize(
|
||||
@@ -351,9 +361,12 @@ class TestExtractZippedPaths:
|
||||
zipped_path = os.path.join(zipped_py.strpath, name.lstrip(r"\/"))
|
||||
extracted_path = extract_zipped_paths(zipped_path)
|
||||
|
||||
assert extracted_path != zipped_path
|
||||
assert os.path.exists(extracted_path)
|
||||
assert filecmp.cmp(extracted_path, __file__)
|
||||
try:
|
||||
assert extracted_path != zipped_path
|
||||
assert os.path.exists(extracted_path)
|
||||
assert filecmp.cmp(extracted_path, __file__)
|
||||
finally:
|
||||
os.remove(extracted_path)
|
||||
|
||||
def test_invalid_unc_path(self):
|
||||
path = r"\\localhost\invalid\location"
|
||||
@@ -583,7 +596,6 @@ def test_parse_dict_header(value, expected):
|
||||
{
|
||||
"boundary": "something",
|
||||
"boundary2": "something_else",
|
||||
"no_equals": True,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -594,7 +606,6 @@ def test_parse_dict_header(value, expected):
|
||||
{
|
||||
"boundary": "something",
|
||||
"boundary2": "something_else",
|
||||
"no_equals": True,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -605,7 +616,6 @@ def test_parse_dict_header(value, expected):
|
||||
{
|
||||
"boundary": "something",
|
||||
"boundary2": "something_else",
|
||||
"no_equals": True,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -616,7 +626,6 @@ def test_parse_dict_header(value, expected):
|
||||
{
|
||||
"boundary": "something",
|
||||
"boundary2": "something_else",
|
||||
"no_equals": True,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -636,6 +645,10 @@ def test__parse_content_type_header(value, expected):
|
||||
"utf-8",
|
||||
),
|
||||
(CaseInsensitiveDict({"content-type": "text/plain"}), "ISO-8859-1"),
|
||||
(
|
||||
CaseInsensitiveDict({"content-type": "text/html; charset"}),
|
||||
"ISO-8859-1",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_get_encoding_from_headers(value, expected):
|
||||
@@ -831,6 +844,29 @@ def test_should_bypass_proxies_no_proxy(url, expected, monkeypatch):
|
||||
assert should_bypass_proxies(url, no_proxy=no_proxy) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url, expected",
|
||||
(
|
||||
("http://localhost/", True),
|
||||
("http://anotherdomain.com:8888/", True),
|
||||
("http://newdomain.com:1234/", True),
|
||||
("http://www.newdomain.com:1234/", True),
|
||||
("http://foo.d.o.t/", True),
|
||||
("http://d.o.t/", True),
|
||||
("http://prelocalhost/", False),
|
||||
("http://newdomain.com/", False),
|
||||
("http://newdomain.com:1235/", False),
|
||||
),
|
||||
)
|
||||
def test_should_bypass_proxies_no_proxy_domain_boundary(url, expected):
|
||||
"""Ensure no_proxy matching respects domain boundaries and does not
|
||||
greedily match domains that merely endswith the no_proxy entry.
|
||||
See CPython bpo-39057.
|
||||
"""
|
||||
no_proxy = "localhost, anotherdomain.com, newdomain.com:1234, .d.o.t"
|
||||
assert should_bypass_proxies(url, no_proxy=no_proxy) == expected
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "nt", reason="Test only on Windows")
|
||||
@pytest.mark.parametrize(
|
||||
"url, expected, override",
|
||||
|
||||
Reference in New Issue
Block a user