Compare commits
344 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 | ||
|
|
b25c87d7cb | ||
|
|
131e506079 | ||
|
|
b336cb2bc6 | ||
|
|
46e939b552 | ||
|
|
4b9c546aa3 | ||
|
|
7618dbef01 | ||
|
|
2edca11103 | ||
|
|
fec96cd597 | ||
|
|
d58d8aa2f4 | ||
|
|
91a3eabd3d | ||
|
|
90fee0876a | ||
|
|
8ff173b186 | ||
|
|
3f07f990ac | ||
|
|
e57b5bf05c | ||
|
|
021dc729f0 | ||
|
|
821770e822 | ||
|
|
59f8aa2adf | ||
|
|
5b4b64c346 | ||
|
|
7bc45877a8 | ||
|
|
96ba401c12 | ||
|
|
7341690e84 | ||
|
|
6716d7c9f2 | ||
|
|
a7e1c745dc | ||
|
|
c799b8167a | ||
|
|
c65c780849 | ||
|
|
579cd9f233 | ||
|
|
c54d0347e2 | ||
|
|
991f05dcd9 | ||
|
|
4ce9520a1c | ||
|
|
42a1dd640c | ||
|
|
c4c8e20289 | ||
|
|
a5cb4284e1 | ||
|
|
2019450b43 | ||
|
|
1764cc938e | ||
|
|
e946665c59 | ||
|
|
8ba805fa93 | ||
|
|
9ebebdef98 | ||
|
|
5074096613 | ||
|
|
1b7c37ef4e | ||
|
|
5d31ddbf7b | ||
|
|
a2fd25ff34 | ||
|
|
d96f9982a2 | ||
|
|
b25f2b5ec2 | ||
|
|
f761e74a4d | ||
|
|
f2a6ae5e32 | ||
|
|
11f63a330b | ||
|
|
e361622fae | ||
|
|
fe0583ba49 | ||
|
|
8c36da656d | ||
|
|
b653d80ded | ||
|
|
23540c93ca | ||
|
|
ad959acfcf | ||
|
|
a6cf27a77f | ||
|
|
ed0b1b5802 | ||
|
|
1f16cffa43 | ||
|
|
9787d0c09c | ||
|
|
26664fa578 | ||
|
|
7335bbf480 | ||
|
|
0bff2d94e6 | ||
|
|
1ae6fc3137 | ||
|
|
5984296b6a | ||
|
|
f519bbae33 | ||
|
|
314e7c9c70 | ||
|
|
92f10cec5b | ||
|
|
83e67c4485 | ||
|
|
0f5ef1be8c | ||
|
|
f12ccbef6d | ||
|
|
173890a48e | ||
|
|
a3ce6f0075 | ||
|
|
877892e67e | ||
|
|
79b74ef704 | ||
|
|
15e1f17bb6 | ||
|
|
01353d3b4a | ||
|
|
4e383642a9 | ||
|
|
f8aa36b92d | ||
|
|
06d2a38441 | ||
|
|
0e322af877 | ||
|
|
e188799322 | ||
|
|
145b539948 | ||
|
|
b1d73ddb50 | ||
|
|
6badbac6e0 | ||
|
|
2e1452234d | ||
|
|
a62a2d35d9 | ||
|
|
88dce9d854 | ||
|
|
c98e4d133e | ||
|
|
92075b330a | ||
|
|
aa1461b68a | ||
|
|
970e8cec98 | ||
|
|
d6ebc4a2f1 | ||
|
|
9a40d12778 | ||
|
|
0c030f78d2 | ||
|
|
555b870eb1 | ||
|
|
d6dded3f00 | ||
|
|
bf24b7d8d1 | ||
|
|
2d5f54779a | ||
|
|
f1bb07d39b | ||
|
|
60047ade64 | ||
|
|
31ebb8102c | ||
|
|
cc23d1c644 | ||
|
|
0790ea4250 | ||
|
|
e45b428960 | ||
|
|
2a438c27b5 | ||
|
|
2daa7b52a7 | ||
|
|
8dd3b26bf5 | ||
|
|
13d892bdbe | ||
|
|
1604e20fc8 | ||
|
|
eeafb6ab3a | ||
|
|
a94e9b5308 | ||
|
|
a58d7f2ffb | ||
|
|
c0813a2d91 | ||
|
|
eea3bbf9ac | ||
|
|
f3f2611d83 | ||
|
|
a0e79bad05 | ||
|
|
0a7f662aa7 | ||
|
|
5f1c3c22ff | ||
|
|
541aa80ca3 | ||
|
|
d3b3399ece | ||
|
|
0ec2780c29 | ||
|
|
b8be93a721 | ||
|
|
3587a5f869 | ||
|
|
60389df6d6 | ||
|
|
382fc2c0c6 | ||
|
|
7a13c041db | ||
|
|
58cea7a728 | ||
|
|
9439fad038 | ||
|
|
8fa4302322 | ||
|
|
28855fd43a | ||
|
|
5fc10bf1f5 | ||
|
|
46c1a3d8c8 | ||
|
|
6106a63eb6 | ||
|
|
4f3f189d6b | ||
|
|
a5a0e4b587 | ||
|
|
3ff3ff21dd | ||
|
|
96b22fa18c | ||
|
|
b0e6c9bf85 | ||
|
|
72eccc8dd8 | ||
|
|
25939d8784 | ||
|
|
bfba9dc68c | ||
|
|
f23346a9b8 | ||
|
|
f3f9784419 | ||
|
|
b5bd0f14cc | ||
|
|
242d3113c0 | ||
|
|
1396eb6f7a | ||
|
|
e4c821a247 | ||
|
|
421b1f1757 | ||
|
|
1447bccc05 | ||
|
|
3b5978fc5d | ||
|
|
1ddf014f47 | ||
|
|
92f9e431c9 | ||
|
|
951dd15fa6 | ||
|
|
51d0d83eaf | ||
|
|
c7c2ebf1a7 | ||
|
|
a64f32ba45 | ||
|
|
a25fde6989 | ||
|
|
ba67dc8dcc | ||
|
|
769bc3ae5c | ||
|
|
d6ffd868ee | ||
|
|
3fd309a5c1 | ||
|
|
ec84f2c539 | ||
|
|
889910c77a | ||
|
|
b37878d340 | ||
|
|
f6707042d8 | ||
|
|
0b4d494192 | ||
|
|
15849947ec | ||
|
|
c6de5a14e5 | ||
|
|
e66a07b286 | ||
|
|
9e98a8749a | ||
|
|
c32b046243 | ||
|
|
89cde235be | ||
|
|
d7490c9d2d | ||
|
|
e9b1217cff | ||
|
|
ad761abcdd | ||
|
|
839a8edec3 | ||
|
|
a8e9c1b436 | ||
|
|
b55bb15c35 | ||
|
|
8187768622 | ||
|
|
f75b9504fc | ||
|
|
8199a2b389 | ||
|
|
42a3b5cce8 | ||
|
|
f5a7aefc2d | ||
|
|
ee93fac6b2 | ||
|
|
a775435c4b | ||
|
|
12862617df | ||
|
|
6c8d9b1d0f | ||
|
|
881281250f | ||
|
|
29fc8d1e93 | ||
|
|
8b560ecb24 | ||
|
|
4585a7fbda | ||
|
|
16dc70df34 | ||
|
|
bea231b033 | ||
|
|
2ee5b0b01c | ||
|
|
89f0eb91b3 | ||
|
|
005571d118 | ||
|
|
d63e94f552 | ||
|
|
e9fa2e2da3 | ||
|
|
678fca8423 | ||
|
|
d8152769ce | ||
|
|
9ff1a25ef5 | ||
|
|
4bd06cd325 | ||
|
|
09a241f59e | ||
|
|
6ad493650c | ||
|
|
9acca90bf6 | ||
|
|
c7933453cf | ||
|
|
ea49261a27 | ||
|
|
8112fcc7be | ||
|
|
2c193bda0c | ||
|
|
34a64bc65e | ||
|
|
9b6c62e235 | ||
|
|
cb7fcd7ebd | ||
|
|
aa4cc78627 | ||
|
|
dfe46c1b7f | ||
|
|
2ecdb68561 | ||
|
|
fecf3dc472 | ||
|
|
cdbc2e2715 | ||
|
|
22db55a889 | ||
|
|
6e5b15d542 | ||
|
|
147c8511dd | ||
|
|
74ea7cf7a6 | ||
|
|
3022253346 | ||
|
|
b639e66c81 | ||
|
|
d3d504436e | ||
|
|
53a0562331 |
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.
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Custom.md
vendored
3
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Request for Help
|
||||
about: Guidance on using Requests.
|
||||
labels:
|
||||
- "Question/Not a bug"
|
||||
- "actions/autoclose-qa"
|
||||
|
||||
---
|
||||
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,6 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels:
|
||||
- "Feature Request"
|
||||
- "actions/autoclose-feat"
|
||||
|
||||
---
|
||||
|
||||
|
||||
17
.github/SECURITY.md
vendored
17
.github/SECURITY.md
vendored
@@ -1,18 +1,9 @@
|
||||
# Vulnerability Disclosure
|
||||
|
||||
If you think you have found a potential security vulnerability in
|
||||
requests, please email [Nate](mailto:nate.prewitt@gmail.com)
|
||||
and [Seth](mailto:sethmichaellarson@gmail.com) directly.
|
||||
**Do not file a public issue.**
|
||||
|
||||
Our PGP Key fingerprints are:
|
||||
|
||||
- 8722 7E29 AD9C FF5C FAC3 EA6A 44D3 FF97 B80D C864 ([@nateprewitt](https://keybase.io/nateprewitt))
|
||||
|
||||
- EDD5 6765 A9D8 4653 CBC8 A134 51B0 6736 1740 F5FC ([@sethmlarson](https://keybase.io/sethmlarson))
|
||||
|
||||
You can also contact us on [Keybase](https://keybase.io) with the
|
||||
profiles above if desired.
|
||||
requests, please open a [draft Security Advisory](https://github.com/psf/requests/security/advisories/new)
|
||||
via GitHub. We will coordinate verification and next steps through
|
||||
that secure medium.
|
||||
|
||||
If English is not your first language, please try to describe the
|
||||
problem and its impact to the best of your ability. For greater detail,
|
||||
@@ -72,7 +63,7 @@ intended patch ahead of time, to ensure that they are able to promptly
|
||||
release their downstream packages. Currently the list of people we
|
||||
actively contact *ahead of a public release* is:
|
||||
|
||||
- Jeremy Cline, Red Hat (@jeremycline)
|
||||
- Python Maintenance Team, Red Hat (python-maint@redhat.com)
|
||||
- Daniele Tricoli, Debian (@eriol)
|
||||
|
||||
We will notify these individuals at least a week ahead of our planned
|
||||
|
||||
30
.github/dependabot.yml
vendored
Normal file
30
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
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:
|
||||
- "*"
|
||||
38
.github/workflows/close-issues.yml
vendored
Normal file
38
.github/workflows/close-issues.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: 'Autoclose Issues'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
close_qa:
|
||||
permissions:
|
||||
issues: write
|
||||
if: github.event.label.name == 'actions/autoclose-qa'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue close $ISSUE_URL \
|
||||
--comment "As described in the template, we won't be able to answer questions on this issue tracker. Please use [Stack Overflow](https://stackoverflow.com/)" \
|
||||
--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:
|
||||
- env:
|
||||
ISSUE_URL: ${{ github.event.issue.html_url }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh issue close $ISSUE_URL \
|
||||
--comment "As described in the template, Requests is not accepting feature requests" \
|
||||
--reason "not planned"
|
||||
gh issue lock $ISSUE_URL --reason off_topic
|
||||
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@v2
|
||||
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@v1
|
||||
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@v1
|
||||
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@v1
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
|
||||
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@@ -7,14 +7,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Run pre-commit
|
||||
uses: pre-commit/action@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@v3
|
||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||
with:
|
||||
issue-lock-inactive-days: 90
|
||||
pr-lock-inactive-days: 90
|
||||
|
||||
95
.github/workflows/publish.yml
vendored
Normal file
95
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test-pypi-only:
|
||||
description: "Publish to Test PyPI only"
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build dists"
|
||||
runs-on: "ubuntu-latest"
|
||||
outputs:
|
||||
artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }}
|
||||
|
||||
steps:
|
||||
- name: "Checkout repository"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Setup Python"
|
||||
uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: "Install dependencies"
|
||||
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: "Upload dists"
|
||||
uses: "actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"
|
||||
id: upload-artifact
|
||||
with:
|
||||
name: "dist"
|
||||
path: "dist/"
|
||||
if-no-files-found: error
|
||||
retention-days: 5
|
||||
|
||||
publish:
|
||||
name: "Publish"
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
needs: ["build"]
|
||||
permissions:
|
||||
id-token: write
|
||||
runs-on: "ubuntu-latest"
|
||||
environment:
|
||||
name: "publish"
|
||||
|
||||
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 PyPI"
|
||||
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
|
||||
66
.github/workflows/run-tests.yml
vendored
66
.github/workflows/run-tests.yml
vendored
@@ -12,24 +12,72 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "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]
|
||||
include:
|
||||
# pypy-3.7 on Mac OS currently fails trying to compile
|
||||
# brotlipy. Moving pypy3 to only test linux.
|
||||
- python-version: pypy-3.7
|
||||
os: ubuntu-latest
|
||||
experimental: false
|
||||
# Pypy-3.11 can't install openssl-sys with rust
|
||||
# which prevents us from testing in GHA.
|
||||
exclude:
|
||||
- { python-version: "pypy-3.11", os: "windows-latest" }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
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
|
||||
run: |
|
||||
make ci
|
||||
|
||||
no_chardet:
|
||||
name: "No Character Detection"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: 'Set up Python 3.10'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
make
|
||||
python -m pip uninstall -y "charset_normalizer" "chardet"
|
||||
- name: Run tests
|
||||
run: |
|
||||
make ci
|
||||
|
||||
urllib3:
|
||||
name: 'urllib3 1.x'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: 'Set up Python 3.10'
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
make
|
||||
python -m pip install "urllib3<2"
|
||||
- name: Run tests
|
||||
run: |
|
||||
make ci
|
||||
|
||||
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.0.1
|
||||
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: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- id: ruff-check
|
||||
args: [--fix]
|
||||
- id: ruff-format
|
||||
exclude: tests/test_lowlevel.py
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
|
||||
29
.readthedocs.yaml
Normal file
29
.readthedocs.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
builder: "dirhtml"
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
formats:
|
||||
- pdf
|
||||
- epub
|
||||
|
||||
# Optional but recommended, declare the Python requirements required
|
||||
# to build your documentation
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- path: .
|
||||
- requirements: docs/requirements.txt
|
||||
17
AUTHORS.rst
17
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
|
||||
````````````````````````````
|
||||
- Kenneth Reitz <me@kennethreitz.org> `@ken-reitz <https://github.com/ken-reitz>`_, reluctant Keeper of the Master Crystal.
|
||||
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
|
||||
```````````````````````
|
||||
@@ -192,3 +192,4 @@ Patches and Suggestions
|
||||
- Alessio Izzo (`@aless10 <https://github.com/aless10>`_)
|
||||
- Sylvain Marié (`@smarie <https://github.com/smarie>`_)
|
||||
- Hod Bin Noon (`@hodbn <https://github.com/hodbn>`_)
|
||||
- Mike Fiedler (`@miketheman <https://github.com/miketheman>`_)
|
||||
|
||||
225
HISTORY.md
225
HISTORY.md
@@ -6,6 +6,229 @@ 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)
|
||||
-------------------
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- The SSLContext caching feature originally introduced in 2.32.0 has created
|
||||
a new class of issues in Requests that have had negative impact across a number
|
||||
of use cases. The Requests team has decided to revert this feature as long term
|
||||
maintenance of it is proving to be unsustainable in its current iteration.
|
||||
|
||||
**Deprecations**
|
||||
- Added support for Python 3.14.
|
||||
- Dropped support for Python 3.8 following its end of support.
|
||||
|
||||
2.32.4 (2025-06-10)
|
||||
-------------------
|
||||
|
||||
**Security**
|
||||
- CVE-2024-47081 Fixed an issue where a maliciously crafted URL and trusted
|
||||
environment will retrieve credentials for the wrong hostname/machine from a
|
||||
netrc file.
|
||||
|
||||
**Improvements**
|
||||
- Numerous documentation improvements
|
||||
|
||||
**Deprecations**
|
||||
- Added support for pypy 3.11 for Linux and macOS.
|
||||
- Dropped support for pypy 3.9 following its end of support.
|
||||
|
||||
|
||||
2.32.3 (2024-05-29)
|
||||
-------------------
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed bug breaking the ability to specify custom SSLContexts in sub-classes of
|
||||
HTTPAdapter. (#6716)
|
||||
- Fixed issue where Requests started failing to run on Python versions compiled
|
||||
without the `ssl` module. (#6724)
|
||||
|
||||
2.32.2 (2024-05-21)
|
||||
-------------------
|
||||
|
||||
**Deprecations**
|
||||
- To provide a more stable migration for custom HTTPAdapters impacted
|
||||
by the CVE changes in 2.32.0, we've renamed `_get_connection` to
|
||||
a new public API, `get_connection_with_tls_context`. Existing custom
|
||||
HTTPAdapters will need to migrate their code to use this new API.
|
||||
`get_connection` is considered deprecated in all versions of Requests>=2.32.0.
|
||||
|
||||
A minimal (2-line) example has been provided in the linked PR to ease
|
||||
migration, but we strongly urge users to evaluate if their custom adapter
|
||||
is subject to the same issue described in CVE-2024-35195. (#6710)
|
||||
|
||||
2.32.1 (2024-05-20)
|
||||
-------------------
|
||||
|
||||
**Bugfixes**
|
||||
- Add missing test certs to the sdist distributed on PyPI.
|
||||
|
||||
|
||||
2.32.0 (2024-05-20)
|
||||
-------------------
|
||||
|
||||
**Security**
|
||||
- Fixed an issue where setting `verify=False` on the first request from a
|
||||
Session will cause subsequent requests to the _same origin_ to also ignore
|
||||
cert verification, regardless of the value of `verify`.
|
||||
(https://github.com/psf/requests/security/advisories/GHSA-9wx4-h78v-vm56)
|
||||
|
||||
**Improvements**
|
||||
- `verify=True` now reuses a global SSLContext which should improve
|
||||
request time variance between first and subsequent requests. It should
|
||||
also minimize certificate load time on Windows systems when using a Python
|
||||
version built with OpenSSL 3.x. (#6667)
|
||||
- Requests now supports optional use of character detection
|
||||
(`chardet` or `charset_normalizer`) when repackaged or vendored.
|
||||
This enables `pip` and other projects to minimize their vendoring
|
||||
surface area. The `Response.text()` and `apparent_encoding` APIs
|
||||
will default to `utf-8` if neither library is present. (#6702)
|
||||
|
||||
**Bugfixes**
|
||||
- Fixed bug in length detection where emoji length was incorrectly
|
||||
calculated in the request content-length. (#6589)
|
||||
- Fixed deserialization bug in JSONDecodeError. (#6629)
|
||||
- Fixed bug where an extra leading `/` (path separator) could lead
|
||||
urllib3 to unnecessarily reparse the request URI. (#6644)
|
||||
|
||||
**Deprecations**
|
||||
|
||||
- Requests has officially added support for CPython 3.12 (#6503)
|
||||
- Requests has officially added support for PyPy 3.9 and 3.10 (#6641)
|
||||
- Requests has officially dropped support for CPython 3.7 (#6642)
|
||||
- Requests has officially dropped support for PyPy 3.7 and 3.8 (#6641)
|
||||
|
||||
**Documentation**
|
||||
- Various typo fixes and doc improvements.
|
||||
|
||||
**Packaging**
|
||||
- Requests has started adopting some modern packaging practices.
|
||||
The source files for the projects (formerly `requests`) is now located
|
||||
in `src/requests` in the Requests sdist. (#6506)
|
||||
- Starting in Requests 2.33.0, Requests will migrate to a PEP 517 build system
|
||||
using `hatchling`. This should not impact the average user, but extremely old
|
||||
versions of packaging utilities may have issues with the new packaging format.
|
||||
|
||||
|
||||
2.31.0 (2023-05-22)
|
||||
-------------------
|
||||
|
||||
**Security**
|
||||
- Versions of Requests between v2.3.0 and v2.30.0 are vulnerable to potential
|
||||
forwarding of `Proxy-Authorization` headers to destination servers when
|
||||
following HTTPS redirects.
|
||||
|
||||
When proxies are defined with user info (`https://user:pass@proxy:8080`), Requests
|
||||
will construct a `Proxy-Authorization` header that is attached to the request to
|
||||
authenticate with the proxy.
|
||||
|
||||
In cases where Requests receives a redirect response, it previously reattached
|
||||
the `Proxy-Authorization` header incorrectly, resulting in the value being
|
||||
sent through the tunneled connection to the destination server. Users who rely on
|
||||
defining their proxy credentials in the URL are *strongly* encouraged to upgrade
|
||||
to Requests 2.31.0+ to prevent unintentional leakage and rotate their proxy
|
||||
credentials once the change has been fully deployed.
|
||||
|
||||
Users who do not use a proxy or do not supply their proxy credentials through
|
||||
the user information portion of their proxy URL are not subject to this
|
||||
vulnerability.
|
||||
|
||||
Full details can be read in our [Github Security Advisory](https://github.com/psf/requests/security/advisories/GHSA-j8r2-6x86-q33q)
|
||||
and [CVE-2023-32681](https://nvd.nist.gov/vuln/detail/CVE-2023-32681).
|
||||
|
||||
|
||||
2.30.0 (2023-05-03)
|
||||
-------------------
|
||||
|
||||
@@ -73,7 +296,7 @@ dev
|
||||
cert verification. All Requests 2.x versions before 2.28.0 are affected. (#6074)
|
||||
- Fixed urllib3 exception leak, wrapping `urllib3.exceptions.SSLError` with
|
||||
`requests.exceptions.SSLError` for `content` and `iter_content`. (#6057)
|
||||
- Fixed issue where invalid Windows registry entires caused proxy resolution
|
||||
- Fixed issue where invalid Windows registry entries caused proxy resolution
|
||||
to raise an exception rather than ignoring the entry. (#6149)
|
||||
- Fixed issue where entire payload could be included in the error message for
|
||||
JSONDecodeError. (#6036)
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
include README.md LICENSE NOTICE HISTORY.md pytest.ini requirements-dev.txt
|
||||
include README.md LICENSE NOTICE HISTORY.md requirements-dev.txt
|
||||
recursive-include tests *.py
|
||||
recursive-include tests/certs *
|
||||
|
||||
24
Makefile
24
Makefile
@@ -1,25 +1,25 @@
|
||||
.PHONY: docs
|
||||
init:
|
||||
pip install -r requirements-dev.txt
|
||||
python -m pip install -r requirements-dev.txt
|
||||
test:
|
||||
# This runs all of the tests on all supported Python versions.
|
||||
tox -p
|
||||
python -m pytest tests
|
||||
|
||||
ci:
|
||||
pytest tests --junitxml=report.xml
|
||||
python -m pytest tests --junitxml=report.xml
|
||||
|
||||
test-readme:
|
||||
python setup.py check --restructuredtext --strict && ([ $$? -eq 0 ] && echo "README.rst and HISTORY.rst ok") || echo "Invalid markup in README.rst or HISTORY.rst!"
|
||||
|
||||
flake8:
|
||||
flake8 --ignore=E501,F401,E128,E402,E731,F821 requests
|
||||
|
||||
coverage:
|
||||
pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=requests tests
|
||||
python -m pytest --cov-config .coveragerc --verbose --cov-report term --cov-report xml --cov=src/requests tests
|
||||
|
||||
publish:
|
||||
pip install 'twine>=1.5.0'
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
.publishenv:
|
||||
python -m venv .publishenv
|
||||
.publishenv/bin/pip install 'twine>=1.5.0' build
|
||||
|
||||
publish: .publishenv
|
||||
.publishenv/bin/python -m build
|
||||
.publishenv/bin/python -m twine upload --skip-existing dist/*
|
||||
rm -fr build dist .egg requests.egg-info
|
||||
|
||||
docs:
|
||||
|
||||
20
README.md
20
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.7+.
|
||||
Requests officially supports Python 3.10+.
|
||||
|
||||
## Supported Features & Best–Practices
|
||||
|
||||
@@ -53,14 +55,10 @@ 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
|
||||
fetch.fsck.badTimezone=ignore` flag to avoid an error about a bad commit (see
|
||||
fetch.fsck.badTimezone=ignore` flag to avoid an error about a bad commit timestamp (see
|
||||
[this issue](https://github.com/psf/requests/issues/2690) for more background):
|
||||
|
||||
```shell
|
||||
|
||||
@@ -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="https://requests.readthedocs.io/en/latest/user/quickstart/">Quickstart</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/user/advanced/">Advanced Usage</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/api/">API Reference</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/community/updates/#release-history">Release History</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/dev/contributing/">Contributors Guide</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/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="https://requests.readthedocs.io/en/latest/user/quickstart/">Quickstart</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/user/advanced/">Advanced Usage</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/api/">API Reference</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/community/updates/#release-history">Release History</a></li>
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/dev/contributing/">Contributors Guide</a></li>
|
||||
|
||||
<p></p>
|
||||
|
||||
<li><a href="https://requests.readthedocs.io/en/latest/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>
|
||||
|
||||
@@ -31,7 +31,6 @@ Exceptions
|
||||
.. autoexception:: requests.RequestException
|
||||
.. autoexception:: requests.ConnectionError
|
||||
.. autoexception:: requests.HTTPError
|
||||
.. autoexception:: requests.URLRequired
|
||||
.. autoexception:: requests.TooManyRedirects
|
||||
.. autoexception:: requests.ConnectTimeout
|
||||
.. autoexception:: requests.ReadTimeout
|
||||
@@ -56,6 +55,7 @@ Lower-Level Classes
|
||||
|
||||
.. autoclass:: Response
|
||||
:inherited-members:
|
||||
:exclude-members: is_permanent_redirect
|
||||
|
||||
|
||||
Lower-Lower-Level Classes
|
||||
|
||||
@@ -22,7 +22,7 @@ Custom User-Agents?
|
||||
-------------------
|
||||
|
||||
Requests allows you to easily override User-Agent strings, along with
|
||||
any other HTTP Header. See `documentation about headers <https://requests.readthedocs.io/en/latest/user/quickstart/#custom-headers>`_.
|
||||
any other HTTP Header. See :ref:`documentation about headers <custom-headers>`.
|
||||
|
||||
|
||||
|
||||
@@ -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 "
|
||||
@@ -55,7 +55,8 @@ Chris Adams gave an excellent summary on
|
||||
Python 3 Support?
|
||||
-----------------
|
||||
|
||||
Yes! Requests officially supports Python 3.7+ and PyPy.
|
||||
Yes! Requests supports all `officially supported versions of Python <https://devguide.python.org/versions/>`_
|
||||
and recent releases of PyPy.
|
||||
|
||||
Python 2 Support?
|
||||
-----------------
|
||||
@@ -64,8 +65,8 @@ No! As of Requests 2.28.0, Requests no longer supports Python 2.7. Users who
|
||||
have been unable to migrate should pin to `requests<2.28`. Full information
|
||||
can be found in `psf/requests#6023 <https://github.com/psf/requests/issues/6023>`_.
|
||||
|
||||
It is *highly* recommended users migrate to Python 3.8+ now since Python
|
||||
2.7 is no longer receiving bug fixes or security updates as of January 1, 2020.
|
||||
It is *highly* recommended users migrate to a supported Python 3.x version now since
|
||||
Python 2.7 is no longer receiving bug fixes or security updates as of January 1, 2020.
|
||||
|
||||
What are "hostname doesn't match" errors?
|
||||
-----------------------------------------
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
Integrations
|
||||
============
|
||||
|
||||
Python for iOS
|
||||
--------------
|
||||
|
||||
Requests is built into the wonderful `Python for iOS <https://itunes.apple.com/us/app/python-2.7-for-ios/id485729872?mt=Python8>`_ runtime!
|
||||
|
||||
To give it a try, simply::
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
Articles & Talks
|
||||
================
|
||||
- `Python for the Web <https://www.gun.io/blog/python-for-the-web>`_ teaches how to use Python to interact with the web, using Requests.
|
||||
- `Daniel Greenfeld's Review of Requests <https://pydanny.blogspot.com/2011/05/python-http-requests-for-humans.html>`_
|
||||
- `My 'Python for Humans' talk <http://python-for-humans.heroku.com>`_ ( `audio <https://codeconf.s3.amazonaws.com/2011/pycodeconf/talks/PyCodeConf2011%20-%20Kenneth%20Reitz.m4a>`_ )
|
||||
- `Issac Kelly's 'Consuming Web APIs' talk <https://issackelly.github.com/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
|
||||
- `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>`_
|
||||
|
||||
@@ -1,110 +1,5 @@
|
||||
Vulnerability Disclosure
|
||||
========================
|
||||
|
||||
If you think you have found a potential security vulnerability in requests,
|
||||
please email `Nate <mailto:nate.prewitt@gmail.com>`_ and `Seth <mailto:@sethmichaellarson@gmail.com>`_ directly. **Do not file a public issue.**
|
||||
|
||||
Our PGP Key fingerprints are:
|
||||
|
||||
- 8722 7E29 AD9C FF5C FAC3 EA6A 44D3 FF97 B80D C864 (`@nateprewitt <https://keybase.io/nateprewitt>`_)
|
||||
|
||||
- EDD5 6765 A9D8 4653 CBC8 A134 51B0 6736 1740 F5FC (`@sethmlarson <https://keybase.io/sethmlarson>`_)
|
||||
|
||||
You can also contact us on `Keybase <https://keybase.io/>`_ with the
|
||||
profiles above if desired.
|
||||
|
||||
If English is not your first language, please try to describe the problem and
|
||||
its impact to the best of your ability. For greater detail, please use your
|
||||
native language and we will try our best to translate it using online services.
|
||||
|
||||
Please also include the code you used to find the problem and the shortest
|
||||
amount of code necessary to reproduce it.
|
||||
|
||||
Please do not disclose this to anyone else. We will retrieve a CVE identifier
|
||||
if necessary and give you full credit under whatever name or alias you provide.
|
||||
We will only request an identifier when we have a fix and can publish it in a
|
||||
release.
|
||||
|
||||
We will respect your privacy and will only publicize your involvement if you
|
||||
grant us permission.
|
||||
|
||||
Process
|
||||
-------
|
||||
|
||||
This following information discusses the process the requests project follows
|
||||
in response to vulnerability disclosures. If you are disclosing a
|
||||
vulnerability, this section of the documentation lets you know how we will
|
||||
respond to your disclosure.
|
||||
|
||||
Timeline
|
||||
~~~~~~~~
|
||||
|
||||
When you report an issue, one of the project members will respond to you within
|
||||
two days *at the outside*. In most cases responses will be faster, usually
|
||||
within 12 hours. This initial response will at the very least confirm receipt
|
||||
of the report.
|
||||
|
||||
If we were able to rapidly reproduce the issue, the initial response will also
|
||||
contain confirmation of the issue. If we are not, we will often ask for more
|
||||
information about the reproduction scenario.
|
||||
|
||||
Our goal is to have a fix for any vulnerability released within two weeks of
|
||||
the initial disclosure. This may potentially involve shipping an interim
|
||||
release that simply disables function while a more mature fix can be prepared,
|
||||
but will in the vast majority of cases mean shipping a complete release as soon
|
||||
as possible.
|
||||
|
||||
Throughout the fix process we will keep you up to speed with how the fix is
|
||||
progressing. Once the fix is prepared, we will notify you that we believe we
|
||||
have a fix. Often we will ask you to confirm the fix resolves the problem in
|
||||
your environment, especially if we are not confident of our reproduction
|
||||
scenario.
|
||||
|
||||
At this point, we will prepare for the release. We will obtain a CVE number
|
||||
if one is required, providing you with full credit for the discovery. We will
|
||||
also decide on a planned release date, and let you know when it is. This
|
||||
release date will *always* be on a weekday.
|
||||
|
||||
At this point we will reach out to our major downstream packagers to notify
|
||||
them of an impending security-related patch so they can make arrangements. In
|
||||
addition, these packagers will be provided with the intended patch ahead of
|
||||
time, to ensure that they are able to promptly release their downstream
|
||||
packages. Currently the list of people we actively contact *ahead of a public
|
||||
release* is:
|
||||
|
||||
- Python Maintenance Team, Red Hat (python-maint@redhat.com)
|
||||
- Daniele Tricoli, Debian (@eriol)
|
||||
|
||||
We will notify these individuals at least a week ahead of our planned release
|
||||
date to ensure that they have sufficient time to prepare. If you believe you
|
||||
should be on this list, please let one of the maintainers know at one of the
|
||||
email addresses at the top of this article.
|
||||
|
||||
On release day, we will push the patch to our public repository, along with an
|
||||
updated changelog that describes the issue and credits you. We will then issue
|
||||
a PyPI release containing the patch.
|
||||
|
||||
At this point, we will publicise the release. This will involve mails to
|
||||
mailing lists, Tweets, and all other communication mechanisms available to the
|
||||
core team.
|
||||
|
||||
We will also explicitly mention which commits contain the fix to make it easier
|
||||
for other distributors and users to easily patch their own versions of requests
|
||||
if upgrading is not an option.
|
||||
|
||||
Previous CVEs
|
||||
-------------
|
||||
|
||||
- Fixed in 2.20.0
|
||||
- `CVE 2018-18074 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2018-18074>`_
|
||||
|
||||
- Fixed in 2.6.0
|
||||
|
||||
- `CVE 2015-2296 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2015-2296>`_,
|
||||
reported by Matthew Daley of `BugFuzz <https://bugfuzz.com/>`_.
|
||||
|
||||
- Fixed in 2.3.0
|
||||
|
||||
- `CVE 2014-1829 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1829>`_
|
||||
|
||||
- `CVE 2014-1830 <https://cve.mitre.org/cgi-bin/cvename.cgi?name=2014-1830>`_
|
||||
The latest vulnerability disclosure information can be found on GitHub in our
|
||||
`Security Policy <https://github.com/psf/requests/blob/main/.github/SECURITY.md>`_.
|
||||
|
||||
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 <a href="https://kenreitz.org/projects">Kenneth Reitz</a> 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/
|
||||
|
||||
@@ -22,19 +22,17 @@ The guide is split into sections based on the type of contribution you're
|
||||
thinking of making, with a section that covers general guidelines for all
|
||||
contributors.
|
||||
|
||||
Be Cordial
|
||||
----------
|
||||
Code of Conduct
|
||||
---------------
|
||||
|
||||
**Be cordial or be on your way**. *—Kenneth Reitz*
|
||||
The Python community is made up of members from around the globe with a diverse
|
||||
set of skills, personalities, and experiences. It is through these differences
|
||||
that our community experiences great successes and continued growth. When you're
|
||||
working with members of the community, follow the
|
||||
`Python Software Foundation Code of Conduct`_ to help steer your interactions
|
||||
and keep Python a positive, successful, and growing community.
|
||||
|
||||
Requests has one very important rule governing all forms of contribution,
|
||||
including reporting bugs or requesting features. This golden rule is
|
||||
"`be cordial or be on your way`_".
|
||||
|
||||
**All contributions are welcome**, as long as
|
||||
everyone involved is treated with respect.
|
||||
|
||||
.. _be cordial or be on your way: https://kenreitz.org/essays/2013/01/27/be-cordial-or-be-on-your-way
|
||||
.. _Python Software Foundation Code of Conduct: https://policies.python.org/python.org/code-of-conduct/
|
||||
|
||||
.. _early-feedback:
|
||||
|
||||
@@ -134,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://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.7+, 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
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Pinning to avoid unexpected breakages.
|
||||
# Used by RTD to generate docs.
|
||||
Sphinx==4.2.0
|
||||
Sphinx==7.2.6
|
||||
|
||||
@@ -291,7 +291,7 @@ versions of Requests.
|
||||
For the sake of security we recommend upgrading certifi frequently!
|
||||
|
||||
.. _HTTP persistent connection: https://en.wikipedia.org/wiki/HTTP_persistent_connection
|
||||
.. _connection pooling: https://urllib3.readthedocs.io/en/latest/reference/index.html#module-urllib3.connectionpool
|
||||
.. _connection pooling: https://urllib3.readthedocs.io/en/latest/reference/urllib3.connectionpool.html
|
||||
.. _certifi: https://certifiio.readthedocs.io/
|
||||
.. _Mozilla trust store: https://hg.mozilla.org/mozilla-central/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -666,6 +667,8 @@ You override this default certificate bundle by setting the ``REQUESTS_CA_BUNDLE
|
||||
>>> import requests
|
||||
>>> requests.get('https://example.org')
|
||||
|
||||
.. _socks:
|
||||
|
||||
SOCKS
|
||||
^^^^^
|
||||
|
||||
@@ -675,11 +678,9 @@ 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``:
|
||||
You can get the dependencies for this feature from ``pip``::
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python -m pip install requests[socks]
|
||||
$ python -m pip install 'requests[socks]'
|
||||
|
||||
Once you've installed those dependencies, using a SOCKS proxy is just as easy
|
||||
as using a HTTP one::
|
||||
@@ -946,7 +947,7 @@ Link Headers
|
||||
Many HTTP APIs feature Link headers. They make APIs more self describing and
|
||||
discoverable.
|
||||
|
||||
GitHub uses these for `pagination <https://developer.github.com/v3/#pagination>`_
|
||||
GitHub uses these for `pagination <https://docs.github.com/en/rest/guides/using-pagination-in-the-rest-api>`_
|
||||
in their API, for example::
|
||||
|
||||
>>> url = 'https://api.github.com/users/kennethreitz/repos?page=1&per_page=10'
|
||||
@@ -967,11 +968,9 @@ Requests will automatically parse these link headers and make them easily consum
|
||||
Transport Adapters
|
||||
------------------
|
||||
|
||||
As of v1.0.0, Requests has moved to a modular internal design. Part of the
|
||||
reason this was done was to implement Transport Adapters, originally
|
||||
`described here`_. Transport Adapters provide a mechanism to define interaction
|
||||
methods for an HTTP service. In particular, they allow you to apply per-service
|
||||
configuration.
|
||||
As of v1.0.0, Requests has moved to a modular internal design using Transport
|
||||
Adapters. These objects provide a mechanism to define interaction methods for an
|
||||
HTTP service. In particular, they allow you to apply per-service configuration.
|
||||
|
||||
Requests ships with a single Transport Adapter, the :class:`HTTPAdapter
|
||||
<requests.adapters.HTTPAdapter>`. This adapter provides the default Requests
|
||||
@@ -994,6 +993,10 @@ The mount call registers a specific instance of a Transport Adapter to a
|
||||
prefix. Once mounted, any HTTP request made using that session whose URL starts
|
||||
with the given prefix will use the given Transport Adapter.
|
||||
|
||||
.. note:: The adapter will be chosen based on a longest prefix match. Be mindful
|
||||
prefixes such as ``http://localhost`` will also match ``http://localhost.other.com``
|
||||
or ``http://localhost@other.com``. It's recommended to terminate full hostnames with a ``/``.
|
||||
|
||||
Many of the details of implementing a Transport Adapter are beyond the scope of
|
||||
this documentation, but take a look at the next example for a simple SSL use-
|
||||
case. For more than that, you might look at subclassing the
|
||||
@@ -1026,8 +1029,29 @@ library to use SSLv3::
|
||||
num_pools=connections, maxsize=maxsize,
|
||||
block=block, ssl_version=ssl.PROTOCOL_SSLv3)
|
||||
|
||||
.. _`described here`: https://kenreitz.org/essays/2012/06/14/the-future-of-python-http
|
||||
Example: Automatic Retries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
By default, Requests does not retry failed connections. However, it is possible
|
||||
to implement automatic retries with a powerful array of features, including
|
||||
backoff, within a Requests :class:`Session <requests.Session>` using the
|
||||
`urllib3.util.Retry`_ class::
|
||||
|
||||
from urllib3.util import Retry
|
||||
from requests import Session
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
s = Session()
|
||||
retries = Retry(
|
||||
total=3,
|
||||
backoff_factor=0.1,
|
||||
status_forcelist=[502, 503, 504],
|
||||
allowed_methods={'POST'},
|
||||
)
|
||||
s.mount('https://', HTTPAdapter(max_retries=retries))
|
||||
|
||||
.. _`urllib3`: https://github.com/urllib3/urllib3
|
||||
.. _`urllib3.util.Retry`: https://urllib3.readthedocs.io/en/stable/reference/urllib3.util.html#urllib3.util.Retry
|
||||
|
||||
.. _blocking-or-nonblocking:
|
||||
|
||||
@@ -1055,7 +1079,7 @@ Header Ordering
|
||||
|
||||
In unusual circumstances you may want to provide headers in an ordered manner. If you pass an ``OrderedDict`` to the ``headers`` keyword argument, that will provide the headers with an ordering. *However*, the ordering of the default headers used by Requests will be preferred, which means that if you override default headers in the ``headers`` keyword argument, they may appear out of order compared to other headers in that keyword argument.
|
||||
|
||||
If this is problematic, users should consider setting the default headers on a :class:`Session <requests.Session>` object, by setting :attr:`Session <requests.Session.headers>` to a custom ``OrderedDict``. That ordering will always be preferred.
|
||||
If this is problematic, users should consider setting the default headers on a :class:`Session <requests.Session>` object, by setting :attr:`Session.headers <requests.Session.headers>` to a custom ``OrderedDict``. That ordering will always be preferred.
|
||||
|
||||
.. _timeouts:
|
||||
|
||||
@@ -1071,7 +1095,7 @@ The **connect** timeout is the number of seconds Requests will wait for your
|
||||
client to establish a connection to a remote machine (corresponding to the
|
||||
`connect()`_) call on the socket. It's a good practice to set connect timeouts
|
||||
to slightly larger than a multiple of 3, which is the default `TCP packet
|
||||
retransmission window <https://www.hjp.at/doc/rfc/rfc2988.txt>`_.
|
||||
retransmission window <https://datatracker.ietf.org/doc/html/rfc2988>`_.
|
||||
|
||||
Once your client has connected to the server and sent the HTTP request, the
|
||||
**read** timeout is the number of seconds the client will wait for the server
|
||||
@@ -1096,4 +1120,18 @@ coffee.
|
||||
|
||||
r = requests.get('https://github.com', timeout=None)
|
||||
|
||||
.. note:: The connect timeout applies to each connection attempt to an IP address.
|
||||
If multiple addresses exist for a domain name, the underlying ``urllib3`` will
|
||||
try each address sequentially until one successfully connects.
|
||||
This may lead to an effective total connection timeout *multiple* times longer
|
||||
than the specified time, e.g. an unresponsive server having both IPv4 and IPv6
|
||||
addresses will have its perceived timeout *doubled*, so take that into account
|
||||
when setting the connection timeout.
|
||||
.. note:: Neither the connect nor read timeouts are `wall clock`_. This means
|
||||
that if you start a request, and look at the time, and then look at
|
||||
the time when the request finishes or times out, the real-world time
|
||||
may be greater than what you specified.
|
||||
|
||||
|
||||
.. _`wall clock`: https://wiki.php.net/rfc/max_execution_wall_time
|
||||
.. _`connect()`: https://linux.die.net/man/2/connect
|
||||
|
||||
@@ -44,6 +44,16 @@ set with `headers=`.
|
||||
If credentials for the hostname are found, the request is sent with HTTP Basic
|
||||
Auth.
|
||||
|
||||
Requests will search for the netrc file at `~/.netrc`, `~/_netrc`, or at the path
|
||||
specified by the `NETRC` environment variable. `~` denotes the user's home
|
||||
directory, which is `$HOME` on Unix based systems and `%USERPROFILE%` on Windows.
|
||||
|
||||
Usage of netrc file can be disabled by setting `trust_env` to `False` in the
|
||||
Requests session::
|
||||
|
||||
>>> s = requests.Session()
|
||||
>>> s.trust_env = False
|
||||
>>> s.get('https://httpbin.org/basic-auth/user/pass')
|
||||
|
||||
Digest Authentication
|
||||
---------------------
|
||||
|
||||
@@ -201,6 +201,8 @@ may better fit your use cases.
|
||||
were returned, use ``Response.raw``.
|
||||
|
||||
|
||||
.. _custom-headers:
|
||||
|
||||
Custom Headers
|
||||
--------------
|
||||
|
||||
@@ -220,6 +222,7 @@ Note: Custom headers are given less precedence than more specific sources of inf
|
||||
are specified in ``.netrc``, which in turn will be overridden by the ``auth=``
|
||||
parameter. Requests will search for the netrc file at `~/.netrc`, `~/_netrc`,
|
||||
or at the path specified by the `NETRC` environment variable.
|
||||
Check details in :ref:`netrc authentication <authentication>`.
|
||||
* Authorization headers will be removed if you get redirected off-host.
|
||||
* Proxy-Authorization headers will be overridden by proxy credentials provided in the URL.
|
||||
* Content-Length headers will be overridden when we can determine the length of the content.
|
||||
@@ -268,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
|
||||
@@ -566,6 +571,3 @@ All exceptions that Requests explicitly raises inherit from
|
||||
-----------------------
|
||||
|
||||
Ready for more? Check out the :ref:`advanced <advanced>` section.
|
||||
|
||||
|
||||
If you're on the job market, consider taking `this programming quiz <https://triplebyte.com/a/b1i2FB8/requests-docs-1>`_. A substantial donation will be made to this project, if you find a job through this platform.
|
||||
|
||||
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 |
128
pyproject.toml
128
pyproject.toml
@@ -1,13 +1,125 @@
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
src_paths = ["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 = [
|
||||
"requests",
|
||||
"tests",
|
||||
]
|
||||
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,33 +0,0 @@
|
||||
"""
|
||||
requests.hooks
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module provides the capabilities for the Requests hooks system.
|
||||
|
||||
Available hooks:
|
||||
|
||||
``response``:
|
||||
The response generated from a Request.
|
||||
"""
|
||||
HOOKS = ["response"]
|
||||
|
||||
|
||||
def default_hooks():
|
||||
return {event: [] for event in HOOKS}
|
||||
|
||||
|
||||
# TODO: response is the only one
|
||||
|
||||
|
||||
def dispatch_hook(key, hooks, hook_data, **kwargs):
|
||||
"""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:
|
||||
_hook_data = hook(hook_data, **kwargs)
|
||||
if _hook_data is not None:
|
||||
hook_data = _hook_data
|
||||
return hook_data
|
||||
@@ -1,13 +1,7 @@
|
||||
-e .[socks]
|
||||
pytest>=2.8.0,<=6.2.5
|
||||
pytest>=2.8.0,<10
|
||||
pytest-cov
|
||||
pytest-httpbin==1.0.0
|
||||
pytest-mock==2.0.0
|
||||
httpbin==0.7.0
|
||||
pytest-httpbin==2.1.0
|
||||
httpbin~=0.10.0
|
||||
trustme
|
||||
wheel
|
||||
cryptography<40.0.0; python_version <= '3.7' and platform_python_implementation == 'PyPy'
|
||||
|
||||
# Flask Stack
|
||||
Flask>1.0,<2.0
|
||||
markupsafe<2.1
|
||||
|
||||
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,<1.27
|
||||
|
||||
[flake8]
|
||||
ignore = E203, E501, W503
|
||||
per-file-ignores =
|
||||
requests/__init__.py:E402, F401
|
||||
requests/compat.py:E402, F401
|
||||
tests/compat.py:F401
|
||||
131
setup.py
131
setup.py
@@ -1,132 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from codecs import open
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
CURRENT_PYTHON = sys.version_info[:2]
|
||||
REQUIRED_PYTHON = (3, 7)
|
||||
|
||||
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.28).
|
||||
""".format(
|
||||
*(REQUIRED_PYTHON + CURRENT_PYTHON)
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
class PyTest(TestCommand):
|
||||
user_options = [("pytest-args=", "a", "Arguments to pass into py.test")]
|
||||
|
||||
def initialize_options(self):
|
||||
TestCommand.initialize_options(self)
|
||||
try:
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
self.pytest_args = ["-n", str(cpu_count()), "--boxed"]
|
||||
except (ImportError, NotImplementedError):
|
||||
self.pytest_args = ["-n", "1", "--boxed"]
|
||||
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
import pytest
|
||||
|
||||
errno = pytest.main(self.pytest_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
# '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==0.0.7",
|
||||
"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, "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={"requests": "requests"},
|
||||
include_package_data=True,
|
||||
python_requires=">=3.7",
|
||||
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.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
cmdclass={"test": PyTest},
|
||||
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,42 +81,45 @@ 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)
|
||||
# charset_normalizer >= 2.0.0 < 4.0.0
|
||||
assert (2, 0, 0) <= (major, minor, patch) < (4, 0, 0)
|
||||
else:
|
||||
raise Exception("You need either charset_normalizer or chardet installed")
|
||||
warnings.warn(
|
||||
"Unable to find acceptable character detection dependency "
|
||||
"(chardet or charset_normalizer).",
|
||||
RequestsDependencyWarning,
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -129,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:
|
||||
@@ -174,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,10 +5,10 @@
|
||||
__title__ = "requests"
|
||||
__description__ = "Python HTTP for Humans."
|
||||
__url__ = "https://requests.readthedocs.io"
|
||||
__version__ = "2.30.0"
|
||||
__build__ = 0x023000
|
||||
__version__ = "2.34.2"
|
||||
__build__ = 0x023402
|
||||
__author__ = "Kenneth Reitz"
|
||||
__author_email__ = "me@kennethreitz.org"
|
||||
__license__ = "Apache 2.0"
|
||||
__license__ = "Apache-2.0"
|
||||
__copyright__ = "Copyright Kenneth Reitz"
|
||||
__cake__ = "\u2728 \U0001f370 \u2728"
|
||||
@@ -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,20 +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
|
||||
@@ -45,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,
|
||||
@@ -54,28 +60,80 @@ 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
|
||||
DEFAULT_RETRIES = 0
|
||||
DEFAULT_POOL_TIMEOUT = None
|
||||
|
||||
|
||||
def _urllib3_request_context(
|
||||
request: PreparedRequest,
|
||||
verify: bool | str | None,
|
||||
client_cert: tuple[str, str] | str | None,
|
||||
poolmanager: PoolManager,
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
host_params: dict[str, Any] = {}
|
||||
pool_kwargs: dict[str, Any] = {}
|
||||
parsed_request_url = urlparse(request.url)
|
||||
scheme = parsed_request_url.scheme.lower()
|
||||
port = parsed_request_url.port
|
||||
|
||||
cert_reqs = "CERT_REQUIRED"
|
||||
if verify is False:
|
||||
cert_reqs = "CERT_NONE"
|
||||
elif isinstance(verify, str):
|
||||
if not os.path.isdir(verify):
|
||||
pool_kwargs["ca_certs"] = verify
|
||||
else:
|
||||
pool_kwargs["ca_cert_dir"] = verify
|
||||
pool_kwargs["cert_reqs"] = cert_reqs
|
||||
if client_cert is not None:
|
||||
if isinstance(client_cert, tuple) and len(client_cert) == 2:
|
||||
pool_kwargs["cert_file"] = client_cert[0]
|
||||
pool_kwargs["key_file"] = client_cert[1]
|
||||
else:
|
||||
# According to our docs, we allow users to specify just the client
|
||||
# cert path
|
||||
pool_kwargs["cert_file"] = client_cert
|
||||
host_params = {
|
||||
"scheme": scheme,
|
||||
"host": parsed_request_url.hostname,
|
||||
"port": port,
|
||||
}
|
||||
return host_params, pool_kwargs
|
||||
|
||||
|
||||
class BaseAdapter:
|
||||
"""The Base Transport Adapter"""
|
||||
|
||||
def __init__(self):
|
||||
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.
|
||||
@@ -92,7 +150,7 @@ class BaseAdapter:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
"""Cleans up adapter specific items."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -124,7 +182,7 @@ class HTTPAdapter(BaseAdapter):
|
||||
>>> s.mount('http://', a)
|
||||
"""
|
||||
|
||||
__attrs__ = [
|
||||
__attrs__: list[str] = [
|
||||
"max_retries",
|
||||
"config",
|
||||
"_pool_connections",
|
||||
@@ -132,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:
|
||||
@@ -154,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 = {}
|
||||
@@ -171,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
|
||||
@@ -196,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
|
||||
@@ -234,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>`.
|
||||
@@ -247,7 +319,6 @@ class HTTPAdapter(BaseAdapter):
|
||||
:param cert: The SSL certificate to verify.
|
||||
"""
|
||||
if url.lower().startswith("https") and verify:
|
||||
|
||||
cert_loc = None
|
||||
|
||||
# Allow self-specified cert location.
|
||||
@@ -255,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(
|
||||
@@ -291,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
|
||||
@@ -301,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", {}))
|
||||
@@ -328,15 +400,138 @@ class HTTPAdapter(BaseAdapter):
|
||||
|
||||
return response
|
||||
|
||||
def get_connection(self, url, proxies=None):
|
||||
"""Returns a urllib3 connection for the given URL. This should not be
|
||||
def build_connection_pool_key_attributes(
|
||||
self, request: PreparedRequest, verify: _t.VerifyType, cert: _t.CertType = None
|
||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
"""Build the PoolKey attributes used by urllib3 to return a connection.
|
||||
|
||||
This looks at the PreparedRequest, the user-specified verify value,
|
||||
and the value of the cert parameter to determine what PoolKey values
|
||||
to use to select a connection from a given urllib3 Connection Pool.
|
||||
|
||||
The SSL related pool key arguments are not consistently set. As of
|
||||
this writing, use the following to determine what keys may be in that
|
||||
dictionary:
|
||||
|
||||
* If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the
|
||||
default Requests SSL Context
|
||||
* If ``verify`` is ``False``, ``"ssl_context"`` will not be set but
|
||||
``"cert_reqs"`` will be set
|
||||
* If ``verify`` is a string, (i.e., it is a user-specified trust bundle)
|
||||
``"ca_certs"`` will be set if the string is not a directory recognized
|
||||
by :py:func:`os.path.isdir`, otherwise ``"ca_cert_dir"`` will be
|
||||
set.
|
||||
* If ``"cert"`` is specified, ``"cert_file"`` will always be set. If
|
||||
``"cert"`` is a tuple with a second item, ``"key_file"`` will also
|
||||
be present
|
||||
|
||||
To override these settings, one may subclass this class, call this
|
||||
method and use the above logic to change parameters as desired. For
|
||||
example, if one wishes to use a custom :py:class:`ssl.SSLContext` one
|
||||
must both set ``"ssl_context"`` and based on what else they require,
|
||||
alter the other keys to ensure the desired behaviour.
|
||||
|
||||
:param request:
|
||||
The PreparedRequest being sent over the connection.
|
||||
:type request:
|
||||
:class:`~requests.models.PreparedRequest`
|
||||
:param verify:
|
||||
Either a boolean, in which case it controls whether
|
||||
we verify the server's TLS certificate, or a string, in which case it
|
||||
must be a path to a CA bundle to use.
|
||||
:param cert:
|
||||
(optional) Any user-provided SSL certificate for client
|
||||
authentication (a.k.a., mTLS). This may be a string (i.e., just
|
||||
the path to a file which holds both certificate and key) or a
|
||||
tuple of length 2 with the certificate file path and key file
|
||||
path.
|
||||
:returns:
|
||||
A tuple of two dictionaries. The first is the "host parameters"
|
||||
portion of the Pool Key including scheme, hostname, and port. The
|
||||
second is a dictionary of SSLContext related parameters.
|
||||
"""
|
||||
return _urllib3_request_context(request, verify, cert, self.poolmanager)
|
||||
|
||||
def get_connection_with_tls_context(
|
||||
self,
|
||||
request: PreparedRequest,
|
||||
verify: _t.VerifyType,
|
||||
proxies: dict[str, str] | None = None,
|
||||
cert: _t.CertType = None,
|
||||
) -> HTTPConnectionPool:
|
||||
"""Returns a urllib3 connection for the given request and TLS settings.
|
||||
This should not be called from user code, and is only exposed for use
|
||||
when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
:param request:
|
||||
The :class:`PreparedRequest <PreparedRequest>` object to be sent
|
||||
over the connection.
|
||||
:param verify:
|
||||
Either a boolean, in which case it controls whether we verify the
|
||||
server's TLS certificate, or a string, in which case it must be a
|
||||
path to a CA bundle to use.
|
||||
:param proxies:
|
||||
(optional) The proxies dictionary to apply to the request.
|
||||
:param cert:
|
||||
(optional) Any user-provided SSL certificate to be used for client
|
||||
authentication (a.k.a., mTLS).
|
||||
:rtype:
|
||||
urllib3.HTTPConnectionPool
|
||||
"""
|
||||
assert _is_prepared(request)
|
||||
|
||||
proxy = select_proxy(request.url, proxies)
|
||||
try:
|
||||
host_params, pool_kwargs = self.build_connection_pool_key_attributes(
|
||||
request,
|
||||
verify,
|
||||
cert,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidURL(e, request=request)
|
||||
if proxy:
|
||||
proxy = prepend_scheme_if_needed(proxy, "http")
|
||||
proxy_url = parse_url(proxy)
|
||||
if not proxy_url.host:
|
||||
raise InvalidProxyURL(
|
||||
"Please check proxy URL. It is malformed "
|
||||
"and could be missing the host."
|
||||
)
|
||||
proxy_manager = self.proxy_manager_for(proxy)
|
||||
conn = proxy_manager.connection_from_host(
|
||||
**host_params, pool_kwargs=pool_kwargs
|
||||
)
|
||||
else:
|
||||
# Only scheme should be lower case
|
||||
conn = self.poolmanager.connection_from_host(
|
||||
**host_params, pool_kwargs=pool_kwargs
|
||||
)
|
||||
|
||||
return conn
|
||||
|
||||
def get_connection(
|
||||
self, url: str, proxies: dict[str, str] | None = None
|
||||
) -> HTTPConnectionPool:
|
||||
"""DEPRECATED: Users should move to `get_connection_with_tls_context`
|
||||
for all subclasses of HTTPAdapter using Requests>=2.32.2.
|
||||
|
||||
Returns a urllib3 connection for the given URL. This should not be
|
||||
called from user code, and is only exposed for use when subclassing the
|
||||
:class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
|
||||
|
||||
: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(
|
||||
(
|
||||
"`get_connection` has been deprecated in favor of "
|
||||
"`get_connection_with_tls_context`. Custom HTTPAdapter subclasses "
|
||||
"will need to migrate for Requests>=2.32.2. Please see "
|
||||
"https://github.com/psf/requests/pull/6710 for more details."
|
||||
),
|
||||
DeprecationWarning,
|
||||
)
|
||||
proxy = select_proxy(url, proxies)
|
||||
|
||||
if proxy:
|
||||
@@ -357,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,
|
||||
@@ -367,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
|
||||
@@ -381,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
|
||||
|
||||
@@ -391,12 +590,13 @@ class HTTPAdapter(BaseAdapter):
|
||||
using_socks_proxy = proxy_scheme.startswith("socks")
|
||||
|
||||
url = request.path_url
|
||||
|
||||
if is_proxied_http_request and not using_socks_proxy:
|
||||
url = urldefragauth(request.url)
|
||||
|
||||
return url
|
||||
|
||||
def add_headers(self, request, **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>`.
|
||||
@@ -410,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
|
||||
@@ -423,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:
|
||||
@@ -432,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.
|
||||
@@ -450,8 +656,12 @@ class HTTPAdapter(BaseAdapter):
|
||||
:rtype: requests.Response
|
||||
"""
|
||||
|
||||
assert _is_prepared(request)
|
||||
|
||||
try:
|
||||
conn = self.get_connection(request.url, proxies)
|
||||
conn = self.get_connection_with_tls_context(
|
||||
request, verify, proxies=proxies, cert=cert
|
||||
)
|
||||
except LocationValueError as e:
|
||||
raise InvalidURL(e, request=request)
|
||||
|
||||
@@ -471,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``.
|
||||
@@ -25,7 +37,7 @@ def request(method, url, **kwargs):
|
||||
:param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
|
||||
:param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
|
||||
``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
|
||||
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
|
||||
or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content_type'`` is a string
|
||||
defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
|
||||
to add for the file.
|
||||
:param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
|
||||
@@ -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,11 +286,11 @@ 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:
|
||||
|
||||
self._thread_local.num_401_calls += 1
|
||||
pat = re.compile(r"digest ", flags=re.IGNORECASE)
|
||||
self._thread_local.chal = parse_dict_header(pat.sub("", s_auth, count=1))
|
||||
@@ -268,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
|
||||
@@ -283,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
|
||||
@@ -303,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),
|
||||
@@ -311,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,12 +7,48 @@ between Python 2 and Python 3. It remains for backwards
|
||||
compatibility until the next major version.
|
||||
"""
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
import charset_normalizer as chardet
|
||||
# pyright: reportUnusedImport=false
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# -------
|
||||
# urllib3
|
||||
# -------
|
||||
from urllib3 import __version__ as urllib3_version
|
||||
|
||||
# Detect which major version of urllib3 is being used.
|
||||
try:
|
||||
is_urllib3_1 = int(urllib3_version.split(".")[0]) == 1
|
||||
except (TypeError, AttributeError):
|
||||
# If we can't discern a version, prefer old functionality.
|
||||
is_urllib3_1 = True
|
||||
|
||||
# -------------------
|
||||
# Character Detection
|
||||
# -------------------
|
||||
|
||||
|
||||
def _resolve_char_detection() -> ModuleType | None:
|
||||
"""Find supported character detection libraries."""
|
||||
chardet = None
|
||||
for lib in ("chardet", "charset_normalizer"):
|
||||
if chardet is None:
|
||||
try:
|
||||
chardet = importlib.import_module(lib)
|
||||
except ImportError:
|
||||
pass
|
||||
return chardet
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import chardet
|
||||
else:
|
||||
chardet = _resolve_char_detection()
|
||||
|
||||
# -------
|
||||
# Pythons
|
||||
@@ -30,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
|
||||
|
||||
@@ -68,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
|
||||
@@ -2,28 +2,36 @@
|
||||
requests.cookies
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Compatibility code to be able to use `cookielib.CookieJar` with requests.
|
||||
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 `cookielib.CookieJar` expects this interface in order to correctly
|
||||
The code in `http.cookiejar.CookieJar` expects this interface in order to correctly
|
||||
manage cookie policies, i.e., determine whether a cookie can be set, given the
|
||||
domains of the request and the cookie.
|
||||
|
||||
@@ -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):
|
||||
"""cookielib has no legitimate use for this method; add it back if you find one."""
|
||||
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()
|
||||
|
||||
|
||||
@@ -104,27 +115,29 @@ class MockResponse:
|
||||
"""Wraps a `httplib.HTTPMessage` to mimic a `urllib.addinfourl`.
|
||||
|
||||
...what? Basically, expose the parsed HTTP headers from the server response
|
||||
the way `cookielib` expects to see them.
|
||||
the way `http.cookiejar` expects to see them.
|
||||
"""
|
||||
|
||||
def __init__(self, headers):
|
||||
"""Make a MockResponse for `cookielib` to read.
|
||||
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: cookielib.CookieJar (not necessarily a RequestsCookieJar)
|
||||
:param jar: http.cookiejar.CookieJar (not necessarily a RequestsCookieJar)
|
||||
:param request: our own requests.Request object
|
||||
:param response: urllib3.HTTPResponse object
|
||||
"""
|
||||
@@ -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,8 +188,8 @@ class CookieConflictError(RuntimeError):
|
||||
"""
|
||||
|
||||
|
||||
class RequestsCookieJar(cookielib.CookieJar, MutableMapping):
|
||||
"""Compatibility class; is a cookielib.CookieJar, but exposes a dict
|
||||
class RequestsCookieJar(CookieJar, MutableMapping[str, str | None]): # type: ignore[misc]
|
||||
"""Compatibility class; is a http.cookiejar.CookieJar, but exposes a dict
|
||||
interface.
|
||||
|
||||
This is the CookieJar we create by default for requests and sessions that
|
||||
@@ -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):
|
||||
"""Deletes a cookie given a name. Wraps ``cookielib.CookieJar``'s
|
||||
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,6 +52,16 @@ class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError):
|
||||
CompatJSONDecodeError.__init__(self, *args)
|
||||
InvalidJSONError.__init__(self, *self.args, **kwargs)
|
||||
|
||||
def __reduce__(self) -> tuple[Any, ...] | str:
|
||||
"""
|
||||
The __reduce__ method called when pickling the object must
|
||||
be the one from the JSONDecodeError (be it json/simplejson)
|
||||
as it expects all the arguments for instantiation, not just
|
||||
one like the IOError, and the MRO would by default call the
|
||||
__reduce__ method from the IOError due to the inheritance order.
|
||||
"""
|
||||
return CompatJSONDecodeError.__reduce__(self)
|
||||
|
||||
|
||||
class HTTPError(RequestException):
|
||||
"""An HTTP error occurred."""
|
||||
@@ -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,
|
||||
48
src/requests/hooks.py
Normal file
48
src/requests/hooks.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
requests.hooks
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module provides the capabilities for the Requests hooks system.
|
||||
|
||||
Available hooks:
|
||||
|
||||
``response``:
|
||||
The response generated from a Request.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import _types as _t
|
||||
from .models import Response
|
||||
|
||||
HOOKS: list[str] = ["response"]
|
||||
|
||||
|
||||
def default_hooks() -> dict[str, list[_t.HookType]]:
|
||||
return {event: [] for event in HOOKS}
|
||||
|
||||
|
||||
# TODO: response is the only one
|
||||
|
||||
|
||||
def dispatch_hook(
|
||||
key: str,
|
||||
hooks: _t.HooksInputType | None,
|
||||
hook_data: Response,
|
||||
**kwargs: Any,
|
||||
) -> Response:
|
||||
"""Dispatches a hook dictionary on a given piece of data."""
|
||||
hooks_dict = hooks or {}
|
||||
hook_list: Iterable[_t.HookType] | _t.HookType | None = hooks_dict.get(key)
|
||||
if hook_list:
|
||||
if isinstance(hook_list, Callable):
|
||||
hook_list = [hook_list]
|
||||
for hook in hook_list:
|
||||
_hook_data = hook(hook_data, **kwargs)
|
||||
if _hook_data is not None:
|
||||
hook_data = _hook_data
|
||||
return hook_data
|
||||
@@ -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 {})
|
||||
|
||||
@@ -170,7 +218,7 @@ class RequestEncodingMixin:
|
||||
)
|
||||
)
|
||||
|
||||
for (k, v) in files:
|
||||
for k, v in files:
|
||||
# support for explicit filename
|
||||
ft = None
|
||||
fh = None
|
||||
@@ -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,20 +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
|
||||
@@ -277,7 +341,7 @@ class Request(RequestHooksMixin):
|
||||
hooks = {} if hooks is None else hooks
|
||||
|
||||
self.hooks = default_hooks()
|
||||
for (k, v) in list(hooks.items()):
|
||||
for k, v in list(hooks.items()):
|
||||
self.register_hook(event=k, hook=v)
|
||||
|
||||
self.method = method
|
||||
@@ -290,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(
|
||||
@@ -332,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
|
||||
@@ -351,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)
|
||||
@@ -377,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:
|
||||
@@ -407,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
|
||||
@@ -471,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()
|
||||
@@ -492,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.
|
||||
@@ -515,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):
|
||||
@@ -535,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
|
||||
@@ -551,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"
|
||||
@@ -568,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)
|
||||
@@ -586,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__)
|
||||
@@ -608,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
|
||||
@@ -624,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])
|
||||
|
||||
@@ -643,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",
|
||||
@@ -656,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
|
||||
@@ -675,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
|
||||
@@ -686,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({})
|
||||
@@ -701,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:
|
||||
@@ -717,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)
|
||||
|
||||
@@ -725,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
|
||||
@@ -738,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
|
||||
@@ -748,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
|
||||
@@ -768,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,
|
||||
@@ -783,16 +889,31 @@ 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."""
|
||||
return chardet.detect(self.content)["encoding"]
|
||||
if chardet is not None:
|
||||
return chardet.detect(self.content)["encoding"]
|
||||
else:
|
||||
# If no character detection library is available, we'll fall back
|
||||
# to a standard Python utf-8 str.
|
||||
return "utf-8"
|
||||
|
||||
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
|
||||
@@ -805,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:
|
||||
@@ -834,43 +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()
|
||||
|
||||
@@ -885,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:
|
||||
@@ -901,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
|
||||
@@ -929,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.
|
||||
@@ -941,8 +1088,10 @@ class Response:
|
||||
|
||||
return content
|
||||
|
||||
def json(self, **kwargs):
|
||||
r"""Returns the json-encoded content of a response, if any.
|
||||
def json(self, **kwargs: Any) -> Any:
|
||||
r"""Decodes the JSON response body (if any) as a Python object.
|
||||
|
||||
This may return a dictionary, list, etc. depending on what is in the response.
|
||||
|
||||
:param \*\*kwargs: Optional arguments that ``json.loads`` takes.
|
||||
:raises requests.exceptions.JSONDecodeError: If the response body does not
|
||||
@@ -975,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 = ""
|
||||
@@ -1020,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.
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import sys
|
||||
|
||||
try:
|
||||
import chardet
|
||||
except ImportError:
|
||||
import warnings
|
||||
|
||||
import charset_normalizer as chardet
|
||||
|
||||
warnings.filterwarnings("ignore", "Trying to detect", module="charset_normalizer")
|
||||
from .compat import chardet
|
||||
|
||||
# This code exists for backwards compatibility reasons.
|
||||
# I don't like it either. Just look the other way. :)
|
||||
@@ -20,9 +13,11 @@ for package in ("urllib3", "idna"):
|
||||
if mod == package or mod.startswith(f"{package}."):
|
||||
sys.modules[f"requests.packages.{mod}"] = sys.modules[mod]
|
||||
|
||||
target = chardet.__name__
|
||||
for mod in list(sys.modules):
|
||||
if mod == target or mod.startswith(f"{target}."):
|
||||
target = target.replace(target, "chardet")
|
||||
sys.modules[f"requests.packages.{target}"] = sys.modules[mod]
|
||||
# Kinda cool, though, right?
|
||||
if chardet is not None:
|
||||
target = chardet.__name__
|
||||
for mod in list(sys.modules):
|
||||
if mod == target or mod.startswith(f"{target}."):
|
||||
imported_mod = sys.modules[mod]
|
||||
sys.modules[f"requests.packages.{mod}"] = imported_mod
|
||||
mod = mod.replace(target, "chardet")
|
||||
sys.modules[f"requests.packages.{mod}"] = imported_mod
|
||||
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,9 +287,8 @@ 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,
|
||||
stream=stream,
|
||||
@@ -280,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"]
|
||||
@@ -300,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
|
||||
@@ -312,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)
|
||||
@@ -324,12 +360,16 @@ class SessionRedirectMixin:
|
||||
except KeyError:
|
||||
username, password = None, None
|
||||
|
||||
if username and password:
|
||||
# urllib3 handles proxy authorization for us in the standard adapter.
|
||||
# Avoid appending this to TLS tunneled requests where it may be leaked.
|
||||
if not scheme.startswith("https") and username and password:
|
||||
headers["Proxy-Authorization"] = _basic_auth_str(username, password)
|
||||
|
||||
return new_proxies
|
||||
|
||||
def rebuild_method(self, prepared_request, 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.
|
||||
"""
|
||||
@@ -371,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",
|
||||
@@ -386,8 +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.
|
||||
|
||||
@@ -535,7 +592,7 @@ class Session(SessionRedirectMixin):
|
||||
for multipart encoding upload.
|
||||
:param auth: (optional) Auth tuple or callable to enable
|
||||
Basic/Digest/Custom HTTP Auth.
|
||||
:param timeout: (optional) How long to wait for the server to send
|
||||
:param timeout: (optional) How many seconds to wait for the server to send
|
||||
data before giving up, as a float, or a :ref:`(connect timeout,
|
||||
read timeout) <timeouts>` tuple.
|
||||
:type timeout: float or tuple
|
||||
@@ -543,6 +600,8 @@ class Session(SessionRedirectMixin):
|
||||
:type allow_redirects: bool
|
||||
:param proxies: (optional) Dictionary mapping protocol or protocol and
|
||||
hostname to the URL of the proxy.
|
||||
:param hooks: (optional) Dictionary mapping hook name to one event or
|
||||
list of events, event must be callable.
|
||||
:param stream: (optional) whether to immediately download the response
|
||||
content. Defaults to ``False``.
|
||||
:param verify: (optional) Either a boolean, in which case it controls whether we verify
|
||||
@@ -557,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(),
|
||||
@@ -572,6 +634,8 @@ class Session(SessionRedirectMixin):
|
||||
)
|
||||
prep = self.prepare_request(req)
|
||||
|
||||
assert _is_prepared(prep)
|
||||
|
||||
proxies = proxies or {}
|
||||
|
||||
settings = self.merge_environment_settings(
|
||||
@@ -588,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.
|
||||
@@ -610,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.
|
||||
@@ -621,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.
|
||||
@@ -634,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.
|
||||
@@ -646,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.
|
||||
@@ -658,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.
|
||||
@@ -668,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
|
||||
@@ -686,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")
|
||||
@@ -709,7 +792,6 @@ class Session(SessionRedirectMixin):
|
||||
|
||||
# Persist cookies
|
||||
if r.history:
|
||||
|
||||
# If the hooks create history then we want those cookies too
|
||||
for resp in r.history:
|
||||
extract_cookies_to_jar(self.cookies, resp.request, resp.raw)
|
||||
@@ -735,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:
|
||||
@@ -746,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.
|
||||
|
||||
@@ -757,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.
|
||||
@@ -777,26 +867,25 @@ 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.
|
||||
|
||||
:rtype: requests.adapters.BaseAdapter
|
||||
"""
|
||||
for (prefix, adapter) in self.adapters.items():
|
||||
|
||||
for prefix, adapter in self.adapters.items():
|
||||
if url.lower().startswith(prefix.lower()):
|
||||
return adapter
|
||||
|
||||
# Nothing matches :-/
|
||||
raise InvalidSchema(f"No connection adapters were found for {url!r}")
|
||||
|
||||
def close(self):
|
||||
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.
|
||||
|
||||
@@ -24,7 +24,7 @@ _codes = {
|
||||
# Informational.
|
||||
100: ("continue",),
|
||||
101: ("switching_protocols",),
|
||||
102: ("processing",),
|
||||
102: ("processing", "early-hints"),
|
||||
103: ("checkpoint",),
|
||||
122: ("uri_too_long", "request_uri_too_long"),
|
||||
200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"),
|
||||
@@ -65,8 +65,8 @@ _codes = {
|
||||
410: ("gone",),
|
||||
411: ("length_required",),
|
||||
412: ("precondition_failed", "precondition"),
|
||||
413: ("request_entity_too_large",),
|
||||
414: ("request_uri_too_large",),
|
||||
413: ("request_entity_too_large", "content_too_large"),
|
||||
414: ("request_uri_too_large", "uri_too_long"),
|
||||
415: ("unsupported_media_type", "unsupported_media", "media_type"),
|
||||
416: (
|
||||
"requested_range_not_satisfiable",
|
||||
@@ -76,10 +76,10 @@ _codes = {
|
||||
417: ("expectation_failed",),
|
||||
418: ("im_a_teapot", "teapot", "i_am_a_teapot"),
|
||||
421: ("misdirected_request",),
|
||||
422: ("unprocessable_entity", "unprocessable"),
|
||||
422: ("unprocessable_entity", "unprocessable", "unprocessable_content"),
|
||||
423: ("locked",),
|
||||
424: ("failed_dependency", "dependency"),
|
||||
425: ("unordered_collection", "unordered"),
|
||||
425: ("unordered_collection", "unordered", "too_early"),
|
||||
426: ("upgrade_required", "upgrade"),
|
||||
428: ("precondition_required", "precondition"),
|
||||
429: ("too_many_requests", "too_many"),
|
||||
@@ -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,24 +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,
|
||||
)
|
||||
from .compat import parse_http_list as _parse_list_header
|
||||
from .compat import (
|
||||
is_urllib3_1,
|
||||
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,
|
||||
@@ -58,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"])
|
||||
)
|
||||
|
||||
@@ -73,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:
|
||||
@@ -97,6 +120,8 @@ if sys.platform == "win32":
|
||||
# '<local>' string by the localhost entry and the corresponding
|
||||
# canonical entry.
|
||||
proxyOverride = proxyOverride.split(";")
|
||||
# filter out empty strings to avoid re.match return true in the following code.
|
||||
proxyOverride = filter(None, proxyOverride)
|
||||
# now check if we match one of the registry values.
|
||||
for test in proxyOverride:
|
||||
if test == "<local>":
|
||||
@@ -109,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,
|
||||
@@ -121,19 +146,26 @@ 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
|
||||
|
||||
if not is_urllib3_1 and isinstance(o, str):
|
||||
# urllib3 2.x+ treats all strings as utf-8 instead
|
||||
# of latin-1 (iso-8859-1) like http.client.
|
||||
o = o.encode("utf-8")
|
||||
|
||||
if hasattr(o, "__len__"):
|
||||
total_length = len(o)
|
||||
|
||||
@@ -196,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,)
|
||||
@@ -211,14 +248,7 @@ def get_netrc_auth(url, raise_errors=False):
|
||||
netrc_path = None
|
||||
|
||||
for f in netrc_locations:
|
||||
try:
|
||||
loc = os.path.expanduser(f)
|
||||
except KeyError:
|
||||
# os.path.expanduser can fail when $HOME is undefined and
|
||||
# getpwuid fails. See https://bugs.python.org/issue20164 &
|
||||
# https://github.com/psf/requests/issues/1846
|
||||
return
|
||||
|
||||
loc = os.path.expanduser(f)
|
||||
if os.path.exists(loc):
|
||||
netrc_path = loc
|
||||
break
|
||||
@@ -228,20 +258,17 @@ def get_netrc_auth(url, raise_errors=False):
|
||||
return
|
||||
|
||||
ri = urlparse(url)
|
||||
host = ri.hostname
|
||||
|
||||
# Strip port numbers from netloc. This weird `if...encode`` dance is
|
||||
# used for Python 3.2, which doesn't support unicode literals.
|
||||
splitstr = b":"
|
||||
if isinstance(url, str):
|
||||
splitstr = splitstr.decode("ascii")
|
||||
host = ri.netloc.split(splitstr)[0]
|
||||
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.
|
||||
@@ -253,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.
|
||||
@@ -288,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:
|
||||
@@ -310,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.,
|
||||
@@ -337,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.,
|
||||
|
||||
@@ -360,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
|
||||
@@ -390,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])
|
||||
@@ -399,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:
|
||||
|
||||
@@ -421,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
|
||||
@@ -434,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.
|
||||
@@ -459,22 +497,18 @@ 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.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
cookie_dict = {}
|
||||
|
||||
for cookie in cj:
|
||||
cookie_dict[cookie.name] = cookie.value
|
||||
|
||||
cookie_dict = {cookie.name: cookie.value for cookie in 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.
|
||||
@@ -485,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.
|
||||
@@ -510,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.
|
||||
@@ -560,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:
|
||||
@@ -577,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:
|
||||
@@ -587,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.
|
||||
@@ -607,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)
|
||||
@@ -621,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.
|
||||
|
||||
@@ -656,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
|
||||
@@ -678,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
|
||||
@@ -693,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
|
||||
@@ -704,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
|
||||
"""
|
||||
@@ -715,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.
|
||||
|
||||
@@ -740,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
|
||||
@@ -748,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
|
||||
@@ -761,15 +807,16 @@ 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.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
# 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
|
||||
@@ -778,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
|
||||
|
||||
@@ -821,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.
|
||||
|
||||
@@ -833,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
|
||||
@@ -859,10 +908,14 @@ 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 a NO_PROXY to strip proxy configurations.
|
||||
such as NO_PROXY to strip proxy configurations.
|
||||
|
||||
:param request: Request or PreparedRequest
|
||||
:param proxies: A dictionary of schemes or schemes and hosts to proxy URLs
|
||||
@@ -871,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()
|
||||
@@ -886,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.
|
||||
|
||||
@@ -895,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
|
||||
"""
|
||||
@@ -909,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"
|
||||
@@ -917,7 +970,7 @@ def parse_header_links(value):
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
links = []
|
||||
links: list[dict[str, str]] = []
|
||||
|
||||
replace_chars = " '\""
|
||||
|
||||
@@ -931,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:
|
||||
@@ -952,7 +1005,7 @@ _null2 = _null * 2
|
||||
_null3 = _null * 3
|
||||
|
||||
|
||||
def guess_json_utf(data):
|
||||
def guess_json_utf(data: bytes) -> str | None:
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
@@ -984,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
|
||||
@@ -1004,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
|
||||
@@ -1013,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.
|
||||
|
||||
@@ -1022,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.
|
||||
|
||||
@@ -1040,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(
|
||||
@@ -1051,21 +1111,21 @@ 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"
|
||||
f"Invalid leading whitespace, reserved character(s), or return "
|
||||
f"character(s) in header {header_kind}: {header_part!r}"
|
||||
)
|
||||
|
||||
|
||||
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:
|
||||
@@ -1076,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)
|
||||
10
tests/certs/README.md
Normal file
10
tests/certs/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Testing Certificates
|
||||
|
||||
This is a collection of certificates useful for testing aspects of Requests'
|
||||
behaviour.
|
||||
|
||||
The certificates include:
|
||||
|
||||
* [expired](./expired) server certificate with a valid certificate authority
|
||||
* [mtls](./mtls) provides a valid client certificate with a 2 year validity
|
||||
* [valid](./valid) has a valid server certificate
|
||||
13
tests/certs/expired/Makefile
Normal file
13
tests/certs/expired/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
.PHONY: all clean ca server
|
||||
|
||||
ca:
|
||||
make -C $@ all
|
||||
|
||||
server:
|
||||
make -C $@ all
|
||||
|
||||
all: ca server
|
||||
|
||||
clean:
|
||||
make -C ca clean
|
||||
make -C server clean
|
||||
11
tests/certs/expired/README.md
Normal file
11
tests/certs/expired/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Expired Certificates and Configuration for Testing
|
||||
|
||||
This has a valid certificate authority in [ca](./ca) and an invalid server
|
||||
certificate in [server](./server).
|
||||
|
||||
This can all be regenerated with:
|
||||
|
||||
```
|
||||
make clean
|
||||
make all
|
||||
```
|
||||
13
tests/certs/expired/ca/Makefile
Normal file
13
tests/certs/expired/ca/Makefile
Normal file
@@ -0,0 +1,13 @@
|
||||
.PHONY: all clean
|
||||
|
||||
root_files = ca-private.key ca.crt
|
||||
|
||||
ca-private.key:
|
||||
openssl genrsa -out ca-private.key 2048
|
||||
|
||||
all: ca-private.key
|
||||
openssl req -x509 -sha256 -days 7300 -key ca-private.key -out ca.crt -config ca.cnf
|
||||
ln -s ca.crt cacert.pem
|
||||
|
||||
clean:
|
||||
rm -f cacert.pem ca.crt ca-private.key *.csr
|
||||
28
tests/certs/expired/ca/ca-private.key
Normal file
28
tests/certs/expired/ca/ca-private.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCfZUh82dF/r9GW
|
||||
89IN2vqOiMMuikIAO3SEI3+uSGCdWT13C+NnrFJ7XF/D6UGy1mvm8KfhSnapWoAk
|
||||
toyPXSc/GNzJzCwZool7xE+rm/0vbu1XbUcQcqB8nQRLzTChDIGuuD8DHs7bmen1
|
||||
9sT5kZy0CIqac383cQxR8W1Fs48xEBJfuBBmyl+bz4ugPci96H4DIuAD2QvP2KKg
|
||||
Gqs4yyDPSmf86k9+okOsLMQVwGnHety+TPJawCn2aCXl+rmMTSCH2sUEc81cXaVQ
|
||||
Yxyf6HaqGncCs1O2MzeRbPugEzb5K4ZVM4NYtDMkxrQnZFCALf4XOma1uv5Kh6Qo
|
||||
FMFHOA6tAgMBAAECgf9YadXLawbJzLx0/smE5fIVHccmCYqSlmgK46XvBjaREO8H
|
||||
GZEJ8IvP4W09PiUzDbzMXLDCRouLZKevtZJB82nRlfjh9l5/2aho/nsytVO6+8yq
|
||||
sfK5LNvYQ0Aey7ItosJMJ+bL1ErphHZB+J9Jz3scYaCAC5CFMC+lREVYZEEI9QD4
|
||||
P2D5QbmaSeu8jmL/H3fWHjNXWDprue3W/MIf96NZa3qJew45go4TAYYMe5i757KW
|
||||
Ja40VNfmsgbz4uI9oDXaYL/NkWUaQP1lnh+Mfrm1YnBe2wsLcP/WuM5h0bYzJW/1
|
||||
ZeSrZM3fqCMW6SJyrVE1qzqvtw1xQBlrq0B6q0ECgYEA0fi4+ySFGR+mL6k5UjP1
|
||||
roREqQgKaLgdhOvD88EnO93Nl6tJ3Qk8LyzPUNbxe1/xTUEKMtglBKOoxCHJJZlg
|
||||
xXnKBAQUtlmrLFKIGe+UCD+r+wfSpS6Sl7BUDmeCSczG9dPN5vnyZA4ixUke2SCC
|
||||
k4Eb9Q0AHyNnbXv928r0sfkCgYEAwlZRYmGTVva6cY2YEmMrqbWy4Wxm2Zmdo+Uq
|
||||
Xu1RZF9a3tGzNbGsyYdeLNY7vVZoVOm1paMJCA8ScNLFtCux2jEPqwqd1OZ8OLhA
|
||||
1VF3/kYtUSdqwLzWoS1RdD6mZCAHeOE+N0pone4lt3A2o8wtpHsaDA+XSTw2rHLR
|
||||
LVS+b1UCgYEAtezJ4Ze31pfMdrkpmCa69JVXpBj6Y9c6hGN+aWFuq/k22/WmTuRk
|
||||
h/9MNR+3JQ1w1l3HB1ytXkKqxBz92hz1csReG3Kpu4EfxYxQriAdY7Q/P4Z8pXAf
|
||||
xVwayEw439aUgIQef8UKllSFHeiH2NrJKCKSZZT5CQG06HCo+Fn1/4kCgYAYuwtY
|
||||
TbqGUpefY7l6fYxM6IZ/EWB1SIs7FCq0MdctwsS5nk4EAzxN2SAu7IRlr91PEP7A
|
||||
uWKo1+Is4WWva/ASKDQqPAuh0EL2pNv7SYbPoPabYTzAkkdt82puNJrQGxNYWrGk
|
||||
L5/omSnLkkghyBX23IOQDVvfQf5jK6la73HckQKBgAI+iLECAkle9HvnJ3flicau
|
||||
9FAU1/9pOdM+WogSanhYQ/P2rAwRiyCIkqu62/OoZR5g4kLxWqOOmVvsK3j+gs5F
|
||||
FtwN7gauq06MAHnWr6qC8ZltzMsGZTVDvqSH2vgV4T1V6ovVpTBPKQ1gWtABEmpm
|
||||
dyfeA6HHeRAHx8VRGpL6
|
||||
-----END PRIVATE KEY-----
|
||||
17
tests/certs/expired/ca/ca.cnf
Normal file
17
tests/certs/expired/ca/ca.cnf
Normal file
@@ -0,0 +1,17 @@
|
||||
[req]
|
||||
default_bits = 2048
|
||||
prompt = no
|
||||
default_md = sha256
|
||||
encrypt_key = no
|
||||
distinguished_name = dn
|
||||
x509_extensions = v3_ca
|
||||
|
||||
[dn]
|
||||
C = US # country code
|
||||
O = Python Software Foundation # organization
|
||||
OU = python-requests # organization unit/department
|
||||
CN = Self-Signed Root CA # common name / your cert name
|
||||
|
||||
[v3_ca]
|
||||
basicConstraints = critical, CA:true
|
||||
keyUsage = critical, cRLSign, digitalSignature, keyCertSign
|
||||
22
tests/certs/expired/ca/ca.crt
Normal file
22
tests/certs/expired/ca/ca.crt
Normal file
@@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpDCCAoygAwIBAgIUQt0yyZmppkHKNx4aXRrmD5tvjbswDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMzI5MTM1MTQ1WhcNNDUwMzI0MTM1MTQ1WjBq
|
||||
MQswCQYDVQQGEwJVUzEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRp
|
||||
b24xGDAWBgNVBAsMD3B5dGhvbi1yZXF1ZXN0czEcMBoGA1UEAwwTU2VsZi1TaWdu
|
||||
ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ9lSHzZ
|
||||
0X+v0Zbz0g3a+o6Iwy6KQgA7dIQjf65IYJ1ZPXcL42esUntcX8PpQbLWa+bwp+FK
|
||||
dqlagCS2jI9dJz8Y3MnMLBmiiXvET6ub/S9u7VdtRxByoHydBEvNMKEMga64PwMe
|
||||
ztuZ6fX2xPmRnLQIippzfzdxDFHxbUWzjzEQEl+4EGbKX5vPi6A9yL3ofgMi4APZ
|
||||
C8/YoqAaqzjLIM9KZ/zqT36iQ6wsxBXAacd63L5M8lrAKfZoJeX6uYxNIIfaxQRz
|
||||
zVxdpVBjHJ/odqoadwKzU7YzN5Fs+6ATNvkrhlUzg1i0MyTGtCdkUIAt/hc6ZrW6
|
||||
/kqHpCgUwUc4Dq0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
|
||||
BAMCAYYwHQYDVR0OBBYEFAhGiD3+10LBrdMW3+j/ceXMXSqcMA0GCSqGSIb3DQEB
|
||||
CwUAA4IBAQBRT21cyZ0Jx0JLA2ilYTLvpMsSryGyWrCOXlmRlBt1MAhmxdTRgCmu
|
||||
UB3UU2pfnrC16UeMVVS411lhzjowFXkXrjAqSUBRcetUIYHlpnGgDdUl4dV/X5kx
|
||||
HxD9VUBx/QwGeyzFhjzjeN89M2v9kPnhU/kkVfcsafwYiHdC6pwN6zeZNz7JP+GS
|
||||
rmI+KVpm5C+Nz6ekm3TR8rFgPIsiDTbY3qj/DNYX2+NhpU1DZfm687vhOr3Ekljx
|
||||
NHNu9++STEjGpirrI8EqQnK+FP2fRJ5D82YZM0d++8tmHKpY0+FRCr8//459sgun
|
||||
CojmhIobDa2NuF81Jx6Cc7lagCPG3/Ts
|
||||
-----END CERTIFICATE-----
|
||||
1
tests/certs/expired/ca/ca.srl
Normal file
1
tests/certs/expired/ca/ca.srl
Normal file
@@ -0,0 +1 @@
|
||||
4F36C3A7E075BA6452D10EEB81E7F189FF489B83
|
||||
16
tests/certs/expired/server/Makefile
Normal file
16
tests/certs/expired/server/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: all clean
|
||||
|
||||
server.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
|
||||
server.csr: server.key
|
||||
openssl req -key $< -new -out $@ -config cert.cnf
|
||||
|
||||
server.pem: server.csr
|
||||
openssl x509 -req -CA ../ca/ca.crt -CAkey ../ca/ca-private.key -in server.csr -outform PEM -out server.pem -days 0 -CAcreateserial
|
||||
openssl x509 -in ../ca/ca.crt -outform PEM >> $@
|
||||
|
||||
all: server.pem
|
||||
|
||||
clean:
|
||||
rm -f server.*
|
||||
24
tests/certs/expired/server/cert.cnf
Normal file
24
tests/certs/expired/server/cert.cnf
Normal file
@@ -0,0 +1,24 @@
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt=no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = US
|
||||
ST = DE
|
||||
O = Python Software Foundation
|
||||
OU = python-requests
|
||||
CN = localhost
|
||||
|
||||
[v3_req]
|
||||
# Extensions to add to a certificate request
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = *.localhost
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
IP.2 = ::1
|
||||
19
tests/certs/expired/server/server.csr
Normal file
19
tests/certs/expired/server/server.csr
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIDHjCCAgYCAQAwbTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkRFMSMwIQYDVQQK
|
||||
DBpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEYMBYGA1UECwwPcHl0aG9uLXJl
|
||||
cXVlc3RzMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDcRRwk8IU1YoNu8CHzB5Vh8HP/yLfBtU69LLZq+7rDG31JlR5s
|
||||
lmcwLLoZ8opUQ5rg8JMRZ7toh5zB4Uc0B4Sg8RhQMSOZYBIJkXdHuqQkciR0vWnN
|
||||
vD/5CkWEhnj4dxE7xTbDufBlxmwAthC/u72UIsZHavAyLCBqsONa7xuTiogz/d+3
|
||||
G+525JrfVr05hhJpT4Ypx5YY+ABkIOuOk/XuudbGm5SquuX6BgjmgaGtDjAuE/Rn
|
||||
BnliIavCDrG0v3KGbG3Xxt7lFTu+98foForwGCbbQBraty27oZrzAtLltfIHlJRn
|
||||
jPz0JA9akNDhAihxEsTUhk2d7jFszsd0Ev7DAgMBAAGgbDBqBgkqhkiG9w0BCQ4x
|
||||
XTBbMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMB
|
||||
MCwGA1UdEQQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATAN
|
||||
BgkqhkiG9w0BAQsFAAOCAQEAVVIxJlDrgeG0bOSufgVVRqDQx7XSd15mGlT+CynM
|
||||
lEFJ3Q9k98T2vRNNGVYYYZAnbdSOW9ACwWGcYm2bzIjbgZV0H2Kz0dLD/GrNuEY+
|
||||
O9j6K2toFKc57G7UUkve+N74ldq+hkR4zbb6FQmTlnL2YaPp2dv5TxdMKfHEfPNf
|
||||
Bg8xpbXdoRc7CYW1ZACme+d2U063GVqQsrIfwGJ+BtE6aNo62T/oEm+G4Wy5iBay
|
||||
jNv/imwf+JKQ75bTvha9YLUg2scqdYwJj8JlBw7cvkBIHW8GydA3fX4dtV9YBbFi
|
||||
8RTlWhhLgCXpYbLoDGOqF6f/MuPSIGkV1wVhCUfYA+p+Gw==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
28
tests/certs/expired/server/server.key
Normal file
28
tests/certs/expired/server/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcRRwk8IU1YoNu
|
||||
8CHzB5Vh8HP/yLfBtU69LLZq+7rDG31JlR5slmcwLLoZ8opUQ5rg8JMRZ7toh5zB
|
||||
4Uc0B4Sg8RhQMSOZYBIJkXdHuqQkciR0vWnNvD/5CkWEhnj4dxE7xTbDufBlxmwA
|
||||
thC/u72UIsZHavAyLCBqsONa7xuTiogz/d+3G+525JrfVr05hhJpT4Ypx5YY+ABk
|
||||
IOuOk/XuudbGm5SquuX6BgjmgaGtDjAuE/RnBnliIavCDrG0v3KGbG3Xxt7lFTu+
|
||||
98foForwGCbbQBraty27oZrzAtLltfIHlJRnjPz0JA9akNDhAihxEsTUhk2d7jFs
|
||||
zsd0Ev7DAgMBAAECggEAUZjCX8a/ufJ3+OE42lQdWO4fsonS1I3LENYe70u4OC2X
|
||||
QGpenmAq8pQnDpSj/KocliZYfLKeII9YGRRQcaw1S/9z/8TsSJVnqSa7dpVj1+J2
|
||||
sc43AxEw65sL/Jdp+bT169vXOTNIpBMYkDzhwH0WMemd5Pfu6c8h5RQI7Pc1knYn
|
||||
nPNY848qSYCWOUjZS3QmBik/gp9X//yxVCyvxB3xVnb1cpvc952D90Va6nFIWfgN
|
||||
ix4NgFgAvwIxCFpWI2z7JF8uBdAHPeFAx8pFukQpAzwhaEILlgt3WbvEob9CsdP5
|
||||
E39SUkzxiIfVM1du+hRquk9SJ/X56OSLnEjxLarSIQKBgQDyVU00yrv7lG7TJLZ5
|
||||
YyfE20A7eUtYdcfatTKfsxKO4KVV+p0mGsNnFhhKziuT5/xtVunbUNFEOaUfmTUP
|
||||
dCjy4XkDbE/ufxzIm1WTSqknUluVoJRWKmLI5gbDFxu/XGRQNLxQbMK54l3N69PT
|
||||
EO4kz/jqBYbd4aEmtaSx2R8JowKBgQDosUTgBGDxDE2ODPVoXc+YjReOmVHvGMKK
|
||||
tA+KDBEs4spxaqhH0OFh6mewx3jA4VHbcIgM50STXrq1VL/QqYclDlY9/VWzkmqp
|
||||
2Ekc4NoAl3H022pgcbmXx3qapC4Z3hokNFOlbtD9xQf9NMx6c5djTKMYTBrBBWXH
|
||||
oFhSz6PIYQKBgQCMGqkydmvMffq89CLTd3JMq/4s5GmdUSsk1VHZZuy50kOEvAoT
|
||||
N7H1bZ7J0Pz83Eji5jb6Z3U1nqZK6Ib20k/CbH1Mb1ifKLp5eOU27Rly9Hiiv15D
|
||||
munWALe0Hy4Zqs8MWBDv5pGGasuU/F1RUB5/BgaBNoTMz2AeQzJe6Iq7RQKBgCca
|
||||
Ku3OLpAzNhEp4k9wfEMxaoT/BMK+EWsHiRj0oCo/zi8y8iZnVoiCwHv3eTZIZt4O
|
||||
Uf6BGof9QjjYjgc9hcVXXGy8Vpt/fkceXmLo8hlpWbAA8yZT1hFIZzT3Y/va09/D
|
||||
n07MiXgrlQUay0XEiOsZ5Mpfd5t6EblzG4SG+gnhAoGAZ+shbacxhji7cUUYJLrO
|
||||
9uZJCDCZiyq8yZ+lMzX1kILQwaP6VmSvF4TzKrkrCuD2HzUYcqebko/TvckeTp/a
|
||||
oYC2put3zt0CHBf/keeeJwjhff19qVyE9mpZwoo7PuS5zmM7pQOLxzCyAM9MdsCz
|
||||
kmnbborcfh74fkfRcwXm6G8=
|
||||
-----END PRIVATE KEY-----
|
||||
44
tests/certs/expired/server/server.pem
Normal file
44
tests/certs/expired/server/server.pem
Normal file
@@ -0,0 +1,44 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpzCCAo+gAwIBAgIUTzbDp+B1umRS0Q7rgefxif9Im4EwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIyWhcNMjUwMjE3MDAzODIyWjBt
|
||||
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCREUxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0
|
||||
d2FyZSBGb3VuZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxF
|
||||
HCTwhTVig27wIfMHlWHwc//It8G1Tr0stmr7usMbfUmVHmyWZzAsuhnyilRDmuDw
|
||||
kxFnu2iHnMHhRzQHhKDxGFAxI5lgEgmRd0e6pCRyJHS9ac28P/kKRYSGePh3ETvF
|
||||
NsO58GXGbAC2EL+7vZQixkdq8DIsIGqw41rvG5OKiDP937cb7nbkmt9WvTmGEmlP
|
||||
hinHlhj4AGQg646T9e651sablKq65foGCOaBoa0OMC4T9GcGeWIhq8IOsbS/coZs
|
||||
bdfG3uUVO773x+gWivAYJttAGtq3LbuhmvMC0uW18geUlGeM/PQkD1qQ0OECKHES
|
||||
xNSGTZ3uMWzOx3QS/sMCAwEAAaNCMEAwHQYDVR0OBBYEFFK1WmMqRSzUD2isk8TL
|
||||
M3JfsVczMB8GA1UdIwQYMBaAFAhGiD3+10LBrdMW3+j/ceXMXSqcMA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQCekjxplL/AI32WtODgw7FpTXNXdyNy8PWEhn3ufL0MqiyseYOZ
|
||||
bIa0PAecsArlKs5hXJzB7p/hu5CZdvaCButw1jyWQBySCpeJXn3FmGdTkBvhwBHv
|
||||
y6npmBoy/nbLkIRNRcoLbALlfn/0iGWfmDTRblT7vRNWJmZCZCTA/+ILXJ36ItbF
|
||||
3JCs3ARF6XWORuZs5Y8cNloOy2brAC/4EHnTfOZBQf8cL8CfHlcNa+nbG1j+wVNO
|
||||
60e/0v9zTxa2wNzdnLBCW4rqJFJ44aGClxat5tWuypv0snA/0xrqIYWTQGXZPVT6
|
||||
rET47dfVbj1QxmW3sAyuy5PskZA9T7pOhcjR
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIUG/CTOPIQbH2BI36TyThUChQyR8wwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIyWhcNNDUwMjEyMDAzODIyWjBq
|
||||
MQswCQYDVQQGEwJVUzEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRp
|
||||
b24xGDAWBgNVBAsMD3B5dGhvbi1yZXF1ZXN0czEcMBoGA1UEAwwTU2VsZi1TaWdu
|
||||
ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ9lSHzZ
|
||||
0X+v0Zbz0g3a+o6Iwy6KQgA7dIQjf65IYJ1ZPXcL42esUntcX8PpQbLWa+bwp+FK
|
||||
dqlagCS2jI9dJz8Y3MnMLBmiiXvET6ub/S9u7VdtRxByoHydBEvNMKEMga64PwMe
|
||||
ztuZ6fX2xPmRnLQIippzfzdxDFHxbUWzjzEQEl+4EGbKX5vPi6A9yL3ofgMi4APZ
|
||||
C8/YoqAaqzjLIM9KZ/zqT36iQ6wsxBXAacd63L5M8lrAKfZoJeX6uYxNIIfaxQRz
|
||||
zVxdpVBjHJ/odqoadwKzU7YzN5Fs+6ATNvkrhlUzg1i0MyTGtCdkUIAt/hc6ZrW6
|
||||
/kqHpCgUwUc4Dq0CAwEAAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
|
||||
CEaIPf7XQsGt0xbf6P9x5cxdKpwwDQYJKoZIhvcNAQELBQADggEBAHMgyQNA3DQG
|
||||
0l9eX8RMl4YNwhAXChU/wOTcvD+F4OsJcPqzy6QHFh1AbBBGOI9/HN7du+EiKwGZ
|
||||
wE+69FEsSbhSv22XidPm+kHPTguWl1eKveeWrrT5MPl9F48s14lKb/8yMEa1/ryG
|
||||
Iu8NQ6ZL91JbTXdkLoBDya9HZqyXjwcGkXLE8fSqTibJ7EhWS5Q3Ic7WPgUoGAum
|
||||
b5ygoxqhm+SEyXC2/LAktwmFawkv1SsMeYpT790VIFqJ/TVVnUl+gQ2RjSEl2WLb
|
||||
UO4Hwq4FZbWF9NrY6JVThLmbcr8eW6+UxWfiXHLw/qTRre4/3367QAUQRt7EuEsb
|
||||
KOWpOS3fbsI=
|
||||
-----END CERTIFICATE-----
|
||||
7
tests/certs/mtls/Makefile
Normal file
7
tests/certs/mtls/Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
.PHONY: all clean
|
||||
|
||||
all:
|
||||
make -C client all
|
||||
|
||||
clean:
|
||||
make -C client clean
|
||||
4
tests/certs/mtls/README.md
Normal file
4
tests/certs/mtls/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Certificate Examples for mTLS
|
||||
|
||||
This has some generated certificates for mTLS utilization. The idea is to be
|
||||
able to have testing around how Requests handles client certificates.
|
||||
16
tests/certs/mtls/client/Makefile
Normal file
16
tests/certs/mtls/client/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: all clean
|
||||
|
||||
client.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
|
||||
client.csr: client.key
|
||||
openssl req -key $< -new -out $@ -config cert.cnf
|
||||
|
||||
client.pem: client.csr
|
||||
openssl x509 -req -CA ./ca/ca.crt -CAkey ./ca/ca-private.key -in client.csr -outform PEM -out client.pem -days 730 -CAcreateserial
|
||||
openssl x509 -in ./ca/ca.crt -outform PEM >> $@
|
||||
|
||||
all: client.pem
|
||||
|
||||
clean:
|
||||
rm -f client.*
|
||||
1
tests/certs/mtls/client/ca
Symbolic link
1
tests/certs/mtls/client/ca
Symbolic link
@@ -0,0 +1 @@
|
||||
../../expired/ca/
|
||||
26
tests/certs/mtls/client/cert.cnf
Normal file
26
tests/certs/mtls/client/cert.cnf
Normal file
@@ -0,0 +1,26 @@
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt=no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = US
|
||||
ST = DE
|
||||
O = Python Software Foundation
|
||||
OU = python-requests
|
||||
CN = requests
|
||||
|
||||
[v3_req]
|
||||
# Extensions to add to a certificate request
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = *.localhost
|
||||
IP.1 = 127.0.0.1
|
||||
IP.2 = ::1
|
||||
URI.1 = spiffe://trust.python.org/v0/maintainer/sigmavirus24/project/requests/org/psf
|
||||
URI.2 = spiffe://trust.python.org/v1/maintainer:sigmavirus24/project:requests/org:psf
|
||||
URI.3 = spiffe://trust.python.org/v1/maintainer=sigmavirus24/project=requests/org=psf
|
||||
24
tests/certs/mtls/client/client.csr
Normal file
24
tests/certs/mtls/client/client.csr
Normal file
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIEGjCCAwICAQAwbDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkRFMSMwIQYDVQQK
|
||||
DBpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEYMBYGA1UECwwPcHl0aG9uLXJl
|
||||
cXVlc3RzMREwDwYDVQQDDAhyZXF1ZXN0czCCASIwDQYJKoZIhvcNAQEBBQADggEP
|
||||
ADCCAQoCggEBAJrdvu5kvCy5g6w67gczETk/u6K0UBmf6Mv0lqXCp3Voyt5fv6SI
|
||||
/OPWrFV1+m4imNe5bLM7JUoUfkqkmULsjTTh7sxVLjW226vLSYOWWy7PA+OSwUTN
|
||||
LjydF1dazlPedHPYdmhVShCmyoOd1pUCDQn0/4cEA/WW4mtzZImmoCf/pyAM3XAC
|
||||
9RBcSSJRywOTe6n9LY6Ko0YUW94ay2M4ClVblDxswDemaAsLFuciKmi53gKx4H/l
|
||||
areo8p60dgubooiMbcc4E9bzp0oJpfh7xhwKeJtCNEpOik1AiiQIZtwqmkkIHvtI
|
||||
Go3SZ7WAQU9Eh2r+u3E6aSl+N0PMXK4Y4JsCAwEAAaCCAWcwggFjBgkqhkiG9w0B
|
||||
CQ4xggFUMIIBUDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDAjCCAR8GA1UdEQSCARYwggESggsqLmxvY2FsaG9zdIcEfwAAAYcQAAAA
|
||||
AAAAAAAAAAAAAAAAAYZNc3BpZmZlOi8vdHJ1c3QucHl0aG9uLm9yZy92MC9tYWlu
|
||||
dGFpbmVyL3NpZ21hdmlydXMyNC9wcm9qZWN0L3JlcXVlc3RzL29yZy9wc2aGTXNw
|
||||
aWZmZTovL3RydXN0LnB5dGhvbi5vcmcvdjEvbWFpbnRhaW5lcjpzaWdtYXZpcnVz
|
||||
MjQvcHJvamVjdDpyZXF1ZXN0cy9vcmc6cHNmhk1zcGlmZmU6Ly90cnVzdC5weXRo
|
||||
b24ub3JnL3YxL21haW50YWluZXI9c2lnbWF2aXJ1czI0L3Byb2plY3Q9cmVxdWVz
|
||||
dHMvb3JnPXBzZjANBgkqhkiG9w0BAQsFAAOCAQEAMOwYPyq+OpMRQUAgoRjfxTQO
|
||||
DiqfBzCsjUsPAacLTtebbWBx8y6TkLSb+/Qn3amq3ESo4iBqKpmVwdlAS4P486GD
|
||||
9f78W3zkZ29jGcnQ+XHb7WvPvzBRoXImE276F9JGqJ+9q39Cbxzh0U2+ofBx2iGY
|
||||
sSutzU0B/l/FKZRc8thuFoeKqHwVePLGD9p2+2nYI9I08QoGqEokTcFAq0tZ858F
|
||||
9PdxBZYOKOMpnLZhiJ8qZo23v3ycBXPOjg5ILtQ9EzHoNEA5Mxx/mfNLKJ0NktZD
|
||||
KXANLWKbXm+w9gTcBLtCPWNeqj5DIGPsHSfq/Bmjbp/o+uOJs6oq3s5rA7WrAA==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
28
tests/certs/mtls/client/client.key
Normal file
28
tests/certs/mtls/client/client.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCa3b7uZLwsuYOs
|
||||
Ou4HMxE5P7uitFAZn+jL9Jalwqd1aMreX7+kiPzj1qxVdfpuIpjXuWyzOyVKFH5K
|
||||
pJlC7I004e7MVS41ttury0mDllsuzwPjksFEzS48nRdXWs5T3nRz2HZoVUoQpsqD
|
||||
ndaVAg0J9P+HBAP1luJrc2SJpqAn/6cgDN1wAvUQXEkiUcsDk3up/S2OiqNGFFve
|
||||
GstjOApVW5Q8bMA3pmgLCxbnIipoud4CseB/5Wq3qPKetHYLm6KIjG3HOBPW86dK
|
||||
CaX4e8YcCnibQjRKTopNQIokCGbcKppJCB77SBqN0me1gEFPRIdq/rtxOmkpfjdD
|
||||
zFyuGOCbAgMBAAECggEABHa5xyNOLTfXrMIyFDELoQvOO71YxbRPQHm3UeXPb9nq
|
||||
Zwh5fKOaLnMEmp4A7uW+ZBFrKatdwsnebgZaiIxK8ahFesxFvbScllIQt2NBE5NR
|
||||
+GBFg9cqKwMYJiNu6Qnzb1dg6lbzAJHeKncFNVxOxeey6dBa0NxdgF1eG32bBiwT
|
||||
yIr6JO7cK5JfcExls5yxdZMZiW08quaeFMQR6Wocod2caDgJUBVbZwivEoNI3ak4
|
||||
/kzQaxvRoDMM8uN3LrVH2YUJgURUxSbpOLu8ycZtfg8JRArprUYI0P0OD6iDlly1
|
||||
3FhJXqBEY0dWQPmZKP/Elt3ywC1oncW+AqPh9cchiQKBgQDMriOKoBrTmplw0kbo
|
||||
EEuxTU1A23BqcZfn2+PoUww6UxOvStL/s7iupjl3g4CXw0RgIfyOuJQldLdq7mh6
|
||||
W7BpK855fH2CLy1VMdIFY1umHSgJ5Bayd5NmyKdfORUw88PTFFSARYyr5DDqufWg
|
||||
jI77BMjqHtbyujQKspV+9U66ZwKBgQDBsi5YptfaqMdzh343G5zAtCiGCd3m9NZO
|
||||
atEEvgq9LKqEVwvJ/FD+3QPAS1MN+ms9KcfJ3G05VUhhEsPGRKibgcE0FNA/VBDO
|
||||
jS2HK6kZ1M0clC5kHmQfLZxp1q3tA5nW6zqjVFdqYzJsis7G5YxFKhnzui0lgP6V
|
||||
I1Io+3QvrQKBgQCK6D+sq92o8AnkfICsq6qDCKA+PO68/pyGOUAiAoKQ7qK0W0Z5
|
||||
TMIwnRTxHCjgViAIUehx/6hjByQXiPcU2zcNGTLGVgtjl5rfb7FGANlJEg6DL+2L
|
||||
bwV1QwX75OSR1U136hsy9oByg6oDEvM040+B4gxsf0OHdYEuJWa5w8eLTwKBgCJt
|
||||
CM+416SFWu2tp0EkJzgYzRsFpermmTBWy8+L91yoE6Zx0iaUMdEadxA2Uwyo9WZp
|
||||
hpjaFI+cGMEoFKOokE8TQMOA74JR7qrHbNAZcnSk3c+2hohE3oasFKC7By6Y9T69
|
||||
kC53TxIZj1y7TwUKx2ODmBk5fcysoJLhNDkUeBIBAoGAQ/YMeMN/pSvTDCPhubbk
|
||||
9bF0SwoN6DWrzw7gzMMhHlVSPM4fjdBA+wXbhnYF7hiNan7uShtRHVhwyfj+glBz
|
||||
KIkDxETjxTWx5mbo3tNf8xEMsbHZBe8KPlKxizdAOvShLvkpthTCPiyqn6XTvbs4
|
||||
vC0zZrsQWCYT4Wbm7gcOOAU=
|
||||
-----END PRIVATE KEY-----
|
||||
44
tests/certs/mtls/client/client.pem
Normal file
44
tests/certs/mtls/client/client.pem
Normal file
@@ -0,0 +1,44 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDpjCCAo6gAwIBAgIUTzbDp+B1umRS0Q7rgefxif9Im4IwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIzWhcNMjcwMjE3MDAzODIzWjBs
|
||||
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCREUxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0
|
||||
d2FyZSBGb3VuZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxETAPBgNV
|
||||
BAMMCHJlcXVlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmt2+
|
||||
7mS8LLmDrDruBzMROT+7orRQGZ/oy/SWpcKndWjK3l+/pIj849asVXX6biKY17ls
|
||||
szslShR+SqSZQuyNNOHuzFUuNbbbq8tJg5ZbLs8D45LBRM0uPJ0XV1rOU950c9h2
|
||||
aFVKEKbKg53WlQINCfT/hwQD9Zbia3NkiaagJ/+nIAzdcAL1EFxJIlHLA5N7qf0t
|
||||
joqjRhRb3hrLYzgKVVuUPGzAN6ZoCwsW5yIqaLneArHgf+Vqt6jynrR2C5uiiIxt
|
||||
xzgT1vOnSgml+HvGHAp4m0I0Sk6KTUCKJAhm3CqaSQge+0gajdJntYBBT0SHav67
|
||||
cTppKX43Q8xcrhjgmwIDAQABo0IwQDAdBgNVHQ4EFgQU3Ujw+VSuTzPgHqU+KFwO
|
||||
T4t2MHswHwYDVR0jBBgwFoAUCEaIPf7XQsGt0xbf6P9x5cxdKpwwDQYJKoZIhvcN
|
||||
AQELBQADggEBAEvrKXWHRRDIf26j2fH9hanx0nh+lxvI4jSWYmK0rJXZA3htEvWn
|
||||
gcoUspmhmLlgmRMP88lGINMjTsogUubu2j6WF/WuKxAWWvl/hUgK8NzOwOHvByPB
|
||||
lhO/rSNGdOWGlnaW1TVO4kI8w6c6LwzOCpY8WvOZLW+v7duLhKUdhaJMR9X77Tbt
|
||||
ohHkyYm0gV79izaFRpA6mdYoyHOR4gyWAKaj942doU794fT4gqQacRNifl/kUbWI
|
||||
lilktTLyLnmBJgrxHVBxcGe8kwnPafA1k3Gb1w7mY5BSoGKglKjutTfqR3uTjAQb
|
||||
vskS4SGXmsEIjLrXYWFNHyoC3pCBXWhX6Kc=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIUG/CTOPIQbH2BI36TyThUChQyR8wwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIyWhcNNDUwMjEyMDAzODIyWjBq
|
||||
MQswCQYDVQQGEwJVUzEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRp
|
||||
b24xGDAWBgNVBAsMD3B5dGhvbi1yZXF1ZXN0czEcMBoGA1UEAwwTU2VsZi1TaWdu
|
||||
ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ9lSHzZ
|
||||
0X+v0Zbz0g3a+o6Iwy6KQgA7dIQjf65IYJ1ZPXcL42esUntcX8PpQbLWa+bwp+FK
|
||||
dqlagCS2jI9dJz8Y3MnMLBmiiXvET6ub/S9u7VdtRxByoHydBEvNMKEMga64PwMe
|
||||
ztuZ6fX2xPmRnLQIippzfzdxDFHxbUWzjzEQEl+4EGbKX5vPi6A9yL3ofgMi4APZ
|
||||
C8/YoqAaqzjLIM9KZ/zqT36iQ6wsxBXAacd63L5M8lrAKfZoJeX6uYxNIIfaxQRz
|
||||
zVxdpVBjHJ/odqoadwKzU7YzN5Fs+6ATNvkrhlUzg1i0MyTGtCdkUIAt/hc6ZrW6
|
||||
/kqHpCgUwUc4Dq0CAwEAAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
|
||||
CEaIPf7XQsGt0xbf6P9x5cxdKpwwDQYJKoZIhvcNAQELBQADggEBAHMgyQNA3DQG
|
||||
0l9eX8RMl4YNwhAXChU/wOTcvD+F4OsJcPqzy6QHFh1AbBBGOI9/HN7du+EiKwGZ
|
||||
wE+69FEsSbhSv22XidPm+kHPTguWl1eKveeWrrT5MPl9F48s14lKb/8yMEa1/ryG
|
||||
Iu8NQ6ZL91JbTXdkLoBDya9HZqyXjwcGkXLE8fSqTibJ7EhWS5Q3Ic7WPgUoGAum
|
||||
b5ygoxqhm+SEyXC2/LAktwmFawkv1SsMeYpT790VIFqJ/TVVnUl+gQ2RjSEl2WLb
|
||||
UO4Hwq4FZbWF9NrY6JVThLmbcr8eW6+UxWfiXHLw/qTRre4/3367QAUQRt7EuEsb
|
||||
KOWpOS3fbsI=
|
||||
-----END CERTIFICATE-----
|
||||
1
tests/certs/valid/ca
Symbolic link
1
tests/certs/valid/ca
Symbolic link
@@ -0,0 +1 @@
|
||||
../expired/ca
|
||||
16
tests/certs/valid/server/Makefile
Normal file
16
tests/certs/valid/server/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
.PHONY: all clean
|
||||
|
||||
server.key:
|
||||
openssl genrsa -out $@ 2048
|
||||
|
||||
server.csr: server.key
|
||||
openssl req -key $< -config cert.cnf -new -out $@
|
||||
|
||||
server.pem: server.csr
|
||||
openssl x509 -req -CA ../ca/ca.crt -CAkey ../ca/ca-private.key -in server.csr -outform PEM -out server.pem -extfile cert.cnf -extensions v3_ca -days 7200 -CAcreateserial
|
||||
openssl x509 -in ../ca/ca.crt -outform PEM >> $@
|
||||
|
||||
all: server.pem
|
||||
|
||||
clean:
|
||||
rm -f server.*
|
||||
31
tests/certs/valid/server/cert.cnf
Normal file
31
tests/certs/valid/server/cert.cnf
Normal file
@@ -0,0 +1,31 @@
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt=no
|
||||
|
||||
[req_distinguished_name]
|
||||
C = US
|
||||
ST = DE
|
||||
O = Python Software Foundation
|
||||
OU = python-requests
|
||||
CN = localhost
|
||||
|
||||
[v3_req]
|
||||
# Extensions to add to a certificate request
|
||||
basicConstraints = critical, CA:FALSE
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = critical, serverAuth
|
||||
subjectAltName = critical, @alt_names
|
||||
|
||||
[v3_ca]
|
||||
# Extensions to add to a certificate request
|
||||
basicConstraints = critical, CA:FALSE
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = critical, serverAuth
|
||||
subjectAltName = critical, @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = *.localhost
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
IP.2 = ::1
|
||||
19
tests/certs/valid/server/server.csr
Normal file
19
tests/certs/valid/server/server.csr
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIDKjCCAhICAQAwbTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkRFMSMwIQYDVQQK
|
||||
DBpQeXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEYMBYGA1UECwwPcHl0aG9uLXJl
|
||||
cXVlc3RzMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDD3OzTz9TgOiJp/STy/au8G1EDVUuP3HWKAX5ZpkWSTYZfc7FF
|
||||
pgPQvBq83oh/K1+9Rw0/529N1+KeTp1i9vUBUM2wJ3EzykJP4rMFh+J/Nt6VFfJh
|
||||
05pTQiHnc85l4U8Qz7fHS6Lc9EG/Yp6yDxt5OeK8QAkNYjvxVhVdpif3GlnICx1e
|
||||
y1EcPxb9rslERyz0eiL6+BtVbhlSvup/rz2skvaYxoh/pP1RVwbu8VtS0it046Fm
|
||||
U0TnIdsjrdFjHXNJ2JSs5g6FDB9QEFhYdDOonL4KMcAkXxBLYXpjJ5fj8/X4LFkU
|
||||
FINry+Q2zdFKYsghCxlW98hmJVJTscVWznqpAgMBAAGgeDB2BgkqhkiG9w0BCQ4x
|
||||
aTBnMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWgMBYGA1UdJQEB/wQMMAoG
|
||||
CCsGAQUFBwMBMC8GA1UdEQEB/wQlMCOCCWxvY2FsaG9zdIcEfwAAAYcQAAAAAAAA
|
||||
AAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAn8TdRWYAGL7L8JKpkX2FRMMY
|
||||
EuFOEeJ2UaQLRbVtb1RhkIkMT6UYulQ16K45dbFe4k8DT59/mHrTl0bJ6pqVPawa
|
||||
vo7vmKCrSYEz8GrImh1+i6lHaFiqbkAs1mJ6z86dZUofMYwfTPVh64ztamUqwBk7
|
||||
Dey+MzWUQvpTVqoaP7cLgzMy+XfcyGuWSoL6pX5XKkt4+A2wiCTsHul1sXOn4vQz
|
||||
4xY96AUVnkKosXMWXvhbMkncLNH+gs+ZeQI0MekakuMa42N+0BAad3Zx60lifG3N
|
||||
ugyVUmvVI0Fq6QUlYp3YV6QQj3FDjbgOVsSJNh+2s4SIARJsb/k//Tddzxei7g==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
28
tests/certs/valid/server/server.key
Normal file
28
tests/certs/valid/server/server.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDD3OzTz9TgOiJp
|
||||
/STy/au8G1EDVUuP3HWKAX5ZpkWSTYZfc7FFpgPQvBq83oh/K1+9Rw0/529N1+Ke
|
||||
Tp1i9vUBUM2wJ3EzykJP4rMFh+J/Nt6VFfJh05pTQiHnc85l4U8Qz7fHS6Lc9EG/
|
||||
Yp6yDxt5OeK8QAkNYjvxVhVdpif3GlnICx1ey1EcPxb9rslERyz0eiL6+BtVbhlS
|
||||
vup/rz2skvaYxoh/pP1RVwbu8VtS0it046FmU0TnIdsjrdFjHXNJ2JSs5g6FDB9Q
|
||||
EFhYdDOonL4KMcAkXxBLYXpjJ5fj8/X4LFkUFINry+Q2zdFKYsghCxlW98hmJVJT
|
||||
scVWznqpAgMBAAECggEACY98I+6uJm/QBDpuFkpZmqn+r1n3gUMynZTrFPcvyC9u
|
||||
krQ0AAFViFfWOkfmg8abOsMAG5FxdmxGTJHrzsvdM748/A9A0FVcHUgkku2KGcmU
|
||||
3dQfa7UHgG7m9sRJW+G+mUR6ZQkFHyHxH6Vxt6FTJvyzW5sIlhWodWRNUK/unXoe
|
||||
5QNIh/PM9Mkg91wKVeg8AGdY27zfuUV0KLwhV052hCSwckayi7pjYrH2xDL8fahi
|
||||
Z/F43N1SI6Q7PTW6izDs2KrlGyexn97jOV+cooSXXaDvivTaB2K4RoidrPt+UAW4
|
||||
/e1zw/LbeBvQXX4vsyC9vLO+Av1gYo2PxAZ/ezc5gQKBgQDvY/kgUud6stdMs73N
|
||||
Qavu9o/Ul4oeQa4fsmfUwj43OjquxpXbfV9sNTLFxFbsl5ADGkpqsR3WhP2090TK
|
||||
62vkyCBiaYZjyMY6WLDATEvrpfbc74ATEzTU+r9T1rEByq/atbWPZq/zoMo8aAdg
|
||||
Qk2X7gQO3pTpPeEpetYG42Di+QKBgQDRc9IqPeELQugXEuI0RZ8rsfGRSFKVRelQ
|
||||
Rz/KpD/S8SsE7ERL0w/w5KgE0IPPbh/SYX2KYxafCqTrPWDNaoguROvUoUJQvf4a
|
||||
uOMTCRkqdqj8j70zaPj2ohuIIZ3GZHyaXgl/isWtuZ5OeaoY5h3r+4gk5mO41Llz
|
||||
YikB71SRMQKBgGBNG18BetVFNI9Kj0QO8xeCYIHpJErfqShfIJ3aNiUJa6n7gTV2
|
||||
zfg9vlsIjN9IaUqWPPGGprYxcc5m2mm3IwQ57a0pPkLN9dBq9U+mYbQ+Y3ylbCRA
|
||||
SbST2nvjlflejDezeYJikM21FSYPw0fZ5FUGDuPcbpMVrYp+O7MxrTwhAoGAcjQq
|
||||
xemTiWZj0iDzwfisP1D5HHRIwyepfaI7wCwquMPS5w5EduuQZ5LloipnlHTBWR7b
|
||||
Kte4f+N35OREofySYFgoFnoPBKNzp/Jjrf9p/2NP5NYjHaMBDMl7JZDezEwCPNFF
|
||||
cIukGYN6M+PWwVjHu+Ica7JLcX5b1/QP1ARBIiECgYAsdDa0CoYTVWMeqMq9GjF3
|
||||
BElBKCLp6iYqpElWyKTj39LCnLhzRICyYQpblM8zZgtQIvRGT8DYh2HA1Q29yPzt
|
||||
Rgrz9yfFACdT5gUM5D6sx7L37xbtMQQ7YYOxEAfCUZ0qnMJgpNTJ8/ECkjGJZaif
|
||||
CXrx6XCdvrM/evZW2JZbyg==
|
||||
-----END PRIVATE KEY-----
|
||||
46
tests/certs/valid/server/server.pem
Normal file
46
tests/certs/valid/server/server.pem
Normal file
@@ -0,0 +1,46 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEEDCCAvigAwIBAgIUTzbDp+B1umRS0Q7rgefxif9Im4MwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIzWhcNNDQxMTA0MDAzODIzWjBt
|
||||
MQswCQYDVQQGEwJVUzELMAkGA1UECAwCREUxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0
|
||||
d2FyZSBGb3VuZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPc
|
||||
7NPP1OA6Imn9JPL9q7wbUQNVS4/cdYoBflmmRZJNhl9zsUWmA9C8GrzeiH8rX71H
|
||||
DT/nb03X4p5OnWL29QFQzbAncTPKQk/iswWH4n823pUV8mHTmlNCIedzzmXhTxDP
|
||||
t8dLotz0Qb9inrIPG3k54rxACQ1iO/FWFV2mJ/caWcgLHV7LURw/Fv2uyURHLPR6
|
||||
Ivr4G1VuGVK+6n+vPayS9pjGiH+k/VFXBu7xW1LSK3TjoWZTROch2yOt0WMdc0nY
|
||||
lKzmDoUMH1AQWFh0M6icvgoxwCRfEEthemMnl+Pz9fgsWRQUg2vL5DbN0UpiyCEL
|
||||
GVb3yGYlUlOxxVbOeqkCAwEAAaOBqjCBpzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB
|
||||
/wQEAwIFoDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDATAvBgNVHREBAf8EJTAjggls
|
||||
b2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwHQYDVR0OBBYEFGg5Q9Ll
|
||||
ADkONmo02kmPg9+aiOxQMB8GA1UdIwQYMBaAFAhGiD3+10LBrdMW3+j/ceXMXSqc
|
||||
MA0GCSqGSIb3DQEBCwUAA4IBAQA1RlUI34tXrIdsRiALD8iLDFhh816B7qUi+F1j
|
||||
3dOkTNgYw0CnQ+Vm4wrQjCEVSDQ/9sry5DOfXeGziDpFlZmQ0UuAeM1EJJD5/42l
|
||||
C/eKquA09+IMEq2U03GPhijrC68sFCfr5wgoB/4HmcZ1c3kLYvRaJhEK7AHDgenY
|
||||
knKbvuKAiRbCd584eG8HFOW7xv+YqGZ257Ic9flbarkPrazmAJg2P709w7/fP83x
|
||||
7lnkPZY09jS3lcIpEdtWvzfm+anGF190hKA1yfPdO5bYPvUcEMxXdMtQ6/pbZd5i
|
||||
F6t95o9CPCqI/lLIn5jAf9Z+Iil3GmKefEZYIGmKJ85HGUqE
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlDCCAnygAwIBAgIUG/CTOPIQbH2BI36TyThUChQyR8wwDQYJKoZIhvcNAQEL
|
||||
BQAwajELMAkGA1UEBhMCVVMxIzAhBgNVBAoMGlB5dGhvbiBTb2Z0d2FyZSBGb3Vu
|
||||
ZGF0aW9uMRgwFgYDVQQLDA9weXRob24tcmVxdWVzdHMxHDAaBgNVBAMME1NlbGYt
|
||||
U2lnbmVkIFJvb3QgQ0EwHhcNMjUwMjE3MDAzODIyWhcNNDUwMjEyMDAzODIyWjBq
|
||||
MQswCQYDVQQGEwJVUzEjMCEGA1UECgwaUHl0aG9uIFNvZnR3YXJlIEZvdW5kYXRp
|
||||
b24xGDAWBgNVBAsMD3B5dGhvbi1yZXF1ZXN0czEcMBoGA1UEAwwTU2VsZi1TaWdu
|
||||
ZWQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ9lSHzZ
|
||||
0X+v0Zbz0g3a+o6Iwy6KQgA7dIQjf65IYJ1ZPXcL42esUntcX8PpQbLWa+bwp+FK
|
||||
dqlagCS2jI9dJz8Y3MnMLBmiiXvET6ub/S9u7VdtRxByoHydBEvNMKEMga64PwMe
|
||||
ztuZ6fX2xPmRnLQIippzfzdxDFHxbUWzjzEQEl+4EGbKX5vPi6A9yL3ofgMi4APZ
|
||||
C8/YoqAaqzjLIM9KZ/zqT36iQ6wsxBXAacd63L5M8lrAKfZoJeX6uYxNIIfaxQRz
|
||||
zVxdpVBjHJ/odqoadwKzU7YzN5Fs+6ATNvkrhlUzg1i0MyTGtCdkUIAt/hc6ZrW6
|
||||
/kqHpCgUwUc4Dq0CAwEAAaMyMDAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
|
||||
CEaIPf7XQsGt0xbf6P9x5cxdKpwwDQYJKoZIhvcNAQELBQADggEBAHMgyQNA3DQG
|
||||
0l9eX8RMl4YNwhAXChU/wOTcvD+F4OsJcPqzy6QHFh1AbBBGOI9/HN7du+EiKwGZ
|
||||
wE+69FEsSbhSv22XidPm+kHPTguWl1eKveeWrrT5MPl9F48s14lKb/8yMEa1/ryG
|
||||
Iu8NQ6ZL91JbTXdkLoBDya9HZqyXjwcGkXLE8fSqTibJ7EhWS5Q3Ic7WPgUoGAum
|
||||
b5ygoxqhm+SEyXC2/LAktwmFawkv1SsMeYpT790VIFqJ/TVVnUl+gQ2RjSEl2WLb
|
||||
UO4Hwq4FZbWF9NrY6JVThLmbcr8eW6+UxWfiXHLw/qTRre4/3367QAUQRt7EuEsb
|
||||
KOWpOS3fbsI=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -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)
|
||||
|
||||
8
tests/test_adapters.py
Normal file
8
tests/test_adapters.py
Normal file
@@ -0,0 +1,8 @@
|
||||
import requests.adapters
|
||||
|
||||
|
||||
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, {})
|
||||
@@ -1,3 +1,5 @@
|
||||
from unittest import mock
|
||||
|
||||
from requests.help import info
|
||||
|
||||
|
||||
@@ -11,15 +13,15 @@ class VersionedPackage:
|
||||
self.__version__ = version
|
||||
|
||||
|
||||
def test_idna_without_version_attribute(mocker):
|
||||
def test_idna_without_version_attribute():
|
||||
"""Older versions of IDNA don't provide a __version__ attribute, verify
|
||||
that if we have such a package, we don't blow up.
|
||||
"""
|
||||
mocker.patch("requests.help.idna", new=None)
|
||||
assert info()["idna"] == {"version": ""}
|
||||
with mock.patch("requests.help.idna", new=None):
|
||||
assert info()["idna"] == {"version": ""}
|
||||
|
||||
|
||||
def test_idna_with_version_attribute(mocker):
|
||||
def test_idna_with_version_attribute():
|
||||
"""Verify we're actually setting idna version when it should be available."""
|
||||
mocker.patch("requests.help.idna", new=VersionedPackage("2.6"))
|
||||
assert info()["idna"] == {"version": "2.6"}
|
||||
with mock.patch("requests.help.idna", new=VersionedPackage("2.6")):
|
||||
assert info()["idna"] == {"version": "2.6"}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user