├── .coveragerc ├── .git-blame-ignore-revs ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── cohabitation-test.yml │ ├── lint.yml │ ├── publish.yml │ └── run-tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS.rst ├── HISTORY.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── docs ├── .nojekyll ├── Makefile ├── _static │ └── logo.png ├── _templates │ └── base.html ├── api.rst ├── community │ ├── extensions.rst │ ├── faq.rst │ ├── release-process.rst │ ├── support.rst │ ├── updates.rst │ └── vulnerabilities.rst ├── conf.py ├── dev │ ├── authors.rst │ ├── contributing.rst │ ├── httpx.rst │ └── migrate.rst ├── images │ └── banner.png ├── index.rst ├── make.bat ├── requirements.txt └── user │ ├── advanced.rst │ ├── authentication.rst │ ├── install.rst │ └── quickstart.rst ├── noxfile.py ├── pyproject.toml ├── requirements-dev.txt ├── src └── niquests │ ├── __init__.py │ ├── __version__.py │ ├── _async.py │ ├── _compat.py │ ├── _constant.py │ ├── _typing.py │ ├── _vendor │ ├── __init__.py │ └── kiss_headers │ │ ├── LICENSE │ │ ├── __init__.py │ │ ├── api.py │ │ ├── builder.py │ │ ├── models.py │ │ ├── py.typed │ │ ├── serializer.py │ │ ├── structures.py │ │ ├── utils.py │ │ └── version.py │ ├── adapters.py │ ├── api.py │ ├── async_api.py │ ├── async_session.py │ ├── auth.py │ ├── cookies.py │ ├── exceptions.py │ ├── extensions │ ├── __init__.py │ ├── _async_ocsp.py │ ├── _ocsp.py │ └── _picotls.py │ ├── help.py │ ├── hooks.py │ ├── models.py │ ├── packages.py │ ├── py.typed │ ├── sessions.py │ ├── status_codes.py │ ├── structures.py │ └── utils.py └── tests ├── __init__.py ├── conftest.py ├── test_async.py ├── test_help.py ├── test_hooks.py ├── test_live.py ├── test_lowlevel.py ├── test_multiplexed.py ├── test_ocsp.py ├── test_requests.py ├── test_sse.py ├── test_structures.py ├── test_testserver.py ├── test_utils.py ├── test_websocket.py ├── testserver ├── __init__.py └── server.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | niquests 4 | disable_warnings = no-sysmon 5 | 6 | [paths] 7 | source = 8 | src/niquests 9 | */niquests 10 | *\niquests 11 | 12 | [report] 13 | omit = 14 | src/niquests/help.py 15 | src/niquests/_vendor/* 16 | 17 | exclude_lines = 18 | except ModuleNotFoundError: 19 | except ImportError: 20 | pass 21 | import 22 | raise NotImplementedError 23 | .* # Platform-specific.* 24 | .*:.* # Python \d.* 25 | .* # Abstract 26 | .* # Defensive: 27 | if (?:typing.)?TYPE_CHECKING: 28 | ^\s*?\.\.\.\s*$ 29 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # You can configure git to automatically use this file with the following config: 2 | # git config --global blame.ignoreRevsFile .git-blame-ignore-revs 3 | 4 | # Add automatic code formatting to Requests 5 | 2a6f290bc09324406708a4d404a88a45d848ddf9 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Treat each other well 2 | 3 | Everyone participating in the _requests_ project, and in particular in the issue tracker, 4 | pull requests, and social media activity, is expected to treat other people with respect 5 | and more generally to follow the guidelines articulated in the 6 | [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). 7 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Before opening any issues or proposing any pull requests, please read 4 | our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/). 5 | 6 | To get the greatest chance of helpful responses, please also observe the 7 | following additional notes. 8 | 9 | ## Questions 10 | 11 | The GitHub issue tracker is for *bug reports* and *feature requests*. Please do 12 | not use it to ask questions about how to use Requests. These questions should 13 | instead be directed to [Stack Overflow](https://stackoverflow.com/). Make sure 14 | that your question is tagged with the `python-requests` tag when asking it on 15 | Stack Overflow, to ensure that it is answered promptly and accurately. 16 | 17 | ## Good Bug Reports 18 | 19 | Please be aware of the following things when filing bug reports: 20 | 21 | 1. Avoid raising duplicate issues. *Please* use the GitHub issue search feature 22 | to check whether your bug report or feature request has been mentioned in 23 | the past. Duplicate bug reports and feature requests are a huge maintenance 24 | burden on the limited resources of the project. If it is clear from your 25 | report that you would have struggled to find the original, that's ok, but 26 | if searching for a selection of words in your issue title would have found 27 | the duplicate then the issue will likely be closed extremely abruptly. 28 | 2. When filing bug reports about exceptions or tracebacks, please include the 29 | *complete* traceback. Partial tracebacks, or just the exception text, are 30 | not helpful. Issues that do not contain complete tracebacks may be closed 31 | without warning. 32 | 3. Make sure you provide a suitable amount of information to work with. This 33 | means you should provide: 34 | 35 | - Guidance on **how to reproduce the issue**. Ideally, this should be a 36 | *small* code sample that can be run immediately by the maintainers. 37 | Failing that, let us know what you're doing, how often it happens, what 38 | environment you're using, etc. Be thorough: it prevents us needing to ask 39 | further questions. 40 | - Tell us **what you expected to happen**. When we run your example code, 41 | what are we expecting to happen? What does "success" look like for your 42 | code? 43 | - Tell us **what actually happens**. It's not helpful for you to say "it 44 | doesn't work" or "it fails". Tell us *how* it fails: do you get an 45 | exception? A hang? A non-200 status code? How was the actual result 46 | different from your expected result? 47 | - Tell us **what version of Requests you're using**, and 48 | **how you installed it**. Different versions of Requests behave 49 | differently and have different bugs, and some distributors of Requests 50 | ship patches on top of the code we supply. 51 | 52 | If you do not provide all of these things, it will take us much longer to 53 | fix your problem. If we ask you to clarify these and you never respond, we 54 | will close your issue without fixing it. 55 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - Ousret 3 | tidelift: pypi/niquests 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Summary. 2 | 3 | ## Expected Result 4 | 5 | What you expected. 6 | 7 | ## Actual Result 8 | 9 | What happened instead. 10 | 11 | ## Reproduction Steps 12 | 13 | ```python 14 | import niquests 15 | 16 | ``` 17 | 18 | ## System Information 19 | 20 | $ python -m niquests.help 21 | 22 | ``` 23 | 24 | ``` 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 8 | 9 | ## Expected Result 10 | 11 | 12 | 13 | ## Actual Result 14 | 15 | 16 | 17 | ## Reproduction Steps 18 | 19 | ```python 20 | import niquests 21 | 22 | ``` 23 | 24 | ## System Information 25 | 26 | $ python -m niquests.help 27 | 28 | ```json 29 | { 30 | "paste": "here" 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | tags: 5 | - feature-request 6 | --- 7 | 8 | - [ ] Explain the use-case carefully. _Maybe with a code example_ 9 | - [ ] Who needs this? 10 | - [ ] What pain does this resolve? 11 | - [ ] Is this standard? 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | ignore: 8 | # Ignore all patch releases as we can manually 9 | # upgrade if we run into a bug and need a fix. 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [main] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [main] 14 | schedule: 15 | - cron: '0 23 * * 0' 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | analyze: 22 | permissions: 23 | actions: read # for github/codeql-action/init to get workflow details 24 | contents: read # for actions/checkout to fetch code 25 | security-events: write # for github/codeql-action/autobuild to send a status report 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 36 | with: 37 | # We must fetch at least the immediate parents so that if this is 38 | # a pull request then we can checkout the head. 39 | fetch-depth: 2 40 | 41 | # If this run was triggered by a pull request event, then checkout 42 | # the head of the pull request instead of the merge commit. 43 | - run: git checkout HEAD^2 44 | if: ${{ github.event_name == 'pull_request' }} 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 49 | with: 50 | languages: "python" 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0 74 | -------------------------------------------------------------------------------- /.github/workflows/cohabitation-test.yml: -------------------------------------------------------------------------------- 1 | name: Test with Cohabitation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ci-cohabitation-${{ github.ref_name }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 30 20 | 21 | steps: 22 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 23 | - name: Set up Python 3.11 24 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 25 | with: 26 | python-version: 3.11 27 | cache: 'pip' 28 | - name: Install dependencies 29 | run: | 30 | pip install nox 31 | - name: Run tests 32 | run: | 33 | nox -s "test_cohabitation" 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint code 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: lint-${{ github.ref_name }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | lint: 18 | runs-on: ubuntu-latest 19 | timeout-minutes: 10 20 | 21 | steps: 22 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 23 | - name: Set up Python 24 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 25 | with: 26 | python-version: "3.x" 27 | - name: Install nox 28 | run: python -m pip install nox 29 | - name: run pre-commit 30 | run: nox -s lint 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | name: "Build dists" 14 | runs-on: "ubuntu-latest" 15 | outputs: 16 | hashes: ${{ steps.hash.outputs.hashes }} 17 | 18 | steps: 19 | - name: "Checkout repository" 20 | uses: "actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871" 21 | 22 | - name: "Setup Python" 23 | uses: "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065" 24 | with: 25 | python-version: "3.x" 26 | 27 | - name: "Install dependencies" 28 | run: python -m pip install build 29 | 30 | - name: "Build dists" 31 | run: | 32 | SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \ 33 | python -m build 34 | 35 | - name: "Generate hashes" 36 | id: hash 37 | run: | 38 | cd dist && echo "::set-output name=hashes::$(sha256sum * | base64 -w0)" 39 | 40 | - name: "Upload dists" 41 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 42 | with: 43 | name: "dist" 44 | path: "dist/" 45 | if-no-files-found: error 46 | retention-days: 5 47 | 48 | provenance: 49 | needs: [build] 50 | permissions: 51 | actions: read 52 | contents: write 53 | id-token: write # Needed to access the workflow's OIDC identity. 54 | uses: "slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0" 55 | with: 56 | base64-subjects: "${{ needs.build.outputs.hashes }}" 57 | upload-assets: true 58 | compile-generator: true # Workaround for https://github.com/slsa-framework/slsa-github-generator/issues/1163 59 | 60 | publish: 61 | name: "Publish" 62 | if: startsWith(github.ref, 'refs/tags/') 63 | environment: 64 | name: pypi 65 | url: https://pypi.org/p/niquests 66 | needs: ["build", "provenance"] 67 | permissions: 68 | contents: write 69 | id-token: write # Needed for trusted publishing to PyPI. 70 | runs-on: "ubuntu-latest" 71 | 72 | steps: 73 | - name: "Download dists" 74 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 75 | with: 76 | name: "dist" 77 | path: "dist/" 78 | 79 | - name: "Upload dists to GitHub Release" 80 | env: 81 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 82 | run: | 83 | gh release upload ${{ github.ref_name }} dist/* --repo ${{ github.repository }} 84 | 85 | - name: "Publish dists to PyPI" 86 | uses: "pypa/gh-action-pypi-publish@67339c736fd9354cd4f8cb0b744f2b82a74b5c70" # v1.12.3 87 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ci-${{ github.ref_name }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | build: 18 | runs-on: ${{ matrix.os }} 19 | timeout-minutes: 30 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy-3.9", "pypy-3.10"] 24 | os: [ubuntu-22.04, macOS-13, windows-latest] 25 | include: 26 | # pypy-3.7, pypy-3.8 may fail due to missing cryptography wheels. Adapting. 27 | - python-version: pypy-3.7 28 | os: ubuntu-22.04 29 | - python-version: pypy-3.8 30 | os: ubuntu-22.04 31 | - python-version: pypy-3.8 32 | os: macOS-13 33 | exclude: 34 | # pypy 3.9 and 3.10 suffers from a wierd bug, probably due to gc 35 | # this bug prevent us from running the suite on Windows. 36 | - python-version: pypy-3.9 37 | os: windows-latest 38 | - python-version: pypy-3.10 39 | os: windows-latest 40 | 41 | steps: 42 | - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 43 | - name: Set up Python ${{ matrix.python-version }} 44 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 45 | with: 46 | python-version: ${{ matrix.python-version }} 47 | allow-prereleases: true 48 | cache: 'pip' 49 | - name: Install dependencies 50 | run: | 51 | pip install nox 52 | - name: Run tests 53 | run: | 54 | nox -s "test-${{ startsWith(matrix.python-version, 'pypy') && 'pypy' || matrix.python-version }}" 55 | - name: "Upload artifact" 56 | uses: "actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1" 57 | with: 58 | name: coverage-data-${{ matrix.os }}-${{ matrix.nox-session }}-${{ matrix.python-version }}-${{ matrix.traefik-server }} 59 | path: ".coverage.*" 60 | include-hidden-files: true 61 | if-no-files-found: error 62 | 63 | coverage: 64 | if: always() 65 | runs-on: "ubuntu-latest" 66 | needs: build 67 | steps: 68 | - name: "Checkout repository" 69 | uses: "actions/checkout@d632683dd7b4114ad314bca15554477dd762a938" 70 | 71 | - name: "Setup Python" 72 | uses: "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065" 73 | with: 74 | python-version: "3.x" 75 | 76 | - name: "Install coverage" 77 | run: "python -m pip install --upgrade coverage" 78 | 79 | - name: "Download artifact" 80 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 81 | with: 82 | pattern: coverage-data* 83 | merge-multiple: true 84 | 85 | - name: "Combine & check coverage" 86 | run: | 87 | python -m coverage combine 88 | python -m coverage html --skip-covered --skip-empty 89 | python -m coverage report --ignore-errors --show-missing --fail-under=75 90 | 91 | - name: "Upload report" 92 | uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 93 | with: 94 | name: coverage-report 95 | path: htmlcov 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | MANIFEST 3 | coverage.xml 4 | nosetests.xml 5 | junit-report.xml 6 | pylint.txt 7 | toy.py 8 | .cache/ 9 | cover/ 10 | build/ 11 | docs/_build 12 | requests.egg-info/ 13 | src/niquests.egg-info 14 | *.pyc 15 | *.swp 16 | *.egg 17 | env/ 18 | .venv/ 19 | .eggs/ 20 | .tox/ 21 | .pytest_cache/ 22 | .vscode/ 23 | .eggs/ 24 | .nox/ 25 | .ruff_cache/ 26 | 27 | .workon 28 | 29 | # in case you work with IntelliJ/PyCharm 30 | .idea 31 | *.iml 32 | .python-version 33 | 34 | 35 | t.py 36 | 37 | t2.py 38 | dist 39 | 40 | /.mypy_cache/ 41 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'docs/|src/niquests/_vendor' 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | # Ruff version. 13 | rev: v0.9.1 14 | hooks: 15 | # Run the linter. 16 | - id: ruff 17 | args: [ --fix, --target-version=py37 ] 18 | # Run the formatter. 19 | - id: ruff-format 20 | - repo: https://github.com/pre-commit/mirrors-mypy 21 | rev: v1.15.0 22 | hooks: 23 | - id: mypy 24 | args: [--check-untyped-defs] 25 | exclude: 'tests/|noxfile.py' 26 | additional_dependencies: ['charset_normalizer', 'urllib3.future>=2.12.900', 'wassima>=1.0.1', 'qh3>=1.4', '.'] 27 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-24.04 5 | tools: 6 | python: "3.12" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Requests was lovingly created by Kenneth Reitz. 2 | 3 | Core maintainers 4 | ```````````````` 5 | - Ahmed Tahri `@Ousret `_. 6 | 7 | Previous from Requests 8 | `````````````````````` 9 | - Kenneth Reitz `@ken-reitz `_, reluctant Keeper of the Master Crystal. 10 | - Cory Benfield `@lukasa `_ 11 | - Ian Cordasco `@sigmavirus24 `_. 12 | - Nate Prewitt `@nateprewitt `_. 13 | - Seth M. Larson `@sethmlarson `_. 14 | 15 | Patches and Suggestions 16 | ``````````````````````` 17 | See https://github.com/jawah/niquests/graphs/contributors 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md SECURITY.md LICENSE NOTICE HISTORY.md requirements-dev.txt 2 | recursive-include tests *.py 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs 2 | init: 3 | python -m pip install nox coverage 4 | test: 5 | # This runs all of the tests on all supported Python versions. 6 | nox -s test 7 | ci: 8 | nox -s test 9 | 10 | coverage: 11 | python -m coverage combine && python -m coverage report --ignore-errors --show-missing 12 | 13 | docs: 14 | nox -s docs 15 | @echo "\033[95m\n\nBuild successful! View the docs homepage at docs/_build/html/index.html.\n\033[0m" 16 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Requests 2 | Copyright 2019 Kenneth Reitz 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Disclosures 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 4 | Tidelift will coordinate the fix and disclosure with maintainers. 5 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(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/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Requests.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Requests.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Requests" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Requests" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends '!base.html' %} 2 | {%- block site_meta -%} 3 | {{ super() }} 4 | 19 | {%- endblock -%} -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: Classes, Functions and Methods API Documentation for Python Niquests. Session, AsyncSession, get, post, put, patch, delete, Response, Exceptions. 3 | :keywords: Python Niquests API, API Docs Niquests, Requests API, Session, AsyncSession, get, post, put, patch, delete, async http, Timeout, ConnectionError, TooManyRedirects, Response, AsyncResponse 4 | 5 | .. _api: 6 | 7 | Developer Interface 8 | =================== 9 | 10 | .. module:: niquests 11 | 12 | This part of the documentation covers all the interfaces of Niquests. For 13 | parts where Niquests depends on external libraries, we document the most 14 | important right here and provide links to the canonical documentation. 15 | 16 | 17 | Main Interface 18 | -------------- 19 | 20 | All of Niquests' functionality can be accessed by these 7 methods. 21 | They all return an instance of the :class:`Response ` object. 22 | 23 | .. autofunction:: request 24 | 25 | .. autofunction:: head 26 | .. autofunction:: get 27 | .. autofunction:: post 28 | .. autofunction:: put 29 | .. autofunction:: patch 30 | .. autofunction:: delete 31 | 32 | .. autofunction:: ahead 33 | .. autofunction:: aget 34 | .. autofunction:: apost 35 | .. autofunction:: aput 36 | .. autofunction:: apatch 37 | .. autofunction:: adelete 38 | 39 | Exceptions 40 | ---------- 41 | 42 | .. autoexception:: niquests.RequestException 43 | .. autoexception:: niquests.ConnectionError 44 | .. autoexception:: niquests.HTTPError 45 | .. autoexception:: niquests.URLRequired 46 | .. autoexception:: niquests.TooManyRedirects 47 | .. autoexception:: niquests.ConnectTimeout 48 | .. autoexception:: niquests.ReadTimeout 49 | .. autoexception:: niquests.Timeout 50 | .. autoexception:: niquests.JSONDecodeError 51 | 52 | 53 | Request Sessions 54 | ---------------- 55 | 56 | .. _sessionapi: 57 | 58 | .. autoclass:: Session 59 | :inherited-members: 60 | 61 | .. autoclass:: AsyncSession 62 | :inherited-members: 63 | 64 | Lower-Level Classes 65 | ------------------- 66 | 67 | .. autoclass:: niquests.Request 68 | :inherited-members: 69 | 70 | .. autoclass:: Response 71 | :inherited-members: 72 | 73 | .. autoclass:: AsyncResponse 74 | :inherited-members: 75 | 76 | .. warning:: AsyncResponse are only to be expected in async mode when you specify ``stream=True``. Otherwise expect the typical Response instance. 77 | 78 | .. autoclass:: RetryConfiguration 79 | :inherited-members: 80 | 81 | .. autoclass:: TimeoutConfiguration 82 | :inherited-members: 83 | 84 | Lower-Lower-Level Classes 85 | ------------------------- 86 | 87 | .. autoclass:: niquests.PreparedRequest 88 | :inherited-members: 89 | 90 | .. autoclass:: niquests.adapters.BaseAdapter 91 | :inherited-members: 92 | 93 | .. autoclass:: niquests.adapters.HTTPAdapter 94 | :inherited-members: 95 | 96 | .. autoclass:: niquests.adapters.AsyncBaseAdapter 97 | :inherited-members: 98 | 99 | .. autoclass:: niquests.adapters.AsyncHTTPAdapter 100 | :inherited-members: 101 | 102 | Authentication 103 | -------------- 104 | 105 | .. autoclass:: niquests.auth.AuthBase 106 | .. autoclass:: niquests.auth.HTTPBasicAuth 107 | .. autoclass:: niquests.auth.HTTPProxyAuth 108 | .. autoclass:: niquests.auth.HTTPDigestAuth 109 | 110 | .. autoclass:: niquests.auth.AsyncAuthBase 111 | 112 | .. _api-cookies: 113 | 114 | Cookies 115 | ------- 116 | 117 | .. autofunction:: niquests.utils.dict_from_cookiejar 118 | .. autofunction:: niquests.utils.add_dict_to_cookiejar 119 | .. autofunction:: niquests.cookies.cookiejar_from_dict 120 | 121 | .. autoclass:: niquests.cookies.RequestsCookieJar 122 | :inherited-members: 123 | 124 | .. autoclass:: niquests.cookies.CookieConflictError 125 | :inherited-members: 126 | 127 | 128 | 129 | Status Code Lookup 130 | ------------------ 131 | 132 | .. autoclass:: niquests.codes 133 | 134 | .. automodule:: niquests.status_codes 135 | 136 | 137 | Migrating to 3.x 138 | ---------------- 139 | 140 | Compared with the 2.0 release, there were relatively few backwards 141 | incompatible changes, but there are still a few issues to be aware of with 142 | this major release. 143 | 144 | 145 | Removed 146 | ~~~~~~~ 147 | 148 | * Property ``apparent_encoding`` in favor of a discrete internal inference. 149 | * Support for the legacy ``chardet`` detector in case it was present in environment. 150 | Extra ``chardet_on_py3`` is now unavailable. 151 | * Deprecated function ``get_encodings_from_content`` from utils. 152 | * Deprecated function ``get_unicode_from_response`` from utils. 153 | * BasicAuth middleware no-longer support anything else than ``bytes`` or ``str`` for username and password. 154 | * Charset fall back **ISO-8859-1** when content-type is text and no charset was specified. 155 | * Mixin classes ``RequestEncodingMixin``, and ``RequestHooksMixin`` due to OOP violations. Now deported directly into child classes. 156 | * Function ``unicode_is_ascii`` as it is part of the stable ``str`` stdlib on Python 3 or greater. 157 | * Alias function ``session`` for ``Session`` context manager that was kept for BC reasons since the v1. 158 | * pyOpenSSL/urllib3 injection in case built-in ssl module does not have SNI support as it is not the case anymore for every supported interpreters. 159 | * Constant ``DEFAULT_CA_BUNDLE_PATH``, and submodule ``certs`` due to dropping ``certifi``. 160 | * Function ``extract_zipped_paths`` because rendered useless as it was made to handle an edge case where ``certifi`` is "zipped". 161 | * Extra ``security`` when installing this package. It was previously emptied in the previous major. 162 | * Warning emitted when passing a file opened in text-mode instead of binary. urllib3.future can overrule 163 | the content-length if it detects an error. You should not encounter broken request being sent. 164 | * Support for ``simplejson`` if was present in environment. 165 | * Submodule ``compat``. 166 | * Dependency check at runtime for ``urllib3``. There's no more check and warnings at runtime for that subject. Ever. 167 | 168 | Behavioural Changes 169 | ~~~~~~~~~~~~~~~~~~~ 170 | 171 | * Niquests negotiate for a HTTP/2 connection by default, fallback to HTTP/1.1 if not available. 172 | * Support for HTTP/3 can be present by default if your platform support the pre-built wheel for qh3. 173 | * Server capability for HTTP/3 is remembered automatically (in-memory) for subsequent requests. 174 | -------------------------------------------------------------------------------- /docs/community/faq.rst: -------------------------------------------------------------------------------- 1 | .. _faq: 2 | 3 | Frequently Asked Questions 4 | ========================== 5 | 6 | This part of the documentation answers common questions about Niquests. 7 | 8 | Encoded Data? 9 | ------------- 10 | 11 | Niquests automatically decompresses gzip-encoded responses, and does 12 | its best to decode response content to unicode when possible. 13 | 14 | When either the `brotli `_ or `brotlicffi `_ 15 | package is installed, requests also decodes Brotli-encoded responses. 16 | 17 | You can get direct access to the raw response (and even the socket), 18 | if needed as well. 19 | 20 | 21 | Custom User-Agents? 22 | ------------------- 23 | 24 | Niquests allows you to easily override User-Agent strings, along with 25 | any other HTTP Header. See `documentation about headers `_. 26 | 27 | 28 | What are "hostname doesn't match" errors? 29 | ----------------------------------------- 30 | 31 | These errors occur when :ref:`SSL certificate verification ` 32 | fails to match the certificate the server responds with to the hostname 33 | Niquests thinks it's contacting. If you're certain the server's SSL setup is 34 | correct (for example, because you can visit the site with your browser). 35 | 36 | `Server-Name-Indication`_, or SNI, is an official extension to SSL where the 37 | client tells the server what hostname it is contacting. This is important 38 | when servers are using `Virtual Hosting`_. When such servers are hosting 39 | more than one SSL site they need to be able to return the appropriate 40 | certificate based on the hostname the client is connecting to. 41 | 42 | Python 3 already includes native support for SNI in their SSL modules. 43 | 44 | .. _`Server-Name-Indication`: https://en.wikipedia.org/wiki/Server_Name_Indication 45 | .. _`virtual hosting`: https://en.wikipedia.org/wiki/Virtual_hosting 46 | 47 | 48 | What are "OverwhelmedTraffic" errors? 49 | ------------------------------------- 50 | 51 | You may witness: " Cannot select a disposable connection to ease the charge ". 52 | 53 | Basically, it means that your pool of connections is saturated and we were unable to open a new connection. 54 | If you wanted to run 32 threads sharing the same ``Session`` objects, you want to allow 55 | up to 32 connections per host. 56 | 57 | Do as follow:: 58 | 59 | import niquests 60 | 61 | with niquests.Session(pool_maxsize=32) as s: 62 | ... 63 | 64 | 65 | .. note:: Previously Requests and urllib3 was non-strict and allowed infinite growth of the pool by default. This is undesirable. 66 | Upon exceeding the maximum pool capacity, urllib3 starts to create "disposable" connections that are killed as soon as possible. 67 | This behavior masked an issue and users were misinformed about it. 68 | 69 | What is "urllib3.future"? 70 | ------------------------- 71 | 72 | It is a fork of the well-known **urllib3** library, you can easily imagine that 73 | Niquests would have been completely unable to serve that much feature with the 74 | existing **urllib3** library. 75 | 76 | **urllib3.future** is independent, managed separately and completely compatible with 77 | its counterpart (API-wise). 78 | 79 | Shadow-Naming 80 | ~~~~~~~~~~~~~ 81 | 82 | Your environment may or may not include the legacy urllib3 package in addition to urllib3.future. 83 | So doing:: 84 | 85 | import urllib3 86 | 87 | May actually import either urllib3 or urllib3.future. 88 | But fear not, if your script was compatible with urllib3, it will most certainly work 89 | out-of-the-box with urllib3.future. 90 | 91 | This behavior was chosen to ensure the highest level of compatibility for your migration, 92 | ensuring the minimum friction during the migration between Requests to Niquests. 93 | 94 | Instead of importing ``urllib3`` do:: 95 | 96 | from niquests.packages import urllib3 97 | 98 | The package internally make sure you get it right everytime! 99 | 100 | Cohabitation 101 | ~~~~~~~~~~~~ 102 | 103 | You may have both urllib3 and urllib3.future installed if wished. 104 | Niquests will use the secondary entrypoint for urllib3.future internally. 105 | 106 | It does not change anything for you. You may still pass ``urllib3.Retry`` and 107 | ``urllib3.Timeout`` regardless of the cohabitation, Niquests will do 108 | the translation internally. 109 | 110 | Why are my headers are lowercased? 111 | ---------------------------------- 112 | 113 | This may come as a surprise for some of you. Until Requests-era, header keys could arrive 114 | as they were originally sent (case-sensitive). This is possible thanks to HTTP/1.1 protocol. 115 | Nonetheless, RFCs specifies that header keys are *case-insensible*, that's why both Requests 116 | and Niquests ships with ``CaseInsensitiveDict`` class. 117 | 118 | So why did we alter it then? 119 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 120 | 121 | The answer is quite simple, we support HTTP/2, and HTTP/3 over QUIC! The newer protocols enforce 122 | header case-insensitivity and we can only forward them as-is (lowercased). 123 | 124 | Can we revert this behavior? Any fallback? 125 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 126 | 127 | Yes... kind of! 128 | Niquests ships with a nice alternative to ``CaseInsensitiveDict`` that is ``kiss_headers.Headers``. 129 | You may access it through the ``oheaders`` property of your usual Response, Request and PreparedRequest. 130 | 131 | Am I obligated to install qh3? 132 | ------------------------------ 133 | 134 | No. But by default, it could be picked for installation. You may remove it safely at the cost 135 | of loosing HTTP/3 over QUIC and OCSP certificate revocation status. 136 | 137 | A shortcut would be:: 138 | 139 | $ pip uninstall qh3 140 | 141 | .. warning:: Your site-packages is shared, do it only if you are sure nothing else is using it. 142 | 143 | What are "pem lib" errors? 144 | -------------------------- 145 | 146 | Ever encountered something along:: 147 | 148 | $ SSLError: [SSL] PEM lib (_ssl.c:2532) 149 | 150 | Yes? Usually it means that you tried to load a certificate (CA or client cert) that is malformed. 151 | 152 | What does malformed means? 153 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 154 | 155 | Could be just a missing newline character *RC*, or wrong format like passing a DER file instead of a PEM 156 | encoded certificate. 157 | 158 | If none of those seems related to your situation, feel free to open an issue at https://github.com/jawah/niquests/issues 159 | 160 | Why HTTP/2 and HTTP/3 seems slower than HTTP/1.1? 161 | ------------------------------------------------- 162 | 163 | Because you are not leveraging its potential properly. Most of the time, developers tend to 164 | make a request and immediately consume the response afterward. Let's call that making OneToOne requests. 165 | HTTP/2, and HTTP/3 both requires more computational power for a single request than HTTP/1.1 (in OneToOne context). 166 | The true reason for them to exist, is not the OneToOne scenario. 167 | 168 | So, how to remedy that? 169 | 170 | You have multiple choices: 171 | 172 | 1. Using multiplexing in a synchronous context or asynchronous 173 | 2. Starting threads 174 | 3. Using async with concurrent tasks 175 | 176 | This example will quickly demonstrate, how to utilize and leverage your HTTP/2 connection with ease:: 177 | 178 | from time import time 179 | from niquests import Session 180 | 181 | #: You can adjust it as you want and verify the multiplexed advantage! 182 | REQUEST_COUNT = 10 183 | REQUEST_URL = "https://httpbin.org/delay/1" 184 | 185 | def make_requests(url: str, count: int, use_multiplexed: bool): 186 | before = time() 187 | 188 | responses = [] 189 | 190 | with Session(multiplexed=use_multiplexed) as s: 191 | for _ in range(count): 192 | responses.append(s.get(url)) 193 | print(f"request {_+1}...OK") 194 | print([r.status_code for r in responses]) 195 | 196 | print( 197 | f"{time() - before} seconds elapsed ({'multiplexed' if use_multiplexed else 'standard'})" 198 | ) 199 | 200 | #: Let's start with the same good old request one request at a time. 201 | print("> Without multiplexing:") 202 | make_requests(REQUEST_URL, REQUEST_COUNT, False) 203 | #: Now we'll take advantage of a multiplexed connection. 204 | print("> With multiplexing:") 205 | make_requests(REQUEST_URL, REQUEST_COUNT, True) 206 | 207 | .. note:: This piece of code demonstrate how to emit concurrent requests in a synchronous context without threads and async. 208 | 209 | We would gladly discuss potential implementations if needed, just open a new issue at https://github.com/jawah/niquests/issues 210 | -------------------------------------------------------------------------------- /docs/community/release-process.rst: -------------------------------------------------------------------------------- 1 | Release Process and Rules 2 | ========================= 3 | 4 | .. versionadded:: v2.6.2 5 | 6 | Starting with the version to be released after ``v2.6.2``, the following rules 7 | will govern and describe how the Niquests core team produces a new release. 8 | 9 | Major Releases 10 | -------------- 11 | 12 | A major release will include breaking changes. When it is versioned, it will 13 | be versioned as ``vX.0.0``. For example, if the previous release was 14 | ``v10.2.7`` the next version will be ``v11.0.0``. 15 | 16 | Breaking changes are changes that break backwards compatibility with prior 17 | versions. If the project were to change the ``text`` attribute on a 18 | ``Response`` object to a method, that would only happen in a Major release. 19 | 20 | Major releases may also include miscellaneous bug fixes. The core developers of 21 | Niquests are committed to providing a good user experience. This means we're 22 | also committed to preserving backwards compatibility as much as possible. Major 23 | releases will be infrequent and will need strong justifications before they are 24 | considered. 25 | 26 | Minor Releases 27 | -------------- 28 | 29 | A minor release will not include breaking changes but may include miscellaneous 30 | bug fixes. If the previous version of Niquests released was ``v10.2.7`` a minor 31 | release would be versioned as ``v10.3.0``. 32 | 33 | Minor releases will be backwards compatible with releases that have the same 34 | major version number. In other words, all versions that would start with 35 | ``v10.`` should be compatible with each other. 36 | 37 | Hotfix Releases 38 | --------------- 39 | 40 | A hotfix release will only include bug fixes that were missed when the project 41 | released the previous version. If the previous version of Niquests released 42 | ``v10.2.7`` the hotfix release would be versioned as ``v10.2.8``. 43 | 44 | Hotfixes will **not** include upgrades to vendored dependencies after 45 | ``v2.6.2`` 46 | -------------------------------------------------------------------------------- /docs/community/support.rst: -------------------------------------------------------------------------------- 1 | .. _support: 2 | 3 | Support 4 | ======= 5 | 6 | If you have questions or issues about Niquests, you may: 7 | 8 | 9 | File an Issue 10 | ------------- 11 | 12 | If you notice some unexpected behaviour in Niquests, or want to see support 13 | for a new feature, 14 | `file an issue on GitHub `_. 15 | -------------------------------------------------------------------------------- /docs/community/updates.rst: -------------------------------------------------------------------------------- 1 | .. _updates: 2 | 3 | 4 | Community Updates 5 | ================= 6 | 7 | If you'd like to stay up to date on the community and development of Niquests, 8 | there are several options: 9 | 10 | 11 | GitHub 12 | ------ 13 | 14 | The best way to track the development of Niquests is through 15 | `the GitHub repo `_. 16 | 17 | 18 | .. mdinclude:: ../../HISTORY.md 19 | -------------------------------------------------------------------------------- /docs/community/vulnerabilities.rst: -------------------------------------------------------------------------------- 1 | Vulnerability Disclosure 2 | ======================== 3 | 4 | If you think you have found a potential security vulnerability in requests, 5 | please use Tidelift vulnerability disclosure contact within the repository. 6 | 7 | If English is not your first language, please try to describe the problem and 8 | its impact to the best of your ability. For greater detail, please use your 9 | native language and we will try our best to translate it using online services. 10 | 11 | Please also include the code you used to find the problem and the shortest 12 | amount of code necessary to reproduce it. 13 | 14 | Please do not disclose this to anyone else. We will retrieve a CVE identifier 15 | if necessary and give you full credit under whatever name or alias you provide. 16 | We will only request an identifier when we have a fix and can publish it in a 17 | release. 18 | 19 | We will respect your privacy and will only publicize your involvement if you 20 | grant us permission. 21 | 22 | Previous CVEs 23 | ------------- 24 | 25 | None to date. 26 | -------------------------------------------------------------------------------- /docs/dev/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | .. include:: ../../AUTHORS.rst 5 | -------------------------------------------------------------------------------- /docs/dev/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Contributor's Guide 4 | =================== 5 | 6 | If you're reading this, you're probably interested in contributing to Niquests. 7 | Thank you very much! Open source projects live-and-die based on the support 8 | they receive from others, and the fact that you're even considering 9 | contributing to the Niquests project is *very* generous of you. 10 | 11 | This document lays out guidelines and advice for contributing to this project. 12 | If you're thinking of contributing, please start by reading this document and 13 | getting a feel for how contributing to this project works. If you have any 14 | questions, feel free to reach out to `Ahmed Tahri`_. 15 | 16 | .. _Ahmed Tahri: https://github.com/Ousret 17 | 18 | The guide is split into sections based on the type of contribution you're 19 | thinking of making, with a section that covers general guidelines for all 20 | contributors. 21 | 22 | Be Cordial 23 | ---------- 24 | 25 | **Be cordial or be on your way**. *—Kenneth Reitz* 26 | 27 | Niquests has one very important rule governing all forms of contribution, 28 | including reporting bugs or requesting features. This golden rule is 29 | "`be cordial or be on your way`_". 30 | 31 | **All contributions are welcome**, as long as 32 | everyone involved is treated with respect. 33 | 34 | .. _be cordial or be on your way: https://kennethreitz.org/essays/2013/be_cordial_or_be_on_your_way 35 | 36 | .. _early-feedback: 37 | 38 | Get Early Feedback 39 | ------------------ 40 | 41 | If you are contributing, do not feel the need to sit on your contribution until 42 | it is perfectly polished and complete. It helps everyone involved for you to 43 | seek feedback as early as you possibly can. Submitting an early, unfinished 44 | version of your contribution for feedback in no way prejudices your chances of 45 | getting that contribution accepted, and can save you from putting a lot of work 46 | into a contribution that is not suitable for the project. 47 | 48 | Contribution Suitability 49 | ------------------------ 50 | 51 | Our project maintainers have the last word on whether or not a contribution is 52 | suitable for Niquests. All contributions will be considered carefully, but from 53 | time to time, contributions will be rejected because they do not suit the 54 | current goals or needs of the project. 55 | 56 | If your contribution is rejected, don't despair! As long as you followed these 57 | guidelines, you will have a much better chance of getting your next 58 | contribution accepted. 59 | 60 | 61 | Code Contributions 62 | ------------------ 63 | 64 | Steps for Submitting Code 65 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 66 | 67 | When contributing code, you'll want to follow this checklist: 68 | 69 | 1. Fork the repository on GitHub. 70 | 2. Run the tests to confirm they all pass on your system. If they don't, you'll 71 | need to investigate why they fail. If you're unable to diagnose this 72 | yourself, raise it as a bug report by following the guidelines in this 73 | document: :ref:`bug-reports`. 74 | 3. Write tests that demonstrate your bug or feature. Ensure that they fail. 75 | 4. Make your change. 76 | 5. Run the entire test suite again, confirming that all tests pass *including 77 | the ones you just added*. 78 | 6. Send a GitHub Pull Request to the main repository's ``main`` branch. 79 | GitHub Pull Requests are the expected method of code collaboration on this 80 | project. 81 | 82 | The following sub-sections go into more detail on some of the points above. 83 | 84 | Code Review 85 | ~~~~~~~~~~~ 86 | 87 | Contributions will not be merged until they've been code reviewed. You should 88 | implement any code review feedback unless you strongly object to it. In the 89 | event that you object to the code review feedback, you should make your case 90 | clearly and calmly. If, after doing so, the feedback is judged to still apply, 91 | you must either apply the feedback or withdraw your contribution. 92 | 93 | Code Style 94 | ~~~~~~~~~~ 95 | 96 | Niquests uses a collection of tools to ensure the code base has a consistent 97 | style as it grows. We have these orchestrated using a tool called 98 | `pre-commit`_. This can be installed locally and run over your changes prior 99 | to opening a PR, and will also be run as part of the CI approval process 100 | before a change is merged. 101 | 102 | You can find the full list of formatting requirements specified in the 103 | `.pre-commit-config.yaml`_ at the top level directory of Niquests. 104 | 105 | .. _pre-commit: https://pre-commit.com/ 106 | .. _.pre-commit-config.yaml: https://github.com/jawah/niquests/blob/main/.pre-commit-config.yaml 107 | 108 | New Contributors 109 | ~~~~~~~~~~~~~~~~ 110 | 111 | If you are new or relatively new to Open Source, welcome! Niquests IS a gentle introduction to the world of Open Source. 112 | If you're concerned about how best to contribute, please consider mailing a maintainer (listed above) and 113 | asking for help. 114 | 115 | Please also check the :ref:`early-feedback` section. 116 | 117 | 118 | Documentation Contributions 119 | --------------------------- 120 | 121 | Documentation improvements are always welcome! The documentation files live in 122 | the ``docs/`` directory of the codebase. They're written in 123 | `reStructuredText`_, and use `Sphinx`_ to generate the full suite of 124 | documentation. 125 | 126 | When contributing documentation, please do your best to follow the style of the 127 | documentation files. This means a soft-limit of 79 characters wide in your text 128 | files and a semi-formal, yet friendly and approachable, prose style. 129 | 130 | When presenting Python code, use single-quoted strings (``'hello'`` instead of 131 | ``"hello"``). 132 | 133 | .. _reStructuredText: http://docutils.sourceforge.net/rst.html 134 | .. _Sphinx: http://sphinx-doc.org/index.html 135 | 136 | 137 | .. _bug-reports: 138 | 139 | Bug Reports 140 | ----------- 141 | 142 | Bug reports are hugely important! Before you raise one, though, please check 143 | through the `GitHub issues`_, **both open and closed**, to confirm that the bug 144 | hasn't been reported before. Duplicate bug reports are a huge drain on the time 145 | of other contributors, and should be avoided as much as possible. 146 | 147 | .. _GitHub issues: https://github.com/jawah/niquests/issues 148 | 149 | 150 | Feature Requests 151 | ---------------- 152 | 153 | Niquests happily accept new feature. Forever? Hopefully! 154 | -------------------------------------------------------------------------------- /docs/dev/migrate.rst: -------------------------------------------------------------------------------- 1 | .. _migrate: 2 | 3 | Requests → Niquests Guide 4 | ========================= 5 | 6 | If you're reading this, you're probably interested in Niquests. We're thrilled to have 7 | you onboard. 8 | 9 | This section will cover two use cases: 10 | 11 | - I am a developer that regularly drive Requests 12 | - I am a library maintainer that depend on Requests 13 | 14 | Developer migration 15 | ------------------- 16 | 17 | Niquests aims to be as compatible as possible with Requests, and that is 18 | with confidence that you can migrate to Niquests without breaking changes. 19 | 20 | .. code:: python 21 | 22 | import requests 23 | requests.get(...) 24 | 25 | Would turn into either 26 | 27 | .. code:: python 28 | 29 | import niquests 30 | niquests.get(...) 31 | 32 | Or simply 33 | 34 | .. code:: python 35 | 36 | import niquests as requests 37 | requests.get(...) 38 | 39 | .. tip:: If you were used to use ``urllib3.Timeout`` or ``urllib3.Retry`` you can either keep them as-is or use our fully compatible ``niquests.RetryConfiguration`` or ``niquests.TimeoutConfiguration`` instead. 40 | 41 | If you were used to depends on urllib3. 42 | 43 | .. code:: python 44 | 45 | from urllib3 import Timeout 46 | import requests 47 | 48 | Will now become: 49 | 50 | .. code:: python 51 | 52 | import niquests 53 | from niquests.packages.urllib3 import Timeout 54 | 55 | .. note:: urllib3 is safely aliased as ``niquests.packages.urllib3``. Using the alias provided by Niquests is safer. 56 | 57 | Maintainer migration 58 | -------------------- 59 | 60 | In order to migrate your library with confidence, you'll have to also adjust your tests. 61 | The library itself (sources) should be really easy to migrate (cf. developer migration) 62 | but the tests may be harder to adapt. 63 | 64 | The main reason behind this difficulty is often related to a strong tie with third-party 65 | mocking library such as ``responses``. 66 | 67 | To overcome this, we will introduce you to a clever bypass. If you are using pytest, do the 68 | following in your ``conftest.py``, see https://docs.pytest.org/en/6.2.x/fixture.html#conftest-py-sharing-fixtures-across-multiple-files 69 | for more information. (The goal would simply to execute the following piece of code before the tests) 70 | 71 | .. code:: python 72 | 73 | from sys import modules 74 | 75 | import niquests 76 | import requests 77 | from niquests.packages import urllib3 78 | 79 | # the mock utility 'response' only works with 'requests' 80 | modules["requests"] = niquests 81 | modules["requests.adapters"] = niquests.adapters 82 | modules["requests.exceptions"] = niquests.exceptions 83 | modules["requests.compat"] = requests.compat 84 | modules["requests.packages.urllib3"] = urllib3 85 | 86 | .. warning:: This code sample is only to be executed in a development environment, it permit to fool the third-party dependencies that have a strong tie on Requests. 87 | 88 | .. warning:: Some pytest plugins may load/import Requests at startup. 89 | Disable the plugin auto-loading first by either passing ``PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`` (in environment) 90 | or ``pytest -p "no:pytest-betamax"`` in CLI parameters. Replace ``pytest-betamax`` by the name of the target plugin. 91 | To find out the name of the plugin auto-loaded, execute ``pytest --trace-config`` as the name aren't usually what 92 | you would expect them to be. 93 | -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/docs/images/banner.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :og:description: Niquests is an elegant and simple HTTP library for Python, built for human beings. It 2 | is designed to be a drop-in replacement for Requests that is no longer under feature freeze. 3 | 4 | Supports HTTP/1.1, HTTP/2 and HTTP/3 out-of-the-box without breaking a sweat! 5 | 6 | .. meta:: 7 | :description: Niquests is an elegant and simple HTTP library for Python, built for human beings. It is designed to be a drop-in replacement for Requests that is no longer under feature freeze. Supports HTTP/1.1, HTTP/2 and HTTP/3 out-of-the-box without breaking a sweat! 8 | :keywords: Python Niquests, Niquests, Python Requests Replacement, Python Requests Alternative, Requests HTTP/2, Requests HTTP/3, Async Requests, Python Requests Documentation, Python Niquests Documentation, Python Requests Drop-in Replacement, Python SSE Client, server side event client, WebSocket Client 9 | 10 | Niquests: HTTP for Humans™ 11 | ========================== 12 | 13 | Release v\ |version| (:ref:`Installation `) 14 | 15 | 16 | .. image:: https://img.shields.io/pypi/dm/niquests.svg 17 | :target: https://pypistats.org/packages/niquests 18 | :alt: Niquests Downloads Per Month Badge 19 | 20 | .. image:: https://img.shields.io/pypi/l/niquests.svg 21 | :target: https://pypi.org/project/niquests/ 22 | :alt: License Badge 23 | 24 | .. image:: https://img.shields.io/pypi/pyversions/niquests.svg 25 | :target: https://pypi.org/project/niquests/ 26 | :alt: Python Version Support Badge 27 | 28 | **Niquests** is an elegant and simple HTTP library for Python, built for human beings. It 29 | is designed to be a drop-in replacement for **Requests** that is no longer under feature freeze. 30 | 31 | Supports HTTP/1.1, HTTP/2 and HTTP/3 out-of-the-box without breaking a sweat! 32 | 33 | ------------------- 34 | 35 | **Behold, the power of Niquests** 36 | 37 | .. raw:: html 38 | 39 |
 40 |    >>> import niquests
 41 |    >>> s = niquests.Session(resolver="doh+google://")
 42 |    >>> r = s.get('https://pie.dev/basic-auth/user/pass', auth=('user', 'pass'))
 43 |    >>> r.status_code
 44 |    200
 45 |    >>> r.headers['content-type']
 46 |    'application/json; charset=utf-8'
 47 |    >>> r.oheaders.content_type.charset
 48 |    'utf-8'
 49 |    >>> r.encoding
 50 |    'utf-8'
 51 |    >>> r.text
 52 |    '{"authenticated": true, ...'
 53 |    >>> r.json()
 54 |    {'authenticated': True, ...}
 55 |    >>> r
 56 |    <Response HTTP/3 [200]>
 57 |    >>> r.ocsp_verified
 58 |    True
 59 |    >>> r.conn_info.established_latency
 60 |    datetime.timedelta(microseconds=38)
 61 |    
62 | 63 | **Niquests** allows you to send HTTP/1.1, HTTP/2 and HTTP/3 requests extremely easily. 64 | There's no need to manually add query strings to your 65 | URLs, or to form-encode your POST data. Keep-alive and HTTP connection pooling 66 | are 100% automatic, thanks to `urllib3.future `_. 67 | 68 | Beloved Features 69 | ---------------- 70 | 71 | Niquests is ready for today's web. 72 | 73 | - DNS over HTTPS, DNS over QUIC, DNS over TLS, and DNS over UDP 74 | - Automatic Content Decompression and Decoding 75 | - OS truststore by default, no more certifi! 76 | - OCSP Certificate Revocation Verification 77 | - Advanced connection timings inspection 78 | - In-memory certificates (CAs, and mTLS) 79 | - Browser-style TLS/SSL Verification 80 | - Sessions with Cookie Persistence 81 | - Keep-Alive & Connection Pooling 82 | - International Domains and URLs 83 | - Automatic honoring of `.netrc` 84 | - Basic & Digest Authentication 85 | - Familiar `dict`–like Cookies 86 | - Network settings fine-tuning 87 | - HTTP/2 with prior knowledge 88 | - Object-oriented headers 89 | - Multi-part File Uploads 90 | - Post-Quantum Security 91 | - Chunked HTTP Requests 92 | - Fully type-annotated! 93 | - SOCKS Proxy Support 94 | - Connection Timeouts 95 | - Streaming Downloads 96 | - HTTP/2 by default 97 | - HTTP/3 over QUIC 98 | - Early Responses 99 | - Happy Eyeballs 100 | - Multiplexed! 101 | - Thread-safe! 102 | - WebSocket! 103 | - Trailers! 104 | - DNSSEC! 105 | - Async! 106 | - SSE! 107 | 108 | Niquests officially supports Python 3.7+, and runs great on PyPy. 109 | 110 | 111 | The User Guide 112 | -------------- 113 | 114 | This part of the documentation, which is mostly prose, begins with some 115 | background information about Niquests, then focuses on step-by-step 116 | instructions for getting the most out of Niquests. 117 | 118 | .. toctree:: 119 | :maxdepth: 2 120 | 121 | user/install 122 | user/quickstart 123 | user/advanced 124 | user/authentication 125 | 126 | 127 | The Community Guide 128 | ------------------- 129 | 130 | This part of the documentation, which is mostly prose, details the 131 | Niquests ecosystem and community. 132 | 133 | .. toctree:: 134 | :maxdepth: 2 135 | 136 | dev/migrate 137 | dev/httpx 138 | community/extensions 139 | community/faq 140 | community/support 141 | community/vulnerabilities 142 | community/release-process 143 | 144 | .. toctree:: 145 | :maxdepth: 1 146 | 147 | community/updates 148 | 149 | The API Documentation / Guide 150 | ----------------------------- 151 | 152 | If you are looking for information on a specific function, class, or method, 153 | this part of the documentation is for you. 154 | 155 | .. toctree:: 156 | :maxdepth: 2 157 | 158 | api 159 | 160 | 161 | The Contributor Guide 162 | --------------------- 163 | 164 | If you want to contribute to the project, this part of the documentation is for 165 | you. 166 | 167 | .. toctree:: 168 | :maxdepth: 3 169 | 170 | dev/contributing 171 | dev/authors 172 | 173 | There are no more guides. You are now guideless. 174 | Good luck. 175 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 1>NUL 2>NUL 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Requests.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Requests.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Pinning to avoid unexpected breakages. 2 | # Used by RTD to generate docs. 3 | Sphinx==7.2.6 4 | sphinx-copybutton==0.5.2 5 | urllib3.future>=2.12.900 6 | wassima>=1,<2 7 | kiss_headers>=2,<4 8 | furo>=2023.9.10 9 | sphinx-mdinclude==0.6.2 10 | sphinxcontrib-googleanalytics==0.4 11 | sphinx-inline-tabs==2023.4.21 12 | sphinxext-opengraph>=0.9.1,<0.10 13 | sphinx-intl>=2.3.1,<3 14 | -------------------------------------------------------------------------------- /docs/user/authentication.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: Authentication with Niquests. Many web services require authentication, and there are many different types. Below, we outline various forms of authentication available in Niquests, from the simple to the complex. OAuth2, JWT, etc... 3 | :keywords: Python Auth, Authentication Requests, OAuth Python, Bearer Python, Async Authentication, Niquests Auth, Niquests Authentication 4 | 5 | .. _authentication: 6 | 7 | Authentication 8 | ============== 9 | 10 | This document discusses using various kinds of authentication with Niquests. 11 | 12 | Many web services require authentication, and there are many different types. 13 | Below, we outline various forms of authentication available in Niquests, from 14 | the simple to the complex. 15 | 16 | 17 | Basic Authentication 18 | -------------------- 19 | 20 | Many web services that require authentication accept HTTP Basic Auth. This is 21 | the simplest kind, and Niquests supports it straight out of the box. 22 | 23 | Making requests with HTTP Basic Auth is very simple:: 24 | 25 | >>> from niquests.auth import HTTPBasicAuth 26 | >>> basic = HTTPBasicAuth('user', 'pass') 27 | >>> niquests.get('https://httpbin.org/basic-auth/user/pass', auth=basic) 28 | 29 | 30 | In fact, HTTP Basic Auth is so common that Niquests provides a handy shorthand 31 | for using it:: 32 | 33 | >>> niquests.get('https://httpbin.org/basic-auth/user/pass', auth=('user', 'pass')) 34 | 35 | 36 | Providing the credentials in a tuple like this is exactly the same as the 37 | ``HTTPBasicAuth`` example above. 38 | 39 | For DNS 40 | ~~~~~~~ 41 | 42 | Doing basic authorization using for DNS over HTTPS resolver can be done easily. 43 | You must provide the user and pass into the DNS url as such:: 44 | 45 | from niquests import Session 46 | 47 | with Session(resolver="doh://user:pass@my-resolver.tld") as s: 48 | resp = s.get("https://httpbingo.org/get") 49 | 50 | Passing a bearer token 51 | ---------------------- 52 | 53 | You may use ``auth=my_token`` as a shortcut to passing ``headers={"Authorization": f"Bearer {my_token}"}`` in 54 | get, post, request, etc... 55 | 56 | .. note:: If you pass a token with its custom prefix, it will be taken and passed as-is. e.g. ``auth="NotBearer eyDdx.."`` 57 | 58 | For DNS 59 | ~~~~~~~ 60 | 61 | Doing a bearer token using for DNS over HTTPS resolver can be done easily. 62 | You must provide the token directly into the DNS url as such:: 63 | 64 | from niquests import Session 65 | 66 | with Session(resolver="doh://token@my-resolver.tld") as s: 67 | resp = s.get("https://httpbingo.org/get") 68 | 69 | netrc Authentication 70 | ~~~~~~~~~~~~~~~~~~~~ 71 | 72 | If no authentication method is given with the ``auth`` argument and the 73 | Authorization header has not been set, Niquests will attempt to get the 74 | authentication credentials for the URL's hostname from the user's netrc file. 75 | 76 | If credentials for the hostname are found, the request is sent with HTTP Basic 77 | Auth. 78 | 79 | 80 | Digest Authentication 81 | --------------------- 82 | 83 | Another very popular form of HTTP Authentication is Digest Authentication, 84 | and Niquests supports this out of the box as well:: 85 | 86 | >>> from niquests.auth import HTTPDigestAuth 87 | >>> url = 'https://httpbin.org/digest-auth/auth/user/pass' 88 | >>> niquests.get(url, auth=HTTPDigestAuth('user', 'pass')) 89 | 90 | 91 | 92 | OAuth 1 Authentication 93 | ---------------------- 94 | 95 | A common form of authentication for several web APIs is OAuth. The ``requests-oauthlib`` 96 | library allows Niquests users to easily make OAuth 1 authenticated requests:: 97 | 98 | >>> import niquests 99 | >>> from requests_oauthlib import OAuth1 100 | 101 | >>> url = 'https://api.twitter.com/1.1/account/verify_credentials.json' 102 | >>> auth = OAuth1('YOUR_APP_KEY', 'YOUR_APP_SECRET', 103 | ... 'USER_OAUTH_TOKEN', 'USER_OAUTH_TOKEN_SECRET') 104 | 105 | >>> niquests.get(url, auth=auth) 106 | 107 | 108 | For more information on how to OAuth flow works, please see the official `OAuth`_ website. 109 | For examples and documentation on requests-oauthlib, please see the `requests_oauthlib`_ 110 | repository on GitHub 111 | 112 | OAuth 2 and OpenID Connect Authentication 113 | ----------------------------------------- 114 | 115 | The ``requests-oauthlib`` library also handles OAuth 2, the authentication mechanism 116 | underpinning OpenID Connect. See the `requests-oauthlib OAuth2 documentation`_ for 117 | details of the various OAuth 2 credential management flows: 118 | 119 | * `Web Application Flow`_ 120 | * `Mobile Application Flow`_ 121 | * `Legacy Application Flow`_ 122 | * `Backend Application Flow`_ 123 | 124 | Other Authentication 125 | -------------------- 126 | 127 | Niquests is designed to allow other forms of authentication to be easily and 128 | quickly plugged in. Members of the open-source community frequently write 129 | authentication handlers for more complicated or less commonly-used forms of 130 | authentication. Some of the best have been brought together under the 131 | `Requests organization`_, including: 132 | 133 | - Kerberos_ 134 | - NTLM_ 135 | 136 | If you want to use any of these forms of authentication, go straight to their 137 | GitHub page and follow the instructions. 138 | 139 | 140 | New Forms of Authentication 141 | --------------------------- 142 | 143 | If you can't find a good implementation of the form of authentication you 144 | want, you can implement it yourself. Niquests makes it easy to add your own 145 | forms of authentication. 146 | 147 | To do so, subclass :class:`AuthBase ` and implement the 148 | ``__call__()`` method:: 149 | 150 | >>> import niquests 151 | >>> class MyAuth(niquests.auth.AuthBase): 152 | ... def __call__(self, r): 153 | ... # Implement my authentication 154 | ... return r 155 | ... 156 | >>> url = 'https://httpbin.org/get' 157 | >>> niquests.get(url, auth=MyAuth()) 158 | 159 | 160 | When an authentication handler is attached to a request, 161 | it is called during request setup. The ``__call__`` method must therefore do 162 | whatever is required to make the authentication work. Some forms of 163 | authentication will additionally add hooks to provide further functionality. 164 | 165 | Further examples can be found under the `Requests organization`_ and in the 166 | ``auth.py`` file. 167 | 168 | .. tip:: As Niquests support async http requests natively you can create async authentication classes by just inheriting from :class:`AsyncAuthBase ` 169 | 170 | .. _OAuth: https://oauth.net/ 171 | .. _requests_oauthlib: https://github.com/requests/requests-oauthlib 172 | .. _requests-oauthlib OAuth2 documentation: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html 173 | .. _Web Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#web-application-flow 174 | .. _Mobile Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#mobile-application-flow 175 | .. _Legacy Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#legacy-application-flow 176 | .. _Backend Application Flow: https://requests-oauthlib.readthedocs.io/en/latest/oauth2_workflow.html#backend-application-flow 177 | .. _Kerberos: https://github.com/requests/requests-kerberos 178 | .. _NTLM: https://github.com/requests/requests-ntlm 179 | .. _Requests organization: https://github.com/requests 180 | -------------------------------------------------------------------------------- /docs/user/install.rst: -------------------------------------------------------------------------------- 1 | .. meta:: 2 | :description: Fast track to install Niquests and support latest protocols and security features. Install via pip, uv or poetry immediately! Discover available and most used optional extensions like zstandard, brotli, and WebSocket support. 3 | :keywords: Install Requests Python, Pip http client, Uv http install, Uv http client, Pip Niquests, Uv Niquests, Python WebSocket, SOCKS proxy, proxy 4 | 5 | .. _install: 6 | 7 | Installation of Niquests 8 | ======================== 9 | 10 | This part of the documentation covers the installation of Niquests. 11 | The first step to using any software package is getting it properly installed. 12 | 13 | 14 | $ Install! 15 | ---------- 16 | 17 | To install Niquests, simply run this simple command in your terminal of choice:: 18 | 19 | $ python -m pip install niquests 20 | 21 | 22 | Dependencies 23 | ~~~~~~~~~~~~ 24 | 25 | Niquests has currently 3 mandatory dependencies. 26 | 27 | - **wassima** 28 | - This package allows Niquests a seamless access to your native OS trust store. It avoids to rely on ``certify``. 29 | - **urllib3-future** 30 | - The direct and immediate replacement for the well known ``urllib3`` package. Serve as the Niquests core. 31 | - **charset-normalizer** 32 | - A clever encoding detector that helps provide a smooth user experience when dealing with legacy resources. 33 | 34 | *urllib3-future* itself depends on three other dependencies. Two of them are installed everytime (mandatory). 35 | 36 | - **h11** 37 | - A state-machine to speak HTTP/1.1 38 | - **jh2** 39 | - A state-machine to speak HTTP/2 40 | - **qh3** 41 | - A QUIC stack with a HTTP/3 state-machine 42 | 43 | .. note:: 44 | **qh3** may not be installed on your machine even if **urllib3-future** depends on it. 45 | That dependency is BOTH required AND optional. 46 | There's a lot of markers in the requirement definition to prevent someone to accidentally try compile the source. 47 | **qh3** is not pure Python and ship a ton of prebuilt wheels, but unfortunately it is 48 | impossible to ship every possible combination of system/architecture. 49 | e.g. this is why doing ``pip install niquests`` on a riscv linux distro will NOT bring **qh3**. but will for arm64, i686 or x86_64. 50 | 51 | .. warning:: 52 | Depending on your environment and installation order you could be surprised that ``urllib3-future`` replaces 53 | ``urllib3`` in your environment. Fear not, as this package was perfectly designed to allows the best 54 | backward-compatibility possible for end-users. 55 | Installing Niquests affect other packages, as they will use ``urllib3-future`` instead of ``urllib3``. 56 | The prevalent changes are as follow: No longer using HTTP/1 by default, not depending on ``http.client``, can 57 | negotiate HTTP/2, and HTTP/3, backward compatible with old Python and OpenSSL, not opinionated on other SSL backend, 58 | and finally faster. This list isn't exhaustive. Any issues encountered regarding this cohabitation will be handled 59 | at https://github.com/jawah/urllib3.future with the utmost priority. Find deeper rational in the exposed repository. 60 | 61 | .. note:: 62 | Doing a ``pip install --force-reinstall urllib3`` will restore the legacy urllib3 in a flash if needed. 63 | At the cost of loosing the perfect backward compatibility with third party plugins (using Niquests). 64 | 65 | Extras 66 | ~~~~~~ 67 | 68 | Niquests come with a few extras that requires you to install the package with a specifier. 69 | 70 | - **ws** 71 | 72 | To benefit from the integrated WebSocket experience, you may install Niquests as follow:: 73 | 74 | $ python -m pip install niquests[ws] 75 | 76 | - **socks** 77 | 78 | SOCKS proxies can be used in Niquests, at the sole condition to have:: 79 | 80 | $ python -m pip install niquests[socks] 81 | 82 | - **Brotli**, **Zstandard** (zstd) and **orjson** 83 | 84 | Niquests can run significantly faster when your environment is capable of decompressing Brotli, and Zstd. 85 | Also, we took the liberty to allows using the alternative json decoder ``orjson`` that is faster than the 86 | standard json library. 87 | 88 | To immediately benefit from this, run:: 89 | 90 | $ python -m pip install niquests[speedups] 91 | 92 | .. note:: You may at your own discretion choose multiple options such as ``pip install niquests[socks,ws]``. 93 | 94 | .. note:: You can install every optionals by running ``pip install niquests[full]``. 95 | 96 | If you don't want ``orjson`` to be present and only zstd for example, run:: 97 | 98 | $ python -m pip install niquests[zstd] 99 | 100 | - **http3** or/and **ocsp** 101 | 102 | As explained higher in this section, our HTTP/3 implementation depends on you having ``qh3`` installed. And it may not 103 | be the case depending on your environment. 104 | 105 | To force install ``qh3`` run the installation using:: 106 | 107 | $ python -m pip install niquests[http3] 108 | 109 | 110 | .. note:: ``ocsp`` extra is a mere alias of ``http3``. Our OCSP client depends on **qh3** inners anyway. 111 | 112 | - **full** 113 | 114 | If by any chance you wanted to get the full list of (extra) features, you may install Niquests with:: 115 | 116 | $ python -m pip install niquests[full] 117 | 118 | Instead of joining the long list of extras like ``zstd,socks,ws`` for example. 119 | 120 | Get the Source Code 121 | ------------------- 122 | 123 | Niquests is actively developed on GitHub, where the code is 124 | `always available `_. 125 | 126 | You can either clone the public repository:: 127 | 128 | $ git clone https://github.com/jawah/niquests.git 129 | 130 | Or, download the `tarball `_:: 131 | 132 | $ curl -OL https://github.com/jawah/niquests/tarball/main 133 | # optionally, zipball is also available (for Windows users). 134 | 135 | Once you have a copy of the source, you can embed it in your own Python 136 | package, or install it into your site-packages easily:: 137 | 138 | $ cd niquests 139 | $ python -m pip install . 140 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | 6 | import nox 7 | 8 | 9 | def tests_impl( 10 | session: nox.Session, 11 | extras: str = "socks,ws", 12 | cohabitation: bool | None = False, 13 | ) -> None: 14 | # Install deps and the package itself. 15 | session.install("-r", "requirements-dev.txt") 16 | session.install(f".[{extras}]", silent=False) 17 | 18 | # Show the pip version. 19 | session.run("pip", "--version") 20 | session.run("python", "--version") 21 | 22 | if cohabitation is True: 23 | session.run("pip", "install", "urllib3") 24 | session.run("python", "-m", "niquests.help") 25 | elif cohabitation is None: 26 | session.run("pip", "install", "urllib3") 27 | session.run("pip", "uninstall", "-y", "urllib3") 28 | session.run("python", "-m", "niquests.help") 29 | 30 | session.run( 31 | "python", 32 | "-m", 33 | "coverage", 34 | "run", 35 | "--parallel-mode", 36 | "-m", 37 | "pytest", 38 | "-v", 39 | "-ra", 40 | f"--color={'yes' if 'GITHUB_ACTIONS' in os.environ else 'auto'}", 41 | "--tb=native", 42 | "--durations=10", 43 | "--strict-config", 44 | "--strict-markers", 45 | *(session.posargs or ("tests/",)), 46 | env={ 47 | "PYTHONWARNINGS": "always::DeprecationWarning", 48 | "NIQUESTS_STRICT_OCSP": "1", 49 | }, 50 | ) 51 | 52 | 53 | @nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy"]) 54 | def test(session: nox.Session) -> None: 55 | tests_impl(session) 56 | 57 | 58 | @nox.session( 59 | python=[ 60 | "3.11", 61 | ] 62 | ) 63 | def test_cohabitation(session: nox.Session) -> None: 64 | tests_impl(session, cohabitation=True) 65 | tests_impl(session, cohabitation=None) 66 | 67 | 68 | @nox.session 69 | def lint(session: nox.Session) -> None: 70 | session.install("pre-commit") 71 | session.run("pre-commit", "run", "--all-files") 72 | 73 | 74 | @nox.session 75 | def docs(session: nox.Session) -> None: 76 | session.install("-r", "docs/requirements.txt") 77 | session.install(".[socks]") 78 | 79 | session.chdir("docs") 80 | if os.path.exists("_build"): 81 | shutil.rmtree("_build") 82 | session.run("sphinx-build", "-b", "html", ".", "_build/html") 83 | 84 | 85 | @nox.session 86 | def i18n(session: nox.Session) -> None: 87 | session.install("-r", "docs/requirements.txt") 88 | session.install(".[socks]") 89 | 90 | session.chdir("docs") 91 | 92 | if os.path.exists("_build"): 93 | shutil.rmtree("_build") 94 | 95 | session.run("sphinx-build", "-b", "gettext", ".", "_build/gettext") 96 | session.run("sphinx-intl", "update", "-p", "_build/gettext", "-l", "fr_FR") 97 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.6.0,<2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "niquests" 7 | description = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in replacement for Requests, which is under feature freeze." 8 | readme = "README.md" 9 | license-files = { paths = ["LICENSE"] } 10 | license = "Apache-2.0" 11 | keywords = ["requests", "http2", "http3", "QUIC", "http", "https", "http client", "http/1.1", "ocsp", "revocation", "tls", "multiplexed", "dns-over-quic", "doq", "dns-over-tls", "dot", "dns-over-https", "doh", "dnssec", "websocket", "sse"] 12 | authors = [ 13 | {name = "Kenneth Reitz", email = "me@kennethreitz.org"} 14 | ] 15 | maintainers = [ 16 | {name = "Ahmed R. TAHRI", email="tahri.ahmed@proton.me"}, 17 | ] 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Environment :: Web Environment", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: Apache Software License", 23 | "Natural Language :: English", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Programming Language :: Python :: 3 :: Only", 35 | "Programming Language :: Python :: Implementation :: CPython", 36 | "Programming Language :: Python :: Implementation :: PyPy", 37 | "Topic :: Internet :: WWW/HTTP", 38 | "Topic :: Software Development :: Libraries", 39 | ] 40 | requires-python = ">=3.7" 41 | dynamic = ["version"] 42 | dependencies = [ 43 | "charset_normalizer>=2,<4", 44 | "urllib3.future>=2.12.900,<3", 45 | "wassima>=1.0.1,<2", 46 | ] 47 | 48 | [project.optional-dependencies] 49 | socks = [ 50 | "urllib3.future[socks]", 51 | ] 52 | http3 = [ 53 | "urllib3.future[qh3]", 54 | ] 55 | ocsp = [ 56 | "urllib3.future[qh3]", 57 | ] 58 | ws = [ 59 | "urllib3.future[ws]", 60 | ] 61 | speedups = [ 62 | "orjson>=3,<4", 63 | "urllib3.future[zstd,brotli]", 64 | ] 65 | zstd = [ 66 | "urllib3.future[zstd]", 67 | ] 68 | brotli = [ 69 | "urllib3.future[brotli]", 70 | ] 71 | full = [ 72 | "orjson>=3,<4", 73 | "urllib3.future[zstd,brotli,ws,socks]", 74 | ] 75 | 76 | [project.urls] 77 | "Changelog" = "https://github.com/jawah/niquests/blob/main/HISTORY.md" 78 | "Documentation" = "https://niquests.readthedocs.io" 79 | "Code" = "https://github.com/jawah/niquests" 80 | "Issue tracker" = "https://github.com/jawah/niquests/issues" 81 | 82 | [tool.hatch.version] 83 | path = "src/niquests/__version__.py" 84 | 85 | [tool.hatch.build.targets.sdist] 86 | include = [ 87 | "/docs", 88 | "/src", 89 | "/tests", 90 | "/requirements-dev.txt", 91 | "/HISTORY.md", 92 | "/README.md", 93 | "/SECURITY.md", 94 | "/AUTHORS.rst", 95 | "/LICENSE", 96 | "/NOTICE", 97 | "/Makefile", 98 | ] 99 | 100 | [tool.hatch.build.targets.wheel] 101 | packages = [ 102 | "src/niquests", 103 | ] 104 | 105 | [tool.ruff] 106 | line-length = 128 107 | 108 | [tool.ruff.lint] 109 | select = [ 110 | "E", # pycodestyle 111 | "F", # Pyflakes 112 | "W", # pycodestyle 113 | "I", # isort 114 | "U", # pyupgrade 115 | ] 116 | 117 | [tool.ruff.lint.isort] 118 | required-imports = ["from __future__ import annotations"] 119 | 120 | [[tool.mypy.overrides]] 121 | module = ["niquests.packages.*"] 122 | ignore_missing_imports = true 123 | 124 | [tool.pytest.ini_options] 125 | addopts = "--doctest-modules" 126 | doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS" 127 | minversion = "6.2" 128 | testpaths = ["tests"] 129 | filterwarnings = [ 130 | "error", 131 | '''ignore:'parse_authorization_header' is deprecated and will be removed:DeprecationWarning''', 132 | '''ignore:The 'set_digest' method is deprecated and will be removed:UserWarning''', 133 | '''ignore:Passing bytes as a header value is deprecated and will:DeprecationWarning''', 134 | '''ignore:The 'JSONIFY_PRETTYPRINT_REGULAR' config key is deprecated and will:DeprecationWarning''', 135 | '''ignore:unclosed .*:ResourceWarning''', 136 | '''ignore:Parsed a negative serial number:cryptography.utils.CryptographyDeprecationWarning''', 137 | '''ignore:A plugin raised an exception during an old-style hookwrapper teardown''', 138 | '''ignore:.*:pytest.PytestUnraisableExceptionWarning''', 139 | ] 140 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -e .[socks] 2 | pytest>=2.8.0,<=7.4.4 3 | coverage>=7.2.7,<7.7 4 | pytest-httpbin>=2,<3 5 | pytest-asyncio>=0.21.1,<1.0 6 | httpbin==0.10.2 7 | trustme 8 | cryptography<40.0.0; python_version <= '3.8' 9 | cryptography<44; python_version > '3.8' 10 | werkzeug>=2.2.3,<=3.0.2 11 | -------------------------------------------------------------------------------- /src/niquests/__init__.py: -------------------------------------------------------------------------------- 1 | # __ 2 | # /__) _ _ _ _ _/ _ 3 | # / ( (- (/ (/ (- _) / _) 4 | # / 5 | 6 | """ 7 | Niquests HTTP Library 8 | ~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | Niquests is an HTTP library, written in Python, for human beings. 11 | Basic GET usage: 12 | 13 | >>> import niquests 14 | >>> r = niquests.get('https://www.python.org') 15 | >>> r.status_code 16 | 200 17 | >>> b'Python is a programming language' in r.content 18 | True 19 | 20 | ... or POST: 21 | 22 | >>> payload = dict(key1='value1', key2='value2') 23 | >>> r = niquests.post('https://httpbin.org/post', data=payload) 24 | >>> print(r.text) 25 | { 26 | ... 27 | "form": { 28 | "key1": "value1", 29 | "key2": "value2" 30 | }, 31 | ... 32 | } 33 | 34 | The other HTTP methods are supported - see `requests.api`. Full documentation 35 | is at . 36 | 37 | :copyright: (c) 2017 by Kenneth Reitz. 38 | :license: Apache 2.0, see LICENSE for more details. 39 | """ 40 | 41 | from __future__ import annotations 42 | 43 | # Set default logging handler to avoid "No handler found" warnings. 44 | import logging 45 | import warnings 46 | from logging import NullHandler 47 | 48 | from ._compat import HAS_LEGACY_URLLIB3 49 | from .packages.urllib3 import ( 50 | Retry as RetryConfiguration, 51 | ) 52 | from .packages.urllib3 import ( 53 | Timeout as TimeoutConfiguration, 54 | ) 55 | from .packages.urllib3.exceptions import DependencyWarning 56 | 57 | # urllib3's DependencyWarnings should be silenced. 58 | warnings.simplefilter("ignore", DependencyWarning) 59 | 60 | # ruff: noqa: E402 61 | from . import utils 62 | from .__version__ import ( 63 | __author__, 64 | __author_email__, 65 | __build__, 66 | __cake__, 67 | __copyright__, 68 | __description__, 69 | __license__, 70 | __title__, 71 | __url__, 72 | __version__, 73 | ) 74 | from .api import delete, get, head, options, patch, post, put, request 75 | from .async_api import ( 76 | delete as adelete, 77 | ) 78 | from .async_api import ( 79 | get as aget, 80 | ) 81 | from .async_api import ( 82 | head as ahead, 83 | ) 84 | from .async_api import ( 85 | options as aoptions, 86 | ) 87 | from .async_api import ( 88 | patch as apatch, 89 | ) 90 | from .async_api import ( 91 | post as apost, 92 | ) 93 | from .async_api import ( 94 | put as aput, 95 | ) 96 | from .async_api import ( 97 | request as arequest, 98 | ) 99 | from .async_session import AsyncSession 100 | from .exceptions import ( 101 | ConnectionError, 102 | ConnectTimeout, 103 | FileModeWarning, 104 | HTTPError, 105 | JSONDecodeError, 106 | ReadTimeout, 107 | RequestException, 108 | RequestsDependencyWarning, 109 | Timeout, 110 | TooManyRedirects, 111 | URLRequired, 112 | ) 113 | from .models import AsyncResponse, PreparedRequest, Request, Response 114 | from .sessions import Session 115 | from .status_codes import codes 116 | 117 | logging.getLogger(__name__).addHandler(NullHandler()) 118 | 119 | __all__ = ( 120 | "RequestsDependencyWarning", 121 | "utils", 122 | "__author__", 123 | "__author_email__", 124 | "__build__", 125 | "__cake__", 126 | "__copyright__", 127 | "__description__", 128 | "__license__", 129 | "__title__", 130 | "__url__", 131 | "__version__", 132 | "delete", 133 | "get", 134 | "head", 135 | "options", 136 | "patch", 137 | "post", 138 | "put", 139 | "request", 140 | "adelete", 141 | "aget", 142 | "ahead", 143 | "aoptions", 144 | "apatch", 145 | "apost", 146 | "aput", 147 | "arequest", 148 | "ConnectionError", 149 | "ConnectTimeout", 150 | "FileModeWarning", 151 | "HTTPError", 152 | "JSONDecodeError", 153 | "ReadTimeout", 154 | "RequestException", 155 | "Timeout", 156 | "TooManyRedirects", 157 | "URLRequired", 158 | "PreparedRequest", 159 | "Request", 160 | "Response", 161 | "Session", 162 | "codes", 163 | "AsyncSession", 164 | "AsyncResponse", 165 | "TimeoutConfiguration", 166 | "RetryConfiguration", 167 | "HAS_LEGACY_URLLIB3", 168 | ) 169 | -------------------------------------------------------------------------------- /src/niquests/__version__.py: -------------------------------------------------------------------------------- 1 | # .-. .-. .-. . . .-. .-. .-. .-. 2 | # |( |- |.| | | |- `-. | `-. 3 | # ' ' `-' `-`.`-' `-' `-' ' `-' 4 | 5 | from __future__ import annotations 6 | 7 | __title__: str = "niquests" 8 | __description__: str = "Python HTTP for Humans." 9 | __url__: str = "https://niquests.readthedocs.io" 10 | 11 | __version__: str 12 | __version__ = "3.14.1" 13 | 14 | __build__: int = 0x031401 15 | __author__: str = "Kenneth Reitz" 16 | __author_email__: str = "me@kennethreitz.org" 17 | __license__: str = "Apache-2.0" 18 | __copyright__: str = "Copyright Kenneth Reitz" 19 | __cake__: str = "\u2728 \U0001f370 \u2728" 20 | -------------------------------------------------------------------------------- /src/niquests/_async.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | 5 | warnings.warn( 6 | ( 7 | "importing niquests._async is deprecated and absolutely discouraged. " 8 | "It will be removed in a future release. In general, never import private " 9 | "modules." 10 | ), 11 | DeprecationWarning, 12 | ) 13 | 14 | from .async_session import * # noqa 15 | -------------------------------------------------------------------------------- /src/niquests/_compat.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | 5 | try: 6 | from urllib3 import __version__ 7 | 8 | HAS_LEGACY_URLLIB3: bool = int(__version__.split(".")[-1]) < 900 9 | except (ValueError, ImportError): # Defensive: tested in separate CI 10 | # Means one of the two cases: 11 | # 1) urllib3 does not exist -> fallback to urllib3_future 12 | # 2) urllib3 exist but not fork -> fallback to urllib3_future 13 | HAS_LEGACY_URLLIB3 = True 14 | 15 | if HAS_LEGACY_URLLIB3: # Defensive: tested in separate CI 16 | import urllib3_future 17 | else: 18 | urllib3_future = None # type: ignore[assignment] 19 | 20 | try: 21 | import urllib3 22 | 23 | # force detect broken or dummy urllib3 package 24 | urllib3.Timeout # noqa 25 | urllib3.Retry # noqa 26 | urllib3.__version__ # noqa 27 | except (ImportError, AttributeError): # Defensive: tested in separate CI 28 | urllib3 = None # type: ignore[assignment] 29 | 30 | 31 | if (urllib3 is None and urllib3_future is None) or (HAS_LEGACY_URLLIB3 and urllib3_future is None): 32 | raise RuntimeError( # Defensive: tested in separate CI 33 | "This is awkward but your environment is missing urllib3-future. " 34 | "Your environment seems broken. " 35 | "You may fix this issue by running `python -m pip install niquests -U` " 36 | "to force reinstall its dependencies." 37 | ) 38 | 39 | if urllib3 is not None: 40 | T = typing.TypeVar("T", urllib3.Timeout, urllib3.Retry) 41 | else: # Defensive: tested in separate CI 42 | T = typing.TypeVar("T", urllib3_future.Timeout, urllib3_future.Retry) # type: ignore 43 | 44 | 45 | def urllib3_ensure_type(o: T) -> T: 46 | """Retry, Timeout must be the one in urllib3_future.""" 47 | if urllib3 is None: 48 | return o 49 | 50 | if HAS_LEGACY_URLLIB3: # Defensive: tested in separate CI 51 | if "urllib3_future" not in str(type(o)): 52 | assert urllib3_future is not None 53 | 54 | if isinstance(o, urllib3.Timeout): 55 | return urllib3_future.Timeout( # type: ignore[return-value] 56 | o.total, # type: ignore[arg-type] 57 | o.connect_timeout, # type: ignore[arg-type] 58 | o.read_timeout, # type: ignore[arg-type] 59 | ) 60 | if isinstance(o, urllib3.Retry): 61 | return urllib3_future.Retry( # type: ignore[return-value] 62 | o.total, 63 | o.connect, 64 | o.read, 65 | redirect=o.redirect, 66 | status=o.status, 67 | other=o.other, 68 | allowed_methods=o.allowed_methods, 69 | status_forcelist=o.status_forcelist, 70 | backoff_factor=o.backoff_factor, 71 | backoff_max=o.backoff_max, 72 | raise_on_redirect=o.raise_on_redirect, 73 | raise_on_status=o.raise_on_status, 74 | history=o.history, # type: ignore[arg-type] 75 | respect_retry_after_header=o.respect_retry_after_header, 76 | remove_headers_on_redirect=o.remove_headers_on_redirect, 77 | backoff_jitter=o.backoff_jitter, 78 | ) 79 | 80 | return o 81 | -------------------------------------------------------------------------------- /src/niquests/_constant.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import wassima 4 | 5 | from ._typing import RetryType, TimeoutType 6 | 7 | #: Default timeout (total) assigned for GET, HEAD, and OPTIONS methods. 8 | READ_DEFAULT_TIMEOUT: TimeoutType = 30 9 | #: Default timeout (total) assigned for DELETE, PUT, PATCH, and POST. 10 | WRITE_DEFAULT_TIMEOUT: TimeoutType = 120 11 | 12 | DEFAULT_POOLBLOCK: bool = False 13 | DEFAULT_POOLSIZE: int = 10 14 | DEFAULT_RETRIES: RetryType = 0 15 | 16 | #: Kept for BC 17 | DEFAULT_CA_BUNDLE: str = wassima.generate_ca_bundle() 18 | -------------------------------------------------------------------------------- /src/niquests/_typing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | from http.cookiejar import CookieJar 5 | from os import PathLike 6 | 7 | from ._vendor.kiss_headers import Headers 8 | from .auth import AsyncAuthBase, AuthBase 9 | from .packages.urllib3 import AsyncResolverDescription, ResolverDescription, Retry, Timeout 10 | from .packages.urllib3.contrib.resolver import BaseResolver 11 | from .packages.urllib3.contrib.resolver._async import AsyncBaseResolver 12 | from .packages.urllib3.fields import RequestField 13 | from .structures import CaseInsensitiveDict 14 | 15 | if typing.TYPE_CHECKING: 16 | from .models import PreparedRequest 17 | 18 | #: (Restricted) list of http verb that we natively support and understand. 19 | HttpMethodType: typing.TypeAlias = str 20 | #: List of formats accepted for URL queries parameters. (e.g. /?param1=a¶m2=b) 21 | QueryParameterType: typing.TypeAlias = typing.Union[ 22 | typing.List[typing.Tuple[str, typing.Union[str, typing.List[str], None]]], 23 | typing.Mapping[str, typing.Union[str, typing.List[str], None]], 24 | bytes, 25 | str, 26 | ] 27 | BodyFormType: typing.TypeAlias = typing.Union[ 28 | typing.List[typing.Tuple[str, str]], 29 | typing.Dict[str, typing.Union[typing.List[str], str]], 30 | ] 31 | #: Accepted types for the payload in POST, PUT, and PATCH requests. 32 | BodyType: typing.TypeAlias = typing.Union[ 33 | str, 34 | bytes, 35 | bytearray, 36 | typing.IO, 37 | BodyFormType, 38 | typing.Iterable[bytes], 39 | typing.Iterable[str], 40 | ] 41 | AsyncBodyType: typing.TypeAlias = typing.Union[ 42 | typing.AsyncIterable[bytes], 43 | typing.AsyncIterable[str], 44 | ] 45 | #: HTTP Headers can be represented through three ways. 1) typical dict, 2) internal insensitive dict, and 3) list of tuple. 46 | HeadersType: typing.TypeAlias = typing.Union[ 47 | typing.MutableMapping[typing.Union[str, bytes], typing.Union[str, bytes]], 48 | typing.MutableMapping[str, str], 49 | typing.MutableMapping[bytes, bytes], 50 | CaseInsensitiveDict, 51 | typing.List[typing.Tuple[typing.Union[str, bytes], typing.Union[str, bytes]]], 52 | Headers, 53 | ] 54 | #: We accept both typical mapping and stdlib CookieJar. 55 | CookiesType: typing.TypeAlias = typing.Union[ 56 | typing.MutableMapping[str, str], 57 | CookieJar, 58 | ] 59 | #: Either Yes/No, or CA bundle pem location. Or directly the raw bundle content itself. 60 | TLSVerifyType: typing.TypeAlias = typing.Union[bool, str, bytes, PathLike] 61 | #: Accept a pem certificate (concat cert, key) or an explicit tuple of cert, key pair with an optional password. 62 | TLSClientCertType: typing.TypeAlias = typing.Union[str, typing.Tuple[str, str], typing.Tuple[str, str, str]] 63 | #: All accepted ways to describe desired timeout. 64 | TimeoutType: typing.TypeAlias = typing.Union[ 65 | int, # TotalTimeout 66 | float, # TotalTimeout 67 | typing.Tuple[typing.Union[int, float], typing.Union[int, float]], # note: TotalTimeout, ConnectTimeout 68 | typing.Tuple[ 69 | typing.Union[int, float], typing.Union[int, float], typing.Union[int, float] 70 | ], # note: TotalTimeout, ConnectTimeout, ReadTimeout 71 | Timeout, 72 | ] 73 | #: Specify (BasicAuth) authentication by passing a tuple of user, and password. 74 | #: Can be a custom authentication mechanism that derive from AuthBase. 75 | HttpAuthenticationType: typing.TypeAlias = typing.Union[ 76 | typing.Tuple[typing.Union[str, bytes], typing.Union[str, bytes]], 77 | str, 78 | AuthBase, 79 | typing.Callable[["PreparedRequest"], "PreparedRequest"], 80 | ] 81 | AsyncHttpAuthenticationType: typing.TypeAlias = typing.Union[ 82 | AsyncAuthBase, 83 | typing.Callable[["PreparedRequest"], typing.Awaitable["PreparedRequest"]], 84 | ] 85 | #: Map for each protocol (http, https) associated proxy to be used. 86 | ProxyType: typing.TypeAlias = typing.Dict[str, str] 87 | 88 | # cases: 89 | # 1) fn, fp 90 | # 2) fn, fp, ft 91 | # 3) fn, fp, ft, fh 92 | # OR 93 | # 4) fp 94 | BodyFileType: typing.TypeAlias = typing.Union[ 95 | str, 96 | bytes, 97 | bytearray, 98 | typing.IO, 99 | ] 100 | MultiPartFileType: typing.TypeAlias = typing.Tuple[ 101 | str, 102 | typing.Union[ 103 | BodyFileType, 104 | typing.Tuple[str, BodyFileType], 105 | typing.Tuple[str, BodyFileType, str], 106 | typing.Tuple[str, BodyFileType, str, HeadersType], 107 | ], 108 | ] 109 | MultiPartFilesType: typing.TypeAlias = typing.List[MultiPartFileType] 110 | #: files (multipart formdata) can be (also) passed as dict. 111 | MultiPartFilesAltType: typing.TypeAlias = typing.Dict[ 112 | str, 113 | typing.Union[ 114 | BodyFileType, 115 | typing.Tuple[str, BodyFileType], 116 | typing.Tuple[str, BodyFileType, str], 117 | typing.Tuple[str, BodyFileType, str, HeadersType], 118 | ], 119 | ] 120 | 121 | FieldValueType: typing.TypeAlias = typing.Union[str, bytes] 122 | FieldTupleType: typing.TypeAlias = typing.Union[ 123 | FieldValueType, 124 | typing.Tuple[str, FieldValueType], 125 | typing.Tuple[str, FieldValueType, str], 126 | ] 127 | 128 | FieldSequenceType: typing.TypeAlias = typing.Sequence[typing.Union[typing.Tuple[str, FieldTupleType], RequestField]] 129 | FieldsType: typing.TypeAlias = typing.Union[ 130 | FieldSequenceType, 131 | typing.Mapping[str, FieldTupleType], 132 | ] 133 | 134 | _HV = typing.TypeVar("_HV") 135 | 136 | HookCallableType: typing.TypeAlias = typing.Callable[ 137 | [_HV], 138 | typing.Optional[_HV], 139 | ] 140 | 141 | HookType: typing.TypeAlias = typing.Dict[str, typing.List[HookCallableType[_HV]]] 142 | 143 | AsyncHookCallableType: typing.TypeAlias = typing.Callable[ 144 | [_HV], 145 | typing.Awaitable[typing.Optional[_HV]], 146 | ] 147 | 148 | AsyncHookType: typing.TypeAlias = typing.Dict[str, typing.List[typing.Union[HookCallableType[_HV], AsyncHookCallableType[_HV]]]] 149 | 150 | CacheLayerAltSvcType: typing.TypeAlias = typing.MutableMapping[typing.Tuple[str, int], typing.Optional[typing.Tuple[str, int]]] 151 | 152 | RetryType: typing.TypeAlias = typing.Union[bool, int, Retry] 153 | 154 | ResolverType: typing.TypeAlias = typing.Union[ 155 | str, 156 | ResolverDescription, 157 | BaseResolver, 158 | typing.List[str], 159 | typing.List[ResolverDescription], 160 | ] 161 | 162 | AsyncResolverType: typing.TypeAlias = typing.Union[ 163 | str, 164 | AsyncResolverDescription, 165 | AsyncBaseResolver, 166 | typing.List[str], 167 | typing.List[AsyncResolverDescription], 168 | ] 169 | -------------------------------------------------------------------------------- /src/niquests/_vendor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/src/niquests/_vendor/__init__.py -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TAHRI Ahmed R. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kiss-Headers 3 | ~~~~~~~~~~~~~~ 4 | 5 | Kiss-Headers is a headers, HTTP or IMAP4 _(message, email)_ flavour, utility, written in pure Python, for humans. 6 | Object oriented headers. Keep it sweet and simple. 7 | Basic usage: 8 | 9 | >>> import requests 10 | >>> from kiss_headers import parse_it 11 | >>> r = requests.get('https://www.python.org') 12 | >>> headers = parse_it(r) 13 | >>> 'charset' in headers.content_type 14 | True 15 | >>> headers.content_type.charset 16 | 'utf-8' 17 | >>> 'text/html' in headers.content_type 18 | True 19 | >>> headers.content_type == 'text/html' 20 | True 21 | >>> headers -= 'content-type' 22 | >>> 'Content-Type' in headers 23 | False 24 | 25 | ... or from a raw IMAP4 message: 26 | 27 | >>> message = requests.get("https://gist.githubusercontent.com/Ousret/8b84b736c375bb6aa3d389e86b5116ec/raw/21cb2f7af865e401c37d9b053fb6fe1abf63165b/sample-message.eml").content 28 | >>> headers = parse_it(message) 29 | >>> 'Sender' in headers 30 | True 31 | 32 | Others methods and usages are available - see the full documentation 33 | at . 34 | 35 | :copyright: (c) 2020 by Ahmed TAHRI 36 | :license: MIT, see LICENSE for more details. 37 | """ 38 | 39 | from __future__ import annotations 40 | 41 | from .api import dumps, explain, get_polymorphic, parse_it 42 | from .builder import ( 43 | Accept, 44 | AcceptEncoding, 45 | AcceptLanguage, 46 | Allow, 47 | AltSvc, 48 | Authorization, 49 | BasicAuthorization, 50 | CacheControl, 51 | Connection, 52 | ContentDisposition, 53 | ContentEncoding, 54 | ContentLength, 55 | ContentRange, 56 | ContentSecurityPolicy, 57 | ContentType, 58 | CrossOriginResourcePolicy, 59 | CustomHeader, 60 | Date, 61 | Digest, 62 | Dnt, 63 | Etag, 64 | Expires, 65 | Forwarded, 66 | From, 67 | Host, 68 | IfMatch, 69 | IfModifiedSince, 70 | IfNoneMatch, 71 | IfUnmodifiedSince, 72 | KeepAlive, 73 | LastModified, 74 | Location, 75 | ProxyAuthorization, 76 | Referer, 77 | ReferrerPolicy, 78 | RetryAfter, 79 | Server, 80 | SetCookie, 81 | StrictTransportSecurity, 82 | TransferEncoding, 83 | UpgradeInsecureRequests, 84 | UserAgent, 85 | Vary, 86 | WwwAuthenticate, 87 | XContentTypeOptions, 88 | XDnsPrefetchControl, 89 | XFrameOptions, 90 | XXssProtection, 91 | ) 92 | from .models import Attributes, Header, Headers, lock_output_type 93 | from .serializer import decode, encode 94 | from .version import VERSION, __version__ 95 | 96 | __all__ = ( 97 | "dumps", 98 | "explain", 99 | "get_polymorphic", 100 | "parse_it", 101 | "Attributes", 102 | "Header", 103 | "Headers", 104 | "lock_output_type", 105 | "decode", 106 | "encode", 107 | "VERSION", 108 | "__version__", 109 | "Accept", 110 | "AcceptEncoding", 111 | "AcceptLanguage", 112 | "Allow", 113 | "AltSvc", 114 | "Authorization", 115 | "BasicAuthorization", 116 | "CacheControl", 117 | "Connection", 118 | "ContentDisposition", 119 | "ContentEncoding", 120 | "ContentLength", 121 | "ContentRange", 122 | "ContentSecurityPolicy", 123 | "ContentType", 124 | "CrossOriginResourcePolicy", 125 | "CustomHeader", 126 | "Date", 127 | "Digest", 128 | "Dnt", 129 | "Etag", 130 | "Expires", 131 | "Forwarded", 132 | "From", 133 | "Host", 134 | "IfMatch", 135 | "IfModifiedSince", 136 | "IfNoneMatch", 137 | "IfUnmodifiedSince", 138 | "KeepAlive", 139 | "LastModified", 140 | "Location", 141 | "ProxyAuthorization", 142 | "Referer", 143 | "ReferrerPolicy", 144 | "RetryAfter", 145 | "Server", 146 | "SetCookie", 147 | "StrictTransportSecurity", 148 | "TransferEncoding", 149 | "UpgradeInsecureRequests", 150 | "UserAgent", 151 | "Vary", 152 | "WwwAuthenticate", 153 | "XContentTypeOptions", 154 | "XDnsPrefetchControl", 155 | "XFrameOptions", 156 | "XXssProtection", 157 | ) 158 | -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from copy import deepcopy 4 | from email.message import Message 5 | from email.parser import HeaderParser 6 | from io import BufferedReader, RawIOBase 7 | from json import dumps as json_dumps 8 | from json import loads as json_loads 9 | from typing import Any, Iterable, Mapping, TypeVar 10 | 11 | from .builder import CustomHeader 12 | from .models import Header, Headers 13 | from .serializer import decode, encode 14 | from .structures import CaseInsensitiveDict 15 | from .utils import ( 16 | class_to_header_name, 17 | decode_partials, 18 | extract_class_name, 19 | extract_encoded_headers, 20 | header_content_split, 21 | header_name_to_class, 22 | is_content_json_object, 23 | is_legal_header_name, 24 | normalize_str, 25 | transform_possible_encoded, 26 | ) 27 | 28 | T = TypeVar("T", bound=CustomHeader, covariant=True) 29 | 30 | 31 | def parse_it(raw_headers: Any) -> Headers: 32 | """ 33 | Just decode anything that could contain headers. That simple PERIOD. 34 | If passed with a Headers instance, return a deep copy of it. 35 | :param raw_headers: Accept bytes, str, fp, dict, JSON, email.Message, requests.Response, niquests.Response, urllib3.HTTPResponse and httpx.Response. 36 | :raises: 37 | TypeError: If passed argument cannot be parsed to extract headers from it. 38 | """ 39 | 40 | if isinstance(raw_headers, Headers): 41 | return deepcopy(raw_headers) 42 | 43 | headers: Iterable[tuple[str | bytes, str | bytes]] | None = None 44 | 45 | if isinstance(raw_headers, str): 46 | if raw_headers.startswith("{") and raw_headers.endswith("}"): 47 | return decode(json_loads(raw_headers)) 48 | headers = HeaderParser().parsestr(raw_headers, headersonly=True).items() 49 | elif ( 50 | isinstance(raw_headers, bytes) 51 | or isinstance(raw_headers, RawIOBase) 52 | or isinstance(raw_headers, BufferedReader) 53 | ): 54 | decoded, not_decoded = extract_encoded_headers( 55 | raw_headers if isinstance(raw_headers, bytes) else raw_headers.read() or b"" 56 | ) 57 | return parse_it(decoded) 58 | elif isinstance(raw_headers, Mapping) or isinstance(raw_headers, Message): 59 | headers = raw_headers.items() 60 | else: 61 | r = extract_class_name(type(raw_headers)) 62 | 63 | if r: 64 | if r in [ 65 | "requests.models.Response", 66 | "niquests.models.Response", 67 | "niquests.models.AsyncResponse", 68 | ]: 69 | headers = [] 70 | for header_name in raw_headers.raw.headers: 71 | for header_content in raw_headers.raw.headers.getlist(header_name): 72 | headers.append((header_name, header_content)) 73 | elif r in [ 74 | "httpx._models.Response", 75 | "urllib3.response.HTTPResponse", 76 | "urllib3._async.response.AsyncHTTPResponse", 77 | "urllib3_future.response.HTTPResponse", 78 | "urllib3_future._async.response.AsyncHTTPResponse", 79 | ]: # pragma: no cover 80 | headers = raw_headers.headers.items() 81 | 82 | if headers is None: 83 | raise TypeError( # pragma: no cover 84 | f"Cannot parse type {type(raw_headers)} as it is not supported by kiss-header." 85 | ) 86 | 87 | revised_headers: list[tuple[str, str]] = decode_partials( 88 | transform_possible_encoded(headers) 89 | ) 90 | 91 | # Sometime raw content does not begin with headers. If that is the case, search for the next line. 92 | if ( 93 | len(revised_headers) == 0 94 | and len(raw_headers) > 0 95 | and (isinstance(raw_headers, bytes) or isinstance(raw_headers, str)) 96 | ): 97 | next_iter = raw_headers.split( 98 | b"\n" if isinstance(raw_headers, bytes) else "\n", # type: ignore[arg-type] 99 | maxsplit=1, 100 | ) 101 | 102 | if len(next_iter) >= 2: 103 | return parse_it(next_iter[-1]) 104 | 105 | # Prepare Header objects 106 | list_of_headers: list[Header] = [] 107 | 108 | for head, content in revised_headers: 109 | # We should ignore when a illegal name is considered as an header. We avoid ValueError (in __init__ of Header) 110 | if is_legal_header_name(head) is False: 111 | continue 112 | 113 | is_json_obj: bool = is_content_json_object(content) 114 | entries: list[str] 115 | 116 | if is_json_obj is False: 117 | entries = header_content_split(content, ",") 118 | else: 119 | entries = [content] 120 | 121 | # Multiple entries are detected in one content at the only exception that its not IMAP header "Subject". 122 | if len(entries) > 1 and normalize_str(head) != "subject": 123 | for entry in entries: 124 | list_of_headers.append(Header(head, entry)) 125 | else: 126 | list_of_headers.append(Header(head, content)) 127 | 128 | return Headers(*list_of_headers) 129 | 130 | 131 | def explain(headers: Headers) -> CaseInsensitiveDict: 132 | """ 133 | Return a brief explanation of each header present in headers if available. 134 | """ 135 | if not Header.__subclasses__(): 136 | raise LookupError( # pragma: no cover 137 | "You cannot use explain() function without properly importing the public package." 138 | ) 139 | 140 | explanations: CaseInsensitiveDict = CaseInsensitiveDict() 141 | 142 | for header in headers: 143 | if header.name in explanations: 144 | continue 145 | 146 | try: 147 | target_class = header_name_to_class(header.name, Header.__subclasses__()[0]) 148 | except TypeError: 149 | explanations[header.name] = "Unknown explanation." 150 | continue 151 | 152 | explanations[header.name] = ( 153 | target_class.__doc__.replace("\n", "").lstrip().replace(" ", " ").rstrip() 154 | if target_class.__doc__ 155 | else "Missing docstring." 156 | ) 157 | 158 | return explanations 159 | 160 | 161 | def get_polymorphic( 162 | target: Headers | Header, desired_output: type[T] 163 | ) -> T | list[T] | None: 164 | """Experimental. Transform a Header or Headers object to its target `CustomHeader` subclass 165 | to access more ready-to-use methods. eg. You have a Header object named 'Set-Cookie' and you wish 166 | to extract the expiration date as a datetime. 167 | >>> header = Header("Set-Cookie", "1P_JAR=2020-03-16-21; expires=Wed, 15-Apr-2020 21:27:31 GMT") 168 | >>> header["expires"] 169 | 'Wed, 15-Apr-2020 21:27:31 GMT' 170 | >>> from kiss_headers import SetCookie 171 | >>> set_cookie = get_polymorphic(header, SetCookie) 172 | >>> set_cookie.get_expire() 173 | datetime.datetime(2020, 4, 15, 21, 27, 31, tzinfo=datetime.timezone.utc) 174 | """ 175 | 176 | if not issubclass(desired_output, Header): 177 | raise TypeError( 178 | f"The desired output should be a subclass of Header not {desired_output}." 179 | ) 180 | 181 | desired_output_header_name: str = class_to_header_name(desired_output) 182 | 183 | if isinstance(target, Headers): 184 | r = target.get(desired_output_header_name) 185 | 186 | if r is None: 187 | return None 188 | 189 | elif isinstance(target, Header): 190 | if header_name_to_class(target.name, Header) != desired_output: 191 | raise TypeError( 192 | f"The target class does not match the desired output class. {target.__class__} != {desired_output}." 193 | ) 194 | r = target 195 | else: 196 | raise TypeError(f"Unable to apply get_polymorphic on type {target.__class__}.") 197 | 198 | # Change __class__ attribute. 199 | if not isinstance(r, list): 200 | r.__class__ = desired_output 201 | else: 202 | for header in r: 203 | header.__class__ = desired_output 204 | 205 | return r # type: ignore 206 | 207 | 208 | def dumps(headers: Headers, **kwargs: Any | None) -> str: 209 | return json_dumps(encode(headers), **kwargs) # type: ignore 210 | -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/src/niquests/_vendor/kiss_headers/py.typed -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/serializer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .models import Header, Headers 4 | 5 | 6 | def encode(headers: Headers) -> dict[str, list[dict]]: 7 | """ 8 | Provide an opinionated but reliable way to encode headers to dict for serialization purposes. 9 | """ 10 | result: dict[str, list[dict]] = dict() 11 | 12 | for header in headers: 13 | if header.name not in result: 14 | result[header.name] = list() 15 | 16 | encoded_header: dict[str, str | None | list[str]] = dict() 17 | 18 | for attribute, value in header: 19 | if attribute not in encoded_header: 20 | encoded_header[attribute] = value 21 | continue 22 | 23 | if isinstance(encoded_header[attribute], list) is False: 24 | # Here encoded_header[attribute] most certainly is str 25 | # Had to silent mypy error. 26 | encoded_header[attribute] = [encoded_header[attribute]] # type: ignore 27 | 28 | encoded_header[attribute].append(value) # type: ignore 29 | 30 | result[header.name].append(encoded_header) 31 | 32 | return result 33 | 34 | 35 | def decode(encoded_headers: dict[str, list[dict]]) -> Headers: 36 | """ 37 | Decode any previously encoded headers to a Headers object. 38 | """ 39 | headers: Headers = Headers() 40 | 41 | for header_name, encoded_header_list in encoded_headers.items(): 42 | if not isinstance(encoded_header_list, list): 43 | raise ValueError("Decode require first level values to be List") 44 | 45 | for encoded_header in encoded_header_list: 46 | if not isinstance(encoded_header, dict): 47 | raise ValueError("Decode require each list element to be Dict") 48 | 49 | header = Header(header_name, "") 50 | 51 | for attr, value in encoded_header.items(): 52 | if value is None: 53 | header += attr 54 | continue 55 | if isinstance(value, str): 56 | header[attr] = value 57 | continue 58 | 59 | for sub_value in value: 60 | header.insert(-1, **{attr: sub_value}) 61 | 62 | headers += header 63 | 64 | return headers 65 | -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/structures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import OrderedDict 4 | from collections.abc import Mapping, MutableMapping 5 | from typing import ( 6 | Any, 7 | Iterator, 8 | List, 9 | Optional, 10 | Tuple, 11 | ) 12 | from typing import ( 13 | MutableMapping as MutableMappingType, 14 | ) 15 | 16 | from .utils import normalize_str 17 | 18 | """ 19 | Disclaimer : CaseInsensitiveDict has been borrowed from `psf/requests`. 20 | Minors changes has been made. 21 | """ 22 | 23 | 24 | class CaseInsensitiveDict(MutableMapping): 25 | """A case-insensitive ``dict``-like object. 26 | 27 | Implements all methods and operations of 28 | ``MutableMapping`` as well as dict's ``copy``. Also 29 | provides ``lower_items``. 30 | 31 | All keys are expected to be strings. The structure remembers the 32 | case of the last key to be set, and ``iter(instance)``, 33 | ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` 34 | will contain case-sensitive keys. However, querying and contains 35 | testing is case insensitive:: 36 | 37 | cid = CaseInsensitiveDict() 38 | cid['Accept'] = 'application/json' 39 | cid['aCCEPT'] == 'application/json' # True 40 | list(cid) == ['Accept'] # True 41 | 42 | For example, ``headers['content-encoding']`` will return the 43 | value of a ``'Content-Encoding'`` response header, regardless 44 | of how the header name was originally stored. 45 | 46 | If the constructor, ``.update``, or equality comparison 47 | operations are given keys that have equal ``.lower()``s, the 48 | behavior is undefined. 49 | """ 50 | 51 | def __init__(self, data: Mapping | None = None, **kwargs: Any): 52 | self._store: OrderedDict = OrderedDict() 53 | if data is None: 54 | data = {} 55 | self.update(data, **kwargs) 56 | 57 | def __setitem__(self, key: str, value: Any) -> None: 58 | # Use the lowercased key for lookups, but store the actual 59 | # key alongside the value. 60 | self._store[normalize_str(key)] = (key, value) 61 | 62 | def __getitem__(self, key: str) -> Any: 63 | return self._store[normalize_str(key)][1] 64 | 65 | def __delitem__(self, key: str) -> None: 66 | del self._store[normalize_str(key)] 67 | 68 | def __iter__(self) -> Iterator[tuple[str, Any]]: 69 | return (casedkey for casedkey, mappedvalue in self._store.values()) 70 | 71 | def __len__(self) -> int: 72 | return len(self._store) 73 | 74 | def lower_items(self) -> Iterator[tuple[str, Any]]: 75 | """Like iteritems(), but with all lowercase keys.""" 76 | return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items()) 77 | 78 | def __eq__(self, other: object) -> bool: 79 | if isinstance(other, Mapping): 80 | other = CaseInsensitiveDict(other) 81 | else: 82 | return NotImplemented 83 | # Compare insensitively 84 | return dict(self.lower_items()) == dict(other.lower_items()) 85 | 86 | # Copy is required 87 | def copy(self) -> CaseInsensitiveDict: 88 | return CaseInsensitiveDict(dict(self._store.values())) 89 | 90 | def __repr__(self) -> str: 91 | return str(dict(self.items())) 92 | 93 | 94 | AttributeDescription = Tuple[List[Optional[str]], List[int]] 95 | AttributeBag = MutableMappingType[str, AttributeDescription] 96 | -------------------------------------------------------------------------------- /src/niquests/_vendor/kiss_headers/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Expose version 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | __version__ = "2.5.0" 8 | VERSION = __version__.split(".") 9 | -------------------------------------------------------------------------------- /src/niquests/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests.exceptions 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | This module contains the set of Requests' exceptions. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import typing 11 | from json import JSONDecodeError as CompatJSONDecodeError 12 | 13 | from .packages.urllib3.exceptions import HTTPError as BaseHTTPError 14 | 15 | if typing.TYPE_CHECKING: 16 | from .models import PreparedRequest, Response 17 | 18 | 19 | class RequestException(IOError): 20 | """There was an ambiguous exception that occurred while handling your 21 | request. 22 | """ 23 | 24 | response: Response | None 25 | request: PreparedRequest | None 26 | 27 | def __init__(self, *args, **kwargs) -> None: 28 | """Initialize RequestException with `request` and `response` objects.""" 29 | response = kwargs.pop("response", None) 30 | self.response = response 31 | self.request = kwargs.pop("request", None) 32 | if self.response is not None and not self.request and hasattr(self.response, "request"): 33 | self.request = self.response.request 34 | super().__init__(*args, **kwargs) 35 | 36 | 37 | class InvalidJSONError(RequestException): 38 | """A JSON error occurred.""" 39 | 40 | 41 | class JSONDecodeError(InvalidJSONError, CompatJSONDecodeError): 42 | """Couldn't decode the text into json""" 43 | 44 | def __init__(self, *args, **kwargs) -> None: 45 | """ 46 | Construct the JSONDecodeError instance first with all 47 | args. Then use it's args to construct the IOError so that 48 | the json specific args aren't used as IOError specific args 49 | and the error message from JSONDecodeError is preserved. 50 | """ 51 | CompatJSONDecodeError.__init__(self, *args) 52 | InvalidJSONError.__init__(self, *self.args, **kwargs) 53 | 54 | 55 | class HTTPError(RequestException): 56 | """An HTTP error occurred.""" 57 | 58 | 59 | class ConnectionError(RequestException): 60 | """A Connection error occurred.""" 61 | 62 | 63 | class ProxyError(ConnectionError): 64 | """A proxy error occurred.""" 65 | 66 | 67 | class SSLError(ConnectionError): 68 | """An SSL error occurred.""" 69 | 70 | 71 | class Timeout(RequestException): 72 | """The request timed out. 73 | 74 | Catching this error will catch both 75 | :exc:`~requests.exceptions.ConnectTimeout` and 76 | :exc:`~requests.exceptions.ReadTimeout` errors. 77 | """ 78 | 79 | 80 | class ConnectTimeout(ConnectionError, Timeout): 81 | """The request timed out while trying to connect to the remote server. 82 | 83 | Requests that produced this error are safe to retry. 84 | """ 85 | 86 | 87 | class ReadTimeout(Timeout): 88 | """The server did not send any data in the allotted amount of time.""" 89 | 90 | 91 | class URLRequired(RequestException): 92 | """A valid URL is required to make a request.""" 93 | 94 | 95 | class TooManyRedirects(RequestException): 96 | """Too many redirects.""" 97 | 98 | 99 | class MissingSchema(RequestException, ValueError): 100 | """The URL scheme (e.g. http or https) is missing.""" 101 | 102 | 103 | class InvalidSchema(RequestException, ValueError): 104 | """The URL scheme provided is either invalid or unsupported.""" 105 | 106 | 107 | class InvalidURL(RequestException, ValueError): 108 | """The URL provided was somehow invalid.""" 109 | 110 | 111 | class InvalidHeader(RequestException, ValueError): 112 | """The header value provided was somehow invalid.""" 113 | 114 | 115 | class InvalidProxyURL(InvalidURL): 116 | """The proxy URL provided is invalid.""" 117 | 118 | 119 | class ChunkedEncodingError(RequestException): 120 | """The server declared chunked encoding but sent an invalid chunk.""" 121 | 122 | 123 | class ContentDecodingError(RequestException, BaseHTTPError): 124 | """Failed to decode response content.""" 125 | 126 | 127 | class StreamConsumedError(RequestException, TypeError): 128 | """The content for this response was already consumed.""" 129 | 130 | 131 | class RetryError(RequestException): 132 | """Custom retries logic failed""" 133 | 134 | 135 | class UnrewindableBodyError(RequestException): 136 | """Requests encountered an error when trying to rewind a body.""" 137 | 138 | 139 | class MultiplexingError(RequestException): 140 | """Requests encountered an unresolvable error in multiplexed mode.""" 141 | 142 | 143 | # Warnings 144 | 145 | 146 | class RequestsWarning(Warning): 147 | """Base warning for Requests.""" 148 | 149 | 150 | class FileModeWarning(RequestsWarning, DeprecationWarning): 151 | """A file was opened in text mode, but Requests determined its binary length.""" 152 | 153 | 154 | class RequestsDependencyWarning(RequestsWarning): 155 | """An imported dependency doesn't match the expected version range.""" 156 | -------------------------------------------------------------------------------- /src/niquests/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This subpackage hold anything that is very relevant 3 | to the HTTP ecosystem but not per-say Niquests core logic. 4 | """ 5 | -------------------------------------------------------------------------------- /src/niquests/help.py: -------------------------------------------------------------------------------- 1 | """Module containing bug report helper(s).""" 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | import platform 7 | 8 | try: 9 | import ssl 10 | except ImportError: 11 | ssl = None # type: ignore 12 | 13 | import sys 14 | import warnings 15 | from json import JSONDecodeError 16 | 17 | import charset_normalizer 18 | import h11 19 | 20 | try: 21 | import idna # type: ignore[import-not-found] 22 | except ImportError: 23 | idna = None # type: ignore[assignment] 24 | 25 | import jh2 # type: ignore 26 | import wassima 27 | 28 | from . import HTTPError, RequestException, Session 29 | from . import __version__ as niquests_version 30 | from ._compat import HAS_LEGACY_URLLIB3 31 | 32 | if HAS_LEGACY_URLLIB3 is True: 33 | import urllib3_future as urllib3 34 | 35 | try: 36 | from urllib3 import __version__ as __legacy_urllib3_version__ 37 | except (ImportError, AttributeError): 38 | __legacy_urllib3_version__ = None # type: ignore[assignment] 39 | else: 40 | import urllib3 # type: ignore[no-redef] 41 | 42 | __legacy_urllib3_version__ = None # type: ignore[assignment] 43 | 44 | try: 45 | import qh3 # type: ignore 46 | except ImportError: 47 | qh3 = None # type: ignore 48 | 49 | try: 50 | import certifi # type: ignore 51 | except ImportError: 52 | certifi = None # type: ignore 53 | 54 | try: 55 | from .extensions._ocsp import verify as ocsp_verify 56 | except ImportError: 57 | ocsp_verify = None # type: ignore 58 | 59 | try: 60 | import wsproto # type: ignore[import-not-found] 61 | except ImportError: 62 | wsproto = None # type: ignore 63 | 64 | 65 | _IS_GIL_DISABLED: bool = hasattr(sys, "_is_gil_enabled") and sys._is_gil_enabled() is False 66 | 67 | 68 | def _implementation(): 69 | """Return a dict with the Python implementation and version. 70 | 71 | Provide both the name and the version of the Python implementation 72 | currently running. For example, on CPython 3.10.3 it will return 73 | {'name': 'CPython', 'version': '3.10.3'}. 74 | 75 | This function works best on CPython and PyPy: in particular, it probably 76 | doesn't work for Jython or IronPython. Future investigation should be done 77 | to work out the correct shape of the code for those platforms. 78 | """ 79 | implementation = platform.python_implementation() 80 | 81 | if implementation == "CPython": 82 | implementation_version = platform.python_version() 83 | elif implementation == "PyPy": 84 | implementation_version = ( 85 | f"{sys.pypy_version_info.major}" # type: ignore[attr-defined] 86 | f".{sys.pypy_version_info.minor}" # type: ignore[attr-defined] 87 | f".{sys.pypy_version_info.micro}" # type: ignore[attr-defined] 88 | ) 89 | if sys.pypy_version_info.releaselevel != "final": # type: ignore[attr-defined] 90 | implementation_version = "".join( 91 | [implementation_version, sys.pypy_version_info.releaselevel] # type: ignore[attr-defined] 92 | ) 93 | elif implementation == "Jython": 94 | implementation_version = platform.python_version() # Complete Guess 95 | elif implementation == "IronPython": 96 | implementation_version = platform.python_version() # Complete Guess 97 | else: 98 | implementation_version = "Unknown" 99 | 100 | return {"name": implementation, "version": implementation_version} 101 | 102 | 103 | def info(): 104 | """Generate information for a bug report.""" 105 | try: 106 | platform_info = { 107 | "system": platform.system(), 108 | "release": platform.release(), 109 | } 110 | except OSError: 111 | platform_info = { 112 | "system": "Unknown", 113 | "release": "Unknown", 114 | } 115 | 116 | implementation_info = _implementation() 117 | urllib3_info = { 118 | "version": urllib3.__version__, 119 | "cohabitation_version": __legacy_urllib3_version__, 120 | } 121 | 122 | charset_normalizer_info = {"version": charset_normalizer.__version__} 123 | 124 | idna_info = { 125 | "version": getattr(idna, "__version__", "N/A"), 126 | } 127 | 128 | if ssl is not None: 129 | system_ssl = ssl.OPENSSL_VERSION_NUMBER 130 | 131 | system_ssl_info = { 132 | "version": f"{system_ssl:x}" if system_ssl is not None else "N/A", 133 | "name": ssl.OPENSSL_VERSION, 134 | } 135 | else: 136 | system_ssl_info = {"version": "N/A", "name": "N/A"} 137 | 138 | return { 139 | "platform": platform_info, 140 | "implementation": implementation_info, 141 | "system_ssl": system_ssl_info, 142 | "gil": not _IS_GIL_DISABLED, 143 | "urllib3.future": urllib3_info, 144 | "charset_normalizer": charset_normalizer_info, 145 | "idna": idna_info, 146 | "niquests": { 147 | "version": niquests_version, 148 | }, 149 | "http3": { 150 | "enabled": qh3 is not None, 151 | "qh3": qh3.__version__ if qh3 is not None else None, 152 | }, 153 | "http2": { 154 | "jh2": jh2.__version__, 155 | }, 156 | "http1": { 157 | "h11": h11.__version__, 158 | }, 159 | "wassima": { 160 | "enabled": wassima.RUSTLS_LOADED, 161 | "certifi_fallback": wassima.RUSTLS_LOADED is False and certifi is not None, 162 | "version": wassima.__version__, 163 | }, 164 | "ocsp": {"enabled": ocsp_verify is not None}, 165 | "websocket": { 166 | "enabled": wsproto is not None, 167 | "wsproto": wsproto.__version__ if wsproto is not None else None, 168 | }, 169 | } 170 | 171 | 172 | pypi_session = Session() 173 | 174 | 175 | def check_update(package_name: str, actual_version: str) -> None: 176 | """ 177 | Small and concise utility to check for updates. 178 | """ 179 | try: 180 | response = pypi_session.get(f"https://pypi.org/pypi/{package_name}/json") 181 | package_info = response.raise_for_status().json() 182 | 183 | if isinstance(package_info, dict) and "info" in package_info and "version" in package_info["info"]: 184 | if package_info["info"]["version"] != actual_version: 185 | warnings.warn( 186 | f"You are using {package_name} {actual_version} and " 187 | f"PyPI yield version ({package_info['info']['version']}) as the stable one. " 188 | "We invite you to install this version as soon as possible. " 189 | f"Run `python -m pip install {package_name} -U`.", 190 | UserWarning, 191 | ) 192 | except (RequestException, JSONDecodeError, HTTPError): 193 | pass 194 | 195 | 196 | PACKAGE_TO_CHECK_FOR_UPGRADE = { 197 | "niquests": niquests_version, 198 | "urllib3-future": urllib3.__version__, 199 | "qh3": qh3.__version__ if qh3 is not None else None, 200 | "jh2": jh2.__version__, 201 | "h11": h11.__version__, 202 | "charset-normalizer": charset_normalizer.__version__, 203 | "wassima": wassima.__version__, 204 | "idna": idna.__version__ if idna is not None else None, 205 | "wsproto": wsproto.__version__ if wsproto is not None else None, 206 | } 207 | 208 | 209 | def main() -> None: 210 | """Pretty-print the bug information as JSON.""" 211 | for package, actual_version in PACKAGE_TO_CHECK_FOR_UPGRADE.items(): 212 | if actual_version is None: 213 | continue 214 | check_update(package, actual_version) 215 | 216 | if __legacy_urllib3_version__ is not None: 217 | warnings.warn( 218 | "urllib3-future is installed alongside (legacy) urllib3. This may cause compatibility issues. " 219 | "Some (Requests) 3rd parties may be bound to urllib3, therefor the plugins may wrongfully invoke " 220 | "urllib3 (legacy) instead of urllib3-future. To remediate this, run " 221 | "`python -m pip uninstall -y urllib3 urllib3-future`, then run `python -m pip install urllib3-future`.", 222 | UserWarning, 223 | ) 224 | 225 | print(json.dumps(info(), sort_keys=True, indent=2)) 226 | 227 | 228 | if __name__ == "__main__": 229 | main() 230 | -------------------------------------------------------------------------------- /src/niquests/hooks.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests.hooks 3 | ~~~~~~~~~~~~~~ 4 | 5 | This module provides the capabilities for the Requests hooks system. 6 | 7 | Available hooks: 8 | 9 | ``pre_request``: 10 | The prepared request just got built. You may alter it prior to be sent through HTTP. 11 | ``pre_send``: 12 | The prepared request got his ConnectionInfo injected. 13 | This event is triggered just after picking a live connection from the pool. 14 | ``on_upload``: 15 | Permit to monitor the upload progress of passed body. 16 | This event is triggered each time a block of data is transmitted to the remote peer. 17 | Use this hook carefully as it may impact the overall performance. 18 | ``response``: 19 | The response generated from a Request. 20 | """ 21 | 22 | from __future__ import annotations 23 | 24 | import asyncio 25 | import typing 26 | 27 | from ._typing import ( 28 | _HV, 29 | AsyncHookCallableType, 30 | AsyncHookType, 31 | HookCallableType, 32 | HookType, 33 | ) 34 | 35 | HOOKS = [ 36 | "pre_request", 37 | "pre_send", 38 | "on_upload", 39 | "early_response", 40 | "response", 41 | ] 42 | 43 | 44 | def default_hooks() -> HookType[_HV]: 45 | return {event: [] for event in HOOKS} 46 | 47 | 48 | def dispatch_hook(key: str, hooks: HookType[_HV] | None, hook_data: _HV, **kwargs: typing.Any) -> _HV: 49 | """Dispatches a hook dictionary on a given piece of data.""" 50 | if hooks is None: 51 | return hook_data 52 | 53 | callables: list[HookCallableType[_HV]] | HookCallableType[_HV] | None = hooks.get(key) 54 | 55 | if callables: 56 | if callable(callables): 57 | callables = [callables] 58 | for hook in callables: 59 | try: 60 | _hook_data = hook(hook_data, **kwargs) 61 | except TypeError: 62 | _hook_data = hook(hook_data) 63 | if _hook_data is not None: 64 | hook_data = _hook_data 65 | 66 | return hook_data 67 | 68 | 69 | async def async_dispatch_hook(key: str, hooks: AsyncHookType[_HV] | None, hook_data: _HV, **kwargs: typing.Any) -> _HV: 70 | """Dispatches a hook dictionary on a given piece of data asynchronously.""" 71 | if hooks is None: 72 | return hook_data 73 | 74 | callables: ( 75 | list[HookCallableType[_HV] | AsyncHookCallableType[_HV]] | HookCallableType[_HV] | AsyncHookCallableType[_HV] | None 76 | ) = hooks.get(key) 77 | 78 | if callables: 79 | if callable(callables): 80 | callables = [callables] 81 | for hook in callables: 82 | if asyncio.iscoroutinefunction(hook): 83 | try: 84 | _hook_data = await hook(hook_data, **kwargs) 85 | except TypeError: 86 | _hook_data = await hook(hook_data) 87 | else: 88 | try: 89 | _hook_data = hook(hook_data, **kwargs) 90 | except TypeError: 91 | _hook_data = hook(hook_data) 92 | 93 | if _hook_data is not None: 94 | hook_data = _hook_data 95 | 96 | return hook_data 97 | -------------------------------------------------------------------------------- /src/niquests/packages.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import typing 5 | 6 | from ._compat import HAS_LEGACY_URLLIB3 7 | 8 | # just to enable smooth type-completion! 9 | if typing.TYPE_CHECKING: 10 | if HAS_LEGACY_URLLIB3: 11 | import urllib3_future as urllib3 # noqa 12 | else: 13 | import urllib3 # type: ignore[no-redef] # noqa 14 | 15 | import charset_normalizer as chardet # noqa 16 | 17 | charset_normalizer = chardet # noqa 18 | 19 | import idna # type: ignore[import-not-found] # noqa 20 | 21 | # This code exists for backwards compatibility reasons. 22 | # I don't like it either. Just look the other way. :) 23 | for package in ( 24 | "urllib3", 25 | "charset_normalizer", 26 | "idna", 27 | "chardet", 28 | ): 29 | to_be_imported: str = package 30 | 31 | if package == "chardet": 32 | to_be_imported = "charset_normalizer" 33 | elif package == "urllib3" and HAS_LEGACY_URLLIB3: 34 | to_be_imported = "urllib3_future" 35 | 36 | try: 37 | locals()[package] = __import__(to_be_imported) 38 | except ImportError: 39 | continue # idna could be missing. not required! 40 | 41 | # This traversal is apparently necessary such that the identities are 42 | # preserved (requests.packages.urllib3.* is urllib3.*) 43 | for mod in list(sys.modules): 44 | if mod == to_be_imported or mod.startswith(f"{to_be_imported}."): 45 | inner_mod = mod 46 | 47 | if HAS_LEGACY_URLLIB3 and inner_mod == "urllib3_future" or inner_mod.startswith("urllib3_future."): 48 | inner_mod = inner_mod.replace("urllib3_future", "urllib3") 49 | elif inner_mod == "charset_normalizer": 50 | inner_mod = "chardet" 51 | 52 | try: 53 | sys.modules[f"niquests.packages.{inner_mod}"] = sys.modules[mod] 54 | except KeyError: 55 | continue 56 | -------------------------------------------------------------------------------- /src/niquests/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/src/niquests/py.typed -------------------------------------------------------------------------------- /src/niquests/status_codes.py: -------------------------------------------------------------------------------- 1 | r""" 2 | The ``codes`` object defines a mapping from common names for HTTP statuses 3 | to their numerical codes, accessible either as attributes or as dictionary 4 | items. 5 | 6 | Example:: 7 | 8 | >>> import niquests 9 | >>> niquests.codes['temporary_redirect'] 10 | 307 11 | >>> niquests.codes.teapot 12 | 418 13 | >>> niquests.codes['\o/'] 14 | 200 15 | 16 | Some codes have multiple names, and both upper- and lower-case versions of 17 | the names are allowed. For example, ``codes.ok``, ``codes.OK``, and 18 | ``codes.okay`` all correspond to the HTTP status code 200. 19 | """ 20 | 21 | from __future__ import annotations 22 | 23 | from .structures import LookupDict 24 | 25 | _codes = { 26 | # Informational. 27 | 100: ("continue",), 28 | 101: ("switching_protocols",), 29 | 102: ("processing",), 30 | 103: ("checkpoint",), 31 | 122: ("uri_too_long", "request_uri_too_long"), 32 | 200: ("ok", "okay", "all_ok", "all_okay", "all_good", "\\o/", "✓"), 33 | 201: ("created",), 34 | 202: ("accepted",), 35 | 203: ("non_authoritative_info", "non_authoritative_information"), 36 | 204: ("no_content",), 37 | 205: ("reset_content", "reset"), 38 | 206: ("partial_content", "partial"), 39 | 207: ("multi_status", "multiple_status", "multi_stati", "multiple_stati"), 40 | 208: ("already_reported",), 41 | 226: ("im_used",), 42 | # Redirection. 43 | 300: ("multiple_choices",), 44 | 301: ("moved_permanently", "moved", "\\o-"), 45 | 302: ("found",), 46 | 303: ("see_other", "other"), 47 | 304: ("not_modified",), 48 | 305: ("use_proxy",), 49 | 306: ("switch_proxy",), 50 | 307: ("temporary_redirect", "temporary_moved", "temporary"), 51 | 308: ( 52 | "permanent_redirect", 53 | "resume_incomplete", 54 | "resume", 55 | ), # "resume" and "resume_incomplete" to be removed in 3.0 56 | # Client Error. 57 | 400: ("bad_request", "bad"), 58 | 401: ("unauthorized",), 59 | 402: ("payment_required", "payment"), 60 | 403: ("forbidden",), 61 | 404: ("not_found", "-o-"), 62 | 405: ("method_not_allowed", "not_allowed"), 63 | 406: ("not_acceptable",), 64 | 407: ("proxy_authentication_required", "proxy_auth", "proxy_authentication"), 65 | 408: ("request_timeout", "timeout"), 66 | 409: ("conflict",), 67 | 410: ("gone",), 68 | 411: ("length_required",), 69 | 412: ("precondition_failed", "precondition"), 70 | 413: ("request_entity_too_large",), 71 | 414: ("request_uri_too_large",), 72 | 415: ("unsupported_media_type", "unsupported_media", "media_type"), 73 | 416: ( 74 | "requested_range_not_satisfiable", 75 | "requested_range", 76 | "range_not_satisfiable", 77 | ), 78 | 417: ("expectation_failed",), 79 | 418: ("im_a_teapot", "teapot", "i_am_a_teapot"), 80 | 421: ("misdirected_request",), 81 | 422: ("unprocessable_entity", "unprocessable"), 82 | 423: ("locked",), 83 | 424: ("failed_dependency", "dependency"), 84 | 425: ("unordered_collection", "unordered", "too_early"), 85 | 426: ("upgrade_required", "upgrade"), 86 | 428: ("precondition_required", "precondition"), 87 | 429: ("too_many_requests", "too_many"), 88 | 431: ("header_fields_too_large", "fields_too_large"), 89 | 444: ("no_response", "none"), 90 | 449: ("retry_with", "retry"), 91 | 450: ("blocked_by_windows_parental_controls", "parental_controls"), 92 | 451: ("unavailable_for_legal_reasons", "legal_reasons"), 93 | 499: ("client_closed_request",), 94 | # Server Error. 95 | 500: ("internal_server_error", "server_error", "/o\\", "✗"), 96 | 501: ("not_implemented",), 97 | 502: ("bad_gateway",), 98 | 503: ("service_unavailable", "unavailable"), 99 | 504: ("gateway_timeout",), 100 | 505: ("http_version_not_supported", "http_version"), 101 | 506: ("variant_also_negotiates",), 102 | 507: ("insufficient_storage",), 103 | 509: ("bandwidth_limit_exceeded", "bandwidth"), 104 | 510: ("not_extended",), 105 | 511: ("network_authentication_required", "network_auth", "network_authentication"), 106 | } 107 | 108 | codes = LookupDict(name="status_codes") 109 | 110 | 111 | def _init(): 112 | for code, titles in _codes.items(): 113 | for title in titles: 114 | setattr(codes, title, code) 115 | if not title.startswith(("\\", "/")): 116 | setattr(codes, title.upper(), code) 117 | 118 | def doc(code): 119 | names = ", ".join(f"``{n}``" for n in _codes[code]) 120 | return f"* {code}: {names}" 121 | 122 | global __doc__ 123 | __doc__ = __doc__ + "\n" + "\n".join(doc(code) for code in sorted(_codes)) if __doc__ is not None else None 124 | 125 | 126 | _init() 127 | -------------------------------------------------------------------------------- /src/niquests/structures.py: -------------------------------------------------------------------------------- 1 | """ 2 | requests.structures 3 | ~~~~~~~~~~~~~~~~~~~ 4 | 5 | Data structures that power Requests. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | import threading 11 | import typing 12 | from collections.abc import Mapping, MutableMapping 13 | 14 | try: 15 | from ._compat import HAS_LEGACY_URLLIB3 16 | 17 | if not HAS_LEGACY_URLLIB3: 18 | from urllib3._collections import _lower_wrapper # type: ignore[attr-defined] 19 | else: # Defensive: tested in separate/isolated CI 20 | from urllib3_future._collections import ( 21 | _lower_wrapper, # type: ignore[attr-defined] 22 | ) 23 | except ImportError: 24 | from functools import lru_cache 25 | 26 | @lru_cache(maxsize=64) 27 | def _lower_wrapper(string: str) -> str: 28 | """backport""" 29 | return string.lower() 30 | 31 | 32 | from .exceptions import InvalidHeader 33 | 34 | 35 | def _ensure_str_or_bytes(key: typing.Any, value: typing.Any) -> tuple[bytes | str, bytes | str]: 36 | if isinstance(key, (bytes, str)) and isinstance(value, (bytes, str)): 37 | return key, value 38 | if isinstance( 39 | value, 40 | ( 41 | float, 42 | int, 43 | ), 44 | ): 45 | value = str(value) 46 | if isinstance(key, (bytes, str)) is False or (value is not None and isinstance(value, (bytes, str)) is False): 47 | raise InvalidHeader(f"Illegal header name or value {key}") 48 | return key, value 49 | 50 | 51 | class CaseInsensitiveDict(MutableMapping): 52 | """A case-insensitive ``dict``-like object. 53 | 54 | Implements all methods and operations of 55 | ``MutableMapping`` as well as dict's ``copy``. Also 56 | provides ``lower_items``. 57 | 58 | All keys are expected to be strings. The structure remembers the 59 | case of the last key to be set, and ``iter(instance)``, 60 | ``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()`` 61 | will contain case-sensitive keys. However, querying and contains 62 | testing is case insensitive:: 63 | 64 | cid = CaseInsensitiveDict() 65 | cid['Accept'] = 'application/json' 66 | cid['aCCEPT'] == 'application/json' # True 67 | list(cid) == ['Accept'] # True 68 | 69 | For example, ``headers['content-encoding']`` will return the 70 | value of a ``'Content-Encoding'`` response header, regardless 71 | of how the header name was originally stored. 72 | 73 | If the constructor, ``.update``, or equality comparison 74 | operations are given keys that have equal ``.lower()``s, the 75 | behavior is undefined. 76 | """ 77 | 78 | def __init__(self, data=None, **kwargs) -> None: 79 | self._store: MutableMapping[bytes | str, tuple[bytes | str, ...]] = {} 80 | if data is None: 81 | data = {} 82 | 83 | # given object is most likely to be urllib3.HTTPHeaderDict or follow a similar implementation that we can trust 84 | if hasattr(data, "getlist"): 85 | self._store = data._container.copy() 86 | elif isinstance(data, CaseInsensitiveDict): 87 | self._store = data._store.copy() # type: ignore[attr-defined] 88 | else: # otherwise, we must ensure given iterable contains type we can rely on 89 | if data or kwargs: 90 | if hasattr(data, "items"): 91 | self.update(data, **kwargs) 92 | else: 93 | self.update( 94 | {k: v for k, v in data}, 95 | **kwargs, 96 | ) 97 | 98 | def __setitem__(self, key: str | bytes, value: str | bytes) -> None: 99 | # Use the lowercased key for lookups, but store the actual 100 | # key alongside the value. 101 | self._store[_lower_wrapper(key)] = _ensure_str_or_bytes(key, value) 102 | 103 | def __getitem__(self, key) -> bytes | str: 104 | e = self._store[_lower_wrapper(key)] 105 | if len(e) == 2: 106 | return e[1] 107 | # this path should always be list[str] (if coming from urllib3.HTTPHeaderDict!) 108 | try: 109 | return ", ".join(e[1:]) if isinstance(e[1], str) else b", ".join(e[1:]) # type: ignore[arg-type] 110 | except TypeError: # worst case scenario... 111 | return ", ".join(v.decode() if isinstance(v, bytes) else v for v in e[1:]) 112 | 113 | def __delitem__(self, key) -> None: 114 | del self._store[_lower_wrapper(key)] 115 | 116 | def __iter__(self) -> typing.Iterator[str | bytes]: 117 | for key_ci in self._store: 118 | yield self._store[key_ci][0] 119 | 120 | def __len__(self) -> int: 121 | return len(self._store) 122 | 123 | def lower_items(self) -> typing.Iterator[tuple[bytes | str, bytes | str]]: 124 | """Like iteritems(), but with all lowercase keys.""" 125 | return ((lowerkey, keyval[1]) for (lowerkey, keyval) in self._store.items()) 126 | 127 | def items(self): 128 | for k in self._store: 129 | t = self._store[k] 130 | if len(t) == 2: 131 | yield t 132 | else: # this case happen due to copying "_container" from HTTPHeaderDict! 133 | try: 134 | yield t[0], ", ".join(t[1:]) # type: ignore[arg-type] 135 | except TypeError: 136 | yield ( 137 | t[0], 138 | ", ".join(v.decode() if isinstance(v, bytes) else v for v in t[1:]), 139 | ) 140 | 141 | def __eq__(self, other) -> bool: 142 | if isinstance(other, Mapping): 143 | other = CaseInsensitiveDict(other) 144 | else: 145 | return NotImplemented 146 | # Compare insensitively 147 | return dict(self.lower_items()) == dict(other.lower_items()) 148 | 149 | # Copy is required 150 | def copy(self) -> CaseInsensitiveDict: 151 | return CaseInsensitiveDict(self) 152 | 153 | def __repr__(self) -> str: 154 | return str(dict(self.items())) 155 | 156 | def __contains__(self, item: str) -> bool: # type: ignore[override] 157 | return _lower_wrapper(item) in self._store 158 | 159 | 160 | class LookupDict(dict): 161 | """Dictionary lookup object.""" 162 | 163 | def __init__(self, name=None) -> None: 164 | self.name: str | None = name 165 | super().__init__() 166 | 167 | def __repr__(self): 168 | return f"" 169 | 170 | def __getitem__(self, key): 171 | # We allow fall-through here, so values default to None 172 | return self.__dict__.get(key, None) 173 | 174 | def get(self, key, default=None): 175 | return self.__dict__.get(key, default) 176 | 177 | 178 | class SharableLimitedDict(typing.MutableMapping): 179 | def __init__(self, max_size: int | None) -> None: 180 | self._store: typing.MutableMapping[typing.Any, typing.Any] = {} 181 | self._max_size = max_size 182 | self._lock: threading.RLock | DummyLock = threading.RLock() 183 | 184 | def __getstate__(self) -> dict[str, typing.Any]: 185 | return {"_store": self._store, "_max_size": self._max_size} 186 | 187 | def __setstate__(self, state: dict[str, typing.Any]) -> None: 188 | self._lock = threading.RLock() 189 | self._store = state["_store"] 190 | self._max_size = state["_max_size"] 191 | 192 | def __delitem__(self, __key) -> None: 193 | with self._lock: 194 | del self._store[__key] 195 | 196 | def __len__(self) -> int: 197 | with self._lock: 198 | return len(self._store) 199 | 200 | def __iter__(self) -> typing.Iterator: 201 | with self._lock: 202 | return iter(self._store) 203 | 204 | def __setitem__(self, key, value): 205 | with self._lock: 206 | if self._max_size and len(self._store) >= self._max_size: 207 | self._store.popitem() 208 | 209 | self._store[key] = value 210 | 211 | def __getitem__(self, item): 212 | with self._lock: 213 | return self._store[item] 214 | 215 | 216 | class QuicSharedCache(SharableLimitedDict): 217 | def __init__(self, max_size: int | None) -> None: 218 | super().__init__(max_size) 219 | self._exclusion_store: typing.MutableMapping[typing.Any, typing.Any] = {} 220 | 221 | def add_domain(self, host: str, port: int | None = None, alt_port: int | None = None) -> None: 222 | if port is None: 223 | port = 443 224 | if alt_port is None: 225 | alt_port = port 226 | self[(host, port)] = (host, alt_port) 227 | 228 | def exclude_domain(self, host: str, port: int | None = None, alt_port: int | None = None): 229 | if port is None: 230 | port = 443 231 | if alt_port is None: 232 | alt_port = port 233 | self._exclusion_store[(host, port)] = (host, alt_port) 234 | 235 | def __setitem__(self, key, value): 236 | with self._lock: 237 | if key in self._exclusion_store: 238 | return 239 | 240 | if self._max_size and len(self._store) >= self._max_size: 241 | self._store.popitem() 242 | 243 | self._store[key] = value 244 | 245 | 246 | class AsyncQuicSharedCache(QuicSharedCache): 247 | def __init__(self, max_size: int | None) -> None: 248 | super().__init__(max_size) 249 | self._lock = DummyLock() 250 | 251 | def __setstate__(self, state: dict[str, typing.Any]) -> None: 252 | self._lock = DummyLock() 253 | self._store = state["_store"] 254 | self._max_size = state["_max_size"] 255 | 256 | 257 | class DummyLock: 258 | def __enter__(self): 259 | return self 260 | 261 | def __exit__(self, exc_type, exc_val, exc_tb): 262 | pass 263 | 264 | def acquire(self): 265 | pass 266 | 267 | def release(self): 268 | pass 269 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Requests test package initialisation.""" 2 | 3 | from __future__ import annotations 4 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | try: 4 | from http.server import HTTPServer, SimpleHTTPRequestHandler 5 | except ImportError: 6 | from BaseHTTPServer import HTTPServer 7 | from SimpleHTTPServer import SimpleHTTPRequestHandler 8 | 9 | import socket 10 | import ssl 11 | import threading 12 | from urllib.parse import urljoin 13 | 14 | import pytest 15 | 16 | 17 | def prepare_url(value): 18 | # Issue #1483: Make sure the URL always has a trailing slash 19 | httpbin_url = value.url.rstrip("/") + "/" 20 | 21 | def inner(*suffix): 22 | return urljoin(httpbin_url, "/".join(suffix)) 23 | 24 | return inner 25 | 26 | 27 | @pytest.fixture 28 | def httpbin(httpbin): 29 | return prepare_url(httpbin) 30 | 31 | 32 | @pytest.fixture 33 | def httpbin_secure(httpbin_secure): 34 | return prepare_url(httpbin_secure) 35 | 36 | 37 | class LocalhostCookieTestServer(SimpleHTTPRequestHandler): 38 | def do_GET(self): 39 | spot = self.headers.get("Cookie", None) 40 | 41 | self.send_response(204) 42 | self.send_header("Content-Length", "0") 43 | 44 | if spot is None: 45 | self.send_header("Set-Cookie", "hello=world; Domain=localhost; Max-Age=120") 46 | else: 47 | self.send_header("X-Cookie-Pass", "1" if "hello=world" in spot else "0") 48 | 49 | self.end_headers() 50 | 51 | 52 | @pytest.fixture 53 | def san_server(tmp_path_factory): 54 | # delay importing until the fixture in order to make it possible 55 | # to deselect the test via command-line when trustme is not available 56 | import trustme 57 | 58 | tmpdir = tmp_path_factory.mktemp("certs") 59 | ca = trustme.CA() 60 | 61 | server_cert = ca.issue_cert("localhost", common_name="localhost") 62 | ca_bundle = str(tmpdir / "ca.pem") 63 | ca.cert_pem.write_to_path(ca_bundle) 64 | 65 | context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 66 | server_cert.configure_cert(context) 67 | server = HTTPServer(("localhost", 0), LocalhostCookieTestServer) 68 | server.socket = context.wrap_socket(server.socket, server_side=True) 69 | server_thread = threading.Thread(target=server.serve_forever) 70 | server_thread.start() 71 | 72 | yield "localhost", server.server_address[1], ca_bundle 73 | 74 | server.shutdown() 75 | server_thread.join() 76 | 77 | 78 | _WAN_AVAILABLE = None 79 | 80 | 81 | @pytest.fixture(scope="session") 82 | def requires_wan() -> None: 83 | global _WAN_AVAILABLE 84 | 85 | if _WAN_AVAILABLE is not None: 86 | if _WAN_AVAILABLE is False: 87 | pytest.skip("Test requires a WAN access to httpbingo.org") 88 | return 89 | 90 | try: 91 | sock = socket.create_connection(("httpbingo.org", 443), timeout=1) 92 | except (ConnectionRefusedError, socket.gaierror, TimeoutError): 93 | _WAN_AVAILABLE = False 94 | pytest.skip("Test requires a WAN access to httpbingo.org") 95 | else: 96 | _WAN_AVAILABLE = True 97 | sock.close() 98 | -------------------------------------------------------------------------------- /tests/test_help.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from unittest import mock 4 | 5 | from niquests.help import info 6 | 7 | 8 | def test_system_ssl(): 9 | """Verify we're actually setting system_ssl when it should be available.""" 10 | assert info()["system_ssl"]["version"] != "" 11 | 12 | 13 | class VersionedPackage: 14 | def __init__(self, version): 15 | self.__version__ = version 16 | 17 | 18 | def test_idna_without_version_attribute(): 19 | """Older versions of IDNA don't provide a __version__ attribute, verify 20 | that if we have such a package, we don't blow up. 21 | """ 22 | with mock.patch("niquests.help.idna", new=None): 23 | assert info()["idna"] == {"version": "N/A"} 24 | 25 | 26 | def test_idna_with_version_attribute(): 27 | """Verify we're actually setting idna version when it should be available.""" 28 | with mock.patch("niquests.help.idna", new=VersionedPackage("2.6")): 29 | assert info()["idna"] == {"version": "2.6"} 30 | -------------------------------------------------------------------------------- /tests/test_hooks.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests import hooks 6 | 7 | 8 | def hook(value): 9 | return value[1:] 10 | 11 | 12 | async def ahook(value): 13 | return value[1:] 14 | 15 | 16 | @pytest.mark.parametrize( 17 | "hooks_list, result", 18 | ( 19 | (hook, "ata"), 20 | ([hook, lambda x: None, hook], "ta"), 21 | ), 22 | ) 23 | def test_hooks(hooks_list, result): 24 | assert hooks.dispatch_hook("response", {"response": hooks_list}, "Data") == result 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "hooks_list, result", 29 | ( 30 | (hook, "ata"), 31 | ([hook, lambda x: None, hook], "ta"), 32 | ), 33 | ) 34 | def test_hooks_with_kwargs(hooks_list, result): 35 | assert hooks.dispatch_hook("response", {"response": hooks_list}, "Data", should_not_crash=True) == result 36 | 37 | 38 | @pytest.mark.asyncio 39 | @pytest.mark.parametrize( 40 | "hooks_list, result", 41 | ( 42 | (ahook, "ata"), 43 | ([ahook, lambda x: None, hook], "ta"), 44 | ), 45 | ) 46 | async def test_ahooks(hooks_list, result): 47 | assert (await hooks.async_dispatch_hook("response", {"response": hooks_list}, "Data")) == result 48 | 49 | 50 | @pytest.mark.asyncio 51 | @pytest.mark.parametrize( 52 | "hooks_list, result", 53 | ( 54 | (hook, "ata"), 55 | ([hook, lambda x: None, ahook], "ta"), 56 | ), 57 | ) 58 | async def test_ahooks_with_kwargs(hooks_list, result): 59 | assert (await hooks.async_dispatch_hook("response", {"response": hooks_list}, "Data", should_not_crash=True)) == result 60 | 61 | 62 | def test_default_hooks(): 63 | assert hooks.default_hooks() == { 64 | "pre_request": [], 65 | "pre_send": [], 66 | "on_upload": [], 67 | "early_response": [], 68 | "response": [], 69 | } 70 | -------------------------------------------------------------------------------- /tests/test_live.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from unittest.mock import MagicMock, patch 5 | 6 | import pytest 7 | 8 | from niquests import Session 9 | from niquests._compat import HAS_LEGACY_URLLIB3 10 | from niquests.exceptions import ConnectionError 11 | from niquests.utils import is_ipv4_address, is_ipv6_address 12 | 13 | if not HAS_LEGACY_URLLIB3: 14 | from urllib3 import HttpVersion, ResolverDescription 15 | else: 16 | from urllib3_future import HttpVersion, ResolverDescription 17 | 18 | try: 19 | import qh3 20 | except ImportError: 21 | qh3 = None 22 | 23 | 24 | @pytest.mark.usefixtures("requires_wan") 25 | class TestLiveStandardCase: 26 | def test_ensure_ipv4(self) -> None: 27 | with Session(disable_ipv6=True, resolver="doh+google://") as s: 28 | r = s.get("https://httpbingo.org/get") 29 | 30 | assert r.conn_info.destination_address is not None 31 | assert is_ipv4_address(r.conn_info.destination_address[0]) 32 | 33 | def test_ensure_ipv6(self) -> None: 34 | if os.environ.get("CI", None) is not None: 35 | # GitHub hosted runner can't reach external IPv6... 36 | with pytest.raises(ConnectionError, match="No route to host|unreachable"): 37 | with Session(disable_ipv4=True, resolver="doh+google://") as s: 38 | s.get("https://httpbingo.org/get") 39 | return 40 | 41 | with Session(disable_ipv4=True, resolver="doh+google://") as s: 42 | r = s.get("https://httpbingo.org/get") 43 | 44 | assert r.conn_info.destination_address is not None 45 | assert is_ipv6_address(r.conn_info.destination_address[0]) 46 | 47 | def test_ensure_http2(self) -> None: 48 | with Session(disable_http3=True, base_url="https://httpbingo.org") as s: 49 | r = s.get("/get") 50 | assert r.conn_info.http_version is not None 51 | assert r.conn_info.http_version == HttpVersion.h2 52 | assert r.url == "https://httpbingo.org/get" 53 | 54 | @pytest.mark.skipif(qh3 is None, reason="qh3 unavailable") 55 | def test_ensure_http3_default(self) -> None: 56 | with Session(resolver="doh+cloudflare://") as s: 57 | r = s.get("https://1.1.1.1") 58 | assert r.conn_info.http_version is not None 59 | assert r.conn_info.http_version == HttpVersion.h3 60 | 61 | @patch( 62 | "urllib3.contrib.resolver.doh.HTTPSResolver.getaddrinfo" 63 | if not HAS_LEGACY_URLLIB3 64 | else "urllib3_future.contrib.resolver.doh.HTTPSResolver.getaddrinfo" 65 | ) 66 | def test_manual_resolver(self, getaddrinfo_mock: MagicMock) -> None: 67 | with Session(resolver="doh+cloudflare://") as s: 68 | with pytest.raises(ConnectionError): 69 | s.get("https://httpbingo.org/get") 70 | 71 | assert getaddrinfo_mock.call_count 72 | 73 | def test_not_owned_resolver(self) -> None: 74 | resolver = ResolverDescription.from_url("doh+cloudflare://").new() 75 | 76 | with Session(resolver=resolver) as s: 77 | s.get("https://httpbingo.org/get") 78 | 79 | assert resolver.is_available() 80 | 81 | assert resolver.is_available() 82 | 83 | def test_owned_resolver_must_close(self) -> None: 84 | with Session(resolver="doh+cloudflare://") as s: 85 | s.get("https://httpbingo.org/get") 86 | 87 | assert s.resolver.is_available() 88 | 89 | assert not s.resolver.is_available() 90 | 91 | def test_owned_resolver_must_recycle(self) -> None: 92 | s = Session(resolver="doh+cloudflare://") 93 | 94 | s.get("https://httpbingo.org/get") 95 | 96 | s.resolver.close() 97 | 98 | assert not s.resolver.is_available() 99 | 100 | s.get("https://httpbingo.org/get") 101 | 102 | assert s.resolver.is_available() 103 | 104 | @pytest.mark.skipif(os.environ.get("CI") is None, reason="Worth nothing locally") 105 | def test_happy_eyeballs(self) -> None: 106 | """A bit of context, this test, running it locally does not get us 107 | any confidence about Happy Eyeballs. This test is valuable in Github CI where IPv6 addresses are unreachable. 108 | We're using a custom DNS resolver that will yield the IPv6 addresses and IPv4 ones. 109 | If this hang in CI, then you did something wrong...!""" 110 | with Session(resolver="doh+cloudflare://", happy_eyeballs=True) as s: 111 | r = s.get("https://httpbingo.org/get") 112 | 113 | assert r.ok 114 | 115 | def test_early_response(self) -> None: 116 | received_early_response: bool = False 117 | 118 | def callback_on_early(early_resp) -> None: 119 | nonlocal received_early_response 120 | if early_resp.status_code == 103: 121 | received_early_response = True 122 | 123 | with Session() as s: 124 | resp = s.get( 125 | "https://early-hints.fastlylabs.com/", 126 | hooks={"early_response": [callback_on_early]}, 127 | ) 128 | 129 | assert resp.status_code == 200 130 | assert received_early_response is True 131 | 132 | @pytest.mark.skipif(qh3 is None, reason="qh3 unavailable") 133 | def test_preemptive_add_http3_domain(self) -> None: 134 | with Session() as s: 135 | s.quic_cache_layer.add_domain("one.one.one.one") 136 | 137 | resp = s.get("https://one.one.one.one") 138 | 139 | assert resp.http_version == 30 140 | 141 | @pytest.mark.skipif(qh3 is None, reason="qh3 unavailable") 142 | def test_preemptive_add_http3_domain_wrong_port(self) -> None: 143 | with Session() as s: 144 | s.quic_cache_layer.add_domain("one.one.one.one", 6666) 145 | 146 | resp = s.get("https://one.one.one.one") 147 | 148 | assert resp.http_version == 20 149 | 150 | @pytest.mark.skipif(qh3 is None, reason="qh3 unavailable") 151 | def test_preemptive_exclude_http3_domain(self) -> None: 152 | with Session() as s: 153 | s.quic_cache_layer.exclude_domain("one.one.one.one") 154 | 155 | for _ in range(2): 156 | resp = s.get("https://one.one.one.one") 157 | assert resp.http_version == 20 158 | -------------------------------------------------------------------------------- /tests/test_multiplexed.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests import Session 6 | 7 | 8 | @pytest.mark.usefixtures("requires_wan") 9 | class TestMultiplexed: 10 | def test_concurrent_request_in_sync(self): 11 | responses = [] 12 | 13 | with Session(multiplexed=True) as s: 14 | responses.append(s.get("https://httpbingo.org/delay/3")) 15 | responses.append(s.get("https://httpbingo.org/delay/1")) 16 | responses.append(s.get("https://httpbingo.org/delay/1")) 17 | responses.append(s.get("https://httpbingo.org/delay/3")) 18 | 19 | assert all(r.lazy for r in responses) 20 | 21 | s.gather() 22 | 23 | assert all(r.lazy is False for r in responses) 24 | assert all(r.status_code == 200 for r in responses) 25 | 26 | def test_redirect_with_multiplexed(self): 27 | with Session(multiplexed=True) as s: 28 | resp = s.get("https://httpbingo.org/redirect/3") 29 | assert resp.lazy 30 | s.gather() 31 | 32 | assert resp.status_code == 200 33 | assert resp.url == "https://httpbingo.org/get" 34 | assert len(resp.history) == 3 35 | 36 | def test_redirect_with_multiplexed_direct_access(self): 37 | with Session(multiplexed=True) as s: 38 | resp = s.get("https://httpbingo.org/redirect/3") 39 | assert resp.lazy 40 | 41 | assert resp.status_code == 200 42 | assert resp.url == "https://httpbingo.org/get" 43 | assert len(resp.history) == 3 44 | assert resp.json() 45 | 46 | def test_lazy_access_sync_mode(self): 47 | with Session(multiplexed=True) as s: 48 | resp = s.get("https://httpbingo.org/headers") 49 | assert resp.lazy 50 | 51 | assert resp.status_code == 200 52 | 53 | def test_post_data_with_multiplexed(self): 54 | responses = [] 55 | 56 | with Session(multiplexed=True) as s: 57 | for i in range(5): 58 | responses.append( 59 | s.post( 60 | "https://httpbingo.org/post", 61 | data=b"foo" * 128, 62 | ) 63 | ) 64 | 65 | s.gather() 66 | 67 | assert all(r.lazy is False for r in responses) 68 | assert all(r.status_code == 200 for r in responses) 69 | assert all(r.json()["data"] != "" for r in responses) 70 | 71 | def test_get_stream_with_multiplexed(self): 72 | with Session(multiplexed=True) as s: 73 | resp = s.get("https://httpbingo.org/headers", stream=True) 74 | assert resp.lazy 75 | 76 | assert resp.status_code == 200 77 | assert resp._content_consumed is False 78 | 79 | payload = b"" 80 | 81 | for chunk in resp.iter_content(32): 82 | payload += chunk 83 | 84 | assert resp._content_consumed is True 85 | 86 | import json 87 | 88 | assert isinstance(json.loads(payload), dict) 89 | 90 | def test_one_at_a_time(self): 91 | responses = [] 92 | 93 | with Session(multiplexed=True) as s: 94 | for _ in [3, 1, 3, 5]: 95 | responses.append(s.get(f"https://httpbingo.org/delay/{_}")) 96 | 97 | assert all(r.lazy for r in responses) 98 | promise_count = len(responses) 99 | 100 | while any(r.lazy for r in responses): 101 | s.gather(max_fetch=1) 102 | promise_count -= 1 103 | 104 | assert len(list(filter(lambda r: r.lazy, responses))) == promise_count 105 | 106 | assert len(list(filter(lambda r: r.lazy, responses))) == 0 107 | 108 | def test_early_close_no_error(self): 109 | responses = [] 110 | 111 | with Session(multiplexed=True) as s: 112 | for _ in [2, 1, 1]: 113 | responses.append(s.get(f"https://httpbingo.org/delay/{_}")) 114 | 115 | assert all(r.lazy for r in responses) 116 | 117 | # since urllib3.future 2.5, the scheduler ensure we kept track of ongoing request even if pool is 118 | # shutdown. 119 | assert all([r.json() for r in responses]) 120 | 121 | def test_early_response(self) -> None: 122 | received_early_response: bool = False 123 | 124 | def callback_on_early(early_resp) -> None: 125 | nonlocal received_early_response 126 | if early_resp.status_code == 103: 127 | received_early_response = True 128 | 129 | with Session(multiplexed=True) as s: 130 | resp = s.get( 131 | "https://early-hints.fastlylabs.com/", 132 | hooks={"early_response": [callback_on_early]}, 133 | ) 134 | 135 | assert received_early_response is False 136 | 137 | assert resp.status_code == 200 138 | assert received_early_response is True 139 | -------------------------------------------------------------------------------- /tests/test_ocsp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests import AsyncSession, Session 6 | from niquests.exceptions import ConnectionError, Timeout 7 | 8 | try: 9 | import qh3 10 | except ImportError: 11 | qh3 = None 12 | 13 | OCSP_MAX_DELAY_WAIT = 5 14 | 15 | 16 | @pytest.mark.usefixtures("requires_wan") 17 | @pytest.mark.skipif(qh3 is None, reason="qh3 unavailable") 18 | class TestOnlineCertificateRevocationProtocol: 19 | """This test class hold the minimal amount of confidence 20 | we need to ensure revoked certificate are properly rejected. 21 | Unfortunately, we need to fetch external resources through a valid WAN 22 | link. We may assemble a complex mocking scenario later on.""" 23 | 24 | @pytest.mark.parametrize( 25 | "revoked_peer_url", 26 | [ 27 | # "https://revoked.badssl.com/", 28 | # "https://revoked-ecc-dv.ssl.com/", 29 | # "https://aaacertificateservices.comodoca.com:444/", 30 | "https://revoked-rsa-ev.ssl.com/", 31 | ], 32 | ) 33 | def test_sync_revoked_certificate(self, revoked_peer_url: str) -> None: 34 | """This test may fail at any moment. Using several known revoked certs as targets tester.""" 35 | 36 | with Session() as s: 37 | assert s._ocsp_cache is None 38 | with pytest.raises( 39 | ConnectionError, 40 | match=f"Unable to establish a secure connection to {revoked_peer_url} because the certificate has been revoked", 41 | ): 42 | try: 43 | s.get(revoked_peer_url, timeout=OCSP_MAX_DELAY_WAIT) 44 | except Timeout: 45 | pytest.mark.skip(f"remote {revoked_peer_url} is unavailable at the moment...") 46 | assert s._ocsp_cache is not None 47 | assert hasattr(s._ocsp_cache, "_store") 48 | assert isinstance(s._ocsp_cache._store, dict) 49 | assert len(s._ocsp_cache._store) == 1 50 | 51 | def test_sync_valid_ensure_cached(self) -> None: 52 | with Session() as s: 53 | assert s._ocsp_cache is None 54 | s.get("https://raw.githubusercontent.com/jawah/niquests/refs/heads/main/README.md", timeout=OCSP_MAX_DELAY_WAIT) 55 | assert s._ocsp_cache is not None 56 | assert hasattr(s._ocsp_cache, "_store") 57 | assert isinstance(s._ocsp_cache._store, dict) 58 | assert len(s._ocsp_cache._store) == 1 59 | s.get("https://pypi.org/pypi/niquests/json", timeout=OCSP_MAX_DELAY_WAIT) 60 | assert len(s._ocsp_cache._store) == 2 61 | s.get("https://one.one.one.one", timeout=OCSP_MAX_DELAY_WAIT) 62 | assert len(s._ocsp_cache._store) == 3 63 | 64 | @pytest.mark.asyncio 65 | @pytest.mark.parametrize( 66 | "revoked_peer_url", 67 | [ 68 | # "https://revoked.badssl.com/", 69 | # "https://revoked-ecc-dv.ssl.com/", 70 | # "https://aaacertificateservices.comodoca.com:444/", 71 | "https://revoked-rsa-ev.ssl.com/", 72 | ], 73 | ) 74 | async def test_async_revoked_certificate(self, revoked_peer_url: str) -> None: 75 | async with AsyncSession() as s: 76 | assert s._ocsp_cache is None 77 | with pytest.raises( 78 | ConnectionError, 79 | match=f"Unable to establish a secure connection to {revoked_peer_url} because the certificate has been revoked", 80 | ): 81 | try: 82 | await s.get(revoked_peer_url, timeout=OCSP_MAX_DELAY_WAIT) 83 | except Timeout: 84 | pytest.mark.skip(f"remote {revoked_peer_url} is unavailable at the moment...") 85 | assert s._ocsp_cache is not None 86 | assert hasattr(s._ocsp_cache, "_store") 87 | assert isinstance(s._ocsp_cache._store, dict) 88 | assert len(s._ocsp_cache._store) == 1 89 | 90 | @pytest.mark.asyncio 91 | async def test_async_valid_ensure_cached(self) -> None: 92 | async with AsyncSession() as s: 93 | assert s._ocsp_cache is None 94 | await s.get( 95 | "https://raw.githubusercontent.com/jawah/niquests/refs/heads/main/README.md", timeout=OCSP_MAX_DELAY_WAIT 96 | ) 97 | assert s._ocsp_cache is not None 98 | assert hasattr(s._ocsp_cache, "_store") 99 | assert isinstance(s._ocsp_cache._store, dict) 100 | assert len(s._ocsp_cache._store) == 1 101 | await s.get("https://pypi.org/pypi/niquests/json", timeout=OCSP_MAX_DELAY_WAIT) 102 | assert len(s._ocsp_cache._store) == 2 103 | await s.get("https://one.one.one.one/", timeout=OCSP_MAX_DELAY_WAIT) 104 | assert len(s._ocsp_cache._store) == 3 105 | -------------------------------------------------------------------------------- /tests/test_sse.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests import AsyncSession, Session 6 | 7 | 8 | @pytest.mark.usefixtures("requires_wan") 9 | class TestLiveSSE: 10 | def test_sync_sse_basic_example(self) -> None: 11 | with Session() as s: 12 | resp = s.get("sse://httpbingo.org/sse") 13 | 14 | assert resp.status_code == 200 15 | assert resp.extension is not None 16 | assert resp.extension.closed is False 17 | 18 | events = [] 19 | 20 | while resp.extension.closed is False: 21 | events.append(resp.extension.next_payload()) 22 | 23 | assert resp.extension.closed is True 24 | assert len(events) > 0 25 | assert events[-1] is None 26 | 27 | @pytest.mark.asyncio 28 | async def test_async_sse_basic_example(self) -> None: 29 | async with AsyncSession() as s: 30 | resp = await s.get("sse://httpbingo.org/sse") 31 | 32 | assert resp.status_code == 200 33 | assert resp.extension is not None 34 | assert resp.extension.closed is False 35 | 36 | events = [] 37 | 38 | while resp.extension.closed is False: 39 | events.append(await resp.extension.next_payload()) 40 | 41 | assert resp.extension.closed is True 42 | assert len(events) > 0 43 | assert events[-1] is None 44 | -------------------------------------------------------------------------------- /tests/test_structures.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests._compat import HAS_LEGACY_URLLIB3 6 | from niquests.structures import CaseInsensitiveDict, LookupDict 7 | 8 | if not HAS_LEGACY_URLLIB3: 9 | from urllib3 import HTTPHeaderDict 10 | else: 11 | from urllib3_future import HTTPHeaderDict 12 | 13 | 14 | class TestCaseInsensitiveDict: 15 | @pytest.fixture(autouse=True) 16 | def setup(self): 17 | """CaseInsensitiveDict instance with "Accept" header.""" 18 | self.case_insensitive_dict = CaseInsensitiveDict() 19 | self.case_insensitive_dict["Accept"] = "application/json" 20 | 21 | def test_list(self): 22 | assert list(self.case_insensitive_dict) == ["Accept"] 23 | 24 | possible_keys = pytest.mark.parametrize("key", ("accept", "ACCEPT", "aCcEpT", "Accept")) 25 | 26 | @possible_keys 27 | def test_getitem(self, key): 28 | assert self.case_insensitive_dict[key] == "application/json" 29 | 30 | @possible_keys 31 | def test_delitem(self, key): 32 | del self.case_insensitive_dict[key] 33 | assert key not in self.case_insensitive_dict 34 | 35 | def test_lower_items(self): 36 | assert list(self.case_insensitive_dict.lower_items()) == [("accept", "application/json")] 37 | 38 | def test_repr(self): 39 | assert repr(self.case_insensitive_dict) == "{'Accept': 'application/json'}" 40 | 41 | def test_copy(self): 42 | copy = self.case_insensitive_dict.copy() 43 | assert copy is not self.case_insensitive_dict 44 | assert copy == self.case_insensitive_dict 45 | 46 | @pytest.mark.parametrize( 47 | "other, result", 48 | ( 49 | ({"AccePT": "application/json"}, True), 50 | ({}, False), 51 | (None, False), 52 | ), 53 | ) 54 | def test_instance_equality(self, other, result): 55 | assert (self.case_insensitive_dict == other) is result 56 | 57 | def test_lossless_convert_into_mono_entry(self): 58 | o = HTTPHeaderDict() 59 | o.add("Hello", "1") 60 | o.add("Hello", "2") 61 | o.add("Hello", "3") 62 | 63 | u = CaseInsensitiveDict(o) 64 | 65 | assert u["Hello"] == "1, 2, 3" 66 | assert "1, 2, 3" in repr(u) 67 | 68 | 69 | class TestLookupDict: 70 | @pytest.fixture(autouse=True) 71 | def setup(self): 72 | """LookupDict instance with "bad_gateway" attribute.""" 73 | self.lookup_dict = LookupDict("test") 74 | self.lookup_dict.bad_gateway = 502 75 | 76 | def test_repr(self): 77 | assert repr(self.lookup_dict) == "" 78 | 79 | get_item_parameters = pytest.mark.parametrize( 80 | "key, value", 81 | ( 82 | ("bad_gateway", 502), 83 | ("not_a_key", None), 84 | ), 85 | ) 86 | 87 | @get_item_parameters 88 | def test_getitem(self, key, value): 89 | assert self.lookup_dict[key] == value 90 | 91 | @get_item_parameters 92 | def test_get(self, key, value): 93 | assert self.lookup_dict.get(key) == value 94 | -------------------------------------------------------------------------------- /tests/test_testserver.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import socket 4 | import threading 5 | import time 6 | 7 | import pytest 8 | 9 | import niquests 10 | from tests.testserver.server import Server 11 | 12 | 13 | class TestTestServer: 14 | def test_basic(self): 15 | """messages are sent and received properly""" 16 | question = b"success?" 17 | answer = b"yeah, success" 18 | 19 | def handler(sock): 20 | text = sock.recv(1000) 21 | assert text == question 22 | sock.sendall(answer) 23 | 24 | with Server(handler) as (host, port): 25 | sock = socket.socket() 26 | sock.connect((host, port)) 27 | sock.sendall(question) 28 | text = sock.recv(1000) 29 | assert text == answer 30 | sock.close() 31 | 32 | def test_server_closes(self): 33 | """the server closes when leaving the context manager""" 34 | with Server.basic_response_server() as (host, port): 35 | sock = socket.socket() 36 | sock.connect((host, port)) 37 | 38 | sock.close() 39 | 40 | with pytest.raises(socket.error): 41 | new_sock = socket.socket() 42 | new_sock.connect((host, port)) 43 | 44 | def test_text_response(self): 45 | """the text_response_server sends the given text""" 46 | server = Server.text_response_server("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nroflol") 47 | 48 | with server as (host, port): 49 | r = niquests.get(f"http://{host}:{port}") 50 | 51 | assert r.status_code == 200 52 | assert r.text == "roflol" 53 | assert r.headers["Content-Length"] == "6" 54 | 55 | def test_early_response_caught(self): 56 | server = Server.text_response_server("HTTP/1.1 100 CONTINUE\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nroflol") 57 | 58 | with server as (host, port): 59 | early_r = None 60 | 61 | def catch_early_response(*args): 62 | nonlocal early_r 63 | early_r = args[0] 64 | 65 | r = niquests.get( 66 | f"http://{host}:{port}", 67 | hooks={"early_response": [catch_early_response]}, 68 | ) 69 | 70 | assert early_r is not None 71 | assert early_r.status_code == 100 72 | 73 | assert r.status_code == 200 74 | assert r.text == "roflol" 75 | assert r.headers["Content-Length"] == "6" 76 | 77 | def test_trailer_header_caught(self): 78 | server = Server.text_response_server( 79 | "HTTP/1.1 200 OK\r\n" 80 | "Transfer-Encoding: chunked\r\n" 81 | "Trailer: Server-Timing\r\n\r\n" 82 | "6\r\nroflol\r\n" 83 | "0\r\n" 84 | "Server-Timing: 1.225\r\n\r\n" 85 | ) 86 | 87 | with server as (host, port): 88 | r = niquests.get( 89 | f"http://{host}:{port}", 90 | ) 91 | 92 | assert r.status_code == 200 93 | assert r.text == "roflol" 94 | assert r.trailers["Server-Timing"] == "1.225" 95 | 96 | def test_invalid_location_response(self): 97 | server = Server.text_response_server( 98 | "HTTP/1.1 302 PERMANENT-REDIRECTION\r\nLocation: http://localhost:1/search/?q=ïðåçèäåíòû+ÑØÀ\r\n\r\n" 99 | ) 100 | 101 | with server as (host, port): 102 | with pytest.raises(niquests.exceptions.ConnectionError) as exc: 103 | niquests.get(f"http://{host}:{port}") 104 | msg = exc.value.args[0].args[0] 105 | assert "/search/?q=%C3%AF%C3%B0%C3%A5%C3%A7%C3%A8%C3%A4%C3%A5%C3%AD%C3%B2%C3%BB+%C3%91%C3%98%C3%80" in msg 106 | 107 | def test_basic_response(self): 108 | """the basic response server returns an empty http response""" 109 | with Server.basic_response_server() as (host, port): 110 | r = niquests.get(f"http://{host}:{port}") 111 | assert r.status_code == 200 112 | assert r.text == "" 113 | assert r.headers["Content-Length"] == "0" 114 | 115 | def test_basic_waiting_server(self): 116 | """the server waits for the block_server event to be set before closing""" 117 | block_server = threading.Event() 118 | 119 | with Server.basic_response_server(wait_to_close_event=block_server) as ( 120 | host, 121 | port, 122 | ): 123 | sock = socket.socket() 124 | sock.connect((host, port)) 125 | sock.sendall(b"send something") 126 | time.sleep(2.5) 127 | sock.sendall(b"still alive") 128 | block_server.set() # release server block 129 | 130 | def test_multiple_requests(self): 131 | """multiple requests can be served""" 132 | requests_to_handle = 5 133 | 134 | server = Server.basic_response_server(requests_to_handle=requests_to_handle) 135 | 136 | with server as (host, port): 137 | server_url = f"http://{host}:{port}" 138 | for _ in range(requests_to_handle): 139 | r = niquests.get(server_url) 140 | assert r.status_code == 200 141 | 142 | # the (n+1)th request fails 143 | with pytest.raises(niquests.exceptions.ConnectionError): 144 | r = niquests.get(server_url) 145 | 146 | @pytest.mark.skip(reason="this fails non-deterministically under pytest-xdist") 147 | def test_request_recovery(self): 148 | """can check the requests content""" 149 | # TODO: figure out why this sometimes fails when using pytest-xdist. 150 | server = Server.basic_response_server(requests_to_handle=2) 151 | first_request = b"put your hands up in the air" 152 | second_request = b"put your hand down in the floor" 153 | 154 | with server as address: 155 | sock1 = socket.socket() 156 | sock2 = socket.socket() 157 | 158 | sock1.connect(address) 159 | sock1.sendall(first_request) 160 | sock1.close() 161 | 162 | sock2.connect(address) 163 | sock2.sendall(second_request) 164 | sock2.close() 165 | 166 | assert server.handler_results[0] == first_request 167 | assert server.handler_results[1] == second_request 168 | 169 | def test_requests_after_timeout_are_not_received(self): 170 | """the basic response handler times out when receiving requests""" 171 | server = Server.basic_response_server(request_timeout=1) 172 | 173 | with server as address: 174 | sock = socket.socket() 175 | sock.connect(address) 176 | time.sleep(1.5) 177 | sock.sendall(b"hehehe, not received") 178 | sock.close() 179 | 180 | assert server.handler_results[0] == b"" 181 | 182 | def test_request_recovery_with_bigger_timeout(self): 183 | """a biggest timeout can be specified""" 184 | server = Server.basic_response_server(request_timeout=3) 185 | data = b"bananadine" 186 | 187 | with server as address: 188 | sock = socket.socket() 189 | sock.connect(address) 190 | time.sleep(1.5) 191 | sock.sendall(data) 192 | sock.close() 193 | 194 | assert server.handler_results[0] == data 195 | 196 | def test_server_finishes_on_error(self): 197 | """the server thread exits even if an exception exits the context manager""" 198 | server = Server.basic_response_server() 199 | with pytest.raises(Exception): 200 | with server: 201 | raise Exception() 202 | 203 | assert len(server.handler_results) == 0 204 | 205 | # if the server thread fails to finish, the test suite will hang 206 | # and get killed by the jenkins timeout. 207 | 208 | def test_server_finishes_when_no_connections(self): 209 | """the server thread exits even if there are no connections""" 210 | server = Server.basic_response_server() 211 | with server: 212 | pass 213 | 214 | assert len(server.handler_results) == 0 215 | 216 | # if the server thread fails to finish, the test suite will hang 217 | # and get killed by the jenkins timeout. 218 | -------------------------------------------------------------------------------- /tests/test_websocket.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from niquests import AsyncSession, ReadTimeout, Session 6 | 7 | try: 8 | import wsproto 9 | except ImportError: 10 | wsproto = None 11 | 12 | 13 | @pytest.mark.usefixtures("requires_wan") 14 | @pytest.mark.skipif(wsproto is None, reason="wsproto unavailable") 15 | class TestLiveWebSocket: 16 | def test_sync_websocket_basic_example(self) -> None: 17 | with Session() as s: 18 | resp = s.get("wss://httpbingo.org/websocket/echo") 19 | 20 | assert resp.status_code == 101 21 | assert resp.extension is not None 22 | assert resp.extension.closed is False 23 | 24 | # greeting_msg = resp.extension.next_payload() 25 | # 26 | # assert greeting_msg is not None 27 | # assert isinstance(greeting_msg, str) 28 | 29 | resp.extension.send_payload("Hello World") 30 | resp.extension.send_payload(b"Foo Bar Baz!") 31 | 32 | assert resp.extension.next_payload() == "Hello World" 33 | assert resp.extension.next_payload() == b"Foo Bar Baz!" 34 | 35 | resp.extension.close() 36 | assert resp.extension.closed is True 37 | 38 | @pytest.mark.asyncio 39 | async def test_async_websocket_basic_example(self) -> None: 40 | async with AsyncSession() as s: 41 | resp = await s.get("wss://httpbingo.org/websocket/echo") 42 | 43 | assert resp.status_code == 101 44 | assert resp.extension is not None 45 | assert resp.extension.closed is False 46 | 47 | # greeting_msg = await resp.extension.next_payload() 48 | # 49 | # assert greeting_msg is not None 50 | # assert isinstance(greeting_msg, str) 51 | 52 | await resp.extension.send_payload("Hello World") 53 | await resp.extension.send_payload(b"Foo Bar Baz!") 54 | 55 | assert (await resp.extension.next_payload()) == "Hello World" 56 | assert (await resp.extension.next_payload()) == b"Foo Bar Baz!" 57 | 58 | await resp.extension.close() 59 | assert resp.extension.closed is True 60 | 61 | def test_sync_websocket_read_timeout(self) -> None: 62 | with Session() as s: 63 | resp = s.get("wss://httpbingo.org/websocket/echo", timeout=3) 64 | 65 | assert resp.status_code == 101 66 | assert resp.extension is not None 67 | assert resp.extension.closed is False 68 | 69 | # greeting_msg = resp.extension.next_payload() 70 | # 71 | # assert greeting_msg is not None 72 | # assert isinstance(greeting_msg, str) 73 | 74 | with pytest.raises(ReadTimeout): 75 | resp.extension.next_payload() 76 | 77 | resp.extension.close() 78 | assert resp.extension.closed is True 79 | 80 | @pytest.mark.asyncio 81 | async def test_async_websocket_read_timeout(self) -> None: 82 | async with AsyncSession() as s: 83 | resp = await s.get("wss://httpbingo.org/websocket/echo", timeout=3) 84 | 85 | assert resp.status_code == 101 86 | assert resp.extension is not None 87 | assert resp.extension.closed is False 88 | # 89 | # greeting_msg = await resp.extension.next_payload() 90 | # 91 | # assert greeting_msg is not None 92 | # assert isinstance(greeting_msg, str) 93 | 94 | with pytest.raises(ReadTimeout): 95 | await resp.extension.next_payload() 96 | 97 | await resp.extension.close() 98 | assert resp.extension.closed is True 99 | -------------------------------------------------------------------------------- /tests/testserver/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawah/niquests/ada4751ef81b576f5d974126a7d21b4a0b77db27/tests/testserver/__init__.py -------------------------------------------------------------------------------- /tests/testserver/server.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import select 4 | import socket 5 | import threading 6 | 7 | 8 | def consume_socket_content(sock, timeout=0.5): 9 | chunks = 65536 10 | content = b"" 11 | 12 | while True: 13 | more_to_read = select.select([sock], [], [], timeout)[0] 14 | if not more_to_read: 15 | break 16 | 17 | new_content = sock.recv(chunks) 18 | if not new_content: 19 | break 20 | 21 | content += new_content 22 | 23 | return content 24 | 25 | 26 | class Server(threading.Thread): 27 | """Dummy server using for unit testing""" 28 | 29 | WAIT_EVENT_TIMEOUT = 5 30 | 31 | def __init__( 32 | self, 33 | handler=None, 34 | host="localhost", 35 | port=0, 36 | requests_to_handle=1, 37 | wait_to_close_event=None, 38 | ): 39 | super().__init__() 40 | 41 | self.handler = handler or consume_socket_content 42 | self.handler_results = [] 43 | 44 | self.host = host 45 | self.port = port 46 | self.requests_to_handle = requests_to_handle 47 | 48 | self.wait_to_close_event = wait_to_close_event 49 | self.ready_event = threading.Event() 50 | self.stop_event = threading.Event() 51 | 52 | @classmethod 53 | def text_response_server(cls, text, request_timeout=0.5, **kwargs): 54 | def text_response_handler(sock): 55 | request_content = consume_socket_content(sock, timeout=request_timeout) 56 | sock.send(text.encode("utf-8")) 57 | 58 | return request_content 59 | 60 | return Server(text_response_handler, **kwargs) 61 | 62 | @classmethod 63 | def basic_response_server(cls, **kwargs): 64 | return cls.text_response_server("HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n\r\n", **kwargs) 65 | 66 | def run(self): 67 | try: 68 | self.server_sock = self._create_socket_and_bind() 69 | # in case self.port = 0 70 | self.port = self.server_sock.getsockname()[1] 71 | self.ready_event.set() 72 | self._handle_requests() 73 | 74 | if self.wait_to_close_event: 75 | self.wait_to_close_event.wait(self.WAIT_EVENT_TIMEOUT) 76 | finally: 77 | self.ready_event.set() # just in case of exception 78 | self._close_server_sock_ignore_errors() 79 | self.stop_event.set() 80 | 81 | def _create_socket_and_bind(self): 82 | sock = socket.socket() 83 | sock.bind((self.host, self.port)) 84 | sock.listen() 85 | return sock 86 | 87 | def _close_server_sock_ignore_errors(self): 88 | try: 89 | self.server_sock.close() 90 | except OSError: 91 | pass 92 | 93 | def _handle_requests(self): 94 | for _ in range(self.requests_to_handle): 95 | sock = self._accept_connection() 96 | if not sock: 97 | break 98 | 99 | handler_result = self.handler(sock) 100 | 101 | self.handler_results.append(handler_result) 102 | sock.close() 103 | 104 | def _accept_connection(self): 105 | try: 106 | ready, _, _ = select.select([self.server_sock], [], [], self.WAIT_EVENT_TIMEOUT) 107 | if not ready: 108 | return None 109 | 110 | return self.server_sock.accept()[0] 111 | except (OSError, ValueError): 112 | return None 113 | 114 | def __enter__(self): 115 | self.start() 116 | if not self.ready_event.wait(self.WAIT_EVENT_TIMEOUT): 117 | raise RuntimeError("Timeout waiting for server to be ready.") 118 | return self.host, self.port 119 | 120 | def __exit__(self, exc_type, exc_value, traceback): 121 | if exc_type is None: 122 | self.stop_event.wait(self.WAIT_EVENT_TIMEOUT) 123 | else: 124 | if self.wait_to_close_event: 125 | # avoid server from waiting for event timeouts 126 | # if an exception is found in the main thread 127 | self.wait_to_close_event.set() 128 | 129 | # ensure server thread doesn't get stuck waiting for connections 130 | self._close_server_sock_ignore_errors() 131 | self.join() 132 | return False # allow exceptions to propagate 133 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import os 5 | 6 | from niquests.utils import getproxies, getproxies_environment 7 | 8 | 9 | @contextlib.contextmanager 10 | def override_environ(**kwargs): 11 | save_env = dict(os.environ) 12 | for key, value in kwargs.items(): 13 | if value is None: 14 | del os.environ[key] 15 | else: 16 | os.environ[key] = value 17 | getproxies.cache_clear() 18 | getproxies_environment.cache_clear() 19 | try: 20 | yield 21 | finally: 22 | os.environ.clear() 23 | os.environ.update(save_env) 24 | getproxies.cache_clear() 25 | getproxies_environment.cache_clear() 26 | --------------------------------------------------------------------------------